adapter

package
v0.13.22 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package adapter is the FROZEN anti-corruption boundary (Epic 9, T-1/T-5/T-6) between swarm's core (daemon/shim/wire/TUI) and the concrete agent CLIs it drives. An Adapter is a stateless, goroutine-safe STRATEGY OBJECT: it answers questions about a CLI (how it is named on PATH and how its version prints, which versions it supports, how to compose its launch/resume argv, which launch options and signal sources it exposes, how to read a conversation id out of its output) but owns NO lifecycle — no process, no fd, no socket, no disk. Core owns all of that. Detection is descriptor-based: the adapter supplies pure Binary/VersionArgs/ParseVersion descriptors and the CORE Detect function (below) does the LookPath/exec through a HostProber, so the boundary stays genuinely I/O-free (ADR-001 / E9.2).

The boundary depends only on this package plus internal/vt (the Snap projection ExtractConversationID reads); adding a real adapter later touches only that adapter's own package. This file pins the interface method set and every data type; conformance.go pins the behavior a conforming adapter must exhibit; fixture.go/capability.go pin the characterization corpus and its derived capability matrix.

Index

Constants

View Source
const (
	KindUserMessage      = "user_message"
	KindAgentMessage     = "agent_message"
	KindToolRun          = "tool_run"
	KindFileChange       = "file_change"
	KindApprovalRequest  = "approval_request"
	KindApprovalResolved = "approval_resolved"
	KindPlanUpdate       = "plan_update"
	KindSessionStatus    = "session_status"
)

Item kinds — interaction-schema.md §3, the AEAD-plaintext-bound discriminator. It lives only inside the item payload: nothing here is ever written to SenderKeyID or EpochID (IS-LAYER-2, PB-SYNC-1).

View Source
const (
	StatusInProgress = "in_progress"
	StatusCompleted  = "completed"
	StatusFailed     = "failed"
	StatusDeclined   = "declined"
)

Item statuses — interaction-schema.md §4. in_progress is the only non-terminal one; IS-ST-1 allows at most one terminal status per item.

View Source
const (
	ModeCard       = "card"
	ModePromptCard = "prompt_card"
)

Approval apply modes — interaction-schema.md §3.5. card means Decision applies the verdict natively; prompt_card is IS-LIFE-6's fallback, where the machine injects a mapped keystroke instead.

View Source
const (
	SourcePhone     = "phone"
	SourceOwner     = "owner"
	SourceDerived   = "derived"
	SourceSynthetic = "synthetic"
)

user_message sources — interaction-schema.md §3.1, plus one the wire never carries.

SourceSynthetic is the CLI's OWN envelope posted through its prompt hook (Claude Code's system-reminder, teammate-message, task-notification, a slash command's stdout; phone refit W2.4, round-1 review ruling). It is a user_message because a user_message is the only turn-opening signal that CLI gives, and the daemon opens the turn on it -- then neither persists nor publishes it, so §3.1's phone|owner|derived stays the whole of the wire's vocabulary. Validate admits it so the daemon can see it at all.

View Source
const (
	VerdictAllow = "allow"
	VerdictDeny  = "deny"
	VerdictOther = "other"
)

