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, cache, providerOptions, strictOutput). strictOutput?: boolean (default false) opts the auto-injected __finish__ tool into the provider's constrained decoding so a capable model emits schema-conforming structured output. Capability-gated and a safe no-op when the model/provider/adapter doesn't support it. See Finishing Agents → Strict structured output.
  • PersistentAgentConfig - Configuration for persistent sub-agents (agent, mode, description)

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

Both Tool and ToolConfig accept an optional strict?: boolean. When true, the LLM adapter asks the model provider to constrain the model's tool arguments to the tool's inputSchema (strict / structured tool use). It is forwarded subject to provider/model capability and is a safe no-op when unsupported — useful for tools with rich/nested inputs and for finishWith tools that need schema-exact output. For an agent's auto-injected __finish__ tool, prefer LLMConfig.strictOutput.

Approval-gated tools (v7)

Set requireApproval to gate tool execution behind a user approval. The runtime emits a tool_approval_request chunk and durably suspends the run until the frontend submits the user's decision via submitToolResult.

typescript
// Static-true form — every invocation pauses
const deleteTool = defineTool({
  name: 'delete_file',
  inputSchema: z.object({ path: z.string() }),
  requireApproval: true,
  execute: async ({ path }) => deleteFile(path),
});

// Function form — gated on runtime args (fail-closed if it throws)
const sendBulkTool = defineTool({
  name: 'send_bulk_email',
  inputSchema: z.object({ to: z.array(z.string()), body: z.string() }),
  requireApproval: ({ to }) => to.length > 50,
  execute: async ({ to, body }) => sendBulkEmail(to, body),
});

requireApproval cannot be combined with execute: 'client' or finishWith: true — the framework rejects those at defineTool() time.

Helpers:

  • isApprovalGatedTool(tool) - type guard returning true for any tool with requireApproval !== undefined && !== false
  • A denied approval synthesizes the canonical message 'Tool call was not approved by the user' as the tool result so the LLM has a stable anchor for the rejection

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
    timeoutMs: 60_000, // Optional wall-clock timeout in ms
  }
);

Signature:

typescript
function createSubAgentTool<TInput extends z.ZodType, TOutputSchema extends z.ZodType>(
  agent: AgentConfig<any, TOutputSchema>,
  inputSchema: TInput,
  options?: { description?: string; timeoutMs?: number }
): SubAgentTool<TInput, TOutputSchema>;

Options:

