Documentation/Long-Running Sessions (Suspend/Resume)
v2.0.0-beta.1Snapshot bebdcd1Markdown ↗

Long-Running Sessions (Suspend/Resume)

Long-Running Sessions (Suspend/Resume)

Some agentic workflows span hours or days. For example, an agent analyzes data, emails the results to a user, and waits for their reply before continuing. The Suspend/Resume feature enables this by letting tools pause the agent loop and external events resume it later.

How It Works

  1. A tool returns a SuspendSignal instead of a normal result
  2. The agent loop adds the display result to context, does a final wrap-up LLM call
  3. Session state (conversation + plugins) is saved automatically
  4. A correlation mapping links the external event ID to the session
  5. AgentResponse returns with status: 'suspended' and full metadata
  6. Later, Agent.hydrate() reconstructs the agent; run(input) continues

Creating a Suspend Tool

typescript
import { SuspendSignal, type ToolFunction } from 'oneringai';

const sendResultsEmail: ToolFunction = {
  definition: {
    type: 'function',
    function: {
      name: 'send_results_email',
      description: 'Email analysis results and wait for user reply',
      parameters: {
        type: 'object',
        properties: {
          to: { type: 'string', description: 'Recipient email' },
          subject: { type: 'string' },
          body: { type: 'string', description: 'Email body with results' },
        },
        required: ['to', 'subject', 'body'],
      },
    },
  },
  execute: async (args) => {
    // Perform the side effect
    const { messageId } = await emailService.send(args.to, args.subject, args.body);

    // Return SuspendSignal — agent loop will pause
    return SuspendSignal.create({
      result: `Email sent to ${args.to} (subject: "${args.subject}"). Waiting for reply.`,
      correlationId: `email:${messageId}`,
      metadata: { messageId, sentTo: args.to },
    });
  },
};

SuspendSignal Options:

Option Type Default Description
result unknown (required) Tool result visible to the LLM
correlationId string (required) ID for routing external events back (e.g., email:msg_123)
resumeAs 'user_message' | 'tool_result' 'user_message' How external input is injected on resume
ttl number 7 days (ms) Time-to-live before the suspended session expires
metadata Record<string, unknown> — App-specific data (email ID, ticket ID, etc.)

Running the Agent

typescript
const agent = Agent.create({
  connector: 'openai',
  model: 'gpt-4.1',
  tools: [sendResultsEmail, analyzeData],
});

const response = await agent.run('Analyze the Q1 sales data and email results to alice@example.com');

if (response.status === 'suspended') {
  console.log('Agent suspended!');
  console.log('Correlation:', response.suspension.correlationId);
  console.log('Session:', response.suspension.sessionId);
  console.log('Expires:', response.suspension.expiresAt);
  // Store the correlation for your webhook handler
}

The AgentResponse.suspension field contains:

typescript
interface SuspensionInfo {
  correlationId: string;    // 'email:msg_123'
  sessionId: string;        // Auto-generated or existing session ID
  agentId: string;          // Agent ID for reconstruction
  resumeAs: 'user_message' | 'tool_result';
  expiresAt: string;        // ISO timestamp
  metadata?: Record<string, unknown>;
}

Resuming a Suspended Session

When the external event arrives (e.g., email reply webhook), reconstruct and resume:

typescript
import { Agent, StorageRegistry } from 'oneringai';
import type { ICorrelationStorage } from 'oneringai';

