Skip to content

@helix-agents/core

Core agent framework functionality - types, factories, state management, LLM interface, store interfaces, stream utilities, and orchestration functions.

Installation

bash
npm install @helix-agents/core

Agent Definition

defineAgent

Create an agent configuration.

typescript
import { defineAgent } from '@helix-agents/core';
import { z } from 'zod';

const MyAgent = defineAgent({
  name: 'my-agent',
  description: 'Does something useful',
  systemPrompt: 'You are a helpful assistant.',
  stateSchema: z.object({ count: z.number().default(0) }),
  outputSchema: z.object({ result: z.string() }),
  tools: [myTool],
  llmConfig: { model: openai('gpt-4o') },
  maxSteps: 10,
});

Types:

  • AgentConfig<TStateSchema, TOutputSchema> - Full agent configuration
  • Agent<TState, TOutput> - Shorthand for configured agent
  • LLMConfig - Model configuration (model, temperature, maxOutputTokens, providerOptions)

Tool Definition

defineTool

Create a tool that agents can use.

typescript
import { defineTool } from '@helix-agents/core';
import { z } from 'zod';

const myTool = defineTool({
  name: 'my_tool',
  description: 'Does something',
  inputSchema: z.object({ query: z.string() }),
  outputSchema: z.object({ result: z.string() }),
  execute: async (input, context) => {
    return { result: 'done' };
  },
});

Types:

  • Tool<TInput, TOutput> - Tool definition
  • ToolConfig<TInput, TOutput> - Tool configuration
  • ToolContext - Context passed to execute function

createSubAgentTool

Create a tool that invokes a sub-agent. The sub-agent must have an outputSchema defined.

typescript
import { createSubAgentTool, defineAgent } from '@helix-agents/core';
import { z } from 'zod';

// First, define the sub-agent with an outputSchema
const WorkerAgent = defineAgent({
  name: 'worker-agent',
  systemPrompt: 'You process tasks.',
  outputSchema: z.object({ result: z.string() }),
  llmConfig: { model: openai('gpt-4o') },
});

// Then create a sub-agent tool from it
const delegateTool = createSubAgentTool(
  WorkerAgent, // The agent config (must have outputSchema)
  z.object({ task: z.string() }), // Input schema for the tool
  { description: 'Delegate to worker' } // Optional description override
);

Signature:

typescript
function createSubAgentTool<TInput, TOutputSchema>(
  agent: AgentConfig<any, TOutputSchema>,
  inputSchema: TInput,
  options?: { description?: string }
): SubAgentTool<TInput, TOutputSchema>;

Types:

  • SubAgentTool - Sub-agent tool definition

Tool Utilities

typescript
import {
  SUBAGENT_TOOL_PREFIX, // 'subagent__'
  FINISH_TOOL_NAME, // '__finish__'
  isSubAgentTool, // Check if tool name is sub-agent
  isFinishTool, // Check if tool name is __finish__
  createFinishTool, // Create finish tool from schema
} from '@helix-agents/core';

State Types

Session vs Run Identifiers

The framework uses two identifiers for tracking agent execution:

IdentifierPurposeUsage
sessionIdPrimary key for all state storage operationsUse for loading/saving state, messages, checkpoints
runIdExecution metadata identifying a specific runUse for logging, tracing, debugging

Key distinction:

  • A session is a conversation container. It contains all messages, custom state, and checkpoints.
  • A run is a single execution within a session. When a session is interrupted and resumed, a new run starts but continues the same session.
  • Multiple runs can occur within a single session (e.g., executeinterruptresume creates 2 runs in 1 session).

Best practices:

  • Use sessionId for all state store operations
  • Pass the same sessionId to continue a conversation
  • Use runId for tracking/tracing specific executions

AgentState

The full state structure for a running agent (used internally for execution and checkpoints).

