Developer docs · Browse & search
BUILD

Agents and conversations

Sessions that remember, runs that ask for something or wait for your next message, plans you give, tools, skills and subagents an agent finds as it needs them, and tools added when you start.

Conversations

A session remembers what came before. Its turns are ordinary runs.

ts
const session = agent.session();
await session.send('My name is Sam.');
const { text } = await session.send('What is my name?');

const later = await jwtson.sessions.get(session.id); // Pick it up again
for await (const text of (await later.start('And my job?')).textStream()) process.stdout.write(text);
await later.close();

A session starts on the first message, so id is null until that's finished. It takes text, for agents whose input is text.

When a run needs something

Most runs just answer. Some ask a question or need a decision, and you can handle that in code:

ts
const result = await workflow.run(input, {
  onInput: wait => askSomeone(wait.prompt),     // Return the answer, or nothing to leave it for someone else
  onApproval: approval => approval.approve(),
});

With no handler, run() waits until someone answers elsewhere, so pass timeoutMs if you'd rather not wait. It looks often at first and then every 2 seconds, which pollMs changes. To work through what's waiting on people:

ts
for await (const item of jwtson.inbox.list()) {
  if (item.type === 'approval') await item.reject('Over budget');
  else await item.answer('Blue');
}

An approval counts only for what you read: if the proposal changed since, deciding throws ConflictError.

Wait for the next message in the same run

The built-in wait_for_input({ prompt, instructions? }) uses the existing input-wait engine without a declared wait binding. It is discoverable by default; use capabilities.alwaysLoaded: ['platform:wait_for_input'] to keep it loaded. In the agent's instructions, say: “Perform each requested action, explain the result, then wait for the next message until I say done.” The agent puts its current response in prompt and can leave itself continuation instructions.

Waiting releases the worker and keeps the same run ID, history, files and plan. The run timeout pauses for this wait, even across worker restarts, so the run can remain open until the user responds. Each accepted reply starts a fresh timeout, step, model-turn and tool-call allowance for the next response. While waiting, the run's deadline is null; after replying it is the reply time plus the configured timeout. No timer or polling model calls are needed. A normal run.say() message resumes this wait, including one sent while the agent was deciding to wait. Declared questions and external-task waits still require their own exact answers and keep their configured deadlines; normal messages do not resolve them or wake a sleeping run early.

ts
const run = await agent.start('Update the report, then wait for my next edit.');
const first = await run.waitForInput({ timeoutMs: 60_000 });
if (first) {
  console.log(first.prompt); // The current response, not a final run result
  await run.say('Shorten the title.', { idempotencyKey: 'report-edit-2' });
  const next = await run.waitForInput({ afterWaitId: first.id, timeoutMs: 60_000 });
  if (next) await run.complete({ waitId: next.id, idempotencyKey: 'report-done' });
}
console.log((await run.result()).text);

wait.answer('…') also resumes the exact wait. wait.complete() and run.complete() send a done response to a conversational input wait, asking the agent to produce its final result through normal validation; they do not force success. Save the wait ID and idempotency key together for retries. run.cancel() stops the run when no final answer is needed. Total token usage, cost and history remain cumulative. Only an accepted reply to this native next-message wait renews execution limits; sleep, worker restarts, duplicate replies and ordinary messages during execution do not. Each response still stops if it exhausts its own allowance. run.wait() and run.result() still wait for the final outcome; use waitForInput() to stop at an interactive prompt instead. SDK polling timeouts only stop the caller's polling; they do not close the waiting run.

Context during long runs

Compaction is on by default. Before model calls it checks the full input, including schemas, and summarizes older work near 80% of the context ceiling, aiming for 60%. Instructions, the original task, current plan, explicit notes and the latest input stay exact. Earlier user messages and tool results may become summaries, while their originals remain saved. The automatically offered result_read tool lets the agent browse a history index and read exact messages or results without repeating actions. Summaries can omit details; use Keep a plan and its notes for lasting facts and constraints, and files for large work products. If mandatory context cannot fit or required compaction fails, the run reports context_limit instead of silently dropping it.

Giving it a plan

A run plans its own steps unless you give them. Given steps, it follows them in order:

ts
const run = await reviewer.start(contract, { plan: ['Read the contract', 'List the risks', { description: 'Suggest wording', required: false }] });
for await (const plan of run.plans()) console.log(plan.steps); // how many are done

Steps are step-1, step-2 and so on unless you give { id }. An agent the SDK creates takes a plan; one set up with allow_supplied_plan: false refuses it.

Discovering capabilities

New agents discover platform actions, authorized registered tools, skills, and their bound subagents through capabilities_search and capabilities_activate. The platform supplies these helpers and their instructions automatically. Only selected schemas and skill instructions enter the model's context. You can bind tools and skills to pin them, or let the agent find authorized ones as needed.

The agent can browse a complete inventory with an empty search query and follow next_cursor through its pages. Results distinguish available actions from public platform features that need setup. For example, file-writing metadata explains that Workspace files needs write access in Capabilities; discovery and activation never enable access themselves. Browsing does not activate tools or load their schemas.

ts
const reviewer = await jwtson.agents.create({
  name: 'Report reviewer', model: provider, instructions: 'Review the supplied report.',
});
const agent = await jwtson.agents.create({
  name: 'Report monitor', model: provider,
  instructions: 'Check the report until ready, sleeping between checks, then ask the reviewer.',
  tools: [{ name: 'check_report', always: true }],
  skills: ['Report procedure'], // { name: 'Report procedure', always: true } loads it immediately
  agents: [{ role: 'reviewer', agent: reviewer, whenToUse: 'Review a completed report.' }],
  capabilities: {
    scope: 'authorized', // Default. Use 'bound' for only bindings and enabled platform actions.
    alwaysLoaded: ['platform:sleep_until'],
    maximumActive: 16,   // Optional: the most optional action schemas loaded together. Leave it out for no limit.
  },
});

Tools and subagents default to discovery too; { always: true } loads their schemas from the first turn. agents[].agent accepts an Agent, family id or name, and must already be published. Only bound subagents are discoverable, and each child uses its own policy and context. alwaysLoaded accepts namespaced binding keys such as tool:check_report, skill:review, and agent:reviewer; kind:* loads that kind's bindings.

Activation checks existing permissions and loads a skill's required authorized tools. The agent can use replace: true when activating to switch its optional working set. Its selected capabilities survive compaction, sleep and worker restart. Existing published versions without this policy keep their previous behavior until updated and republished. Search/activation add tool calls; use always-loaded settings for frequently needed actions. See ADR 0045.

Adding tools and skills when you start

An agent keeps what it was published with, and a run or session turn can add more. Whoever starts it needs a grant on each.

ts
await agent.run('Refund order 42', { tools: ['issue_refund'], skills: [{ name: 'Refunds', always: true }] });
await session.send('Now check the stock', { tools: ['stock_level'] }); // Later turns keep it

A workflow does this with rules on an agent step, so no model decides:

ts
{ id: 'answer', type: 'agent', version_id: agent.versionId, task: { ref: 'input.text' }, next: 'done',
  add: { skills: [{ skill: refundsId, when: { op: 'eq', left: { ref: 'input.topic' }, right: { value: 'refund' } } }] } }

Every tool and skill a step lists is checked when the run starts, whichever way its rules go.

From the @jwtson/sdk README, as the code stands.