A minimal, composable Go library for building agentic tool-calling loops on top of LLM APIs.
- 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),
)Implements the core agent loop: call the LLM → execute tool calls → feed results back → repeat. Everything else (storage, prompts, routing) is your problem.
- 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/clawprovides a REPL harness for manual testing
- Force a specific LLM provider — use built-in adapters or implement your own Chat()provider
- Manage conversation storage — you serialise the Threadtype however you want
- Construct system prompts — you pass a string
- Orchestrate multi-agent workflows — call Run()from a tool for sub-agents
- Single Run()function, not a framework
- Providerinterface 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 PendingToolCallsfor approval workflows
- Composes naturally with ACP and MCP
- 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, andgo vet
- Implement provider/openaiResponses adapter (stateful continuation + streaming)
- Implement provider/anthropicadapter (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
- 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.
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
}
}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}
}),
)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
_ = runErrOPENAI_API_KEY=... go run ./examples/clawThen 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.