Skip to content

Observer Example

Z-M-Huang edited this page Apr 27, 2026 · 2 revisions

Observer hook (reference)

A minimal reference observer. Demonstrates the canonical shape of an observer hook that records tool-call activity without affecting the turn. Reference only — normative shape and rules live in Hooks (contract).


Purpose

An observer hook fires and returns. It cannot vote, it cannot mutate payload. Its job is to see what happened and write that observation to its own state slot, emit an event, or route to a logger sink.

This example counts tool invocations per tool name across the session. The count is useful for analytics ("how often does the user reach for Bash vs Edit?") without changing any behavior. A hook that did the same counting as a guard or transform would pollute a decision path that should be pure.


Shape

Field Value
Kind Hook.
subKind observer.
Attachment TOOL_CALL/post.
Firing sync or async. This example declares async because the counter write is I/O-bound and the turn should not wait for it.
asyncTimeoutMs 1000. Required for async observers per Hooks § Firing mode.
Validation severity Optional by default per Hooks § Validation severity.
State slot Used for per-tool counters. See Extension State.

Behavior

flowchart LR
    Post[TOOL_CALL/post fires] --> Dispatch[core dispatches observer<br/>on detached task]
    Dispatch -->|turn continues| NextTurn[turn stage moves on]
    Dispatch --> Read[read result.toolName + success flag]
    Read --> Bump[increment state-slot counter]
    Bump --> Emit[publish ToolInvocationObserved event]
    Emit --> Done[observer returns]
Loading

Observer rules visible in the flow:

  1. Async dispatch does not gate the turn. The turn stage continues immediately. If the counter write is slow, the session is not slowed.
  2. The counter lives in this observer's own state slot. An observer may not read or write another extension's slot per Hooks § Interaction with core.
  3. Failures do not affect the turn. A write error emits a HookAsyncFailed audit record but the turn is already past.
  4. Timeout is enforced by core. If the counter write takes longer than asyncTimeoutMs, core cancels the task and audits HookAsyncTimedOut. See Hooks § Firing mode — Cancellation.

Config

Field Meaning Default
enabled Whether the hook participates. true.
events[] Event names to record. ["ToolInvocationCompleted", "ToolInvocationFailed"].
retentionTurns How many turns back to keep per-tool counters. 100. Counters older than this are pruned on the state-slot write path.

What this observer does not do

  • It does not decide anything. A result arrives, the observer records, the turn proceeds. That is the entire role. A hook that tries to "observe and then block on certain conditions" is two hooks: an observer plus a guard.
  • It does not read env. Observers participate in LLM Context Isolation the same way transforms do — they cannot splice env into LLM-visible payload. This observer doesn't touch LLM payload at all.
  • It does not call tools. Only the message loop calls tools.
  • It does not drive the Interaction Protocol. An observer that wanted to ask the user something is the wrong pattern — use a tool that prompts, or an SM stage.

Async observers carry the same capability set as sync observers. Non-blocking is not more permissive — the detached task inherits the observer's constraints.


When to copy this pattern

  • You need to record something about the turn without affecting it.
  • The record can tolerate best-effort semantics — drop on overflow, timeout on stall.
  • The record's value does not gate any decision in the current turn.

Do not use an observer for:

  • Anything that affects the turn — use a transform (reshape) or a guard (block).
  • Durable analytics that must never be dropped — write a Logger sink instead; loggers have bounded queues and drop policy, but they also carry audit records which are never dropped.
  • Long-running background processing — that is a Command or a separate process, not a hook.

Sync vs async

This observer declares async. A sync observer would work too, with trade-offs:

Aspect sync observer async observer
Blocks turn Yes, the turn stage awaits. No, the turn stage continues.
Ordering Participates in the slot's ordering manifest. Does not — fires in registration order.
Cancellation Coupled to the turn. Timeout-bounded via asyncTimeoutMs.
Typical use Cheap work (bump a counter in memory). I/O-bound work (write to disk, emit an event).

Choose sync when the observer is cheap and deterministic ordering matters; choose async when the observer does I/O the turn should not await.


Related pages

Introduction

Reading

Core runtime

Contracts

Category contracts

Context

Security

Runtime behavior

Operations

Providers (bundled)

Integrations

Reference extensions

Tools

UI

Session Stores

Loggers

Providers

Hooks

Context Providers

Commands

Case studies

Flows

Maintainers

Clone this wiki locally