The dual-loop architecture
The dual-loop architecture
The production agent processes a user request in 500ms-3s: receive query, retrieve context, call the LLM, parse the response, return the answer. This is the task loop — it runs on every request, optimized for latency, scoped to a single user interaction. If the agent fails on this request, the damage is one bad response.
The improvement loop operates on a different timescale entirely. It collects production logs over hours or days, extracts recurring failure patterns, generates hypotheses about what changes would fix them, tests those hypotheses against held-out data, and stages improvements for review. A single execution of the improvement loop might take 10-30 minutes and analyze 10,000 logged interactions. If the improvement loop produces a bad change and it passes review, the damage propagates to every subsequent request.
This asymmetry — fast but bounded damage versus slow but systemic damage — is why the two loops must be architecturally separated.
The task loop in detail
The task loop is what users interact with. Its structure is familiar: receive input, process through a pipeline of model calls and tool invocations, return output. The addition for self-improving systems is instrumentation — the task loop must emit structured logs that the improvement loop can consume.
import time
import uuid
import json
from dataclasses import dataclass, asdict
from openai import OpenAI
@dataclass
class ExecutionTrace:
trace_id: str
timestamp: float
user_input: str
system_prompt_version: str
model: str
raw_output: str
parsed_output: dict
latency_ms: float
input_tokens: int
output_tokens: int
tool_calls: list[dict]
quality_signals: dict
class TaskLoop:
def __init__(self, prompt_registry, log_store):
self.client = OpenAI()
self.prompt_registry = prompt_registry
self.log_store = log_store
def execute(self, user_input: str, context: dict = None) -> dict:
trace_id = str(uuid.uuid4())
start = time.time()
active_prompt = self.prompt_registry.get_active()
messages = [
{"role": "system", "content": active_prompt["content"]},
{"role": "user", "content": user_input},
]
response = self.client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0,
)
latency_ms = (time.time() - start) * 1000
result = response.choices[0].message.content
usage = response.usage
trace = ExecutionTrace(
trace_id=trace_id,
timestamp=time.time(),
user_input=user_input,
system_prompt_version=active_prompt["version"],
model="gpt-4o",
raw_output=result,
parsed_output={"response": result},
latency_ms=latency_ms,
input_tokens=usage.prompt_tokens,
output_tokens=usage.completion_tokens,
tool_calls=[],
quality_signals={},
)
self.log_store.write(asdict(trace))
return {"trace_id": trace_id, "response": result}The ExecutionTrace captures everything the improvement loop needs: what prompt version was active, what the model produced, how long it took, how many tokens it consumed, and — critically — a quality_signals field for downstream quality labels. Quality signals might come from explicit user feedback (thumbs up/down), implicit signals (did the user retry the query? did they edit the response?), or automated evaluators (an LLM-as-judge scoring the output).
The improvement loop in detail
The improvement loop runs on a schedule — daily, weekly, or triggered by quality metric degradation. It operates in five stages:
Stage 1: Log collection. Pull all execution traces since the last improvement cycle. Filter to traces with quality signals (not all traces will have feedback). A system processing 10,000 requests/day with a 5% feedback rate yields 500 labeled traces per day.
Stage 2: Pattern extraction. Cluster failures by root cause. The meta-agent reads a sample of low-quality traces and categorizes them: "fails on multi-part questions" (23% of failures), "hallucinates when context is insufficient" (18%), "wrong output format on edge cases" (12%). This step costs ~$0.20-0.50 in API calls for 100 failure traces.
Stage 3: Hypothesis generation. For each failure pattern, the meta-agent generates candidate fixes. For "fails on multi-part questions," it might propose: "Add instruction: 'For questions with multiple parts, address each part separately with a labeled section.'" For "hallucinates when context is insufficient," it might propose: "Add instruction: 'If the provided context does not contain enough information to answer, say so explicitly rather than inferring.'" Each hypothesis is a concrete prompt diff.
Stage 4: Testing on held-out data. Each candidate is evaluated against a held-out test set that was not used during hypothesis generation. The test set must include both examples of the failure pattern (to verify the fix works) and examples of currently-passing cases (to detect regressions). A candidate that fixes 80% of the targeted failures but regresses 5% of passing cases may not be worth deploying.
class ImprovementLoop:
def __init__(self, meta_agent, prompt_registry, test_set: list[dict]):
self.meta_agent = meta_agent
self.prompt_registry = prompt_registry
self.test_set = test_set
def run_cycle(self) -> dict:
current = self.prompt_registry.get_active()
current_score = self.meta_agent.evaluate_candidate(current["content"])
failures = self.meta_agent.analyze_failures(window_hours=24)
if not failures:
return {"action": "no_change", "reason": "no failure patterns detected"}
candidates = []
for _ in range(3):
candidate = self.meta_agent.generate_candidate(failures)
score = self.meta_agent.evaluate_candidate(candidate["improved_prompt"])
candidates.append({**candidate, "score": score})
best = max(candidates, key=lambda c: c["score"]["accuracy"])
improvement = best["score"]["accuracy"] - current_score["accuracy"]
if improvement < 0.02:
return {
"action": "no_change",
"reason": f"best candidate only +{improvement:.1%} over current",
"current_score": current_score,
"best_candidate_score": best["score"],
}
return {
"action": "propose",
"candidate": best,
"current_score": current_score,
"improvement": improvement,
"review_required": True,
}Stage 5: Staged deployment. If the best candidate passes the improvement threshold (typically 2-5 percentage points above the current version), it is staged for human review. The output is a diff showing what changed, the test results, and the failure patterns it addresses. A human approves or rejects the change. Approved changes enter canary deployment — 5% of traffic initially, scaling to 100% if production metrics hold.
The separation principle
The meta-agent that proposes improvements must be architecturally separate from the production agent. It runs in a different process, uses a different set of credentials, and has different access patterns:
- Production agent: read access to user data (scoped per request), write access to response endpoints, no access to its own configuration.
- Meta-agent: read access to production logs (aggregated, potentially anonymized), write access to the staging prompt registry, no access to user data, no access to production endpoints.
This separation is not just a security measure — it prevents feedback loops. If the production agent could modify its own prompt mid-request based on the current user's feedback, a single adversarial user could manipulate the system prompt for all subsequent users. The improvement loop's batched, offline, human-reviewed process is the firewall.
The review gate
Every proposed change surfaces as a structured diff for human review:
def format_review(proposal: dict) -> str:
lines = [
f"Improvement Proposal — {proposal['candidate']['created_at']}",
f"Current accuracy: {proposal['current_score']['accuracy']:.1%}",
f"Proposed accuracy: {proposal['candidate']['score']['accuracy']:.1%}",
f"Improvement: +{proposal['improvement']:.1%}",
"",
"Changes made:",
proposal["candidate"]["changes_made"],
"",
"Rationale:",
proposal["candidate"]["rationale"],
"",
"Failure patterns addressed:",
]
for pattern in proposal.get("failure_patterns", []):
lines.append(f" - {pattern['pattern']} ({pattern['frequency']} occurrences)")
return "\n".join(lines)The review gate is the control surface. In early deployments, every change goes through manual review. As confidence in the improvement loop grows, the threshold can shift: changes below 5% improvement require review; changes above 5% with no regressions can auto-deploy to canary. Fully automated deployment — no human in the loop — is the end state for mature systems, but most production deployments in 2026 keep the review gate active.
The cost structure of the dual-loop architecture: the task loop costs scale linearly with traffic (API calls per request). The improvement loop costs are fixed per cycle regardless of traffic — a daily cycle analyzing 500 traces and testing 3 candidates costs $3-8 in API calls. For a system spending $500/day on production LLM calls, the improvement loop adds 0.6-1.6% overhead.