Sessions freelancers only
Once an agent is deployed, end users interact with it through sessions. Each session is one user's ongoing conversation: its own message history, its own attached files, its own per-session variables. A single deployed agent serves many concurrent sessions in parallel; each one is independent.
The session model on this page — an.sessions.start({ user: …, vars: … }), embed widgets creating one session per browser tab, thousands of parallel sessions per agent, per-user fan-out — is how Freelancers serve many end users. Teammates work differently. A Teammate has one ongoing conversation per workspace member (typically just the workspace owner), accessed through the console — not through this SDK. There's no sessions.start call, no embed, no per-user fan-out. If you're building a Teammate, see Roles → Teammates and Teams & workflow.
How a session works
an.sessions.start({ agentId, env, user, vars })
Mints a session for one user's conversation. vars become per-session state readable from every tool handler.
for await (const event of an.sessions.chat(id, msg))
Opens an SSE iterator for one turn. The iterator closes when the agent's reply is complete.
AgentEvents stream back over SSE
message-delta → chat, tool-start/tool-end → spinners, widget-update → renderers, phase → progress, done → end of turn.
How sessions get created
| Source | Behavior |
|---|---|
| Your code | an.sessions.start({ agentId, env, user, vars }) mints a session ID. Use it on every subsequent chat call. |
| Embed widget | The embed creates one session per browser tab on first message. The session ID is persisted in localStorage, so users can refresh and resume. |
| Implicit (single-turn) | Call an.sessions.chat without an ID and the runtime mints a one-shot session, returns the response, and tears it down. Good for fire-and-forget invocations. |
How sessions relate to users
Think of the agent as someone the user hires. Each user gets their own continuous relationship — same history, same per-session vars. That model is the default and is controlled by sessionMode on agents.configure(...).
| sessionMode | What it means |
|---|---|
per-user default |
One session per (agent, user.id) pair, forever. Same user returning next week continues the same thread. Different users are completely isolated. When the user starts a chat or your code calls an.agents.run(agentId, { ..., user: { id: '…' } }), the runtime looks up the session by user.id — create it once, return it on every subsequent call. |
per-call |
Every invocation creates a fresh session. No continuity, no accumulation. Use for stateless webhooks, fire-and-forget reports, and smoke tests. |
Cron + per-user
A schedule trigger on a per-user agent fans out across every user who's interacted. One user (you) running a trading agent = one cron firing on your continuous thread. A real-estate assistant with 1,000 active users + cron: '0 9 * * MON' = 1,000 parallel Monday-morning runs, each on its own user's session. Cron firings on agents that nobody has interacted with yet are a no-op — the agent hasn't been hired by anyone.
Start a session
const session = await an.sessions.start({
agentId: 'agt_2b8e',
env: 'prod', // or 'test'
user: { id: 'u_42', name: 'Jamie' }, // your auth's user identity, propagated to tool handlers
vars: { plan: 'gold', region: 'eu' }, // per-session vars, readable in tool handlers as ctx.vars
});
// → { id: 'sess_5d3a', agentId: 'agt_2b8e', env: 'prod', startedAt: '…' }
Resume an existing session by ID:
const existing = await an.sessions.get(sessionId);
The chat call
Each turn returns an async iterator of AgentEvents. The iterator completes when the agent's turn ends.
for await (const event of an.sessions.chat(session.id, 'Can you walk me through this floor plan?')) {
switch (event.type) {
case 'message-start': /* assistant turn begins */ break;
case 'message-delta': appendTokens(event.delta); break;
case 'message-end': /* full text in event.final */ break;
case 'tool-start': showSpinner(event.name); break;
case 'tool-end': hideSpinner(event.toolId); break;
case 'phase': updatePhase(event.label, event.state); break;
case 'widget-update': renderWidget(event); break;
case 'widget-remove': unmountWidget(event.widgetId); break;
case 'error': reportError(event.message); break;
case 'done': /* iterator will close after this */ break;
}
}
The shape is the same whether the agent is on a test or a prod deploy. See Spec → Event stream for the full type union.
Sending structured messages
Pass an object instead of a string for messages with role, attachments, or metadata:
for await (const event of an.sessions.chat(session.id, {
role: 'user',
content: 'Can you walk me through this floor plan?',
attachments: [
{ id: 'file_4f1c9a', type: 'image', name: 'floor-plan.png' },
],
meta: { source: 'embed-widget', clientVersion: '1.4.2' },
})) {
// …
}
Attachments
Attachments are files the user uploads as part of a conversation. They live for the lifetime of the session and are not added to the agent's persistent Knowledge base. They're transient context for that one chat.
// Upload a file to a session; returns a file ID you reference in a message
const file = await an.sessions.attachFile(session.id, blob, { name: 'floor-plan.png' });
// → { id: 'file_4f1c9a', type: 'image', status: 'ready' }
await an.sessions.chat(session.id, {
role: 'user',
content: 'What rooms are visible?',
attachments: [{ id: file.id }],
});
The embed widget does this automatically when the user drops a file into the chat composer (if data-uploads="true" is on the script tag; see Operate → Embed).
Per-session variables
Pass vars at session start. Read them from any tool handler via ctx.vars; update them mid-session via ctx.setVar.
// At session start
await an.sessions.start({
agentId: 'agt_2b8e',
vars: { plan: 'gold', region: 'eu', tenantId: 't_91' },
});
// In a tool handler
defineTool({
name: 'fetch_quote',
handler: async (input, ctx) => {
const tenant = ctx.vars.tenantId;
const plan = ctx.vars.plan;
const q = await pricingApi.quote(input, { tenant, plan });
ctx.setVar('lastQuoteId', q.id); // visible to subsequent tools and widgets
return q;
},
});
Vars are per-session, not per-workspace. Use them for user identity propagation, feature flags, tenant scoping, and any state that should survive across turns within one conversation but not leak across users.
Session lifecycle
| State | What it means |
|---|---|
active | Touched within the last hour. Messages stream as SSE. |
idle | No activity for an hour, but history is still in memory. Resuming is instant. |
hibernated | No activity for 24h. History persisted to durable storage; first resume after this has a small wake-up cost (~200ms). |
closed | Explicitly ended via an.sessions.close(id) or by the agent emitting a handoff event. Attachments are deleted; messages remain queryable from the audit log. |
When sessions end
Sessions don't end on their own under normal use. Three things can release one:
- Explicit close —
an.sessions.close(sessionId). Rare. Useful for "start fresh" resets, end-of-test cleanup, admin tooling. - Agent version deletion: cascades to every session that pinned to it.
- Inactivity TTL — 90 days for
per-user, 5 minutes forper-call. The 90-day rule only matters once you have a long tail of abandoned users; until then, idle sessions hibernate at storage-only cost.
Listing & querying
// list recent sessions for one agent
const recent = await an.sessions.list({ agentId: 'agt_2b8e', since: '2026-05-10T00:00:00Z' });
// get all messages from a single session
const history = await an.sessions.messages(sessionId);
// close a session explicitly
await an.sessions.close(sessionId);
The workspace dashboard surfaces session counts, average turn count, common handoff reasons, and per-version metrics. For programmatic analytics, query an.sessions.list with date ranges + an.sessions.messages per session.