harness

package module
v0.0.0-...-9376b9c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: MIT Imports: 8 Imported by: 0

README

harness

A Go library for calling different AI coding agent CLIs (Docker Agent, Claude Code, Pi, Codex, opencode) through a unified interface. Switch between agents by changing a single line of code.

Install

go get github.com/rumpl/harness

Quick start

package main

import (
	"context"
	"fmt"

	"github.com/rumpl/harness"
	"github.com/rumpl/harness/dockeragent"
)

func main() {
	// Create a provider — swap this line to switch agents.
	p := dockeragent.New("coder")

	var sessionID string
	// Run the agent and handle streaming events.
	harness.Run(context.Background(), p, "Explain goroutines", func(ev harness.Event) {
		switch ev.Type {
		case harness.EventSessionID:
			sessionID = ev.SessionID
		case harness.EventText:
			fmt.Print(ev.Text)
		case harness.EventToolCallStart:
			fmt.Printf("[tool: %s]\n", ev.ToolName)
		case harness.EventToolCallDelta:
			fmt.Print(ev.ToolArgs)
		case harness.EventToolCall:
			fmt.Printf("[tool: %s] %s\n", ev.ToolName, ev.ToolArgs)
		case harness.EventToolResult:
			fmt.Printf("[tool result: %s] %s\n", ev.ToolName, ev.ToolOutput)
		case harness.EventResult:
			fmt.Printf("\nResult: %s\n", ev.Result)
		}
	})

	// Continue the same conversation. This works with every built-in provider.
	harness.Resume(context.Background(), p, sessionID, "Now show an example", func(ev harness.Event) {
		if ev.Type == harness.EventText {
			fmt.Print(ev.Text)
		}
	})
}
Resuming sessions

Every built-in provider persists fresh sessions and emits an EventSessionID event. Save its SessionID, then pass it to the provider-independent Resume function:

var sessionID string
err := harness.Run(ctx, p, "Start the task", func(ev harness.Event) {
	if ev.Type == harness.EventSessionID {
		sessionID = ev.SessionID
	}
})
if err != nil {
	return err
}

return harness.Resume(ctx, p, sessionID, "Continue the task", handleEvent)

Session data remains owned by each agent CLI, so the resumed process must use the same working directory and have access to that CLI's normal session store. Third-party providers can opt in by implementing harness.ResumableProvider; providers with a custom streaming transport can additionally implement a matching Resume method.

A complete runnable example starts a session, captures its ID, and immediately resumes it:

go run ./examples/resume -provider claude-code -model claude-sonnet-4-6

You can also select another provider:

go run ./examples/resume -provider codex -model gpt-5.4-mini
go run ./examples/resume -provider pi -model claude-sonnet-4-6
go run ./examples/resume -provider docker-agent -model coder
go run ./examples/resume -provider opencode -model anthropic/claude-sonnet-4-6
Switching providers
// Docker Agent
p := dockeragent.New("coder")

// Claude Code
p := claudecode.New("claude-sonnet-4-6", claudecode.WithEffort(claudecode.EffortHigh))

// Pi
p := pi.New("claude-sonnet-4-6")

// Codex
p := codex.New("gpt-5.4-mini")

// opencode
p := opencode.New("anthropic/claude-sonnet-4-6")

The rest of your code stays exactly the same — all providers implement harness.Provider.

For providers whose CLIs have their own default model, pass an empty model string to omit the model flag entirely (for example, codex.New("") emits codex exec ... without -m).

The Provider interface

type Provider interface {
	Name() string
	PrintCommand(prompt string) string
	InteractiveArgs(prompt string) []string
	ParseStreamLine(line string) []Event
}

type ResumableProvider interface {
	Provider
	ResumeCommand(sessionID, prompt string) string
}
Method Purpose
Name() Human-readable identifier ("docker-agent", "claude-code", etc.)
PrintCommand() Shell command for non-interactive mode (sh -c safe)
InteractiveArgs() Arg list for interactive mode (first element = binary)
ParseStreamLine() Parse one NDJSON line into []Event
ResumeCommand() Build a command that continues a session (optional interface)

Event types

Type Fields set
EventText Text
EventResult Result, Usage (opt)
EventToolCallStart ToolID (opt), ToolName
EventToolCallDelta ToolID (opt), ToolName (opt), ToolArgs raw delta
EventToolCall ToolID (opt), ToolName, ToolArgs
EventToolResult ToolID (opt), ToolName (opt), ToolOutput, ToolError
EventReasoning Reasoning
EventSessionID SessionID