typescript
interface AgentState<TState, TOutput> {
  sessionId: string; // Primary key for session
  runId: string; // Current run identifier
  agentType: string;
  streamId: string;
  status: AgentStatus;
  stepCount: number;
  customState: TState;
  messages: Message[];
  output?: TOutput;
  error?: string;
  parentSessionId?: string;
  subSessionRefs: SubSessionRef[]; // References to child agent runs
  aborted: boolean;
  abortReason?: string;
}

Message Types

typescript
type Message = SystemMessage | UserMessage | AssistantMessage | ToolResultMessage;

interface SystemMessage {
  role: 'system';
  content: string;
  metadata?: Record<string, unknown>; // Optional metadata
}

interface UserMessage {
  role: 'user';
  content: string;
  metadata?: Record<string, unknown>; // Optional metadata
}

interface AssistantMessage {
  role: 'assistant';
  content?: string;
  toolCalls?: ToolCallRequest[];
  thinking?: ThinkingContent;
  metadata?: Record<string, unknown>; // Optional metadata
}

interface ToolResultMessage {
  role: 'tool';
  toolCallId: string;
  toolName: string;
  content: string;
  metadata?: Record<string, unknown>; // Optional metadata
}

Message Metadata

All message types support an optional metadata field for attaching arbitrary data.

typescript
import {
  COMMON_METADATA_KEYS,
  stripMetadata,
  filterByMetadata,
  getMessagesWithMetadataKey,
} from '@helix-agents/core';

COMMON_METADATA_KEYS

Standard metadata key constants:

KeyValueDescription
SOURCE'source'Origin of the message (e.g., 'web-ui', 'api')
HIDDEN'hidden'Whether to hide from UI
TIMESTAMP'timestamp'When the message was created
DURATION'duration'Processing time in ms
MODEL'model'LLM model used
IS_SUB_AGENT'isSubAgent'Whether from a sub-agent
PARENT_SESSION_ID'parentSessionId'Parent session ID for sub-agents
TAGS'tags'Array of tags

stripMetadata

Remove metadata from a message (returns a new object):

typescript
const cleanMessage = stripMetadata(message);
// cleanMessage.metadata is undefined

filterByMetadata

Filter messages by metadata predicate:

typescript
const webMessages = filterByMetadata(messages, (m) => m.source === 'web-ui');
const hiddenMessages = filterByMetadata(messages, (m) => m.hidden === true);

getMessagesWithMetadataKey

Find messages containing a specific metadata key:

typescript
const messagesWithSource = getMessagesWithMetadataKey(messages, 'source');

State Helpers

typescript
import {
  isAssistantMessage,
  isToolResultMessage,
  stripThinking, // Remove thinking from messages
  getSubSessionRefsByType, // Filter sub-agent refs
  getToolResultsFromMessages,
  createInitialAgentState,
} from '@helix-agents/core';

Stream Types

Stream Chunks

All chunk types emitted during agent execution:

typescript
type StreamChunk =
  | TextDeltaChunk // Token-by-token text
  | ThinkingChunk // Reasoning content
  | ToolStartChunk // Tool invocation starting
  | ToolEndChunk // Tool execution complete
  | SubAgentStartChunk // Sub-agent starting
  | SubAgentEndChunk // Sub-agent complete
  | CustomEventChunk // Custom events from tools
  | StatePatchChunk // RFC 6902 state patches
  | ErrorChunk // Error events
  | OutputChunk // Structured output
  | RunInterruptedChunk // Agent interrupted
  | RunResumedChunk // Agent resumed
  | RunPausedChunk // Agent paused for confirmation
  | CheckpointCreatedChunk // Checkpoint saved
  | ExecutorSupersededChunk // Executor superseded
  | StepCommittedChunk // Step changes committed
  | StepDiscardedChunk // Step changes discarded
  | StreamResyncChunk; // Stream recovery notification

All stream chunks have a common base with optional step field for cleanup targeting:

typescript
interface BaseChunk {
  type: string;
  agentId: string;
  agentType: string;
  step?: number; // Step number for cleanup targeting (added for crash recovery)
}