OptionTypeDescription
descriptionstringOverride the tool description (defaults to the agent's description)
timeoutMsnumberWall-clock timeout in milliseconds. In the DO runtime, defaults to 10 minutes if not set.

Types:

  • SubAgentTool - Sub-agent tool definition (extends Tool with _isSubAgent, _agentConfig, _timeoutMs)

Tool Utilities

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

createFinishTool(outputSchema, options?) accepts an optional second argument { strict?: boolean }. Passing { strict: true } marks the generated __finish__ tool as strict so the provider constrains its output to outputSchema (capability-gated, safe no-op when unsupported). Runtimes derive this from LLMConfig.strictOutput.

Companion Tools (Persistent Sub-Agents)

Auto-injected tools for managing persistent sub-agents. These are added when an agent has persistentAgents configured.

typescript
import {
  COMPANION_TOOL_PREFIX, // 'companion__'
  isCompanionTool, // Check if tool is a companion tool
} from '@helix-agents/core';

import type {
  CompanionTool, // Tool interface with _isCompanionTool marker
  CompanionType, // 'spawnAgent' | 'sendMessage' | 'listChildren' | 'getChildStatus' | 'waitForResult' | 'terminateChild'
} from '@helix-agents/core';

Companion tool creators:

typescript
import {
  createSpawnAgentTool,
  createSendMessageTool,
  createListChildrenTool,
  createGetChildStatusTool,
  createWaitForResultTool,
  createTerminateChildTool,
} from '@helix-agents/core';

These are used internally by buildEffectiveTools() and are not typically called directly. They are auto-injected when an agent's persistentAgents array is populated.

Types:

  • CompanionTool<TInput, TOutput> -- Extends Tool with _isCompanionTool: true and _companionType: CompanionType
  • CompanionType -- 'spawnAgent' | 'sendMessage' | 'listChildren' | 'getChildStatus' | 'waitForResult' | 'terminateChild'
  • COMPANION_TOOL_PREFIX -- 'companion__' (the prefix for all companion tool names)

Detection:

typescript
import { isCompanionTool, COMPANION_TOOL_PREFIX } from '@helix-agents/core';

// Check if a tool is a companion tool
if (isCompanionTool(tool)) {
  console.log(tool._companionType); // e.g., 'spawnAgent'
}

// Check by name prefix
if (toolName.startsWith(COMPANION_TOOL_PREFIX)) {
  const companionType = toolName.slice(COMPANION_TOOL_PREFIX.length);
}

PersistentAgentConfig

Configuration for persistent sub-agents on AgentConfig:

typescript
interface PersistentAgentConfig {
  agent: AgentConfig; // The child agent definition
  mode: 'blocking' | 'non-blocking'; // How the parent interacts
  description?: string; // Optional description for companion tool context
}

Used in AgentConfig.persistentAgents:

typescript
const agent = defineAgent({
  name: 'coordinator',
  persistentAgents: [
    { agent: WorkerAgent, mode: 'blocking' },
    { agent: MonitorAgent, mode: 'non-blocking', description: 'Background monitor' },
  ],
  // ...
});

Skills

Progressive-disclosure skill library. Configure via AgentConfig.skills (and optionally AgentConfig.preloadSkills); the framework appends a catalog to the system prompt and auto-injects the load_skill / read_skill_file tools. See the Skills guide for the conceptual model.

See also: @helix-agents/skill-cli for baking remote skill packages (git repos / Claude plugin marketplaces) into the in-code provider at build time.

AgentConfig.skills / preloadSkills

typescript
import { defineAgent, defineSkill } from '@helix-agents/core';

const agent = defineAgent({
  name: 'assistant',
  systemPrompt: 'You are a helpful assistant.',
  llmConfig: { model },
  // SkillProvider OR an array of in-code SkillDefinitions (sugar for inCodeSkillProvider)
  skills: [
    defineSkill({
      name: 'pdf-processing',
      description: 'Extract text and tables from PDFs. Use when working with PDF files.',
      body: '# PDF processing\n…',
    }),
  ],
  // Names whose full bodies are injected on every step (always in context)
  preloadSkills: ['pdf-processing'],
});
FieldTypeDescription
skillsSkillsConfigA SkillProvider or an array of SkillDefinition (sugar for inCodeSkillProvider). Unset = no-op.
preloadSkillsstring[]Skill names whose full bodies are preloaded into the system prompt every step. Unknown names warn-and-skip. No-op when skills is unset.

Sub-agents do NOT inherit a parent's skills or preloadSkills.

SkillProvider interface

typescript
interface SkillProvider {
  listSkills(): Promise<SkillMetadata[]>; // Level 1 — catalog (framework sorts by name)
  getSkill(name: string): Promise<Skill | null>; // Level 2 — body + resource listing (null if unknown)
  readResource(name: string, path: string, range?: ReadResourceRange): Promise<string | null>; // Level 3 — file contents (null if unknown/forbidden)
}

Skill data types

typescript
import type {
  Skill,
  SkillMetadata,
  SkillDefinition,
  SkillProvider,
  SkillResourceRef,
  ReadResourceRange,
  SkillsConfig,
} from '@helix-agents/core';

// Level-1 metadata: always resident in the system prompt
interface SkillMetadata {
  name: string; // lowercase a-z/0-9, single hyphens, ≤64 chars, no underscores, no 'anthropic'/'claude'
  description: string; // ≤1024 chars; triggers-only ("what + Use when…")
  license?: string;
  compatibility?: string; // ≤500 chars
  metadata?: Record<string, string>;
  allowedTools?: string[]; // open-standard field; parsed + carried, not enforced in v1
}

// Level-2 payload
interface Skill extends SkillMetadata {
  body: string;
  resources?: SkillResourceRef[];
}

// In-code definition (the "plain data" delivery mode)
interface SkillDefinition extends SkillMetadata {
  body: string;
  // Skill-relative path → content (string) or a lazy loader
  resources?: Record<string, string | (() => string | Promise<string>)>;
}

interface SkillResourceRef {
  path: string; // skill-relative, e.g. "references/forms.md"
  description?: string; // reserved for future use
}

// 1-indexed, inclusive line range for read_skill_file
interface ReadResourceRange {
  startLine?: number;
  endLine?: number;
}

// AgentConfig.skills value
type SkillsConfig = SkillProvider | SkillDefinition[];

Helpers

typescript
import {
  defineSkill, // Validate + identity-return an in-code SkillDefinition
  inCodeSkillProvider, // Build a SkillProvider over an in-memory SkillDefinition[]
  resolveSkillsCatalog, // Resolve the system-prompt fragment (catalog + preloaded bodies)
  collectLoadedSkillNames, // Set<string> of skills loaded into a transcript (recovery-safe)
  parseSkillFile, // Parse + validate a SKILL.md (frontmatter + body); pure/Workers-safe
} from '@helix-agents/core';
import type { ParseResult } from '@helix-agents/core';
  • defineSkill(def) — validates name/description/body and returns def unchanged. Throws on an invalid name/description or empty body. defineAgent() runs this on each entry of an array-form skills.
  • inCodeSkillProvider(skills: SkillDefinition[]) — returns a dependency-free, Workers-safe SkillProvider. Throws on a duplicate skill name. Passing an array directly to skills calls this for you.
  • resolveSkillsCatalog(skills, preloadSkills?) — async; resolves the Level-1 catalog plus any preloaded bodies into the system-prompt fragment. Runtimes call this once per run where IO is allowed and thread the result into buildMessagesForLLM. Returns '' for no skills; a throwing provider warns and returns '' (never crashes the agent).
  • collectLoadedSkillNames(messages) — pure, recovery-safe scan of durable message history returning the set of skill names successfully loaded via load_skill. The building block for programmatic dedup / round-trip assertions.
  • parseSkillFile(raw: string): ParseResult — parses a SKILL.md (YAML frontmatter + markdown body), validates the metadata against skillMetadataSchema, and normalizes the open-standard allowed-tools field (string or list) to string[]. Pure (no IO) and Workers-safe; shared by the filesystem provider (@helix-agents/skill-fs) and the build-time baker (@helix-agents/skill-cli). Returns { ok: true; metadata; body } | { ok: false; error } (never throws).

Reserved tool names

typescript
import { LOAD_SKILL_TOOL_NAME, READ_SKILL_FILE_TOOL_NAME } from '@helix-agents/core';

LOAD_SKILL_TOOL_NAME; // 'load_skill'
READ_SKILL_FILE_TOOL_NAME; // 'read_skill_file'

Both are in RESERVED_TOOL_NAMESdefineTool() throws if a user tool uses either name. Skill names use [a-z0-9-] (no underscores) so they can never collide with the tool names either.

Schema

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

// Zod schema validating SkillMetadata (name/description rules, optional fields).
const parsed = skillMetadataSchema.safeParse(frontmatter);

Attachment Types

Types and utilities for multimodal tool attachments (images, files).

typescript
import type {
  Attachment,
  ImageUrlAttachment,
  ImageDataAttachment,
  FileUrlAttachment,
  FileDataAttachment,
} from '@helix-agents/core';

Types:

  • Attachment — Union of all attachment types
  • ImageUrlAttachment{ type: 'image-url', url: string, mediaType?: string }
  • ImageDataAttachment{ type: 'image-data', data: string, mediaType: string }
  • FileUrlAttachment{ type: 'file-url', url: string, mediaType?: string }
  • FileDataAttachment{ type: 'file-data', data: string, mediaType: string }

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>;
  providerOptions?: Record<string, Record<string, unknown>>;
}

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

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

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

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
MEMORY_INJECTION'memoryInjection'Marks a persisted hidden user message containing auto-injected memories (see collectInjectedMemoryIds)

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';

