Hooks

[TK]

1. Hook Definition

A hook is an interception point in the agent execution lifecycle. Hooks enable logging, validation, transformation, and side effects without modifying core agent logic.

1.1 Available Hooks

The specification defines the following hooks:

HookTrigger PointPurpose
filter_messagesBefore LLM context assemblyFilter/transform message history
prefilter_llm_historyAfter context assemblyFinal adjustments before LLM request
before_create_messageBefore message insertTransform message before storage
after_create_messageAfter message insertSide effects after storage
before_update_messageBefore message updateTransform update data
after_update_messageAfter message updateSide effects after update
before_store_tool_resultBefore tool result storageTransform tool results
after_tool_call_successAfter successful tool callPost-process success results
after_tool_call_failureAfter failed tool callHandle/recover from errors

1.2 Hook Registration

Hooks are identified by their name and MUST implement the signature defined in Section 7:

  • Hook names MUST match one of the defined hook types in Section 1.1
  • Each hook implementation MUST be created using defineHook()
  • Multiple implementations of the same hook MAY exist (composition order is implementation-defined)
  • Hook implementations SHOULD be idempotent where possible
  • Implementations SHOULD enforce timeouts on hook execution
  • Hooks SHOULD NOT perform blocking operations that could timeout execution
  • Hooks MUST NOT expose sensitive data in logs or error messages

2. Hook Context

2.1 ThreadState Parameter

All hooks receive a ThreadState instance as their first parameter. See the Threads specification for the complete interface.

Key properties available during hook execution:

PropertyTypeDescription
threadIdstringUnique thread identifier
agentIdstringAgent that owns this thread
userIdstring | nullAssociated user
executionExecutionStateAlways present in hooks

2.2 Execution State

Since hooks run during agent execution, state.execution is always available:

PropertyTypeDescription
flowIdstringCurrent execution flow identifier
currentSide'a' | 'b'Current execution side
stepCountnumberCurrent step count (LLM cycles)
stoppedbooleanWhether execution has stopped
abortSignalAbortSignalCancellation signal

2.3 Using ThreadState in Hooks

defineHook('filter_messages', async (state, messages) => {
  // Access thread identity
  console.log(`Thread: ${state.threadId}`);

  // Access execution state
  console.log(`Step: ${state.execution.stepCount}`);

  // Load resources
  const agent = await state.loadAgent(state.agentId);

  return messages;
});

3. Message Filtering Hooks

3.1 filter_messages

Called before messages are assembled into LLM context. Receives raw message data from storage.

Signature:

(state: ThreadState, messages: HookMessage[]) => Promise<HookMessage[]>

Use cases:

  • Limit conversation history to N most recent messages
  • Filter out system-only messages
  • Remove messages matching certain patterns
  • Inject synthetic messages

Example:

defineHook('filter_messages', async (state, messages) => {
  // Only include last 20 messages
  return messages.slice(-20);
});

3.2 prefilter_llm_history

Called after messages are transformed into LLM chat format, before the request is sent.

Signature:

(state: ThreadState, messages: LLMMessage[]) => Promise<LLMMessage[]>

Use cases:

  • Add dynamic instructions to context
  • Modify message content before LLM sees it
  • Inject reminders or constraints

Hooks modifying message content SHOULD validate inputs to prevent injection attacks.

Example:

defineHook('prefilter_llm_history', async (state, messages) => {
  // Add reminder to keep responses concise
  const last = messages[messages.length - 1];
  if (last?.role === 'user' && typeof last.content === 'string') {
    last.content += '\n\n(Remember to be concise)';
  }
  return messages;
});

4. Message Lifecycle Hooks

4.1 before_create_message

Called before a message is inserted into storage. Return modified data to transform the message.

Signature:

(state: ThreadState, message: Record<string, unknown>) => Promise<Record<string, unknown>>

Behavior:

  • Implementations MUST call this hook before inserting any message
  • The returned object MUST be used for insertion
  • Failures SHOULD prevent message creation

4.2 after_create_message

Called after a message is successfully inserted. Cannot modify the message.

Signature:

(state: ThreadState, message: Record<string, unknown>) => Promise<void>

Behavior:

  • Implementations MUST call this hook after successful insertion
  • Failures SHOULD NOT affect the created message
  • Use for logging, analytics, or triggering external systems

4.3 before_update_message

Called before a message update is applied. Return modified update data.

Signature:

(state: ThreadState, messageId: string, updates: Record<string, unknown>) => Promise<Record<string, unknown>>

Behavior:

  • Implementations MUST call this hook before updating
  • The returned object MUST be used for the update
  • Failures SHOULD prevent the update

4.4 after_update_message

Called after a message is successfully updated.

Signature:

(state: ThreadState, message: HookMessage) => Promise<void>

Behavior:

  • Implementations MUST call this hook after successful update
  • Failures SHOULD NOT affect the updated message

5. Tool Execution Hooks

5.1 before_store_tool_result

Called before a tool result is stored in the database.

Signature:

(state: ThreadState, toolCall: Record<string, unknown>, toolResult: Record<string, unknown>) => Promise<Record<string, unknown>>