StreamResyncChunk

Emitted when a stream is resynced after crash recovery, rollback, or branching. Clients should use this to refresh their UI state.

typescript
interface StreamResyncChunk extends BaseChunk {
  type: 'stream_resync';
  checkpointId: string; // Checkpoint ID that was restored
  stepCount: number; // Step count at the checkpoint
  messageCount: number; // Message count at the checkpoint
  fromSequence: number; // Stream sequence at the checkpoint
  reason: 'crash_recovery' | 'rollback' | 'branch';
}

Chunk Type Guards

typescript
import {
  isTextDeltaChunk,
  isThinkingChunk,
  isToolStartChunk,
  isToolEndChunk,
  isSubAgentStartChunk,
  isSubAgentEndChunk,
  isCustomEventChunk,
  isStatePatchChunk,
  isErrorChunk,
  isOutputChunk,
  isStreamEnd,
  // Interrupt/Resume type guards
  isRunInterruptedChunk,
  isRunResumedChunk,
  isRunPausedChunk,
  isCheckpointCreatedChunk,
  isExecutorSupersededChunk,
  isStepCommittedChunk,
  isStepDiscardedChunk,
  // Recovery type guard
  isStreamResyncChunk,
} from '@helix-agents/core';

Schemas

typescript
import {
  StreamChunkSchema, // Zod schema for chunks
  StreamMessageSchema, // Zod schema for messages
} from '@helix-agents/core';

Runtime Types

StepResult

Result from an LLM generation step:

typescript
type StepResult<TOutput> =
  | TextStepResult
  | ToolCallsStepResult
  | StructuredOutputStepResult<TOutput>
  | ErrorStepResult;

interface TextStepResult {
  type: 'text';
  content: string;
  thinking?: ThinkingContent;
  shouldStop: boolean;
  stopReason?: StopReason;
}

interface ToolCallsStepResult {
  type: 'tool_calls';
  content?: string;
  toolCalls: ParsedToolCall[];
  subAgentCalls: ParsedSubAgentCall[];
  thinking?: ThinkingContent;
  stopReason?: StopReason;
}

StopReason

Normalized stop reasons from LLM providers:

typescript
type StopReason =
  | 'end_turn' // Normal completion
  | 'stop_sequence' // Hit stop sequence
  | 'tool_use' // Tool call requested
  | 'max_tokens' // Token limit (error)
  | 'content_filter' // Safety filter (error)
  | 'refusal' // Model refused (error)
  | 'error' // Generation error
  | 'unknown'; // Unrecognized

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

Execution Types

typescript
interface AgentExecutionHandle<TOutput> {
  readonly sessionId: string;
  readonly runId: string;
  stream(): Promise<AsyncIterable<StreamChunk> | null>;
  result(): Promise<AgentResult<TOutput>>;
  abort(reason?: string): Promise<void>;
  getState(): Promise<AgentState<unknown, TOutput>>;
  canResume(): Promise<CanResumeResult>;
  resume(options?: ResumeOptions): Promise<AgentExecutionHandle<TOutput>>;
  retry(options?: RetryOptions): Promise<AgentExecutionHandle<TOutput>>;
}

interface AgentResult<TOutput> {
  status: AgentStatus;
  output?: TOutput;
  error?: string;
}

interface CanResumeResult {
  canResume: boolean;
  reason?: string;
}

RetryOptions

Options for the retry() method.

typescript
interface RetryOptions {
  mode?: 'from_checkpoint' | 'from_start';
  checkpointId?: string;
  message?: string;
  abortSignal?: AbortSignal;
}

State Management

ImmerStateTracker

Track state mutations with RFC 6902 patches.

typescript
import { ImmerStateTracker, createImmerStateTracker } from '@helix-agents/core';

const tracker = new ImmerStateTracker(initialState, {
  arrayDeltaMode: 'append_only', // or 'full_replace'
});

