Stripping the Agent Harness to a Single Go While-Loop
Most agent frameworks overcomplicate what is fundamentally an iterative evaluation step. When engineers reach for heavyweight Python DAG frameworks like LangGraph or CrewAI, they often inherit a maze of custom state-graph abstractions, cyclic node definitions, and serialization mechanics just to run an LLM until it decides to stop calling tools. In backend services built for throughput and predictability, importing a massive graph domain-specific language is an unnecessary maintenance liability. I have watched too many production services bog down under framework-level graph machinery when all the architecture required was a bounded while-loop.

This is why I find the design of agentloop compelling. Built by Steve Hanov as an un-opinionated Go 1.21+ runtime, it strips out graph state machines and isolates the single mechanical problem every autonomous agent builder faces: prompt the model, parse tool requests, execute those calls, append the outputs to conversation state, and repeat until done or bounded.
Instead of writing graph transitions, you interact with standard Go runtime boundaries: standard error bubbling, explicit functional options like WithDupCallLimit, and native context.Context propagation for deadlines and cancellation. Idiomatic Go turns out to be a natural fit for agent harnesses because cancellation trees and timeouts belong in the runtime context, not in graph transition metadata.
| Module / Type / Preset | Runtime Responsibility | Upstream Dependencies | Configuration Boundary |
|---|---|---|---|
agentloop.Agent / MustNew |
Primary runner; instantiates bounded while-loop and manages prompt execution. | Go standard library (context, sync), llmhub |
Functional options (WithTools, WithHooks, WithSystemPrompt) |
agentloop.Tool |
Defines tool schema and typed execution handler (Run) with concurrency semantics. |
llmhub.NewTool, encoding/json |
Fields: Def, Run func(ctx, RawMessage), Sequential bool |
agentloop.Hooks |
Intercepts agent lifecycle events for tracing, approvals, telemetry, and steering. | Go standard library (context) |
Callback fields: OnStart, AfterToolCall, stream watchdogs |
agentloop.Conversation |
Retains in-memory message history across multiple turns (Ask, AskStream). |
agentloop.Agent |
Conversation options (WithGenerateOptions, cache keys) |
search.New (Preset) |
Reference multi-step research agent generating source-grounded answers. | agentloop, Serper API (search.WithSerperKey) |
Client API options, API keys |
bash (Preset) |
Reference SWE-bench-style coding agent modeled after mini-swe-agent. |
agentloop, Host/workspace shell environment |
CLI flags (-cwd, -step-limit, -cost-limit) |
By rejecting graph DSLs, agentloop makes the runtime surface transparent. When you need to scale or constrain execution, you do not debug graph edge transitions; you tune Go context deadlines and inspect tool dispatch callbacks directly in your service layer. For teams already operating in Go, this eliminates cross-language wrappers and brings agent execution directly into standard microservice middleware pipelines.
Tracing an Execution Turn from Model Prompt to Tool Feedback
Tracing a request through agentloop exposes how little abstraction an agent harness actually requires to stay robust. When a caller invokes Ask or AskStream on an Agent or Conversation, the harness passes the prompt along with existing message history to the underlying LLM via llmhub. As the model streams its completion, agentloop monitors chunk arrival via stream watchdogs and extracts structured tool calls.
Before invoking any tool, execution passes through the Hooks lifecycle interceptor. If an OnStart or tool-level gate rejects the invocation or if safety bounds are breached, execution halts immediately. Otherwise, agentloop inspects the Sequential field on the registered agentloop.Tool. Stateless tools without strict order dependencies can be evaluated according to the harness runner, while stateful tools—such as a shell session in a coding preset—run strictly sequentially to avoid data races against the workspace.
sequenceDiagram
autonumber
participant Client
participant Harness as agentloop Runtime
participant Hooks as Lifecycle Hooks
participant Hub as llmhub Normalizer
participant Provider as Model Provider (API)
participant Tool as Tool Execution
Client->>Harness: Ask(ctx, prompt)
Harness->>Hooks: OnStart(ev)
Harness->>Hub: Generate(ctx, messages, tools)
Hub->>Provider: Vendor-specific payload
Provider-->>Hub: Token stream / tool_calls
Hub-->>Harness: Normalized ToolCall declarations
Harness->>Hooks: Pre-tool check & watchdog
alt Sequential == true
Harness->>Tool: Run(ctx, args) [Synchronous]
else Concurrent Dispatch
Harness->>Tool: Run(ctx, args) [Dispatched]
end
Tool-->>Harness: Tool output string / exit code error
Harness->>Hooks: AfterToolCall(ev)
Harness->>Harness: Rehydrate Conversation history
Note over Harness: Check turn bounds, duplicates, and context deadline
Harness->>Hub: Generate(ctx, updated_messages, tools)
Hub->>Provider: Follow-up inference
Provider-->>Hub: Final completion text
Hub-->>Harness: Normalized response
Harness-->>Client: Answer (text, sources, cost)
Error handling during tool dispatch preserves execution context rather than aborting prematurely. When a tool returns a non-zero exit code or execution error, agentloop formats the failure into a structured tool result message and appends it directly back into conversation history. This gives the model an opportunity to inspect standard error output and self-correct on the next iteration.
To prevent infinite spinning, the harness bounds execution using two hard mechanisms: Go's context.Context deadline and duplicate-call detection. If a model falls into a degenerative state where it emits identical tool arguments repeatedly, agentloop halts the loop once the threshold configured by WithDupCallLimit is reached.
In testing harness resilience, verifying that context cancellation cleanly stops worker execution is critical. Consider a benchmark test exercising these boundaries under simulated looping conditions:
clock := agentloop.Tool{
Def: llmhub.NewTool("clock", "Current time in RFC 3339", map[string]any{
"type": "object", "properties": map[string]any{},
}),
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return time.Now().UTC().Format(time.RFC3339), nil
},
}
This tool provides a deterministic execution handler that the harness invokes when the model demands system time.
If an artificial or degraded model continuously calls clock with empty JSON arguments, the harness tracks consecutive identical invocations. When that tally exceeds the limit, the while-loop breaks immediately, surfacing a bounded error rather than burning inference budget. Similarly, if the parent ctx deadline fires mid-stream or during tool execution, agentloop unwinds the call stack and releases goroutines cleanly.
The Three Anchors: llmhub Normalization, Hooks Interception, and Directory-Based Skills
The architecture of agentloop rests on three structural components: vendor normalization via llmhub, operational observability through Hooks, and dynamic context loading via directory-based skills.

