Agents, threads, runs
Three nouns. They show up in every URL and every SDK call.
Agent: configuration
An agent is a saved configuration: durable context, a band, a set of capabilities, and a list of attached adapters. Agents don't run anything by themselves. They're a description of how a run should behave, including whether a run may delegate work to subagents.
const agent = await novo.agents.create({
context: 'You are a refund triage assistant.',
band: 'core',
capabilities: { web: true, userInteraction: true },
});
Agents are mutable via agents.update(...) and have a stable id you persist on your side. The shared system framing lives in context blocks; per-conversation framing lives on the thread.
Thread: conversation
A thread is one conversation with one agent. The thread holds the message history; each turn in the conversation is a separate run on that thread.
const thread = await novo.threads.create({ agentId: agent.id });
Threads are cheap. Create a new one per user, per ticket, per task: whatever maps to "this is one conversation."
A thread can also carry a default timeZone for date/time reasoning. Omit it to use UTC, set an IANA timezone such as America/New_York when the conversation is tied to a user or business locale, or override it on an individual run.
Run: execution
A run is one turn of the loop on one thread. It starts with input, lets the agent plan and delegate when useful, executes tools, may pause for approval or user input, and terminates. You stream events from it while it runs and read totals from it after.
const stream = await novo.threads.stream(thread.id, {
input: 'Refund order #ABC?',
});
const result = await stream.streamToCompletion();
process.stdout.write(result.text);
const { status, usage, terminalReason } = result;
Novo provides the agent with the current date and time at run start, using the effective timezone for that run. You do not need to repeat today's date in input unless the date itself is part of the user's request.
Which entry point
| Goal | Use |
|---|---|
| Stream a reply in the same request | threads.stream(threadId, input) |
| Start a run, persist the id, attach later | runs.start(...) → save runId → runs.stream(runId) |
| Resume after a dropped connection | stream.reconnect({ lastEventId }) |
| List recent runs | runs.list({ agentId?, threadId?, status?, limit? }) |
| Read or cancel a specific run | runs.get(runId) / runs.cancel(runId) |
threads.stream is sugar for runs.start + immediate stream attach. Use it when one request both starts and consumes the run. Use the explicit pair when those two halves live in different processes, such as a webhook handler kicking the run while a worker tails the stream.
Lifecycle
Runs move through a closed set of statuses:
queued → preparing → running →
{ waiting_for_approval | waiting_for_input }* →
{ completed | failed | cancelled }
waiting_for_approval and waiting_for_input are pause states. Resume by resolving the approval or interaction. A paused run does not bill model time.
When a run terminates, the RunFinalResult carries:
status: one ofcompleted | failed | cancelledterminalReason: aNovoTerminalReasonwhen Novo policy stopped the run (budget, quota, max steps, …), ornullon normal completionusage: token totals and final Novo-billed cost
Identifiers
Every resource has a prefixed id: agent_…, thread_…, run_…. The prefixes are stable; you can pattern-match on them in logs without parsing JSON.