π‘ AI Labo Benchmark Series β Experiment
E04(Orchestration & Control Flow)This post is part of our empirical benchmark series testing AI coding agent harness patterns inside network-isolated Proxmox VMs against a hidden 20-case grading suite.
Metadata & Experiment Identity
- Experiment ID:
E04(Backlog CandidateC001) - Run ID:
e04-vm208-20261027-01 - Sandbox Environment: Proxmox Isolated VM
VM208(ai-e04-208, 0 NICs,virtio-serialQGA relay,systemdUID separation) - Model:
gemini-3.8-flash(thinkingLevel: LOW) - Pattern Under Test: "Loops and Graphs: How to Stop Babysitting Agents and Only Approve the Last Step" (@hanakoxbt) β Single Free-Form ReAct Loop vs. Stage-Gated Directed Graph
1. Executive Summary (TL;DR)
When building an AI coding agent, the default instinct is to give a single free-form ReAct Loop (Single Loop) access to every tool (inspect_all, edit_file, run_tests, SUBMIT) and let the LLM figure out what to inspect, which file to patch next, when to run tests, and when to stop.
On multi-module tasks, that design suffers from two compounding taxes:
- Orchestration Overhead: Over half of the LLM calls are spent deciding how to sequence steps rather than writing code.
- Snowballing Context History: Every file read, test log, and previously generated code patch stays in the chat history, inflating prompt tokens on every subsequent turn.
Inspired by the "Loops and Graphs" patternβwhich separates inner local Generator + Verifier loops from an outer deterministic Directed Graphβwe ran a controlled A/B experiment on a 3-module order & invoice pipeline (parser.py, pricing.py, report.py) containing 6 coupled bugs inside isolated VM VM208:
- Mode A (
SingleLoopRunner): A single free-form agent loop with full tool access across all 3 modules and persistent conversation history. - Mode B (
DirectedGraphRunner): A 4-node deterministic Directed Graph (node_1_parserβnode_2_pricingβnode_3_reportβnode_4_terminal_approval_gate). Each node receives a fresh, minimal context for its single target module, runs a deterministic stage verifier immediately upon edit, and resets chat history before advancing to the next node.
Key Result
Both modes achieved a perfect 20 / 20 (100%) pass on the hidden grading suite and generated virtually the exact same volume of Python code (1,188 vs. 1,189 output tokensβa 1-token difference!). Yet moving workflow orchestration into a stage-gated Directed Graph cut LLM calls by -57.1% (7 β 3 turns), cumulative input tokens by -74.4% (23,322 β 5,966 tokens), execution time by -48.0% (1.92x faster), and API cost by -59.3% ($0.0220 β $0.0089).
| Metric | Mode A: Single Free-Form Loop (SingleLoop) |
Mode B: Stage-Gated Directed Graph (DirectedGraph) |
Delta / Improvement |
|---|---|---|---|
| Hidden Grading Suite Score (20 Cases) | 20 / 20 (100%) |
20 / 20 (100%) |
Identical 100% accuracy |
LLM Calls (model_calls) |
7 calls |
3 calls |
-57.1% (4 orchestration turns eliminated) |
Actual File Edits (file_edits) |
3 edits |
3 edits |
Identical coding work |
Cumulative Input Tokens (promptTokenCount) |
23,322 tokens |
5,966 tokens |
-74.4% (~4x context compression) |
Cumulative Output Tokens (candidatesTokenCount) |
1,188 tokens |
1,189 tokens |
Identical (+1 token) |
Elapsed Execution Time (elapsed_seconds) |
36.69 sec |
19.07 sec |
-48.0% (1.92x faster) |
Estimated API Cost (estimated_micro_usd) |
$0.0220 (21,950 micro-USD) |
$0.0089 (8,930 micro-USD) |
-59.3% cost reduction |
2. Experiment Architecture: Single Loop vs. Directed Graph
+-------------------------------------------------------------------------+
| Proxmox Isolated VM208 (0 NICs, virtio-serial QGA, systemd UID split) |
| Target Task: 3-Module Order & Invoice Pipeline (6 Coupled Bugs) |
| (parser.py β pricing.py β report.py) |
| |
| +-------------------------------------------------------------------+ |
| | Mode A: SingleLoopRunner (Traditional Free-Form ReAct Loop) | |
| | - Turn 1: inspect_all (1,652 in / 8 out) | |
| | - Turn 2: run_tests [Fail] (2,848 in / 8 out) | |
| | - Turn 3: edit parser.py (2,883 in / 357 out) | |
| | - Turn 4: edit pricing.py (3,324 in / 591 out) | |
| | - Turn 5: edit report.py (4,000 in / 209 out) | |
| | - Turn 6: run_tests [Pass] (4,297 in / 8 out) | |
| | - Turn 7: SUBMIT (4,318 in / 7 out) | |
| | --> 7 calls | 23,322 in | 1,188 out | 36.69s | 20/20 Pass | |
| +-------------------------------------------------------------------+ |
| |
| +-------------------------------------------------------------------+ |
| | Mode B: DirectedGraphRunner (Stage-Gated Graph + Context Reset) | |
| | - Node 1 [Fresh]: patch parser.py + verify_parser (1,947 in) | |
| | - Node 2 [Reset]: patch pricing.py + verify_pricing (2,174 in) | |
| | - Node 3 [Reset]: patch report.py + verify_report (1,845 in) | |
| | - Node 4 [Gate]: Terminal Approval Gate (AUTO_SUBMIT, 0 tokens) | |
| | --> 3 calls | 5,966 in | 1,189 out | 19.07s | 20/20 Pass | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
The 6 Coupled Bugs Across the 3-Stage Pipeline
- Stage 1 (
parser.pyβparse_items):- Allowed
qty == 0and Pythonbool(isinstance(True, int) == True) instead of rejecting non-positive/boolean quantities withValueError. - Failed to normalize SKU strings (
.strip().upper()) and mutated caller input dictionaries in-place.
- Allowed
- Stage 2 (
pricing.pyβcompute_invoice):- Ignored conflicting
unit_pricevalues for the same SKU instead of raisingValueError. - Applied
WELCOME500coupon before checking the volume discount and used> 10000instead of inclusive>= 10000βwiping out the 10% volume discount on exact10,000orders (and silently ignoring invalid coupon codes). - Used Python banker's rounding (
round()) instead of integer floor truncation (// 10) for discounts and tax.
- Ignored conflicting
- Stage 3 (
report.pyβformat_summary):- Hardcoded item quantity as
x1and read the wrong key (invoice.get("discount", 0)instead ofdiscount_total), omitting the mandatory signed zero format (DISCOUNT: -0).
- Hardcoded item quantity as
3. Turn-by-Turn Token Anatomy: Where Did 74.4% of Input Tokens Go?
Inspecting all 10 recorded LLM calls (seq 0β6 for Mode A, seq 7β9 for Mode B) reveals why free-form loops waste tokens even when the model makes zero coding mistakes:
| Mode | Call Seq | Node / Action | Input Tokens | Output Tokens | Context State |
|---|---|---|---|---|---|
| Mode A | 0 |
inspect_all |
1,652 |
8 |
Spec only |
| Mode A | 1 |
run_tests (initial fail) |
2,848 |
8 |
+ All 3 initial files appended |
| Mode A | 2 |
edit_file (parser.py) |
2,883 |
357 |
+ Initial test failure trace appended |
| Mode A | 3 |
edit_file (pricing.py) |
3,324 |
591 |
+ Full patched parser.py in history |
| Mode A | 4 |
edit_file (report.py) |
4,000 |
209 |
+ Full patched pricing.py in history |
| Mode A | 5 |
run_tests (green) |
4,297 |
8 |
+ Full patched report.py in history |
| Mode A | 6 |
SUBMIT |
4,318 |
7 |
2.6x larger than Turn 1 just to say SUBMIT |
| Mode A Total | β | 7 turns | 23,322 |
1,188 |
Snowballing history across all 7 turns |
| Mode B | 7 |
node_1_parser + auto-verify |
1,947 |
366 |
Fresh context (parser.py only) |
| Mode B | 8 |
node_2_pricing + auto-verify |
2,174 |
600 |
Context reset (pricing.py only) |
| Mode B | 9 |
node_3_report + auto-verify β AUTO_SUBMIT |
1,845 |
223 |
Context reset (report.py only) |
| Mode B Total | β | 3 turns (+ 0-token gate) | 5,966 |
1,189 |
Flat ~2,000-token context on every node |
Notice the smoking gun on Call 6: Mode A paid 4,318 input tokens just to emit a 7-token {"op": "SUBMIT"} JSON object. Meanwhile, Mode B kept every single node under 2,200 input tokens and transitioned to AUTO_SUBMIT deterministically in Python the instant verify_report_stage passed.
4. All 20 Hidden Grading Suite Results (labjudge04)
Both modes achieved 20 / 20 on the hidden evaluation suite executed by the isolated labjudge04 user:
| Stage | Test Case IDs | Invariants Verified | Mode A (SingleLoop) |
Mode B (DirectedGraph) |
|---|---|---|---|---|
Stage 1 (parser.py) |
parser_01 to parser_06 |
Trim/uppercase SKU, non-mutation, reject []/bool/float/qty <= 0, allow unit_price == 0 |
6 / 6 PASS |
6 / 6 PASS |
Stage 2 (pricing.py) |
pricing_01 to pricing_08 |
First-seen SKU order, price conflict check, 9999 vs 10000 boundary, volume-before-coupon order, floor // 10, coupon cap, unknown coupon rejection |
8 / 8 PASS |
8 / 8 PASS |
Stage 3 & E2E (report.py) |
e2e_01 to e2e_06 |
DISCOUNT: -0 signed zero, multi-SKU summary, free-item invoice, floor tax E2E, line order, cross-stage ValueError propagation |
6 / 6 PASS |
6 / 6 PASS |
| Total | 20 Hidden Cases | β | 20 / 20 (100%) |
20 / 20 (100%) |
5. Practical Takeaway: Separate Reasoning (Loops) From Control Flow (Graphs)
- Don't ask an LLM to be a state machine: Let the LLM write patches inside small, bounded loops, and move deterministic transitions ("load file β run stage test β advance on green β submit") into regular Python/TypeScript graph edges.
- Use deterministic stage verifiers as context compressors: Once a module's contract verifier passes, drop the raw editing history before moving to the next module. (Note: As we show in
E05, only split into graph nodes when each stage has a deterministic code verifierβnever split into role-based subagents passing lossy natural-language summaries!)
π Continue Reading the Series
- π Master Benchmark Hub (All 41 Patterns Ranked): I Benchmarked 41 AI Agent Harness Patterns in Isolated VMs: How to Cut Tokens by 90% Without Losing Accuracy
- π‘οΈ Methodology & Sandbox Architecture: How to Benchmark AI Agents Without Eval Cheating or API Key Leaks: Designing a Proxmox & systemd Sandbox
- πͺ Next Companion Post (
E05): Why Role-Based Subagents Slow Down Coding Agents: Subagent Folding + Progressive Skill Disclosure (2.66x Faster) - π₯ Rank #1 Deep Dive (
E20): Stop Your Coding Agent From Stepping on the Same Rake Twice: Cold-Start Exploration vs. a 5-ItemMEMORY.md
Comments
Join the discussionNo comments yet. Be the first to comment.