Sessions
Sessions provide stateful multi-turn conversation management. Instead of manually tracking message history, sessions accumulate context across runs and optionally persist conversations for later resumption.
Session Types
ModelRelay offers two session modes:
| Mode | Storage | Use Case |
|---|---|---|
| Local | Client-side (memory, file, SQLite) | Privacy-sensitive workflows, offline agents, CLI tools |
| Remote | Server-side | Cross-device continuity, team collaboration, audit trails |
Quick Start
import { ModelRelay } from "@modelrelay/sdk";
const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);
// Create a local session (history stays on device)
const session = mr.sessions.createLocal({
defaultModel: "claude-sonnet-4-20250514",
});
// Each run automatically includes previous conversation context
const result1 = await session.run("What is the capital of France?");
console.log(result1.output); // "The capital of France is Paris."
const result2 = await session.run("What about Germany?");
console.log(result2.output); // "The capital of Germany is Berlin."
// The model understood "Germany" from context
await session.close();
import (
"context"
sdk "github.com/modelrelay/sdk-go"
)
client, _ := sdk.NewClientFromSecretKey(os.Getenv("MODELRELAY_API_KEY"))
// Create a remote session (history persisted on server)
session, _ := client.Sessions.Create(ctx, sdk.SessionCreateRequest{
Metadata: &map[string]interface{}{"project": "my-app"},
})
// Add messages to the session
client.Sessions.AddMessage(ctx, session.Id, sdk.SessionMessageCreateRequest{
Role: "user",
Content: []map[string]interface{}{{"type": "text", "text": "What is the capital of France?"}},
})
// Get session with full message history
sessionWithMessages, _ := client.Sessions.Get(ctx, session.Id)
for _, msg := range sessionWithMessages.Messages {
fmt.Printf("[%s] %v\n", msg.Role, msg.Content)
}
use modelrelay::{Client, SessionCreateRequest, SessionMessageCreateRequest};
let client = Client::from_secret_key(std::env::var("MODELRELAY_API_KEY")?)?
.build()?;
// Create a remote session (history persisted on server)
let session = client.sessions().create(SessionCreateRequest {
end_user_id: None,
metadata: Some(serde_json::json!({"project": "my-app"})),
}).await?;
// Add messages to the session
client.sessions().add_message(&session.id.to_string(), SessionMessageCreateRequest {
role: "user".into(),
content: vec![serde_json::json!({"type": "text", "text": "What is the capital of France?"})],
run_id: None,
}).await?;
// Get session with full message history
let session_with_messages = client.sessions().get(&session.id.to_string()).await?;
for msg in session_with_messages.messages {
println!("[{}] {:?}", msg.role, msg.content);
}
Local Sessions
Local sessions keep history on the client side. Use for:
- CLI agents (like Claude Code) where history shouldn’t leave the device
- Privacy-sensitive workflows where data must stay local
- Offline-capable agents that work without network
Creating a Local Session
import { ModelRelay, createLocalFSTools } from "@modelrelay/sdk";
const mr = ModelRelay.fromSecretKey(process.env.MODELRELAY_API_KEY!);
const session = mr.sessions.createLocal({
// Tool registry for automatic tool execution
toolRegistry: createLocalFSTools({ root: process.cwd() }),
// Default model for all runs (can override per-run)
defaultModel: "claude-sonnet-4-20250514",
// Persistence mode: "memory" (default), "file", or "sqlite"
persistence: "sqlite",
// Custom storage path (default: ~/.modelrelay/sessions/)
storagePath: "./my-sessions",
// Optional metadata
metadata: { project: "my-app" },
});
Persistence Modes
| Mode | Description | Best For |
|---|---|---|
memory |
In-memory only, lost on exit | Quick scripts, testing |
file |
JSON file per session | Simple persistence |
sqlite |
SQLite database | Multiple sessions, querying |
Resuming a Session
// Resume a previous session by ID
const session = await mr.sessions.resumeLocal("session-uuid", {
persistence: "sqlite",
storagePath: "./my-sessions",
});
if (session) {
console.log(`Resumed with ${session.history.length} messages`);
const result = await session.run("Continue where we left off");
}
Remote Sessions
Remote sessions store history on the server. Use for:
- Cross-device continuity (start on CLI, continue in browser)
- Team collaboration (share sessions between developers)
- Audit trails (persistent record of all interactions)
Creating a Remote Session
const session = await mr.sessions.create({
// Optional metadata stored with the session
metadata: { name: "Feature implementation" },
// Associate with an end user
endUserId: "user-123",
// Tools for client-side execution
toolRegistry: myToolRegistry,
// Default model
defaultModel: "claude-sonnet-4-20250514",
});
console.log(`Session ID: ${session.id}`);
Retrieving a Session
// Get an existing session by ID
const session = await mr.sessions.get("session-uuid");
console.log(`${session.history.length} messages in history`);
// Continue the conversation
const result = await session.run("What did we discuss earlier?");
Listing Sessions
const { sessions, nextCursor } = await mr.sessions.list({
limit: 10,
endUserId: "user-123", // Optional filter
});
for (const info of sessions) {
console.log(`${info.id}: ${info.messageCount} messages`);
}
// Paginate
if (nextCursor) {
const nextPage = await mr.sessions.list({ cursor: nextCursor });
}
Deleting a Session
// Requires a secret key (not publishable key)
await mr.sessions.delete("session-uuid");
Running Prompts
The run() method executes a prompt with full conversation context:
const result = await session.run("Explain the code in src/main.ts", {
// Override the model for this run
model: "claude-sonnet-4-20250514",
// Additional tools for this run (merged with session defaults)
tools: [myCustomTool],
// Maximum LLM turns for tool loops
maxTurns: 10,
// Customer ID for billing attribution
customerId: "cust-123",
// Abort signal for cancellation
signal: controller.signal,
});
Run Results
interface SessionRunResult {
// "complete", "waiting_for_tools", "error", or "canceled"
status: SessionRunStatus;
// Final text output (when status is "complete")
output?: string;
// Pending tool calls (when status is "waiting_for_tools")
pendingTools?: SessionPendingToolCall[];
// Error message (when status is "error")
error?: string;
// Server run ID
runId: RunId;
// Token and call usage
usage: {
inputTokens: number;
outputTokens: number;
totalTokens: number;
llmCalls: number;
toolCalls: number;
};
// All events from this run
events: RunEventV0[];
}
Tool Execution
Sessions integrate with tool registries for automatic tool execution:
import { createLocalFSTools, ToolRegistry } from "@modelrelay/sdk";
// Built-in file system tools
const fsTools = createLocalFSTools({ root: process.cwd() });
// Custom tool registry
const customTools: ToolRegistry = {
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
},
],
async execute(name, args) {
if (name === "get_weather") {
return { temperature: 72, condition: "sunny" };
}
throw new Error(`Unknown tool: ${name}`);
},
};
const session = mr.sessions.createLocal({
toolRegistry: customTools,
defaultModel: "claude-sonnet-4-20250514",
});
// Tools are executed automatically during runs
const result = await session.run("What's the weather in Tokyo?");
Manual Tool Handling
For client-side tools that can’t be auto-executed:
const result = await session.run("Search my local files for TODO comments");
if (result.status === "waiting_for_tools") {
// Execute tools manually
const toolResults = await Promise.all(
result.pendingTools!.map(async (tool) => ({
toolCallId: tool.toolCallId,
output: await executeToolLocally(tool.name, JSON.parse(tool.arguments)),
}))
);
// Submit results to continue the run
const continued = await session.submitToolResults(toolResults);
console.log(continued.output);
}
Session History
Access the full conversation history:
for (const message of session.history) {
console.log(`[${message.role}] ${message.seq}: ${message.createdAt}`);
for (const part of message.content) {
if (part.type === "text") {
console.log(part.text);
}
}
// Messages from runs include the run ID
if (message.runId) {
console.log(` (from run ${message.runId})`);
}
}
Linking Runs to Sessions
Remote sessions can track which workflow runs belong to them:
// Create a session
const session = await mr.sessions.create();
// Runs created through session.run() are automatically linked
const result = await session.run("Analyze the codebase");
// The run_id is recorded with assistant messages
console.log(result.runId);
// When you retrieve the session, messages include run_id references
const retrieved = await mr.sessions.get(session.id);
for (const msg of retrieved.history) {
if (msg.runId) {
// This message came from a workflow run
console.log(`Message from run: ${msg.runId}`);
}
}
API Reference
Sessions API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /sessions |
Create a new session |
| GET | /sessions |
List sessions (with pagination) |
| GET | /sessions/{session_id} |
Get session with messages |
| DELETE | /sessions/{session_id} |
Delete a session |
| POST | /sessions/{session_id}/messages |
Add a message |
| POST | /sessions/{session_id}/runs |
Create a run in session context |
Create Session
POST /api/v1/sessions
Content-Type: application/json
Authorization: Bearer mr_sk_...
{
"end_user_id": "user-123",
"metadata": {
"name": "Feature implementation"
}
}
Response:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "project-uuid",
"end_user_id": "user-123",
"metadata": { "name": "Feature implementation" },
"message_count": 0,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:30:00.000Z"
}
List Sessions
GET /api/v1/sessions?limit=10&end_user_id=user-123
Authorization: Bearer mr_sk_...
Response:
{
"sessions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "project-uuid",
"end_user_id": "user-123",
"metadata": {},
"message_count": 5,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:35:00.000Z"
}
],
"next_cursor": "10"
}
Get Session
GET /api/v1/sessions/{session_id}
Authorization: Bearer mr_sk_...
Response includes full message history:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"project_id": "project-uuid",
"metadata": {},
"message_count": 2,
"created_at": "2025-01-15T10:30:00.000Z",
"updated_at": "2025-01-15T10:35:00.000Z",
"messages": [
{
"id": "msg-uuid-1",
"seq": 1,
"role": "user",
"content": [{ "type": "text", "text": "Hello!" }],
"created_at": "2025-01-15T10:30:00.000Z"
},
{
"id": "msg-uuid-2",
"seq": 2,
"role": "assistant",
"content": [{ "type": "text", "text": "Hello! How can I help?" }],
"run_id": "run-uuid",
"created_at": "2025-01-15T10:30:05.000Z"
}
]
}
Add Message
POST /api/v1/sessions/{session_id}/messages
Content-Type: application/json
Authorization: Bearer mr_sk_...
{
"role": "user",
"content": [{ "type": "text", "text": "What is 2+2?" }],
"run_id": "optional-run-uuid"
}
Best Practices
-
Choose the right session type: Use local for privacy, remote for collaboration
-
Set default models: Configure
defaultModelto avoid specifying it on every run -
Use tool registries: Let sessions handle tool execution automatically
-
Close local sessions: Call
session.close()to flush pending writes -
Handle tool waiting: Check
result.statusand handlewaiting_for_tools -
Paginate large lists: Use
cursorfor listing many sessions -
Use metadata: Store context like project names or user preferences