Skip to content

@helix-agents/runtime-js

JavaScript runtime for in-process agent execution. Non-durable execution suitable for development, testing, and single-process deployments.

Installation

bash
npm install @helix-agents/runtime-js

JSAgentExecutor

The main executor class for running agents in-process.

Constructor

typescript
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.

typescript
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 from defineAgent()
  • input - String message or { message: string, state?: Partial<TState> }

Returns: AgentExecutionHandle<TOutput>

getHandle

Get a handle to an existing execution.

typescript
const handle = await executor.getHandle(MyAgent, 'session-123');

if (handle) {
  const result = await handle.result();
}

Parameters:

  • agent - Agent configuration
  • sessionId - Session identifier

Returns: AgentExecutionHandle | null

canResume

Check if an execution can be resumed.

typescript
const result = await executor.canResume(MyAgent, 'session-123');

if (result.canResume) {
  // Can continue execution
} else {
  console.log('Cannot resume:', result.reason);
}

Returns:

typescript
interface CanResumeResult {
  canResume: boolean;
  reason?: string;
  state?: AgentState;
}

resume

Resume a paused or interrupted execution.

typescript
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

typescript
handle.sessionId; // Unique session identifier (readonly)

stream

Get a stream of events from the execution. Returns null if streaming is not available.

typescript
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.

typescript
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:

typescript
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.

typescript
await handle.abort('User requested cancellation');

interrupt

Interrupt execution for later resumption. This is a SOFT stop - the agent can be resumed.

typescript
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.

typescript
const { canResume, reason } = await handle.canResume();

if (canResume) {
  const resumed = await handle.resume();
}

Returns:

typescript
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.

typescript
// 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:

ModeDescription
continueResume from last checkpoint (default)
with_messageResume with a new user message
with_confirmationResume with data for pending tool
from_checkpointTime-travel to specific checkpoint

retry

Retry a failed execution.

typescript
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:

typescript
retry(options?: RetryOptions): Promise<AgentExecutionHandle<TOutput>>

Throws: Error if not in 'failed' status

RetryOptions

Options for the retry() operation.

typescript
interface RetryOptions {
  mode?: 'from_checkpoint' | 'from_start';
  checkpointId?: string;
  message?: string;
  abortSignal?: AbortSignal;
}
PropertyTypeDefaultDescription
mode'from_checkpoint' | 'from_start''from_checkpoint'How to retry
checkpointIdstringLatestCheckpoint to restore from
messagestringOriginalReplacement message
abortSignalAbortSignal-Abort signal

getState

Get current agent state.

typescript
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.

typescript
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

typescript
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:

typescript
// If LLM requests multiple tool calls, they run concurrently
const tools = [searchTool, fetchTool, analyzeTool];
// All three execute in parallel

Sub-Agent Handling

Sub-agents are executed recursively in the same process:

typescript
// When a sub-agent tool is invoked:
// 1. New execution context is created
// 2. Sub-agent runs to completion
// 3. Result is returned to parent

Abort Handling

Abort signals are propagated to tool executions:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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.

typescript
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 fails
  • retry() - When CAS fails

StaleStateError

Thrown when state version conflicts during save (optimistic locking failure).

typescript
import { StaleStateError } from '@helix-agents/core';

// Indicates another process modified the state
// The current process should stop execution

Limitations

  • 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.

See Also

Released under the MIT License.