1 1 2 3 5 8 13 21 34 55 89 144 φ ≈ 1.6180339887… fibon design log 中文 fibon design log 中文 ✕ Chapters Field Notes Experiments & Analysis 01 Why I'm Building My Own AI Assistant 02 Is One AI Not Enough? 03 How Does an AI "Remember" You? 04 Why an LLM's Confidence Isn't Evidence 05 Can an AI Modify Its Own Source Code? 06 How Do You Safely Run Untrusted Code? 07 Not a Plan, but a Pause Button I Could Actually Press * Where this chapter really started: a pause button that works * A kitchen choice you make every day * The weak spots of the common ReAct pattern (Style A) * How fibon builds Style B: planning and execution, fully separated * Assembly line vs. autopilot: two independent axes * The three-round amendment cap: why the review stage needs a circuit breaker * Giving a rigid plan its flexibility back: three structured interventions * No free lunch: what Plan-Execute costs you * Implementation details
🔒 How Did fibon Grow, One Piece at a Time? Planned (2026-07) 🔒 UI/UX — The Last Mile to Open Source, and the Part That Hurt Most Planned (2026-07) Hoping to finish 😭 🔒 The Things I Still Haven't Figured Out Planned (2026-07) 🔒 A White-Box, Auditable Distributed Observability Layer Planned · open-source (Jul 2026) 🔒 Cross-AI Communication — The A2A Protocol and a Distributed Trust Layer Planned · open-source (Jul 2026) 🔒 LLM Token Economics and Stratified-RAG Cost Optimization Planned · open-source (Jul 2026) 🔒 Twelve Lines of Steel Defense Planned · open-source (Jul 2026) 🔒 Subject Attribution and a Project-Scoped Epistemic Container Planned · open-source (Jul 2026) 🔒 The Boundaries, Physical Limits, and Epistemology of AI Self-Evolution Planned · open-source (Jul 2026) 🔒 The Behavioral Psychology of Cross-Vendor Models and Per-Vendor Harness Practice Planned · open-source (Jul 2026) 🔒 The Pre-Orchestrated Workflow Engine — a Third Path for Recurring Tasks Planned · open-source (Jul 2026) 🔒 The Industry Spectrum of Execution Control — Stopping a Mid-Run Agent in 2026 Planned · open-source (Jul 2026) 2026-06 Zombie Caches and Stolen Keys: A Teardown of Two Runaway AI Bills 2026-06 Runaway Sub-Agents: The June 2 Claude Outage and the Lesson of the Infinite Loop 2026-05 The Attack That Waits: When AI's 'Memory' Becomes the Attack Surface 2026-03 The Lobster's Bill: When an AI Agent's 'Token-Eating Monster' Meets the Abandonment Wave 2026-02 12% of the Marketplace Was Poison: The ClawHub Supply-Chain Attack 2026-01 Tools as Attack Surface: 2026's MCP Vulnerability Cascade and 'Tool Poisoning' 2026-06 It Got More Confident After I Corrected It: a small experiment that grew out of a reader's comment 2026-06 Frameworks Amplify Execution, Not Direction: Reading DeepMind's From AGI to ASI ☰ Chapter 7
The trade-off between ReAct and Plan-Execute, and why sometimes you have to kick the AI out of the execution stage
📅 2026-04 ⏱ 27 min 🍳 Updated 2026-07-08
Quick summary : when to let the AI improvise, when to lock it into a Plan-Execute pipeline, and what that trade-off costs. Skip if : you only want the one-line conclusion: high-risk tasks get a frozen plan, exploratory tasks get free rein. How to read this chapter : we start with where this design really came from (a pause button you can actually press on a running agent) → use an everyday “kitchen recipe” analogy to split the two execution styles → break down the three weak spots of the common think-as-you-go pattern (ReAct) → get to fibon’s answer: wrapping plan-first execution into a
plan_and_executetool with four stages of separated powers, plan → human review → execute (brain steps aside) → synthesize → then add the three “structured interventions” that give a rigid plan its flexibility back → and close by being honest about the three costs of this design. Once you reach “What Plan-Execute costs you,” you’ve seen the core. The closing “Implementation details” collect three appendices (context control on long runs and the “fat turn” bug, a glance at the pre-orchestrated Workflow engine, and why not ToT / LATS) as optional reading.
mid 2026-04 · while designing the plan_and_execute tool The starting point of this architecture may sound backwards: what I wanted first wasn’t a plan. It was a pause. After the previous chapter split the brain (Brain) from the hands (Worker), there was a by-product I didn’t pay much attention to at the time: since every action has to be dispatched from the brain to the hands, there are natural gaps between one action and the next. I stared at that call chain and thought: aren’t these gaps ready-made checkpoints? The scenario I wanted was very concrete. You ask the agent to plan a trip. It starts checking the weather, then hotels. Halfway through, you suddenly think “oh, look up attractions too” — no need to cancel and start over, just drop the new task in. Or you realize you gave the wrong instructions, or left out a condition, so you force-stop the whole thing and keep the results it already fetched. And because every step is dispatched asynchronously, you don’t have to sit there waiting for it to finish before you can speak. Tasks run in the background, you interject whenever you like. That’s real multitasking. Holding all of this up is one implicit requirement: at any moment, the system must be able to answer “which step are we on, and what did each step return?” Auditability comes along for free. But break “pause anytime, add tasks mid-run, force-stop” down and you hit an unavoidable premise: the system has to know what comes next before “where to interrupt” even means anything. ReAct thinks as it goes; until the LLM emits the next step, not even the system knows what it will be. So where would you pause? Between which two steps would you insert a task? Only when the steps are laid out as an explicit plan do the gaps between actions become handles a human can grab. In fibon, “plan first, then execute” was never the goal. It’s the means — a way to make a running AI stoppable at any moment. Once that was clear, the industry’s two execution styles finally had a coordinate system to sit in.
Imagine you’re attempting a foreign dish you’ve never cooked before. You have a recipe. Which style do you cook in? Style A: cook as you read (rolling adjustments). Open the recipe, read line one, “dice the onion” → grab the knife and dice. Read line two, “add tomato paste” → open the fridge and, oops, you’re out of tomato paste. So you take off the apron, turn off the stove, run to the corner store, come back, and read the next line. Upside: highly flexible, great at improvising around surprises. Downside: the overall flow is unpredictable. You may realize halfway through that “I should have started the stock half an hour ago, it’s too late now.” The final dish is luck, and when it goes wrong you can’t tell which step failed. Style B: plan first, then cook (architect thinking). Before lighting the stove, read the recipe cover to cover, rehearse it in your head, list every ingredient, write a precise shopping list, buy everything in one trip, lay out the mise en place, then follow steps one-two-three-four in strict order. Upside: predictable and controllable. No mid-cook scramble over missing ingredients; if the flavor is off, the log makes it easy to pinpoint which step went wrong. Downside: no real-time flexibility, and it feels rigid. If the recipe itself has a misprint (say, an extra zero on the salt), your brain won’t second-guess it; you’ll follow the wrong list to the end. Neither style is inherently good or evil. It’s all about the setting. Cooking familiar home dishes for a few friends on a weekend? Style A is fine. But if you’re the executive chef of a top catering company preparing a hundred dishes for a royal banquet tonight, you must run Style B and operate the whole pipeline on a plan. In most tool-calling interfaces, the most natural way for an LLM to run — and the way it’s most often designed to run — is Style A. But once you put AI into serious, high-frequency personal-assistant production use, Style A’s downsides show their true face. This chapter is about how fibon takes these two styles apart at the foundation and picks between them.
In many agent frameworks, tutorials, and production implementations, the most common tool-execution pattern is some form of ReAct (Reasoning and Acting). It’s Style A as an infinite loop: Aside ReAct (Reason + Act) : a tool-calling pattern where the AI thinks one step, acts one step. The brain reasons about which tool to use, calls it, reads the result, then reasons about the next step, over and over until it decides it’s done. Extremely flexible, well suited to exploratory tasks. But every step is a fresh LLM call, so neither the order nor the cost is predictable.
[ User submits a task ] ──> LLM thinks (Reason) ──> calls tool A (Act) ──> reads the result (Observation)
│
▲ ▼
└──────────────────────────────────────────────── LLM thinks again (Reason)
│
▼ calls tool B (Act)
[ Repeat until the AI decides it's done ]
In this loop, every step the AI takes is a new cloud LLM API call, and the next step is improvised from whatever text the previous tool returned. This “feel the stones as you cross the river” approach looks like genius on exploratory tasks. The textbook example is bug fixing: read the error log → guess which line is wrong → change it → run the tests → a new error appears → adjust again based on the new error. For chaotic tasks that genuinely need to evolve with intermediate results, ReAct fits well. But drop ReAct into a routine, deterministic assistant task and three fatal weaknesses show up: * Weak spot 1: the execution trace isn’t reproducible. An LLM is a statistical machine with randomness built in. Ask it the exact same question twice and the tool-call sequence underneath can come out completely different. For a dead-simple task like “fetch the latest model list from the official site and report back,” you want it to reliably go “fetch page → filter text → synthesize.” Under ReAct, it’s up to fate: this time it fetches the page, next time it runs a Google search, the time after that it fobs you off with stale memory. The trace is uncontrollable. * Weak spot 2: failures land in a black box, and debugging is miserable. In a ReAct loop, the “planning brain” and the “tool-running hands” are kneaded into the same LLM process. When the user sees a wrong answer, you open the logs and can’t tell whether the upstream brain planned wrong on round three, or a midstream external API returned garbage that the brain didn’t notice. It’s all mixed together, and short of re-running, you have nothing. * Weak spot 3: the user loses the right to know, and side-effect APIs fire blind. Every ReAct step calls external APIs immediately, mid-conversation, sight unseen. The human never sees the plan as a whole. If the brain goes off the rails on step five and writes something absurd, then by the time you notice, the side-effect tool has already run in the background. The breakup email is already sent; last month’s payroll row is already wiped. You never even get the chance to click stop.
Aside Side effect : when an action doesn’t just compute a result but changes the outside world in a way you can’t take back — sending an email, charging a card, deleting a record. Side-effect-free queries (reading a page, crunching numbers) can simply be redone if they go wrong. A side-effect tool, once it runs, is spilled water. That’s why a human should see it before it runs.
🟢 Status: implemented. The four-stage
plan_and_executepipeline in this section (Planner → Checkpoint → Dispatch → Synthesize) runs in the repo today; the planning, review, dispatch, and synthesis nodes inplan_execute/nodes.pyare all complete. As of 2026-07-07, the three mid-run interventions (including frontend breakpoint marking and mid-run pause/stop) have full frontend-backend integration. Remaining rough edges are disclosed at the end of the chapter. Following the opening thread — no plan, no interruption points — and along the way shutting off the security risks of ReAct’s blind sprint, fibon made a directional decision: wrap Style B (plan first, then execute) into a core built-in tool namedplan_and_execute, registered in the unified tool registry. Whenever the Butler senses that a task belongs to the serious-assistant category (multi-step, cross-dependent, and heavy with physical side effects — “plan my five-day Kyoto trip with flights and hotels,” “on the 1st of every month, audit all my SaaS renewals and warn me before each charge”), it callsplan_and_executefrom inside its ReAct loop. The moment the call triggers, control passes to the underlying code, and the system switches into a four-stage pipeline where the LLM’s powers are forcibly separated :
[ plan_and_execute triggered ]
│
▼
Stage 1: Planning (Planner) ───> strip the LLM of all tools; it can only type out a structured JSON plan
│
▼
Stage 2: Review (Checkpoint) ──> risk triage: low-risk plans auto-approve (a read-only card keeps you informed)
medium/high-risk plans freeze first; a panel waits for the human verdict
(⚠ current behavior: a 5-minute timeout still auto-approves; not the end state,
see "The three-round amendment cap")
│
▼
Stage 3: Execution (Dispatch) ─> separation of powers: the LLM leaves the execution line; pure code runs the pipeline
│
▼
Stage 4: Synthesis ───────────> wrap-up. The LLM is called back to turn the collected data into natural language
Stage 1: Planning (Planner) — strip the tools, force the brain to be a pure architect. For the call where the brain writes the plan, fibon sends the API request with the tools list set to empty. Under that constraint the LLM loses every privilege to touch browsers or databases. It can’t fumble around; the only intelligence it’s allowed to exercise is writing an honest, structured JSON plan:
{
"needs_worker": true,
"rationale": "The user needs contract research plus a meeting schedule. I must fetch the latest pricing page first, then call the scheduler.",
"steps": [
{ "order": 1, "tool": "read_page", "args": {"url": "https://docs.anthropic.com/v1/pricing"} },
{ "order": 2, "tool": "summarize_text", "args": {"input": "step1.output"} },
{ "order": 3, "tool": "add_schedule", "args": {"title": "Contract review biweekly", "time": "2026-06-09T10:00:00+08:00"} }
]
}
It spells out which tool each step calls, with what arguments, and how one step’s output feeds the next. This plan isn’t written to move a human reader. It’s written for the code dispatcher underneath.
Stage 2: Review (Checkpoint) — freeze the execution line, hand the wheel back to the human. Risk triage comes first: once the plan is persisted, a risk assessor scores it. Low-risk plans (pure queries, no side-effect tools) skip the popup and proceed , but a read-only plan card is pushed into the chat stream so you know what happened, with a one-click switch on the card that says “ask me first for this kind of thing from now on” (it writes your per-user checkpoint_mode preference: plan_only by default, every_step, or off). What freezes and pops up is the medium/high-risk plan: the execution line halts, Brain sends a WebSocket signal through the gateway, and a plan_preview panel appears with the full step list, the DAG-validated execution layers and topological order, and the computed risk assessment. Two fields you might expect, estimated duration and estimated token cost, aren’t in the payload yet; they’re on the roadmap. At this point sovereignty returns to the human and the system waits for one of three choices. The ugly part first: this “waiting” currently auto-approves after a five-minute timeout. That’s the current state, not the intended end state. “Fail-closed for any plan containing high-risk side-effect steps” isn’t built yet; the trade-off and roadmap are laid out honestly in “The three-round amendment cap.” The three choices: [Approve] (run it); [Amend] (you type natural language — “plan looks good, but after the step-2 summary, add a step: email the summary to Nina” — and fibon takes your intent back to Stage 1 to re-plan; these natural-language re-planning rounds are capped at 3 to keep the conversation from looping forever); [Cancel] (you spotted something dangerous, kill the task).
Aside
DAG (directed acyclic graph) : draw the dependencies between steps as a graph where every arrow points one way and you can never loop back to where you started. It guarantees each step’s inputs come from steps already finished, and that no “A waits for B while B waits for A” deadlock can form, which makes it the sturdiest way to order a plan.
Stage 3: Execution (Dispatch) — separation of powers; the LLM is escorted out of the loop. The moment the human clicks approve, the chapter’s key design takes the stage: the reasoning LLM is escorted out of the execution loop by code. For the long minutes the tools run, the system is governed by a dispatcher with zero randomness in it, written in plain Python (dispatch_node inside Brain: it lives in the same LangGraph flow as the planner, but at this node the LLM has no decision power at all; it’s mechanical logic dispatching along the DAG topology). The dispatcher works like a strict factory foreman with the user-signed JSON plan in hand: step 1 says read_page → notify the Worker container to fetch the page; the data lands → apply the argument mapping exactly as the JSON specifies and feed it into step 2’s summarize_text.
The operational guarantee of a de-LLM’d execution stage : for the seconds or minutes the tool pipeline runs, the LLM gets no opportunity to reach into the execution line. How each step is called and which arguments go in were frozen into the JSON plan long ago. Architecturally, this severs one specific attack path: “the AI reads poisoned web content mid-execution (prompt injection), gets talked into changing the plan, and fires dangerous tools on its own.” Note the wording. What’s severed is injection’s path into the control flow, not all prompt injection; poisoned data can still contaminate a single step’s output, but it can no longer change what runs next. Here’s what a plan_and_execute-orchestrated workflow actually looks like as it moves through the microservices:
sequenceDiagram
actor User as Human at the screen
participant FE as Frontend (Astro/Vue)
participant GW as Gateway (control plane)
participant DB as PostgreSQL
participant BR as Brain (core process)
participant WK as Worker (sandbox gatekeeper)
User->>FE: clicks "Run Workflow"
FE->>GW: POST /workflows/{id}/run
GW->>DB: INSERT run history (status='running')
GW->>BR: gRPC: ExecuteWorkflowStep
Note over BR: Stages 1 & 2 passed and persisted<br/>entering Stage 3: pure-code dispatch
Note over BR: Step 1: llm_call step — the LLM invoked as a "tool"<br/>(text output only; routing stays in pure code)
BR->>BR: freeze tool list, render prompt, generate text
BR->>DB: UPDATE step_logs (Step 1 completed)
BR-->>GW: pointer: next_step_id = 'step2'
Note over BR: Step 2: tool call (tool_call)<br/>zero LLM involvement
BR->>WK: gRPC: ExecuteMcpToolCall
WK->>WK: runs the tool in the isolated sandbox / MCP (read_page)
WK-->>BR: returns clean data
BR->>DB: UPDATE step_logs (Step 2 completed)
Note over BR: Step 3: conditional branch (condition)<br/>eval() banned; allowlisted operators only
BR->>BR: safe filter evaluates: e.g., if (result.status == 200)
BR-->>GW: pointer: next_step_id = 'step4'
Note over BR: Stage 4: Synthesis
BR->>BR: LLM called back to write the natural-language wrap-up
BR-->>GW: final report: next_step_id = NULL (all done)
GW->>DB: UPDATE workflow_runs (status='completed')
GW-->>FE: SSE broadcast: workflow.completed
FE-->>User: renders the clean final report
One easy misreading to defuse right away: Step 1 in the diagram is an llm_call, which looks like the LLM sneaking back into the execution line. It isn’t. llm_call is one of the Workflow engine’s nine step types; here the LLM is a worker the plan explicitly hired, producing a piece of text and nothing more, no different in kind from read_page fetching a page. Where to go next (next_step_id) is always decided by pure-code routing. “The brain steps aside” means it gives up control-flow power , not that no step may use an LLM.
Aside
eval() and prompt injection : eval() executes a string as code. Convenient, but if an attacker gets anything into that string, you’ve opened a door for them to run arbitrary commands on your machine. Prompt injection is exactly that attack: hide malicious instructions in web pages or documents the AI will read, and trick it into treating them as commands. Combine the two and things get ugly fast, which is why conditionals should use dumb, hard-coded allowlisted operators instead.
Aside
Allowlist vs. blocklist : a blocklist names what’s forbidden and lets everything else through; an allowlist names what’s permitted and blocks everything else. Against unknown attacks the allowlist is far more reliable — you can never enumerate all the bad things, but you can enumerate the good ones. Chapter 5’s path-validation allowlist and the previous chapter’s 25-module sandbox allowlist follow the same philosophy; the seven-operator allowlist here applies it to “what a conditional is allowed to say.”
Two engineering constraints are buried down in the code here. First, eval()is banned from condition evaluation : when Step 3 handles a plan’s dynamic branches (condition steps), fibon’s backend refuses at the source level to call Python’s built-in eval() (one injected line in malicious page content could ride eval() out of the sandbox to a host shell). The workflow condition filter is entirely hand-written dead code, a minimal parser supporting exactly seven allowlisted operators: >=, <=, !=, ==, >, <, contains. The dumbest approach, and the most dependable one. Second, context-length control on long runs : as steps advance, accumulated trace logs snowball. fibon uses hard truncation (history carries only the last N=2 turns) plus offloading intermediate results into state/event cards to keep the context at a low watermark. The evolution of this defense, and the recently caught “one ‘hi’ carried twenty-five thousand tokens” fat-turn bug, are in Implementation detail 1 at the end of the chapter.
Stage 4: Synthesis — call the brain back to do what it does best. Only after the dispatcher finishes the whole plan and gathers the hard data does the code invite the reasoning brain back into the room, hand it the results and logs as settled facts, and issue the final instruction: “the user originally asked X; we ran steps 1 through 5 for them on a pure-code pipeline; the clean data is below. Use your gift for language to weave this into a graceful, warm answer.” At this stage the LLM holds “integrate and package” permissions only. It can distill step 1’s findings and step 3’s results into a layered summary, but the power to say “I don’t think there’s enough data, let me run one more tool” has been stripped. Execution is over; all it can do is polish the words. The separation of powers closes its loop here.
⇄ Counterpoint A question probably popped into your head: “you’ve built all this machinery in the back — does that mean users have to manually flip between Mode A and Mode B every day?” Not at all. Users never touch these switches. The style is chosen in the backend, from the task’s semantics, and invoked automatically. Here’s how the split works. Behind this are two fully independent, parallel control axes. Compare them to the two schools of the automotive world: The autopilot school (ReAct) : the car makes high-frequency black-box decisions every moment it’s moving (brake now? accelerate? change lanes?). Wonderfully adaptive, but every judgment carries a probability of error, and after a crash you can dig through the black box and still not figure out which millisecond of shifting light triggered the hallucination. The assembly-line school (Plan-Execute) : the robot arms on a factory floor, every motion welded in place by code (station one drives screws → station two sprays paint → station three inspects). Zero flexibility, but every re-run repeats the exact same motions with the exact same parameters, so when a product comes out flawed, an engineer can pinpoint the clogged paint valve at station two in seconds.
[ User types an everyday instruction ]
│
▼
[ Enters the SmartChatStrategy brain (settled 2026-05-26) ]
(legacy General/Smart toggle removed; five-channel RAG on by default in production)
│
▼
[ Brain interprets the intent ]
│
┌─────────────────────────┴─────────────────────────┐
▼ vague, needs iterative exploration ▼ multi-step, side effects, serious task
Runs as: [ 🏎️ ReAct autopilot ] Runs as: [ 🏭 plan_and_execute pipeline ]
(think as you go, adapt on the fly) (brain steps aside; pure-code dispatcher runs it)
Of the two axes, the “chat mode” axis was collapsed on 2026-05-26 by ADR-028 : the once-switchable General and Smart chat modes became a false choice after three patches (cache optimization, dynamic tool RAG filtering, the N=2 history window) pushed Smart’s cost down to par with General. The whole system now runs a single SmartChatStrategy, and the frontend toggle is gone.
So in today’s fibon, the only axis still alive is execution style. And even that switch isn’t the user’s problem: it’s a high-level tool the Butler invokes on its own, inside its ReAct loop, based on what you say. Ask “what’s the weather in Taipei today?” and the brain classifies it as a simple single-turn query, declines to invoke the planner, and answers in a second via ReAct. Ask “plan my five-day Kyoto trip next month, distill the highlights, and set calendar reminders” and the ToolSelector’s tool RAG surfaces plan_and_execute into this turn’s candidate list; the Butler reads the multi-step, side-effect nature of the task, calls it, and hands control to the pure-code dispatcher. You don’t need to know any of this architecture. Talk to it like a smart personal secretary and the backend takes it from there.
Rewind to Stage 2, the review. As mentioned, once the AI produces a JSON plan, you can push back in natural language (Amend) and force a rewrite. Here fibon carves a hard rule into the control plane’s finite-state machine: natural-language amendment rounds are capped system-wide at 3 (Max Amendment Turns = 3).
Aside
Finite-state machine (FSM) : break a process into a finite set of explicit states plus rules for which jumps are allowed. At any moment you know exactly which state the system is in and where it can go next; everything is enumerable, no ambiguous in-between states. Ideal for locking down a “review → amend → execute” gate that has to converge.
Why install an amendment circuit breaker here at all? Because in stress testing I watched the “non-converging nesting-doll” scenario with my own eyes: round 1, the AI produces plan v1 → the user adds a comment; round 2, v2 → the user reorders steps; round 3, v3 → the user suddenly wants a brand-new edge case handled… Without a breaker, this kind of natural-language revision, given the randomness of a probabilistic model, can mathematically fail to converge at all. The task never reaches execution, and every extra round fires another expensive Planner call at the cloud while the token bill climbs.
Three isn’t the output of some careful calculation; it’s an engineering hunch. In ordinary assistant scenarios, zero to one round of light touch-up is enough to align the brain with human intent. Three rounds (MAX_CHECKPOINT_ROUNDS = 3) is the tolerance margin left for both sides to make mistakes and self-correct.
Aside
Fail-open vs. fail-closed : the two defaults for “no answer arrived / something broke.” Fail-open waves it through, protecting the flow from deadlock. Fail-closed rejects everything, protecting safety. Which to pick depends on the cost of being wrong at that gate: everyday scheduling gets the former, modifying your own source code gets the latter.
⚠ Honest note
Full honesty: when round 4 arrives, the current code doesn’t slam a circuit breaker. It fail-opens and auto-approves. Open checkpoint_node in plan_execute/nodes.py: past three amendment rounds, the system marks the latest plan approved and sends it into execution. And it’s not just there. If the review panel fails to reach the frontend, auto-approve. If the user presses nothing for five minutes (CHECKPOINT_TIMEOUT_SECONDS = 300), auto-approve. Even an exception thrown while waiting tips toward approval. In every edge case, this checkpoint gate chooses “let it through” over “lock it up.”
Why dare to design it this way? It’s a sober risk-tiering ledger. Any plan that reaches this point has already passed DAG validation and risk assessment, and plan_and_execute handles everyday assistant work (fetching pages, scheduling, summarizing). The operational cost of a task frozen in waiting forever — an overnight automation hung for good, every task stalled the moment you walk away from the keyboard — outweighs the risk of releasing an already-validated plan. The counterexample is Chapter 5’s self-evolution Approval Gate: there the AI is modifying fibon’s own source code, a different risk class entirely, so approval_service.py runs a 30-minute fail-closed timeout. No human nod means rejected; any gRPC failure, any exception, all of it tips toward rejection. Same system, two timeout policies, matching two very different blast radii. Upgrading plans that contain high-risk side-effect steps to fail-closed (while the rest stay fail-open) is already on my roadmap; for now I’m writing down the current state honestly rather than pretending it exists.
One more old claim to correct: this “3” is not some system-wide unified cap. Chapter 2’s delegation cap is indeed 3, but the ping-pong cap between spawned sub-agents defaults to 5. Three mechanisms, three numbers, each guarding its own boundary.
🟢 Status: implemented (frontend-backend integration completed 2026-07-07). All three interventions work end to end, frontend through Brain: the
checkpoint_nodeedit gate, frontend breakpoint marking with the breakpoint set sent back,amend_nodedynamic changes, and mid-run Pause/Stop controls. Remaining rough edges are in the honest note at the end of this section. ⇄ Counterpoint Colleagues who prize flexibility will be protesting by now: “this Style B of yours is just a rigid machine. Once a human clicks approve, it shuts its eyes and executes the plan to the end. If step 3 fetches data nobody expected and steps 4 and 5 are now obviously pointless, your pure-code dispatcher will run them anyway. How is such a stiff framework usable in the real world?” Fair challenge. This used to be the collective sore spot of agent frameworks: once a plan entered execution, you lost the right to steer , and all you could do was watch it fail or cancel the whole task and rewrite the prompt. In 2026 the primitives for pausing and approval have been landing across the major frameworks (who has built what, and what differentiation fibon has left, is fully accounted in Deep Dive I, “The industry spectrum of execution control,” shipping with the Deep Dive series on open-source day). fibon’s answer is to assemble those primitives into out-of-the-box behavior: on the skeleton of the pure-code dispatcher, three “structured interventions” let a human reach in and grab the wheel at any point mid-run. Intervention 1: direct plan editing before execution (Pre-Execution Edit). At the Stage 2 review gate, alongside the usual[Approve]button, the plan card has an edit mode. No prompt-wrangling required: change a step’s arguments right on the panel, delete an unnecessary step, add a new one, or reorder the causality (inter-step dependencies re-wire automatically). Submit, and the dispatcher executes your adjusted plan as-is. This is the cheapest intervention in the whole system, costing exactly zero tokens. Intervention 2: breakpoints between steps. When the plan is presented at Stage 2, you can click the breakpoint marker on any step to plant a safe interrupt (“after step 3 fetches the contract, hold and wait for my word”). The system also suggests breakpoint positions from where you’ve hit the brakes before (adaptive_checkpoint); keep them or dismiss them. The moment the pure-code dispatcher finishes step 3 and sees the marker, it suspends in the background and pops the control panel back up with the raw data step 3 just fetched, waiting for one of three calls:[Continue](data looks right, release step 4);[Stop](the intermediate data already answers the question, everything further is waste, wrap it up early);[Edit](the data is not what anyone expected, so you rewrite the upcoming steps 4 and 5 on the spot, and the dispatcher picks up the new plan and continues seamlessly from the breakpoint). Intervention 3: natural-language interception mid-run (Dynamic Amendment), the highest-freedom option. This was the hardest code to write in the whole mechanism. At any instant during execution (even while a tool is crunching away in the sandbox), you just type a sentence into the chat: “wait — the quote it fetched looks wrong. Skip the calendar steps, email Nina instead and ask what’s going on.” The message first passes a cheap-model intent classifier (amend / status / unrelated, single-word output, and any failure falls back to treating it as an amendment so nothing gets dropped): asking “how far along are we?” reads progress straight from the database and answers instantly, no Planner involved; saying hi won’t accidentally trigger a re-plan. If it really is an amendment, Brain’sawait_node(the LangGraph node whose whole job is waiting, subscribed to both theplan:{run_id}:resultsandplan:{run_id}:amendRedis channels) catches the message and triggers a dynamic revision: the backend wakes one Planner call, packing in your sentence, the data from the three steps already executed, and the remaining unexecuted steps; the brain reads your intent and locally rewrites the remainder (the three persisted steps stay intact; the not-yet-run calendar steps are dropped and a freshly planned email step takes their place). Once the revised plan passes DAG validation, the dispatcher picks it up and continues from where it stopped.
[ 🏭 Dispatcher grinding through plan steps 1 ──> 2 ──> 3 ]
│
[ Interrupt: user types a natural-language Amend mid-run ]
│
▼ (Redis broadcast wakes amend_node)
[ Planner locally rewrites the remaining plan ]
│
▼ (DAG validation passes)
[ 🏭 Dispatcher picks up the new plan, continues seamlessly ──> step 4 (Email) ──> 5 (End) ]
Back to the pause button from the start of this chapter: Intervention 2’s breakpoints are that button in pre-planted form, and Intervention 3 is its natural-language version. There’s also a literal button. While a plan runs, the frontend always shows [Pause] (the dispatcher gets the signal before dispatching the next layer and opens the same panel a breakpoint would) and [Stop] (kill the unexecuted steps, keep the finished results, and let the synthesis stage report honestly on what got done and what never ran). And the “adding a dish mid-meal” case from the opening (halfway through trip planning: “look up attractions too”) rides the same amend channel, with the new steps planned into the tail of the remaining work. The three things this chapter set out wanting — pause anytime, add tasks mid-run, force-stop — all land here.
Put fibon’s Style B+ (Plan-Execute with the three structured interventions) side by side with traditional Style A (ReAct) for a final tally:
| Operational dimension | Traditional Style A: ReAct | fibon Style B+: Plan-Execute + three interventions |
| --- | --- | --- |
| Who triggers mid-run turns | Entirely up to the LLM’s mood as it reads the chat history (automatic, but a black box you can’t predict). | Mostly in the human user’s hands, exercised through 3 interventions (deliberate and predictable). |
| Debt from already-executed tools | The AI swerves on its own; tools that already ran wrong have made irreversible changes to the world. | Finished steps are preserved and persisted as assets; unexecuted risky steps are erased, and nothing is paid for twice. |
| Audit and compliance | The failure trail is a tangle of chat context, impossible to replay or audit. | Every steering intent and every revised JSON plan leaves precise step_logs in PostgreSQL, fully replayable and auditable. |
This also answers a common prejudice: that plan-first systems are doomed to be rigid. Not necessarily — a plan and a pipeline can have real dynamic flexibility, as long as that flexibility is built on structured interventions that are predictable, auditable, and respectful of human sovereignty.
To pull the whole chapter into one sentence:
ReAct leaves the flexibility to the AI. Plan-Execute hands control back to the system. fibon is after a third thing: let the flexibility exist, but make every exercise of it pass through an intervention point a human can see, review, and trace.
⚠ Honest note In full honesty: when the first draft was written in 2026-05, this section was missing the transport line for manually marked breakpoints. The UI toggle existed, but clicking it only changed local browser state; the breakpoint set never reached the backend, and the only breakpoints that actually worked were the system-suggested adaptive ones. That line (together with mid-run Pause/Stop, amend intent classification, and the low-risk auto-approval notice card) was fully wired up on 2026-07-07, with every operation going through the single
POST /chat/checkpointentry point. Three rough edges remain, laid out as usual. First, Pause/Stop takes effect at the granularity of an execution layer: steps in the same layer dispatch in parallel, and a step already running inside a Worker isn’t killed, it finishes naturally, so there can be up to a layer’s worth of delay between pressing pause and actually stopping. Second, there’s one lazy old pattern inside dispatch: the user’s action at a breakpoint panel is stashed in a shared global spot and read back, rather than passed as proper arguments. Fine for a single running plan, but with multiple concurrent plans in one process, plan A’s action could in theory be picked up by plan B. Tracked as TD-83. Third, the estimated-duration and estimated-token-cost fields on the review panel still don’t exist.
The upsides are covered. Once again, every architecture has its downsides and limits, so here are the three costs this one pays:
* Cost 1: when the router misjudges, you pay for one extra LLM call. By design, a trivial question (“what’s in my_notes.txt?”) should never enter the pipeline: the Butler routes it to ReAct and picks up read_file on the first turn. But the line is drawn by semantic judgment, and no judgment is error-free. The moment the Butler mistakes a simple task for a serious one and calls plan_and_execute, the system spends a full Planner round to produce a one-step JSON plan, burning Planner tokens and wall-clock time for nothing. The real name of this cost is misjudgment cost, and the more conservatively you draw the line (better to over-review than under-review), the more often you pay it.
Aside Time to first token (TTFT) : the wait between sending your message and the first character appearing on screen. It doesn’t change total duration, but it dominates how laggy things feel: ReAct streams as it thinks, so there’s an instant reaction; Plan-Execute has to finish the plan first, so the opening seconds are a spinner. * Cost 2: streaming feels worse, because TTFT gets longer. Under ReAct the AI streams while it thinks and the typewriter starts immediately, which feels great. Plan-Execute is strictly staged (wait for the Planner to finish the JSON → wait for the database write → wait for the dispatcher to run the tools → only at synthesis does the typewriter start), so the first two or three seconds of a conversation are a loading spinner. * Cost 3: steering the plan requires a deliberate human action. Unlike ReAct, where the AI sees your new message and silently swerves inside its black box, any change to a fibon plan requires you to click the edit controls (Interventions 1 and 2) or send an explicit correction message (Intervention 3). It asks something of you, and gives up that disposable-chatbot feeling of “everything just reacts automatically.”
▸ Decision point 2026-04-15
Facing these three costs, fibon made a clear-eyed call: accept the pricey first-token delay, accept the extra Planner charge. For a personal assistant meant to run at high frequency in production, close to a human’s property and daily life, a few seconds of waiting and a few cents of tokens buy 100% reproducible tool call order and arguments (the outside world’s responses will of course still vary, but which step ran, with which arguments, calling whom, is identical every time and fully auditable), and they buy the safety of having every high-risk side-effect tool pass in front of human eyes before it runs. That’s a good trade.
💡 Second thoughts
Some questions to chew on, for the engineers:
* Where exactly do you draw the ReAct-vs-plan_and_execute line? Misjudge one way and a simple task burns a Planner round; misjudge the other way and a side-effect-laden task skips the review gate entirely. The router is two-stage: tool RAG decides via semantic embedding whether plan_and_execute even enters the candidate list, and the Butler decides whether to call it. Where would you set the threshold, and how would you verify the Butler isn’t letting things slip?
* The round-4 fail-open bets that “a validated plan is low-risk enough.” But “fail-closed for plans containing high-risk side-effect steps” isn’t built yet. Until it is: for a plan that runs unattended at 3 a.m. and auto-approves after a five-minute timeout, which tools are you comfortable letting it touch, and which are absolutely off-limits?
* When a plan mixes dangerous and harmless steps, how fine-grained should approval be? Today the review panel rules on the whole plan at once, one click approves everything. If only step 4 of 5 is dangerous (sending mail, deleting data), whole-plan approval makes the harmless steps hostage to the dangerous one; per-step checkboxes turn review into a tedious form, and consent fatigue means people tick everything with their eyes closed. Where do you cut it — whole plan, by risk tier, or pull only the dangerous steps out for explicit confirmation?
* When everything runs asynchronously, what should the frontend even look like? Chat is inherently a linear question-answer narrative, but the background may be running three plans at once: one parked at a breakpoint waiting for you, one freshly rewritten by an amend, one quietly on step 7. That’s not a conversation anymore, it’s a dashboard. Pour it all into the chat stream and it floods; split it into a separate panel and you fracture the “talking to my assistant” mental model. Where’s the boundary between conversation and dashboard?
(Two more questions — “does approval have to happen up front, or could the system block only when a dangerous step is actually reached?” and “how can the machine spot anomalies and call a distracted human back?” — touch design space the whole industry hasn’t converged on, so they’ve moved to the end of Deep Dive I.)
▶ 🔧 Implementation detail 1: context control on long runs, and the 'fat turn' bug for engineers
From compression to offloading (TD-61). As a long run advances, trace logs snowball. Early on there was a CompactionService sentinel that compressed history once it passed 80% of the model window, but TD-61 removed it: compression destroys auditable detail, which collides with the white-box principle. The replacement is two moves: hard truncation (CONVERSATION_HISTORY_WINDOW, default N=2 turns) plus offloading intermediate results into state/event cards, keeping context at a low watermark.
The fat-turn bug (caught 2026-07-06). Truncating by turn count has a hole nobody planned for: N=2 guarantees the last two user turns, but says nothing about how expensive those turns are. Real case: the user’s previous message was “summarize this security news article,” and the AI pulled a full browser snapshot of the page, sidebar ads, related-article lists, social embeds and all. The next message was just “hi” — and that “hi” hauled over twenty-five thousand input tokens and took more than seven seconds to answer. The window never exceeded two turns; one of the turns was simply obese, and it blew the whole window out.
The fix direction. For turns that have already ended, keep only the user message and that turn’s final plain-text reply when assembling the prompt; the tool-call details in between stay out of history entirely. Mind the protocol trap here: every major provider requires that an AIMessage carrying tool_calls be immediately followed by its paired ToolMessage, or the API rejects the request outright. So you can’t just filter out the ToolMessages; the whole call-plus-result pair has to come out together. The full raw trace (tool_call_logs) still lands on disk untouched, and if the user later asks “what exactly did that tool find?”, the AI calls a built-in tool that reads the historical records. Same philosophy as offloading facts into cards: the short-term conversation keeps the skeleton, and details are fetched on demand.
▶ 🔧 Implementation detail 2: the third path for recurring tasks — a glance at the Workflow engine for engineers
Some multi-step tasks run the exact same pipeline every day or every week (weekly news digests, monthly vulnerability audits). Sending those through plan_and_execute so the brain can re-derive the same plan is pure token waste. So fibon built a separate pre-orchestrated Workflow engine in the control plane, aligned with the Temporal school of design: the plan is frozen as a workflow definition in the database, and execution makes zero Planner calls. It’s a third growth path beyond ReAct and Plan-Execute, with its own step-type system and versioning/fork mechanics. It’s related to this chapter but doesn’t belong to it, so this is just a wave hello; the full teardown lives in Deep Dive H, “The pre-orchestrated Workflow engine.”
▶ 🔧 Implementation detail 3: why Plan-Execute, and not Tree of Thoughts (ToT) or LATS? for engineers
Academia and open source have spent the past two years promoting advanced reasoning patterns: ToT (tree-of-thoughts search), LATS (language agent tree search), ReWOO and friends, which let an AI grow a whole decision tree, backtrack, and prune itself. After a round of reading the papers, fibon’s conclusion was: no. Four reasons, one line each:
1. Wrong arena. Tree search shines on hard-reasoning problems with huge answer spaces and tiny error tolerance; a personal assistant’s compound tasks are fundamentally multi-step scheduling, and a linear plan covers them.
2. Auditability. A linear JSON plan is something a human can read at a glance and approve on a frontend panel; a tangle of fifty thought nodes pruning and backtracking against each other cannot be explained to an ordinary user, and human-in-the-loop review collapses entirely.
3. Token cost. A linear plan’s cost scales linearly with step count and can be estimated up front; tree search grows exponentially with depth and breadth, and an AI playing chess against itself overnight can burn tens of thousands of calls, which betrays the core goal of driving token costs down.
4. The reasoning-model dividend. By 2026, native reasoning models (with built-in extended thinking) have folded the backtrack-and-prune work into the model itself: route the Planner to one of these and what comes out is already a clean linear plan, making another ToT framework layered on top redundant engineering.
On balance, Plan-Execute is the highest-ROI choice. That wraps the question of “who signs off on an untrusted sequence of steps, and how a human keeps eyes on it before and after execution.” The next chapter switches reading modes entirely: we lay the whole log back onto a timeline for a chronological review — back to the days of architecture torn down and rebuilt, of endless collisions with LLMs — and watch how fibon grew from its first line of code into what it is today, including which designs were right from day one and which took the long way around. See you in Chapter 8. ← Previous · Chapter 6 How Do You Safely Run Untrusted Code? ↑ ☾ © 2026 Aaron · fibon design log Feedback & challenges welcome: Enjoying the log? Buy me a coffee ☕