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
| State | Description |
|---|---|
starting | Sandbox is being created. Transitions to active on success or error on failure. |
active | Sandbox is running and accepting messages. |
paused | Sandbox may still be alive but the session is idle. Can be resumed. |
ended | Session is terminated. Sandbox is destroyed. Cannot be resumed. |
error | Something went wrong (sandbox crash, runner unavailable). Can be resumed. |
State transitions:
starting --> active --> paused --> active (resume)
\ \-> ended
\-> error --> active (resume)
\-> ended
Creating a Session
- TypeScript
- Python
- CLI
- curl
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"
from ash_sdk import AshClient
client = AshClient("http://localhost:4100", api_key=os.environ["ASH_API_KEY"])
session = client.create_session("my-agent")
print(session.id) # "a1b2c3d4-..."
print(session.status) # "active"
ash session create my-agent
curl -X POST $ASH_SERVER_URL/api/sessions \
-H "Content-Type: application/json" \
-d '{"agent": "my-agent"}'
Response (201):
{
"session": {
"id": "a1b2c3d4-...",
"agentName": "my-agent",
"sandboxId": "a1b2c3d4-...",
"status": "active",
"model": null,
"createdAt": "2025-01-15T10:30:00.000Z",
"lastActiveAt": "2025-01-15T10:30:00.000Z"
}
}
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.
- TypeScript
- Python
- CLI
- curl
const session = await client.createSession('my-agent', { model: 'claude-opus-4-6' });
session = client.create_session("my-agent", model="claude-opus-4-6")
ash session create my-agent --model claude-opus-4-6
curl -X POST $ASH_SERVER_URL/api/sessions \
-H "Content-Type: application/json" \
-d '{"agent": "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.
- TypeScript
- curl
const session = await client.createSession('my-agent', {
mcpServers: {
'customer-tools': { url: 'http://host-app:8000/mcp?tenant=t_abc123' },
},
});
curl -X POST $ASH_SERVER_URL/api/sessions \
-H "Content-Type: application/json" \
-d '{
"agent": "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.
- TypeScript
- curl
const session = await client.createSession('my-agent', {
systemPrompt: 'You are a support agent for tenant t_abc123. Be concise.',
});
curl -X POST $ASH_SERVER_URL/api/sessions \
-H "Content-Type: application/json" \
-d '{
"agent": "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:
- TypeScript
- curl
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.`,
});
curl -X POST $ASH_SERVER_URL/api/sessions \
-H "Content-Type: application/json" \
-d '{
"agent": "my-agent",
"mcpServers": {
"tenant-tools": { "url": "http://host-app:8000/mcp?tenant=t_abc123" }
},
"systemPrompt": "You are a support agent for Acme Corp. 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.
- TypeScript
- Python
- CLI
- curl
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');
}
}
for event in client.send_message_stream(session.id, "What is the capital of France?"):
if event.type == "message":
print(event.data)
elif event.type == "done":
print("Turn complete")
ash session send <session-id> "What is the capital of France?"
curl -X POST $ASH_SERVER_URL/api/sessions/<session-id>/messages \
-H "Content-Type: application/json" \
-d '{"content": "What is the capital of France?"}' \
-N
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.
- TypeScript
- Python
- curl
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
}
for event in client.send_message_stream(session.id, "Analyze this complex codebase", model="claude-opus-4-6"):
pass # This message uses Opus regardless of the session/agent default
curl -X POST $ASH_SERVER_URL/api/sessions/<session-id>/messages \
-H "Content-Type: application/json" \
-d '{"content": "Analyze this complex codebase", "model": "claude-opus-4-6"}' \
-N
Multi-Turn Conversations
Sessions preserve full conversation context across turns. Each message builds on the previous ones.
- TypeScript
- Python
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}`);
}
session = client.create_session("my-agent")
# Turn 1
for event in client.send_message_stream(session.id, "My name is Alice."):
pass # Agent acknowledges
# Turn 2 -- agent remembers context from turn 1
for event in client.send_message_stream(session.id, "What is my name?"):
if event.type == "message":
data = event.data
if data.get("type") == "assistant":
for block in data.get("message", {}).get("content", []):
if block.get("type") == "text":
print(block["text"]) # "Your name is Alice."
Messages are persisted to the database. You can retrieve them later:
messages = client.list_messages(session.id)
for msg in messages:
print(f"[{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.
- TypeScript
- Python
- CLI
- curl
const session = await client.pauseSession(session.id);
console.log(session.status); // "paused"
session = client.pause_session(session.id)
ash session pause <session-id>
curl -X POST $ASH_SERVER_URL/api/sessions/<session-id>/pause
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.
- TypeScript
- Python
- CLI
- curl
const session = await client.resumeSession(session.id);
console.log(session.status); // "active"
session = client.resume_session(session.id)
ash session resume <session-id>
curl -X POST $ASH_SERVER_URL/api/sessions/<session-id>/resume
Response includes the resume path taken:
{
"session": {
"id": "a1b2c3d4-...",
"status": "active",
"sandboxId": "a1b2c3d4-..."
}
}
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.
- TypeScript
- Python
- CLI
- curl
const session = await client.endSession(session.id);
console.log(session.status); // "ended"
session = client.end_session(session.id)
ash session end <session-id>
curl -X DELETE $ASH_SERVER_URL/api/sessions/<session-id>
Listing Sessions
- TypeScript
- Python
- CLI
- curl
// All sessions
const sessions = await client.listSessions();
// Filter by agent
const sessions = await client.listSessions('my-agent');
sessions = client.list_sessions()
ash session list
# All sessions
curl $ASH_SERVER_URL/api/sessions
# Filter by agent
curl "$ASH_SERVER_URL/api/sessions?agent=my-agent"
API Reference
| Method | Endpoint | Description |
|---|---|---|
POST | /api/sessions | Create a session |
GET | /api/sessions | List sessions (optional ?agent= filter) |
GET | /api/sessions/:id | Get session details |
POST | /api/sessions/:id/messages | Send a message (returns SSE stream) |
GET | /api/sessions/:id/messages | List persisted messages |
POST | /api/sessions/:id/pause | Pause a session |
POST | /api/sessions/:id/resume | Resume a session |
DELETE | /api/sessions/:id | End a session |