@helix-agents/core
Core agent framework functionality - types, factories, state management, LLM interface, store interfaces, stream utilities, and orchestration functions.
Installation
npm install @helix-agents/coreAgent Definition
defineAgent
Create an agent configuration.
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 configurationAgent<TState, TOutput>- Shorthand for configured agentLLMConfig- Model configuration (model, temperature, maxOutputTokens, cache, providerOptions,strictOutput).strictOutput?: boolean(defaultfalse) 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.
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 definitionToolConfig<TInput, TOutput>- Tool configurationToolContext- 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.
// 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 withrequireApproval !== 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.
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:
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:
| Option | Type | Description |
|---|---|---|
description | string | Override the tool description (defaults to the agent's description) |
timeoutMs | number | Wall-clock timeout in milliseconds. In the DO runtime, defaults to 10 minutes if not set. |
Types:
SubAgentTool- Sub-agent tool definition (extendsToolwith_isSubAgent,_agentConfig,_timeoutMs)
Tool Utilities
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.
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:
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>-- ExtendsToolwith_isCompanionTool: trueand_companionType: CompanionTypeCompanionType--'spawnAgent' | 'sendMessage' | 'listChildren' | 'getChildStatus' | 'waitForResult' | 'terminateChild'COMPANION_TOOL_PREFIX--'companion__'(the prefix for all companion tool names)
Detection:
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:
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:
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
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'],
});| Field | Type | Description |
|---|---|---|
skills | SkillsConfig | A SkillProvider or an array of SkillDefinition (sugar for inCodeSkillProvider). Unset = no-op. |
preloadSkills | string[] | 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
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
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
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 returnsdefunchanged. Throws on an invalid name/description or empty body.defineAgent()runs this on each entry of an array-formskills.inCodeSkillProvider(skills: SkillDefinition[])— returns a dependency-free, Workers-safeSkillProvider. Throws on a duplicate skill name. Passing an array directly toskillscalls 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 intobuildMessagesForLLM. 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 viaload_skill. The building block for programmatic dedup / round-trip assertions.parseSkillFile(raw: string): ParseResult— parses aSKILL.md(YAML frontmatter + markdown body), validates the metadata againstskillMetadataSchema, and normalizes the open-standardallowed-toolsfield (string or list) tostring[]. 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
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_NAMES — defineTool() 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
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).
import type {
Attachment,
ImageUrlAttachment,
ImageDataAttachment,
FileUrlAttachment,
FileDataAttachment,
} from '@helix-agents/core';Types:
Attachment— Union of all attachment typesImageUrlAttachment—{ 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:
| Identifier | Purpose | Usage |
|---|---|---|
sessionId | Primary key for all state storage operations | Use for loading/saving state, messages, checkpoints |
runId | Execution metadata identifying a specific run | Use 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.,
execute→interrupt→resumecreates 2 runs in 1 session).
Best practices:
- Use
sessionIdfor all state store operations - Pass the same
sessionIdto continue a conversation - Use
runIdfor tracking/tracing specific executions
AgentState
The full state structure for a running agent (used internally for execution and checkpoints).
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
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.
import {
COMMON_METADATA_KEYS,
stripMetadata,
filterByMetadata,
getMessagesWithMetadataKey,
} from '@helix-agents/core';COMMON_METADATA_KEYS
Standard metadata key constants:
| Key | Value | Description |
|---|---|---|
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):
const cleanMessage = stripMetadata(message);
// cleanMessage.metadata is undefinedfilterByMetadata
Filter messages by metadata predicate:
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:
const messagesWithSource = getMessagesWithMetadataKey(messages, 'source');State Helpers
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:
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 notificationAll stream chunks have a common base with optional step field for cleanup targeting:
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.
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
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
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.
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:
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
isStreamStatusEventguard — narrow onevent.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.
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.
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.
interface SuspendedChildWait {
childSessionId: string;
parentToolCallId: string;
spawnedAt: number;
}StepResult
Result from an LLM generation step:
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:
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)- Returnstrueformax_tokens,content_filter,refusal,error,unknownisRecoverableErrorStopReason(reason)- Returnstrueformax_tokensonly. Recoverable error stop reasons can be retried via completion retry when the agent has anoutputSchema(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
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.
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.
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.
// 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.
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 chunkNote:
stepWritesToRFC6902(tracker.getStepWrites())is the only supported path to RFC 6902 patches — it takes the canonicalStepWritesrepresentation and produces correct/-end-of-array paths for pure-append mutations. (The oldconvertImmerPatchToRFC6902Immer-patch helper was removed.)
LLM Module
LLMAdapter Interface
Interface for LLM providers:
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:
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:
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
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.
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:
- On
execute():createRun()is called withturn: 1 - On
resume():createRun()is called with incremented turn number - On completion/failure/interrupt:
updateRunStatus()is called with final status
Query Methods:
getCurrentRun(sessionId): Returns the most recent (active) run for a sessionlistRuns(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:
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
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
import { CustomStateStreamer, createStateStreamer } from '@helix-agents/core';
const streamer = createStateStreamer({
streamManager,
streamId: 'run-123',
});
// Emit state patches
await streamer.emitPatch(patches);State Projection
import { createStateProjection, StreamProjector } from '@helix-agents/core';
// Project subset of state
const projection = createStateProjection<FullState, { count: number }>((state) => ({
count: state.count,
}));Resumable Stream Handler
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:
import type { UserInputMessage, FileInput, AgentInput, SendInput } from '@helix-agents/core';UserInputMessage — A structured user message with optional metadata and file attachments:
interface UserInputMessage {
role: 'user';
content: string;
metadata?: Record<string, unknown>;
files?: FileInput[];
}FileInput — A file attachment (base64-encoded data with media type):
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():
// 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:
type SendInput = string | UserInputMessage[];initializeAgentState
Create initial state from input:
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:
import { buildMessagesForLLM } from '@helix-agents/core';
const messages = buildMessagesForLLM(state.messages, agent.systemPrompt, state.customState);buildEffectiveTools
Get tools including __finish__:
import { buildEffectiveTools } from '@helix-agents/core';
const tools = buildEffectiveTools(agent);planStepProcessing
Analyze LLM result and plan actions:
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:
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.
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 cachingCacheRequest—{ messages, tools, config: LLMConfig, context: { sessionId } }— inputs to a strategyCacheResult—{ messages?, tools?, providerOptions?, headers? }— annotations returned by a strategyAppliedCacheResult— result of folding all strategies;messagesandtoolsare always presentAnthropicCacheOptions—{ ttl?: string }— options foranthropicCache()
Built-in helpers:
| Helper | Provider | Effect |
|---|---|---|
anthropicCache({ ttl? }) | Anthropic (Claude) | Places cache_control markers on the last system message, last tool definition, and rolling conversation breakpoints. Default ttl: '1h'. |
openaiCache() | OpenAI | Sets 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:
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.
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
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:
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:
import {
AgentAlreadyRunningError,
AgentNotResumableError,
FencingTokenMismatchError,
StaleStateError,
ExecutorSupersededError,
StreamTruncatedError,
} from '@helix-agents/core';AgentAlreadyRunningError
Thrown when attempting to execute/resume an agent that is already running.
class AgentAlreadyRunningError extends Error {
readonly sessionId: string;
readonly currentStatus: string;
}AgentNotResumableError
Thrown when attempting to resume an agent that cannot be resumed.
class AgentNotResumableError extends Error {
readonly sessionId: string;
readonly currentStatus: string;
}StaleStateError
Thrown when a state save fails due to version mismatch (optimistic concurrency).
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.
class ExecutorSupersededError extends Error {
readonly sessionId: string;
}FencingTokenMismatchError
Thrown when a fencing token doesn't match expected value (split-brain detection).
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.
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.
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_overloaded→provider).
Error Codes:
| Category | Codes |
|---|---|
provider | provider_overloaded, provider_rate_limited, provider_auth_error, provider_content_filtered, provider_refused, provider_timeout, provider_invalid_request, provider_error |
tool | tool_input_invalid, tool_execution_failed, tool_not_found, tool_timeout |
state | state_concurrency_conflict, state_session_not_found, state_already_running, state_not_resumable |
transport | transport_error |
validation | validation_error |
framework | framework_internal_error, framework_not_supported, framework_cancelled |
classifyError
Convert any error to a typed HelixError:
import { classifyError } from '@helix-agents/core';
const classified = classifyError(unknownError);
// Returns HelixError with appropriate code, category, retryableHandles HelixError (pass-through), AbortError → framework_cancelled, internal agent state errors → specific state codes, generic errors → framework_internal_error.
extractErrorMessage
Extract a string message from any error type:
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:
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
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
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
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:
import { NoOpLockManager } from '@helix-agents/core';
const lockManager = new NoOpLockManager();
// All operations succeed immediatelyLock Errors
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:
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:
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 interruptedResumableStreamStatus (Streams)
Used for stream state in FrontendSnapshot.status:
type ResumableStreamStatus =
| 'active' // Stream is active, events flowing
| 'paused' // Stream is paused
| 'ended' // Stream completed (note: 'ended', not 'completed')
| 'failed'; // Stream failedSubSessionStatusValue (Sub-agents)
Used for tracking sub-agent lifecycle:
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:
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:
SessionStatususes'active'for ready/running state (storage perspective)AgentStatusValueuses'running'for active execution (runtime perspective)ResumableStreamStatususes'ended'for completion (stream lifecycle)
InterruptContext
Context stored when an agent is interrupted:
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:
import { createToolContext } from '@helix-agents/core';
const context = createToolContext({
agentId: 'run-123',
agentType: 'my-agent',
stateTracker,
streamWriter,
});Logger Types
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:
import type { Draft } from '@helix-agents/core';
// Use in updateState callbacks
context.updateState((draft: Draft<MyState>) => {
draft.items.push(newItem);
});