Example CLI

go run ./cmd/harness-example --provider docker-agent --model coder "Hello world"
go run ./cmd/harness-example --provider claude-code --model claude-sonnet-4-6 "Hello world"
go run ./cmd/harness-example --provider pi --model claude-sonnet-4-6 "Hello world"
go run ./cmd/harness-example --provider codex --model gpt-5.4-mini "Hello world"
go run ./cmd/harness-example --provider opencode --model anthropic/claude-sonnet-4-6 "Hello world"

# Resume a session reported by an earlier run:
go run ./cmd/harness-example --provider codex --model gpt-5.4-mini --resume <session-id> "Continue"

# Just print the command without executing:
go run ./cmd/harness-example --print-cmd --provider docker-agent --model coder "test"

License

MIT

Documentation

Overview

Package harness provides a unified interface for interacting with different AI coding agent CLIs (Docker Agent, Claude Code, Pi, Codex, OpenCode). It abstracts away the differences in command construction and stream output parsing so that switching between agents requires minimal code changes.

The central type is Provider. Create one with the constructor from a sub-package (e.g. [claudecode.New]), then call its methods to build commands or parse streaming output.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseJSON

func ParseJSON(line string) (map[string]any, bool)

ParseJSON is a convenience that unmarshals a line into a map. It returns nil, false if the line is not valid JSON or does not start with '{'.

func Resume

func Resume(ctx context.Context, p Provider, sessionID, prompt string, fn func(Event)) error

Resume continues sessionID with prompt and streams events to fn. It uses the same callback contract as Run. Resume returns an error when sessionID is empty or when p provides neither a resumable command nor a custom resume transport.

func Run

func Run(ctx context.Context, p Provider, prompt string, fn func(Event)) error

Run executes the provider in print (non-interactive) mode and streams parsed events to the callback. It blocks until the command finishes or the context is cancelled. The callback is invoked synchronously for each event as it arrives. A persistent provider emits EventSessionID, whose SessionID can later be passed to Resume.

func ShellEscape

func ShellEscape(s string) string

ShellEscape returns s wrapped in single quotes with any embedded single quotes escaped for POSIX shells: ' → '\”

Types

type Event

type Event struct {
	Type       EventType `json:"type"`
	Text       string    `json:"text,omitempty"`
	Result     string    `json:"result,omitempty"`
	SessionID  string    `json:"session_id,omitempty"`
	Usage      *Usage    `json:"usage,omitempty"`
	ToolID     string    `json:"tool_id,omitempty"`
	ToolName   string    `json:"name,omitempty"`
	ToolArgs   string    `json:"args,omitempty"`
	ToolOutput string    `json:"output,omitempty"`
	ToolError  bool      `json:"tool_error,omitempty"`
	Reasoning  string    `json:"reasoning,omitempty"` // set when Type == EventReasoning
}

Event is a single parsed event from an agent's streaming output. Depending on the Type field, different fields are populated:

  • EventText: Text is set.
  • EventResult: Result and (optionally) Usage are set.
  • EventToolCallStart: ToolID (optional) and ToolName are set.
  • EventToolCallDelta: ToolID (optional), ToolName (optional), and ToolArgs are set to the raw argument delta.
  • EventToolCall: ToolID (optional), ToolName, and ToolArgs are set to the provider's JSON argument object when available.
  • EventToolResult: ToolID (optional), ToolName (optional), ToolOutput, and ToolError are set.
  • EventReasoning: Reasoning is set.
  • EventSessionID: SessionID is set.

type EventType

type EventType string

EventType enumerates the kinds of events that a stream can produce.

const (
	// EventText is emitted when the agent produces a chunk of text output.
	EventText EventType = "text"
	// EventResult is emitted when the agent produces its final answer.
	EventResult EventType = "result"
	// EventToolCallStart is emitted when the agent starts building a tool call.
	EventToolCallStart EventType = "tool_call_start"
	// EventToolCallDelta is emitted when the agent streams tool call arguments.
	EventToolCallDelta EventType = "tool_call_delta"
	// EventToolCall is emitted when the agent invokes a tool.
	EventToolCall EventType = "tool_call"
	// EventToolResult is emitted when a tool invocation completes.
	EventToolResult EventType = "tool_result"
	// EventReasoning is emitted when the agent produces reasoning/thinking content.
	// Not all providers support this; check provider documentation.
	EventReasoning EventType = "reasoning"
	// EventSessionID identifies the persistent session used by the run. Save
	// SessionID and pass it to [Resume] to continue the conversation later.
	EventSessionID EventType = "session_id"
)