Stream Event Wire Types

The StreamEvent discriminated union is the wire format used by StreamManager implementations for SSE / WebSocket / pub-sub transport: chunk | end | fail | status | truncated.

typescript
import {
  // Types
  type StreamEvent,
  type StreamChunkEvent,
  type StreamEndEvent,
  type StreamFailEvent,
  type StreamStatusEvent, // { type: 'status'; status: 'paused' | 'active' }
  type StreamTruncatedEvent, // G4 truncation signal (see below)
  // Schemas
  StreamEventSchema,
  StreamChunkEventSchema,
  StreamEndEventSchema,
  StreamFailEventSchema,
  StreamStatusEventSchema,
  StreamTruncatedEventSchema,
  // Helpers
  parseStreamEvent,
  parseNamedSSEEvent,
  serializeStreamEvent,
  // Type guards
  isStreamChunkEvent,
  isStreamEndEvent,
  isStreamFailEvent,
  isStreamTruncatedEvent,
} from '@helix-agents/core';

StreamTruncatedEvent is the push-transport counterpart of G4 (see StreamManager and the stream-protocol internals doc). Its shape mirrors StreamTruncatedError:

typescript
interface StreamTruncatedEvent {
  type: 'truncated';
  // The `stepCount` boundary passed to `cleanupToStep`; chunks with
  // `step > truncatedAtStep` were removed.
  truncatedAtStep: number;
  // The stream's sequence high-water mark at the moment of cleanup; readers
  // gate the throw on `atSequence > lastYieldedSequence`. Optional for wire
  // compatibility.
  atSequence?: number;
}