// Make mutations
tracker.update((draft) => {
  draft.notes.push({ content: 'New note' });
});

// Get patches
const patches = tracker.getPatches();

// Get current state
const currentState = tracker.getState();

// Reset patch tracking
tracker.resetPatches();

Types:

  • ImmerStateTrackerOptions - Configuration options
  • MergeChanges - Function type for merging state

convertImmerPatchToRFC6902

Convert Immer patches to RFC 6902 format.

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

const rfc6902Patch = convertImmerPatchToRFC6902(immerPatch);

LLM Module

LLMAdapter Interface

Interface for LLM providers:

typescript
interface LLMAdapter {
  generateStep(input: LLMGenerateInput): Promise<StepResult<unknown>>;
}

interface LLMGenerateInput {
  messages: Message[];
  tools: Tool[];
  config: LLMConfig;
  abortSignal?: AbortSignal;
  callbacks?: LLMStreamCallbacks;
  agentId: string;
  agentType: string;
}

interface LLMStreamCallbacks {
  onTextDelta?: (delta: string) => void;
  onThinking?: (content: string, isComplete: boolean) => void;
  onToolCall?: (toolCall: ParsedToolCall) => void;
  onError?: (error: Error) => void;
}

MockLLMAdapter

Mock adapter for testing:

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

const mock = new MockLLMAdapter([
  { type: 'text', content: 'Hello!' },
  { type: 'tool_calls', toolCalls: [{ id: 't1', name: 'search', arguments: {} }] },
  { type: 'structured_output', output: { result: 'done' } },
]);

Stop Reason Mapping

typescript
import {
  mapVercelFinishReason,
  mapOpenAIFinishReason,
  mapAnthropicStopReason,
  mapGeminiFinishReason,
} from '@helix-agents/core';

Store Interfaces

SessionStateStore

Interface for session-centric state persistence. Uses sessionId as the primary key for all operations. Supports atomic operations for safe concurrent modifications from parallel tool execution.

typescript
interface SessionStateStore {
  // Session lifecycle
  createSession<TState>(sessionId: string, options?: CreateSessionOptions<TState>): Promise<void>;
  sessionExists(sessionId: string): Promise<boolean>;
  deleteSession(sessionId: string): Promise<void>;

  // State operations
  loadState<TState, TOutput>(sessionId: string): Promise<SessionState<TState, TOutput> | null>;
  saveState(sessionId: string, state: UntypedSessionState): Promise<void>;

  // Atomic operations (safe for parallel tool execution)
  appendMessages(sessionId: string, messages: Message[]): Promise<void>;
  mergeCustomState(sessionId: string, changes: MergeChanges): Promise<{ warnings: string[] }>;
  updateStatus(
    sessionId: string,
    status: AgentStatus,
    context?: { interruptContext?: InterruptContext }
  ): Promise<void>;
  incrementStepCount(sessionId: string): Promise<number>;

  // Sub-agent management
  addSubSessionRefs(
    sessionId: string,
    refs: Array<{
      subSessionId: string;
      agentType: string;
      parentToolCallId: string;
      startedAt: number;
    }>
  ): Promise<void>;
  updateSubSessionRef(
    sessionId: string,
    update: {
      subSessionId: string;
      status: 'running' | 'completed' | 'failed';
      completedAt?: number;
    }
  ): Promise<void>;
  getSubSessionRefs(sessionId: string): Promise<SubSessionRef[]>;

  // Message queries
  getMessages(sessionId: string, options?: GetMessagesOptions): Promise<PaginatedMessages>;
  getMessageCount(sessionId: string): Promise<number>;

  // Message cleanup (for crash recovery)
  truncateMessages(sessionId: string, messageCount: number): Promise<void>;

  // Checkpoint operations
  getCheckpoint(checkpointId: string): Promise<Checkpoint | null>;
  getLatestCheckpoint(sessionId: string): Promise<Checkpoint | null>;
  listCheckpoints(sessionId: string, options?: ListCheckpointsOptions): Promise<PaginatedCheckpoints>;
  createCheckpoint(sessionId: string, params: CreateCheckpointParams): Promise<string>;

