agentrun

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Feb 28, 2026 License: MIT Imports: 8 Imported by: 0

README

agentrun

CI Go Report Card Go Reference

Composable Go interfaces for running AI agent sessions.

agentrun is a zero-dependency Go library that abstracts over different AI agent runtimes (CLI subprocesses, API clients) with a uniform Engine/Process model. Build agent orchestrators without coupling to any specific AI tool.

Installation

go get github.com/dmora/agentrun

Each backend requires its CLI tool installed and on your PATH. The Quick Start below uses Claude Code (claude).

Quick Start

package main

import (
    "context"
    "fmt"
    "time"

    "github.com/dmora/agentrun"
    "github.com/dmora/agentrun/engine/cli"
    "github.com/dmora/agentrun/engine/cli/claude"
)

func main() {
    engine := cli.NewEngine(claude.New())

    ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
    defer cancel()

    proc, err := engine.Start(ctx, agentrun.Session{
        CWD:    "/path/to/project",
        Prompt: "Hello, world!",
    })
    if err != nil {
        panic(err)
    }
    defer proc.Stop(ctx)

    for msg := range proc.Output() {
        fmt.Println(msg.Content)
    }
    if err := proc.Err(); err != nil {
        fmt.Printf("session error: %v\n", err)
    }
}

Process Lifecycle

Engine.Start() returns a Process — the active session handle:

Method Description
Output() Returns <-chan Message for receiving agent output
Send(ctx, msg) Sends a follow-up message to the agent
Stop(ctx) Terminates the subprocess (blocks until output channel closes)
Wait() Blocks until the session ends naturally
Err() Returns the terminal error, or nil if still running

Output() is the primary consumption path — range over it to receive messages. When the channel closes, check Err() for the exit status.

Multi-Turn Conversations

Use RunTurn for safe concurrent Send+drain. It handles ACP's blocking RPC and works with all engine types:

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

err := agentrun.RunTurn(ctx, proc, "Explain the auth module", func(msg agentrun.Message) error {
    fmt.Println(msg.Content)
    return nil
})

RunTurn sends the message in a goroutine while draining Output() on the calling goroutine. It returns when MessageResult arrives, the channel closes, or context expires.

Turn semantics differ by backend:

  • Streaming (Claude, ACP) — persistent subprocess, messages flow on a shared channel
  • Spawn-per-turn (OpenCode, Codex) — each turn spawns a new subprocess via Resumer. Call Output() at the start of each turn rather than caching the channel across turns.

See examples/interactive for a full multi-turn REPL.

Filtering Messages

The filter package provides composable channel middleware for message streams. Each filter consumes an input channel and returns a new, filtered channel — they can be chained:

import "github.com/dmora/agentrun/filter"

// Drop streaming deltas, keep only complete messages.
for msg := range filter.Completed(ctx, proc.Output()) {
    fmt.Println(msg.Content)
}

Available filters:

Function Effect
filter.Completed(ctx, ch) Drops streaming deltas, passes everything else
filter.ResultOnly(ctx, ch) Keeps only MessageResult
filter.Filter(ctx, ch, types...) Keeps only the specified message types

The filter.IsDelta(t) predicate returns true for _delta message types — useful in custom filtering logic.

Message Types

Messages from proc.Output() carry a Type field indicating the kind of content:

Type Constant Description
text MessageText Assistant text output
tool_use MessageToolUse Agent is invoking a tool
tool_result MessageToolResult Tool invocation output
error MessageError Error from agent or runtime
system MessageSystem System-level status changes
init MessageInit Handshake at session start
result MessageResult Turn completion with usage data
context_window MessageContextWindow Mid-turn context window fill state
thinking MessageThinking Complete thinking/reasoning block
eof MessageEOF End of message stream

Streaming deltas — partial content from token-level streaming:

Type Constant Description
text_delta MessageTextDelta Partial text token
tool_use_delta MessageToolUseDelta Partial tool use JSON
thinking_delta MessageThinkingDelta Partial thinking content

Message Metadata

Messages carry structured metadata beyond Content. Key fields on the Message struct:

type Message struct {
    Type       MessageType      // kind of message (see table above)
    Content    string           // text content (semantics vary by Type)
    Tool       *ToolCall        // tool invocation details (tool_use, tool_result)
    Usage      *Usage           // token counts and cost (result, context_window)
    StopReason StopReason       // why the turn ended (result only)
    ErrorCode  string           // machine-readable error code (error only)
    ResumeID   string           // session ID for resume (init only)
    Init       *InitMeta        // model, agent name/version (init only)
    Process    *ProcessMeta     // subprocess PID and binary (init only)
    Raw        json.RawMessage  // original unparsed JSON
    Timestamp  time.Time        // when the message was produced
}

Pointer fields (Usage, Init, Process, Tool) are nil unless meaningful data is present.

Accessing result metadata:

for msg := range proc.Output() {
    if msg.Type == agentrun.MessageResult && msg.Usage != nil {
        fmt.Printf("tokens: in=%d out=%d cost=$%.4f\n",
            msg.Usage.InputTokens, msg.Usage.OutputTokens, msg.Usage.CostUSD)
        fmt.Printf("stop: %s\n", msg.StopReason)
    }
}

