agentinstance

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
const (
	StateStarting = "starting"
	StateRunning  = "running"
	StateStopped  = "stopped"
	StateError    = "error"
	StateWarning  = "warning"
)

Instance lifecycle states, the vocabulary of InstanceStatus.State.

View Source
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.

View Source
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.

View Source
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

View Source
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.

func New

func New(agents agentregistryservice.Service, opts ...Option) Manager

New returns a Manager that resolves declared agents via agents, owning a fresh root context; call Close to tear everything down at shutdown.

type Option

type Option func(*manager)

Option configures a Manager.

func WithEventSink

func WithEventSink(sink EventSink) Option

WithEventSink installs sink as the lifecycle event sink (see EventSink).

func WithFilesystem added in v0.41.0

func WithFilesystem(fs InstanceFileSystem) Option

func WithJournalSize

func WithJournalSize(n int) Option

WithJournalSize overrides the per-session replay journal size (see defaultJournalSize).

func WithKillGrace

func WithKillGrace(d time.Duration) Option

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

func WithRestart(limit int) Option

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

func WithSelfDBPath(path string) Option

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

func WithSelfExecutable(path string) Option

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

func WithStderr(w io.Writer) Option

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

func WithWorkspaceID(id string) Option

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

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.

Jump to

Keyboard shortcuts

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