  // Staging operations (for atomic step commits)
  stageChanges(sessionId: string, stepId: string, changes: StagedChanges): Promise<void>;
  getStaged(sessionId: string, stepId: string): Promise<StagedChanges[] | null>;
  promoteStaging(sessionId: string, stepId: string): Promise<void>;
  discardStaging(sessionId: string, stepId: string): Promise<void>;

  // Distributed coordination
  compareAndSetStatus(
    sessionId: string,
    expectedStatuses: AgentStatus[],
    newStatus: AgentStatus,
    context?: { interruptContext?: InterruptContext }
  ): Promise<boolean>;
  incrementResumeCount(sessionId: string): Promise<number>;

  // Run tracking
  createRun(sessionId: string, runId: string, metadata: RunMetadata): Promise<void>;
  updateRunStatus(runId: string, status: RunStatus, updates?: RunStatusUpdate): Promise<void>;
  getCurrentRun(sessionId: string): Promise<RunRecord | null>;
  listRuns(sessionId: string): Promise<RunRecord[]>;
}

interface ListCheckpointsOptions {
  offset?: number;
  limit?: number;
}

### Run Tracking Types

```typescript
// Status of a run
type RunStatus = 'running' | 'completed' | 'failed' | 'interrupted';

// Metadata when creating a run
interface RunMetadata {
  turn: number;          // Turn number (1 for execute, incremented for resume)
  startSequence?: number; // Stream sequence at start
}

// Updates when changing run status
interface RunStatusUpdate {
  stepCount?: number;
  completedAt?: number;
  error?: string;
}

// Full run record
interface RunRecord {
  runId: string;
  sessionId: string;
  turn: number;
  status: RunStatus;
  stepCount: number;
  startedAt: number;
  completedAt?: number;
  error?: string;
}

Run Tracking Lifecycle:

  1. On execute(): createRun() is called with turn: 1
  2. On resume(): createRun() is called with incremented turn number
  3. On completion/failure/interrupt: updateRunStatus() is called with final status

Query Methods:

  • getCurrentRun(sessionId): Returns the most recent (active) run for a session
  • listRuns(sessionId): Returns all runs for a session (for debugging/auditing)

interface PaginatedCheckpoints { items: CheckpointMeta[]; total: number; hasMore: boolean; }

interface GetMessagesOptions { offset?: number; // Starting position (default: 0) limit?: number; // Max messages (default: 50) includeThinking?: boolean; // Include thinking content (default: true) }

interface PaginatedMessages { messages: Message[]; total: number; offset: number; limit: number; hasMore: boolean; }


### StreamManager

Interface for real-time streaming:

```typescript
interface StreamManager {
  // Create a writer for emitting chunks (implicitly creates stream)
  createWriter(streamId: string, runId: string, agentType: string): Promise<StreamWriter>;

  // Create a reader to consume chunks
  createReader(streamId: string): Promise<StreamReader | null>;

  // Create a resumable reader (optional, for crash recovery)
  createResumableReader?(
    streamId: string,
    options?: ResumableReaderOptions
  ): Promise<ResumableStreamReader | null>;

  // Mark stream as complete
  endStream(streamId: string, output?: unknown): Promise<void>;

  // Mark stream as failed
  failStream(streamId: string, error: string): Promise<void>;

  // Stream cleanup (for crash recovery)
  cleanupToStep(streamId: string, stepCount: number): Promise<void>;
  resetStream(streamId: string): Promise<void>;

  // Stream info (for snapshot status)
  getStreamInfo?(streamId: string): Promise<StreamInfo | null>;
}

interface StreamInfo {
  status: 'active' | 'paused' | 'ended' | 'failed';
  latestSequence: number;
  chunkCount: number;
}

