agent

package
v0.0.0-...-7911669 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package agent is the registry of coding agents coop can drive. Each agent is one file implementing Agent and self-registering; adding or removing an agent is a single-file change, and the compiler enforces that every agent answers every question — no switch case to forget.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AuthenticationFailure

func AuthenticationFailure(provider, output string) bool

AuthenticationFailure reports whether provider-owned output proves that provider's login failed. Signals stay adapter-owned; anchoring them to an error-shaped line keeps ordinary narration about authentication from killing a healthy credential or replaying work under another account.

func CLIRateLimited

func CLIRateLimited(output string) bool

CLIRateLimited reports whether provider CLI output carries a broad rate-limit marker. Callers must also require a non-zero exit status; successful task output is untrusted prose and must never trigger rotation by itself.

func ClassifyCLIError

func ClassifyCLIError(spec LiveCredentialSpec, output string) string

ClassifyCLIError returns a redacted diagnostic class without retaining provider output.

func ClaudeProjectKey

func ClaudeProjectKey(ws string) string

ClaudeProjectKey is how Claude Code names a project's session dir: the absolute cwd with every non-alphanumeric character (not just "/") turned into a dash. coop must match it exactly to find the session file to resume.

func DisplayTarget

func DisplayTarget(target string) string

DisplayTarget keeps credential diagnostics one-line and terminal-safe even if a legacy profile directory or hand-written preset contains control characters.

func LoginCommand

func LoginCommand(target string) string

LoginCommand renders a copy-pasteable shell command for one provider@account target. Profile directories can predate today's CLI validation or be created by hand, so quote an unsafe target instead of letting its name become shell syntax in an actionable error.

func Names

func Names() []string

Names returns every registered agent name, sorted for a stable order.

func Packages

func Packages() []string

Packages is the union of every agent's npm packages, for the box image's install.

func ShellRateLimitDetector

func ShellRateLimitDetector() string

ShellRateLimitDetector renders the POSIX-shell function used by generated role wrappers. The literal marker rows come from cliLimitMarkers, so adding a provider phrase updates host and in-box classification together.

func SupportsEffort

func SupportsEffort(a Agent) bool

SupportsEffort reports whether the agent has any reasoning-effort control (a CLI flag or an env var). A target that names an effort for an agent without one is rejected in ParseTarget.

func Valid

func Valid(name string) bool

Valid reports whether name is a known agent.

func ValidSessionID

func ValidSessionID(id string) bool

ValidSessionID accepts the canonical UUID form used by provider session stores. Session metadata is provider-writable, so unchecked values must never reach provider argv.

func WrapperRateLimited

func WrapperRateLimited(output string) bool

WrapperRateLimited applies the stricter detector used after a failed role command.

Types

type ACPSessionSetting

type ACPSessionSetting struct {
	Method   ACPSettingMethod
	ConfigID string
	Value    string
}

ACPSessionSetting is one provider-owned, ordered setting Coop applies after every session new/load/recreate. Order is significant when one setting resets another.

type ACPSettingMethod

type ACPSettingMethod uint8

ACPSettingMethod is how an adapter changes one setting on an established session.

const (
	ACPSetConfigOption ACPSettingMethod = iota
	ACPSetModel
)

type ACPSignal

type ACPSignal struct {
	Key   string
	Value string
}

ACPSignal is one structured rate-limit marker in an ACP adapter's JSON-RPC errors: a string value (optionally pinned to the JSON key carrying it; "" matches any key) that structurally proves a rate/usage limit. Matching is compact — lowercased with _-/space stripped — so RESOURCE_EXHAUSTED and resourceExhausted are one marker.

type Agent

