@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' },
});
// Or with multiple messages (for context injection, file attachments, etc.)
const handle = await executor.execute(MyAgent, {
message: [
{
role: 'user',
content: 'Background: user is a premium subscriber',
metadata: { hidden: true },
},
{ role: 'user', content: 'What features do I have access to?' },
],
});
// Messages can include file attachments
const handle = await executor.execute(MyAgent, {
message: [
{
role: 'user',
content: 'Analyze this image',
files: [{ data: 'data:image/png;base64,...', mediaType: 'image/png', filename: 'chart.png' }],
},
],
});Parameters:
agent- Agent configuration fromdefineAgent()input- String message, or{ message: string | UserInputMessage[], 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 multiple messages
const newHandle = await handle.resume({
mode: 'with_message',
message: [
{ role: 'user', content: 'Additional context from the system' },
{ role: 'user', content: 'Please continue with this focus' },
],
});
// 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. Accepts a string or UserInputMessage[].
const handle1 = await executor.execute(agent, 'Hello');
await handle1.result();
const handle2 = await handle1.send('Tell me more');
const result = await handle2.result();
// Or with multiple messages
const handle3 = await handle1.send([
{ role: 'user', content: 'System context: user upgraded to pro', metadata: { source: 'system' } },
{ role: 'user', content: 'What new features can I use?' },
]);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 to prevent concurrent execution of the same session. Lock managers are standalone components — acquire a lock before calling the executor:
import { RedisLockManager } from '@helix-agents/store-redis';
const lockManager = new RedisLockManager(redis);
// Acquire lock before executing
await lockManager.withLock(`session:${sessionId}`, async () => {
await executor.execute(agent, { message: 'Hello' }, { sessionId });
});See Distributed Coordination for details on lock manager implementations.
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.