interface StreamWriter {
  write(chunk: StreamChunk): Promise<void>;
  close(): Promise<void>; // Closes this writer, NOT the stream
}

interface StreamReader extends AsyncIterable<StreamChunk> {
  [Symbol.asyncIterator](): AsyncIterator<StreamChunk>;
  close(): Promise<void>;
}

interface ResumableStreamReader extends StreamReader {
  readonly currentSequence: number;
  readonly totalChunks: number;
  readonly latestSequence: number;
}

Stream Utilities

Stream Filters

typescript
import {
  filterByAgentId,
  filterByAgentType,
  filterByType,
  excludeTypes,
  filterWith,
  combineStreams,
  take,
  skip,
  collectText,
  collectAll,
} from '@helix-agents/core';

// Filter by agent
const filtered = filterByAgentId(stream, 'agent-123');

// Filter by chunk type
const textOnly = filterByType(stream, ['text_delta']);

// Exclude types
const noThinking = excludeTypes(stream, ['thinking']);

// Collect all text
const fullText = await collectText(stream);

State Streaming

typescript
import { CustomStateStreamer, createStateStreamer } from '@helix-agents/core';

const streamer = createStateStreamer({
  streamManager,
  streamId: 'run-123',
});

// Emit state patches
await streamer.emitPatch(patches);

State Projection

typescript
import { createStateProjection, StreamProjector } from '@helix-agents/core';

// Project subset of state
const projection = createStateProjection<FullState, { count: number }>((state) => ({
  count: state.count,
}));

Resumable Stream Handler

typescript
import { createResumableStreamHandler, extractResumePosition } from '@helix-agents/core';

const handler = createResumableStreamHandler({
  streamManager,
});

// Handle request with resume support
const response = await handler.handle({
  streamId: 'run-123',
  resumeAt: extractResumePosition(lastEventId),
});

Orchestration

initializeAgentState

Create initial state from input:

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

const state = initializeAgentState({
  agent,
  input: 'Hello', // or { message: 'Hello', state: { ... } }
  runId: 'run-123',
  streamId: 'run-123',
  parentSessionId: undefined,
});

buildMessagesForLLM

Prepare messages with system prompt:

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

const messages = buildMessagesForLLM(state.messages, agent.systemPrompt, state.customState);

buildEffectiveTools

Get tools including __finish__:

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

const tools = buildEffectiveTools(agent);

planStepProcessing

Analyze LLM result and plan actions:

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

const plan = planStepProcessing(stepResult, {
  outputSchema: agent.outputSchema,
});

// plan.assistantMessagePlan - For creating assistant message
// plan.pendingToolCalls - Tools to execute
// plan.pendingSubAgentCalls - Sub-agents to invoke
// plan.statusUpdate - Status change to apply
// plan.isTerminal - Whether execution should stop
// plan.output - Parsed output (if __finish__ called)

shouldStopExecution

Check if agent should stop:

typescript
import { shouldStopExecution, determineFinalStatus } from '@helix-agents/core';

const shouldStop = shouldStopExecution(stepResult, stepCount, {
  maxSteps: 10,
  stopWhen: (result) => result.type === 'text' && result.content.includes('DONE'),
});

const finalStatus = determineFinalStatus(stepResult);

Message Builders

typescript
import {
  createAssistantMessage,
  createToolResultMessage,
  createSubAgentResultMessage,
} from '@helix-agents/core';

const assistantMsg = createAssistantMessage(plan.assistantMessagePlan);

const toolResult = createToolResultMessage({
  toolCallId: 'tc1',
  toolName: 'search',
  result: { data: 'found' },
  success: true,
});

Recovery

recoverConversation

Resume a conversation from stored state:

typescript
import { recoverConversation, loadConversationMessages } from '@helix-agents/core';

const { messages, canResume } = await recoverConversation({
  stateStore,
  runId: 'run-123',
});

// Or just load messages
const messages = await loadConversationMessages(stateStore, 'run-123');

Error Types