Use cases:

  • Sanitize sensitive data from results
  • Add metadata to tool results
  • Transform result format

5.2 after_tool_call_success

Called after a tool executes successfully. Can return modified result or null for original.

Signature:

(state: ThreadState, toolCall: HookToolCall, toolResult: HookToolResult) => Promise<HookToolResult | null>

Behavior:

  • If hook returns null, the original result is used
  • If hook returns a result, it replaces the original
  • Use for logging, metrics, or result post-processing

5.3 after_tool_call_failure

Called after a tool execution fails. Can return modified error or null for original.

Signature:

(state: ThreadState, toolCall: HookToolCall, toolResult: HookToolResult) => Promise<HookToolResult | null>

Use cases:

  • Error logging and alerting
  • Error recovery attempts
  • Error message transformation

Hooks accessing external services MUST handle failures gracefully.

6. Type Definitions

6.1 HookMessage

interface HookMessage {
  id: string;
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  name?: string | null;
  tool_calls?: string | null;
  tool_call_id?: string | null;
  created_at: number;
  parent_id?: string | null;
  depth?: number;
}

6.2 HookToolCall

interface HookToolCall {
  id: string;
  type: 'function';
  function: {
    name: string;
    arguments: string;
  };
}

6.3 HookToolResult

interface HookToolResult {
  status: 'success' | 'error';
  result?: string;
  error?: string;
  stack?: string;
}

6.4 LLMMessage

interface LLMMessage {
  role: string;
  content: string | null;
  tool_calls?: unknown;
  tool_call_id?: string;
  name?: string;
}

7. TypeScript Reference

/**
 * Hook signatures for all available hooks.
 * All hooks receive ThreadState as their first parameter.
 * See the Threads specification for the ThreadState interface.
 */
interface HookSignatures<
  Message = HookMessage,
  ToolCall = HookToolCall,
  ToolResult = HookToolResult,
> {
  filter_messages: (state: ThreadState, messages: Message[]) => Promise<Message[]>;

  prefilter_llm_history: (
    state: ThreadState,
    messages: LLMMessage[]
  ) => Promise<LLMMessage[]>;

  before_create_message: (
    state: ThreadState,
    message: Record<string, unknown>
  ) => Promise<Record<string, unknown>>;

  after_create_message: (
    state: ThreadState,
    message: Record<string, unknown>
  ) => Promise<void>;

  before_update_message: (
    state: ThreadState,
    messageId: string,
    updates: Record<string, unknown>
  ) => Promise<Record<string, unknown>>;

  after_update_message: (state: ThreadState, message: Message) => Promise<void>;

  before_store_tool_result: (
    state: ThreadState,
    toolCall: Record<string, unknown>,
    toolResult: Record<string, unknown>
  ) => Promise<Record<string, unknown>>;

  after_tool_call_success: (
    state: ThreadState,
    toolCall: ToolCall,
    toolResult: ToolResult
  ) => Promise<ToolResult | null>;

  after_tool_call_failure: (
    state: ThreadState,
    toolCall: ToolCall,
    toolResult: ToolResult
  ) => Promise<ToolResult | null>;
}

/**
 * Valid hook names.
 */
type HookName = keyof HookSignatures;

/**
 * Define a hook with strict typing.
 */
function defineHook<K extends keyof HookSignatures>(
  hookName: K,
  implementation: HookSignatures[K]
): HookSignatures[K];

8. Examples

8.1 Message Context Limiting

import { defineHook } from '@standardagents/spec';

export default defineHook('filter_messages', async (state, messages) => {
  // Keep only the most recent 50 messages
  return messages.slice(-50);
});

8.2 Logging All Tool Calls

import { defineHook } from '@standardagents/spec';

export default defineHook('after_tool_call_success', async (state, toolCall, result) => {
  console.log({
    event: 'tool_success',
    threadId: state.threadId,
    step: state.execution.stepCount,
    tool: toolCall.function.name,
    args: toolCall.function.arguments,
    result: result.result,
    timestamp: Date.now(),
  });
  return null; // Use original result
});

8.3 Error Alerting

import { defineHook } from '@standardagents/spec';

export default defineHook('after_tool_call_failure', async (state, toolCall, result) => {
  // Send alert for critical tool failures
  if (toolCall.function.name === 'payment_process') {
    await sendAlert({
      level: 'critical',
      message: `Payment tool failed: ${result.error}`,
      context: { threadId: state.threadId },
    });
  }
  return null; // Use original error
});

8.4 Adding Metadata to Messages

import { defineHook } from '@standardagents/spec';

export default defineHook('before_create_message', async (state, message) => {
  return {
    ...message,
    metadata: JSON.stringify({
      flow_id: state.execution.flowId,
      step: state.execution.stepCount,
      side: state.execution.currentSide,
    }),
  };
});

8.5 Sanitizing Sensitive Data

import { defineHook } from '@standardagents/spec';

export default defineHook('before_store_tool_result', async (state, toolCall, result) => {
  // Redact credit card numbers from results
  const sanitized = { ...result };
  if (typeof sanitized.result === 'string') {
    sanitized.result = sanitized.result.replace(
      /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
      '****-****-****-****'
    );
  }
  return sanitized;
});