lox/agent-harness

A minimal, composable Go library for building agentic tool-calling loops

Go

5

18 commits

updated Jul 19, 2026

See the code

See what people are saying

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

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.

lox/agent-harness

A minimal, composable Go library for building agentic tool-calling loops

Go

5

18 commits

updated Jul 19, 2026

See the code

See what people are saying

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

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.

Languages

Go

100.0%