N AgentNava
agentnava.com →
Why this is Freelancer-only

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

Your UI · start 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.
session id minted
AgentNava platform · state Session holds its own history, attachments, vars Independent from every other session on the same deployed agent. Auto-hibernates after 24h idle, resumes instantly.
each turn
Your UI · chat 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.
agent runs · streams events
AgentNava platform · runtime Typed 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

SourceBehavior
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.

sessionModeWhat 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

StateWhat it means
activeTouched within the last hour. Messages stream as SSE.
idleNo activity for an hour, but history is still in memory. Resuming is instant.
hibernatedNo activity for 24h. History persisted to durable storage; first resume after this has a small wake-up cost (~200ms).
closedExplicitly 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 for per-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.