type Agent interface {
	Name() string
	// DisplayName is the human product name for UX surfaces (the ACP toolbar dropdowns):
	// "Claude Code", "Codex", … Name() stays the grammar token everywhere a value is parsed.
	DisplayName() string
	// Badge is the 1-cell letter identifying this agent in dense dashboards (the fleet/watch
	// grids), so a row names who runs it without spending a name column. Plain data — the
	// presentation layer paints it (internal/cli's agentBadgeColors).
	Badge() string
	// Interactive is the autonomous default command — what `coop <agent>` runs.
	Interactive(cfg *config.Config) []string
	// Headless is the one-shot, non-interactive form carrying a prompt (the loop).
	Headless(cfg *config.Config, prompt string) []string
	// Stream is the agent's structured-output schema and the flags that enable it.
	Stream() StreamSpec
	// ACP is the agent's ACP adapter command over stdio (for editors like Zed). It takes
	// cfg so an adapter that IS the agent's own binary (gemini --acp) can carry the
	// resolved model flag; a separate adapter binary (claude-agent-acp, codex-acp) takes
	// no flags — claude's picks the model up via ModelEnv instead.
	ACP(cfg *config.Config) []string
	// ACPSessionDirs are the agent-home-relative dirs where this agent's ACP adapter keeps session
	// state — the transcript AND any session index/aux state session/load needs (claude keeps a
	// sessions/ index alongside the projects/ transcript). For an ACP box coop bind-mounts a shared,
	// credential-independent copy of each so switching the credential mid-session doesn't lose the
	// conversation — session/load still finds it. Empty → no sharing for this agent.
	ACPSessionDirs() []string
	// Resume re-enters a fork's interactive session, scoped to ws; the bool reports
	// whether a session was found (else the caller starts fresh via StartSession). id
	// is the persisted session id for this (fork, agent, account): preset-id agents resume the
	// coop-owned id; codex resumes its previously discovered native id. Fork launch owns the
	// one-time legacy Codex lookup, so ordinary adapter resumes remain exact-only.
	Resume(cfg *config.Config, ws, id string) ([]string, bool)
	// StartSession is the fresh interactive command under the coop-chosen session id:
	// claude/gemini/grok stamp it via --session-id so a later Resume can pin exactly it;
	// codex ignores id and mints its own. An empty id falls back to Interactive.
	StartSession(cfg *config.Config, id string) []string
	// PresetSessionID reports whether the agent honors a caller-chosen session id. When
	// false (codex), coop allocates none and discovers the native ID around a fresh run.
	PresetSessionID() bool
	// Login authenticates the agent (its token persists in its config dir).
	Login(cfg *config.Config) []string
	// ConsultCmd is the read-only, non-interactive command to ask this agent a
	// question as a fusion peer — it returns analysis and never edits files.
	ConsultCmd(question string) []string
	// InstructionFile is the agent's native global instruction filename, e.g.
	// "CLAUDE.md" — where coop writes the shared INSTRUCTIONS.md and fusion directive.
	InstructionFile() string
	// NativeSubagents owns this adapter's generated native-role format and in-home destination.
	// A zero descriptor means native preset roles degrade to read-only consults.
	NativeSubagents() NativeSubagentSupport
	// AuthMarker is the credential file (under the agent's config dir) it writes on login and
	// its canonical primary env-file key. Presence checks use CredentialEnvKeys so alternate
	// tokens are first-class too.
	AuthMarker() (file, envKey string)
	// CredentialEnvKeys is every env-file key this agent reads a token from — the
	// AuthMarker key plus any alternates it honors (e.g. claude also reads
	// ANTHROPIC_AUTH_TOKEN and CLAUDE_CODE_OAUTH_TOKEN). A scoped run strips all of an
	// out-of-scope agent's keys, so a peer's alternate token can't leak into a box that
	// isn't authorized for it.
	CredentialEnvKeys() []string
	// ActiveCredentialEnvKeys returns the exact env-key authority for one selected account. A
	// nonempty result means those keys, not a stale marker, define presence and execution authority.
	// The caller reports marker presence; adapters decide precedence once from it and their selector.
	ActiveCredentialEnvKeys(profileDir string, markerPresent bool) []string
	// StoredCredentialStatus validates the adapter's native marker for user-facing credential status.
	// It never reads provider-wide env credentials; those retain their presence-based status.
	StoredCredentialStatus(profileDir string, now time.Time) StoredCredentialStatus
	// LiveCredentials declares the access-only credential projection and redacted compatibility
	// diagnostics for this adapter. It is consumed only by opt-in live tests, but compiler-required
	// so a registered provider cannot silently evade the registry-generated suite.
	LiveCredentials() LiveCredentialSpec
	// Models is a short, curated list of model names this agent's CLI accepts — the menu
	// `coop models` shows. Illustrative, not authoritative: model ids churn faster than
	// coop releases, so ANY id the CLI accepts works with --model; coop never validates
	// against this list.
	Models() []string
	// ModelEnv is the environment variable the agent's CLI reads a default model from
	// ("" when it has none). box.Run exports it into the box when a model is resolved, so
	// a separate adapter binary that takes no flags (claude-agent-acp) still honors the
	// chosen model.
	ModelEnv() string
	// Effort is this agent's command grammar for reasoning effort. A zero descriptor means the
	// agent takes no effort flag (gemini has none; a no-flag ACP adapter may use EffortEnv).
	// Levels pass through verbatim; the agent's own CLI validates them.
	Effort() EffortSpec
	// EffortEnv is the environment variable the agent's CLI reads a reasoning effort from
	// ("" when it has none) — the effort analog of ModelEnv, for a no-flag ACP adapter
	// (claude-agent-acp reads CLAUDE_CODE_EFFORT_LEVEL). box.Run exports it when an effort is
	// resolved so that adapter still honors the chosen effort.
	EffortEnv() string
	// MCP returns the config files to mount so the agent sees the shared mcp.json — its
	// native translation (gemini/codex) or none when it reads mcp.json directly (claude).
	MCP(cfg *config.Config) ([]MCPMount, error)
	// ACPMCPServers is the same servers as the ACP session parameter, for an adapter that
	// cannot be pointed at a file: it takes no flags, so a mount MCP already covers is no
	// use to it. Nil for every agent whose adapter reads what MCP mounts — sending the list
	// as well would register each server twice. mcpFile is the mcp.json THIS session runs
	// with, which is not always the shared one; lookupEnv resolves a bearer_token_env_var
	// against the environment that session's box gets, since ACP carries headers only.
	ACPMCPServers(mcpFile string, lookupEnv func(string) (string, bool)) ([]map[string]any, error)
	// EnsureDefaults pre-answers the agent's first-run prompts (theme, folder-trust,
	// sandbox) in its config dir so a fresh box goes straight to work. Best-effort; an
	// agent that needs nothing leaves it empty. workdir is the resolved box cwd.
	EnsureDefaults(cfg *config.Config, workdir string)
	// Packages are the npm packages the box image installs for this agent — its CLI and
	// (if separate) its ACP adapter.
	Packages() []string
	// ACPRateLimitSignals are the STRUCTURED markers this agent's ACP adapter embeds in
	// a JSON-RPC error to signal a rate/usage limit — proof the ACP controller rotates
	// on without parsing prose. The output-token axis (finishReason/stopReason =
	// length/MAX_TOKENS) is a cross-provider convention owned by the controller, not
	// declared here: stopReason is the ACP-protocol stop-reason field and finishReason
	// the common upstream-API leak, so no single adapter owns them.
	ACPRateLimitSignals() []ACPSignal
	// ACPSessionSettings are provider-owned, ordered settings Coop force-applies after a
	// session is (re)established. The target is the complete active provider/model/effort
	// intent. Re-applied on every restart; nil when the adapter uses launch args only.
	ACPSessionSettings(Target) []ACPSessionSetting
	// BoxEnv are env vars this agent's CLI needs inside the box (beyond ModelEnv and
	// credentials), given the box home dir. Exported into every box — a var is inert
	// where its agent isn't running — so a new agent's env needs no box.Run edit.
	BoxEnv(homeInBox string) []string
	// HomeFallbacks are committed repo artifacts Coop may copy into this agent's user-level
	// home for a box run. Each project artifact suppresses its matching fallback. Empty means
	// the agent has no config shape shared through .agent/ beyond workflow skills.
	HomeFallbacks() []HomeFallback
	// ConsultFresh is the shell body for a fresh read-only consult session in the
	// coop-consult wrapper — run against the wrapper's variables $prompt, $id, $model
	// (uniformly resolved), and $candidate_idfile. A fresh arm records only its candidate;
	// the wrapper publishes continuation state after a bounded usable reply. The arm also
	// has the run/new_id helpers. It analyses and reports; it never edits files.
	ConsultFresh() string
	// ConsultResume is the shell body for resuming a consult by the wrapper's validated $id.
	ConsultResume() string
	// DelegateExec is the raw write-capable shell body for coop-delegate, using $prompt,
	// $model, and $effort. It must be one simple command, without a pipeline or control operator,
	// because the wrapper prefixes it with run_delegate to bound the whole provider process group.
	// The wrapper also enforces commit:never and serialization.
	DelegateExec() string
	// ShellPrelude is optional helper-function shell the wrappers emit ONCE before the
	// per-agent case (e.g. codex's output filter); "" for agents that need none.
	ShellPrelude() string
	// InstallScript is a non-npm box-image install command (e.g. an install-script
	// download); "" means this agent installs via Packages() on the npm layer.
	InstallScript() string
}

