Skip to content

Hooks API Reference ​

Complete type definitions for the hooks system in @helix-agents/core.

Installation ​

Hooks are included in the core package:

bash
npm install @helix-agents/core

Types ​

HookContext ​

Context passed to all hooks with execution info and capabilities.

typescript
interface HookContext<TState = unknown, TOutput = unknown> {
  // Execution Info (read-only)
  readonly sessionId: string;
  readonly agentType: string;
  readonly streamId: string;
  readonly stepCount: number;
  readonly parentSessionId?: string;
  readonly abortSignal: AbortSignal;

  // Session Context (read-only)
  readonly userId?: string;
  readonly tags?: string[];
  readonly metadata?: Record<string, string>;

  // State Methods
  getState: () => TState;
  updateState: (updater: (draft: Draft<TState>) => void) => void;

  // Stream Methods
  emit: (chunk: StreamChunk) => Promise<void>;
  emitCustom: (eventName: string, data: unknown) => Promise<void>;
}

AgentHooks ​

Interface defining all available hooks. All hooks are optional and can be sync or async.

typescript
interface AgentHooks<TState = unknown, TOutput = unknown> {
  // Agent Lifecycle
  onAgentStart?: (
    payload: AgentStartPayload<TState, TOutput>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  onAgentComplete?: (
    payload: AgentCompletePayload<TState, TOutput>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  onAgentFail?: (
    payload: AgentFailPayload<TState>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // A newer run took the session over (AgentResult.status 'superseded'):
  // fires INSTEAD of onAgentComplete / onAgentFail.
  onAgentSuperseded?: (
    payload: AgentSupersededPayload<TState>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // LLM Calls
  beforeLLMCall?: (
    payload: BeforeLLMCallPayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  afterLLMCall?: (
    payload: AfterLLMCallPayload<TOutput>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // Tool Execution
  beforeTool?: (
    payload: BeforeToolPayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  afterTool?: (
    payload: AfterToolPayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // Sub-Agent Execution
  beforeSubAgent?: (
    payload: BeforeSubAgentPayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  afterSubAgent?: (
    payload: AfterSubAgentPayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // State & Messages
  onStateChange?: (
    payload: StateChangePayload<TState>,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  onMessage?: (
    payload: MessagePayload,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;

  // Suspension Lifecycle (v7) — fire across every runtime
  onAgentSuspended?: (ctx: AgentSuspendedHookContext) => void | Promise<void>;

  onAgentResumed?: (ctx: AgentResumedHookContext) => void | Promise<void>;

  // #177: the start signal of a run that retry() began (observe-only; not yet Temporal / DBOS)
  onAgentRetry?: (ctx: AgentRetryHookContext) => void | Promise<void>;

  // Streaming (NOT YET IMPLEMENTED - reserved for future use)
  onTextDelta?: (delta: string, context: HookContext<TState, TOutput>) => void | Promise<void>;

  onThinking?: (
    content: string,
    isComplete: boolean,
    context: HookContext<TState, TOutput>
  ) => void | Promise<void>;
}

HookManager ​

Interface for managing hook registration and invocation.

typescript
interface HookManager<TState = unknown, TOutput = unknown> {
  /** Register a hook handler */
  register(hooks: AgentHooks<TState, TOutput>): void;

  /** Unregister a specific hook handler */
  unregister(hooks: AgentHooks<TState, TOutput>): void;

  /** Invoke a hook with payload and context */
  invoke<K extends keyof AgentHooks<TState, TOutput>>(
    hookName: K,
    ...args: Parameters<NonNullable<AgentHooks<TState, TOutput>[K]>>
  ): Promise<void>;

  /** Create a child manager that inherits parent hooks */
  createChild(): HookManager<TState, TOutput>;

  /** Check if any hooks are registered */
  hasHooks(): boolean;
}

Payload Types ​

AgentStartPayload ​

Data passed to onAgentStart. It fires once per turn (see Hooks: start signals).

typescript
interface AgentStartPayload<TState = unknown, TOutput = unknown> {
  agent: AgentConfig<ZodType<TState>, ZodType<TOutput>>;
  input: UserMessage[];
  initialState: TState;
  /**
   * `true` when onAgentStart is the run's start signal (a new turn); `false` when the
   * run began with another signal (onAgentResumed / onAgentRetry) and owes the turn's
   * start work. Temporal and DBOS always report `true` until #177 MR 2.
   */
  startSignal: boolean;
}

AgentCompletePayload ​

Data passed to onAgentComplete. This hook is guaranteed to fire for all completion paths: __finish__ tool, finishWith tools, and sub-agent completion. See Hook Guarantees for details.

typescript
interface AgentCompletePayload<TState = unknown, TOutput = unknown> {
  output: TOutput;
  finalState: TState;
  stepCount: number;
  durationMs: number;
}

AgentFailPayload ​

Data passed to onAgentFail.

typescript
interface AgentFailPayload<TState = unknown> {
  error: Error;
  finalState: TState;
  stepCount: number;
  durationMs: number;
  recoverable: boolean;
}

AgentSupersededPayload ​

Data passed to onAgentSuperseded. It fires when the run stopped because another entry took the session over, or its own write hit the superseded-run fence (RunSupersededError, code: 'state_run_superseded'). Neither onAgentComplete nor onAgentFail fires for that run, and its finalState is never persisted. One exception: when the supersede lands after onAgentComplete fired and before the run's terminal (K5) commit, onAgentComplete has already fired and onAgentSuperseded follows it; the last terminal hook is authoritative (like onAgentComplete then onAgentFail when the terminal commit fails). A run that fails at its entry (its run-start commit, stream claim or run_started) fires no lifecycle hook at all, and onAgentFail fires only for a run whose start hook (onAgentStart / onAgentResumed) fired. See the upgrade guide.

typescript
interface AgentSupersededPayload<TState = unknown> {
  error: Error; // the typed stop
  finalState: TState; // in-memory state at the stop (never persisted)
  stepCount: number;
  durationMs: number;
}

BeforeLLMCallPayload ​

Data passed to beforeLLMCall.

typescript
interface BeforeLLMCallPayload {
  messages: Message[];
  tools: Tool[];
  config: AgentConfig<any, any>;
  model?: string;
  modelParameters?: ModelParameters;
  // 'work' for ordinary calls, 'forced_completion' during the forced-completion
  // phase (see /guide/finishing-agents#forced-completion).
  executionPhase: ExecutionPhase;
  // The provider-neutral tool-choice directive actually applied to this call.
  // Set to the named `{ type: 'tool', toolName }` on a `forced_tool` forced
  // call; absent under `native_output` and on ordinary calls. Key on
  // `executionPhase` to detect a forced call.
  toolChoice?: LLMToolChoice;
  // The completion strategy applied to a forced call ('forced_tool' |
  // 'native_output'); omitted on ordinary calls.
  completionStrategy?: CompletionStrategy;
  // The capability profile the call's model resolved to (e.g. 'legacy',
  // 'anthropic-5.5'), on every call whose capabilities resolved.
  capabilityProfile?: string;
}

interface ModelParameters {
  temperature?: number;
  maxOutputTokens?: number;
  topP?: number;
  topK?: number;
  [key: string]: unknown;
}

completionStrategy and capabilityProfile are set by runtime-js, the Cloudflare DO, Cloudflare Workflows and Temporal; DBOS does not set them yet. See Finishing Agents → Completion strategies.

AfterLLMCallPayload ​

Data passed to afterLLMCall.

typescript
interface AfterLLMCallPayload<TOutput = unknown> {
  result: StepResult;
  plan: StepProcessingPlan<TOutput>;
  durationMs: number;
  model?: string;
  usage?: TokenUsage;
  completionStrategy?: CompletionStrategy; // as on BeforeLLMCallPayload
  capabilityProfile?: string; // as on BeforeLLMCallPayload
}

interface TokenUsage {
  promptTokens?: number;
  completionTokens?: number;
  totalTokens?: number;
  reasoningTokens?: number;
  cachedTokens?: number;
}

BeforeToolPayload ​

Data passed to beforeTool.

typescript
interface BeforeToolPayload {
  toolCall: ParsedToolCall;
  tool: Tool;
}

interface ParsedToolCall {
  id: string;
  name: string;
  arguments: Record<string, unknown>;
}

AfterToolPayload ​

Data passed to afterTool.

typescript
interface AfterToolPayload {
  toolCall: ParsedToolCall;
  tool: Tool;
  result: unknown;
  success: boolean;
  error?: Error;
  durationMs: number;
}

BeforeSubAgentPayload ​

Data passed to beforeSubAgent.

typescript
interface BeforeSubAgentPayload {
  call: ParsedSubAgentCall;
  agentConfig?: AgentConfig<any, any>; // undefined for remote sub-agents
  parentSessionId: string;
}

interface ParsedSubAgentCall {
  id: string;
  agentType: string;
  input: Record<string, unknown>;
}

AfterSubAgentPayload ​

Data passed to afterSubAgent.

typescript
interface AfterSubAgentPayload {
  call: ParsedSubAgentCall;
  agentConfig?: AgentConfig<any, any>; // undefined for remote sub-agents
  result: unknown;
  success: boolean;
  error?: Error;
  durationMs: number;
  subSessionId: string;
}

StateChangePayload ​

Data passed to onStateChange.

typescript
interface StateChangePayload<TState = unknown> {
  previousState: TState;
  newState: TState;
  source: 'tool' | 'hook' | 'system';
  toolName?: string;
}

MessagePayload ​

Data passed to onMessage.

typescript
interface MessagePayload {
  message: Message;
  messageIndex: number;
}

AgentSuspendedHookContext ​

Data passed to onAgentSuspended. The v7 stateless suspension model fires this hook every time the runLoop reaches a HITL boundary (pending client tool, pending approval, awaiting children) and the runtime returns control to the caller.

typescript
interface AgentSuspendedHookContext {
  sessionId: string;
  runId: string;
  /**
   * Why the run reached a suspension boundary:
   * - 'client_tool'        — waiting for one or more client-executed tool results.
   * - 'awaiting_children'  — waiting for persistent/ephemeral child agents.
   * - 'step_partial'       — a partial step was persisted and the run will resume.
   */
  reason: 'client_tool' | 'awaiting_children' | 'step_partial';
  /** IDs of the tool calls awaiting client submission (empty for non-client_tool reasons). */
  pendingToolCallIds: string[];
  /** Child sessions being awaited (empty for non-awaiting_children reasons). */
  awaitingChildren: SuspendedChildWait[];
  /** Number of steps completed before suspension. */
  stepCount: number;
  hookManager?: HookManager;
  logger?: Logger;
}

AgentResumedHookContext ​

Data passed to onAgentResumed. Fires when a fresh execution starts to resume a previously-suspended run. On Temporal/CFW Workflows this fires inside the new __resume-N workflow instance; on JS/CF DO it fires when the next chat-handler request lands and the runtime reads the suspension context. Errors thrown by this hook are logged but not fatal — resume proceeds even if the hook throws.

typescript
interface AgentResumedHookContext {
  sessionId: string;
  /** New run ID for this resume. */
  runId: string;
  /** The run ID that suspended (if known). */
  previousRunId?: string;
  /** Checkpoint ID restored at resume time (if applicable). */
  resumedFromCheckpointId?: string;
  hookManager?: HookManager;
  logger?: Logger;
}

previousRunId linkage contract

previousRunId is the runId of the run that suspended — always populated on resume across all runtimes (JS, Temporal, Cloudflare, DBOS). Use it for span linkage / audit / tracing-parent identification. Do not use runId !== previousRunId to detect a resume: on DBOS Branch 1 (recv-driven in-place continuation) runId === previousRunId (a self-loop), so the inequality check fails there. See docs/internals/concepts.md > "Resume runId semantics".

AgentRetryHookContext ​

Data passed to onAgentRetry (#177): the start signal of a run that retry() began, on runtime-js, the Cloudflare Durable Object and Cloudflare Workflows (Temporal and DBOS do not fire it until #177 MR 2). It fires after the retried run's run-start commit and before its first step. Observe-only: errors are logged, never fatal. When the restored state owes the turn's start work, onAgentStart follows it with startSignal: false.

typescript
interface AgentRetryHookContext {
  sessionId: string;
  /** The retried run's (new) run ID. */
  runId: string;
  /** The failed run this retry re-runs. */
  retriedRunId: string;
  /** The checkpoint the retry restored, when it restored one (absent: genesis). */
  checkpointId?: string;
  /** Whether the retry carries a new message (`RetryOptions.message`). */
  hasMessage: boolean;
  hookManager?: HookManager;
  logger?: Logger;
}

Factory Functions ​

createHookManager ​

Create a hook manager with optional initial hooks.

typescript
function createHookManager<TState = unknown, TOutput = unknown>(
  initialHooks?: AgentHooks<TState, TOutput>
): HookManager<TState, TOutput>;

Example:

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

const manager = createHookManager({
  onAgentStart: (payload, ctx) => console.log('Starting:', ctx.sessionId),
  afterTool: (payload, ctx) => console.log('Tool done:', payload.tool.name),
});

composeHookManagers ​

Compose multiple hook sets into a single manager. Hooks run in order provided.

typescript
function composeHookManagers<TState = unknown, TOutput = unknown>(
  ...hooksList: (AgentHooks<TState, TOutput> | undefined)[]
): HookManager<TState, TOutput>;

Example:

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

const manager = composeHookManagers(
  createLoggingHooks(logger),
  createMetricsHooks(metrics),
  createAuditHooks(audit)
);

mergeHooks ​

Merge agent-level and execution-level hooks. Agent hooks run first.

typescript
function mergeHooks<TState = unknown, TOutput = unknown>(
  agentHooks?: AgentHooks<TState, TOutput>,
  executionHooks?: AgentHooks<TState, TOutput>,
  existingManager?: HookManager<TState, TOutput>
): HookManager<TState, TOutput>;

Example:

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

const manager = mergeHooks(
  agent.hooks, // Run first
  executionHooks // Run second
);

createHookContext ​

Create a HookContext from options. Used internally by runtimes.

typescript
function createHookContext<TState = unknown, TOutput = unknown>(
  options: CreateHookContextOptions<TState, TOutput>
): HookContext<TState, TOutput>;

interface CreateHookContextOptions<TState = unknown, TOutput = unknown> {
  sessionId: string;
  agentType: string;
  streamId: string;
  stepCount: number;
  parentSessionId?: string;
  abortSignal: AbortSignal;
  getState: () => TState;
  updateState: (updater: (draft: Draft<TState>) => void) => void;
  emit: (chunk: StreamChunk) => Promise<void>;
  userId?: string;
  tags?: string[];
  metadata?: Record<string, string>;
}

Classes ​

DefaultHookManager ​

Primary hook manager implementation with parent-child support.

typescript
class DefaultHookManager<TState = unknown, TOutput = unknown> implements HookManager<
  TState,
  TOutput
> {
  constructor(parent?: HookManager<TState, TOutput>);

  register(hooks: AgentHooks<TState, TOutput>): void;
  unregister(hooks: AgentHooks<TState, TOutput>): void;

  invoke<K extends keyof AgentHooks<TState, TOutput>>(
    hookName: K,
    ...args: Parameters<NonNullable<AgentHooks<TState, TOutput>[K]>>
  ): Promise<void>;

  createChild(): HookManager<TState, TOutput>;
  hasHooks(): boolean;
}

Example:

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

const parent = new DefaultHookManager();
parent.register({ onAgentStart: () => console.log('Parent') });

const child = parent.createChild();
child.register({ onAgentStart: () => console.log('Child') });

// Invoking on child runs both: "Parent" then "Child"
await child.invoke('onAgentStart', payload, context);

NoopHookManager ​

No-op implementation for when hooks are disabled. All methods are no-ops.

typescript
class NoopHookManager<TState = unknown, TOutput = unknown> implements HookManager<TState, TOutput> {
  register(): void;
  unregister(): void;
  invoke(): Promise<void>;
  createChild(): HookManager<TState, TOutput>;
  hasHooks(): boolean; // Always returns false
}

noopHookManager ​

Singleton no-op hook manager instance.

typescript
const noopHookManager: HookManager<any, any>;

Example:

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

// Use when no hooks are configured
const manager = agent.hooks ? createHookManager(agent.hooks) : noopHookManager;

Usage with defineAgent ​

Register hooks at agent definition time:

typescript
import { defineAgent } from '@helix-agents/core';
import type { AgentHooks } from '@helix-agents/core';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

const StateSchema = z.object({ counter: z.number().default(0) });
const OutputSchema = z.object({ result: z.string() });

type State = z.infer<typeof StateSchema>;
type Output = z.infer<typeof OutputSchema>;

const hooks: AgentHooks<State, Output> = {
  onAgentStart: (payload, ctx) => {
    console.log(`Starting with state: ${payload.initialState.counter}`);
  },
  onAgentComplete: (payload, ctx) => {
    console.log(`Output: ${payload.output.result}`);
  },
};

const agent = defineAgent({
  name: 'my-agent',
  systemPrompt: 'You are helpful.',
  stateSchema: StateSchema,
  outputSchema: OutputSchema,
  llmConfig: { model: openai('gpt-6-luna') },
  hooks,
});

Usage with execute ​

Pass hooks at execution time (where executor is a JSAgentExecutor or other runtime executor):

typescript
// Option 1: Pass individual hooks
const handle = await executor.execute(agent, 'Hello', {
  hooks: {
    afterTool: (payload, ctx) => {
      metrics.recordToolCall(payload.tool.name, payload.durationMs);
    },
  },
  sessionId: 'session-123',
  userId: 'user-456',
});

// Option 2: Pass a pre-built hook manager
const handle = await executor.execute(agent, 'Hello', {
  hookManager: myHookManager,
  sessionId: 'session-123',
  userId: 'user-456',
});

Note: Typically use either hooks or hookManager, not both. If both are provided, hooks run in this order: (1) hooks already registered on hookManager, (2) agent-level hooks from defineAgent({ hooks }), (3) execution-time hooks passed here.

Exports ​

All hooks types and utilities are exported from @helix-agents/core:

typescript
import {
  // Types
  type Draft, // Re-exported from 'immer' - needed for updateState
  type HookContext,
  type AgentHooks,
  type HookManager,
  type CreateHookContextOptions,
  type ModelParameters,

  // Payload Types
  type AgentStartPayload,
  type AgentCompletePayload,
  type AgentFailPayload,
  type BeforeLLMCallPayload,
  type AfterLLMCallPayload,
  type BeforeToolPayload,
  type AfterToolPayload,
  type BeforeSubAgentPayload,
  type AfterSubAgentPayload,
  type StateChangePayload,
  type MessagePayload,
  type AgentResumedHookContext,
  type AgentRetryHookContext,

  // Classes
  DefaultHookManager,
  NoopHookManager,

  // Factory Functions
  createHookManager,
  composeHookManagers,
  mergeHooks,
  createHookContext,

  // Singleton
  noopHookManager,
} from '@helix-agents/core';

See Also ​

Released under the MIT License.