agent

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package agent defines the pluggable adapter interface that lets the MCP server drive different headless CLI coding agents (Claude Code, Cursor, or any tool configured through CustomAdapter) through one uniform surface.

Design: rather than trying to control an interactive TUI (pseudo-terminals, ANSI parsing, prompt detection — fragile), every adapter invokes its agent in *headless / print* mode, streaming newline-delimited output. That gives a clean programmatic contract: a session id to resume, and an unambiguous terminal "result" event signalling the task is done — with the process exit code as a universal backstop.

To support another agent, implement Adapter and register it in the Registry.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CanPlan

func CanPlan(a Adapter) bool

CanPlan reports whether a is able to honour RunSpec.PlanOnly.

func RepairedEnviron

func RepairedEnviron() (env []string, repaired []string)

RepairedEnviron returns this process's environment with missing system variables restored, plus the names of whatever it had to restore. The names are worth surfacing: they are a precise fingerprint of what the launching client failed to pass on.

func RunMock

func RunMock(args []string) int

RunMock implements the `__mock` subcommand: it prints a short, deterministic Claude-style stream-json transcript for the given prompt and exits 0.

Types

type Adapter

type Adapter interface {
	// Name is the stable identifier used in tool calls ("claude", "cursor").
	Name() string

	// Available reports whether the agent looks usable on this machine, with a
	// human-readable detail (resolved binary path or the reason it is missing).
	Available() (ok bool, detail string)

	// Command builds the exec.Cmd for a run. It must NOT set Dir or Env; the
	// task manager owns those. Env is inherited from the server process, so
	// whatever the host machine can reach (VPN routes, SSH agent, credentials)
	// is available to the child with no extra wiring.
	Command(ctx context.Context, spec RunSpec) (*exec.Cmd, error)

	// ParseLine interprets one line of stdout.
	ParseLine(line string) Event
}

Adapter knows how to launch and interpret one specific CLI agent.

type AdapterDiag

type AdapterDiag struct {
	Name      string `json:"name"`
	Available bool   `json:"available"`
	Detail    string `json:"detail"`
	Launcher  string `json:"launcher,omitempty"` // what PATH resolution found
	RunsAs    string `json:"runs_as,omitempty"`  // what we would actually execute
	ShimFixed bool   `json:"shim_resolved,omitempty"`
}

AdapterDiag reports how one adapter resolves on this machine.

type ClaudeAdapter

type ClaudeAdapter struct {
	Bin             string   // launcher; defaults handled by caller ("claude")
	PermissionMode  string   // --permission-mode value
	AllowedTools    string   // --allowedTools value (patterns, e.g. "Bash(git *),Edit")
	DisallowedTools string   // --disallowedTools value
	AppendPrompt    string   // --append-system-prompt value (standing guidance)
	ExtraArgs       []string // appended verbatim
}

ClaudeAdapter drives Claude Code in headless (`-p`) mode with streaming JSON.

func NewClaudeAdapter

func NewClaudeAdapter(bin, permissionMode, allowedTools, disallowedTools, appendPrompt string, extraArgs []string) *ClaudeAdapter

NewClaudeAdapter constructs a Claude Code adapter.

func (*ClaudeAdapter) Available

func (a *ClaudeAdapter) Available() (bool, string)

func (*ClaudeAdapter) Command

func (a *ClaudeAdapter) Command(ctx context.Context, spec RunSpec) (*exec.Cmd, error)

func (*ClaudeAdapter) Name

func (a *ClaudeAdapter) Name() string

func (*ClaudeAdapter) ParseLine

func (a *ClaudeAdapter) ParseLine(line string) Event

func (*ClaudeAdapter) SupportsPlanOnly

func (a *ClaudeAdapter) SupportsPlanOnly() bool

SupportsPlanOnly reports that Claude Code can propose without executing, via --permission-mode plan.

type CursorAdapter

type CursorAdapter struct {
	Bin       string   // fallback launcher when detection fails ("cursor-agent")
	ExtraArgs []string // appended verbatim
	// contains filtered or unexported fields
}

CursorAdapter drives Cursor's headless agent (`cursor-agent -p`).

The `cursor-agent` launcher is a shell/PowerShell wrapper around a bundled Node runtime. Where possible we detect that bundled node + index.js and invoke them directly, mirroring the wrapper's own dispatch logic: on Windows this avoids .cmd/.ps1 re-quoting entirely. If detection fails we simply fall back to the `cursor-agent` launcher on PATH.

func NewCursorAdapter

func NewCursorAdapter(bin string, extraArgs []string) *CursorAdapter

NewCursorAdapter constructs a Cursor adapter, attempting node.exe detection.

func (*CursorAdapter) Available

func (a *CursorAdapter) Available() (bool, string)

func (*CursorAdapter) Command