Agent is everything coop needs to drive one coding agent. To add an agent, write a new file implementing this interface and self-register it from an init().

func Get

func Get(name string) (Agent, bool)

Get returns the agent registered under name.

type CredentialArtifact

type CredentialArtifact struct {
	Name    string
	Primary bool
	Project func([]byte) ([]byte, error)
}

CredentialArtifact is one adapter-owned file that can be copied into an isolated credential home. Name is a single basename. Primary marks the AuthMarker file; every other artifact is an optional refresh companion or selector. Project is required: it synthesizes an auth-only representation and returns nil data when the source contains no portable credential state.

type CredentialPortability

type CredentialPortability uint8

CredentialPortability says whether an isolated file credential can be used without carrying refresh authority. NotPortable covers host-bound keychains; RefreshRequired covers a portable access-token shape whose current token will not outlive the requested deadline.

const (
	CredentialUnknown CredentialPortability = iota
	CredentialPortable
	CredentialRefreshRequired
	CredentialNotPortable
)

type EffortFlagStyle

type EffortFlagStyle uint8

EffortFlagStyle is how an agent's command expresses one reasoning-effort value.

const (
	EffortFlagSplit      EffortFlagStyle = iota // --effort high
	EffortFlagJoined                            // --effort=high
	EffortFlagAssignment                        // -c model_reasoning_effort=high
)

