Skip to content

Capability: hooks ​

A pre, in and post hook point on every operation the harness performs, run as an ordered chain of typed executors, local or remote.

What it is ​

The extension mechanism everything else is built on: observability, retries, policy, compaction and the harness's own default behaviours are hook executors, not core code. Every operation runs through the hook-wrapped operation runner, which is also the body of every Temporal activity. Lives in tiny_harness/harness/hooks/.

Current behaviour ​

  • The catalogue SHALL be the operations request.received, task.created, task.state_changed, task.complete, context.created, llm.invoked, tool_calls.extracted, tool.invoked, system_one.invoked, plan.created, step.started, step.finished, subtask.spawned, help.requested, help.decided, channel.sent, channel.received, compaction.trigger, compaction.keep, compaction.summarise, persistence.read, persistence.write, activity.failed, activity.retried, heartbeat.tick, shutdown, skill.loaded, skill.unloaded, agent.invoked and command.invoked (a slash command, whose context carries the command name, its argument text, the sender, the sender's role and a snapshot of the task), each with the phases pre, in and post; the hook point name is <operation>.<phase>.
  • Executors for one hook point SHALL run as an ordered chain by priority, each receiving the typed context the previous one returned and returning a replacement or None to pass through.
  • WHEN a pre executor returns a replacement THEN the operation SHALL use it as its input; a pre executor on tool.invoked can veto or rewrite one tool call. WHEN a post executor returns a replacement THEN it SHALL be the operation's output.
  • The in phase SHALL be the operation's body: the built-in plugin registers each default body at priority 1000, and an executor with a lower priority that sets the context's result replaces it.
  • WHEN an executor raises a typed abort THEN the operation SHALL be cancelled, the abort recorded, and surfaced as a task failure or a refusal, never as an unhandled exception.
  • Every context SHALL carry the task reference, the participants, the entity references, the correlation id and the attempt number.
  • On command.invoked, each command's body SHALL be an in executor wrapped so it sees only its own command's contexts (a remote executor never receives another command's arguments). A built-in pre executor at priority 100 refuses a sender with no role and a watcher's /model <x>, /effort <x> and /sub-task; a HookAbort from any pre becomes the reply refused: <reason> with the state unchanged; a post replacement becomes the reply; a body that raises or fails remotely becomes /<name> failed: <message>. Commands and controls do not run request.received.
  • WHEN an operation's body raises or is cancelled after its pre chain ran THEN the runner SHALL call HookManager.abandon, which tells every executor on the pre point that implements AbandonObserver (abandon(point, ctx, error), synchronous, its own failure suppressed), so what pre opened, such as a span, is closed.
  • The redactor SHALL scrub the context before any executor sees it and the output before it is returned.
  • Remote executors SHALL be supported over JSON-RPC 2.0 (method = the hook point, params {"context": …}) and over MCP (tool hooks.<operation>.<phase>); any transport failure or non-conforming reply SHALL raise HookTransportError, so the operation fails through the retry path and activity.failed runs. Nothing passes through silently.
  • Hooks SHALL never run in workflow code: the activity wrapper runs the chain around each activity's body, and the workflow runs activity.failed and activity.retried through the dispatch_hooks activity between attempts.
  • The async HTTP client, the clock and the random source SHALL be provider objects given to the harness at construction; workflow code uses Temporal's workflow.now() and workflow.random().

Design ​

design.md § Hooks, decision-004.

History ​

Work itemWhat changedLinks
issue-3Hook points, contexts, executor chain, remote executors, providers (Layer 1); the operation runner (Layer 4); the activity wrapper and workflow-managed retries (Layer 5)spec, PR #7, PR #10, PR #11
issue-25command.invoked (context, per-command body filter, the built-in role pre executor, refusal and failure replies); AbandonObserver and HookManager.abandon close what pre opened when the body never reaches postspec, PR #26, decision-007