Init metadata — captured at session start:

  • Init.Model — model identifier (all backends)
  • Init.AgentName, Init.AgentVersion — agent identity (ACP only)
  • Process.PID, Process.Binary — subprocess info (CLI/ACP engines)
  • ResumeID — persist and pass back via OptionResumeID to resume later

Error metadata:

  • ErrorCode — machine-readable code (e.g., "rate_limit"); human description in Content

Session Configuration

Sessions carry cross-cutting options that backends translate into CLI flags or API parameters:

session := agentrun.Session{
    CWD:    "/path/to/project",
    Prompt: "Refactor the auth module",
    Model:  "claude-sonnet-4-20250514",
    Options: map[string]string{
        agentrun.OptionSystemPrompt:   "You are a Go expert.",
        agentrun.OptionMode:           "act",                    // plan or act
        agentrun.OptionEffort:         "high",                   // low/medium/high/max
        agentrun.OptionThinkingBudget: "10000",                  // enable extended thinking
        agentrun.OptionHITL:           "off",                    // human-in-the-loop
        agentrun.OptionAddDirs:        "/shared/lib\n/shared/proto", // newline-separated
    },
    Env: map[string]string{
        "OPENCODE_AUTO_APPROVE": "1",
    },
}

Backend-specific options use a namespace prefix (e.g., claude.OptionPermissionMode, codex.OptionSandbox). See each backend package for available options.

Error Handling

Sentinel errors for engine operations:

Error Meaning
ErrUnavailable Engine cannot start (binary not found, API unreachable)
ErrTerminated Session was terminated (Stop() called, connection closed)
ErrSendNotSupported Backend lacks Send capability

Subprocess exit codes are wrapped in *ExitError. Use ExitCode() to extract:

err := proc.Wait()
if code, ok := agentrun.ExitCode(err); ok {
    fmt.Printf("agent exited with code %d\n", code)
}

ErrTerminated always takes precedence over ExitError when Stop() is called.

Architecture

agentrun (interfaces + value types)
│
├── filter/                  Composable channel middleware
│
├── engine/cli/              CLI subprocess transport
│   ├── claude/              Claude Code backend
│   ├── codex/               Codex CLI backend
│   └── opencode/            OpenCode backend
│
├── engine/acp/              ACP JSON-RPC 2.0 engine
│
├── engine/api/
│   └── adk/                 Google ADK API engine
│
└── enginetest/              Compliance test suites

The root agentrun package defines four core types:

  • Engine — starts and validates agent sessions
  • Process — an active session handle with output channel
  • Session — minimal session state passed to engines (value type)
  • Message — structured output from agent processes

Engine implementations live in subpackages. CLI backends share a generic cli.Engine that delegates to backend-specific interfaces. Optional capabilities (resume, streaming) are discovered via type assertion.

Built-in Backends
Backend Package Transport Resumer Streamer
Claude Code engine/cli/claude CLI (streaming stdin) yes yes
Codex engine/cli/codex CLI (spawn-per-turn) yes
OpenCode engine/cli/opencode CLI (spawn-per-turn) yes
ACP engine/acp JSON-RPC 2.0 n/a n/a

ACP is a separate engine type (not cli.Backend) — it communicates via a persistent JSON-RPC 2.0 subprocess.

Write a Custom Backend

Implement Spawner + Parser (required) and Resumer (for multi-turn). The cli.Engine handles subprocess lifecycle, stdout scanning, and message pumping.

CLI backend interfaces (Backend = Spawner + Parser, optional capabilities are separate):

Interface Method Purpose
Spawner SpawnArgs(Session) (string, []string) Build command to start a session
Parser ParseLine(string) (Message, error) Parse one stdout line into a Message
Resumer ResumeArgs(Session, string) (string, []string, error) Resume or start a new turn
Streamer StreamArgs(Session) (string, []string) Build long-lived streaming command
InputFormatter FormatInput(string) ([]byte, error) Encode messages for stdin pipe

Step 1 — Implement the interfaces:

type myBackend struct{}

func (b *myBackend) SpawnArgs(session agentrun.Session) (string, []string) {
    return "my-agent", []string{"--prompt", session.Prompt}
}

func (b *myBackend) ParseLine(line string) (agentrun.Message, error) {
    // Parse your CLI tool's JSON output into agentrun.Message.
    // Return cli.ErrSkipLine for blank lines or heartbeats.
    return agentrun.Message{}, cli.ErrSkipLine
}

func (b *myBackend) ResumeArgs(session agentrun.Session, msg string) (string, []string, error) {
    return "my-agent", []string{"--resume", session.Options[agentrun.OptionResumeID], "--prompt", msg}, nil
}

Step 2 — Wire into the engine:

engine := cli.NewEngine(&myBackend{})
proc, _ := engine.Start(ctx, agentrun.Session{CWD: cwd, Prompt: "Hello"})