type EffortSpec

type EffortSpec struct {
	Style      EffortFlagStyle
	Flag       string
	Aliases    []string
	Assignment string
}

EffortSpec is the adapter-owned command grammar for reasoning effort. Assignment is used only with EffortFlagAssignment; Aliases are alternate flag names accepted by the same grammar.

func (EffortSpec) Args

func (s EffortSpec) Args(level string) []string

type HomeFallback

type HomeFallback struct {
	Source  string
	Project string
	Target  string
	Dir     bool
}

HomeFallback describes one agent-owned config artifact synthesized from a committed source. Paths are repo-relative except Target, which is relative to the agent's user-level home.

type LiveCredentialSpec

type LiveCredentialSpec struct {
	Artifacts []CredentialArtifact
	// Prepare runs against the trusted source profile before projection. It may renew an
	// expiring access credential, but refresh authority must remain in the source profile.
	Prepare     func(profileDir string, deadline time.Time) error
	Portability func(profileDir string, deadline time.Time) CredentialPortability
	AuthSignals []string
}

LiveCredentialSpec is the complete adapter-owned boundary for opt-in live compatibility tests. Portability inspects only the isolated projected profile, never the source credential.

type MCPMount

type MCPMount struct {
	Content string
	BoxPath string
}

MCPMount is one generated config file an agent needs to see the shared mcp.json: its content and where it mounts inside the box.

type NativeSubagent

type NativeSubagent struct {
	Name        string
	Description string
	Model       string
	Effort      string
	Prompt      string
}

NativeSubagent is the provider-neutral role material an adapter may render into its own native subagent format. The adapter owns file syntax and destination; preset owns the role semantics.

type NativeSubagentSupport

type NativeSubagentSupport struct {
	HomeDir string
	Render  func(NativeSubagent) (filename, content string)
}

NativeSubagentSupport describes an adapter's complete native-role capability. A zero value means unsupported. HomeDir is relative to that adapter's in-box home; Render returns one file.

type SessionDiscoverer

type SessionDiscoverer interface {
	LatestSessionID(cfg *config.Config, cwd string) string
	SessionIDs(cfg *config.Config, cwd string) []string
	// ProducesSession reports whether launching the agent with these args creates a
	// discoverable interactive session a concurrent producer could collide with — so it
	// needs the interactive-session lock. Codex's `exec` subcommand writes source:"exec"
	// rollouts that discovery excludes, so those are fully concurrent and need no lock.
	ProducesSession(args []string) bool
}

SessionDiscoverer is the optional capability for an adapter that cannot choose its new session ID but can discover the native ID after a run. Forks persist it and resume exactly it.

type StoredCredentialStatus

type StoredCredentialStatus uint8

StoredCredentialStatus is the adapter's best-effort validity check for its native credential marker. Unknown preserves presence-based behavior for opaque or host-bound stores. Ready includes credentials the native CLI can refresh without another login.

const (
	StoredCredentialUnknown StoredCredentialStatus = iota
	StoredCredentialReady
	StoredCredentialReauthRequired
)