Note: there is no isStreamStatusEvent guard — narrow on event.type === 'status' directly.

Runtime Types

RunOutcome (v7)

Discriminated union returned by every runtime's runLoop. The variants 'suspended_*' represent the v7 stateless suspension boundaries — at each, the runLoop EXITS and the next resume creates a fresh execution.

typescript
type RunOutcome<TState, TOutput> =
  | { kind: 'completed'; finalState: AgentState<TState, TOutput>; output?: TOutput }
  | { kind: 'failed'; finalState: AgentState<TState, TOutput>; error: string }
  | { kind: 'interrupted'; finalState: AgentState<TState, TOutput>; reason?: string }
  | { kind: 'aborted'; finalState: AgentState<TState, TOutput>; reason?: string }
  | {
      kind: 'suspended_client_tool';
      finalState: AgentState<TState, TOutput>;
      pendingClientToolCalls: Record<string, PendingClientToolCall>;
    }
  | {
      kind: 'suspended_awaiting_children';
      finalState: AgentState<TState, TOutput>;
      suspendedAwaitingChildren: Record<string, SuspendedChildWait>;
    }
  | {
      kind: 'suspended_step_partial';
      finalState: AgentState<TState, TOutput>;
      suspendedStepId: string;
    };

StepOutcome (v7)

Per-step result from runStepIteration(). Used by core orchestration helpers and surfaced into RunOutcome once the loop terminates.

typescript
type StepOutcome<TState, TOutput> =
  | { kind: 'continue' }
  | { kind: 'stop'; reason: 'completed' | 'failed' | 'interrupted' | 'aborted' | 'suspended_*'; ... };

SuspendedChildWait (v7)

Entry in SessionState.suspendedAwaitingChildren. Records that a parent session is durably blocked on a sub-agent child completing. Keyed by parentToolCallId so parallel siblings don't collide.

