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

  1. Choose the right session type: Use local for privacy, remote for collaboration

  2. Set default models: Configure defaultModel to avoid specifying it on every run

  3. Use tool registries: Let sessions handle tool execution automatically

  4. Close local sessions: Call session.close() to flush pending writes

  5. Handle tool waiting: Check result.status and handle waiting_for_tools

  6. Paginate large lists: Use cursor for listing many sessions

  7. Use metadata: Store context like project names or user preferences

Next Steps