type Provider

type Provider interface {
	// Name returns a human-readable identifier for this provider (e.g.
	// "claude-code", "pi", "codex").
	Name() string

	// PrintCommand returns a complete shell command string that runs the
	// agent in non-interactive (print/exec) mode with the given prompt.
	// The returned string is safe to pass to "sh -c".
	PrintCommand(prompt string) string

	// InteractiveArgs returns the argument list (including the binary name
	// as the first element) for launching the agent in interactive mode.
	InteractiveArgs(prompt string) []string

	// ParseStreamLine parses a single line of newline-delimited JSON output
	// from the agent's streaming mode into zero or more [Event] values. Lines
	// that are not valid JSON or not recognized are silently ignored (an empty
	// slice is returned).
	ParseStreamLine(line string) []Event
}

Provider is the interface that all agent harnesses implement. It lets you build shell commands for non-interactive (print) mode, build argument lists for interactive mode, and parse newline-delimited JSON streaming output into a common event representation.

type ResumableProvider

type ResumableProvider interface {
	Provider

	// ResumeCommand returns a shell command that sends prompt to sessionID.
	// The returned string is safe to pass to "sh -c".
	ResumeCommand(sessionID, prompt string) string
}

ResumableProvider is implemented by providers whose CLI can continue an existing session. All providers shipped by this module implement it. Callers normally use Resume rather than invoking ResumeCommand directly.

type Usage

type Usage struct {
	InputTokens              int     `json:"input_tokens"`
	OutputTokens             int     `json:"output_tokens"`
	CacheReadInputTokens     int     `json:"cache_read_input_tokens"`
	CacheCreationInputTokens int     `json:"cache_creation_input_tokens"`
	TotalCostUSD             float64 `json:"total_cost_usd"`
	NumTurns                 int     `json:"num_turns"`
	DurationMS               int     `json:"duration_ms"`
}

Usage captures token and cost statistics reported by the agent at the end of a run.

func ExtractCodexUsage

func ExtractCodexUsage(obj map[string]any) *Usage

ExtractCodexUsage pulls token usage from a Codex turn.completed object. Codex uses: {"usage":{"input_tokens":N,"output_tokens":N,"cached_input_tokens":N}}

func ExtractPiUsage

func ExtractPiUsage(msg map[string]any) *Usage

ExtractPiUsage pulls token usage from a Pi assistant message object. Pi uses a different schema: {"usage":{"input":N,"output":N,"cacheRead":N,"cacheWrite":N,"totalTokens":N,"cost":{...}}}

func ExtractUsage

func ExtractUsage(obj map[string]any) *Usage

ExtractUsage pulls token usage information out of a generic JSON object. Returns nil if the required fields are missing.

Directories

Path Synopsis
Package claudecode provides a harness.Provider implementation for the Claude Code CLI agent.
Package claudecode provides a harness.Provider implementation for the Claude Code CLI agent.
cmd
harness-example command
Command harness-example demonstrates how to use the harness library to run an AI coding agent and process its streaming output.
Command harness-example demonstrates how to use the harness library to run an AI coding agent and process its streaming output.
Package codex provides a harness.Provider implementation for the OpenAI Codex CLI agent.
Package codex provides a harness.Provider implementation for the OpenAI Codex CLI agent.
Package dockeragent provides a harness.Provider implementation for the Docker Agent CLI.
Package dockeragent provides a harness.Provider implementation for the Docker Agent CLI.
examples
resume command
Command resume demonstrates starting and then resuming an agent session.
Command resume demonstrates starting and then resuming an agent session.
Package opencode provides a harness.Provider implementation for the opencode CLI agent (https://opencode.ai, https://github.com/sst/opencode).
Package opencode provides a harness.Provider implementation for the opencode CLI agent (https://opencode.ai, https://github.com/sst/opencode).
Package pi provides a harness.Provider implementation for the Pi CLI agent.
Package pi provides a harness.Provider implementation for the Pi CLI agent.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL