NorthlightSDK
Concepts

Runs

A run is an agent working on the person's runtime, with their AI subscription, under the grant's scopes. Status, outcome, result, activity, and how to bound one.

A run is an agent that works on a person's Northlight runtime, on your behalf and with their AI subscription, under the scopes of their grant. Your server starts it with runs.start, and the run keeps going on the runtime whether or not your server is waiting.

const runtime = await oauth.client(userId); // a client on this person's runtime, from their stored connection
const started = await runtime.runs.start({prompt: 'Reply with exactly the word: pong', scopes: ['crtr:llm', 'crtr:act']});
const run = await runtime.runs.wait(started.run_id); // status is 'idle' once the reply is done

runs.start returns at once with a run_id. runs.wait(runId) long-polls until the run is no longer running.

Status

RunObject.status is one of:

statusMeaning
runningThe run's root agent is working.
waiting_on_userThe agent asked the person a question (pending_question_ids lists them).
idleThe agent finished a turn and is waiting. A reply to your prompt leaves the run idle. Send runs.message for another turn, or runs.cancel to end it.
settledThe run has ended. outcome says how.

runs.wait returns on waiting_on_user, idle and settled, so check the status before you read a result.

Outcome and result

A settled run has an outcome:

outcome.kind / reasonMeaning
resultThe run finished. If you passed an output_schema, result holds the validated value.
declinedThe agent could not honestly satisfy the schema. details has reason, code and retryable.
canceledYou or the person ended it.
deadline_exceededA deadline ended it.
runtime_errorThe runtime failed. details.crouter_reason says why.

last_report is the agent's last report text. To get data back, give the run an output_schema and read it with runs.parse: see Typed results. Do not parse the assistant's text out of runs.trace. The trace is what the model said, not a value anything checked.

Scopes

runs.start({prompt, scopes}) starts the run with only those scopes. crtr:llm goes in every run. See Scopes and grants.

Showing progress

While a run is running, runs.get(runId).activity says what the root agent is doing now. It is null in any other status.

phaseThe root agent is
startinglaunching; its first turn has not begun
thinkingreasoning, or waiting on the model before any output
toolcalling a tool: tool.name, and tool.summary (the command's first line or the path it acts on)
submitting_resultwriting or recording its output_schema result
writingstreaming its answer text
between_turnsbetween turns while the run still works: a child agent is running, a message is about to start the next turn, or the runtime is reminding it to submit its result
const run = await runtime.runs.get(runId);
const badge = run.activity?.phase === 'tool' ? `Running ${run.activity.tool.name}` : run.activity?.phase ?? run.status;

activity covers the root agent only and is read each time you call runs.get, so a phase shorter than your poll interval can be missed. To show text as it streams, follow runs.events or runs.reply instead of polling. Do not read runs.trace for progress.

Bounding a run

runs.wait and runs.parse take a signal. When it aborts, the helper rejects with the signal's reason and leaves the run alone. Call runs.cancel if you want the run ended:

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error('took too long')), 10 * 60_000);
let runId: string | undefined;
try {
  const started = await runtime.runs.start({prompt, scopes: ['crtr:llm']});
  runId = started.run_id;
  const run = await runtime.runs.wait(runId, {signal: controller.signal});
  // read run.status, run.outcome, run.result
} catch (error) {
  if (controller.signal.aborted && runId) await runtime.runs.cancel(runId);
  throw error;
} finally {
  clearTimeout(timer);
}

runs.parse rejects with APIError code run_idle when the agent stopped without submitting a result. The runtime reminds the agent of the schema up to three times, then leaves the run idle with the schema still armed. Send runs.message to nudge it, or runs.cancel to end it. A question from the run rejects with waiting_on_user unless you pass onQuestion.

Learn when a background run ends

A run you start and do not wait on ends while nobody holds a request. oauth.watchRuns(...) follows those runs for you and calls onEnded for each one that comes to rest. See Background runs.

Checked and not checked

From the SDK source and its tests. The runs.start, runs.wait and activity behavior against a real runtime on production is not yet checked.

On this page