Step 3 — Verify with the compliance suite:

func TestCompliance(t *testing.T) {
    clitest.RunBackendTests(t, func() cli.Backend { return &myBackend{} })
}

See examples/custom-backend for a full runnable example. See CONTRIBUTING.md for development guidelines.

License

MIT

Documentation

Overview

Package agentrun provides composable interfaces for running AI agent sessions.

agentrun is a zero-dependency Go library that abstracts over different AI agent runtimes (CLI subprocesses, API clients) with a uniform Engine/Process model.

Core Types

  • Engine — starts and validates agent sessions
  • Process — an active session handle with message output channel
  • Session — minimal session state passed to engines (value type)
  • Message — structured output from agent processes
  • Option — functional options for [Engine.Start]

Vocabulary

The root package defines the shared vocabulary for all backends:

  • Output vocabulary: MessageType constants define what agents produce
  • Input vocabulary: Option* constants define cross-cutting session configuration

Backends translate this vocabulary into their wire format. Cross-cutting concepts (system prompts, turn limits, thinking budgets) are defined here as well-known [Session.Options] keys. Backend-specific concepts remain in their respective packages.

Quick Start

engine := cli.NewEngine(claude.New())
proc, err := engine.Start(ctx, agentrun.Session{
    CWD:    "/path/to/project",
    Prompt: "Hello",
})
if err != nil { log.Fatal(err) }
for msg := range proc.Output() {
    fmt.Println(msg.Content)
}

Index

Examples

Constants

View Source
const (
	// OptionSystemPrompt sets the system prompt for the session.
	// Value is the raw prompt string.
	OptionSystemPrompt = "system_prompt"

	// OptionMaxTurns limits the number of agentic turns per invocation.
	// Value is a positive integer string (e.g., "5").
	OptionMaxTurns = "max_turns"

	// OptionThinkingBudget controls the model's thinking/reasoning output.
	// When set to a positive integer string (e.g., "10000"), backends that
	// support extended thinking will emit MessageThinking and
	// MessageThinkingDelta messages with the model's reasoning content.
	//
	// The value interpretation is backend-specific:
	//   - Claude CLI: token count, maps to --max-thinking-tokens
	//   - Other backends: may accept token counts, levels, or other formats
	//
	// When empty or absent, thinking output is disabled (backend default).
	OptionThinkingBudget = "thinking_budget"

	// OptionMode sets the operating mode for the session.
	// Backends that support modes map this to their native mechanism.
	// Backends that don't recognize this option silently ignore it.
	// Values should be Mode constants (ModePlan, ModeAct).
	OptionMode = "mode"

	// OptionHITL controls human-in-the-loop supervision.
	// When "on" (or absent), the backend requires human approval for
	// actions with side effects. When "off", autonomous operation.
	// Values should be HITL constants (HITLOn, HITLOff).
	OptionHITL = "hitl"

	// OptionResumeID sets the backend-assigned session identifier for resume.
	// Consumers capture this value from MessageInit.ResumeID after the first
	// session, persist it, and set it here for subsequent sessions.
	// When set, backends include their native resume flag
	// (e.g., --resume for Claude, --session for OpenCode).
	// Value format is backend-specific and opaque to the root package.
	// Values are not portable across backends.
	//
	// An empty MessageInit.ResumeID signals that the backend could not
	// capture a session ID (e.g., invalid format from the agent runtime).
	// Consumers should treat empty ResumeID as "no ID available" and avoid
	// persisting it for future resume.
	OptionResumeID = "resume_id"

	// OptionAgentID identifies which agent specification to use.
	// Cross-cutting: OpenCode maps to --agent <id>, ADK uses agent registry.
	// Backends that don't support agent selection silently ignore this.
	OptionAgentID = "agent_id"

	// OptionEffort controls reasoning depth/quality tradeoff.
	// Value should be an Effort constant (low, medium, high, max).
	// Backend support varies — unsupported values are silently skipped:
	//   - Claude CLI: low, medium, high (maps to --effort)
	//   - Codex CLI: low, medium, high, max (maps to -c model_reasoning_effort; max → "xhigh")
	//   - OpenCode: low, high, max (maps to --variant; medium has no equivalent)
	//
	// When set and mappable to the backend's native values, OptionEffort
	// takes precedence over backend-specific effort options (e.g.,
	// opencode.OptionVariant). When set but unmappable (e.g., "medium"
	// on OpenCode), the backend falls through to its own option.
	// When absent, backend-specific options are used.
	OptionEffort = "effort"

	// OptionAddDirs specifies additional directories the agent may access
	// beyond CWD. Value is newline-separated absolute paths.
	//
	// Backend support: Claude (--add-dir), Codex (--add-dir).
	// Backends without directory scoping silently ignore this option.
	OptionAddDirs = "add_dirs"
)

Well-known option keys for Session.Options.

These keys define cross-cutting concepts that multiple backends may support. Backends silently ignore keys they don't recognize. Backend-specific options are defined in their own packages.

Variables

