AI Agent

From Agent Loop to PlanMode: A Production-Ready AI Agent Engineering Skeleton

A production-ready AI Agent engineering skeleton explained through an operating-system analogy. The Harness is the OS, the LLM is the CPU, and the Agent Loop is the kernel's scheduling loop — constantly assembling context, dispatching tool calls, and waiting for I/O. A clear termination condition, real verification, and disk persistence are non-negotiable. Two-Stage ReAct separates thinking from doing: a dry-run first, then real execution. Four tools — read, write, edit, bash — map to file-system operations, scheduled in parallel for read-only and serially for writes. AGENTS.md, Skills, and PlanMode form a three-level memory system that keeps long-running agents on track. Includes ready-to-use pseudocode.

Contents

Let's use an analogy to remember the knowledge system this article covers:

  • Harness is the operating system — the top-level container that manages all resources.

  • LLM is the CPU — it takes in instructions and produces results.

  • Agent Loop is the kernel's main scheduling loop — constantly picking up tasks, dispatching, waiting for I/O to return, then picking up the next one.

  • Two-Stage ReAct is the OS's dry-run mode — it runs a trial first to see the plan, and only after confirmation does it really execute.

  • The four tools are the basic operations of a file system — read, write, edit, bash, corresponding to the rwx permission model.

  • AGENTS.md / Skills / PlanMode form a three-level storage system — BIOS, hard disk, memory + disk persistence.


1. Agent Loop: The Kernel's Main Scheduling Loop

At its core, every Agent is just a while(!done).

while not terminated:

         1\. Assemble context (system prompt + history + tool declarations + current observation)

         2\. Hand the context to the LLM

         3\. Parse the LLM's response:

            3.1 Text segment → append to context history

            3.2 If there are tool\_calls → execute each one, write results back to history, continue

            3.3 No tool\_calls → terminate the loop and return the text

Just like the OS kernel's main scheduling loop:

Take a process from the ready queue → allocate a CPU time slice → wait for the I/O interrupt to return → schedule the next one.

The Agent Loop is:

Keep assembling context → hand it to the LLM → wait for the response → execute tools (I/O) → write results back to history → loop again.

It is the heart of the entire system.

To keep this loop running reliably, you need to think through three things:

  1. Termination condition: no tool_call, hitting max_turns, user cancellation, or a verifier judging the task complete. Without an explicit termination condition, the loop will spin forever and burn tokens until you go broke.

  2. Verification method: don't trust the model's claim that it's done — verify by running tests, checking diffs, and assertion scripts.

  3. State persistence: context history, PLAN.md/TODO.md, tool-call logs. Context compression can silently swallow early information, so write it to disk.

Scenarios that suit an Agent Loop:

  • High frequency (run at least once a week); for one-off tasks there's no need to bother with an Agent.

  • Almost no human intervention required.

  • Failures can be re-run.


2. Two-Stage ReAct: Dry-Run First, Then Really Execute

If you just hand the tool declarations straight to the LLM, it will be very impulsive about calling tools instead of first thinking about what to do.

Two-Stage ReAct splits "thinking" and "doing" into two independent LLM calls (letting the LLM restrain its impulses), physically isolating them:

Stage 1 (no-tool prompt): give only the task + context, expose no tools at all

      → LLM outputs a plan / reasoning chain / assumptions (dry-run)

Stage 2 (with-tool prompt): stitch Stage 1's output back in, then expose the tool declarations

      → LLM calls read/bash/edit... according to the plan

This is like the OS's dry-run mode — apt-get install -s, kubectl apply --dry-run, make -n.

The first call is simulated execution: it touches no system state and only outputs what it intends to do;

The second is real execution, which modifies state.

Why does this layer still have engineering value even when models already have "native thinking" ability?

  • Intervention point: between Stage 1 and Stage 2 you can intercept — revise the plan, require human approval, run compliance audits. Native thinking is a black box; you can't intercept it.

  • Observability: Two-Stage makes the plan explicit as text, so it can be diffed, rolled back, and reviewed. Native thinking blocks are visible but hard to audit.

  • Multi-model compatibility: any program can dry-run; you don't need the program itself to support any special capability. You can swap in a cheap small model and still run, without depending on a large model with native thinking.

