Documentation
¶
Overview ¶
Package agentinstance spawns and owns running ACP agent instances on a server-rooted context, independent of any client connection, and lets multiple viewers attach to a session's event stream. Exactly one attached controller per session answers its permission and terminal requests; a watchDog restarts a dead downstream but the conversation context is lost.
Index ¶
- Constants
- Variables
- type Event
- type EventKind
- type EventSink
- type FleetEntry
- type InstanceStatus
- type Manager
- type Option
- func WithEventSink(sink EventSink) Option
- func WithJournalSize(n int) Option
- func WithKillGrace(d time.Duration) Option
- func WithPermissionFallback(fn PermissionFallback) Option
- func WithRestart(limit int) Option
- func WithSelfDBPath(path string) Option
- func WithSelfExecutable(path string) Option
- func WithStderr(w io.Writer) Option
- func WithWorkspaceID(id string) Option
- type PermissionFallback
- type SessionSpec
- type TerminalServer
- type UnattendedPermission
- type Viewer
Constants ¶
const ( StateStarting = "starting" StateRunning = "running" StateStopped = "stopped" StateError = "error" StateWarning = "warning" )
Instance lifecycle states, the vocabulary of InstanceStatus.State.
const ( ChainACPSubcommand = "acp" ChainPathEnvVar = "CONTENOX_ACP_CHAIN_PATH" ChainHopEnvVar = "CONTENOX_EVENT_HOP" // ChainDBEnvVar hands the child the database its parent is actually using. A // subagent's report, plan and verdict are writes against the mission row the // parent created, so a child that resolved its own default database would find // no such row and fail every mission tool with a not-found. It is an internal // handoff between two contenox processes, not a user-facing knob: an explicit // --db always wins over it. ChainDBEnvVar = "CONTENOX_ACP_DB" ChainWorkspaceFlag = "workspace-id" )
ChainACPSubcommand, ChainPathEnvVar, ChainHopEnvVar, ChainDBEnvVar, and ChainWorkspaceFlag describe this binary's own ACP server for a chain-kind spawn: its ACP subcommand, the env var naming the chain file, the env var carrying the dispatch hop, the env var naming the database the dispatching host opened, and the flag carrying the dispatching host's workspace id.
const AgentModeConfigOptionID = "contenox.agent-mode"
AgentModeConfigOptionID is the reserved SessionConfigOption id that surfaces the downstream agent's session Modes as a synthetic select option; setting it translates to session/set_mode.
const AgentModelConfigOptionID = "contenox.agent-model"
AgentModelConfigOptionID is the reserved SessionConfigOption id that surfaces the downstream agent's model-picker state as a synthetic select option; setting it translates to session/set_model.
Variables ¶
var ErrNotFound = errors.New("agentinstance: instance not found")
ErrNotFound is returned for an unknown instance id; a sentinel for errors.Is(err, ErrNotFound).
Functions ¶
This section is empty.
Types ¶
type Event ¶
type Event struct {
Kind EventKind `json:"kind"`
InstanceID string `json:"instanceId"`
AgentID string `json:"agentId"`
AgentName string `json:"agentName"`
State string `json:"state,omitempty"` // EventStateChange
SessionID libacp.SessionID `json:"sessionId,omitempty"` // EventAttach / EventDetach / EventUnsupervisedDeny
ViewerID string `json:"viewerId,omitempty"` // EventAttach / EventDetach
Controller bool `json:"controller,omitempty"` // EventAttach
Time time.Time `json:"time"`
}
Event is one instance-lifecycle event, self-contained so a sink can react without calling back into the Manager; subscribe via WithEventSink.
type EventKind ¶
type EventKind string
EventKind classifies a lifecycle Event.
const ( // EventStateChange fires on every instance state transition (Event.State // carries the new state). EventStateChange EventKind = "state_change" // EventAttach fires when a viewer attaches to a session (Event.Controller // reports whether it became the controller). EventAttach EventKind = "attach" // EventDetach fires when a viewer detaches from a session. EventDetach EventKind = "detach" // EventUnsupervisedDeny fires when a downstream permission request reaches a // session with no controller and is refused (built-in deny or an injected // PermissionFallback); it never fires for a permitted request. EventUnsupervisedDeny EventKind = "unsupervised_permission" )
type EventSink ¶
type EventSink func(Event)
EventSink receives every lifecycle Event synchronously on the producing goroutine; it must not block or call back into the Manager.
type FleetEntry ¶
type FleetEntry struct {
AgentID string `json:"agentId"`
AgentName string `json:"agentName"`
Kind string `json:"kind"`
Instances []InstanceStatus `json:"instances"`
}
FleetEntry joins one declared agent with its live instances (empty when declared but not running).
func (FleetEntry) Running ¶
func (e FleetEntry) Running() bool
Running reports whether this declared agent has at least one live instance.
type InstanceStatus ¶
type InstanceStatus struct {
ID string `json:"id"`
AgentID string `json:"agentId"`
AgentName string `json:"agentName"`
Kind string `json:"kind"`
State string `json:"state"`
// Sessions is how many downstream sessions are open on the instance, always
// len(SessionIDs).
Sessions int `json:"sessions"`
// Viewers is how many viewers are attached across those sessions, independent of
// Sessions.
Viewers int `json:"viewers"`
StartedAt time.Time `json:"startedAt"`
// SessionIDs lists every session currently open on the instance, sorted for a
// deterministic snapshot; an unwatched or silent session is still listed.
SessionIDs []string `json:"sessionIds"`
}
InstanceStatus is a point-in-time snapshot of one instance. It is a value copy: mutating it never affects the live instance.
type Manager ¶
type Manager interface {
// Start resolves agentName via the registry and brings up an instance bound to the
// Manager's root context (cwd is the sandbox workspace); prefer StartResolved if
// already resolved.
Start(ctx context.Context, agentName, cwd string) (instanceID string, err error)
// StartResolved spawns an instance from an already-resolved agent with no registry
// read of its own; cwd is the sandbox workspace root, overridden by the agent's own
// declared Cwd if set.
StartResolved(ctx context.Context, agent *runtimetypes.Agent, cwd string) (instanceID string, err error)
// Attach registers viewer against (instanceID, sessionID), replaying the journal
// then joining the live fan-out; the first viewer of a session becomes its controller.
Attach(ctx context.Context, instanceID string, sessionID libacp.SessionID, viewer Viewer) (controllerGranted bool, err error)
// Detach removes viewerID from (instanceID, sessionID)'s fan-out,
// promoting the earliest-attached survivor if it was the controller.
Detach(instanceID string, sessionID libacp.SessionID, viewerID string) error
// List returns every declared agent joined with its live instances
// (empty = not running).
List(ctx context.Context) ([]FleetEntry, error)
// Get returns the status of one instance.
Get(instanceID string) (InstanceStatus, error)
// OpenSession drives the downstream ACP handshake on instanceID
// (initialize once, then session/new) and returns the downstream
// session id that Attach and the other session methods use.
OpenSession(ctx context.Context, instanceID string, spec SessionSpec) (libacp.SessionID, error)
// Prompt drives one downstream session/prompt turn and returns its stop reason; a
// ctx cancellation or concurrent Cancel resolves as StopReasonCancelled with a nil
// error.
Prompt(ctx context.Context, instanceID string, sessionID libacp.SessionID, prompt []libacp.ContentBlock) (libacp.StopReason, error)
// DeliverToSession injects n into sessionID's fan-out on whichever live instance
// owns that session, as if it were a downstream update; ErrNotFound when no instance
// owns it.
DeliverToSession(ctx context.Context, sessionID libacp.SessionID, n libacp.SessionNotification) error
// Cancel cancels sessionID's in-flight prompt turn. Safe with no turn in flight.
Cancel(instanceID string, sessionID libacp.SessionID) error
// CloseSession ends sessionID and drops its kernel state without stopping the
// instance; only the consumer that called OpenSession should call this.
CloseSession(instanceID string, sessionID libacp.SessionID) error
// SetConfigOption forwards a config-option change downstream and adopts the
// confirmed value; the synthetic mode/model ids map to session/set_mode and
// session/set_model, every other id forwards to session/set_config_option.
SetConfigOption(ctx context.Context, instanceID string, sessionID libacp.SessionID, configID string, value libacp.SessionConfigOptionValue) error
// SessionConfigOptions returns sessionID's captured config-option
// surface (synthetic mode + model selects, then the downstream's own),
// or nil for an unknown session.
SessionConfigOptions(instanceID string, sessionID libacp.SessionID) ([]libacp.SessionConfigOption, error)
// AvailableCommands returns sessionID's captured slash-command menu, or nil for an unknown session.
AvailableCommands(instanceID string, sessionID libacp.SessionID) ([]libacp.AvailableCommand, error)
// Stop tears an instance down and removes it from the registry, preventing any
// watchDog restart. Idempotent.
Stop(instanceID string) error
// Close stops every instance and cancels the Manager's root context, after which
// Start returns an error. Idempotent.
Close() error
}
Manager owns the lifecycle of running agent instances; every method is safe for concurrent use and returns ErrNotFound for an unknown instanceID.
type Option ¶
type Option func(*manager)
Option configures a Manager.
func WithEventSink ¶
WithEventSink installs sink as the lifecycle event sink (see EventSink).
func WithJournalSize ¶
WithJournalSize overrides the per-session replay journal size (see defaultJournalSize).
func WithKillGrace ¶
WithKillGrace overrides how long an external instance's teardown waits for its subprocess to exit on stdin-close before killing it (see defaultKillGrace).
func WithPermissionFallback ¶
func WithPermissionFallback(fn PermissionFallback) Option
WithPermissionFallback installs fn as the answerer for permission requests reaching a session with no controller viewer; default unset keeps the built-in deny.
func WithRestart ¶
WithRestart enables the watchDog restart policy: a dying external instance is re-spawned up to limit times before parking in StateWarning (default: disabled, terminal StateError); a restart loses the downstream conversation context.
func WithSelfDBPath ¶ added in v0.40.3
WithSelfDBPath passes path to every chain-kind spawn via ChainDBEnvVar so a dispatched unit writes its reports to the same database the dispatching host read its mission from; empty (default) leaves the child to resolve its own.
func WithSelfExecutable ¶
WithSelfExecutable overrides the program a chain-kind agent is spawned from (default os.Executable()), letting a test point the spawn at a built fixture binary.
func WithStderr ¶
WithStderr forwards each spawned external instance's subprocess stderr to w; defaults to io.Discard.
func WithWorkspaceID ¶ added in v0.38.0
WithWorkspaceID passes id to every chain-kind spawn via ChainWorkspaceFlag so the child's mission-event publisher stamps the dispatching host's workspace; empty (default) adds no flag.
type PermissionFallback ¶
type PermissionFallback func(ctx context.Context, req UnattendedPermission) (libacp.RequestPermissionResponse, error)
PermissionFallback answers a permission request that arrived at an unattended session; it may block (runs on the request's own goroutine), and an error falls back to the built-in deny.
type SessionSpec ¶
type SessionSpec struct {
// Cwd is the downstream session's working directory; ACP requires one,
// and spec-correct agents expect it absolute.
Cwd string
// AdditionalDirectories are extra absolute workspace roots beyond Cwd; omitted/empty
// means none.
AdditionalDirectories []string
// McpServers are the already-resolved MCP servers to forward in session/new,
// filtered to what the downstream's mcpCapabilities can consume; nil forwards none.
McpServers []libacp.McpServer
// Meta is an opaque session/new `_meta` blob forwarded verbatim, unread by the
// kernel; nil forwards none.
Meta json.RawMessage
// Terminal, if set, advertises the terminal client capability to the downstream at
// initialize; terminal/* is refused unless the session's controller implements
// TerminalServer.
Terminal bool
}
SessionSpec is the fully-resolved input to Manager.OpenSession: everything the kernel needs to negotiate the downstream connection's capabilities and drive session/new.
type TerminalServer ¶
type TerminalServer interface {
CreateTerminal(ctx context.Context, req libacp.CreateTerminalRequest) (libacp.CreateTerminalResponse, error)
TerminalOutput(ctx context.Context, req libacp.TerminalOutputRequest) (libacp.TerminalOutputResponse, error)
WaitForTerminalExit(ctx context.Context, req libacp.WaitForTerminalExitRequest) (libacp.WaitForTerminalExitResponse, error)
KillTerminal(ctx context.Context, req libacp.KillTerminalRequest) (libacp.KillTerminalResponse, error)
ReleaseTerminal(ctx context.Context, req libacp.ReleaseTerminalRequest) (libacp.ReleaseTerminalResponse, error)
}
TerminalServer is an optional Viewer capability that services a downstream agent's terminal/* callbacks for the session it controls; routed only to the controller (else terminal/* answers MethodNotFound), and callbacks may block on their own goroutine.
type UnattendedPermission ¶
type UnattendedPermission struct {
InstanceID string
AgentID string
AgentName string
// SessionID is the downstream session the request arrived on.
SessionID libacp.SessionID
Request libacp.RequestPermissionRequest
}
UnattendedPermission is the self-contained input to a PermissionFallback: one downstream permission request that reached a session with no controller viewer, plus the identity of the instance that raised it.
type Viewer ¶
type Viewer interface {
// ID uniquely identifies this viewer within a session; two viewers on the
// same session must not share an ID.
ID() string
// Deliver receives one session update, in order (replayed backlog then live); it
// must not block since it runs under the session lock, and its returned error is
// advisory only.
Deliver(ctx context.Context, n libacp.SessionNotification) error
// RequestPermission answers the downstream agent's session/request_permission;
// called only on the controller viewer, it runs on its own goroutine and may block
// awaiting a decision.
RequestPermission(ctx context.Context, req libacp.RequestPermissionRequest) (libacp.RequestPermissionResponse, error)
}
Viewer is a consumer attached to one downstream session: it receives the session's streamed updates via Deliver and, when it is the session's controller, answers the downstream agent's permission requests via RequestPermission.