Agent Sessions
Long-running AI agent loops with automatic sandbox rotation.
Agent Sessions
Agent sessions let you run autonomous AI coding loops that survive sandbox rotations. The platform manages sandbox lifecycle, checkpoints, and state persistence so your agent can focus on the task.
Concepts
| Concept | Description |
|---|---|
| **Session** | A long-running container for agent work. Owns a sandbox, budget, context, and event log. |
| **Rotation** | When a sandbox nears expiry the platform checkpoints it, creates a fresh sandbox, and restores state — seamlessly. |
| **Budget** | Cost cap (cents), iteration limit, and/or duration limit. Execution is rejected when exceeded. |
| **Context** | Working directory, env vars, and custom JSON state that persist across rotations. |
| **Event log** | Append-only stream of everything that happens: executions, rotations, budget warnings, etc. |
Quick Start
Node.js
import { StateSetSandbox } from '@stateset/sandbox-sdk';
const client = new StateSetSandbox({
baseUrl: 'https://api.sandbox.stateset.app',
authToken: process.env.SANDBOX_API_KEY!,
});
// 1. Create a session
const { session } = await client.createAgentSession({
name: 'refactor-auth',
budget: { cost_cap_cents: 500, iteration_limit: 100 },
sandbox: { cpus: '2', memory: '4Gi', timeout_seconds: 3600 },
});
// 2. Start the session (provisions the first sandbox)
await client.startAgentSession(session.id);
// 3. Execute commands (budget-checked automatically)
const result = await client.executeInAgentSession(session.id, {
command: ['python3', 'analyze.py'],
working_dir: '/workspace/src',
});
console.log(result.stdout);
// 4. Write files
await client.writeAgentSessionFile(session.id, '/workspace/notes.md', '# Progress\n- Step 1 done');
// 5. Update context (persists across rotations)
await client.updateAgentContext(session.id, {
working_dir: '/workspace/src',
custom_state: { step: 1, branch: 'feature/auth' },
});
// 6. Stop when done
await client.stopAgentSession(session.id, 'Task complete');Python
from stateset_sandbox import StateSetSandbox
import os
client = StateSetSandbox(
base_url="https://api.sandbox.stateset.app",
auth_token=os.environ["SANDBOX_API_KEY"],
)
# 1. Create a session
session = client.create_agent_session(
name="refactor-auth",
budget={"cost_cap_cents": 500, "iteration_limit": 100},
sandbox={"cpus": "2", "memory": "4Gi", "timeout_seconds": 3600},
)
# 2. Start the session
client.start_agent_session(session.id)
# 3. Execute commands
result = client.execute_in_agent_session(
session.id,
command=["python3", "analyze.py"],
working_dir="/workspace/src",
)
print(result.stdout)
# 4. Write files
client.write_agent_session_file(session.id, "/workspace/notes.md", "# Progress\n- Step 1 done")
# 5. Update context
client.update_agent_context(session.id, {
"working_dir": "/workspace/src",
"custom_state": {"step": 1, "branch": "feature/auth"},
})
# 6. Stop when done
client.stop_agent_session(session.id, reason="Task complete")Budget Configuration
Budgets prevent runaway costs. Set one or more limits when creating a session:
const { session } = await client.createAgentSession({
name: 'code-review',
budget: {
cost_cap_cents: 1000, // $10 max
iteration_limit: 500, // 500 executions max
duration_limit_seconds: 7200, // 2 hours max
},
});The platform tracks consumption in session.budget_consumed:
const { session } = await client.getAgentSession(sessionId);
console.log(`Cost: ${session.budget_consumed.cost_cents}¢`);
console.log(`Iterations: ${session.budget_consumed.iterations}`);
console.log(`Duration: ${session.budget_consumed.duration_seconds}s`);When a limit is approached (80%) a budget_warning event is emitted. When exceeded, further executions are rejected with budget_exceeded.
Sandbox Rotation
Sessions can outlive individual sandboxes. When a sandbox nears its timeout the platform:
- Creates a checkpoint of the current sandbox
- Provisions a fresh sandbox with the same config
- Restores the checkpoint into the new sandbox
- Updates the session's
current_sandbox_id
Rotation Config
Control how early rotation begins:
const { session } = await client.createAgentSession({
name: 'long-task',
rotation: {
pre_rotate_buffer_seconds: 120, // Start rotation 2 min before expiry
},
sandbox: { timeout_seconds: 3600 },
});Detecting Rotations
Poll the event log for rotation events:
const { events } = await client.getAgentSessionEvents(sessionId, {
event_type: 'rotation_completed',
});
for (const event of events) {
console.log(`Rotation at seq ${event.sequence_number}:`, event.payload);
}Agent Context
Context persists across sandbox rotations. Use it to track working directory, environment, and custom state.
Node.js
// Set context
await client.updateAgentContext(sessionId, {
working_dir: '/workspace/project',
env_vars: { NODE_ENV: 'test' },
custom_state: {
current_task: 'implement-login',
files_modified: ['src/auth.ts', 'src/middleware.ts'],
},
});
// Read context (part of session)
const { session } = await client.getAgentSession(sessionId);
console.log(session.agent_context.custom_state);Python
# Set context
client.update_agent_context(session_id, {
"working_dir": "/workspace/project",
"env_vars": {"NODE_ENV": "test"},
"custom_state": {
"current_task": "implement-login",
"files_modified": ["src/auth.ts", "src/middleware.ts"],
},
})
# Read context
session = client.get_agent_session(session_id)
print(session.agent_context["custom_state"])Tool Registry
Register tools so the session tracks which capabilities the agent has:
Node.js
await client.registerAgentTool(sessionId, {
name: 'run_tests',
tool_type: 'custom',
description: 'Run the project test suite',
input_schema: {
type: 'object',
properties: {
filter: { type: 'string', description: 'Test name filter' },
},
},
});
const { tools } = await client.listAgentTools(sessionId);
console.log(tools.map(t => t.name));Python
client.register_agent_tool(session_id, {
"name": "run_tests",
"tool_type": "custom",
"description": "Run the project test suite",
"input_schema": {
"type": "object",
"properties": {
"filter": {"type": "string", "description": "Test name filter"},
},
},
})
tools = client.list_agent_tools(session_id)
print([t.name for t in tools])Event Log
The event log is an append-only stream of session activity. Use after_sequence for efficient polling.
Node.js
let lastSeq = 0;
async function pollEvents(sessionId: string) {
const { events } = await client.getAgentSessionEvents(sessionId, {
after_sequence: lastSeq,
limit: 50,
});
for (const event of events) {
console.log(`[${event.event_type}] seq=${event.sequence_number}`, event.payload);
lastSeq = event.sequence_number;
}
}Python
last_seq = 0
def poll_events(session_id: str):
global last_seq
events = client.get_agent_session_events(
session_id,
after_sequence=last_seq,
limit=50,
)
for event in events:
print(f"[{event.event_type}] seq={event.sequence_number}", event.payload)
last_seq = event.sequence_numberEvent Types
| Event | Description |
|---|---|
session_started | Session started, first sandbox provisioned |
session_paused | Session paused |
session_resumed | Session resumed |
session_completed | Session stopped gracefully |
session_failed | Session encountered a fatal error |
session_cancelled | Session force-cancelled |
sandbox_created | New sandbox provisioned |
sandbox_terminated | Sandbox terminated |
rotation_started | Sandbox rotation in progress |
rotation_completed | Rotation finished, new sandbox ready |
rotation_failed | Rotation failed |
exec_started | Command execution started |
exec_completed | Command execution finished |
budget_warning | Approaching budget limit (80%) |
budget_exceeded | Budget limit reached |
checkpoint_created | Checkpoint saved |
checkpoint_restored | Checkpoint restored into sandbox |
tool_registered | Tool added to registry |
heartbeat | Client heartbeat received |
Session Lifecycle
pending ──▶ running ──▶ completed
│
├──▶ paused ──▶ running
│
├──▶ rotating ──▶ running
│
├──▶ failed
│
└──▶ cancelled- **pending** — Created but not started.
- **running** — Active sandbox, accepting executions.
- **rotating** — Sandbox rotation in progress (brief, automatic).
- **paused** — Paused by client. Sandbox stays alive but rotation timer stops.
- **completed** — Stopped gracefully.
- **failed** — Unrecoverable error (e.g., rotation failure).
- **cancelled** — Force-terminated by client.
Heartbeat & Reattach
Send periodic heartbeats to prevent idle cleanup and monitor budget:
const { session } = await client.agentSessionHeartbeat(sessionId);
console.log(`Budget used: ${session.budget_consumed.cost_cents}¢`);Reconnect after a disconnect using a client ID:
// On create, provide a client_id
const { session } = await client.createAgentSession({
name: 'my-agent',
client_id: 'agent-instance-abc',
});
// Later, reconnect
const { session: existing } = await client.reattachAgentSession('agent-instance-abc');
if (existing) {
console.log(`Reattached to session ${existing.id}`);
}Complete Example: Multi-Step Coding Agent
import { StateSetSandbox } from '@stateset/sandbox-sdk';
const client = new StateSetSandbox({
baseUrl: 'https://api.sandbox.stateset.app',
authToken: process.env.SANDBOX_API_KEY!,
});
async function codingAgent(task: string) {
// Create and start session
const { session } = await client.createAgentSession({
name: `coding-${Date.now()}`,
budget: { cost_cap_cents: 500, iteration_limit: 50 },
sandbox: { cpus: '2', memory: '4Gi', timeout_seconds: 3600 },
});
await client.startAgentSession(session.id);
try {
// Step 1: Set up project
await client.executeInAgentSession(session.id, {
command: 'git clone https://github.com/example/project.git /workspace/project',
});
await client.updateAgentContext(session.id, { working_dir: '/workspace/project' });
// Step 2: Install dependencies
await client.executeInAgentSession(session.id, {
command: ['npm', 'install'],
working_dir: '/workspace/project',
});
// Step 3: Write code
await client.writeAgentSessionFile(
session.id,
'/workspace/project/src/feature.ts',
`export function newFeature() {\n return 'implemented';\n}\n`,
);
// Step 4: Run tests
const testResult = await client.executeInAgentSession(session.id, {
command: ['npm', 'test'],
working_dir: '/workspace/project',
});
// Step 5: Track result in context
await client.updateAgentContext(session.id, {
custom_state: {
task,
tests_passed: testResult.exit_code === 0,
output: testResult.stdout.slice(0, 500),
},
});
return { success: testResult.exit_code === 0, output: testResult.stdout };
} finally {
await client.stopAgentSession(session.id, 'Agent finished');
}
}