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 ¶
- func CanPlan(a Adapter) bool
- func RepairedEnviron() (env []string, repaired []string)
- func RunMock(args []string) int
- type Adapter
- type AdapterDiag
- type ClaudeAdapter
- type CursorAdapter
- type CustomAdapter
- type DiagnosticReport
- type Event
- type MockAdapter
- type PlanCapable
- type Registry
- type ResultFromOutput
- type RunSpec
- type SpawnProbe
- type Usage
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RepairedEnviron ¶
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.
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) 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) 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) 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) 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 ¶
NewRegistry builds a registry from the given adapters, in 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.