// In your webhook handler
app.post('/webhooks/email-reply', async (req, res) => {
  const { inReplyTo, body } = req.body;

  // 1. Resolve which session this reply belongs to
  const correlationStorage = StorageRegistry.get('correlations') as ICorrelationStorage;
  const ref = await correlationStorage.resolve(`email:${inReplyTo}`);
  if (!ref) {
    return res.status(404).send('Unknown or expired session');
  }

  // 2. Reconstruct agent from stored definition + session
  const agent = await Agent.hydrate(ref.sessionId, {
    agentId: ref.agentId,
  });

  // 3. Customize — add tools, hooks, etc.
  agent.tools.register(sendResultsEmail);
  agent.setLifecycleHooks({ onError: myErrorHandler });

  // 4. Continue with the user's reply
  const result = await agent.run(body);

  if (result.status === 'suspended' && result.suspension) {
    // Agent suspended again — another email sent, cycle continues
    console.log('Re-suspended:', result.suspension.correlationId);
  } else {
    console.log('Agent completed:', result.output_text);
  }

  res.status(200).send('OK');
});

Agent.hydrate() API

text
static async hydrate(
  sessionId: string,
  options: {
    agentId: string;
    definitionStorage?: IAgentDefinitionStorage;
    overrides?: Partial<AgentConfig>;
  }
): Promise<Agent>

hydrate() is a static method that:

  1. Loads the agent definition via Agent.fromStorage(agentId)
  2. Loads the session state (conversation + plugin states) via loadSession(sessionId)
  3. Cleans up correlation mappings for the session
  4. Returns a fully reconstructed Agent ready for customization and run()

Prerequisites: The agent definition must have been saved via agent.saveDefinition(), and StorageRegistry must have sessions configured (or the agent was created with explicit session storage).

Correlation Storage

By default, correlations are stored as files in ~/.oneringai/correlations/. Configure a custom backend:

typescript
import { StorageRegistry, FileCorrelationStorage } from 'oneringai';

// Use default file-based storage (auto-configured)
// OR configure explicitly:
StorageRegistry.set('correlations', new FileCorrelationStorage({
  baseDirectory: '/custom/path/correlations',
}));

// Or implement ICorrelationStorage for Redis, database, etc.
StorageRegistry.set('correlations', new RedisCorrelationStorage());

ICorrelationStorage interface:

typescript
interface ICorrelationStorage {
  save(correlationId: string, ref: SessionRef): Promise<void>;
  resolve(correlationId: string): Promise<SessionRef | null>;
  delete(correlationId: string): Promise<void>;
  exists(correlationId: string): Promise<boolean>;
  listBySession(sessionId: string): Promise<string[]>;
  listByAgent(agentId: string): Promise<CorrelationSummary[]>;
  pruneExpired(): Promise<number>;
  getPath(): string;
}

Multi-Step Workflows

Suspend/resume naturally supports multi-step workflows. Each run() can either complete or suspend again:

typescript
// Step 1: Agent analyzes data, emails results, suspends
const r1 = await agent.run('Analyze Q1 data and email results');
// r1.status === 'suspended' (email sent, waiting for reply)

// Step 2: User replies, agent processes feedback, sends follow-up, suspends
const agent2 = await Agent.hydrate(r1.suspension.sessionId, { agentId });
agent2.tools.register(sendResultsEmail);
const r2 = await agent2.run('Looks good, but also analyze Q2');
// r2.status === 'suspended' (another email sent)

// Step 3: User replies, agent completes
const agent3 = await Agent.hydrate(r2.suspension.sessionId, { agentId });
agent3.tools.register(sendResultsEmail);
const r3 = await agent3.run('Perfect, thanks!');
// r3.status === 'completed'

Events

The agent emits an execution:suspended event when suspension occurs:

typescript
agent.on('execution:suspended', (event) => {
  console.log('Session suspended:', event.sessionId);
  console.log('Correlation:', event.correlationId);
  console.log('Expires:', event.expiresAt);
});

Housekeeping

Expired correlations can be pruned:

typescript
const correlationStorage = StorageRegistry.get('correlations') as ICorrelationStorage;
const pruned = await correlationStorage.pruneExpired();
console.log(`Pruned ${pruned} expired correlations`);

Documentation snapshot · 2026-11-02Suggest a correction ↗

Search documentation

Type to search the v2 documentation.