Recorded evidence · Chapter 39

Replay Chapter 39: Where a Decision Can Still Be Stopped

One scripted tool call, three ways: no gate, a gate before the effect, and a rewrite after it.

← Read Seams, Hooks, and Irreversible Decisions
Execution Recordedin process runtime

This is a recording, not a live run. The experiment ran in the book's test harness on 5 October 2026, with a scripted stand-in for the model. Nothing executes in your browser, and nothing here shows how a real model behaves.

Claim

Where a decision can still be stopped

Of the agent core's hooks only beforeToolCall can refuse, and it fires before the effect. A hook after the effect can rewrite what the model is told but cannot undo what happened.

Prediction, stated before the run

With the gate before the effect, execute() runs zero times and the side effect is absent. With a rewrite after the effect, execute() has already run once.

Result: PASS 7 observations · OBSERVED · in process runtime

Evidence

What was observed

ObservationExpectedObservedOutcome
The model proposed the forbidden call (run 2) yes yes held
execute() invocations when the gate blocks 0 0 held
The side effect exists when the gate blocks no no held
The model received the denial as an error result yes yes held
Without a gate, execute() ran 1 1 held
A rewrite after the effect cannot undo it: execute() invocations 1 1 held
tool_execution_start is announced before the gate decides (run 2) yes yes held

Trace

What happened, in order

1. No gate

No gate. The call goes through.

  1. core hook prepareRequest: before the provider is called
    2.2 ms
  2. core hook transformContext: what is sent can still change
    2.4 ms
  3. model the model is asked
    3.6 ms
  4. loop tool_execution_start is announced BEFORE beforeToolCall decides
    4.8 ms
  5. core hook beforeToolCall sees write_file(.env): allow. Nothing has run yet
    8.5 ms
  6. effect execute() RAN and wrote .env
    8.7 ms
  7. core hook afterToolCall: the tool has already finished
    8.8 ms
  8. loop tool_execution_end: isError=false
    8.8 ms
  9. core hook finishTurn: the turn's tools are finished
    9 ms
  10. core hook prepareRequest: before the provider is called
    9.1 ms
  11. core hook transformContext: what is sent can still change
    9.1 ms
  12. model the model is asked again and receives: an ok result
    9.2 ms
  13. core hook finishTurn: the turn's tools are finished
    9.4 ms
  14. result execute() invocation count: 1
    9.5 ms

2. Gate before the effect

beforeToolCall blocks the call.

  1. core hook prepareRequest: before the provider is called
    0.2 ms
  2. core hook transformContext: what is sent can still change
    0.2 ms
  3. model the model is asked
    0.2 ms
  4. loop tool_execution_start is announced BEFORE beforeToolCall decides
    0.4 ms
  5. core hook beforeToolCall sees write_file(.env): BLOCK. Nothing has run yet
    0.6 ms
  6. loop tool_execution_end: isError=true
    0.7 ms
  7. core hook finishTurn: the turn's tools are finished
    0.8 ms
  8. core hook prepareRequest: before the provider is called
    0.8 ms
  9. core hook transformContext: what is sent can still change
    0.8 ms
  10. model the model is asked again and receives: an ERROR result
    1 ms
  11. core hook finishTurn: the turn's tools are finished
    1.3 ms
  12. result execute() invocation count: 0
    1.3 ms

3. Rewrite after the effect

afterToolCall rewrites the result, too late.

  1. core hook prepareRequest: before the provider is called
    0.2 ms
  2. core hook transformContext: what is sent can still change
    0.2 ms
  3. model the model is asked
    0.3 ms
  4. loop tool_execution_start is announced BEFORE beforeToolCall decides
    0.5 ms
  5. core hook beforeToolCall sees write_file(.env): allow. Nothing has run yet
    0.5 ms
  6. effect execute() RAN and wrote .env
    0.5 ms
  7. core hook afterToolCall REWRITES the result to [redacted]. The effect already happened
    0.5 ms
  8. loop tool_execution_end: isError=true
    0.6 ms
  9. core hook finishTurn: the turn's tools are finished
    0.6 ms
  10. core hook prepareRequest: before the provider is called
    0.6 ms
  11. core hook transformContext: what is sent can still change
    0.6 ms
  12. model the model is asked again and receives: an ERROR result
    0.7 ms
  13. core hook finishTurn: the turn's tools are finished
    0.8 ms
  14. result execute() invocation count: 1
    0.8 ms

Provenance

Where this came from

node
v25.8.2
pi-agent-core
1.0.2
pi-ai
1.0.2
pi-coding-agent
1.0.2
platform
win32
model
faux (scripted)
test
examples/ch39-seams/seams.test.ts
exporter
examples/experiences/ch39.ts

What this does not show

  • The model is scripted: it proposes the forbidden call because the script says so. This shows what the loop does when a call is proposed, not how often a real model proposes one.
  • One tool and one forbidden path.
Inspect the context supplied by this chapter

Pinned to Pi 1.0.2. The agent core with beforeToolCall, afterToolCall and a scripted provider. Claim class: observed. Source: examples/ch39-seams/seams.test.ts.

The recording runs the real agent loop with a scripted model that proposes a write to a protected path. Compare how many times the tool body ran in each run.