View Source
var (
	// ErrUnavailable indicates the engine cannot start
	// (binary not found, API unreachable, etc.).
	ErrUnavailable = errors.New("agentrun: engine unavailable")

	// ErrTerminated indicates the session was terminated
	// (process killed, connection closed).
	ErrTerminated = errors.New("agentrun: session terminated")

	// ErrSessionNotFound indicates the requested session does not exist.
	ErrSessionNotFound = errors.New("agentrun: session not found")

	// ErrSendNotSupported indicates the engine's backend cannot fulfill
	// Process.Send (no Streamer+InputFormatter or Resumer capability).
	// Returned by Engine.Start when the backend lacks a send path.
	ErrSendNotSupported = errors.New("agentrun: send not supported")
)

Sentinel errors for engine operations.

Functions

func ExitCode

func ExitCode(err error) (int, bool)

ExitCode extracts the exit code from an error chain containing *ExitError. Returns (0, false) if the error does not contain an ExitError. Convenience wrapper around errors.As — equivalent to:

var exitErr *ExitError
if errors.As(err, &exitErr) { return exitErr.Code, true }

func MergeEnv

func MergeEnv(base []string, extra map[string]string) []string

MergeEnv returns base with extra entries appended as "key=value" pairs.

Override semantics: extra entries are appended after base. When a key exists in both base and extra, exec.Cmd uses last-wins semantics on most platforms — the appended value takes effect. The original entry remains in the slice but is shadowed.

Nil contract: returns nil when extra is empty. Callers should pass nil to exec.Cmd.Env to inherit the parent environment unchanged. When extra is non-empty, os.Environ() is snapshotted at call time.

Engines call: MergeEnv(os.Environ(), session.Env)

func ParseBoolOption

func ParseBoolOption(opts map[string]string, key string) (bool, bool, error)

ParseBoolOption returns the boolean value for key in opts. If the key is absent or empty, it returns (false, false, nil). Truthy values: "true", "on", "1", "yes" (case-insensitive). Falsy values: "false", "off", "0", "no" (case-insensitive). Unrecognized values return an error.

func ParseListOption

func ParseListOption(opts map[string]string, key string) []string

ParseListOption splits a newline-separated option value into individual entries. Empty entries and entries containing null bytes are skipped. Returns nil when the key is absent or empty.

func ParsePositiveIntOption

func ParsePositiveIntOption(opts map[string]string, key string) (int, bool, error)

ParsePositiveIntOption returns the integer value for key in opts. If the key is absent or empty, it returns (0, false, nil). If the value is present but not a valid positive integer, or contains null bytes, it returns an error.

func RunTurn

func RunTurn(ctx context.Context, proc Process, message string, handler func(Message) error) error

RunTurn sends a message and drains Output() concurrently until MessageResult or channel close. handler is called for each message (including MessageResult). Safe for all engine types — handles the concurrent Send+drain requirement that ACP needs (Send blocks on RPC) and CLI tolerates.

Send runs in a goroutine. The calling goroutine drains Output(). If Send returns an error, the drain stops and RunTurn returns the Send error. If the handler returns an error, the drain stops and RunTurn returns it. If the channel closes without MessageResult, RunTurn returns proc.Err(). Context cancellation stops both Send and the drain.

The caller should provide a context with a deadline or timeout. The Send goroutine is not joined on return — if Send blocks indefinitely (e.g., a hung RPC), the goroutine leaks until the context is canceled. After MessageResult arrives, any in-flight Send error is collected non-blocking; a Send error that arrives after MessageResult is intentionally dropped.

func StringOption

func StringOption(opts map[string]string, key, defaultVal string) string

StringOption returns the value for key in opts, or defaultVal if the key is absent or empty.

func ValidateEnv

func ValidateEnv(env map[string]string) error

ValidateEnv checks all keys and values in env. Keys must be non-empty and must not contain '=' or null bytes. Values must not contain null bytes. Returns the first validation error encountered. Nil or empty env is valid.

Types

type Effort

type Effort string

Effort controls reasoning depth/quality tradeoff.

const (
	// EffortLow requests minimal reasoning for speed.
	EffortLow Effort = "low"

	// EffortMedium requests balanced reasoning (default for most models).
	EffortMedium Effort = "medium"

	// EffortHigh requests thorough reasoning for complex tasks.
	EffortHigh Effort = "high"

	// EffortMax requests maximum reasoning depth.
	EffortMax Effort = "max"
)

func (Effort) Valid

func (e Effort) Valid() bool

Valid reports whether e is a recognized Effort value.

type Engine

type Engine interface {
	// Start initializes a session and returns a Process handle.
	// The Process immediately begins producing Messages on its Output channel.
	// Options override Session fields for this specific invocation.
	Start(ctx context.Context, session Session, opts ...Option) (Process, error)

	// Validate checks that the engine is available and ready.
	// For CLI engines, this verifies the binary exists and is executable.
	// For API engines, this checks connectivity and authentication.
	Validate() error
}

Engine starts and validates agent sessions.

