不同Harness的Agent Loop怎么做?ClaudeCode、Codex等6套方案详解
How to Implement Agent Loops for Different Harnesses? Detailed Explanation of Six Solutions Including ClaudeCode and Codex
Summary
A seemingly simple Agent Loop—how complex is it to actually build a Coding Agent? The basic flow of an Agent Loop isn’t complicated. The model initiates a tool call, Harness
A seemingly simple Agent Loop—how complex is it to actually build a Coding Agent?
The basic workflow of the Agent Loop is not complicated.
The model initiates a tool call, Harness executes it and returns the result to the model, which then decides the next step. By focusing on these steps, you can build the simplest Agent loop.
But in real products like Claude Code and Codex, a large number of concrete issues still need to be addressed: how tools execute in parallel, whether users can add instructions midway, where permission checks should occur, how to compress when the context approaches its limit, and when the entire loop should terminate.
Different Harnesses handle these issues in different ways.
Recently someone compiled Claude Code, Codex, OpenCode, Pi, Oh My Pi and DeepSeek Harness’s Agent Loop, systematically reviewing the concrete implementations of the six systems—from loop structures and tool execution to context management and extension mechanisms.
The six systems follow a similar basic process, but each makes different design trade‑offs in loop organization, state management, and extension mechanisms.
Claude Code
Claude Code’s agent loop is a single‑threaded main loop: call the model → if there is tool_use then execute → fill back the result → call again, until the model returns a response without tool calls.
The loop itself is simple; the complexity lies in the tools, permissions, hooks, automatic compression, and sub‑agents.
Loop Structure
-
The official term is “agentic loop”: collect context → act → verify, with the three stages interleaved.
-
In implementation it is just a while loop: if the model output contains
tool_usecontinue; otherwise stop. -
A single “model output + tool execution + result feedback” is called a turn.
-
The Agent SDK exposes the same loop, shared by CLI / desktop / IDE.
A single model invocation
-
Include with every request: system prompt, CLAUDE.md, tool definitions, full history.
-
Static prefixes use prompt caching to reduce redundant cost.
effort Control inference depth; output the thinking block separately.
Accumulate cost after each response return, used for budget checking.
</h2
Tool Execution
-
Multiple calls in the same round: read‑only tools (Read / Glob / Grep, MCP marked as read‑only) are executed concurrently.
-
State‑changing tools (Edit / Write / Bash) are executed serially.
-
Permission check before execution: PreToolUse hook → deny/allow rules → permission mode → prompt the user.
-
In auto mode, a classifier model reviews commands and network requests.
User Interjection
-
You can interrupt at any time, change direction, add information, or have it try a different approach.
-
With streaming input, messages that arrive during execution are queued and incorporated into subsequent model calls.
Multiple calls in the same round: read‑only tools (Read / Glob / Grep, MCP marked as read‑only) are executed concurrently.
State‑changing tools (Edit / Write / Bash) are executed serially.
Permission check before execution: PreToolUse hook → deny/allow rules → permission mode → prompt the user.
In auto mode, a classifier model reviews commands and network requests.
You can interrupt at any time, change direction, add information, or have it try a different approach.
With streaming input, messages that arrive during execution are queued and incorporated into subsequent model calls.
Messages still in the queue when maxTurns is reached are not merged, but start a new turn.
Context Compression
-
Auto-compress when approaching the window limit: clear old tool output first, then summarize the conversation.
-
Emit in the stream
compact_boundary; the PreCompact hook can archive the entire text first. -
A single massive output causes repeated compression to stop after a few attempts and raise an error, preventing an infinite loop.
-
You can specify in CLAUDE.md what to keep when compressing.
</h2
Stopping Conditions and Sub agent
-
The model response does not contain tool_use → finish and return ResultMessage (including cost / usage).
-
maxTurns (counting only tool rounds) / maxBudgetUsd caps; exceeding them returns an error sub‑type.
-
The stop hook can block and stop, allowing Claude to continue.
-
Agent tool spawns a sub‑agent: a fresh context, only returns the final reply to the parent loop; budget includes the sub‑agent.
The model response does not contain tool_use → finish and return ResultMessage (including cost / usage).
maxTurns (counting only tool rounds) / maxBudgetUsd caps; exceeding them returns an error sub‑type.
The stop hook can block and stop, allowing Claude to continue.
Agent tool spawns a sub‑agent: a fresh context, only returns the final reply to the parent loop; budget includes the sub‑agent.
Agent Loop Flowchart
Core Loop Pseudocode
*Restore according to official documentation
messages = [system, CLAUDE_md, *history, user_input] # UserPromptSubmit
while True:
maybe_auto_compact(messages) # PreCompact: clean old output → summary
resp = claude.stream(messages, tools, effort) # thinking / text / tool_use
messages.append(resp)
if not resp.tool_uses:
if stop_hook(resp) == "block": # Stop hook let it continue
messages.append(stop_reason); continue
return Result(resp.text, cost, usage)
allowed = [c for c in resp.tool_uses if permitted(c)] # PreToolUse → rules → patterns → query
results = run_parallel([c for c in allowed if c.read_only])
results += run_serial([c for c in allowed if not c.read_only])
messages.append(user(tool_results=results)) # PostToolUse
messages += drain_queued_user_messages() # queued interjections during execution
if turns >= max_turns or cost >= max_budget:
return Error("error_max_turns / error_max_budget_usd")
Extension point: where to intervene in the loop
-
UserPromptSubmit: before submitting input; can rewrite, supplement context, or intercept.
-
PreToolUse: before tool execution; can reject, allow, or modify parameters.
-
PermissionRequest: when a permission dialog pops up; can decide on behalf of the user.
-
PostToolUse: after a tool is executed; can provide additional feedback to the model.
-
PostToolBatch: after a batch of tools have all completed.
-
PreCompact / PostCompact: before and after automatic or manual compression.
-
Stop: when about to finish; you can block to let it continue.
-
SubagentStart / SubagentStop: start and stop of sub‑agents.
Design Trade-offs
Highlights
-
The loop itself is minimalist; the complexity lies in the tools, permissions, and hooks.
-
Read concurrency, write serialization, balancing speed and safety.
-
Stop hooks, budgets, and sub‑agents allow the loop to be orchestrated externally.
-
Hook events are abundant, allowing intervention at almost every stage.
Cost
-
The source code is not open source, and internal scheduling details can only be viewed in the documentation.
The child agent only returns the final response, and the parent loop cannot see the intermediate steps.
Compression is a lossy summarization, and early details may be lost.
Claude Code’s loop is a “thin loop + thick shell”: The loop only handles model invocation and tool execution, while security and control are delegated to the permission system and hooks.
Source
[1] Claude Code — Working Principle
https://code.claude.com/docs/en/how-claude-code-works
[2] Claude Code — Agent Loop Official Documentation
https://code.claude.com/docs/en/agent-sdk/agent-loop
[3] Claude Code — Hooks Reference Documentation
https://code.claude.com/docs/en/hooks
Codex
In Codex (codex-rs), a single user input starts a turn, and the turn loops through multiple samplings: each sampling is a Responses API request, plus the execution of all tool calls in that response.
The loop condition is needs_follow_up: the model still needs to continue, or there are user interjections not yet incorporated.
Loop Structure
-
The client and kernel communicate via SQ/EQ: submit Op into the queue, and the kernel pushes back EventMsg.
-
run_turn()contains a loop: merge interleaved → sample → execute tool → decide whether to continue. -
A thread has only one active turn at a time.
-
Before a turn starts, run the SessionStart / UserPromptSubmit hooks.
One model invocation
-
build_prompt: historical ResponseItem + tool definitions +parallel_tool_calls: true. -
Streaming reception: text, inference, and plan updates are pushed to the client in real time.
-
When the stream breaks, automatically retry according to
stream_max_retries. -
turn_contextFix the model, sandbox, approval policy and other settings for this turn.
</h2
Tool Execution
-
Execute while streaming:OutputItemDone as soon as it arrives, start the future tool without waiting for the whole response to finish.
-
FuturesOrdered ensures results are filled back in the order of invocation.
-
The parallel gate is a read‑write lock: tools that support parallelism acquire a read lock for concurrency, others acquire a write lock exclusively.
-
ToolOrchestrator: approve → select sandbox → execute → if blocked by sandbox, upgrade and retry (no duplicate approval).
User comment
-
turn can run turn/steer: messages go input_queue, continuously turn.
<li
Before each sampling, take out the pending input and merge it (first through the UserPromptSubmit hook).
-
Any input that hasn’t been merged yet also counts needs_follow_up, and the loop will continue.
-
turn/interrupt Interrupt the current turn and record turn_aborted.
Context Compression
-
Pre-sampling check: if exceeds model_auto_compact_token_limit compress first.
-
If sampling continues after reaching the limit, compress in place and keep running (remote compression also supported).
-
When switching to a model with a smaller window, the old model will first compress it once.
-
The compression result is written as a Compacted record, replacing the history.
Stopping Conditions and Multi agent
-
needs_follow_up is false → run Stop hooks.
-
Stop hook can block, and inject the continuation prompt as new input, continuing the loop.
-
Otherwise, push TurnComplete to EQ, ending the turn.
-
Multiple agents: sub‑agents run as independent threads, and the parent turn collects results via tools.
Agent Loop Flowchart

