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.
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
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
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
| Method | Returns |
|---|---|
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
| Method | Description |
|---|---|
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
| Method | Description |
|---|---|
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.
const stop = r.live.run(agentId, runId, (run) => { if (run) render(run); });
// later
stop();
| Method | Callback gets | On a read error |
|---|---|---|
live.session(agentId, sessionId, cb, { active? }) | The session, or null | cb(null) once |
live.messages(agentId, sessionId, cb, { active? }) | Messages | Keeps the last value |
live.plan(agentId, planId, cb, { active? }) | The plan, or null | cb(null) once |
live.run(agentId, runId, cb, { active? }) | The run, or null | cb(null) once |
live.reportVersions(agentId, runId, cb, { active? }) | Report versions | cb([]) once |
live.templateRuns(agentId, cb, { active? }) | The agent's runs | Keeps the last value |
live.runSteps(agentId, runId, cb) | Every step so far, ordered by time | cb([]) 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.