Implementations include CLI subprocess engines (engine/cli) and API-based engines (engine/api/adk). Use Validate to check that the engine's prerequisites are met before calling Start.

type ExitError

type ExitError struct {
	Code int
	Err  error
}

ExitError represents a subprocess that exited with a non-zero status. Wraps the underlying error to preserve the error chain — consumers can errors.As to *exec.ExitError for OS-level detail (signal info, etc.).

Code semantics: positive = exit status, negative (-1) = signal-killed.

Engines produce ExitError only for natural exits. User-initiated stops (via Process.Stop) produce ErrTerminated instead.

func (*ExitError) Error

func (e *ExitError) Error() string

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

type HITL

type HITL string

HITL represents the human-in-the-loop setting for a session.

const (
	// HITLOn requires human approval for actions with side effects.
	HITLOn HITL = "on"

	// HITLOff enables autonomous operation without human prompts.
	HITLOff HITL = "off"
)

func (HITL) Valid

func (h HITL) Valid() bool

Valid reports whether h is a recognized HITL value.

type InitMeta

type InitMeta struct {
	// Model is the model identifier reported by the backend at session start.
	// Reflects the agent's reported model during handshake; may differ from
	// the active model if the caller overrides via Session.Model.
	// Empty means the backend did not report a model on init.
	// Sanitized: control chars rejected, truncated to 128 bytes at parse time.
	Model string `json:"model,omitempty"`

	// AgentName is the agent implementation's name (e.g., "opencode", "claude-code").
	// Empty means the backend did not report an agent name.
	// Currently populated by ACP backends only.
	// Sanitized: control chars rejected, truncated to 128 bytes at parse time.
	AgentName string `json:"agent_name,omitempty"`

	// AgentVersion is the agent implementation's version string.
	// Empty means the backend did not report a version.
	// Currently populated by ACP backends only.
	// Sanitized: control chars rejected, truncated to 128 bytes at parse time.
	AgentVersion string `json:"agent_version,omitempty"`
}

InitMeta carries metadata from the agent's initialization handshake. Set exclusively on MessageInit messages. Nil on all other message types.

Not all backends populate every field — AgentName and AgentVersion are currently only populated by ACP backends. Consumers should check individual fields rather than assuming non-nil InitMeta means all fields are present.

Nil-guard contract: backends only set Init when at least one field is non-empty. A non-nil InitMeta always has meaningful data.

type Message

type Message struct {
	// Type identifies the kind of message.
	Type MessageType `json:"type"`

	// Content is the text content. Semantics vary by Type:
	//   - MessageText: assistant text output
	//   - MessageError: human-readable error description (see ErrorCode for machine-readable)
	//   - MessageSystem: status text
	//   - MessageResult: optional result text (may be empty; see StopReason for completion signal)
	//   - *Delta types: partial content fragment (text, JSON, or thinking)
	Content string `json:"content,omitempty"`

	// Tool contains tool invocation details (for ToolUse, ToolResult messages).
	Tool *ToolCall `json:"tool,omitempty"`

	// Usage contains token usage data (typically on Text messages).
	Usage *Usage `json:"usage,omitempty"`

	// StopReason indicates why the agent's turn ended.
	// Set exclusively on MessageResult messages. Empty means the backend
	// did not report a stop reason.
	//
	// Consumers should handle unknown values gracefully — backends may
	// report values beyond the StopReason constants defined in this package.
	StopReason StopReason `json:"stop_reason,omitempty"`

	// ErrorCode is the machine-readable error code from the backend.
	// Set exclusively on MessageError messages. Human-readable description
	// is in Content. Empty means no structured code was provided.
	//
	// Intentionally a plain string (not a named type like StopReason):
	// error codes have no universal constants across backends — CLI backends
	// emit string codes (e.g., "rate_limit"), ACP emits library-defined
	// constants (e.g., acp.ErrCodeToolCallFailed). A named type would imply
	// root-level constants that don't exist. Consumers should match on raw
	// string values or use backend-exported constants where available.
	//
	// Backend parsers populate this field when the wire format includes a
	// structured error code. Empty means no code was provided.
	ErrorCode string `json:"error_code,omitempty"`

	// ResumeID is the backend-assigned session identifier for resume.
	// Set exclusively on MessageInit messages. Consumers persist this value
	// and pass it back via OptionResumeID to resume the session later.
	// Empty means the backend could not capture a session ID.
	ResumeID string `json:"resume_id,omitempty"`

	// Init carries metadata from the agent's initialization handshake.
	// Set exclusively on MessageInit messages. Nil on all other types.
	// Only non-nil when at least one field contains meaningful data.
	Init *InitMeta `json:"init,omitempty"`

	// Process carries subprocess metadata from the engine.
	// Set on MessageInit messages by subprocess engines (CLI, ACP).
	// Nil on all other message types and for API-based engines.
	Process *ProcessMeta `json:"process,omitempty"`

	// Raw is the original unparsed JSON from the backend.
	// Backends populate this for pass-through or debugging.
	Raw json.RawMessage `json:"raw,omitempty"`

	// Timestamp is when the message was produced.
	// Producers should always set this; zero value serializes as
	// "0001-01-01T00:00:00Z".
	Timestamp time.Time `json:"timestamp"`
}

