Documentation
¶
Overview ¶
Package attachadapter bridges a *agent.Agent onto the attach-mode HTTP/SSE surface (pkg/attach). It is phase 4 of the pkg/agent decomposition (docs/agent-package-split-design.md): the 22 Attach* capability methods and the WithAttach* provider options that used to live on the core Agent now live here, so the frozen agent surface stays narrow and hosts that never serve attach-mode never carry the wiring.
Usage:
a := agent.New(llm, ...) // core options only
ad := attachadapter.New(a,
attachadapter.WithMemoryProvider(f), // formerly agent.WithAttachMemoryProvider
attachadapter.WithPromptBroker(b),
)
reg.Register(ad) // ad satisfies attach.Registrant
The adapter satisfies attach.Registrant plus every optional attach capability interface (ToolsProvider, UsageProvider, PermsController, ...); registering it with a *attach.SessionRegistry makes the agent reachable over HTTP/SSE via attach.NewServer.
Index ¶
- Variables
- func DescribePlanOwner(p tools.PlanInfo) string
- func ReplanHandler(gate *permissions.Gate, agentsDir string, owner func() tools.PlanOwner) func(context.Context, attach.ReplanRequest) (attach.ReplanResponse, error)
- type Adapter
- func (ad *Adapter) Agent() *agent.Agent
- func (ad *Adapter) AppName() string
- func (ad *Adapter) AttachAddAllow(patterns []string) error
- func (ad *Adapter) AttachAddDeny(patterns []string) error
- func (ad *Adapter) AttachAgents() []attach.AgentInfo
- func (ad *Adapter) AttachAskSideQuestion(ctx context.Context, question string) (string, error)
- func (ad *Adapter) AttachCapabilities() attach.CapabilityReport
- func (ad *Adapter) AttachCheckpoint(ctx context.Context, note string) (attach.CheckpointResponse, error)
- func (ad *Adapter) AttachCompact(ctx context.Context, focus string) (attach.CompactResponse, error)
- func (ad *Adapter) AttachContext() attach.ContextInfo
- func (ad *Adapter) AttachGuardrails() attach.GuardrailInfo
- func (ad *Adapter) AttachInterrupt() bool
- func (ad *Adapter) AttachMCP() attach.MCPInfo
- func (ad *Adapter) AttachMemory() []attach.MemorySource
- func (ad *Adapter) AttachPerms() attach.PermsInfo
- func (ad *Adapter) AttachPricing() attach.PricingInfo
- func (ad *Adapter) AttachPromptBroker() *attach.PromptBroker
- func (ad *Adapter) AttachRefreshPricing(ctx context.Context) (attach.PricingRefreshResponse, error)
- func (ad *Adapter) AttachReload(ctx context.Context) attach.ReloadResponse
- func (ad *Adapter) AttachReplan(ctx context.Context, req attach.ReplanRequest) (attach.ReplanResponse, error)
- func (ad *Adapter) AttachResetGuardrail(req attach.GuardrailResetRequest) (attach.GuardrailResetResponse, error)
- func (ad *Adapter) AttachSetManualPricing(req attach.PricingSetRequest) error
- func (ad *Adapter) AttachSkills() []attach.SkillInfo
- func (ad *Adapter) AttachSpawnSubagent(ctx context.Context, spec attach.SubagentSpec) (attach.SubagentSpawnResponse, error)
- func (ad *Adapter) AttachStatus() attach.StatusInfo
- func (ad *Adapter) AttachSubagentCatalog() []attach.SubagentCatalogInfo
- func (ad *Adapter) AttachTools() []attach.ToolInfo
- func (ad *Adapter) AttachUsage() attach.UsageInfo
- func (ad *Adapter) Description() string
- func (ad *Adapter) EventLog() *eventlog.Handle
- func (ad *Adapter) Inject(message string) error
- func (ad *Adapter) InjectAs(message string, caller auth.Caller) error
- func (ad *Adapter) MarkInterruptPending()
- func (ad *Adapter) RequestWake()
- func (ad *Adapter) SessionID() string
- func (ad *Adapter) SetAttachEmitter(f func(eventType string, payload any))deprecated
- func (ad *Adapter) SetOperatorEventEmitter(f func(eventType string, payload any))
- func (ad *Adapter) UserID() string
- type Option
- func WithMCPProvider(fn func() attach.MCPInfo) Option
- func WithMemoryProvider(fn func() []attach.MemorySource) Option
- func WithPricingProvider(fn func() attach.PricingInfo) Option
- func WithPricingSetter(fn func(req attach.PricingSetRequest) error) Option
- func WithPromptBroker(b *attach.PromptBroker) Option
- func WithRefreshPricer(fn func(ctx context.Context) (attach.PricingRefreshResponse, error)) Option
- func WithReloader(fn func(ctx context.Context) attach.ReloadResponse) Option
- func WithReplanner(...) Option
- func WithSkillsProvider(fn func() []attach.SkillInfo) Option
Constants ¶
This section is empty.
Variables ¶
ErrSubagentSpawnerUnavailable is returned by AttachSpawnSubagent when the agent wasn't constructed with agent.WithBackgroundManager. The attach handler maps this to HTTP 501 so the operator sees "subagent spawn not registered" instead of a 500.
The message string is load-bearing: pkg/attach matches it literally (it can't import this package's sentinel without knowing about the agent side), so change it only in lockstep with isSubagentSpawnerUnavailable in pkg/attach/handlers_slash.go. The prefix says "attachadapter" — the sentinel's home since the #443 split (the stale "agent:" prefix from its old home was fixed under #492 item 5).
Functions ¶
func DescribePlanOwner ¶ added in v2.9.0
DescribePlanOwner renders a plan artifact's frontmatter attribution for the /replan message that reports a plan it declined to archive (#747). It states what the file says rather than inferring: "another agent" would be a guess, and the likeliest way to reach this branch is the same agent in an earlier session after a daemon restart.
func ReplanHandler ¶ added in v2.9.0
func ReplanHandler(gate *permissions.Gate, agentsDir string, owner func() tools.PlanOwner) func(context.Context, attach.ReplanRequest) (attach.ReplanResponse, error)
ReplanHandler builds the closure WithReplanner wants: archive the caller's own active plan artifact, clear the gate's planRecorded flag, and describe what actually happened in terms the operator can act on.
It lives here rather than in a host because there are two hosts. The single-session CLI wired this inline; the multi-session hub left /replan returning 501, which meant a recipe running plan_mode "required" under the hub could arm the plan-first gate and never revoke it from the session that owned the plan (#763). Duplicating the closure would have duplicated the parts #747 proved are load-bearing: the owner scoping and the three-way message.
gate is required — clearing the flag is half the contract, and a handler that silently skipped it would report a revocation that didn't happen; a nil one degrades to an honest error rather than panicking a tenant's request. agentsDir may be empty; the closure then reports that plan artifacts have nowhere to live instead of pretending success.
owner is a func, not a value, because both hosts learn the agent's identity after the adapter options are built (the CLI late-binds agentRef via WithPostConstruct). A nil owner, or one returning the zero PlanOwner, degrades to newest-wins — the pre-#747 behavior, which is the right fallback for a /replan that beat construction.
Types ¶
type Adapter ¶
type Adapter struct {
// contains filtered or unexported fields
}
Adapter wraps a *agent.Agent with the attach-facing capability surface. Construct with New; the zero value is not useful.
The capability methods (Attach*) are safe on a nil receiver — they degrade to the same "capability not registered" responses an unwired closure produces, matching the nil-safety convention of the agent package. The plain Registrant forwards (AppName, Inject, ...) assume a real wrapped agent, same as registering a bare *agent.Agent did before the split.
func New ¶
New wraps a with the attach capability surface.
Contract (one rule, not two): the CAPABILITY methods (Attach*) are nil-safe — on a nil adapter or nil wrapped agent they degrade to the same "capability not registered" / zero-value responses an unwired closure produces. The plain attach.Registrant forwards are NOT nil-safe: they require a real wrapped agent and misbehave otherwise (the identity accessors — AppName, SessionID, EventLog — panic; Inject/InjectAs error; RequestWake no-ops), exactly as registering a bare *agent.Agent did before the #443 split. Passing a nil agent is therefore only useful for constructing a capability-only value in tests — never register one (attach.SessionRegistry would reject its empty identity anyway).
func (*Adapter) Agent ¶
Agent returns the wrapped *agent.Agent. Hosts that thread the adapter through construction seams (session factories, TUI deps) use this to recover the agent for Run/Inject/etc without carrying both values.
func (*Adapter) AttachAddAllow ¶
AttachAddAllow implements attach.PermsController. Delegates to permissions.Gate.AddAllowPatterns. Returns nil if no gate was wired (no-op rather than error — operators shouldn't see an error for an absent gate). Surfaces validation errors from the gate so the operator sees malformed-pattern feedback.
func (*Adapter) AttachAddDeny ¶
AttachAddDeny implements attach.PermsController. Delegates to permissions.Gate.AddDenyPatterns.
func (*Adapter) AttachAgents ¶
AttachAgents implements attach.AgentsProvider. Returns the live background subagents from the agent's SubagentManager, or an empty slice when no manager was wired.
func (*Adapter) AttachAskSideQuestion ¶
AttachAskSideQuestion implements attach.SideQueryProvider. Wraps agent.AskSideQuestion (the /btw side-channel that doesn't persist to the event log).
func (*Adapter) AttachCapabilities ¶
func (ad *Adapter) AttachCapabilities() attach.CapabilityReport
AttachCapabilities implements attach.CapabilityReporter (#490). The adapter satisfies every optional capability interface unconditionally (see the conformance block below), so interface presence stopped signaling wiredness the moment the adapter became the universal registration path — every session advertised mcp/perms_stream/specialists and all five slash commands, and remote UIs rendered dead affordances backed by empty payloads or 501s. This report states what is actually wired:
- perms_stream ⇔ a prompt broker was supplied (WithPromptBroker);
- mcp ⇔ an MCP snapshot fn was supplied (WithMCPProvider);
- specialists / "subagent" ⇔ the agent carries a background manager (agent.WithBackgroundManager);
- interrupt, guardrails, "btw" ⇔ a live agent is wrapped (all three are core agent capabilities — Interrupt, the guardrail read/reset pair and AskSideQuestion need no extra wiring);
- cost_ceiling ⇔ a per-turn or per-session spend cap is armed (#666 — a live-state read, not interface presence);
- "compact" ⇔ agent.HasCompactor(); "done" ⇔ HasCheckpointer();
- "replan" ⇔ a replanner fn was supplied (WithReplanner).
func (*Adapter) AttachCheckpoint ¶
func (ad *Adapter) AttachCheckpoint(ctx context.Context, note string) (attach.CheckpointResponse, error)
AttachCheckpoint implements attach.CheckpointSlashProvider. Wraps agent.Checkpoint.
func (*Adapter) AttachCompact ¶
AttachCompact implements attach.CompactSlashProvider. Wraps agent.Compact and projects the result into the JSON wire format. Errors propagate; the attach handler turns them into 500s.
func (*Adapter) AttachContext ¶
func (ad *Adapter) AttachContext() attach.ContextInfo
AttachContext implements attach.ContextProvider. Projects the agent's ContextStats (compaction / checkpoint / subtask shape) into the attach wire format. Same cost as ContextStats (one session.Service.Get() + O(events) scan) — operator-driven, infrequent.
func (*Adapter) AttachGuardrails ¶ added in v2.9.0
func (ad *Adapter) AttachGuardrails() attach.GuardrailInfo
AttachGuardrails implements attach.GuardrailProvider. Projects the agent's watchdog + cost-ceiling state into the wire shape. Nil-safe: a capability-only adapter reports every backstop off and nothing tripped, which is the truthful answer for an agent with no guardrails.
func (*Adapter) AttachInterrupt ¶
AttachInterrupt implements attach.InterruptProvider so the attach-mode POST /sessions/<sid>/interrupt handler can dispatch cancel intents from a remote operator. Forwards to agent.Interrupt.
func (*Adapter) AttachMemory ¶
func (ad *Adapter) AttachMemory() []attach.MemorySource
AttachMemory implements attach.MemoryProvider. Returns nil when no provider was wired — the handler emits 200 with an empty `{"sources": []}`.
func (*Adapter) AttachPerms ¶
AttachPerms implements attach.PermsProvider. Returns the gate's current Snapshot (mode + allow + deny pattern lists) projected into the attach wire format, plus the per-session approval log so the remote TUI's /permissions slash can render what was approved this session. Returns zero PermsInfo if no gate was wired via agent.WithGate.
func (*Adapter) AttachPricing ¶
func (ad *Adapter) AttachPricing() attach.PricingInfo
AttachPricing implements attach.PricingProvider.
func (*Adapter) AttachPromptBroker ¶
func (ad *Adapter) AttachPromptBroker() *attach.PromptBroker
AttachPromptBroker implements attach.PromptBrokerProvider.
func (*Adapter) AttachRefreshPricing ¶
AttachRefreshPricing implements attach.PricingController. Returns attach.ErrCapabilityNotRegistered when no func was wired — the handler maps that to HTTP 501.
func (*Adapter) AttachReload ¶
func (ad *Adapter) AttachReload(ctx context.Context) attach.ReloadResponse
AttachReload implements attach.Reloader. Returns a response with Errors populated by ErrCapabilityNotRegistered when no func was wired so the handler emits the same 501 the other unwired controllers do.
func (*Adapter) AttachReplan ¶
func (ad *Adapter) AttachReplan(ctx context.Context, req attach.ReplanRequest) (attach.ReplanResponse, error)
AttachReplan implements attach.ReplanProvider. Routes to the closure wired by WithReplanner; returns ErrCapabilityNotRegistered when no func was wired.
func (*Adapter) AttachResetGuardrail ¶ added in v2.9.0
func (ad *Adapter) AttachResetGuardrail(req attach.GuardrailResetRequest) (attach.GuardrailResetResponse, error)
AttachResetGuardrail implements attach.GuardrailResetter.
Order of operations matters and is deliberate:
- Decide whether the reset can work BEFORE mutating anything. A per-session cost trip whose spend already exceeds ceiling + requested budget is refused with attach.ErrGuardrailRetrip and leaves the agent untouched — no half-applied budget, and no 200 that the next turn undoes.
- Raise the ceiling, then clear the flag. The reverse order leaves a window where a concurrent post-turn enforcement pass re-trips against the old ceiling.
The refusal in (1) is whole-request, not per-guardrail: an under-budget guardrail=all on a session that ALSO tripped the watchdog clears neither. Clearing just the watchdog would leave the session halted anyway (the ceiling still refuses the next turn), so a partial 200 would only obscure that the operator's request didn't work. Scope to guardrail=watchdog to clear one in isolation.
Budget is applied whenever cost_ceiling is in scope, tripped or not: raising the bar before a long run hits it is the same affordance as raising it after, and refusing the pre-emptive case would just teach operators to wait for the halt.
func (*Adapter) AttachSetManualPricing ¶
func (ad *Adapter) AttachSetManualPricing(req attach.PricingSetRequest) error
AttachSetManualPricing implements attach.PricingController.
func (*Adapter) AttachSkills ¶
AttachSkills implements attach.SkillsProvider.
func (*Adapter) AttachSpawnSubagent ¶
func (ad *Adapter) AttachSpawnSubagent(ctx context.Context, spec attach.SubagentSpec) (attach.SubagentSpawnResponse, error)
AttachSpawnSubagent implements attach.SubagentSpawner. Delegates to the agent's wired SubagentManager. Returns ErrSubagentSpawnerUnavailable when no manager is attached.
func (*Adapter) AttachStatus ¶
func (ad *Adapter) AttachStatus() attach.StatusInfo
AttachStatus implements attach.StatusProvider. V1 returns the agent's model name + a coarse "idle" state — finer-grained state (running / deferred / paused) would require run-loop instrumentation that hasn't been wired yet; the design doc captures pause/resume + state mutation as v3 work.
func (*Adapter) AttachSubagentCatalog ¶ added in v2.9.0
func (ad *Adapter) AttachSubagentCatalog() []attach.SubagentCatalogInfo
AttachSubagentCatalog implements attach.SubagentCatalogProvider (#627). Returns the CONFIGURED subagent roster — declarative templates + predefined catalog specs the manager was wired with — distinct from AttachAgents (live/spawned instances). Empty when no manager is wired (e.g. --no-background-agents): nothing is spawnable by reference, so the roster is empty; the sync subagent tools still appear in AttachTools with source="subagent".
func (*Adapter) AttachTools ¶
AttachTools implements attach.ToolsProvider. Returns the agent's full tool catalog as ToolInfo entries with source classification (builtin / subagent / other) and the gate's pre-flight state per tool when a gate was wired via agent.WithGate. MCP / skill attribution is "other" in v1 — distinguishing them at the slice level needs an upstream metadata pass we haven't done yet.
func (*Adapter) AttachUsage ¶
AttachUsage implements attach.UsageProvider. Returns the agent's usage tracker totals plus a per-model breakdown when more than one model has been used in this session (typical pattern: parent on a frontier model, subtasks on a cheap flash-tier model via --agentic-small-model), plus a per-turn array so operators can answer per-turn cost/cache questions without hand-scraping the eventlog (issue #222). Returns a zero UsageInfo if no usage tracker was wired (agent.WithUsageTracker).
cost_usd_uncached_reference is computed per-turn using the resolved pricing for that turn's model — sessions that mix models (parent + subtask on a flash-tier via --agentic-small-model) get accurate reference numbers instead of averaging one model's rates over the other. Rolled up into Overall / PerModel by summing per-turn contributions.
func (*Adapter) Description ¶
Description implements attach.DescriptionProvider — the /.well-known/agent-card.json handler falls back to this when no explicit AgentCardConfig.Description override is supplied.
func (*Adapter) MarkInterruptPending ¶
func (ad *Adapter) MarkInterruptPending()
MarkInterruptPending implements attach.InterruptSelfAuditor. The /interrupt handler calls it (instead of appending the audit row out-of-band) so the audit is written from the agent's own turn loop after the interrupted turn unwinds, dodging the OCC race that mislabeled operator cancels as stale-session errors (#565). Forwards to agent.MarkInterruptPending.
func (*Adapter) RequestWake ¶
func (ad *Adapter) RequestWake()
RequestWake implements attach.Registrant.
func (*Adapter) SetAttachEmitter
deprecated
func (*Adapter) SetOperatorEventEmitter ¶
SetOperatorEventEmitter implements attach.OperatorEventTarget. The attach broadcaster calls this on first SSE subscriber (wiring its Emit method) and again with nil when the last subscriber disconnects. Forwards to the agent, which owns the emit machinery — the core run loop is the thing that emits status/turn/usage events.
type Option ¶
type Option func(*Adapter)
Option configures an Adapter under construction.
func WithMCPProvider ¶
WithMCPProvider wires a snapshot func for /sessions/<sid>/mcp (backs /mcp). Formerly agent.WithAttachMCPProvider.
func WithMemoryProvider ¶
func WithMemoryProvider(fn func() []attach.MemorySource) Option
WithMemoryProvider wires a snapshot func that returns the agent's loaded instruction sources for the remote-attach /sessions/<sid>/memory endpoint (backs the remote TUI's /memory slash). The caller usually projects an `instruction.Loaded`'s Sources list into []attach.MemorySource; nil = endpoint returns empty. Formerly agent.WithAttachMemoryProvider.
func WithPricingProvider ¶
func WithPricingProvider(fn func() attach.PricingInfo) Option
WithPricingProvider wires a snapshot func for /sessions/<sid>/pricing (backs the remote TUI's /pricing read). Formerly agent.WithAttachPricingProvider.
func WithPricingSetter ¶
func WithPricingSetter(fn func(req attach.PricingSetRequest) error) Option
WithPricingSetter wires a func that runs on POST /sessions/<sid>/pricing/set — writes a manual per-model rate and rebuilds the catalog. Formerly agent.WithAttachPricingSetter.
func WithPromptBroker ¶
func WithPromptBroker(b *attach.PromptBroker) Option
WithPromptBroker wires the broker that bridges the agent's permissions.Gate prompts to remote operators over GET /sessions/<sid>/perms/stream and POST /perms/respond. The caller is also responsible for wiring this broker into the gate (typically via Gate.SetPrompter(broker)) so prompts the gate generates actually flow through it. Without this option the /perms/stream + /perms/respond routes return 501. Formerly agent.WithAttachPromptBroker.
func WithRefreshPricer ¶
WithRefreshPricer wires a func that runs on POST /sessions/<sid>/pricing/refresh — typically calls into `pkg/pricing.Refresh` and rebuilds the catalog. Returns the outcome the operator sees. Formerly agent.WithAttachRefreshPricer.
func WithReloader ¶
func WithReloader(fn func(ctx context.Context) attach.ReloadResponse) Option
WithReloader wires a func that runs on POST /sessions/<sid>/reload. The closure is expected to re-walk project deps (instruction sources, skills bundles, MCP config) and return per-surface success in the response so the operator sees which parts succeeded and which failed. The adapter doesn't inspect the response shape; what "reload" means is the host's concern. Without this option the operator sees 501 / capability not registered. Formerly agent.WithAttachReloader.
func WithReplanner ¶
func WithReplanner(fn func(ctx context.Context, req attach.ReplanRequest) (attach.ReplanResponse, error)) Option
WithReplanner wires a func that runs on POST /sessions/<sid>/slash/replan and on the in-process TUI's /replan slash dispatch. The closure is expected to clear the gate's planRecorded flag and archive the latest plan artifact (typically `tools.RevokeLatestPlan(gate, agentsDir)`). Without this option the slash returns 501 / "capability not registered".
Wiring it under `plan_mode: "advisory"` or `"off"` is harmless — there is no gate flag set, so the closure archives whatever artifact exists (or reports none) and blocks nothing. The CLI wires it unconditionally for that reason; the closure's own response text is what distinguishes the modes, since telling an operator "the next mutating call will be denied" when advisory mode will deny nothing is the same unenforced-claim bug the mode exists to avoid. Formerly agent.WithAttachReplanner.
func WithSkillsProvider ¶
WithSkillsProvider wires a snapshot func for /sessions/<sid>/skills (backs /skills). Formerly agent.WithAttachSkillsProvider.