Example

Map each agent-core hook by what it can still decide

Record the order of the core hooks on 1.0.4 and use the seam matrix to tell a refusal from a rewrite from an observation.

A hook is a real seam only for decisions it can still make when it fires. beforeToolCall is the one hook that can refuse an effect; afterToolCall rewrites the story but cannot undo the deed. This page reproduces the observed order on pi-agent-core 1.0.4, then the seam matrix that says which decisions are still cheap where.

The order, observed

One tool-using run, a recorder on every hook — the exact assertion from the test:

	assert.deepEqual(order, [
		// turn 1: request preparation, then the provider call
		"prepareRequest",
		"transformContext",
		"convertToLlm",
		// the model asked for a tool; execution is announced before the gate runs
		"tool.execute starts",
		"beforeToolCall",
		"afterToolCall",
		"finishTurn",
		"turn_end",
		// turn 2: the same preparation, then the final answer
		"prepareRequest",
		"transformContext",
		"convertToLlm",
		"finishTurn",
		"turn_end",
	]);

Two things are not what you would guess: prepareRequest runs before transformContext, and tool.execute starts is announced before beforeToolCall decides — a UI that draws “running…” on that event draws it for refused calls too. Group the sequence by what has happened:

BEFORE THE REQUEST   prepareRequest, transformContext, convertToLlm
                     nothing has been asked of the model yet
BEFORE THE EFFECT    beforeToolCall
                     the model decided; nothing has run
AFTER THE EFFECT     afterToolCall, finishTurn, prepareNextTurn(...)
                     the tool ran; its consequences exist

What each hook can still decide

The measured rows are observed in seams.test.ts; the “no” / absent-field cells are readings of the declared result types, not runs.

Hook Runs Can transform Can refuse Effect already happened
prepareRequest before each request context, model, thinking level no veto field no
transformContext before conversion messages (sent, not stored) no no
beforeToolCall after validation, before execute() no args field yes no
afterToolCall after execute() the result no yes
finishTurn after the turn’s tools no end or continue yes

The measured cases: a blocked call leaves the tool body’s counter at 0; a rewrite still shows the counter at 1; a transform sends one user message while the transcript keeps two; finishTurn can stop at one request or force a second; prepareRequest swaps the model for one call; and a nested call through runToolCall() passes the same gate, so a blocked one never executes.

Mechanism and limitations

Documented: the nine AgentOptions hooks and their result types, that beforeToolCall runs after tool_execution_start and argument validation, terminate’s batch rule, and runToolCall()’s contract (pi-agent-core README and types.d.ts, 1.0.4). Observed: the order, the block-prevents-effect and rewrite-does-not-undo behaviours, the transform-sends-not-stores behaviour, and the model swap. A mutation of args inside beforeToolCall also lands on 1.0.4 — it works, nothing declares it, so it is not something to build a policy on. Proposed: the grouping of the hooks and the advice to put prevention inside the tool or beneath the process.

Absent fields are not proven impossibilities; they are statements about the type. No hook can undo an effect, because by the time any hook fires the effect is the tool’s own business. This example runs the core under a scripted provider, so it says nothing about a real model’s tool choices.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.