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 /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 -
/resume with explicit
PendingToolCallsfor approval workflows -
Extract the reusable harness core (
Run, messages, tools, provider interface) -
Add /resume support (
Stopd,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.ToolAction, nil
}
return harness.ToolActionContinue, nil
}),
)
if result != nil {
thread.Append(result)
}
if err != nil {
return err
}
if result.StopReason == harness.Stopd {
// 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
_ = runErr
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.