Message is a structured output from an agent process.

type MessageType

type MessageType string

MessageType identifies the kind of message from an agent process.

const (
	// MessageText is assistant text output.
	MessageText MessageType = "text"

	// MessageToolUse indicates the agent is invoking a tool.
	MessageToolUse MessageType = "tool_use"

	// MessageToolResult contains the output of a tool invocation.
	MessageToolResult MessageType = "tool_result"

	// MessageError indicates an error from the agent or runtime.
	MessageError MessageType = "error"

	// MessageSystem contains system-level messages (e.g., status changes).
	MessageSystem MessageType = "system"

	// MessageInit is the handshake message sent at session start.
	MessageInit MessageType = "init"

	// MessageResult signals turn completion with optional usage data.
	// The completion reason is in Message.StopReason (not Content).
	// Token usage and cost data are in Message.Usage.
	MessageResult MessageType = "result"

	// MessageContextWindow carries context window fill state emitted mid-turn.
	// Contains window capacity and current fill level. Not all backends emit
	// this type; it is currently produced by ACP's usage_update notification.
	//
	// Only ContextSizeTokens and ContextUsedTokens are meaningful on this
	// message type. Other Usage fields (InputTokens, OutputTokens, CostUSD,
	// etc.) are zero and should be ignored — they belong on MessageResult.
	// Cost data is NOT included — CostUSD is authoritative only on
	// MessageResult to avoid double-counting.
	MessageContextWindow MessageType = "context_window"

	// MessageEOF signals the end of the message stream.
	MessageEOF MessageType = "eof"

	// MessageThinking contains complete thinking/reasoning content from models
	// with extended thinking enabled. Thinking content arrives as a complete
	// content block inside assistant messages.
	//
	// Requires OptionThinkingBudget to be set in Session.Options. Without it,
	// models think internally but do not expose thinking in their output.
	// The Completed() filter passes MessageThinking through; consumers wanting
	// only final text should use ResultOnly().
	MessageThinking MessageType = "thinking"

	// MessageTextDelta is a partial text token from streaming output.
	// Content holds the text fragment. Emitted when the backend supports
	// streaming and partial messages are enabled (default for Claude).
	MessageTextDelta MessageType = "text_delta"

	// MessageToolUseDelta is partial tool use input JSON from streaming output.
	// Content holds a JSON fragment. Emitted during incremental tool input.
	MessageToolUseDelta MessageType = "tool_use_delta"

	// MessageThinkingDelta is partial thinking content from streaming output.
	// Content holds a thinking text fragment.
	//
	// Emitted by the Claude CLI backend when OptionThinkingBudget is set and
	// streaming is enabled. Also emitted by API-based backends that expose
	// raw streaming thinking deltas.
	MessageThinkingDelta MessageType = "thinking_delta"
)

type Mode

type Mode string

Mode represents the operating mode for a session.

const (
	// ModePlan requests analysis-only behavior.
	ModePlan Mode = "plan"

	// ModeAct authorizes the agent to take actions.
	ModeAct Mode = "act"
)

func (Mode) Valid

func (m Mode) Valid() bool

Valid reports whether m is a recognized Mode value.

type Option

type Option func(*StartOptions)

Option configures an Engine.Start invocation.

func WithModel

func WithModel(model string) Option

WithModel overrides the session model for this invocation.

Example
package main

import (
	"fmt"

	"github.com/dmora/agentrun"
)

func main() {
	opts := agentrun.ResolveOptions(agentrun.WithModel("claude-sonnet-4-5-20250514"))
	fmt.Println(opts.Model)
}
Output:

claude-sonnet-4-5-20250514

func WithPrompt

func WithPrompt(prompt string) Option

WithPrompt overrides the session prompt for this invocation.

Example
package main

import (
	"fmt"

	"github.com/dmora/agentrun"
)

func main() {
	opts := agentrun.ResolveOptions(agentrun.WithPrompt("Summarize this code"))
	fmt.Println(opts.Prompt)
}
Output:

Summarize this code

func WithTimeout

func WithTimeout(d time.Duration) Option

WithTimeout sets a deadline for the start operation.

Example
package main

import (
	"fmt"
	"time"

	"github.com/dmora/agentrun"
)

func main() {
	opts := agentrun.ResolveOptions(agentrun.WithTimeout(10 * time.Second))
	fmt.Println(opts.Timeout)
}
Output:

10s

type Process

