Capability: hooks
A
pre,inandposthook 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.invokedandcommand.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 phasespre,inandpost; 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
Noneto pass through. - WHEN a
preexecutor returns a replacement THEN the operation SHALL use it as its input; apreexecutor ontool.invokedcan veto or rewrite one tool call. WHEN apostexecutor returns a replacement THEN it SHALL be the operation's output. - The
inphase 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'sresultreplaces 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 aninexecutor wrapped so it sees only its own command's contexts (a remote executor never receives another command's arguments). A built-inpreexecutor at priority 100 refuses a sender with no role and a watcher's/model <x>,/effort <x>and/sub-task; aHookAbortfrom anyprebecomes the replyrefused: <reason>with the state unchanged; apostreplacement becomes the reply; a body that raises or fails remotely becomes/<name> failed: <message>. Commands and controls do not runrequest.received. - WHEN an operation's body raises or is cancelled after its
prechain ran THEN the runner SHALL callHookManager.abandon, which tells every executor on theprepoint that implementsAbandonObserver(abandon(point, ctx, error), synchronous, its own failure suppressed), so whatpreopened, 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 (toolhooks.<operation>.<phase>); any transport failure or non-conforming reply SHALL raiseHookTransportError, so the operation fails through the retry path andactivity.failedruns. 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.failedandactivity.retriedthrough thedispatch_hooksactivity 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()andworkflow.random().
Design
design.md § Hooks, decision-004.
History
| Work item | What changed | Links |
|---|---|---|
| issue-3 | Hook 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-25 | command.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 post | spec, PR #26, decision-007 |