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
| API | Purpose |
|---|---|
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
| Limit | Value |
|---|---|
| Outstanding turns per session | 20 |
| Outstanding turns per mini tool | 200 |
| Retry interval when queue is full | 1000 ms |
| Event retention count / duration | 10,000 events / 7 days |
Common mistakes
- Forgetting to declare
permissions.sessionsorcontributes.sessionContainers. - Passing a
containerIdnot declared in the manifest. - Omitting
idempotencyKey, so every external webhook retry starts a new turn. - Using
context: 'caller'outside a tool call. - Assuming
assistant.deltais persisted. - Not handling
send()'srejectedqueue-full state. - Creating a new session for every external message instead of reusing one session per contact.