typescript
interface SuspendedChildWait {
  childSessionId: string;
  parentToolCallId: string;
  spawnedAt: number;
}

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, isRecoverableErrorStopReason } from '@helix-agents/core';
  • isErrorStopReason(reason) - Returns true for max_tokens, content_filter, refusal, error, unknown
  • isRecoverableErrorStopReason(reason) - Returns true for max_tokens only. Recoverable error stop reasons can be retried via completion retry when the agent has an outputSchema (the model wrote text instead of calling the completion tool, and a correction message can nudge it to use the tool). Non-recoverable errors (content_filter, refusal, error, unknown) cannot be fixed by retrying.

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 {
  /**
   * Which checkpoint to restore from. Defaults to the latest checkpoint.
   * If an explicit checkpointId is provided but cannot be resolved, retry()
   * throws rather than silently falling back to a genesis restart.
   */
  checkpointId?: string;
  /** Replacement message. If omitted, the original triggering message is preserved. */
  message?: string | UserInputMessage[];
  /** Abort signal for the retried execution. */
  abortSignal?: AbortSignal;
  /** Optional usage store for tracking token/tool usage on the retried run. */
  usageStore?: UsageStore;
}

State Management

ImmerStateTracker

Track state mutations with RFC 6902 patches.

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

const tracker = new ImmerStateTracker(initialState, {
  arrayDeltaMode: true, // enables parallel-safe append classification (default: false)
});

When arrayDeltaMode is true, getStepWrites() classifies pure-append array mutations as {kind: 'append', key, items: delta} instead of {kind: 'replace', key, value: fullArray}. Use when this tracker may run concurrently with other trackers against the same baseline (e.g., parallel server tools). All four runtimes use arrayDeltaMode: true.

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

// Get step writes (canonical representation for persistence + wire)
const stepWrites = tracker.getStepWrites();

// Get RFC 6902 patches derived from step writes (for streaming)
const patches = stepWritesToRFC6902(stepWrites);

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

// Reset tracking between steps
tracker.resetTracking();

Types:

  • ImmerStateTrackerOptions - Configuration options (arrayDeltaMode?: boolean)
  • StepWrites - Unified per-tool write representation (ops + warnings)
  • WriteOp - Discriminated union: {kind: 'append', key, items} | {kind: 'replace', key, value} | {kind: 'delete', key}

stepWritesToRFC6902

Convert StepWrites to RFC 6902 patch operations for state_patch stream chunks.

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

// Preferred pattern: derive patches from step writes
const stepWrites = tracker.getStepWrites();
const patches = stepWritesToRFC6902(stepWrites);
// patches: JSONPatchOperation[] — ready to emit in a state_patch chunk

Note: stepWritesToRFC6902(tracker.getStepWrites()) is the only supported path to RFC 6902 patches — it takes the canonical StepWrites representation and produces correct /- end-of-array paths for pure-append mutations. (The old convertImmerPatchToRFC6902 Immer-patch helper was removed.)

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;
}

Tool-input coercion helpers

The framework guarantees that tool-call arguments and sub-agent input are always objects, and that structured output is repaired schema-aware. Core enforces this in planStepProcessing(), so every runtime and store is protected regardless of adapter. These helpers are exported for custom adapters and bring-your-own-loop code that produces tool calls at the boundary:

typescript
import { coerceToolCallArguments, repairStructuredOutput } from '@helix-agents/core';

// Always returns a non-null, non-array object.
// - object         → returned unchanged
// - '{"a":1}'      → JSON.parse'd to { a: 1 }
// - anything else  → {} (and logs a warning via the optional logger)
const args: Record<string, unknown> = coerceToolCallArguments(raw, logger);

// Schema-aware repair for structured output (the __finish__ value). Only repairs
// a JSON string when it matches the declared outputSchema; never object-coerces,
// so legitimate non-object outputs (z.string()/z.number()/z.array()) are
// preserved. Never throws — returns the value unchanged when it can't repair.
const output = repairStructuredOutput(raw, outputSchema, logger);

A non-coercible tool input becomes {} and logs [helix-agents] non-object tool-call input coerced to {}; a repaired structured output logs [helix-agents] structured output repaired from JSON string.

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, writes: StepWrites): 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>;

  // Atomic single-write primitive (v7) — saves state, appends messages,
  // promotes staging, and creates a checkpoint in one operation.
  saveStateAndPromoteStaging(
    sessionId: string,
    state: SessionState,
    appendMessages: Message[],
    checkpointMeta: { stepId: string; stepCount: number; streamSequence: number },
    options?: { expectedVersion?: number }
  ): Promise<{ checkpointId: string; newVersion: number }>;

  // Distributed coordination — v7 returns a discriminated union
  compareAndSetStatus(
    sessionId: string,
    expectedStatuses: SessionStatus[],
    newStatus: SessionStatus,
    options?: {
      interruptContext?: InterruptContext;
      error?: string;
      expectedVersion?: number;
    }
  ): Promise<
    | { ok: true; newVersion: number }
    | { ok: false; currentStatus: SessionStatus; currentVersion: number }
  >;
  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)
