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
- A tool returns a
SuspendSignalinstead of a normal result - The agent loop adds the display result to context, does a final wrap-up LLM call
- Session state (conversation + plugins) is saved automatically
- A correlation mapping links the external event ID to the session
AgentResponsereturns withstatus: 'suspended'and full metadata- Later,
Agent.hydrate()reconstructs the agent;run(input)continues
Creating a Suspend Tool
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
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:
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:
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
static async hydrate(
sessionId: string,
options: {
agentId: string;
definitionStorage?: IAgentDefinitionStorage;
overrides?: Partial<AgentConfig>;
}
): Promise<Agent>
hydrate() is a static method that:
- Loads the agent definition via
Agent.fromStorage(agentId) - Loads the session state (conversation + plugin states) via
loadSession(sessionId) - Cleans up correlation mappings for the session
- Returns a fully reconstructed
Agentready for customization andrun()
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:
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:
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:
// 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:
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:
const correlationStorage = StorageRegistry.get('correlations') as ICorrelationStorage;
const pruned = await correlationStorage.pruneExpired();
console.log(`Pruned ${pruned} expired correlations`);