Research · 01

Research agents.

olbrain.research drives a research agent from JavaScript — open a session, stream a turn, approve its plan, and read the report. It lives on the same Olbrain client as workflow runs and needs @olbrain/js-sdk 1.3.0 or later. Python has the same calls on Olbrain.research from olbrain-python-sdk 0.5.0, without the live reads — see the Python SDK.

Sign-ins only

Research calls authenticate as a signed-in Olbrain user through getIdToken. An Access Key gets NotFoundError (agent_not_found). The agent must be a research agent bound to a screen in the user's organization.

Create a client

ClientTypeScript
import { Olbrain } from '@olbrain/js-sdk';

const olbrain = new Olbrain({ getIdToken: () => auth.currentUser!.getIdToken() });
const r = olbrain.research;

getIdToken runs before every request. Every method takes the research agent's id first, and every path sits under /api/agents/{agent_id}/research. Payloads pass through untyped, because their shape belongs to the research runtime.

Stream a turn

sessions.streamMessageTypeScript
const { session_id: sid } = await r.sessions.create(agentId);

const ac = new AbortController();
for await (const ev of r.sessions.streamMessage(agentId, sid,
  { content: 'Size the Indian EV charger market' }, { signal: ac.signal })) {
  if (ev.type === 'text_delta') process.stdout.write(ev.text);
  if (ev.type === 'error') throw new Error(ev.message ?? 'research error');
}

Frames arrive unchanged from the research runtime: text_delta, tool_use_start, complete, and error. A stream can wait up to 600 seconds for its first byte while the agent searches. Abort the signal to cancel it.

The input is { content, attachments?, effort?, escalates_run_id?, command?, target_run_id? }. To attach files, upload them with attachments.add first and pass the refs it returns.

Sessions

MethodReturns
sessions.create(agentId)The session, with session_id.
sessions.list(agentId, { includeArchived? })Sessions.
sessions.get(agentId, sessionId)The session.
sessions.patch(agentId, sessionId, patch)The session. patch passes through to the runtime.
sessions.delete(agentId, sessionId)Nothing.
sessions.models(agentId, sessionId)The models this session can use.
sessions.setModel(agentId, sessionId, model)Sets the model. null resets to the default.
sessions.setReviewModel(agentId, sessionId, model)Same, for the review model.
sessions.streamMessage(agentId, sessionId, input, { signal? })An AsyncGenerator of frames.
messages.older(agentId, sessionId, beforeTs)One page of older messages, oldest first.

Plans, attachments, and runs

MethodDescription
plans.approve(agentId, sessionId, messageId, body?)Approves the plan proposed in a message.
plans.approveById(agentId, sessionId, planId, body?)Approves a plan by its id.
attachments.add(agentId, sessionId, files)Uploads Blob[] as multipart. Returns attachment refs.
attachments.uploadUrl(agentId, sessionId, storagePath)Returns { url }.
runs.cancel(agentId, runId)Cancels a research run.
runs.retry(agentId, runId)Retries a research run.
runs.activeJob(agentId, runId)The run's active job, or null.
runs.resolveClaims(agentId, runId, resolutions)Sends claim resolutions for the run.
templates.versions(agentId)The agent's template versions.

Reports and review

MethodDescription
reports.get(agentId, runId, { version? })The latest report, or the given version.
reports.artifactUrl(agentId, runId, profileId, format)The URL of a report artifact in the given format.
reports.evidence(agentId, runId)The report's evidence.
reports.share.get / revoke(agentId, runId)Reads or revokes the share link.
reports.share.set(agentId, runId, visibility)Sets who can see the shared report.
review.get(agentId, runId)The latest review.
review.run(agentId, runId)Runs a review. Blocks for up to 600 seconds.
review.apply(agentId, runId, findingIds)Applies review findings. Blocks for up to 600 seconds.
review.stream(agentId, runId, { signal? })Streamed review.run.
review.streamApply(agentId, runId, findingIds, { signal? })Streamed review.apply.

Live reads

Live reads keep a value up to date and call you when it changes. Each returns an unsubscribe function — call it when you're done.

live.runTypeScript
const stop = r.live.run(agentId, runId, (run) => { if (run) render(run); });
// later
stop();
MethodCallback getsOn a read error
live.session(agentId, sessionId, cb, { active? })The session, or nullcb(null) once
live.messages(agentId, sessionId, cb, { active? })MessagesKeeps the last value
live.plan(agentId, planId, cb, { active? })The plan, or nullcb(null) once
live.run(agentId, runId, cb, { active? })The run, or nullcb(null) once
live.reportVersions(agentId, runId, cb, { active? })Report versionscb([]) once
live.templateRuns(agentId, cb, { active? })The agent's runsKeeps the last value
live.runSteps(agentId, runId, cb)Every step so far, ordered by timecb([]) if nothing has arrived

Every live read except runSteps polls every 2 seconds while active and every 10 seconds after stop.setActive(false) (or { active: false }). Polls pause while the browser tab is hidden, and the callback fires only when the server's etag changes. stop.refresh() polls at once. When a streamMessage turn emits complete, the SDK refreshes that session's live.messages for you.

runSteps follows a server-sent event stream instead. It resumes from its cursor after a reconnect, retries with backoff from 1 to 30 seconds, and stops by itself when the run ends. Its unsubscribe function has no setActive or refresh.

Errors

Research calls throw the same classes as the rest of the Olbrain client. Errors from the research runtime arrive as { detail }; the SDK reads the code and message from it and keeps the raw value on ApiError.detail. Aborting your signal throws NetworkError with the message Request aborted.