@helix-agents/runtime-js
JavaScript runtime for in-process agent execution. Non-durable execution suitable for development, testing, and single-process deployments.
Installation
npm install @helix-agents/runtime-jsJSAgentExecutor
The main executor class for running agents in-process.
Constructor
import { JSAgentExecutor } from '@helix-agents/runtime-js';
const executor = new JSAgentExecutor(
stateStore, // StateStore implementation
streamManager, // StreamManager implementation
llmAdapter // LLMAdapter implementation
);execute
Start executing an agent.
const handle = await executor.execute(MyAgent, 'Hello, agent!');
// Or with initial state
const handle = await executor.execute(MyAgent, {
message: 'Hello',
state: { userId: 'user-123' },
});Parameters:
agent- Agent configuration fromdefineAgent()input- String message or{ message: string, state?: Partial<TState> }
Returns: AgentExecutionHandle<TOutput>
getHandle
Get a handle to an existing execution.
const handle = await executor.getHandle(MyAgent, 'session-123');
if (handle) {
const result = await handle.result();
}Parameters:
agent- Agent configurationsessionId- Session identifier
Returns: AgentExecutionHandle | null
canResume
Check if an execution can be resumed.
const result = await executor.canResume(MyAgent, 'session-123');
if (result.canResume) {
// Can continue execution
} else {
console.log('Cannot resume:', result.reason);
}Returns:
interface CanResumeResult {
canResume: boolean;
reason?: string;
state?: AgentState;
}resume
Resume a paused or interrupted execution.
const handle = await executor.resume(MyAgent, 'session-123', {
additionalMessage: 'Continue with this context',
});AgentExecutionHandle
Handle returned by execute() for interacting with a running agent.
Properties
handle.sessionId; // Unique session identifier (readonly)stream
Get a stream of events from the execution. Returns null if streaming is not available.
const stream = await handle.stream();
if (stream) {
for await (const chunk of stream) {
switch (chunk.type) {
case 'text_delta':
process.stdout.write(chunk.delta);
break;
case 'tool_start':
console.log(`Tool: ${chunk.toolName}`);
break;
case 'tool_end':
console.log(`Result: ${JSON.stringify(chunk.result)}`);
break;
}
}
}result
Wait for the execution to complete and get the result.
const result = await handle.result();
if (result.status === 'completed') {
console.log('Output:', result.output);
} else if (result.status === 'failed') {
console.error('Error:', result.error);
}Returns:
interface AgentResult<TOutput> {
status: 'running' | 'completed' | 'failed' | 'paused' | 'waiting_tool';
output?: TOutput;
error?: string;
}abort
Cancel the execution. This is a HARD stop - the agent fails and cannot be resumed.
await handle.abort('User requested cancellation');interrupt
Interrupt execution for later resumption. This is a SOFT stop - the agent can be resumed.
await handle.interrupt('user_requested');
// Agent status is now 'interrupted'
const state = await handle.getState();
console.log(state.status); // 'interrupted'The current step is rolled back to the last checkpoint. Use resume() to continue execution.
canResume
Check if execution can be resumed.
const { canResume, reason } = await handle.canResume();
if (canResume) {
const resumed = await handle.resume();
}Returns:
interface CanResumeResult {
canResume: boolean;
reason?: string; // Why resume isn't possible
}resume
Resume interrupted or paused execution. Returns a new handle for the resumed execution.
// Continue from where it stopped
const newHandle = await handle.resume();
// Resume with a new message
const newHandle = await handle.resume({
mode: 'with_message',
message: 'Continue with this context',
});
// Resume with confirmation data
const newHandle = await handle.resume({
mode: 'with_confirmation',
data: { approved: true },
});
// Time-travel to a specific checkpoint
const newHandle = await handle.resume({
mode: 'from_checkpoint',
checkpointId: 'cpv1-session-123-s5-...',
});Resume Modes:
| Mode | Description |
|---|---|
continue | Resume from last checkpoint (default) |
with_message | Resume with a new user message |
with_confirmation | Resume with data for pending tool |
from_checkpoint | Time-travel to specific checkpoint |
retry
Retry a failed execution.
const result = await handle.result();
if (result.status === 'failed') {
const retryHandle = await handle.retry();
// Or with options
const retryHandle = await handle.retry({
message: 'Let me try again...',
});
}Signature:
retry(options?: RetryOptions): Promise<AgentExecutionHandle<TOutput>>Throws: Error if not in 'failed' status
RetryOptions
Options for the retry() operation.
interface RetryOptions {
mode?: 'from_checkpoint' | 'from_start';
checkpointId?: string;
message?: string;
abortSignal?: AbortSignal;
}| Property | Type | Default | Description |
|---|---|---|---|
mode | 'from_checkpoint' | 'from_start' | 'from_checkpoint' | How to retry |
checkpointId | string | Latest | Checkpoint to restore from |
message | string | Original | Replacement message |
abortSignal | AbortSignal | - | Abort signal |
getState
Get current agent state.
const state = await handle.getState();
console.log('Status:', state.status);
console.log('Step count:', state.stepCount);
console.log('Messages:', state.messages.length);send
Continue the conversation after completion. Returns a new handle.
const handle1 = await executor.execute(agent, 'Hello');
await handle1.result();
const handle2 = await handle1.send('Tell me more');
const result = await handle2.result();Usage Example
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { defineAgent, defineTool } from '@helix-agents/core';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
// Define agent
const MyAgent = defineAgent({
name: 'my-agent',
systemPrompt: 'You are a helpful assistant.',
outputSchema: z.object({ response: z.string() }),
tools: [
defineTool({
name: 'greet',
description: 'Generate a greeting',
inputSchema: z.object({ name: z.string() }),
outputSchema: z.object({ greeting: z.string() }),
execute: async ({ name }) => ({ greeting: `Hello, ${name}!` }),
}),
],
llmConfig: { model: openai('gpt-4o-mini') },
});
// Create executor
const executor = new JSAgentExecutor(
new InMemoryStateStore(),
new InMemoryStreamManager(),
new VercelAIAdapter()
);
// Execute agent
const handle = await executor.execute(MyAgent, 'Greet John');
// Stream results
for await (const chunk of (await handle.stream()) ?? []) {
if (chunk.type === 'text_delta') {
process.stdout.write(chunk.delta);
}
}
// Get final result
const result = await handle.result();
console.log('\nOutput:', result.output);Behavior
Parallel Tool Execution
The JS runtime executes independent tool calls in parallel:
// If LLM requests multiple tool calls, they run concurrently
const tools = [searchTool, fetchTool, analyzeTool];
// All three execute in parallelSub-Agent Handling
Sub-agents are executed recursively in the same process:
// When a sub-agent tool is invoked:
// 1. New execution context is created
// 2. Sub-agent runs to completion
// 3. Result is returned to parentAbort Handling
Abort signals are propagated to tool executions:
defineTool({
execute: async (input, context) => {
// Check abort signal
if (context.abortSignal?.aborted) {
throw new Error('Execution aborted');
}
// Long-running operation
await doWork();
},
});Interrupt and Resume
The JS runtime supports full interrupt/resume functionality:
// Start execution
const handle = await executor.execute(MyAgent, 'Research AI');
// Later, interrupt
await handle.interrupt('user_requested');
// Even later, resume
const { canResume } = await handle.canResume();
if (canResume) {
const resumed = await handle.resume();
const result = await resumed.result();
}Crash Recovery
With a persistent state store (Redis), execution can be resumed after process restarts:
import { RedisStateStore, RedisStreamManager } from '@helix-agents/store-redis';
const executor = new JSAgentExecutor(
new RedisStateStore({ host: 'localhost' }),
new RedisStreamManager({ host: 'localhost' }),
new VercelAIAdapter()
);
// After crash, reconnect to existing session
const handle = await executor.getHandle(MyAgent, savedSessionId);
if (handle) {
const { canResume } = await handle.canResume();
if (canResume) {
const resumed = await handle.resume();
}
}Distributed Coordination
For multi-process deployments, use a lock manager:
import { RedisLockManager } from '@helix-agents/store-redis';
const executor = new JSAgentExecutor(stateStore, streamManager, llmAdapter, {
lockManager: new RedisLockManager(redis),
});This prevents race conditions when multiple processes try to resume the same agent.
Concurrency Safety
The JS runtime prevents concurrent executions on the same session.
AgentAlreadyRunningError
Thrown when attempting to execute/resume/retry a session that is already running or when a concurrent operation has claimed it.
import { AgentAlreadyRunningError } from '@helix-agents/core';
try {
await executor.execute(agent, message, { sessionId });
} catch (error) {
if (error instanceof AgentAlreadyRunningError) {
console.log(`Session ${error.sessionId} is already running`);
console.log(`Current status: ${error.currentStatus}`);
}
}Thrown by:
execute()- When session status is 'running'resume()- When CAS failsretry()- When CAS fails
StaleStateError
Thrown when state version conflicts during save (optimistic locking failure).
import { StaleStateError } from '@helix-agents/core';
// Indicates another process modified the state
// The current process should stop executionLimitations
- No built-in durability - Requires persistent stores for crash recovery
- Single process - No automatic distribution across workers
- No timeout isolation - Tool timeouts must be handled manually
For production workloads requiring built-in durability, consider @helix-agents/runtime-temporal.