lox/agent-harness
A minimal, composable Go library for building agentic tool-calling loops - lox/agent-harness
5 stars · 0 forks
README
🧰 agent-harness A minimal, composable Go library for building agentic tool-calling loops on top of LLM APIs. Requirements
Go 1.26+
result, err := harness.Run(ctx, provider, harness.WithSystem("You are a helpful assistant."), harness.WithMessages(thread.Messages...), harness.WithTools(tools...), harness.WithModel("claude-opus-4-6"), harness.WithMaxSteps(10), ) What it does Implements the core agent loop: call the LLM → execute tool calls → feed results back → repeat. Everything else (storage, prompts, routing) is your problem. Status
Core harness loop, hooks, and thread state are implemented Unit tests are in place for core loop behaviour and pause/resume OpenAI Responses API adapter is implemented (provider/openai) Anthropic provider adapter is implemented (provider/anthropic) Optional file-backed memory, recall tools, capture, and promotion are implemented (memory) examples/claw provides a REPL harness for manual testing
What it doesn't do
Force a specific LLM provider — use built-in adapters or implement your own Chat() provider Manage conversation storage — you serialise the Thread type however you want Construct system prompts — you pass a string Orchestrate multi-agent workflows — call Run() from a tool for sub-agents
Design
Single Run() function, not a framework Provider interface with one method Tools bundle schema + execution in one place Hooks for approval gates (WithBeforeTool), streaming (WithOnDelta), and observability (WithEventHandler) Progressive disclosure via WithToolFilter Pause/resume with explicit PendingToolCalls for approval workflows Composes naturally with ACP and MCP
Milestones
Extract the reusable harness core (Run, messages, tools, provider interface) Add pause/resume support (StopPaused, PendingToolCalls, Thread.ResolvePending) Add lifecycle hooks and event emission Stabilise core loop semantics with unit tests Add a runnable REPL example under examples/claw Add CI for go test, go test -race, and go vet Implement provider/openai Responses adapter (stateful continuation + streaming) Implement provider/anthropic adapter (non-streaming + streaming) Add provider integration tests using local HTTP test servers Add provider-neutral finish states, continuation, and cache-aware usage Add optional file-backed memory and recoverable tool transcripts
Documentation
docs/architecture.md — API shape, loop lifecycle, and state model docs/runner.md — optional helper for starting/stopping active runs docs/providers.md — provider adapter contracts and type mappings docs/memory.md — optional file-backed memory package, recall tools, and promotion primitives docs/research.md — research notes and design rationale
The docs/ directory is the source of truth for design and implementation guidance. Pause And Resume thread := harness.NewThread() thread.AddUser("Delete old preview deployments")
result, err := harness.Run(ctx, provider, harness.WithMessages(thread.Messages...), harness.WithTools(tools...), harness.WithBeforeTool(func(ctx context.Context, call harness.ToolCall) (harness.ToolAction, error) { if call.Name == "delete_deployment" { return harness.ToolActionPause, nil } return harness.ToolActionContinue, nil }), ) if result != nil { thread.Append(result) } if err != nil { return err }
if result.StopReason == harness.StopPaused { // approval flow happens outside the harness err = thread.ResolvePending(ctx, func(ctx context.Context, call harness.ToolCall) (*harness.ToolResult, error) { return executeApprovedTool(ctx, call) }) if err != nil { return err }
result, err = harness.Run(ctx, provider, harness.WithMessages(thread.Messages...), harness.WithTools(tools...), ) if result != nil { thread.Append(result) } if err != nil { return err } } Progressive Tool Disclosure result, err := harness.Run(ctx, provider, harness.WithMessages(thread.Messages...), harness.WithTools(readTool, writeTool), harness.WithToolFilter(func(step int, _ []harness.Message) []harness.Tool { if step == 0 { return []harness.Tool{readTool} } return []harness.Tool{readTool, writeTool} }), ) Cancelling Active Runs Use runner.Runner when you want to interrupt an in-flight run from external control input such as a user saying "stop". r := runner.New()
done, err := r.Start(context.Background(), thread.ID, func(ctx context.Context) error { result, err := harness.Run(ctx, provider, harness.WithMessages(thread.Messages...), harness.WithTools(tools...), ) if result != nil { thread.Append(result) } return err }) if err != nil { return err }
// elsewhere: control-plane stop command if strings.EqualFold(strings.TrimSpace(userInput), "stop") { r.Stop(thread.ID) }
runErr := <-done _ = runErr Running Claw Example OPENAI_API_KEY=... go run ./examples/claw Then type prompts or control commands (/stop, /history, /tools, /memory, /remember <text>, /new, /quit). The example enables file-backed memory by default at ~/.agent-harness/claw; pass --memory-dir "" to disable it. See docs/architecture.md for the primary implementation guide.