Skip to main content

Managing Sessions

A session is a stateful conversation between a client and a deployed agent. Each session runs inside an isolated sandbox with its own workspace directory. Sessions persist messages across turns and can be paused, resumed, and ended.

Session States

StateDescription
startingSandbox is being created. Transitions to active on success or error on failure.
activeSandbox is running and accepting messages.
pausedSandbox may still be alive but the session is idle. Can be resumed.
endedSession is terminated. Sandbox is destroyed. Cannot be resumed.
errorSomething went wrong (sandbox crash, runner unavailable). Can be resumed.

State transitions:

starting --> active --> paused --> active (resume)
\ \-> ended
\-> error --> active (resume)
\-> ended

Creating a Session

import { AshClient } from '@ash-ai/sdk';

const client = new AshClient({ serverUrl: 'http://localhost:4100', apiKey: process.env.ASH_API_KEY });
const session = await client.createSession('my-agent');
console.log(session.id); // "a1b2c3d4-..."
console.log(session.status); // "active"

Creating a Session with a Model Override

You can specify a model when creating a session. This overrides the agent's default model for the entire session.

const session = await client.createSession('my-agent', { model: 'claude-opus-4-6' });

Creating a Session with Per-Session MCP Servers

You can inject MCP servers at session creation time. This enables the sidecar pattern: your host application exposes tenant-specific tools as MCP endpoints, and each session connects to its own URL.

Session-level MCP servers are merged into the agent's .mcp.json. If both define a server with the same key, the session entry wins.

const session = await client.createSession('my-agent', {
mcpServers: {
'customer-tools': { url: 'http://host-app:8000/mcp?tenant=t_abc123' },
},
});

Creating a Session with a System Prompt Override

You can replace the agent's CLAUDE.md for a specific session. The agent definition is not modified — only the sandbox workspace copy is overwritten.

const session = await client.createSession('my-agent', {
systemPrompt: 'You are a support agent for tenant t_abc123. Be concise.',
});

Combining MCP Servers and System Prompt

For full per-tenant customization, pass both mcpServers and systemPrompt together:

const session = await client.createSession('my-agent', {
mcpServers: {
'tenant-tools': { url: `http://host-app:8000/mcp?tenant=${tenantId}` },
},
systemPrompt: `You are a support agent for ${tenantName}. Use the tenant-tools MCP server to look up account data.`,
});

Sending Messages

Messages are sent via POST and return an SSE stream. See the Streaming Responses guide for full details on consuming the stream.

for await (const event of client.sendMessageStream(session.id, 'What is the capital of France?')) {
if (event.type === 'message') {
console.log(event.data);
} else if (event.type === 'done') {
console.log('Turn complete');
}
}

Per-Message Model Override

You can override the model for a single message. This takes the highest precedence — it overrides both the session model and the agent's default. Useful for using a more capable model on hard tasks or a cheaper model on simple ones.

for await (const event of client.sendMessageStream(session.id, 'Analyze this complex codebase', {
model: 'claude-opus-4-6',
})) {
// This message uses Opus regardless of the session/agent default
}

Multi-Turn Conversations

Sessions preserve full conversation context across turns. Each message builds on the previous ones.

const session = await client.createSession('my-agent');

// Turn 1
for await (const event of client.sendMessageStream(session.id, 'My name is Alice.')) {
// Agent acknowledges
}

// Turn 2 -- agent remembers context from turn 1
for await (const event of client.sendMessageStream(session.id, 'What is my name?')) {
if (event.type === 'message') {
const text = extractTextFromEvent(event.data);
if (text) console.log(text); // "Your name is Alice."
}
}

Messages are persisted to the database. You can retrieve them later:

const messages = await client.listMessages(session.id);
for (const msg of messages) {
console.log(`[${msg.role}] ${msg.content}`);
}

Pausing a Session

Pausing a session marks it as idle. The sandbox may remain alive for fast resume, but the session stops accepting new messages until resumed.

const session = await client.pauseSession(session.id);
console.log(session.status); // "paused"

Resuming a Session

Resume brings a paused or errored session back to active. Ash uses two resume paths:

Fast path (warm resume): If the original sandbox is still alive, the session resumes instantly with no state loss. This is the common case when resuming shortly after pausing.

Cold path (cold resume): If the sandbox was reclaimed (idle timeout, OOM, server restart), Ash creates a new sandbox. Workspace state is restored from the persisted snapshot if available. Conversation history is preserved in the database regardless.

const session = await client.resumeSession(session.id);
console.log(session.status); // "active"

Ending a Session

Ending a session destroys the sandbox and marks the session as permanently closed. The session's messages and events remain in the database for retrieval, but no new messages can be sent.

const session = await client.endSession(session.id);
console.log(session.status); // "ended"

Listing Sessions

// All sessions
const sessions = await client.listSessions();

// Filter by agent
const sessions = await client.listSessions('my-agent');

API Reference

MethodEndpointDescription
POST/api/sessionsCreate a session
GET/api/sessionsList sessions (optional ?agent= filter)
GET/api/sessions/:idGet session details
POST/api/sessions/:id/messagesSend a message (returns SSE stream)
GET/api/sessions/:id/messagesList persisted messages
POST/api/sessions/:id/pausePause a session
POST/api/sessions/:id/resumeResume a session
DELETE/api/sessions/:idEnd a session