type StreamFormat

type StreamFormat uint8

StreamFormat identifies the provider-owned NDJSON schema emitted by a headless agent.

const (
	StreamNone StreamFormat = iota
	StreamClaudeJSON
	StreamCodexJSON
	StreamGeminiJSON
	StreamGrokJSON
)

type StreamSpec

type StreamSpec struct {
	Format        StreamFormat
	Flags         []string
	TrailingArgs  int
	ToolLifecycle StreamToolLifecycle
}

StreamSpec describes how a headless command opts into structured output. TrailingArgs keeps positional prompts (or a flag/value prompt pair) after the inserted stream flags. ToolLifecycle is the adapter's declaration about its own schema (see StreamToolLifecycle).

func (StreamSpec) TracksTools

func (s StreamSpec) TracksTools() bool

TracksTools reports whether the watchdog may supervise this stream's foreground tools — suspend its idle deadline on a tool start and cap the oldest open one. Only an explicit ToolLifecycleIDs declaration qualifies: an undeclared stream reads as absent, so a provider nobody probed gets the conservative policy rather than a deadline its schema cannot feed.

type StreamToolLifecycle

type StreamToolLifecycle uint8

StreamToolLifecycle is what a provider's structured stream proves about the FOREGROUND TOOLS it runs — the one capability the attempt watchdog changes policy on. It is a DECLARATION each adapter makes about its own schema, never something coop infers at runtime from process names, CPU, or the bytes seen so far: a stream that has simply not opened a tool yet is indistinguishable from one that never will.

const (
	// ToolLifecycleUndeclared is the zero value — nobody probed this stream. It is not a third
	// behavior: consumers read it as ToolLifecycleAbsent (the conservative side), and
	// TestEveryStreamDeclaresItsToolLifecycle fails so it cannot ship.
	ToolLifecycleUndeclared StreamToolLifecycle = iota
	// ToolLifecycleAbsent: the stream reports no tool start or end at all, so silence during a
	// long foreground gate is indistinguishable from a wedged attempt.
	ToolLifecycleAbsent
	// ToolLifecycleIDs: tool starts and ends arrive under a provider-supplied id the watchdog can
	// pair, so an open tool can suspend the idle deadline and carry its own absolute cap.
	ToolLifecycleIDs
)

type Target

type Target struct {
	Provider string   // a registered agent; required
	Model    string   // "" = the agent CLI's own default model
	Effort   string   // "" = the agent CLI's own default; else a reasoning-effort level passed to the agent
	Accounts []string // nil/empty = every signed-in account (the widest ladder); else the explicit subset, in order
}

Target is coop's single addressing scheme — WHO runs on WHAT: a provider, optionally a model, optionally a reasoning effort, optionally one or more accounts. It is the ONE spelling used on the CLI, in preset `agent:` keys, and in fleet entries, parsed by ParseTarget. The wire grammar is

provider[:model][/effort][@account[,account…]]

claude                     provider only          (model → the CLI default; all accounts)
claude:opus                provider + model
claude:opus/xhigh          provider + model + reasoning effort
codex/high@work            provider + effort + account (default model)
claude:opus@work,personal  provider + model + an ACCOUNT LADDER (fan out work → personal)

`:` splits provider from model; `/` sets reasoning effort (a level the agent's CLI accepts — low/medium/high/xhigh/max — coop passes it through, the agent validates); `@` starts the account slot; `,` separates ACCOUNTS (and only accounts, at every level — a list of peers is repeated flags, never a comma list).

func ParseTarget

func ParseTarget(s string) (Target, error)

ParseTarget parses one target token, validating SYNTAX and that the provider is a registered agent. It does NOT check that the accounts exist (that needs the config's signed-in list — the caller validates against cfg.Profiles). A malformed token returns an error naming the fix; this is the single place every surface funnels through, so the diagnostics are identical everywhere.

func (Target) Account

func (t Target) Account() string

Account returns the target's single account — the first listed, or "" when none. It is the rung view: a rotation ladder expands a multi-account target into concrete one-account rungs (expandLadder), so rung consumers (the loop, ACP, applyPreset) read this instead of Accounts.

func (Target) String

func (t Target) String() string

String renders a target back to its wire form (provider[:model][/effort][@a,b]) — for messages, config round-trips, and tests. A Target with only a provider is just the provider.

Jump to

Keyboard shortcuts

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