Agent Errors

Errors for interrupt/resume and distributed coordination:

typescript
import {
  AgentAlreadyRunningError,
  AgentNotResumableError,
  FencingTokenMismatchError,
  StaleStateError,
  ExecutorSupersededError,
} from '@helix-agents/core';

AgentAlreadyRunningError

Thrown when attempting to execute/resume an agent that is already running.

typescript
class AgentAlreadyRunningError extends Error {
  readonly sessionId: string;
  readonly currentStatus: string;
}

AgentNotResumableError

Thrown when attempting to resume an agent that cannot be resumed.

typescript
class AgentNotResumableError extends Error {
  readonly sessionId: string;
  readonly currentStatus: string;
}

StaleStateError

Thrown when a state save fails due to version mismatch (optimistic concurrency).

typescript
class StaleStateError extends Error {
  readonly sessionId: string;
  readonly expectedVersion: number;
  readonly actualVersion: number;
}

ExecutorSupersededError

Thrown when an executor is superseded by another executor. This is a graceful shutdown signal, not a failure.

typescript
class ExecutorSupersededError extends Error {
  readonly sessionId: string;
}

FencingTokenMismatchError

Thrown when a fencing token doesn't match expected value (split-brain detection).

typescript
class FencingTokenMismatchError extends Error {
  readonly sessionId: string;
  readonly expectedToken: number;
}

Checkpoints

Types and utilities for checkpoint management.

Checkpoint Types

typescript
import type { Checkpoint, CheckpointMeta, ParsedCheckpointId } from '@helix-agents/core';

interface Checkpoint<TState = unknown, TOutput = unknown> {
  id: string; // Unique checkpoint ID
  sessionId: string; // Session this checkpoint belongs to
  stepCount: number; // Step count when created
  timestamp: number; // Creation time (ms since epoch)
  state: AgentState<TState, TOutput>; // Complete agent state
  messageCount: number; // Message count at checkpoint (for recovery coordination)
  streamSequence: number; // Stream sequence at checkpoint (for resumption)
}

interface CheckpointMeta {
  id: string;
  sessionId: string;
  stepCount: number;
  timestamp: number;
  status: AgentStatus;
}

Checkpoint ID Utilities

typescript
import {
  generateCheckpointId,
  parseCheckpointId,
  CHECKPOINT_ID_VERSION,
  CHECKPOINT_ID_PREFIX,
} from '@helix-agents/core';

// Generate a new checkpoint ID
const id = generateCheckpointId('session-123', 5);
// Returns: 'cpv1-session-123-s5-t1703123456789-a1b2c3'

// Parse a checkpoint ID
const parsed = parseCheckpointId(id);
// Returns: { version: 1, sessionId: 'session-123', stepCount: 5, timestamp: 1703123456789, random: 'a1b2c3' }

Lock Manager

Interface for distributed lock coordination.

LockManager Interface

typescript
import type {
  LockManager,
  DistributedLock,
  LockAcquisitionResult,
  AcquireOptions,
} from '@helix-agents/core';

interface LockManager {
  readonly holderId: string;
  acquire(resource: string, options: AcquireOptions): Promise<LockAcquisitionResult>;
  extend(lock: DistributedLock, ttlMs: number): Promise<DistributedLock | null>;
  release(lock: DistributedLock): Promise<boolean>;
  withLock<T>(
    resource: string,
    options: { ttlMs: number; heartbeatMs?: number },
    fn: (lock: DistributedLock) => Promise<T>
  ): Promise<T>;
}

interface DistributedLock {
  readonly resource: string;
  readonly lockId: string;
  readonly fencingToken: number; // Monotonic token for split-brain prevention
  readonly acquiredAt: number;
  readonly expiresAt: number;
  readonly holderId: string;
}

interface AcquireOptions {
  ttlMs: number; // Lock TTL in milliseconds
  wait?: boolean; // Wait for lock if held
  waitTimeoutMs?: number; // Max wait time
}

