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 decides how a session is keyed when a user starts one, by opening a chat or by calling agents.run(...) with a user. It does not decide where a trigger lands. See Triggers continue the thread.
| 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 user-initiated invocation creates a fresh session. No continuity, no accumulation. Use for fire-and-forget reports and smoke tests. This does not apply to triggers: a cron, schedule or webhook run continues the agent's thread even under per-call. |
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.
Triggers continue the thread
An agent is one ongoing conversation. A person typing, a manual run, a cron tick, a schedule and an inbound webhook are all just what woke it for a given turn, so every one of them continues the agent's existing session and sees the files already on its Drive. A schedule that says "re-run the report on /workspace/spend.csv" finds the file it names.
The order is: the session the trigger was bound to, then the firing user's own session when the trigger names a user, then the agent's most recently started session. A new session is created only when the agent has genuinely never run.
This is independent of sessionMode. Earlier releases resolved triggered runs through sessionMode and defaulted them to per-call, so each firing opened a thread of its own, against a workspace that began empty, and its reply was written where nobody would read it.
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: { month: '2026-07' }, // typed session inputs, readable in tool handlers as ctx.vars
fresh: true, // always start a NEW session (see below)
});
// → { id: 'sess_5d3a', agentId: 'agt_2b8e', env: 'prod', startedAt: '…', versionId: 'ver_91c4' }
Resume an existing session by ID:
const existing = await an.sessions.get(sessionId);
Resume, or start a new one
When you pass a user, the default is to resume that user's
existing session for the agent rather than mint a new empty one. That is what you
want for a chat surface that re-issues this call on every page load.
Pass fresh: true to always create a new session instead. An agent can
run many sessions at once, each with its own workspace, its own pinned
version and its own per-session bindings. This is what a "launch" action sends.
A session pins its version
Saving an agent mints a new version. A session records which version it started with and runs that version for its whole life, so editing an agent never changes the behaviour of a conversation already in progress.
const s1 = await an.sessions.start({ agentId, fresh: true }); // pins version 3
await an.specs.push({ key: 'sal', instructions: '…' }); // mints version 4
await an.agents.configure({ key: 'sal', /* … */ });
// s1 still runs version 3. Launch a new session to pick up version 4:
const s2 = await an.sessions.start({ agentId, fresh: true }); // pins version 4
This is what makes a session reproducible: the same version, the same compiled configuration, every turn. It is also why "edit and try again" means launching a new session rather than expecting the open one to change underneath you.
The version a session pinned comes back on start as
versionId, and on every entry from
GET /v1/agents/:id/sessions.
Reading an agent's versions
GET /v1/agents/agt_2b8e/versions
// → { versions: [ { versionId, versionNumber, specId, specVersion, createdAt, … }, … ] }
Newest first. Versions are never deleted, only superseded, so this is the agent's whole configuration history.
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 (summaries, newest first)
const { sessions } = await an.sessions.list({ agentId: 'agt_2b8e', limit: 50 });
// → [{ id, agentId, env, startedAt, versionId, user, turns }, …]
// get all messages from a single session
const history = await an.sessions.messages(sessionId);
// close a session explicitly
await an.sessions.close(sessionId);
Over HTTP that is GET /v1/agents/:id/sessions?limit=50. It returns
summaries, not transcripts: a list of sessions deliberately does not carry
every conversation's history. Fetch one session to read its messages.
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.