func (a *CursorAdapter) Command(ctx context.Context, spec RunSpec) (*exec.Cmd, error)

func (*CursorAdapter) Name

func (a *CursorAdapter) Name() string

func (*CursorAdapter) ParseLine

func (a *CursorAdapter) ParseLine(line string) Event

ParseLine is tolerant of Cursor's exact schema: it extracts a session id and a terminal result where recognizable, and always preserves the raw line so callers never lose output even if the schema drifts. Task completion is also backstopped by the process exit code in the task manager.

type CustomAdapter

type CustomAdapter struct {
	Bin          string
	ArgsTemplate []string
	// contains filtered or unexported fields
}

CustomAdapter drives *any* CLI agent, configured entirely from the environment — no Go code required. This is the escape hatch that makes the server usable with tools this project doesn't ship an adapter for.

You give it a binary and an argument template. Each template argument may contain placeholders that are substituted per run:

{{prompt}}   the task text
{{cwd}}      the working directory
{{model}}    the model override (may be empty)
{{session}}  the session id when resuming (empty on the first turn)

Rule: if an argument's placeholder expands to an empty value, that whole argument is dropped. So write optional flags in the single-argument `--flag=value` form (e.g. `--model={{model}}`) — that way the flag disappears cleanly when the value is absent, instead of leaving a dangling `--model`.

Output handling is deliberately forgiving: JSON lines are parsed tolerantly (a recognized session id enables follow-ups; a recognized terminal event sets the result), and plain-text lines are streamed as progress. Completion is determined by the process exit code, and because most simple CLIs just print their answer, the collected output is used as the task result.

func NewCustomAdapter

func NewCustomAdapter(name, bin string, argsTemplate []string) *CustomAdapter

NewCustomAdapter builds a user-configured adapter. An empty name defaults to "custom"; an empty bin yields an adapter that reports itself unconfigured.

func (*CustomAdapter) Available

func (a *CustomAdapter) Available() (bool, string)

func (*CustomAdapter) Command

func (a *CustomAdapter) Command(ctx context.Context, spec RunSpec) (*exec.Cmd, error)

func (*CustomAdapter) Name

func (a *CustomAdapter) Name() string

func (*CustomAdapter) ParseLine

func (a *CustomAdapter) ParseLine(line string) Event

func (*CustomAdapter) UseOutputAsResult

func (a *CustomAdapter) UseOutputAsResult() bool

UseOutputAsResult reports that this agent has no reliable terminal result event, so the task manager should fall back to its collected output.

type DiagnosticReport

type DiagnosticReport struct {
	OS            string        `json:"os"`
	Arch          string        `json:"arch"`
	Executable    string        `json:"executable"`
	Packaged      bool          `json:"packaged_app"`
	PackageName   string        `json:"package_name,omitempty"`
	SpawnProbes   []SpawnProbe  `json:"spawn_probes"`
	Adapters      []AdapterDiag `json:"adapters"`
	EnvRepaired   []string      `json:"env_repaired,omitempty"`
	Notes         []string      `json:"notes,omitempty"`
	SpawnWorks    bool          `json:"spawn_works"`
	SilentFailure bool          `json:"silent_failure_detected"`

	// InteractivePermission says whether a worker can put a permission request
	// to the person driving this client, and when it cannot, why. It belongs
	// here because the symptom of it being off is a task that quietly does less
	// than it was asked to, with nothing in the transcript pointing at the cause.
	InteractivePermission bool   `json:"interactive_permission"`
	PermissionDetail      string `json:"interactive_permission_detail,omitempty"`
}

DiagnosticReport is the full picture.

func Diagnose

func Diagnose(ctx context.Context, reg *Registry) DiagnosticReport

Diagnose runs the probes. It never mutates anything and never runs a user-supplied command.

func (DiagnosticReport) Text

func (r DiagnosticReport) Text() string

Text renders the report for a human reading the tool result.

type Event

type Event struct {
	Raw        string // the original line, always kept
	SessionID  string // non-empty when this line revealed the session id
	Text       string // human-facing text extracted from this line, if any
	Final      bool   // true when this is the terminal result event
	FinalError bool   // for a final event: whether the run reported an error
	FinalText  string // for a final event: the summarized result text
	ToolName   string // when the worker invoked a tool, its name (for the audit trail)
	ToolInput  string // the tool's input, compact JSON, truncated (for the audit trail)

	IsToolResult    bool // this line is a tool's result coming back
	ToolResultError bool // for a tool result: whether the tool reported failure

	// Model is the model the agent reported it is actually using, which is the
	// only way to learn it when no override was requested.
	Model string

	// Usage is non-nil on a line that carried accounting.
	Usage *Usage
}

Event is the adapter's interpretation of a single line of agent stdout. Raw is always populated; the other fields are best-effort extractions.

type MockAdapter