Core Loop Pseudocode
*Simplified from codex-rs/core/src/session/turn.rs
async fn run_turn(sess, input) {
run_hooks_and_record_inputs(&input).await; // SessionStart / UserPromptSubmit
run_pre_sampling_compact().await; // Compress first when exceeding threshold
loop {
let pending = input_queue.get_pending_input(); // turn/steer interjection
run_hooks_and_record_inputs(&pending).await;
let r = run_sampling_request(build_prompt(history, tools)).await;
// In streaming events, OutputItemDone(FunctionCall) → in_flight.push_back(tool_future)
// Tools: RwLock read parallel / write exclusive; approval → sandbox → execution → upgrade retry
drain_in_flight().await; // Sequentially backfill FunctionCallOutput
let follow = r.needs_follow_up || input_queue.has_pending_input();
if follow && token_limit_reached { run_auto_compact().await; continue; }
if !follow {
match run_turn_stop_hooks().await {
Block(prompt) => record(prompt), // Stop hook requests continuation
_ => break,
}
}
}
send_event(TurnComplete); // Push to EQ
}
Extension point: where to intervene in the loop
-
SessionStart: Session starts / resumes / after compression.
-
UserPromptSubmit: Each input (including steer) is merged before.
-
PreToolUse: before tool execution; can be intercepted.
-
PermissionRequest: when approval is required.
-
PostToolUse: after the tool is executed.
-
Stop: the turn will end; can block and provide a continuation prompt.
</ul
Design Trade-offs
Highlights
-
The tool runs while streaming, calls in long responses don’t have to wait until the end.
Sandbox + approval + upgrade retry are unified in the orchestrator, providing the most complete security model.
-
Steer is merged into the current turn, so interjections do not disrupt the flow.
-
Compress the two timings of pre‑sampling and during sampling, making long tasks less likely to overflow the window.
Execute while streaming:OutputItemDone as soon as it arrives, start the future tool without waiting for the whole response to finish.
FuturesOrdered ensures results are filled back in the order of invocation.
The parallel gate is a read‑write lock: tools that support parallelism acquire a read lock for concurrency, others acquire a write lock exclusively.
ToolOrchestrator: approve → select sandbox → execute → if blocked by sandbox, upgrade and retry (no duplicate approval).
turn can run turn/steer: messages go input_queue, continuously turn.
<li
Before each sampling, take out the pending input and merge it (first through the UserPromptSubmit hook).
Any input that hasn’t been merged yet also counts needs_follow_up, and the loop will continue.
turn/interrupt Interrupt the current turn and record turn_aborted.
Pre-sampling check: if exceeds model_auto_compact_token_limit compress first.
If sampling continues after reaching the limit, compress in place and keep running (remote compression also supported).
When switching to a model with a smaller window, the old model will first compress it once.
The compression result is written as a Compacted record, replacing the history.
needs_follow_up is false → run Stop hooks.
Stop hook can block, and inject the continuation prompt as new input, continuing the loop.
Otherwise, push TurnComplete to EQ, ending the turn.
Multiple agents: sub‑agents run as independent threads, and the parent turn collects results via tools.
async fn run_turn(sess, input) {
run_hooks_and_record_inputs(&input).await; // SessionStart / UserPromptSubmit
run_pre_sampling_compact().await; // Compress first when exceeding threshold
loop {
let pending = input_queue.get_pending_input(); // turn/steer interjection
run_hooks_and_record_inputs(&pending).await;
let r = run_sampling_request(build_prompt(history, tools)).await;
// In streaming events, OutputItemDone(FunctionCall) → in_flight.push_back(tool_future)
// Tools: RwLock read parallel / write exclusive; approval → sandbox → execution → upgrade retry
drain_in_flight().await; // Sequentially backfill FunctionCallOutput
let follow = r.needs_follow_up || input_queue.has_pending_input();
if follow && token_limit_reached { run_auto_compact().await; continue; }
if !follow {
match run_turn_stop_hooks().await {
Block(prompt) => record(prompt), // Stop hook requests continuation
_ => break,
}
}
}
send_event(TurnComplete); // Push to EQ
}
SessionStart: Session starts / resumes / after compression.
UserPromptSubmit: Each input (including steer) is merged before.
PreToolUse: before tool execution; can be intercepted.
PermissionRequest: when approval is required.
PostToolUse: after the tool is executed.
Stop: the turn will end; can block and provide a continuation prompt.
</ul
Design Trade-offs
Highlights
-
The tool runs while streaming, calls in long responses don’t have to wait until the end.
Sandbox + approval + upgrade retry are unified in the orchestrator, providing the most complete security model.
Steer is merged into the current turn, so interjections do not disrupt the flow.
Compress the two timings of pre‑sampling and during sampling, making long tasks less likely to overflow the window.
Cost
-
A thread has only one active turn at a time.
-
Whether a tool can run in parallel depends on the tool’s own declaration; the default is conservative (exclusive).
-
The logic is spread across multiple modules like turn.rs, tasks, and tools, making it somewhat heavy to read.
Codex’s loop is organized around “sampling”: Streaming execution tools, safety is backed by the sandbox orchestrator, and needs_follow_up determines whether to sample again.
Source
[1] OpenAI — Codex Official Repository
https://github.com/openai/codex
[2] Codex — Agent Loop Main Cycle (turn.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/session/turn.rs
[3] Codex — Parallel Tool Execution (parallel.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/parallel.rs
[4] Codex — Tool Execution Orchestrator (orchestrator.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/tools/orchestrator.rs
[5] Codex — Hooks Runtime (hook_runtime.rs)
https://github.com/openai/codex/blob/main/codex-rs/core/src/hook_runtime.rs
[6] Codex — Hooks Module
https://github.com/openai/codex/blob/main/codex-rs/hooks/src/lib.rs
OpenCode
The loop of OpenCode is driven by SessionRunner driven: fetch input from the inbox in SQLite, loop by steps; each step is a model request, and if there is a tool call, another step follows.
The tool runs as soon as it appears in the pipeline, taking a Git snapshot before and after each step; compression, retry, and continuation are all handled within the step. (Based on the core rewrite version of the current dev branch)
Loop Structure
-
SessionRunner.drain: take one input, then advance in a while loop by step. -
One model request per step;
needsContinuation(when there is a tool call) increment step and continue. -
runStep contains an inner loop that specifically handles compression, retries, and continuation.
-
Written with Effect, state changes are pushed to each client via the event bus.
One model invocation
-
Reload context for each step: instructions, agent, model, tools, history.
When exceeding agent.steps, append a MAX_STEPS warning and set toolChoice: none.
Capture the step‑start snapshot in parallel as the request is sent, without delaying the first character.
The final step also retains the tool definition to hit the provider’s prefix cache.
Tool Execution
-
When a tool-call event appears in the stream, it executes immediately (fiber concurrency), without waiting for the entire response to finish.
-
Tools executed by the provider itself (providerExecuted) skip local execution.
-
Permission rules allow / ask / deny; ask triggers a popup, and a DeclinedError is thrown when denied.
Calls with incomplete parameters are not executed; respond to the model with “Please restart.”
User Interjection (Inbox)
-
inbox each item has
delivery: queue (wait for idle) or steer (insert while running). -
During operation, only take steer; only when reaching an idle boundary take a queue input.
-
The inbox can also hold control items: compaction (manual compression), move (migrate session location).
-
After the process restarts, first clean up leftover compressions and tool invocations, then continue.
Compression and Failure Recovery
-
Run before each step
compaction.compact(auto), after compaction reload and continue. -
Request reports context overflow → compress once due to overflow and retry.
-
Retriable errors wait according to the strategy; when a disconnection occurs, synthesize a sentence “Please continue from the interruption point.”
-
Attempt result: Completed / Retry / Continue / Compacted / RecoverFull.
Stopping Conditions and Sub agent
-
needsContinuationis false (no tool call) → this drain is completed. -
step settlement: end snapshot → patch file diff → tokens / cost.
-
When
agent.stepsreaches its limit, force output only a text summary. -
A sub‑agent runs in a sub‑session and returns its result to the parent session upon completion.
Agent Loop Flowchart
Core Loop Pseudocode
*Simplified from packages/core/src/session/runner/llm.ts, step.ts
drain(sessionID):
settleStaleCompactions(); settleStaleToolCalls() # Cleanup after crash
whiletrue:
next = advanceToStep() # Promote inbox to queue / steer input
if next != Ready: return next # Complete / Moved / Reloaded
continuing = runStep(next.context, step); step += 1
runStep(ctx, step):
whiletrue: # Compression / Retry / Continuation loop
if compaction.compact(reason="auto").completed: continue
req = buildRequest(ctx, maxStepsPrompt if step >= agent.steps)
outcome = attempt(req) # In stream, tool-call → execute immediately (concurrently)
match outcome: # Permission allow / ask / deny
Completed(needsContinuation) -> return needsContinuation
Retry -> retry.wait()
Continue -> retry.wait(); synthetic("Please continue from the interruption point")
Compacted -> Context overflow, compressed, retry
Extension point: where to intervene in the loop
-
tool · execute.before: before the tool runs; can reject the call.
-
tool · execute.after: after the tool runs; can modify the output.
-
permission · evaluate: participates in decision‑making during permission evaluation.
-
session · request 等: request construction, compression, title generation and other stages.
-
aisdk: modify AI SDK / provider parameters.
-
事件总线(SSE): push all state changes in real time to TUI / desktop / IDE.
Design Trade-offs
Highlights
-
The tool runs while streaming, with step states stored entirely in the database, allowing recovery after a crash.
-
A Git snapshot is taken before and after each step, allowing precise rollback to the step.
-
The inbox uniformly carries input and control items (compression, migration).
-
The step limit reaches forced summarization to prevent infinite loops.
Cost
-
Depends on SQLite + an event bus, heavier than a pure log implementation.
-
Effect‑style code has a relatively high barrier.
-
core is being rewritten (dev branch), allowing rapid changes.
OpenCode turns the agent loop into a “database‑driven step machine”: inputs, states, and snapshots are all persisted to the database, and the client only subscribes to events.
Source
[1] opencode — Official repository
https://github.com/anomalyco/opencode
[2] opencode — SessionRunner entry (index.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/index.ts
[3] opencode — Model invocation and loop handling (llm.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/runner/llm.ts
[4] opencode — Input queue (inbox.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/session/inbox.ts
[5] opencode — Permission control (permission.ts)
https://github.com/anomalyco/opencode/blob/dev/packages/core/src/permission.ts
[6] opencode — Plugin package definition
https://github.com/anomalyco/opencode/blob/dev/packages/plugin/package.json
Pi
Pi’s agent loop is an ultra‑minimal two‑layer loop: the inner layer keeps invoking the model while there are tool calls or steering interjections, and the outer layer checks follow‑up when it’s time to stop.
The loop only recognizes a few callbacks (prepareNextTurn / prepareRequest / beforeToolCall / afterToolCall / finishTurn); compression, retry, and extension are all attached by the outer AgentSession.
Loop Structure
-
agentLoop()→runLoop: the inner while handles tools and steering, the outer while handles follow‑up. -
Inner condition:
hasMoreToolCalls || pendingMessages.length > 0. -
Use AgentMessage throughout, only at the moment of invoking the model
convertToLlm. -
Externally it’s an event stream: agent_start / turn* / message* / tool_execution_* / agent_end.
</h2
One model invocation
-
prepareNextTurn: After the previous round ends, you can change the context, model, or reasoning level.
-
Before injecting a message, calculate tool additions/removals and write them as a system message (toolsAdded / toolsRemoved).
-
prepareRequest: AgentSession projects the session‑tree context, and the virtual model is routed here.
-
transformContext → convertToLlm → Streaming call.
Tool Execution
-
Default parallel: first prepare each one (validation + beforeToolCall), then execute concurrently with Promise.all.
-
If any tool in the batch has executionMode: sequential, the entire batch will be set to sequential.
-
afterToolCall Result can be rewritten; the result is backfilled in the order provided by the model.
-
When all results have terminate, the inner loop ends early.
User Interjection
-
steering: insert interjections during execution, injecting between each round (including after compression and other equal‑length preparations, then retrieve once).
-
follow-up: the agent only retrieves when it is about to stop, letting the outer layer run another cycle.
-
Both queues can be set to one-at-a-time (default) or all.
-
finishTurn Can return “continue”; will run another round even without new messages.
</h2
Compression and Failure Recovery
-
When stopReason is error / aborted: end the current run immediately, without retrying in the loop.
-
After AgentSession run ends: retryable errors → exponential backoff automatic retry (maxRetries).
-
Context overflow → compress and retry; exceeds threshold → compress without retry; the preparation phase between rounds also checks the threshold.
-
Output truncated by length: the entire batch of tool calls is not executed, an error is returned prompting the model to resend.
Stopping Conditions and Sub agent
-
No tool calls, and no steering → inner layer ends.
-
No follow-up, and no continue decision → agent_end.
Extended finishTurn returns end, which can be used to terminate early at any time.
-
The core deliberately does not include sub‑agents or plan mode; use extensions or packages to add them if needed.
Agent Loop Flowchart

prepareNextTurn: After the previous round ends, you can change the context, model, or reasoning level.
Before injecting a message, calculate tool additions/removals and write them as a system message (toolsAdded / toolsRemoved).
prepareRequest: AgentSession projects the session‑tree context, and the virtual model is routed here.
transformContext → convertToLlm → Streaming call.
Tool Execution
-
Default parallel: first prepare each one (validation + beforeToolCall), then execute concurrently with Promise.all.
-
If any tool in the batch has
executionMode: sequential, the entire batch will be set to sequential. -
afterToolCallResult can be rewritten; the result is backfilled in the order provided by the model. -
When all results have
terminate, the inner loop ends early.
User Interjection
-
steering: insert interjections during execution, injecting between each round (including after compression and other equal‑length preparations, then retrieve once).
-
follow-up: the agent only retrieves when it is about to stop, letting the outer layer run another cycle.
-
Both queues can be set to one-at-a-time (default) or all.
-
finishTurnCan return “continue”; will run another round even without new messages.
</h2
Compression and Failure Recovery
-
When stopReason is error / aborted: end the current run immediately, without retrying in the loop.
-
After AgentSession run ends: retryable errors → exponential backoff automatic retry (maxRetries).
-
Context overflow → compress and retry; exceeds threshold → compress without retry; the preparation phase between rounds also checks the threshold.
-
Output truncated by length: the entire batch of tool calls is not executed, an error is returned prompting the model to resend.
Stopping Conditions and Sub agent
-
No tool calls, and no steering → inner layer ends.
-
No follow-up, and no continue decision → agent_end.
When stopReason is error / aborted: end the current run immediately, without retrying in the loop.
After AgentSession run ends: retryable errors → exponential backoff automatic retry (maxRetries).
Context overflow → compress and retry; exceeds threshold → compress without retry; the preparation phase between rounds also checks the threshold.
Output truncated by length: the entire batch of tool calls is not executed, an error is returned prompting the model to resend.
No tool calls, and no steering → inner layer ends.
No follow-up, and no continue decision → agent_end.
Extended finishTurn returns end, which can be used to terminate early at any time.
The core deliberately does not include sub‑agents or plan mode; use extensions or packages to add them if needed.
Core Loop Pseudocode
*Simplified from packages/agent/src/agent-loop.ts
let pending = await getSteeringMessages()
while (true) { // Outer: follow-up
let hasMoreToolCalls = true
while (hasMoreToolCalls || pending.length > 0) { // Inner: tool + steering
if (lastTurn) await prepareNextTurn(lastTurn) // Switch model / context, compressible
inject(declareToolChanges(ctx, pending)); pending = []
await prepareRequest(ctx) // Session projection · model routing
const msg = await streamAssistantResponse(ctx) // transformContext → convertToLlm
if (msg.stopReason === 'error' || msg.stopReason === 'aborted') return end()
const batch = msg.stopReason === 'length'
? failTruncatedToolCalls(msg) // Truncation may cut off, do not execute
: await executeToolCalls(msg) // parallel / sequential
hasMoreToolCalls = msg.toolCalls.length > 0 && !batch.terminate
const d = await finishTurn(lastTurn = {msg, batch}) // end / continue
if (d?.action === 'end') return end()
pending = await getSteeringMessages()
}
const followUp = await getFollowUpMessages()
if (followUp.length) { pending = followUp; continue }
if (explicitContinuation) continue // finishTurn requires another round
break
}
emit('agent_end') // AgentSession: retry / compress
Extension point: where to intervene in the loop
-
prepareNextTurn: between rounds; switch context, model, thinking level.
-
prepareRequest: before request; project context, route model.
-
afterToolCall → tool_result: after tool; extensions can rewrite results.
-
finishTurn → turn_end: end of each round; extensions can be end / continue.
-
before_agent_start: before run starts; can modify system prompts, tools.
-
agent_before_settle: run will stop; extensions can let it continue.
<li
transformContext / convertToLlm: rewrite the context and convert it into model messages.
<li
beforeToolCall → tool_call: before tool; extensions can intercept and modify parameters.
Design Trade-offs
Highlights
-
Less than a thousand lines of loop, the two‑layer structure is instantly understandable.
-
All strategies are callbacks, compression, retry, and routing can all be swapped.
When a tool declares sequential it automatically degrades to serial execution, and concurrency safety is reported by the tool itself.
Truncated tool calls are never executed, to avoid operating with incomplete parameters.
Cost
-
Retries and overflow recovery occur after the run ends; a failure requires re-running the entire request round.
-
No built-in permission approval; security relies on extensions you write yourself.
-
No sub‑agent, no plan mode; complex orchestration must be built by yourself.
Pi reduces the agent loop to the minimum: the loop only handles “invoke model → run tool → collect dialogue”, and all other capabilities are attached via callbacks and extensions.
Source
[1] Pi — Official Repository
https://github.com/earendil-works/pi
[2] Pi — Agent Loop Core Implementation (agent-loop.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent-loop.ts
[3] Pi — Agent State and Invocation (agent.ts)
https://github.com/earendil-works/pi/blob/main/packages/agent/src/agent.ts
[4] Pi — Session Management (agent-session.ts)
https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts
[5] Pi — Project Description
https://github.com/earendil-works/pi/blob/main/README.md
Oh My Pi
Oh My Pi inherits Pi’s double-loop: the outer loop handles follow‑up, and the inner loop continuously invokes the model when there are “tool calls or interjections”.
It adds tool concurrency tiering, interruptible tools, TTSR in‑stream rules, LoopGuards correction, and advisor bypass review on top, while retries and compression are handled by the outer AgentSession.
Loop Structure
-
agentLoop()→runLoop: outer while handles follow‑up, inner while handles tools and steering. -
Inner condition:
hasMoreToolCalls || pendingMessages.length > 0. -
External event stream: agent_start / turn_start / message* / tool_execution* / turn_end / agent_end.
-
AgentSession wraps everything externally, handling persistence, retries, compression, advisor, etc.
Single model invocation
-
transformContext(can be overridden by extensions) →convertToLlm→ normalize into provider message. -
Supported providers can receive live steering directly in the middle of the stream.
-
TTSR: When a rule is hit in the stream, the current output is aborted and restarted after injecting the rule.
-
If a deadline is set, a timeout stops further processing and no subsequent request is sent.
Tool Execution
</h2
-
First, prepare: parameter validation + beforeToolCall hook (can modify parameters / reject).
-
concurrency: shared concurrent execution, exclusive runs exclusively after all preceding tasks are completed.
-
afterToolCall hook processes the result; results are emitted in batch order.
First, prepare: parameter validation + beforeToolCall hook (can modify parameters / reject).
concurrency: shared concurrent execution, exclusive runs exclusively after all preceding tasks are completed.
afterToolCall hook processes the result; results are emitted in batch order.
When a tool cannot be found, suggest a similar tool name to the model.
User Interjection
-
steering: runtime interjection, injected at the tool batch boundary.
-
Interruptible tools (such as wait) will be immediately interrupted by steering; bash etc. only receive soft signals and will not be killed.
-
asides: non‑interrupting side notes (e.g., advisor sticky notes), carried over to the next round.
-
follow‑up: start a new round only after the agent has completely stopped.
Failure Recovery and Correction
-
stopReason is error / aborted: placeholder result for an incomplete toolCall.
-
turn-recovery: instant error retry, wait when quota is limited or switch to a backup model chain.
-
Automatic compression and automatic retry are at the AgentSession layer, checked after the turn ends.
LoopGuards: Inject corrective prompts when the same tool call appears repeatedly.
Stopping Conditions and Sub‑Agents
-
No toolCall and no steering → inner termination.
-
Re-check for delayed steering, asides, follow-up; if any, let the outer layer run another round.
-
None → agent_end.
-
Task tool-derived sub-agent, can run in isolation.
Agent Loop Flowchart
Core Loop Pseudocode
*Simplified from packages/agent/src/agent-loop.ts
let pending = await getSteeringMessages()
while (true) { // Outer loop: follow‑up
while (hasMoreToolCalls || pending.length > 0) { // Inner loop: tool + steering
inject(pending); pending = []
const ctx = convertToLlm(await transformContext(messages))
const msg = await streamAssistantResponse(ctx) // TTSR hit → abort + inject rules + retry
if (msg.stopReason === 'error' || msg.stopReason === 'aborted') {
emitPlaceholderToolResults(msg); return // AgentSession handles retry
}
const prepared = await prepareToolCallDispatch(msg) // validation + beforeToolCall
const results = await executeToolCalls(prepared) // shared concurrency / exclusive barrier
emit('turn_end') // Outer: LoopGuards, compression, advisor
hasMoreToolCalls = msg.toolCalls.length > 0
pending = [...await getSteeringMessages(), ...asides()]
}
pending = [...lateSteering, ...asides(), ...await getFollowUpMessages()]
if (pending.length === 0) break
}
emit('agent_end')
Extension point: where to intervene in the loop
-
transformContext: rewrite the context before each request.
-
convertToLlm: convert AgentMessage into model messages.
-
beforeToolCall: before the tool; can modify parameters, reject, or add context.
-
afterToolCall: after the tool; can rewrite the result.
-
getSteering / FollowUp / Aside: callbacks for retrieving data from the three interjection queues.
-
extensions / hooks runner: extensions can subscribe to all agent events.
-
Continues Pi’s double-loop, with a simple and easy-to-understand structure.
-
TTSR, LoopGuards, advisor incorporate “correction” into the loop.
-
shared / exclusive let tools declare their own concurrency safety.
</ul
Design Trade-offs
Highlights
Interjection split soft and hard: waiting-type tools are interrupted immediately, tools with side effects are not hard‑killed.
Cost
-
agent-loop.ts about 4,000 lines, with many edge case handling.
-
Many capabilities reside in the AgentSession layer (about 13,000 lines), coupling loops with sessions.
-
Restarting TTSR after aborting consumes extra tokens.
Oh My Pi retains Pi’s simple skeleton of “outer follow‑up + inner tool”, and adds correction capability at three points: in the stream, at the tool batch boundary, and at the end of a turn.
Source
[1] oh my pi — Official repository
https://github.com/can1357/oh-my-pi
[2] oh my pi — Agent Loop core implementation (agent-loop.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/agent/src/agent-loop.ts
[3] oh my pi — AgentSession implementation (agent-session.ts)
https://github.com/can1357/oh-my-pi/blob/main/packages/coding-agent/src/session/agent-session.ts
[4] oh my pi — Streaming calls and session mechanism description
https://github.com/can1357/oh-my-pi/blob/main/docs/provider-streaming-internals.md
DeepSeek Harness(dsh)的 ReactLoopAgent is an observable state machine: turn → step → attempt three layers, each boundary is recorded in the Session log.
Input goes through two persistent queues (next-turn / next-step), compression, target continuation, and approval are plugins attached to a few extension points.
Loop Structure
-
driver:
kick()insidewhile (await turn()), continuously invoke turn when there is pending input. -
Three layers: turn (one task) → step (one request + the current tool) → attempt (retry on failure).
-
Each boundary logs: turn/start, step/start, …, step/end, turn/end.
-
Each request is constructed from the log projection (Surface) and not saved in memory as history.
Single model invocation
-
agent/requestwaterfall: the plugin determines provider / model / effort. -
System prompts and runtime context are written as system/message, user/message as needed.
Streamed frame real-time broadcast (agent/assistant-stream), ending with assistant/message.
On failure, falls to assistant/attempt, with agent/request-error determining whether to retry.
Tool Execution
-
executeToolCalls: exclusive calls act as a barrier, while parallel calls enter a bounded scroll pool. -
Execution can overlap, but results and additional context are submitted in model order.
-
Scheduling splits prepare → dispatch → finalize, approvals and hooks are all realized as events.
-
When aborted, calls that were not started are supplemented with error results to ensure replay validity.
User Interjection (Inbox)
-
Two persistent queues: next-turn and next-step.
-
followup → next-turn and wake up; steer → next-step and wake up; inject → next-step without waking up.
-
Before each step, preStep retrieves next-step (a new turn then retrieves another next-turn).
-
Cancel the default clearing of the queue; keepInbox can be retained.
Compression and Failure Recovery
-
Compression is a plugin: attached to agent/pre-step, compress first when exceeding the threshold.
-
When a request reports context overflow, retry after compressing and hanging on
agent/request-error. -
Save the already output prefix on interruption (
interrupted: true); failed attempts do not enter the model-visible history. -
When a step fails, supplement the pending tool call with tool/result before closing the step.
</h2
Stopping Conditions and Outer Orchestration
-
No tool-call → completed; output reaches limit → max-tokens (sticky, not overwritten by subsequent steps).
-
When the tool result includes concludesTurn, it also ends the turn; pre-step rejection → blocked.
-
When it stops and next-step is empty, agent/turn-stopping is triggered, and the plugin can inject input again.
-
The outer orchestration is the plugin: Goal (continues within the same session) and Ralph (creates a new sub‑agent each round).
Agent Loop Flowchart
No tool-call → completed; output reaches limit → max-tokens (sticky, not overwritten by subsequent steps).
When the tool result includes concludesTurn, it also ends the turn; pre-step rejection → blocked.
When it stops and next-step is empty, agent/turn-stopping is triggered, and the plugin can inject input again.
The outer orchestration is the plugin: Goal (continues within the same session) and Ralph (creates a new sub‑agent each round).
Core Loop Pseudocode
*Simplified from packages/core/agent-loop/src/agent.ts
while (await this.turn()) {} // driver: inbox has stock, start the next turn
async turn() {
log('turn/start')
while (true) {
const d = await preStep(target) // acquire inbox + agent/pre-step (compression plugin here)
if (d.kind === 'reject') { ends = 'blocked'; break }
log('step/start')
for (;;) { // attempt loop
const cfg = await waterfall('agent/request') // select provider / model / effort
msg = await stream(buildRequest(surface)) // project request from logs
if (!failed) { log('assistant/message'); break }
log('assistant/attempt')
if ((await waterfall('agent/request-error'))?.kind !== 'retry') throw err
}
if (msg.toolCalls.length) await executeToolCalls() // barrier + parallel pool, submit sequentially
else ends = 'completed'
log('step/end')
if (ends && inbox.nextStep.length === 0) await serial('agent/turn-stopping')
if (ends && inbox.nextStep.length === 0) break
target = 'next-step'
}
log('turn/end', { reason: ends })
return inbox.hasPending
}
Extension point: where to intervene in the loop
-
agent/pre-step: waterfall: before each step, enter / reject, can rewrite the input.
-
agent/request: waterfall: determines the model configuration for this request.
-
agent/request-error: waterfall: decides whether to retry when the request fails.
-
agent/turn-stopping: serial: when the turn is about to stop, new input can be inserted.
-
agent/inbox/*: inserted / claimed / discarded.
-
agent/status: idle ↔ running state transition.
</h2
Design Trade-offs
Highlights
-
Each boundary is a log event; crashes, replays, and forks can all align to a step.
-
The state machine only opens a few waterfall extension points; compression, targets, and approvals are all plugins.
-
Tools execute in parallel and submit sequentially, achieving both speed and replayability.
-
The termination reasons of a turn are recorded in a structured way, facilitating UI and analytics.
Cost
-
Many concepts (turn / step / attempt / Surface / inbox), high learning curve.
-
Behavior depends on the plugin’s waterfall order; troubleshooting requires checking the logs.
-
The project is relatively new, and the API is still evolving rapidly.
DeepSeek Harness turns the agent loop into a “logging state machine”: the loop only advances turn/step, and all strategies are modularized via extension‑point plugins.
Source
[1] DeepSeek Harness — Official Repository
https://github.com/deepseek-ai/deepseek-harness
[2] DeepSeek Harness — Agent Loop Core Implementation (agent.ts)
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/src/agent.ts
[3] DeepSeek Harness — Agent Loop Module Description
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/agent-loop/README.md
[4] DeepSeek Harness — Context Compression Module
https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/compaction/README.md
[5] How Different Harnesses Implement the Agent Loop
https://xhslink.cn/o/8vbCd3WFQjy
More reading
Get MirrorSpeed
Get MirrorSpeed