Documentation
¶
Overview ¶
Package adapters defines backend-neutral agent events and the adapters that derive them from normalized terminal text.
Index ¶
- Constants
- Variables
- func CheckVersion(adapterID string, installedVersion string) (unverified bool, reason string)
- func GetVerifiedVersions(adapterID string) []string
- func IsSensitiveText(value string) bool
- func IsVendorAdapter(adapterID string) bool
- func ParseVersion(output, product string) string
- func ValidateLine(line string) error
- func VendorExecutable(adapterID string) string
- type Adapter
- type AiderAdapter
- type ClaudeAdapter
- type CodexAdapter
- type Decision
- type Descriptor
- type DetectionState
- type Event
- type EventType
- type Factory
- type GenericRegexAdapter
- type GooseAdapter
- type Hooks
- type OpenInterpreterAdapter
- type Pattern
- type Processor
- func (p *Processor) Acknowledge(eventID string) error
- func (p *Processor) AnsiOutput() string
- func (p *Processor) Consume(chunk []byte) error
- func (p *Processor) DetectionWindowLen() int
- func (p *Processor) IsBlocked() bool
- func (p *Processor) MarkProcessExitEvent(exitCode *int, failed bool) Event
- func (p *Processor) NewProcessExitEvent(exitCode *int, failed bool) Event
- func (p *Processor) Output() string
- func (p *Processor) Pending() *Event
- func (p *Processor) ReconcileSnapshot(raw []byte) (*Event, bool, error)
- func (p *Processor) Resize(columns, rows int)
- func (p *Processor) Resolve(eventID string, deliver func() error) error
- func (p *Processor) Restore(event Event) error
- func (p *Processor) Revision() uint64
- func (p *Processor) Run(ctx context.Context, reader io.Reader) (err error)
- func (p *Processor) SendLine(ctx context.Context, line string, deliver func([]byte) error) error
- func (p *Processor) WaitSemanticEvents()
- type Registry
- type RiskLevel
- type Status
- type ToolCall
- type ToolCallParam
Constants ¶
const ( // MetadataCommandKind is the metadata key under which an adapter says what // kind of text its Event.Command holds. Without it the field is only a // fragment of the question line (the generic adapter's quoted word, which may // be a file name or the word "yes"), good enough for a policy to match and not // something to put in front of a person as the command the agent asks to run. // The journal admits no metadata key of this name. MetadataCommandKind = "command_kind" // CommandKindShell marks a prompt that asks to run one shell command, whose // Event.Command is that command as the agent's screen showed it. It is set by // a rule written for such a prompt, never by a configured pattern. CommandKindShell = "shell" )
const (
// AiderID identifies the Aider AI pair programming adapter.
AiderID = "aider"
)
const ( // ClaudeID identifies the experimental Claude Code adapter. Its vendor // rules are limited to the layouts listed in docs/adapters.md; the Claude // Code 2.1.286 ones rest on test strings, not on stored fixtures. ClaudeID = "claude" )
const ( // CodexID is experimental and intentionally limited to interactions backed // by the anonymized captures in testdata/codex. CodexID = "codex" )
const GenericID = "generic"
const (
// GooseID identifies the Goose AI agent adapter.
GooseID = "goose"
)
const MaxLineBytes = 4096
MaxLineBytes is the maximum encoded line accepted by the safe line-input boundary, before its terminating carriage return is appended.
const (
// OpenInterpreterID identifies the Open Interpreter AI agent adapter.
OpenInterpreterID = "interpreter"
)
Variables ¶
var ( ErrUnknownAdapter = errors.New("unknown adapter") ErrDecisionUnsupported = errors.New("unsupported decision") ErrEventMismatch = errors.New("pending event mismatch") ErrProcessorTerminated = errors.New("adapter semantic stream terminated") )
var ( // ErrEventPending reports that an actionable event must be resolved before // direct instructions can be delivered. ErrEventPending = errors.New("actionable event pending") // ErrInvalidLine reports line input which cannot be encoded unambiguously. ErrInvalidLine = errors.New("invalid terminal line") // ErrLineUnsupported reports that a transport has no atomic line boundary. ErrLineUnsupported = errors.New("line send not supported") // ErrLineDeliveryUncertain reports that a transport was called but could // not prove whether zero, some, or all line bytes reached the target. The // original transport error is deliberately not retained: it may echo the // submitted text and must never escape through errors.Unwrap. ErrLineDeliveryUncertain = errors.New("line delivery uncertain") )
var VerifiedVersions = map[string][]string{
AiderID: {"0.86.2"},
ClaudeID: {"2.1.286", "2.1.285", "2.1.59"},
GooseID: {"1.52.0"},
OpenInterpreterID: {"0.4.3"},
CodexID: {"0.148.0", "0.148.0-alpha.21"},
}
VerifiedVersions defines the list of tool versions empirically verified for each vendor adapter. Running another version might lead to unhandled prompts or changed interaction patterns.
Functions ¶
func CheckVersion ¶ added in v0.8.18
CheckVersion evaluates whether the installed version is verified for the given adapter. Returns (unverified, reason). For generic/custom adapters, unverified is always false.
func GetVerifiedVersions ¶ added in v0.8.18
GetVerifiedVersions returns the list of verified versions for the given adapter.
func IsSensitiveText ¶
func IsVendorAdapter ¶ added in v0.8.18
IsVendorAdapter returns true if the adapterID belongs to a vendor-specific adapter that has an empirically captured prompt baseline.
func ParseVersion ¶ added in v0.8.18
ParseVersion extracts a semver-like version from the one line of output that names the product, and from no other line. --version output is not only the version: a launcher prints its own (npx node v22…), a shell rc greets, a wrapper banners. The first number in such output is not the agent's version, and a banner that became "the agent's version" was shown to every operator — and every viewer — as the installed version. An empty product anchors nothing: output with no product line yields no version.
func ValidateLine ¶ added in v0.8.9
ValidateLine reports ErrInvalidLine for text that is not one line of application text as the line boundary takes it: valid UTF-8, no control character at all, CR and LF included, and no more than MaxLineBytes. The supervision core holds a person's typed answer to the same rule: it reaches the agent as typed, followed by the adapter's own terminator, and the rule is what keeps it one answer rather than a stream of keystrokes.
func VendorExecutable ¶ added in v0.8.18
VendorExecutable names the one executable a vendor adapter's own distribution installs. It is the only binary the version probe may run: anything else — a shell wrapper (sh -c, cmd /c), a launcher (npx, node, python, docker), a differently named script — would report its own version as the agent's, or replay the configured command line.
Types ¶
type Adapter ¶
type Adapter interface {
ID() string
Detect(state *DetectionState, chunk []byte) ([]Event, error)
EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
}
Adapter interprets normalized, ANSI-free terminal text. Implementations do not own processes, PTYs, tmux sessions, rendered history or audit sinks.
type AiderAdapter ¶
type AiderAdapter struct {
// contains filtered or unexported fields
}
AiderAdapter recognizes prompts generated by the Aider coding assistant CLI. Configured intercept_patterns retain configured priority and serve as fallback.
func NewAiderAdapter ¶
func NewAiderAdapter(patterns []Pattern) (*AiderAdapter, error)
NewAiderAdapter validates and copies intercept_patterns used by generic fallback.
func (*AiderAdapter) Detect ¶
func (a *AiderAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect gives configured intercept_patterns priority. Aider prompts are matched only when no configured pattern matches.
func (*AiderAdapter) EncodeDecision ¶
func (a *AiderAdapter) EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
EncodeDecision encodes automated or manual responses for Aider prompts. Aider reads a line: y and Enter ran the captured command, created the file, applied the edit or added the output, and n and Enter did none of it (testdata/aider).
func (*AiderAdapter) ID ¶
func (*AiderAdapter) ID() string
type ClaudeAdapter ¶
type ClaudeAdapter struct {
// contains filtered or unexported fields
}
ClaudeAdapter recognizes only these layouts: the 2.1.59 workspace-trust and environment-key prompts, the 2.1.285 Bash, create-file and edit-file cases (provenance unconfirmed), and the Bash and create-file layouts of 2.1.286, which rest on test strings. Configured intercept_patterns retain their configured order and take priority, preserving the semantics of existing configurations.
The adapter remains experimental: no automatic allow or deny byte sequence is claimed because the highlighted TUI selection can change independently of the prompt text visible to Relayer.
func NewClaudeAdapter ¶
func NewClaudeAdapter(patterns []Pattern) (*ClaudeAdapter, error)
NewClaudeAdapter validates both the observed rules and every configured intercept_pattern before a session starts.
func (*ClaudeAdapter) Detect ¶
func (a *ClaudeAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect delegates bounded stream handling and legacy regex behavior to the generic adapter, then enriches only rules tied to real Claude Code fixtures.
func (*ClaudeAdapter) EncodeDecision ¶
func (a *ClaudeAdapter) EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
EncodeDecision preserves exact manual input compatibility. Automatic allow and deny remain unsupported for every Claude Code prompt, the Bash, create and edit prompts included: no answer to them has been typed into a real Claude Code with its effect checked, and an Enter takes whichever option the menu has highlighted, which the prompt text Relayer reads does not show. The unsupported decisions make a policy decision on these prompts a question for a person instead of a byte written into the agent.
func (*ClaudeAdapter) ID ¶
func (*ClaudeAdapter) ID() string
type CodexAdapter ¶
type CodexAdapter struct {
// contains filtered or unexported fields
}
CodexAdapter recognizes only prompts captured from the CLI version recorded in testdata/codex. Configured intercept_patterns remain a complete fallback; this wrapper does not silently replace or weaken them.
func NewCodexAdapter ¶
func NewCodexAdapter(patterns []Pattern) (*CodexAdapter, error)
NewCodexAdapter validates and copies the legacy intercept_patterns used by the generic fallback.
func (*CodexAdapter) Detect ¶
func (a *CodexAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect gives configured intercept_patterns priority so enabling the Codex adapter cannot change an existing policy's event semantics. Verified Codex prompts are considered only when no configured pattern matches. Probes are value copies, so a chunk is committed to the bounded DetectionState exactly once.
func (*CodexAdapter) EncodeDecision ¶
func (a *CodexAdapter) EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
EncodeDecision supports only byte sequences verified against the captured CLI. Generic fallback events retain the established manual-input behavior.
func (*CodexAdapter) ID ¶
func (*CodexAdapter) ID() string
type Decision ¶
type Decision string
Decision is deliberately limited to actions represented by current code. Adapters may reject allow or deny when they cannot encode the action reliably; callers must then retain the pending event for human input.
type Descriptor ¶
Descriptor is safe to display in diagnostics and documentation.
type DetectionState ¶
type DetectionState struct {
SessionID string
AgentID string
AdapterID string
// contains filtered or unexported fields
}
DetectionState is the bounded, per-session state supplied to an Adapter. A state must never be shared by two terminal sessions.
func NewDetectionState ¶
func NewDetectionState(sessionID, agentID, adapterID string) *DetectionState
NewDetectionState creates independent state for one agent session.
func (*DetectionState) IsBlocked ¶
func (s *DetectionState) IsBlocked() bool
IsBlocked reports whether an actionable event still awaits a decision.
func (*DetectionState) Pending ¶
func (s *DetectionState) Pending() *Event
Pending returns a defensive copy of the current actionable event.
func (*DetectionState) UseRenderedScreen ¶
func (s *DetectionState) UseRenderedScreen(text string, burstStart int, inCodeFence bool, anchors screen.Anchors)
UseRenderedScreen hands the state the screen text for this chunk, replacing the accumulated byte window. Only the Processor calls this, and only for an agent that has repainted.
type Event ¶
type Event struct {
ID string
Signature string
Sequence uint64
SessionID string
AgentID string
Adapter string
Type EventType
Summary string
Match string
Command string
Sensitive bool
Risk RiskLevel
Timestamp time.Time
Metadata map[string]string
// ToolCall is the MCP tool invocation this occurrence is asking about, when
// one could be read out of the prompt block. It is nil far more often than
// not: most prompts are not tool calls, and an agent may run a tool without
// printing anything recognisable.
//
// Its parameter values are agent-controlled terminal text. They exist so an
// operator can see what a tool is about to be given before answering, and
// they must never reach the audit journal, which has no field for content.
// Only the tool's identity is journalled.
ToolCall *ToolCall
// contains filtered or unexported fields
}
Event is the single semantic representation shared by adapters, backends, snapshots and the TUI. ID identifies one occurrence; Signature is stable for equivalent normalized content and is used only while reconciling duplicates.
func NewProcessExitEvent ¶
func NewProcessExitEvent(sessionID, agentID, adapterID string, sequence uint64, exitCode *int, failed bool) Event
NewProcessExitEvent creates the sole lifecycle event currently represented in the semantic stream. Metadata contains only a numeric exit code.
func (Event) Actionable ¶
Actionable reports whether Relayer must pause for a human decision.
func (Event) IsShellCommandPrompt ¶ added in v0.8.19
IsShellCommandPrompt reports whether the occurrence is a prompt that asks to run one shell command, as its adapter's own rule read it. Such a prompt is about its command, not about an MCP tool call, and its command is the one text a front end may show as such.
type EventType ¶
type EventType string
EventType describes an observation with a concrete meaning in Relayer. Output invalidations and backend failures intentionally remain transport signals: they are not agent events.
type GenericRegexAdapter ¶
type GenericRegexAdapter struct {
// contains filtered or unexported fields
}
GenericRegexAdapter is the stable compatibility adapter for existing intercept_patterns. It receives only normalized text from Processor.
func NewGenericRegexAdapter ¶
func NewGenericRegexAdapter(patterns []Pattern) (*GenericRegexAdapter, error)
NewGenericRegexAdapter validates and defensively copies all expressions.
func (*GenericRegexAdapter) Detect ¶
func (a *GenericRegexAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect preserves pattern order and reports at most one actionable event while another occurrence is pending. A match may span chunks, but it must reach the active terminal line affected by the newest normalized chunk.
func (*GenericRegexAdapter) EncodeDecision ¶
func (*GenericRegexAdapter) ID ¶
func (*GenericRegexAdapter) ID() string
type GooseAdapter ¶ added in v0.3.0
type GooseAdapter struct {
// contains filtered or unexported fields
}
GooseAdapter recognizes prompts generated by the Goose CLI agent. Configured intercept_patterns retain configured priority and serve as fallback.
func NewGooseAdapter ¶ added in v0.3.0
func NewGooseAdapter(patterns []Pattern) (*GooseAdapter, error)
NewGooseAdapter validates and copies intercept_patterns used by generic fallback.
func (*GooseAdapter) Detect ¶ added in v0.3.0
func (a *GooseAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect gives configured intercept_patterns priority. Goose prompts are matched only when no configured pattern matches.
func (*GooseAdapter) EncodeDecision ¶ added in v0.3.0
func (a *GooseAdapter) EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
EncodeDecision picks Allow or Deny on Goose's menu, whatever option is highlighted when the keys arrive. Manual input names an option: "allow", "deny" or "cancel"; Always Allow, which changes Goose's permissions for every later call, is left to the terminal.
func (*GooseAdapter) ID ¶ added in v0.3.0
func (*GooseAdapter) ID() string
type Hooks ¶
type Hooks struct {
OnOutput func()
OnEvent func(Event)
// OnEventWithdrawn reports an occurrence the agent has taken back off its
// screen before anyone decided on it. It is not a decision and it is not a
// failure: the question simply stopped being asked, and whatever is showing
// it to the operator has to stop showing it. A caller that ignores this
// leaves a card an operator can still click, and the click is refused with
// ErrEventMismatch rather than delivered.
OnEventWithdrawn func(Event)
// OnRawChunk reports the escape-sequence-aligned transport bytes, before
// this package throws the escapes away. It exists so a session transcript
// can be recorded verbatim; nothing in detection depends on it.
//
// It is called after the state lock is released, with a slice this package
// no longer references. The callee therefore owns the bytes, and must not
// block: the caller is the PTY read loop.
OnRawChunk func(at time.Time, data []byte)
}
Hooks are invoked synchronously after Processor releases its state lock. OnEvent may inspect Processor state; process termination remains the responsibility of the owning backend rather than an event callback.
type OpenInterpreterAdapter ¶ added in v0.3.0
type OpenInterpreterAdapter struct {
// contains filtered or unexported fields
}
OpenInterpreterAdapter recognizes prompts generated by the Open Interpreter CLI. Configured intercept_patterns retain configured priority and serve as fallback.
func NewOpenInterpreterAdapter ¶ added in v0.3.0
func NewOpenInterpreterAdapter(patterns []Pattern) (*OpenInterpreterAdapter, error)
NewOpenInterpreterAdapter validates and copies intercept_patterns used by generic fallback.
func (*OpenInterpreterAdapter) Detect ¶ added in v0.3.0
func (a *OpenInterpreterAdapter) Detect(state *DetectionState, chunk []byte) ([]Event, error)
Detect gives configured intercept_patterns priority. Open Interpreter prompts are matched only when no configured pattern matches.
func (*OpenInterpreterAdapter) EncodeDecision ¶ added in v0.3.0
func (a *OpenInterpreterAdapter) EncodeDecision(event Event, decision Decision, manualInput string) ([]byte, error)
EncodeDecision encodes automated or manual responses for Open Interpreter prompts. Open Interpreter prompts expect Enter ('\r') to confirm input.
func (*OpenInterpreterAdapter) ID ¶ added in v0.3.0
func (*OpenInterpreterAdapter) ID() string
type Pattern ¶
Pattern is the backward-compatible representation of intercept_patterns.
func DefaultPatterns ¶
func DefaultPatterns() []Pattern
type Processor ¶
type Processor struct {
// contains filtered or unexported fields
}
Processor separates raw transport bytes, normalized detection text and the bounded sanitized text rendered by Bubble Tea. Raw bytes are never retained.
func NewProcessor ¶
func (*Processor) Acknowledge ¶
func (*Processor) AnsiOutput ¶
AnsiOutput returns the bounded terminal output retaining ANSI escape sequences (colors, cursor motions, progress bar carriage returns) for terminal emulators such as xterm.js. If no ANSI output is buffered, it falls back to Output().
func (*Processor) Consume ¶
Consume accepts raw terminal bytes, strips fragmented ANSI sequences and sends only normalized text to the adapter.
func (*Processor) DetectionWindowLen ¶
func (*Processor) MarkProcessExitEvent ¶
MarkProcessExitEvent atomically terminates any pending interaction and reserves the next occurrence sequence under the same lock used by Detect and SendLine. It deliberately does not wait for earlier semantic hooks: a process owner can mark the transport closed immediately after Wait, perform bounded descendant cleanup, then call WaitSemanticEvents before publishing the returned process_exit event.
func (*Processor) NewProcessExitEvent ¶
NewProcessExitEvent marks the processor terminated, then preserves the historical ordering guarantee that every earlier semantic hook completes before the process_exit event is returned to its caller.
func (*Processor) ReconcileSnapshot ¶
ReconcileSnapshot uses the last active logical line for generic detection. A vendor adapter may additionally fingerprint only its verified prompt block so a resize remains idempotent while a successive prompt discovered after an attach receives a fresh occurrence ID. An unchanged snapshot after a successful acknowledgement cannot resurrect the old event.
func (*Processor) Resize ¶
Output returns what the operator should see.
For an agent that repaints, that is the rendered screen: the appended buffer holds every redraw stacked on top of the last, which is not what is on the agent's terminal. For every other agent the appended buffer is exact and is returned unchanged, so nothing that worked before changes. Resize tells the rendered screen the size of the terminal the agent is attached to. Until it is called the screen uses a default, which is wrong for wrapping but never wrong about what was erased.
The size crosses as plain integers rather than a terminal.Size: internal terminal imports this package, so the dependency cannot run the other way.
func (*Processor) Resolve ¶
Resolve serializes a decision with detection. The pending occurrence is cleared only after delivery succeeds; terminal output arriving concurrently is processed afterwards, so an immediate second prompt cannot be overwritten by rollback of the first one.
func (*Processor) Revision ¶
Revision returns the latest semantic occurrence sequence for snapshots.
func (*Processor) Run ¶
Run reads the agent's output until the reader ends, the context is cancelled or processing fails. A panic while processing one chunk, from the adapter reading text the agent chose, is returned as the error that ends this session's reader, like any other processing failure. Nothing in the Go runtime would otherwise stop it before it ended the whole process, and with it the supervision of every other agent.
func (*Processor) SendLine ¶
SendLine serializes direct instructions with event detection and process termination. It never resolves or acknowledges an event. Delivery happens under the same lock as Detect, so either the line is sent first or a prompt becomes pending first; the two outcomes cannot race past one another.
func (*Processor) WaitSemanticEvents ¶
func (p *Processor) WaitSemanticEvents()
WaitSemanticEvents waits until every actionable event reserved before process termination has reached its hook. It must stay outside p.mu because hooks are allowed to inspect Processor state.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry resolves explicit adapters and executable-name hints. Generic is always installed as the final fallback. Claude and Codex remain experimental and recognize only the interactions listed in docs/adapters.md; for Claude the 2.1.286 layouts rest on test strings, not on fixtures.
func NewRegistry ¶
NewRegistry builds the production registry around legacy regex patterns.
func (*Registry) Descriptors ¶
func (r *Registry) Descriptors() []Descriptor
Descriptors returns a deterministic, defensive maturity inventory.
type RiskLevel ¶
type RiskLevel string
RiskLevel is descriptive audit metadata. It does not apply a policy.
func ToolCallRisk ¶ added in v0.8.0
ToolCallRisk classifies a tool by name only.
The server participates in the high-risk check because a server named "payments" or "deploy" raises the stakes of everything it exposes. The low-risk check reads the tool alone, so a read-looking tool on such a server stays high rather than talking itself down.
type Status ¶
type Status string
Status exposes the maturity of a registered adapter without claiming that an experimental placeholder is implemented.
type ToolCall ¶ added in v0.8.0
type ToolCall struct {
Server string
Tool string
Params []ToolCallParam
// Risk is derived from the tool name alone, never from its arguments.
Risk RiskLevel
// ParamsTruncated reports that parameters were dropped past the cap.
ParamsTruncated bool
}
ToolCall is one MCP tool invocation read out of terminal text.
func DetectToolCall ¶ added in v0.8.0
DetectToolCall finds at most one MCP tool call in a block of normalized terminal text. It returns false when the block carries none.
Several names in one block resolve to the most dangerous of them, and among equals to the last one, which is the call closest to the prompt the operator is being asked about. Resolving to the last name alone handed the agent a downgrade, because a name printed inside an unquoted argument sits AFTER the name of the call being made: "mcp__fs__delete_file path=mcp__docs__get_page" was read as a page fetch. A second name can now only raise the risk of a block, never lower it, which is what keeps an argument from talking a call down.
A name that only appears as documentation, inside a code fence or inside quotes, is not a call.
type ToolCallParam ¶ added in v0.8.0
ToolCallParam is one named argument of an MCP tool call. Value is bounded and sanitised: it is agent-controlled terminal text.