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' },
});

// 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 from defineAgent()
  • input - String message, or { message: string | UserInputMessage[], 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 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:

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. Accepts a string or UserInputMessage[].

typescript
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

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 to prevent concurrent execution of the same session. Lock managers are standalone components — acquire a lock before calling the executor:

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

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.