type Process interface {
	// Output returns the channel for receiving messages from the agent.
	// The channel is closed when the session ends (normally or on error).
	// The first message may be of type MessageInit for engines that
	// perform a handshake.
	//
	// Callers must drain this channel concurrently with Send for engines
	// that stream updates during the RPC call. See [RunTurn] for a helper
	// that handles this correctly.
	Output() <-chan Message

	// Send transmits a user message to the active session.
	// For ACP engines, Send blocks until the agent's turn completes.
	// Callers must drain Output() concurrently to avoid deadlock.
	// See [RunTurn] for a helper that handles this correctly.
	Send(ctx context.Context, message string) error

	// Stop terminates the session. For CLI engines, this sends SIGTERM
	// then SIGKILL after a grace period.
	Stop(ctx context.Context) error

	// Wait blocks until the session ends naturally.
	// Returns nil on clean exit, or an error describing the failure.
	Wait() error

	// Err returns the terminal error after the Output channel is closed.
	// Returns nil if the session ended cleanly. Callers should call Err
	// after the Output channel is closed to distinguish clean exit from
	// failure.
	Err() error
}

Process is an active session handle.

Messages flow through the Output channel. Send transmits user messages to the running agent. Stop terminates the session, and Wait blocks until it ends naturally.

Important: some engines (notably ACP) require Output() to be drained concurrently while Send() is in progress — Send blocks on an RPC call while the agent streams updates to Output(). Use RunTurn to handle this correctly for all engine types.

Process is an interface to enable wrapping with logging, metrics, or retry middleware.

type ProcessMeta

type ProcessMeta struct {
	// PID is the OS process identifier of the subprocess.
	// Per the nil-guard contract on ProcessMeta, this field is always > 0.
	PID int `json:"pid,omitempty"`

	// Binary is the resolved path to the subprocess executable.
	// Populated from exec.Cmd.Path (the result of exec.LookPath).
	// Empty means not available.
	Binary string `json:"binary,omitempty"`
}

ProcessMeta describes the OS subprocess backing a session. Set on MessageInit messages by subprocess engines. Nil for API-based engines and for all non-init message types.

All fields are snapshot values captured at init time. For spawn-per-turn backends (e.g., OpenCode, Codex), PID reflects the first subprocess and may change between turns — callers should not treat PID as a stable session-lifetime identifier.

Nil-guard contract: engines only set Process when PID > 0. A non-nil ProcessMeta always has meaningful data.

type Session

type Session struct {
	// ID uniquely identifies the session.
	ID string `json:"id"`

	// CWD is the working directory for the agent process.
	CWD string `json:"cwd"`

	// Model specifies the AI model to use (e.g., "claude-sonnet-4-5-20250514").
	Model string `json:"model,omitempty"`

	// Prompt is the initial prompt or message for the session.
	Prompt string `json:"prompt,omitempty"`

	// Options holds session configuration as key-value pairs.
	// Use well-known Option* constants for cross-cutting features
	// (e.g., OptionSystemPrompt, OptionMaxTurns). Backend-specific
	// options are defined in their respective packages.
	Options map[string]string `json:"options,omitempty"`

	// Env holds additional environment variables for the agent process.
	// These are merged with the parent process environment via MergeEnv —
	// os.Environ() provides the base, and Env entries override matching keys.
	// Nil or empty means inherit parent environment unchanged.
	//
	// Keys must not be empty, contain '=' or null bytes.
	// Values must not contain null bytes.
	//
	// Security note: consumers are responsible for what they pass.
	// The library validates key syntax but does not blocklist specific
	// variable names (e.g., LD_PRELOAD, PATH). Treat Session.Env like
	// cmd.Env — the caller owns the security boundary.
	Env map[string]string `json:"env,omitempty"`
}

Session is the minimal session state passed to engines.

Session is a value type — it carries identity and configuration but no runtime state (no mutexes, no channels, no process handles). Orchestrators that need richer state should embed or wrap Session.

func (Session) Clone

func (s Session) Clone() Session

Clone returns a deep copy of s, cloning the Options and Env maps. Use Clone before mutating a session that may be shared.

type StartOptions

type StartOptions struct {
	// Prompt overrides Session.Prompt for this invocation.
	// Zero value means use Session.Prompt.
	Prompt string

	// Model overrides Session.Model for this invocation.
	// Zero value means use Session.Model.
	Model string

	// Timeout sets a deadline for the Start operation.
	// Zero means no timeout beyond the context deadline.
	Timeout time.Duration
}

StartOptions holds resolved configuration for Engine.Start. Engine implementations call ResolveOptions to collapse functional options into this struct.

func ResolveOptions

func ResolveOptions(opts ...Option) StartOptions

ResolveOptions applies functional options and returns the resolved config. Engine implementations call this in their Start method.

Example
package main

import (
	"fmt"
	"time"

	"github.com/dmora/agentrun"
)

func main() {
	opts := agentrun.ResolveOptions(
		agentrun.WithPrompt("Hello, agent"),
		agentrun.WithModel("claude-sonnet-4-5-20250514"),
		agentrun.WithTimeout(30*time.Second),
	)
	fmt.Println(opts.Prompt)
	fmt.Println(opts.Model)
	fmt.Println(opts.Timeout)
}
Output:

Hello, agent
claude-sonnet-4-5-20250514
30s
Example (Empty)
package main

import (
	"fmt"

	"github.com/dmora/agentrun"
)