type MockAdapter struct {
	// contains filtered or unexported fields
}

MockAdapter is a self-contained agent used for testing the full spawn → stream → parse → complete pipeline without Claude Code or Cursor installed. It re-invokes this very binary's hidden `__mock` subcommand, which emits Claude-compatible stream-json.

func NewMockAdapter

func NewMockAdapter() *MockAdapter

NewMockAdapter builds a mock adapter bound to the current executable.

func (*MockAdapter) Available

func (a *MockAdapter) Available() (bool, string)

func (*MockAdapter) Command

func (a *MockAdapter) Command(ctx context.Context, spec RunSpec) (*exec.Cmd, error)

func (*MockAdapter) Name

func (a *MockAdapter) Name() string

func (*MockAdapter) ParseLine

func (a *MockAdapter) ParseLine(line string) Event

func (*MockAdapter) SupportsPlanOnly

func (a *MockAdapter) SupportsPlanOnly() bool

SupportsPlanOnly lets the mock exercise the plan-only path end to end.

type PlanCapable

type PlanCapable interface {
	SupportsPlanOnly() bool
}

PlanCapable is an optional interface for adapters whose agent can run a plan-only turn: propose the steps it *would* take, executing nothing.

This must fail closed. Callers are required to check it before honouring RunSpec.PlanOnly, because an adapter that silently ignored PlanOnly would execute the task when the caller explicitly asked it not to.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the set of adapters known to the server.

func NewRegistry

func NewRegistry(adapters ...Adapter) *Registry

NewRegistry builds a registry from the given adapters, in order.

func (*Registry) All

func (r *Registry) All() []Adapter

All returns adapters in registration order.

func (*Registry) Get

func (r *Registry) Get(name string) Adapter

Get returns the adapter for name, or nil.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns the registered adapter names in registration order.

type ResultFromOutput

type ResultFromOutput interface {
	UseOutputAsResult() bool
}

ResultFromOutput is an optional interface for adapters whose agent has no reliable terminal result event. When an adapter implements it and returns true, the task manager falls back to the turn's collected output as the task result instead of leaving it empty.

type RunSpec

type RunSpec struct {
	Prompt       string   // the instruction/task text
	Cwd          string   // working directory for the child process
	Model        string   // optional model override
	SessionID    string   // when set, resume this session instead of starting fresh
	ExtraArgs    []string // extra flags appended verbatim
	AllowedTools []string // per-run tools to pre-approve (merged with server policy)
	PlanOnly     bool     // propose a plan without executing anything

	// MaxCostUSD, when > 0, is what this ONE turn may spend. The manager passes
	// the budget the task has left rather than the configured total, so a task
	// driven through several turns cannot be handed the whole allowance again on
	// each of them.
	MaxCostUSD float64

	// MCPConfigPath and PermissionTool wire the agent to an approval endpoint,
	// so a tool call it would otherwise stall on becomes a question asked of
	// the human upstream. Both are set together or not at all.
	MCPConfigPath  string
	PermissionTool string
}

RunSpec fully describes one turn to run against an agent.

type SpawnProbe

type SpawnProbe struct {
	Name     string `json:"name"`
	Command  string `json:"command"`
	OK       bool   `json:"ok"`
	ExitCode int    `json:"exit_code"`
	Output   string `json:"output,omitempty"`
	Err      string `json:"error,omitempty"`
	Silent   bool   `json:"silent,omitempty"` // failed AND produced no output at all
}

SpawnProbe is the result of trying to run one harmless command.

type Usage added in v0.11.0

type Usage struct {
	CostUSD          float64 `json:"cost_usd,omitempty"`
	DurationMS       int64   `json:"duration_ms,omitempty"`
	APIDurationMS    int64   `json:"api_duration_ms,omitempty"`
	NumTurns         int     `json:"num_turns,omitempty"`
	InputTokens      int     `json:"input_tokens,omitempty"`
	OutputTokens     int     `json:"output_tokens,omitempty"`
	CacheReadTokens  int     `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens int     `json:"cache_write_tokens,omitempty"`
}

Usage is the accounting an agent reports for the work it did: what it cost, how long it spent, and how many tokens it moved.

It is worth extracting rather than discarding. Delegating to a headless worker hides exactly the things a person watching a terminal would have seen, and cost is the one that compounds silently — a task that quietly burned two dollars looks identical to one that burned two cents until the bill arrives.

func (*Usage) Add added in v0.11.0

func (u *Usage) Add(v Usage)

Add accumulates another turn's accounting into u. Counters sum; NumTurns is a running total the agent already reports for the session, so the later value replaces rather than adds to the earlier one.

func (Usage) Empty added in v0.11.0

func (u Usage) Empty() bool

Empty reports whether the agent told us nothing worth showing.

Jump to

Keyboard shortcuts

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