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.
Index ¶
- Constants
- Variables
- type Event
- type EventKind
- type EventSink
- type FileSystemServer
- type FleetEntry
- type InstanceFileSystem
- type InstanceStatus
- type Manager
- type Option
- func WithEventSink(sink EventSink) Option
- func WithFilesystem(fs InstanceFileSystem) 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 WithTerminalServer(ts TerminalServer) 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 using, since a // subagent writes against the mission row the parent created. 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.
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 FileSystemServer ¶ added in v0.41.0
type FileSystemServer interface {
ReadTextFile(ctx context.Context, req libacp.ReadTextFileRequest) (libacp.ReadTextFileResponse, error)
WriteTextFile(ctx context.Context, req libacp.WriteTextFileRequest) (libacp.WriteTextFileResponse, error)
}
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 InstanceFileSystem ¶ added in v0.41.0
type InstanceFileSystem interface {
FileSystemServer
FileSystemCapabilities() libacp.FileSystemCapabilities
}
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; 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 WithFilesystem ¶ added in v0.41.0
func WithFilesystem(fs InstanceFileSystem) Option
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 WithTerminalServer ¶ added in v1.0.0
func WithTerminalServer(ts TerminalServer) Option
WithTerminalServer installs ts as the instance-wide terminal server a viewer-less unit's terminal/* callbacks fall back to, the terminal peer of WithFilesystem. A controller viewer that serves terminals still wins.
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
FS libacp.FileSystemCapabilities
}
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. It is routed only to the controller; otherwise terminal/* answers MethodNotFound.
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. It must not block, since it
// runs under the session lock; its returned error is advisory.
Deliver(ctx context.Context, n libacp.SessionNotification) error
// RequestPermission answers the downstream agent's session/request_permission.
// Called only on the controller viewer, it 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 and, when it is the controller, answers the downstream agent's permission requests.