Skip to main content

Session Loop

Creating sessions, sending messages, getting results, and multi-agent orchestration

ctx.sessions lets a mini tool create and drive its own sessions. Typical uses: external platform bots, background task processing, multi-agent orchestration.

Isolation boundary: a mini tool can only access sessions it created itself — it can't read the user's regular conversations, and it can't touch another mini tool's sessions either.

The full loop

Creating a session

const session = await ctx.sessions.create({
  containerId: 'inbox',            // Placement into a container
  title: 'Chat with Alice',
  activity: 'interactive',         // or 'background'
  permissionMode: 'ask',           // interactive defaults to ask; background defaults to acceptCalls
  initialMessage: {
    text: 'Hello from the bot.',
    idempotencyKey: 'welcome-alice-2026-01-01',
  },
});

Choose exactly one placement, never both:

  • containerId — placed into your own container (the mini tool's default choice).
  • space: { spaceId } — placed into a regular Space's session list, appearing as if the user created it, while ownership still belongs to the mini tool.

What activity actually changes: a background session doesn't trigger a system notification when it completes or is waiting — only a red dot appears at the container entry — and defaults to acceptCalls permissions, suited for unattended tasks. interactive is a normal chat session.

context: 'caller' can only be used inside an agent tool's execute(). It makes the new session inherit the caller's cwd, model, policy, and Space context — useful for forking a side thread off the current conversation. Using it outside a tool call throws.

A create() call with initialMessage that fails does not leave a ghost session behind.

Sending messages

send() is strictly FIFO within a single session:

const receipt = await ctx.sessions.send(session.sessionId, {
  text: 'What is the weather?',
  idempotencyKey: 'msg-123',       // required
});

if (receipt.state === 'rejected') {
  // Queue full — retry after receipt.retryAfterMs, don't hammer it immediately
}

idempotencyKey is required, up to 512 characters. Use a stable message ID from the external source, not a random UUID — resending it returns the original receipt instead of starting a new turn.

Limits: 100,000 characters of text; 10 attachments per message, 20MB each, 20MB total.

Getting results: three APIs, three jobs

APIPurpose
waitForTurn(sessionId, turnId)You need the exact final result of this turn right now — request/response-style orchestration
onDidReceiveEvent(cb)Long-term observation across multiple sessions and turns — bot scenarios
listEvents({ sessionId, after })For historical replay and reconnect recovery only
const result = await ctx.sessions.waitForTurn(
  sessionId, receipt.turnId, { timeoutMs: 60_000 },
);
if (result.state === 'completed') console.log(result.outputText);
if (result.state === 'failed')    console.error(result.code);
if (result.state === 'timeout')   console.log('Still running');

Timeout defaults to 60 seconds, clamped between 1–600 seconds. A timeout only ends the wait — it doesn't cancel the turn itself.

Among event types, assistant.delta is a real-time streaming fragment, never persisted; reconnect recovery must rely on assistant.message or turn.completed. Events are retained for 7 days, up to 10,000 per mini tool.

Never sleep and poll listEvents() — use waitForTurn() instead.

Orchestration pattern: Planner → Worker → Writer

The standard shape for multi-agent orchestration: the main tool breaks down a task in a single call, spawns parallel child sessions, waits for all results, and aggregates the output.

const results = await Promise.all(tasks.map(async (task, i) => {
  const s = await ctx.sessions.create({
    containerId: 'workers',
    activity: 'background',
    context: 'caller',
    initialMessage: { text: task, idempotencyKey: `job-${jobId}-${i}` },
  });
  const r = await ctx.sessions.waitForTurn(s.sessionId, s.turnId, { timeoutMs: 300_000 });
  return r.state === 'completed' ? r.outputText : `Task ${i} failed`;
}));

Use background so child sessions don't interrupt the user, while the user can still click into the container to watch each child session's full progress.

Quotas

LimitValue
Outstanding turns per session20
Outstanding turns per mini tool200
Retry interval when queue is full1000 ms
Event retention count / duration10,000 events / 7 days

Common mistakes

  • Forgetting to declare permissions.sessions or contributes.sessionContainers.
  • Passing a containerId not declared in the manifest.
  • Omitting idempotencyKey, so every external webhook retry starts a new turn.
  • Using context: 'caller' outside a tool call.
  • Assuming assistant.delta is persisted.
  • Not handling send()'s rejected queue-full state.
  • Creating a new session for every external message instead of reusing one session per contact.