When to enable thinking (not every turn — it's expensive):

  • When a new task goal is first issued (dry-run the plan first).

  • When a tool returns an unexpected result (execution went wrong; re-dry-run to reconsider whether to switch approaches).

  • Before high-risk actions like deletion, pushing, payment, or writing to a production database (trial-run major operations first).


3. Tools: The Basic Operations of a File System

Give the Harness only four basic tools: read, write, edit, bash.

This exactly corresponds to the basic operations in a file system / permission model:

ToolFile-system operationEffect
readRead file (open + read)No side effects; look only, don't touch
writeWrite file (overwrite, like > redirection)Whole-file overwrite
editModify file content (like sed -i, precise patch)Only changes the lines that need changing
bashExecute program (execute)Run any command; one tool beats ten special-purpose tools

Why have edit when you have write? write overwrites the whole file; once a file gets large, the token consumption is enormous — both slow and expensive. What's more, large models are extremely prone to truncation when generating long text, or to accidentally introducing new syntax errors. edit works like sed -i: it only touches the lines that need changing, so diffs are controllable, reviewable, and rollbackable.

Giving too many tools is like chmod 777 — opening up permissions recklessly: it doesn't make the model more capable, it makes it more likely to impulsively call tools it shouldn't, and the error surface gets bigger instead. It also tends to limit a smart model's initiative (a smart model could do a better operation itself, but once it sees you've provided a ready-made operation, it assumes based on your description that this one is more suitable and uses it — yet the model doesn't know how you implemented it, and it may not be the operation the model actually wanted). Four basic operations plus bash's all-purpose execution capability already cover the vast majority of tasks.

edit's uniqueness check (mandatory): if a fuzzy match hits more than one location, the tool must absolutely not blindly replace — just like sed globally replacing when a multi-line match has no line number specified would change lines that shouldn't be changed. It must raise an error and ask the LLM to provide more surrounding code to pinpoint the exact location. This is the safeguard against an Agent "accidentally editing a different similar fragment" while changing code.

def apply\_edit(text, old, new):

         cnt = text.count(old)

         if cnt == 0:

             raise ToolError("old\_text not found")

         if cnt > 1:

             raise ToolError("ambiguous match (>1); provide more surrounding context")

         return text.replace(old, new, 1)

4. Improving Harness Execution Efficiency: Concurrent for Read-Only, Serial for Writes

A model's single reasoning pass may emit multiple tool_calls. If the harness executes them all serially one by one, efficiency suffers — but the harness also shouldn't run them blindly in parallel. It needs a proper scheduling strategy: generally, if all the tools are read-only, they can run in parallel.

func dispatch(calls \[]ToolCall) {

         var readonly, writable \[]ToolCall

         for \_, c := range calls {

             if isReadOnly(c) {

                 readonly = append(readonly, c)

             } else {

                 writable = append(writable, c)

             }

         }

         if len(writable) == 0 {

             // read-only batch: concurrent goroutines

             fanOut(readonly)

         } else {

             // write-involving batch: strictly serial execution

             for \_, c := range calls {

                 exec(c)

             }

         }

}

At extremely low complexity, this strategy guarantees both performance and correctness in the vast majority of scenarios.


5. Three-Level Memory: AGENTS.md / Skills / PlanMode

5.1 AGENTS.md

This solves the question of "what the current project looks like." You can compare it to BIOS info on an operating system — loaded at startup, telling the system what hardware config it has, what the directory structure is, and what restricted zones exist. When an Agent starts, it reads this once and knows which project it's working in.

Note: Claude Code still stubbornly reads CLAUDE.md, which serves the same purpose as AGENTS.md.

5.2 Skills

This solves the question of "how a specific task should be done." Compare it to binary programs on a hard disk loaded on demand — only when the corresponding scenario is hit are they loaded into context (the CPU). AGENTS.md tells the machine what its config is; Skills tell it which "program" to invoke for this task.

5.3 PlanMode

  • PLAN.md: the macro routing table, the strategic direction. It keeps an Agent from drifting off course across long tasks spanning dozens of turns.

  • TODO.md: the scheduling queue — check off each item as it's completed.

Context compression is like memory reclamation losing data — early dialogue history compacted away is gone. So you have to periodically flush to disk: write progress into PLAN.md/TODO.md to prevent loss on "power failure" (compression). When the Agent wakes up and re-reads the disk, it knows what to continue with.

ThinkingPhase (slow thinking) is pipeline-level error correction on the CPU: even if TODO.md already contains new tasks, without a per-turn slow-thinking constraint the model may still take shortcuts when choosing the concrete implementation path — skipping boundary-condition validation, picking the first runnable solution without weighing tradeoffs. Slow thinking is immediate correction at every step; PlanMode is the state safety net on disk. The two complement each other.


6. Tying It All Together in One Sentence

Harness (operating system)

      ├── LLM (CPU computing core)

      │    └── Agent Loop (kernel's main scheduling loop)

      │         └── Two-Stage ReAct (dry-run simulated execution → real execution)

      ├── Tool set (basic file-system operations: read/write/edit/bash)

      └── Three-level memory (BIOS / hard disk / memory + disk persistence)

The Agent that runs reliably isn't the one with the strongest model — it's the one whose Loop has a termination condition, whose tools have boundary constraints, whose read/write operations have parallel/serial optimization, and whose long tasks have disk-persistence as a safety net.


Appendix: Directly Reusable Loop Pseudocode

def agent\_loop(task, max\_turns=30):

         ctx = bootstrap(task)          # AGENTS.md + Skill discovery + history

         for turn in range(max\_turns):

             if need\_plan(ctx):         # PlanMode intervention

                 ctx = two\_stage\_plan(ctx)

             resp = llm(ctx)            # Stage 2 (with tools)

             ctx.append(resp.text)

             if not resp.tool\_calls:

                 return resp.text       # termination condition

             batch = group\_by\_readonly(resp.tool\_calls)

             for r in concurrent\_run(batch.readonly):

                 ctx.append(r.result)

             for w in sequential\_run(batch.writable):

                 if w.name == "edit" and match\_count(w) != 1:

                     ctx.append(edit\_ambiguity\_error(w))

                     break

                 ctx.append(w.result)

         raise Timeout("max\_turns reached")

Android utilities we built — take a look

Kuaiyatu compresses images locally (batch supported); Kuailuping records your screen locally. No upload, no sign-in.

↑ Back to top