typescript
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). G4: removing chunks past an
  // ATTACHED reader's cursor surfaces a `StreamTruncatedError` on that
  // reader. Push transports (Cloudflare DO SSE/WS) broadcast a `truncated`
  // wire event; marker/poll transports detect it via a stored marker on the
  // next `next()`. Best-effort on push — a reader attaching after cleanup
  // (fresh replay) won't see it.
  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

Input Types

Types for agent execution input:

typescript
import type { UserInputMessage, FileInput, AgentInput, SendInput } from '@helix-agents/core';

UserInputMessage — A structured user message with optional metadata and file attachments:

typescript
interface UserInputMessage {
  role: 'user';
  content: string;
  metadata?: Record<string, unknown>;
  files?: FileInput[];
}

FileInput — A file attachment (base64-encoded data with media type):

typescript
interface FileInput {
  data: string; // base64 or data URI
  mediaType: string; // e.g., 'image/png', 'application/pdf'
  filename?: string;
}

AgentInput — The input type accepted by execute():

typescript
// String shorthand
type AgentInput = string;

// Or structured input with optional state override
type AgentInput = {
  message: string | UserInputMessage[];
  state?: Partial<TState>;
  messages?: Message[]; // External conversation history
};

SendInput — The input type accepted by handle.send() and resume with_message:

typescript
type SendInput = string | UserInputMessage[];

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

// Multi-message input
const state = initializeAgentState({
  agent,
  input: {
    message: [
      { role: 'user', content: 'Context', metadata: { hidden: true } },
      { role: 'user', content: 'Question' },
    ],
  },
  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);

applyCacheStrategies

Apply one or more cache strategies to messages and tools. Used internally by all runtimes except DBOS when a cache strategy is set on LLMConfig (DBOS does not apply cache strategies — they are not serializable across the durable step boundary). Supply one of the built-in helpers (anthropicCache, openaiCache, xaiCache) or a custom CacheStrategy function.

typescript
import {
  applyCacheStrategies,
  anthropicCache,
  openaiCache,
  xaiCache,
  mergeProviderOptions,
} from '@helix-agents/core';
import type {
  CacheStrategy,
  CacheRequest,
  CacheResult,
  AppliedCacheResult,
} from '@helix-agents/core';

const result = applyCacheStrategies(llmConfig.cache, {
  messages,
  tools,
  config: llmConfig,
  context: { sessionId },
});
// result.messages - Messages with cache annotations
// result.tools - Tools with cache annotations
// result.providerOptions - Provider options to merge (e.g., OpenAI promptCacheKey)
// result.headers - HTTP headers to merge (e.g., xAI x-grok-conv-id)

Caching types:

  • CacheStrategy(request: CacheRequest) => CacheResult — a pure function that annotates a request for prompt caching
  • CacheRequest{ messages, tools, config: LLMConfig, context: { sessionId } } — inputs to a strategy
  • CacheResult{ messages?, tools?, providerOptions?, headers? } — annotations returned by a strategy
  • AppliedCacheResult — result of folding all strategies; messages and tools are always present
  • AnthropicCacheOptions{ ttl?: string } — options for anthropicCache()

Built-in helpers:

HelperProviderEffect
anthropicCache({ ttl? })Anthropic (Claude)Places cache_control markers on the last system message, last tool definition, and rolling conversation breakpoints. Default ttl: '1h'.
openaiCache()OpenAISets providerOptions.openai.promptCacheKey to the session ID for cache affinity.
xaiCache()xAI (Grok)Sets the x-grok-conv-id header to the session ID.

Google / Gemini needs no helper — it uses implicit prefix caching server-side with nothing to annotate.

mergeProviderOptions:

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

// Shallow-merge per-provider option objects without mutating the originals.
const merged = mergeProviderOptions(existing, additions);

collectInjectedMemoryIds

Collect the memory IDs already injected into the transcript via MEMORY_INJECTION-marked messages. Use this to deduplicate auto-loaded memories across turns so the same memory is never injected twice.

typescript
import { collectInjectedMemoryIds, COMMON_METADATA_KEYS } from '@helix-agents/core';

// Returns a Set<string> of all memory IDs already present in the transcript.
const alreadyInjected = collectInjectedMemoryIds(state.messages);

// Typically used alongside MemoryManager.buildInjectionMessage():
const memoryMessage = await memoryManager.buildInjectionMessage({
  query: userMessage,
  context,
  excludeMemoryIds: collectInjectedMemoryIds(state.messages),
});
if (memoryMessage) {
  await stateStore.appendMessages(sessionId, [memoryMessage]);
}

The function scans messages for those marked with COMMON_METADATA_KEYS.MEMORY_INJECTION === true and returns the union of their memoryIds metadata arrays. Runtimes call this once per turn, before the LLM call, to build the dedup set passed to buildInjectionMessage.

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,
  StreamTruncatedError,
} 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;
}