First, agentloop delegates model interaction to llmhub, an abstraction package that normalizes chat completions and function definitions across OpenAI, Anthropic, Gemini, Ollama, xAI, and custom HTTP endpoints. Relying on an intermediate abstraction layer over direct vendor SDKs eliminates vendor-specific boilerplate from the loop, allowing you to swap backends without rewriting tool-calling harnesses.
However, this abstraction layer carries structural tradeoffs. When upstream providers release novel protocol additions—such as computer-use interfaces or provider-specific reasoning token fields—a custom abstraction layer invariably lags behind official client libraries. Furthermore, subtle protocol differences can leak across the boundary.
Second, lifecycle control is exposed through the public Hooks struct. Rather than subclassing or implementing complex visitor patterns, you provide callback functions that tap directly into runtime events.
flowchart TD
StartStep["Start Iteration Turn"] --> WatchdogCheck{"Stream Watchdog Active?"}
WatchdogCheck -- Yes --> CheckLatency["Evaluate TTFT & Inter-Token Latency"]
CheckLatency -- Stalled --> AbortRun["Cancel context.Context & Terminate"]
WatchdogCheck -- No --> CheckApproval{"Approval / Gating Hook?"}
CheckLatency -- OK --> CheckApproval
CheckApproval -- Rejected --> SkipExecution["Return Error Result to Context"]
CheckApproval -- Approved --> SkillInj{"Directory Skill Required?"}
SkillInj -- Yes --> LoadSkill["Inject Modular Skill File into History"]
SkillInj -- No --> ExecTool["Execute Tool.Run(ctx, args)"]
LoadSkill --> ExecTool
ExecTool --> PostHook["Invoke Hooks.AfterToolCall"]
PostHook --> CostTracker["Tally Token Usage & Cost Ceilings"]
CostTracker --> CheckBudget{"Budget Ceiling Exceeded?"}
CheckBudget -- Yes --> HaltRun["Halt Loop Execution"]
CheckBudget -- No --> AppendHistory["Rehydrate Conversation State"]
The reference search and bash coding agents do not use private runtime internals; they are implemented entirely through this public Hooks interface. By wiring callbacks into OnStart and AfterToolCall, you can log execution traces, implement human-in-the-loop checkpoints, update streaming UIs, or aggregate token expenses without modifying the core while-loop.
Third, agentloop approaches prompt construction through directory-based skills: "skills, not prompt stuffing." Traditional agent harnesses often cram extensive operational manuals and API documentation directly into a monolithic system prompt. This inflates baseline token overhead on every single turn and degrades instruction-following quality as context fills up. By organizing capabilities into modular skill directories, procedural instructions are dynamically surfaced to the model only when the relevant tool or domain is engaged. This keeps the baseline conversation compact while maintaining deep procedural execution capabilities.
Testing the Safe Loop Claim: Why Exact Duplicate Call Detection Fails in Practice
The headline safety mechanism in agentloop is its duplicate call limit, configured through functional options like WithDupCallLimit. The premise is straightforward: autonomous agents commonly get stuck executing the exact same failed action in an unconstrained cycle, so tracking identical tool arguments and breaking the loop arrests runaway spend. In practice, relying on exact matching across raw JSON argument payloads is an extraordinarily brittle defense against degenerative execution.
Exact matching checks whether the serialized string representation or parsed key-value structure of (tool_name, tool_arguments) matches a prior turn. This catches naive crashes—such as a model repeating an empty lookup or hitting an unhandled error without changing state—but modern frontier models rarely fail so cleanly. When an LLM struggles to parse an error or find a missing resource, it routinely alters non-semantic elements. It will mutate whitespace in shell commands, alternate between ls -l and ls -la, or oscillate between two equivalent search queries (A -> B -> A -> B). Because each step differs by a byte or alternates in a cycle of length two, the consecutive duplicate counter resets. The model continues burning inference budget completely unimpeded by the safety threshold.
At the same time, exact string checking introduces severe false positives into legitimate autonomous behaviors. Many production tasks require an agent to poll an external service—checking whether an asynchronous batch evaluation finished, awaiting a database migration, or polling a webhook listener. When an agent calls an inspection tool like check_job_status(job_id) with identical arguments across three turns, a naive duplicate-call counter assumes the agent has degraded into an infinite spin.
Without dedicated hook middleware to intercept and filter specific idempotent tools, exact duplicate call checking punishes correct polling while letting semantic looping run free. If you deploy an agent into production, you cannot treat WithDupCallLimit as a complete safeguard. A realistic loop detector must analyze cyclical call sequences and consecutive error states, rather than assuming an errant model will obligingly repeat the exact same JSON payload forever.
Where the Harness Fractures: In-Process Ephemeral Memory and Unsandboxed Shell Execution
When evaluating agentloop from a systems engineering and reliability perspective, the trade-off for its simplicity becomes clear. The runtime holds conversation history in an in-memory slice inside the agentloop.Conversation struct. While this makes local testing effortless, it creates an immediate barrier to horizontal scalability. In a multi-tenant backend with multiple worker nodes behind a load balancer, subsequent turns in a multi-step workflow will fail unless you enforce sticky sessions or manually serialize state. If a container restarts or an orchestrator shifts a pod mid-run, the conversational state vanishes. Teams must build custom persistence around (c) Messages() to snapshot and rehydrate history across turns.
This in-memory state model also worsens token bleed. In autonomous coding workflows, tools routinely output hundreds of lines of compiler errors, test logs, or minified assets. Because agentloop appends tool results directly back into the conversational message array, a single verbose command can instantly saturate context limits, degrading model reasoning and ballooning inference costs.
The operational risk escalates when examining the reference coding agent preset modeled on SWE-bench's mini-swe-agent. The tool interface executes shell commands directly in a local directory using standard system execution primitives:
session := &shellSession{dir: "/tmp/workspace"} // your type: holds cwd, env, history
sh := agentloop.Tool{
Def: llmhub.NewTool("shell", "Run one command in the workspace", map[string]any{
"type": "object",
"properties": map[string]any{
"command": map[string]any{"type": "string"},
},
"required": []string{"command"},
}),
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return session.Run(ctx, string(args)) // reads and mutates session state
},
Sequential: true, // stateful: strict call order, never overlaps anything
}
This tool executes arbitrary shell commands inside the specified workspace directory with sequential concurrency guarantees.
Running this pattern without an external sandbox means executing arbitrary shell strings with the ambient permissions of the host process. Any prompt injection or hallucinated command (rm -rf, outbound network exfiltration, or environment variable inspection) runs directly on your host or worker container. I have explored this boundary extensively in discussions around isolating execution layers, such as evaluating Anthropic srt or Docker Containers for Securing Local Agent Execution. Running shell tools without kernel-isolated boundaries is an unacceptable operational risk.
stateDiagram-v2
[*] --> Initialized
Initialized --> StreamingInference: Ask / AskStream
StreamingInference --> AbortedByWatchdog: Watchdog Timeout Exceeded
StreamingInference --> ToolExecuting: Parsed ToolCall Declaration
StreamingInference --> Completed: Final Text Completion Emitted
ToolExecuting --> StreamingInference: Result Appended to History
ToolExecuting --> TerminatedWithBudgetExceeded: Step / Cost Limit Reached
AbortedByWatchdog --> [*]
TerminatedWithBudgetExceeded --> [*]
Completed --> [*]
If a stream watchdog treats the total duration of a turn as an idle timeout without measuring inter-token emission, any long-running tool invocation or complex reasoning pause will trip the watchdog and abort a healthy execution run.
Hardening agentloop for Isolated Production Services
Taking agentloop from a clean local runtime to an enterprise-grade service requires wrapping it in defensive middleware. You should never expose raw harness execution directly to public HTTP handlers or run untrusted tool actions on host infrastructure. The core engine excels at driving the iterative while-loop; your production wrapper must supply state persistence, strict output sanitization, and execution isolation.
To deploy this reliably, you must decouple the agent loop from synchronous web requests into an asynchronous worker tier. For instance, teams building an asynchronous background code janitor can establish a service that consumes GitHub issue webhooks and dispatches tasks to an Asynq or Temporal queue. The worker process instantiates agentloop using the bash coding preset, but crucially wires session.Run to an ephemeral Docker container client or Firecracker microVM rather than host execution. As the agent navigates the codebase and attempts fixes, an event-logging Hooks interceptor snapshots diffs and structured turn events directly into a PostgreSQL database. To protect the context window, you must place a strict 20KB head-tail byte-truncation hook on stdout and stderr, preventing catastrophic token bleed when the agent triggers verbose compiler outputs or test suite dumps.
hooks := agentloop.Hooks{
OnStart: func(ctx context.Context, ev *agentloop.Event) error {
fmt.Println("run:", ev.Query)
return nil
},
AfterToolCall: func(ctx context.Context, ev *agentloop.Event) error {
fmt.Printf("%s -> %.80s (%v)\n", ev.Call.Name, ev.Result, ev.Duration)
return nil
},
}
ag := agentloop.MustNew(llm, agentloop.WithTools(clock), agentloop.WithHooks(hooks))
This configuration registers lifecycle hooks to intercept and log tool execution metadata on every iteration.
For analytical workloads, consider structuring a deterministic research pipeline with dynamic skills. In this pattern, agentloop coordinates multi-step investigations using the search preset backed by Serper. Instead of relying on a static prompt, you utilize directory-based skills to load specialized research templates on demand, standardizing extraction across OpenAI and Anthropic through llmhub. To avoid runaway billing, the harness uses cost-accounting hooks to enforce dollar ceilings. The primary operational trap here is query oscillation: an agent generating minor variations of an identical query will bypass WithDupCallLimit. Implementing a custom hook that tracks recent semantic embeddings or normalized query keys prevents repetitive search calls from burning API credits.
Finally, engineers designing an audited internal operations assistant can leverage the harness for database diagnostics and infrastructure health checks. By connecting agentloop to read-only SQL inspection tools and routing through llmhub to a private Ollama deployment, engineers obtain automated incident analysis. Because operational actions require oversight, an interactive CLI approval hook pauses execution before running potentially impactful commands. Decoupling model stream watchdogs from the interactive review pause is mandatory here; otherwise, the context deadline or watchdog timer will kill the entire session while the operator reads the prompt and considers the approval.
Resources
Updated 2026-10-24 by Mehran Mozaffari.
Related posts
1 November 2026
Code Mode vs Function Calling for Multi-Step Agents
1 November 2026
Offline Trajectory Scoring Replaces the Prompt Engineering Treadmill
1 November 2026
Should You Build Your Enterprise Agent Semantic Layer on NVIDIA Auto Ontology? Not Yet
31 October 2026
Unreal Engine Skills for Claude Code: How Epic Bypassed the Binary Asset Barrier to Mutate Live Viewports
30 October 2026
Agent Memory Was an Indexing Problem All Along
29 October 2026
How 3D OS Turns a Coding Agent into a Six-Department Animation Studio Without a GUI