Decision verdicts — the grant/refuse polarity of one offered decision, set by the adapter AT CAPTURE from its own CLI vocabulary (owner ruling 2026-08-07, ADR-009's amendment of that date). interaction-schema.md §3.5 keeps the decision IDS the CLI's own — Codex offers accept | acceptWithExecpolicyAmendment | cancel — so this is the ONLY normalized thing about a decision, and it is what §3.6's allowed/denied split is classified from.

VerdictOther is IS-TOOL-2's posture applied here: a decision the adapter cannot place is declared unclassified rather than guessed at.

View Source
const CaptureRaw = "raw"

CaptureRaw is the ONLY capture value ADR-010 §1 defines.

ponytail: an unrecognized value is a conformance VIOLATION, not an ignored key. A typo would otherwise silently flatten the CLI's bodies away with no signal at all — the failure would surface as an empty transcript, three layers downstream.

View Source
const DescriptorCapture = "capture"

DescriptorCapture is the SignalSource.Descriptor key an adapter sets to declare that an event's body must be PRESERVED rather than flattened to top-level strings (ADR-010 §1). Capture is declared in the existing descriptor map, so SignalSource itself is untouched and an adapter that declares nothing behaves exactly as today.

View Source
const FixtureSchemaVersion = 1

FixtureSchemaVersion is the current fixture schema version. Bump it whenever the Fixture/HookPayload wire format changes; fixtureio.LoadFixture rejects any newer version and Validate rejects any other version.

Variables

This section is empty.

Functions

func AcceptsConversationID

func AcceptsConversationID(a Adapter, id string) bool

AcceptsConversationID applies an adapter's native-ID validator when it has one. Adapters without this optional extension retain their historical opaque- ID behavior.

func CanonicalTopLevelConversationID

func CanonicalTopLevelConversationID(raw json.RawMessage, key string) (string, bool)

CanonicalTopLevelConversationID reads exactly one named string field from one top-level JSON object. It rejects duplicate keys (including duplicates with the same value), trailing JSON and oversized bodies so an adapter cannot guess an identity out of ambiguous or unbounded input.

func CaptureEvents

func CaptureEvents(a Adapter) []string

CaptureEvents returns the events a declares capture=raw on, in declaration order. An adapter that declares none returns nil, which is the ordinary case: the extension is optional (ADR-010 §5).

It is the READ side of DescriptorCapture, and it exists because the declaration has to travel. A `swarm hook` process knows its event name but not the adapter that launched it, so the core resolves the rows once at spawn and injects them (hookclient.EnvCapture) — the same trick Detect(a, HostProber) plays, with the adapter supplying pure data and the core doing everything else.

func CheckBackendPlan

func CheckBackendPlan(src BackendSource, spec BackendSpec) error

CheckBackendPlan is conformance obligation 8: a DECLARED backend must name a non-empty Program. A declared backend that starts nothing is a session whose agent attaches to a socket nobody serves.

func CheckConformance

func CheckConformance(a Adapter) []error

CheckConformance returns every conformance violation in a, or an empty slice for a fully conformant adapter. It is pure: it calls only the adapter's pure methods, opens nothing, and mutates no shared state. Every check runs against the ADAPTER UNDER TEST (a), so a real adapter's own extractor/descriptors are probed, not a fixed baseline.

func CheckInteractionFixture

func CheckInteractionFixture(a Adapter, fx Fixture) []error

CheckInteractionFixture runs ADR-010's conformance obligations 3 (corpus half) and 4 against a recorded fixture: every payload is replayed through the adapter's shaper, every shaped item must be well-formed, and any event that shapes an item MUST declare capture=raw — at runtime an undeclared event's body is flattened to top-level strings before the shaper ever sees it (ADR-010 §6), so shaping it here would be proving something the ingest path cannot deliver.

It is PURE: fx is already in memory (the disk read lives in fixtureio, E9.2). An adapter with no InteractionSource has nothing to replay and yields nothing. The per-CLI GOLDEN comparison is the caller's: it holds the expected items.

func Conformance

func Conformance(t *testing.T, a Adapter)

Conformance runs the conformance suite against a and reports each violation via t.Error. It is the entrypoint real adapters call from their tests. Beyond the pure CheckConformance rules it runs the goroutine-neutrality and parallel-determinism checks against THIS adapter (not a fixed baseline), so a real adapter that leaks a goroutine or races on shared state is caught here. Both use only runtime introspection — no fd/exec — so the contract package stays I/O-free (the fd-count signal, which needs /dev/fd, is a test-layer check in conformance_test.go).

func IsCanonicalConversationID

func IsCanonicalConversationID(id string) bool

IsCanonicalConversationID accepts the canonical lowercase UUID spelling used by both Codex and Claude. Provider ids are opaque beyond this syntax: no UUID version is imposed because currently observed Codex ids include UUIDv7 while Claude ids include UUIDv4.

Types

type Adapter

type Adapter interface {
	// Name is the stable, non-empty identifier of the CLI this adapter drives
	// (e.g. "claude-code"). It never changes across calls.
	Name() string

	// Binary is the executable name to find on PATH (e.g. "claude"). It is a
	// pure descriptor — non-empty, never a path with metacharacters — that the
	// CORE Detect function feeds to a HostProber's LookPath. The adapter owns no
	// fds: it merely names what to look for.
	Binary() string

	// VersionArgs are the arguments that make the CLI print its version
	// (e.g. {"--version"}). It is a pure descriptor and must be non-nil (an
	// empty slice, meaning "the bare binary prints its version", is allowed;
	// nil is not). Detect passes these to HostProber.Run.
	VersionArgs() []string

	// ParseVersion extracts a semantic version from the version command's stdout.
	// It is PURE and TOTAL: it never panics on any string (garbage, empty,
	// multibyte, unbounded) and is deterministic. ok==true implies a non-empty
	// version. Detect calls it on the stdout HostProber.Run captured.
	ParseVersion(output string) (version string, ok bool)

	// SupportedVersions is the inclusive version range this adapter is known to
	// drive correctly.
	SupportedVersions() VersionConstraint

	// Command composes the exec argv for a fresh launch. argv[0] is a real
	// program path — never a shell, never a single metacharacter-bearing string.
	// It is pure: the same spec yields the same argv, and it opens nothing.
	Command(spec LaunchSpec) ([]string, error)

	// Options is the declarative launch-option schema the launch form renders.
	Options() []OptionSpec

	// SignalSources declares how this CLI's idle/active signal is observed
	// (hooks, an event stream, or grid heuristics).
	SignalSources() []SignalSource

	// Resume composes the exec argv to resume an existing conversation. With no
	// ConversationID it returns an empty argv (nothing to resume); a non-empty
	// argv always carries the id.
	Resume(spec ResumeSpec) ([]string, error)

	// ExtractConversationID reads the conversation id out of the CLI's rendered
	// grid and/or raw transcript tail. It is TOTAL — never panics on a nil or
	// garbage grid+tail — and deterministic. ok==true implies a non-empty id.
	ExtractConversationID(grid *vt.Snap, tail []byte) (string, bool)
}

Adapter is the ONE interface every agent-CLI integration implements. Every method is pure and goroutine-safe: given the same inputs it returns the same outputs, holds no mutable state, and may be shared by value across goroutines.

type ApprovalApplier

type ApprovalApplier interface {
	// ApprovalKeys returns the keystrokes that answer the dialog CURRENTLY on
	// snap with the given verdict (VerdictAllow | VerdictDeny), to be written to
	// the session's PTY VERBATIM.
	//
	// action is the PENDING REQUEST'S own ToolAction.Type, as the adapter
	// classified it at capture, and the adapter must refuse a dialog that is not
	// THAT request's. Proving only that some answerable dialog is on screen is
	// not enough: a hook is fire-and-forget, so a dialog reaches the glass before
	// its own card exists, and an answer for the request the owner just closed at
	// the terminal would otherwise be typed into the one that replaced it. Which
	// screens belong to which action is per-CLI knowledge and therefore the
	// adapter's, exactly like the key map.
	//
	// ok is false for any grid the adapter cannot positively identify as a
	// dialog it has a RECORDED key map for, for a dialog raised by a different
	// action than the request's, and for a verdict that dialog has no key for.
	// All three are refusals and never guesses: a key returned for the wrong
	// screen is typed into whatever has focus, while a refusal only declines the
	// phone's tap and leaves the terminal's own dialog untouched.
	ApprovalKeys(snap *vt.Snap, verdict, action string) (keys string, ok bool)
}

ApprovalApplier is the OPTIONAL interface an adapter implements when a pending approval can be answered by TYPING INTO THE CLI'S OWN DIALOG — the apply path mirror-program.md section 3 makes M1's primary one for Claude Code, after rejecting the held-hook alternative on co-presence grounds (a hook held undecided hides the terminal's own prompt).

It is a PURE function of the rendered grid, which is what keeps it inside the T-5 boundary while being callable from the daemon layer, where the session tap's snapshots arrive.

ABSENCE IS A SIGNAL, exactly as it is for InteractionSource (ADR-010 §5): an adapter that does not implement this has no keystroke answer for its approvals, which is the normal case rather than a defect — mirror-program.md's own table answers Codex by native RPC and opencode over HTTP, and neither should ever be typed at. The daemon refuses to apply rather than inventing a key.

func AsApprovalApplier

func AsApprovalApplier(a Adapter) (ApprovalApplier, bool)

AsApprovalApplier reports whether a can answer its approvals by keystroke.

type AuthProbe added in v0.13.16

type AuthProbe interface {
	// AuthCredentialsFile is the credentials file path RELATIVE to the user
	// home directory (e.g. ".codex/auth.json"). It returns a relative path,
	// never an absolute one: the core joins it beneath the daemon's home, and
	// joining is the core's act.
	AuthCredentialsFile() string

	// AuthIdentity derives a stable ACCOUNT identity from the file's raw bytes.
	// The contract that makes the watcher sound: the identity is INVARIANT
	// under routine token refreshes (providers rewrite the file with fresh
	// tokens on a cadence, so mtime or a whole-file hash would false-positive)
	// and changes exactly when the logged-in account changes. The value must
	// never carry a secret -- derive a digest, not the token. ok==false means
	// the bytes do not parse as this provider's credentials (mid-login, a
	// truncated write, garbage), which the watcher treats as "unknown: hold".
	AuthIdentity(raw []byte) (identity string, ok bool)
}

AuthProbe is the OPTIONAL extension a CLI implements when swarm has CHARACTERIZED where that CLI stores its login credentials on disk. It is discovered by TYPE ASSERTION (AsAuthProbe), never by a method on Adapter: the frozen method set gains nothing (ADR-010 Non-goals; the TranscriptLayout precedent).

WHY IT EXISTS (ADR-024). A provider's long-lived processes -- the PTY CLI and any per-session backend -- load the credentials file once at startup and hold its tokens in memory. A logout/login to another account afterwards rotates the stored account; every process started before the change keeps failing token refresh until it is RESTARTED (the 2026-09-01 "access token could not be refreshed" incident). The probe gives the daemon the two pure halves of the detection: where the credentials live, and what counts as "the same account". Everything else -- reading the file, remembering the last identity, deciding to recycle a session -- is the core's job.

IT NAMES AND DERIVES; IT NEVER OPENS. Both halves are PURE and TOTAL on the same terms as Command/Resume: deterministic, no panic on any input, and NO filesystem access whatsoever. Every byte of I/O stays in the core (ADR-001).

ABSENCE IS THE SIGNAL (ADR-010 section 5). An adapter whose credential layout nobody has characterized implements nothing, the assertion fails, and the watcher simply does not watch that provider.

func AsAuthProbe added in v0.13.16

func AsAuthProbe(a Adapter) (AuthProbe, bool)

AsAuthProbe reports whether a has a characterized credentials layout.

type BackendPlan

type BackendPlan struct {
	// Program is the backend executable's NAME on PATH (e.g. "codex"). It must contain
	// no path separator: naming a program and naming a path are different acts, and only
	// the first is the adapter's.
	Program string
	// Args are the backend process's arguments (argv[1:]).
	Args []string
	// AgentArgs are appended VERBATIM to the agent's own argv so it attaches to the
	// backend the core started. Empty is legal.
	AgentArgs []string
}

BackendPlan is the adapter's pure description of the side process, plus the arguments the AGENT needs in order to attach to it.

There is deliberately no argv[0]: Program is a NAME, resolved by the core.

func ResolveBackend

func ResolveBackend(src BackendSource, prober HostProber, spec BackendSpec, sessionDir string) (BackendPlan, bool, error)

ResolveBackend performs the three checks the CORE owes on an adapter's plan, and returns the plan with Program resolved to an absolute path. ok==false means the adapter declares no backend (and err is nil).

It lives in the contract package for exactly the reason Detect(a, HostProber) does: the checks are pure functions of the plan plus a capability interface, so they are testable and reusable without this package gaining an fd.

  • 9a: Program resolves through HostProber.LookPath, and a Program containing a path separator is refused OUTRIGHT before any lookup.
  • 9c: no element of Args/AgentArgs (after stripping an optional `unix://`) names an ABSOLUTE path outside the session dir. This is the one check a malicious or merely buggy adapter cannot talk its way past, because the core performs it on data it does not trust.

9b -- exec'd DIRECTLY, never through a shell -- belongs to whoever builds the exec.Cmd (internal/shim), which is where it is fenced.

type BackendSource

type BackendSource interface {
	// Backend describes the side process for one session, or reports ok==false.
	//
	// ok == false IS THE ORDINARY CASE and never a defect: most CLIs need no backend, and
	// an adapter that answers false is complete and fully supported (ADR-010 §5's posture).
	//
	// PURE and TOTAL on Interactions' terms: deterministic, never panics on an empty or
	// pathological SocketPath, and it performs no I/O.
	Backend(spec BackendSpec) (BackendPlan, bool)
}

BackendSource is the OPTIONAL extension a CLI whose structured plane lives in a side process implements. Discovered by TYPE ASSERTION like every other extension here; the frozen Adapter method set gains nothing.

func AsBackendSource

func AsBackendSource(a Adapter) (BackendSource, bool)

AsBackendSource reports whether a describes a session backend.

type BackendSpec

type BackendSpec struct {
	// SocketPath is the per-session backend endpoint, inside the session state dir.
	SocketPath string
}

BackendSpec is what the CORE supplies to an adapter that describes a backend. The socket path is the core's, because the core owns the session dir.

type CapabilityEntry

type CapabilityEntry struct {
	CLI            string   `json:"cli"`
	Version        string   `json:"version"`
	Hooks          bool     `json:"hooks"`
	Resume         bool     `json:"resume"`
	ConversationID bool     `json:"conversation_id"`
	Options        int      `json:"options"`
	Signals        []string `json:"signals"` // sorted, de-duped SignalSource kinds
}

CapabilityEntry is one row of the capability matrix.

func Capability

func Capability(a Adapter, fx Fixture, grid *vt.Snap) CapabilityEntry

Capability derives a's capability entry, cross-checked against fx. CLI and Version are copied from the fixture; Hooks/Resume/Options/Signals come from the adapter's declarations; ConversationID is proven by extracting an id from the fixture's REAL rendered grid + raw capture — grid is the *vt.Snap the harness builds from fx.PTYCapture (not nil), so extraction is exercised exactly as the engine drives it at runtime.

type ContextGuardAction added in v0.13.17

type ContextGuardAction struct {
	Method            string
	ThreadIDParameter string
	AutomaticDispatch bool
	Support           ContextGuardSupport
}

ContextGuardAction is a pure native-action template. AutomaticDispatch is the adapter's version-fenced assertion that the daemon MAY dispatch the method automatically; the daemon still gates every dispatch behind its enforced serialization, quiet revalidation, and the unattended rule (ADR-023 D5/D6).

type ContextGuardContinuer added in v0.13.18

type ContextGuardContinuer interface {
	// ContextGuardContinuation returns the request that starts text as the
	// continuation turn on thread. Pure, total, no I/O, and version-fenced
	// exactly like the automatic action: ok=false means nothing may be sent.
	ContextGuardContinuation(version, threadID, text string) (method string, params map[string]any, ok bool)
}

ContextGuardContinuer is a second optional extension (ADR-023 amendment 2): after a compaction the guard itself dispatched has verifiably completed, the daemon resumes the interrupted task by starting one ordinary turn. The adapter only shapes that request; the daemon worker owns when (and whether) to send it -- at the composer lane's head, fully revalidated, at-most-once per compaction cycle, and never for a compaction the daemon did not itself dispatch. The provider's native queue was investigated and rejected for this: a queued message is unrevokable while the compaction runs, so a human arriving in that window would still receive a surprise turn (docs/design/context-guard-continuation.md).

func AsContextGuardContinuer added in v0.13.18

func AsContextGuardContinuer(a Adapter) (ContextGuardContinuer, bool)

AsContextGuardContinuer discovers the optional extension without widening Adapter.

type ContextGuardEventKind added in v0.13.17

type ContextGuardEventKind string

ContextGuardEventKind is the normalized meaning of one provider notification.

const (
	ContextGuardUsage               ContextGuardEventKind = "usage"
	ContextGuardCompactionStarted   ContextGuardEventKind = "compaction_started"
	ContextGuardCompactionCompleted ContextGuardEventKind = "compaction_completed"
)

type ContextGuardNotification added in v0.13.17

type ContextGuardNotification struct {
	Kind         ContextGuardEventKind
	ThreadID     string
	UsedTokens   uint64
	ContextLimit uint64
	Quality      ContextGuardQuality
	Deprecated   bool
}

ContextGuardNotification is pure provider evidence. Usage fields are set only for ContextGuardUsage; Deprecated records a characterized legacy lifecycle frame so a daemon can observe it without treating it as a new contract.

type ContextGuardQuality added in v0.13.17

type ContextGuardQuality string

ContextGuardQuality says how exact provider usage evidence is. Only Exact may be action-eligible; parser absence is the fail-closed result for every other provider shape.

const (
	ContextGuardExact ContextGuardQuality = "exact"
)

type ContextGuardSource added in v0.13.17

type ContextGuardSource interface {
	ParseContextGuardNotification(raw []byte, expectedThreadID string) (ContextGuardNotification, bool)
	ContextGuardAction(version string) (ContextGuardAction, bool)
}

ContextGuardSource is optionally implemented by an Adapter. It is pure, total, deterministic, and performs no transport I/O. expectedThreadID binds an event to the daemon's current provider identity.

func AsContextGuardSource added in v0.13.17

func AsContextGuardSource(a Adapter) (ContextGuardSource, bool)

AsContextGuardSource discovers the optional extension without widening Adapter.

type ContextGuardSupport added in v0.13.17

type ContextGuardSupport string

ContextGuardSupport is the action rollout state expressed by a provider.

const (
	ContextGuardObserveOnly ContextGuardSupport = "observe_only"
	// ContextGuardAutomatic authorizes the daemon's automatic dispatch of the
	// native action, UNDER THE DAEMON'S OWN SERIALIZATION. The 2026-09-01 live
	// gates (ADR-023 amendment 1) proved the provider itself serializes
	// nothing -- a compact sent mid-turn cancels the turn, and two concurrent
	// compacts interrupt each other -- so this value asserts only that the
	// action's method and telemetry are characterized for the version; every
	// concurrency guarantee is the dispatch lane's to enforce.
	ContextGuardAutomatic ContextGuardSupport = "automatic"
)

type ConversationIDValidator

type ConversationIDValidator interface {
	IsValidConversationID(string) bool
}

ConversationIDValidator is an optional adapter extension for providers whose native conversation IDs have a defensible syntax. It lets generic resume paths reject corrupt persisted metadata without adding provider switches or widening the frozen Adapter interface. Implementations are pure, total and deterministic.

func AsConversationIDValidator

func AsConversationIDValidator(a Adapter) (ConversationIDValidator, bool)

AsConversationIDValidator reports whether a implements the optional native-ID validation extension.

type ConversationIdentitySource

type ConversationIdentitySource interface {
	ConversationIDFromEvent(HookPayload) (string, bool)
}

ConversationIdentitySource extracts a provider-owned conversation id from one authenticated structured event. Implementations are pure, total and deterministic; ok implies a canonical non-empty id.

func AsConversationIdentitySource

func AsConversationIdentitySource(a Adapter) (ConversationIdentitySource, bool)

AsConversationIdentitySource reports whether a implements the optional authenticated-event identity extension.

type DatedTranscriptLayout added in v0.13.21

type DatedTranscriptLayout interface {
	// TranscriptDay returns the UTC calendar day the CLI filed convID's transcript
	// under, when the id itself carries that day, and false when it does not. It does
	// NOT validate convID beyond what reading the day needs -- callers owe
	// IsCanonicalConversationID before it and an os.Root anchor after it.
	TranscriptDay(convID string) (time.Time, bool)
}

DatedTranscriptLayout is the OPTIONAL extension a CLI implements when it files transcripts by DAY rather than by working directory, so no path can be composed from a cwd and an id alone: the file sits in a day directory named after when the conversation began and carries a launch timestamp of its own in its name. The adapter names the DAY -- its knowledge is that its ids carry one -- and the core's anchored, budgeted resolver lists that day and matches the id. Same terms as TranscriptLayout: pure, total, no I/O, discovered by type assertion, and absence is the signal (ADR-010 Amendment 5 F1).

func AsDatedTranscriptLayout added in v0.13.21

func AsDatedTranscriptLayout(a Adapter) (DatedTranscriptLayout, bool)

AsDatedTranscriptLayout reports whether a files its transcripts by day.

type DecisionAction

type DecisionAction struct {
	Reply json.RawMessage
}

DecisionAction is the core-executed effect: the body core writes back on the pending hook or JSON-RPC channel. The prompt-card path carries NO DecisionAction — spike S-C's carve-out is exactly the path on which Decision is never called, so its decision-to-keystroke map is produced at capture and held MACHINE-SIDE. It is never a field on the item and never reaches the phone (interaction-schema.md IS-APR-3 and IS-LIFE-6; ADR-009 (4)).

type DecisionChoice

type DecisionChoice struct {
	ID    string
	Label string
	// Verdict is this decision's grant/refuse polarity: allow | deny | other.
	// REQUIRED on an approval_request (conformance obligation, owner ruling
	// 2026-08-07); Validate additionally rejects a value outside the three.
	//
	// It is the daemon's only source for §3.6's allowed/denied split. Nothing
	// downstream can derive it: the ids are the CLI's own by §3.5's design, and a
	// daemon classifying `cancel` as a refusal would be guessing at a vocabulary
	// it does not own — the posture IS-TOOL-2 forbids for the same reason. The
	// adapter knows it at capture, which is where Mode is decided too.
	//
	// ponytail: MACHINE-SIDE, like Keystrokes — it is never copied onto the item
	// and never reaches the phone. The card labels its buttons from
	// decisions[].label (IS-APR-3) and no phone surface switches on polarity, so a
	// wire field would be a second place for the two to disagree. A phone that
	// later needs it (styling a destructive button) is an additive §3.5 field and
	// a schema change, not an unused field shipped ahead of its consumer.
	Verdict string
}

DecisionChoice is one decision the CLI offers for a pending approval (§3.5). The ids are the CLI's own vocabulary (spike-SB captured Codex's accept / acceptWithExecpolicyAmendment / cancel), never a normalized set.

type Detection

type Detection struct {
	Found    bool   // the CLI binary was located
	Path     string // absolute path to the binary, when Found
	Version  string // detected version string, when Found
	InRange  bool   // Version falls within SupportedVersions
	ProbeErr string // first line of the version probe's output when it errored/exited non-zero (the CLI's own diagnostic, e.g. codex's "Reinstall Codex"); empty on success

	// ConfiguredModel and Models are ADDITIVE DATA ONLY — the frozen contract
	// gains fields, no behavior. The CORE Detect above never sets them (it stays
	// I/O-free); they are filled by the host-I/O model probe in
	// internal/adapter/detect (reading each CLI's on-disk config) and consumed by
	// the launch-form model-option overlay in cmd/swarm.
	ConfiguredModel string        // the model the CLI is actually configured to default to (best-effort; empty when unknown), pre-filling the launch form's model field
	Models          []ModelChoice // the model choices the CLI exposes, discovered from its config (best-effort; nil when the CLI has no enumeration or none could be read)
}

Detection is the result of probing the host for the CLI.

func Detect

func Detect(a Adapter, h HostProber) Detection

Detect probes the host for a's CLI through h and reports what it found. It is the CORE-owned detection routine that replaces the old impure Detect method: the adapter contributes only pure descriptors (Binary, VersionArgs, ParseVersion, SupportedVersions) while h performs every LookPath/exec. A binary that is present but whose version cannot be run or parsed is reported Found with an empty Version (and InRange false).

type Fixture

type Fixture struct {
	SchemaVersion int           `json:"schema_version"`
	CLI           string        `json:"cli"`
	Version       string        `json:"version"`
	Scenario      string        `json:"scenario"`
	PTYCapture    []byte        `json:"pty_capture"` // base64 on the wire
	HookPayloads  []HookPayload `json:"hook_payloads,omitempty"`
}

Fixture is one recorded characterization scenario.

func (Fixture) Validate

func (f Fixture) Validate() error

Validate reports whether the fixture is well-formed: an exact schema-version match, non-empty identity fields, a non-empty PTY capture, and — for every hook — a non-empty event and a syntactically valid JSON raw body. A CLI with no hooks (empty HookPayloads) is legitimate.

type HookPayload

type HookPayload struct {
	Event        string          `json:"event"`
	Raw          json.RawMessage `json:"raw"`
	ReceivedAtMs int64           `json:"received_at_ms"`
}

HookPayload is one hook callback the CLI emitted during characterization.

type HostProber

type HostProber interface {
	// LookPath resolves name to an absolute program path, or returns an error if
	// it is not found on PATH.
	LookPath(name string) (string, error)
	// Run executes path with args and returns its combined version stdout.
	Run(path string, args []string) (stdout string, err error)
}

HostProber is the host-I/O capability the CORE Detect function needs: locate a binary on PATH and run it to capture its stdout. Its implementations own the fds and exec and therefore live OUTSIDE this pure contract package (see internal/adapter/detect for the real exec-based Host). The contract names only the interface, so internal/adapter itself opens nothing.

type Interaction

type Interaction struct {
	// Kind is one of the eight kinds above. Required.
	Kind string
	// Status is one of the four statuses above, or empty when the kind carries
	// none (interaction-schema.md §2: "absent means not applicable to the kind").
	Status string

	// Ref is the CLI's OWN id for this interaction, in the CLI's own vocabulary.
	// It is NOT the item_id and never reaches the phone: the daemon mints the
	// ULID item_id (IS-APR-1 leaves exactly one id on the wire) and maps Ref to
	// it. It serves two machine-side jobs, which is why there is one field and
	// not two:
	//
	//   - it is the ref Decision(ref, decisionID) is later called with, for an
	//     approval_request (ADR-010 §4);
	//   - it is the correlation key that lets the daemon fold successive records
	//     of ONE item under one item_id — the agent_message increments of
	//     IS-DELTA-1 and the tool_run open+close IS-DELTA-3 collapses. The
	//     adapter is the only party that sees the CLI's own id, so nobody else
	//     can supply it.
	//
	// Empty for a self-contained one-record item.
	Ref string

	// ClientRef is the id THE CLIENT supplied when it originated this interaction, echoed
	// back by the CLI verbatim -- Codex's `userMessage.clientId`, which the daemon set as
	// `clientUserMessageId` on turn/start or turn/steer.
	//
	// It exists because it is the ONLY exact composer-echo correlation any provider offers.
	// The alternative -- matching the echoed prompt by TEXT -- has a probed mis-attribution
	// on record (internal/skeleton/chat.go's pendingSendTTL doc: an OWNER-typed "yes" was
	// stamped source=phone because a phone send of "yes" was still pending). An adapter
	// whose CLI carries no such id leaves this empty, and the daemon falls back to what it
	// already does.
	//
	// Like Ref, it is MACHINE-SIDE: the daemon consumes it and it never reaches the wire.
	ClientRef string

	// TurnRef is the CLI's OWN id for the TURN this interaction belongs to -- Codex's
	// `params.turnId`, a UUIDv7 the app-server mints.
	//
	// THE TURN RULE ITSELF IS STILL THE DAEMON'S. IS-ENV-1 says a turn opens on a
	// user_message and closes on a terminal agent_message, `turn_id` on the wire is the
	// daemon's own ULID, and none of that changes: this field sources NO boundary and
	// decides NO grouping. It is the third machine-side correlation key beside Ref and
	// ClientRef, and it exists for one reason -- a provider whose turn operations take the
	// CLI's own turn id as a PRECONDITION cannot be driven with an id the CLI never minted.
	//
	// PROBED (Wave R7 review BLOCKING 1, and confirmed against the real 0.147.0 server):
	// `turn/steer` documents expectedTurnId as "Required active turn id precondition. The
	// request fails when it does not match the currently active turn", and R7 round 1 sent
	// the daemon's 26-char ULID against the server's UUIDv7 -- so EVERY mid-turn phone send
	// was rejected, and `turn/interrupt` was worse, because the server's honest
	// `no active turn to interrupt` for an id it had never seen was swallowed as benign and
	// a Stop that stopped nothing reported success.
	//
	// Like Ref and ClientRef it is MACHINE-SIDE and never reaches the wire. An adapter whose
	// CLI has no turn identity leaves it empty, and the daemon's turn state is unaffected.
	TurnRef string

	// user_message (§3.1) / agent_message (§3.2). Text on an agent_message is the
	// INCREMENT this record appends, never the accumulated body (IS-DELTA-1).
	Text       string
	Source     string // user_message only: phone | owner | derived
	StopReason string // agent_message terminal record only: end_turn | interrupted | error

	// tool_run (§3.3). ExitCode is meaningful only for an execute action; the
	// daemon omits it otherwise, so a zero here is not "exited 0" for a read.
	Tool             string
	Action           ToolAction // §7; also the approval_request summary action
	OutputExcerpt    string
	TruncationMarker string // the CLI's own truncation text, VERBATIM (IS-TOOL-3)
	ExitCode         int

	// file_change (§3.4). IS-FC-1: only an APPLIED change is a file_change; a
	// proposed edit is an approval_request.
	Path        string
	Change      string // create | modify | delete | rename
	OldPath     string // rename only
	DiffExcerpt string // unified diff text; the producer normalizes (spike-SB: Claude's Edit body is old_string/new_string)
	Added       int
	Removed     int

	// approval_request (§3.5). content_hash, expires_at and the agent_instance
	// binding tuple are daemon-authoritative and deliberately absent here.
	Summary   string
	Decisions []DecisionChoice // the CLI's OWN decision vocabulary, not a normalized one
	// Mode declares the apply mechanism AT CAPTURE, because that is where the
	// spike-SC carve-out is decidable — the adapter can see the tool and its
	// input then, so the daemon knows before the phone renders whether the
	// request resolves natively (ADR-010 §4).
	Mode        string
	PromptLines []string // prompt_card only: the sanitized prompt region, as text

	// Keystrokes is the decision->keystroke map, keyed by DecisionChoice.ID.
	// Present IFF Mode == prompt_card, because Decision is never called on that
	// path, so the map must be produced at capture (ADR-010 §2/§4).
	//
	// ponytail: this is MACHINE-SIDE data with a deliberate ceiling — the daemon
	// holds it and SHALL NOT copy it onto the item. IS-APR-3 forbids the item
	// carrying it and IS-LIFE-6 forbids the phone authoring the keystroke, so a
	// wire field for it would only invite the implementation those rules exist to
	// prevent. It rides Interaction because Interaction is the only adapter->core
	// carrier there is; DecisionAction deliberately has no Keys field.
	Keystrokes map[string]string

	// plan_update (§3.7). IS-PLAN-1: latest-state, not incremental.
	Revision int
	Steps    []PlanStep
}

Interaction is the pure, normalized content ONE adapter shaped out of ONE captured event body. It is the adapter's whole output: the daemon is the sole producer of what goes on the wire (ADR-010 §3).

Fields are grouped by the kind that uses them, mirroring interaction-schema.md §3.1-§3.7. A field belonging to another kind is left zero.

ponytail: there are no fields for approval_resolved (§3.6) or session_status (§3.8) because no adapter sources them — IS-LIFE-2's resolver covers five paths of which four are daemon-observed, IS-ST-2's sweep fires on instance death, and IS-SS-1 makes session_status the status.* projection the roster already derives. The KIND constants cover all eight (they are the wire vocabulary the daemon reuses) and Validate accepts all eight, so a later agent-sourced cancel is an additive field, not a contract change.

func (Interaction) Validate

func (in Interaction) Validate() error

Validate reports the first structural violation in one shaped item: an unknown kind, status or enum value, or a broken card/prompt-card pairing. It is PURE and checks SHAPE ONLY — interaction-schema.md §5's size caps, §2's ids and timestamps, and §3.5's hash/expiry are daemon-side and deliberately unchecked here (ADR-010 §3).

type InteractionSource

type InteractionSource interface {
	// Interactions maps ONE captured event body to zero or more items. PURE and
	// TOTAL, on the same terms as ExtractConversationID: never panics on a nil,
	// truncated, garbage, or unbounded body; deterministic; and it never returns
	// content it did not observe in p.Raw.
	Interactions(p HookPayload) []Interaction

	// Decision describes HOW to apply one offered choice to the pending approval
	// named by ref, as a descriptor the CORE executes — the adapter performs no I/O
	// (E9.2), exactly as Command/Resume return an argv core runs. ok == false means
	// this CLI has no native mechanism here and the daemon must use the prompt card.
	//
	// decisionID is a DecisionChoice.ID — the CLI's OWN vocabulary (§3.5), which is
	// what the adapter itself offered. It is NOT DecisionChoice.Verdict: the
	// normalized allow|deny|other bit is what the DAEMON classifies §3.6's
	// allowed/denied from, and an adapter switching on it here would be answering a
	// choice it never offered.
	Decision(ref, decisionID string) (DecisionAction, bool)
}

InteractionSource is the OPTIONAL extension a CLI-native adapter implements. It is discovered by TYPE ASSERTION (AsInteractionSource), never by a method on Adapter: the frozen method set gains nothing (ADR-010 Non-goals).

func AsInteractionSource

func AsInteractionSource(a Adapter) (InteractionSource, bool)

AsInteractionSource reports whether a implements the optional capture extension. ok == false is the GENERIC-FALLBACK SIGNAL (ADR-010 §5): the adapter is complete and fully supported, and the daemon derives items from the sanitized snapshot instead. Native capture is an upgrade, never a precondition.

type KeystrokeComposer

type KeystrokeComposer interface {
	// ComposerKeys is the byte sequence that enters text into the CLI's own composer,
	// written to the session's PTY verbatim. The daemon supplies the submit framing.
	ComposerKeys(text string) []byte
}

KeystrokeComposer is the OPTIONAL seam an adapter implements when a remote message may be delivered by TYPING IT INTO THE CLI'S OWN COMPOSER -- the mechanism Wave R6 shipped for Claude Code, where the CLI's prompt is a terminal input region and nothing else exists.

It is spelled as a seam for exactly one reason: ABSENCE IS THE REFUSAL. Before R7 the daemon wrote text plus a CR into the PTY for EVERY provider with no seam and no provider check anywhere on the path, so a phone send to a Codex session typed into the Codex TUI -- the thing playbook §8.2 forbids in as many words. With the seam, a CLI that must never be typed at simply implements nothing, and the daemon refuses rather than inventing a keystroke (ADR-010 §5, IS-TOOL-2's never-guess posture one layer down).

func AsKeystrokeComposer

func AsKeystrokeComposer(a Adapter) (KeystrokeComposer, bool)

AsKeystrokeComposer reports whether a proves a keystroke composer seam.

type LaunchGateAnswerer added in v0.13.22

type LaunchGateAnswerer interface {
	// LaunchGateKeys returns the keystrokes that ACCEPT the gate currently on snap, to be
	// written to the session's PTY verbatim as one complete answer (select and confirm).
	//
	// ok is false for any grid the adapter cannot positively identify as that gate, with
	// its options on screen and the selection state readable. A refusal types nothing; a
	// key returned for the wrong screen is typed into whatever has focus.
	LaunchGateKeys(snap *vt.Snap) (keys string, ok bool)
}

LaunchGateAnswerer is implemented by an adapter whose CLI can put a recorded startup gate on the glass, and knows the keys that answer it.

func AsLaunchGateAnswerer added in v0.13.22

func AsLaunchGateAnswerer(a Adapter) (LaunchGateAnswerer, bool)

AsLaunchGateAnswerer reports whether a records a startup gate answer.

type LaunchSpec

type LaunchSpec struct {
	Cwd           string
	Name          string
	Options       map[string]string
	InitialPrompt string
}

LaunchSpec is the input to Command: the working directory, optional native session name, resolved option values, and optional initial prompt.

type LiveName

type LiveName struct {
	Name  string
	Since time.Time
}

LiveName is one fact a CLI publishes about a running session: the name it shows the user right now, and when that name was set. Since is the newest-wins clock of ADR-022: the assembly adopts the name only if Since is later than the last name swarm itself stamped on the session.

type LiveNameSource

type LiveNameSource interface {
	// LiveNameDir is the registry directory, relative to the daemon user's home.
	LiveNameDir() string
	// LiveNameFromFile parses one registry file. ok reports that the file names
	// conversationID's session AND carries a usable name and timestamp. Total and
	// deterministic on any input.
	LiveNameFromFile(raw []byte, conversationID string) (LiveName, bool)
}

LiveNameSource is the optional extension for a CLI that keeps a per-process registry of its running sessions under the user's home (Claude Code: ~/.claude/sessions/<pid>.json). The adapter stays pure: it names the directory and parses one file; the assembly lists the directory, reads the files and applies the result. Absence is supported: a CLI without such a registry simply never has its name adopted.

func AsLiveNameSource

func AsLiveNameSource(a Adapter) (LiveNameSource, bool)

AsLiveNameSource reports whether a publishes live session names.

type ModelChoice

type ModelChoice struct {
	ID      string
	Display string
}

ModelChoice is one model a CLI exposes: ID is the launch value (the slug the CLI's -m/--model accepts), Display its human-readable label. It is pure data carried on Detection for the launch form's model picker — the frozen adapter contract gains no behavior.

type OptionSpec

type OptionSpec struct {
	Key      string   // unique option key
	Label    string   // human-readable label
	Type     string   // "string" | "bool" | "choice"
	Default  string   // default value; for a choice it must be "" or one of Choices
	Choices  []string // permitted values when Type == "choice"
	Suggest  []string // optional curated values an editable string option cycles (left/right); empty = free text only
	Required bool     // the form must collect a value
}

OptionSpec declares one launch option the form renders and validates.

type PlanStep

type PlanStep struct {
	Text  string
	State string // pending | in_progress | completed | cancelled
}

PlanStep is one step of a plan_update (§3.7).

type ResumeSpec

type ResumeSpec struct {
	Cwd            string
	ConversationID string
	Name           string
	Options        map[string]string
}

ResumeSpec is the input to Resume: the working directory, the conversation to resume, an optional native session name, and the resolved option values.

type SessionNameRequest

type SessionNameRequest struct {
	Method string
	Params map[string]string
}

SessionNameRequest is a provider-native structured request that makes the provider's session label match Swarm's. The assembly executes it over the already-owned backend connection; adapters remain pure and own no I/O.

type SessionNameSync

type SessionNameSync interface {
	SetSessionName(conversationID, name string) (SessionNameRequest, bool)
	SessionNameFromEvent(HookPayload) (conversationID, name string, ok bool)
}

SessionNameSync is the optional naming extension for providers with a structured runtime naming plane. Absence is supported: launch-only providers can still consume LaunchSpec.Name without claiming live bidirectional sync.

func AsSessionNameSync

func AsSessionNameSync(a Adapter) (SessionNameSync, bool)

AsSessionNameSync reports whether an adapter supports live structured naming.

type SignalSource

type SignalSource struct {
	Kind       string
	Descriptor map[string]string
}

SignalSource declares one way the CLI's idle/active state is observed. Kind is one of "hook", "event", or "heuristic"; Descriptor carries kind-specific metadata (e.g. the hook event name or the grid heuristic).

type ToolAction

type ToolAction struct {
	Type    string // read | edit | write | search | execute | fetch | other
	Path    string // read/edit/write
	Query   string // search
	Command string // execute
}

ToolAction is interaction-schema.md §7's structured summary — what makes a card read "Read src/main.rs". IS-TOOL-1 puts its production machine-side, in the per-CLI adapter; IS-TOOL-2 says an unclassifiable call is "other", never guessed at.

type TranscriptLayout

type TranscriptLayout interface {
	// ProjectDirName encodes a working directory into the single directory name
	// the CLI files that cwd's transcripts under (e.g. "/a/b" -> "-a-b"). It
	// returns a BARE NAME, never a path: the core joins it beneath the provider
	// root it opened, and joining is the core's act.
	ProjectDirName(cwd string) string

	// TranscriptFileName names the transcript file for one conversation id
	// within that directory. It does NOT validate convID -- callers owe
	// IsCanonicalConversationID before this and an os.Root anchor after it.
	TranscriptFileName(convID string) string
}

TranscriptLayout is the OPTIONAL extension a CLI implements when swarm has CHARACTERIZED where that CLI keeps its conversation transcripts on disk. It is discovered by TYPE ASSERTION (AsTranscriptLayout), never by a method on Adapter: the frozen method set gains nothing (ADR-010 Non-goals).

IT NAMES; IT NEVER OPENS. Both halves are PURE and TOTAL on the same terms as Command/Resume: deterministic, no panic on any input, and NO filesystem access whatsoever -- no Stat, no Open, no read. Every byte of I/O stays in the core's existing anchored, budgeted resolver (an os.Root traversal under the DAEMON's home -- not the session's; the resolver is built once per daemon and its home is immutable -- with the history budgets and alias caps), because that resolver is the only party allowed to touch the disk (ADR-001 / E9.2). An adapter that opened a file here would move traversal safety inside the boundary that exists precisely to keep it out.

WHY TWO METHODS RATHER THAN ONE TranscriptPath(convID). The two callers disagree about what they know. The hands-off composer holds a conversation id and wants the file. The resume-history MIGRATION resolver holds NO id -- it is searching for one -- and needs the DIRECTORY so it can read the candidate ids out of it. A single path-composing method cannot serve the second caller at all, so the seam splits exactly where their knowledge does.

ABSENCE IS THE SIGNAL (ADR-010 section 5), and here it is load-bearing. An adapter whose layout nobody has characterized implements NOTHING, the assertion fails, and the caller refuses that provider BY NAME. A stub returning "" would instead look like an answer and send an anchored open at a directory named "" -- a silent wrong path in place of a named refusal.

func AsTranscriptLayout

func AsTranscriptLayout(a Adapter) (TranscriptLayout, bool)

AsTranscriptLayout reports whether a has a characterized transcript layout.

type TurnInterrupter

type TurnInterrupter interface {
	// InterruptKeys is the CLI's OWN cancel sequence, written to the session's PTY
	// verbatim. It must be non-empty: a declared interrupt that types nothing is a Stop
	// button that does nothing.
	InterruptKeys() []byte
}

TurnInterrupter is the OPTIONAL interface an adapter implements when its CLI's current turn can be interrupted by a RECORDED key sequence (Wave R6, Mirror M2.4's semantic Stop; ADR-017 T2/T6). It is pure data out -- the CORE does the writing, exactly as Command/ Resume return an argv core runs -- and it is discovered by type assertion like every other extension here.

ABSENCE IS A SIGNAL (ADR-010 §5): an adapter that implements nothing here is complete and fully supported; the daemon refuses turn_interrupt with interrupt_unsupported rather than guessing a keystroke into a CLI whose cancel key nobody recorded (IS-TOOL-2's never-guess posture one layer down), and the ADR-017 capability record derives its `interrupt` field from this same seam, so the phone's Stop affordance and the op it rides agree by construction.

func AsTurnInterrupter

func AsTurnInterrupter(a Adapter) (TurnInterrupter, bool)

AsTurnInterrupter reports whether a proves a semantic interrupt seam.

type VersionConstraint

type VersionConstraint struct {
	Min string
	Max string
}

VersionConstraint is an inclusive [Min, Max] version range.

Directories

Path Synopsis
Package agy is the Antigravity CLI (agy) adapter (v1.1 CLI duo, issue agents-tracker-5gv, plan .claude/tmp/cli-duo-implementation-plan.md Phase D): a stateless, goroutine-safe strategy object over the `agy` CLI.
Package agy is the Antigravity CLI (agy) adapter (v1.1 CLI duo, issue agents-tracker-5gv, plan .claude/tmp/cli-duo-implementation-plan.md Phase D): a stateless, goroutine-safe strategy object over the `agy` CLI.
Package claude is the Claude Code adapter (Epic 11, E11.2/E11.6/E11.8): a stateless, goroutine-safe strategy object over the `claude` CLI.
Package claude is the Claude Code adapter (Epic 11, E11.2/E11.6/E11.8): a stateless, goroutine-safe strategy object over the `claude` CLI.
Package codex is the Codex adapter (Epic 11, E11.4/E11.6/E11.8): a stateless, goroutine-safe strategy object over the `codex` CLI.
Package codex is the Codex adapter (Epic 11, E11.4/E11.6/E11.8): a stateless, goroutine-safe strategy object over the `codex` CLI.
Package detect is the host-I/O side of adapter detection (E9.1 / E9.2).
Package detect is the host-I/O side of adapter detection (E9.1 / E9.2).
Package fixtureio is the DISK side of the fixture corpus (E9.2 / E9.4).
Package fixtureio is the DISK side of the fixture corpus (E9.2 / E9.4).
Package hermes implements Swarm's pure adapter strategy for the NousResearch Hermes Agent classic CLI.
Package hermes implements Swarm's pure adapter strategy for the NousResearch Hermes Agent classic CLI.
Package opencode is the opencode adapter (Phase E, R-E1..R-E8): a stateless, goroutine-safe strategy object over the `opencode` CLI.
Package opencode is the opencode adapter (Phase E, R-E1..R-E8): a stateless, goroutine-safe strategy object over the `opencode` CLI.
Package refadapter is the fixture-only REFERENCE ADAPTER (E9.5 / T-5).
Package refadapter is the fixture-only REFERENCE ADAPTER (E9.5 / T-5).
Package registry is the Epic 11 adapter registry: the single table mapping a stable agent name ("agy" / "claude" / "codex" / "hermes" / "opencode" / "reference") to the constructor that builds that adapter.Adapter.
Package registry is the Epic 11 adapter registry: the single table mapping a stable agent name ("agy" / "claude" / "codex" / "hermes" / "opencode" / "reference") to the constructor that builds that adapter.Adapter.

Jump to

Keyboard shortcuts

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