NoOpLockManager

No-op implementation for single-process deployments:

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

const lockManager = new NoOpLockManager();
// All operations succeed immediately

Lock Errors

typescript
import { LockNotAcquiredError, LockLostError } from '@helix-agents/core';

class LockNotAcquiredError extends Error {
  readonly resource: string;
  readonly heldBy: string;
}

class LockLostError extends Error {
  readonly resource: string;
  readonly fencingToken: number;
}

Status Types

The framework uses different status types for different domains:

SessionStatus (Storage)

Used for persistent session state in SessionState.status:

typescript
type SessionStatus =
  | 'active' // Session is ready for execution
  | 'completed' // Session finished successfully
  | 'failed' // Session encountered an error
  | 'interrupted' // User interrupted, resumable
  | 'paused'; // Waiting for input (e.g., tool confirmation)

AgentStatusValue (Runtime)

Used during agent execution:

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

AgentStatusValues.RUNNING; // 'running' - currently executing
AgentStatusValues.COMPLETED; // 'completed' - finished successfully
AgentStatusValues.FAILED; // 'failed' - encountered error
AgentStatusValues.PAUSED; // 'paused' - waiting for confirmation
AgentStatusValues.WAITING_TOOL; // 'waiting_tool' - awaiting tool results
AgentStatusValues.INTERRUPTED; // 'interrupted' - user interrupted

ResumableStreamStatus (Streams)

Used for stream state in FrontendSnapshot.status:

typescript
type ResumableStreamStatus =
  | 'active' // Stream is active, events flowing
  | 'paused' // Stream is paused
  | 'ended' // Stream completed (note: 'ended', not 'completed')
  | 'failed'; // Stream failed

SubSessionStatusValue (Sub-agents)

Used for tracking sub-agent lifecycle:

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

SubSessionStatusValues.RUNNING; // 'running'
SubSessionStatusValues.COMPLETED; // 'completed'
SubSessionStatusValues.FAILED; // 'failed'

Status Conversion

The framework provides helpers to convert between storage and runtime statuses:

typescript
import {
  sessionStatusToAgentStatus,
  agentStatusToSessionStatus,
} from '@helix-agents/core';

// Storage → Runtime
sessionStatusToAgentStatus('active'); // Returns 'running'

// Runtime → Storage
agentStatusToSessionStatus('running'); // Returns 'active'
agentStatusToSessionStatus('waiting_tool'); // Returns 'active'

Key distinction:

  • SessionStatus uses 'active' for ready/running state (storage perspective)
  • AgentStatusValue uses 'running' for active execution (runtime perspective)
  • ResumableStreamStatus uses 'ended' for completion (stream lifecycle)

InterruptContext

Context stored when an agent is interrupted:

typescript
import type { InterruptContext } from '@helix-agents/core';
import { InterruptContextSchema } from '@helix-agents/core';

interface InterruptContext {
  reason?: string; // Why interrupted (e.g., 'user_requested')
  pendingToolCallId?: string; // Tool call waiting for confirmation
  pendingToolName?: string; // Tool name
  stepCount: number; // Step count at interruption
  timestamp: number; // When interrupted
}

// Zod schema for validation
const validated = InterruptContextSchema.parse(context);

Utilities

createToolContext

Create a tool execution context:

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

const context = createToolContext({
  agentId: 'run-123',
  agentType: 'my-agent',
  stateTracker,
  streamWriter,
});

Logger Types

typescript
import { noopLogger, consoleLogger, type Logger } from '@helix-agents/core';

const logger: Logger = {
  debug: (msg, data) => { ... },
  info: (msg, data) => { ... },
  warn: (msg, data) => { ... },
  error: (msg, data) => { ... },
};

Type Re-exports

The package re-exports Draft from Immer for tool authors:

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

// Use in updateState callbacks
context.updateState((draft: Draft<MyState>) => {
  draft.items.push(newItem);
});

Released under the MIT License.