StreamTruncatedError

Thrown on an attached reader whose cursor is past the truncation point when cleanupToStep(N) removes chunks the reader has not yet yielded (G4). The reader's next next() throws; subsequent calls return done. Re-attach via createResumableReader({ fromSequence }) to continue.

typescript
class StreamTruncatedError extends Error {
  readonly streamId: string;
  readonly lastValidSequence: number; // highest sequence yielded before truncation
  readonly truncatedAtStep: number; // stepCount passed to cleanupToStep
}

Error Classification

Unified error classification system for categorizing and handling errors across the framework.

HelixError

Base error class with typed error codes and categories. Uses Symbol.for('helix.agents.error') for cross-package detection.

typescript
import { HelixError } from '@helix-agents/core';
import type { ErrorCode, ErrorCategory, HelixErrorOptions } from '@helix-agents/core';

const error = new HelixError({
  message: 'Provider overloaded',
  code: 'provider_overloaded',
  retryable: true,
  statusCode: 503,
});

error.code; // 'provider_overloaded'
error.category; // 'provider' (auto-derived from code prefix)
error.retryable; // true
error.statusCode; // 503

// Cross-package type check (works across different package versions)
if (HelixError.isInstance(someError)) {
  console.log(someError.code);
}

Types:

  • ErrorCategory'provider' | 'tool' | 'state' | 'transport' | 'validation' | 'framework'
  • ErrorCode — 22 specific codes. Category is derived from the code prefix (e.g., provider_overloadedprovider).

Error Codes:

CategoryCodes
providerprovider_overloaded, provider_rate_limited, provider_auth_error, provider_content_filtered, provider_refused, provider_timeout, provider_invalid_request, provider_error
tooltool_input_invalid, tool_execution_failed, tool_not_found, tool_timeout
statestate_concurrency_conflict, state_session_not_found, state_already_running, state_not_resumable
transporttransport_error
validationvalidation_error
frameworkframework_internal_error, framework_not_supported, framework_cancelled

classifyError

Convert any error to a typed HelixError:

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

const classified = classifyError(unknownError);
// Returns HelixError with appropriate code, category, retryable

Handles HelixError (pass-through), AbortErrorframework_cancelled, internal agent state errors → specific state codes, generic errors → framework_internal_error.

extractErrorMessage

Extract a string message from any error type:

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

extractErrorMessage(new Error('test')); // 'test'
extractErrorMessage({ message: 'Overloaded' }); // 'Overloaded'
extractErrorMessage('plain string'); // 'plain string'
extractErrorMessage(null); // 'null'

ensureError

Convert unknown values to Error instances, preserving the original as cause:

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

const err = ensureError({ message: 'Overloaded' });
err instanceof Error; // true
err.message; // 'Overloaded'
err.cause; // { message: 'Overloaded' }

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.