func main() {
	opts := agentrun.ResolveOptions()
	fmt.Println(opts.Prompt == "")
	fmt.Println(opts.Model == "")
	fmt.Println(opts.Timeout)
}
Output:

true
true
0s

type StopReason

type StopReason string

StopReason indicates why an agent's turn ended. This is output vocabulary — backends populate it, consumers read it. Unknown values pass through as-is (the type is an open set, not a closed enum).

No Valid() method: unlike Mode/HITL/Effort (input vocabulary validated before reaching a subprocess), StopReason is read-only output that consumers match on. Adding Valid() would imply a closed set and force updates to root when a new backend introduces a new stop reason.

const (
	// StopEndTurn means the agent completed its response normally.
	StopEndTurn StopReason = "end_turn"

	// StopMaxTokens means the response was truncated due to token limits.
	StopMaxTokens StopReason = "max_tokens"

	// StopToolUse means the agent stopped to invoke a tool.
	StopToolUse StopReason = "tool_use"
)

type ToolCall

type ToolCall struct {
	// Name is the tool identifier.
	Name string `json:"name"`

	// Input is the tool's input parameters as raw JSON.
	Input json.RawMessage `json:"input,omitempty"`

	// Output is the tool's result as raw JSON.
	Output json.RawMessage `json:"output,omitempty"`
}

ToolCall describes a tool invocation by the agent.

type Usage

type Usage struct {
	// InputTokens is the cumulative context window fill.
	// Always serialized (0 means zero tokens used).
	InputTokens int `json:"input_tokens"`

	// OutputTokens is the number of tokens generated.
	// Always serialized (0 means zero tokens generated).
	OutputTokens int `json:"output_tokens"`

	// CacheReadTokens is tokens served from cache instead of recomputed.
	// Omitted when zero (0 means not reported by this backend).
	CacheReadTokens int `json:"cache_read_tokens,omitempty"`

	// CacheWriteTokens is tokens written to cache for future reuse.
	// Omitted when zero (0 means not reported by this backend).
	CacheWriteTokens int `json:"cache_write_tokens,omitempty"`

	// ThinkingTokens is tokens used for model reasoning/thinking.
	// Omitted when zero (0 means not reported by this backend).
	ThinkingTokens int `json:"thinking_tokens,omitempty"`

	// CostUSD is the estimated cost in USD for this turn.
	// Omitted when zero. Always a finite non-negative value; parsers
	// must sanitize NaN/Inf to zero before populating this field.
	// Approximate — not suitable for billing reconciliation.
	CostUSD float64 `json:"cost_usd,omitempty"`

	// ContextSizeTokens is the total context window capacity in tokens.
	// Omitted when zero (0 means not reported by this backend).
	// Set on MessageContextWindow messages (ACP usage_update).
	ContextSizeTokens int `json:"context_size_tokens,omitempty"`

	// ContextUsedTokens is the current context window fill level in tokens.
	// Omitted when zero (0 means not reported by this backend).
	// Set on MessageContextWindow messages (ACP usage_update).
	ContextUsedTokens int `json:"context_used_tokens,omitempty"`
}

Usage contains token usage data from the agent's model.

Directories

Path Synopsis
engine
acp
Package acp provides an Agent Client Protocol (ACP) engine for agentrun.
Package acp provides an Agent Client Protocol (ACP) engine for agentrun.
api/adk
Package adk provides a Google Agent Development Kit (ADK) API engine for agentrun.
Package adk provides a Google Agent Development Kit (ADK) API engine for agentrun.
cli
Package cli provides a CLI subprocess transport adapter for agentrun engines.
Package cli provides a CLI subprocess transport adapter for agentrun engines.
cli/claude
Package claude provides a Claude Code CLI backend for agentrun.
Package claude provides a Claude Code CLI backend for agentrun.
cli/codex
Package codex provides a Codex CLI backend for agentrun.
Package codex provides a Codex CLI backend for agentrun.
cli/internal/jsonutil
Package jsonutil provides safe JSON extraction helpers for CLI backend parsers.
Package jsonutil provides safe JSON extraction helpers for CLI backend parsers.
cli/internal/optutil
Package optutil provides shared option resolution helpers for CLI backends.
Package optutil provides shared option resolution helpers for CLI backends.
cli/opencode
Package opencode provides an OpenCode CLI backend for agentrun.
Package opencode provides an OpenCode CLI backend for agentrun.
internal/errfmt
Package errfmt provides shared error formatting for backend parsers.
Package errfmt provides shared error formatting for backend parsers.
internal/stoputil
Package stoputil provides shared StopReason sanitization for CLI backends.
Package stoputil provides shared StopReason sanitization for CLI backends.
Package enginetest provides compliance test suites for agentrun implementations.
Package enginetest provides compliance test suites for agentrun implementations.
clitest
Package clitest provides compliance test suites for cli.Backend implementations.
Package clitest provides compliance test suites for cli.Backend implementations.
Package filter provides composable channel middleware for filtering agentrun message streams.
Package filter provides composable channel middleware for filtering agentrun message streams.

Jump to

Keyboard shortcuts

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