agentinstance

package
v0.39.0 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 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; a watchDog restarts a dead downstream but the conversation context is lost.

Index

Constants

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

Instance lifecycle states — the vocabulary of InstanceStatus.State.

  • StateStarting: transient, while a subprocess is (re)spawning.
  • StateRunning: a live downstream connection.
  • StateStopped: torn down intentionally (Stop/Close); the watchDog never restarts out of this state.
  • StateError: the downstream died unexpectedly. Terminal if restart is disabled, else transient (leads back to StateStarting).
  • StateWarning: restart was enabled but exhausted its limit, or a re-spawn itself failed.
View Source
const (
	ChainACPSubcommand = "acp"
	ChainPathEnvVar    = "CONTENOX_ACP_CHAIN_PATH"
	ChainHopEnvVar     = "CONTENOX_EVENT_HOP"
	ChainWorkspaceFlag = "workspace-id"
)

ChainACPSubcommand, ChainPathEnvVar, ChainHopEnvVar, and ChainWorkspaceFlag describe this binary's own ACP server for a chain-kind spawn: the subcommand that serves ACP over stdio, the env var naming which chain file to run, the env var carrying the dispatch hop of the context that spawned it, and the flag (bare name, no dashes) carrying the dispatching host's workspace id so the child stamps mission events with the firing workspace instead of its own default. Declared here rather than imported to avoid an import cycle with the packages that own them; kept exported so those packages can assert the definitions still agree.

View Source
const AgentModeConfigOptionID = "contenox.agent-mode"

AgentModeConfigOptionID is the reserved SessionConfigOption id under which a session surfaces the downstream agent's session Modes as a single synthetic "select" option (type "select", one value per available mode, currentValue the current mode id). SetConfigOption on this id translates to session/set_mode; a downstream current_mode_update is captured onto it. A reserved dotted namespace so it never collides with a downstream agent's own option ids.

View Source
const AgentModelConfigOptionID = "contenox.agent-model"

AgentModelConfigOptionID is the same synthetic-option scheme as AgentModeConfigOptionID, for the downstream agent's unstable model-picker state. SetConfigOption on this id translates to session/set_model; unlike modes, there is no model-update stream kind, so the set_model response alone becomes the new current value.

Variables

View Source
var ErrNotFound = errors.New("agentinstance: instance not found")

ErrNotFound is returned for an unknown instance id. It is a sentinel so callers can branch on 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 — by the built-in
	// headless deny or an injected PermissionFallback. It does not fire when
	// a fallback permits the request, so the audit trail never claims a
	// refusal that didn't happen.
	EventUnsupervisedDeny EventKind = "unsupervised_permission"
)

type EventSink

type EventSink func(Event)

EventSink receives every lifecycle Event, called synchronously on the goroutine that produced it. 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), read from the same snapshot.
	Sessions int `json:"sessions"`
	// Viewers is how many viewers are attached across those sessions,
	// independent of Sessions: an open session with nobody watching still
	// counts toward Sessions but not Viewers.
	Viewers   int       `json:"viewers"`
	StartedAt time.Time `json:"startedAt"`
	// SessionIDs lists every session currently open on the instance, sorted
	// for a deterministic snapshot. A session nobody is watching, or that
	// has emitted no update yet, 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, not ctx, so it outlives the
	// request. cwd is the sandbox workspace. Prefer StartResolved if the
	// agent is 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, so a caller enforcing a policy decision
	// (e.g. Enabled) spawns exactly the record it judged. 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 (controllerGranted true).
	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; the
	// kernel adds nothing to n. ErrNotFound when no instance owns sessionID.
	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 Close, Start returns an error. Idempotent.
	Close() error
}

Manager owns the lifecycle of running agent instances. Every method is safe for concurrent use, and every method taking an instanceID returns ErrNotFound when it is unknown.

func New

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

New returns a Manager that resolves declared agents via agents. The Manager owns 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 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 that reach a session with no controller viewer (see PermissionFallback). Default: unset, which keeps the built-in headless deny.

func WithRestart

func WithRestart(limit int) Option

WithRestart enables the watchDog restart policy: an external instance whose subprocess dies unexpectedly is re-spawned up to limit times before parking in StateWarning. Default: disabled (unexpected death is terminal StateError). A restart loses the downstream agent's conversation context.

func WithSelfExecutable

func WithSelfExecutable(path string) Option

WithSelfExecutable overrides the program a chain-kind agent is spawned from (default os.Executable()). Lets a test point the spawn at a built fixture binary, since the compiled test binary itself has no ACP server.

func WithStderr

func WithStderr(w io.Writer) Option

WithStderr forwards each spawned external instance's subprocess stderr to w. Defaults to io.Discard.

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 rather than resolving its own default. Empty (the default) adds no flag and leaves the child to its own resolution.

type PermissionFallback

type PermissionFallback func(ctx context.Context, req UnattendedPermission) (libacp.RequestPermissionResponse, error)

PermissionFallback answers a permission request that arrived at an unattended session — the kernel's only human-in-the-loop concession. It may block (runs on the request's own goroutine); ctx is the downstream request's context. Returning an error falls back to the built-in headless 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 for the session, on top of
	// Cwd. Omitted/empty means none.
	AdditionalDirectories []string

	// McpServers are the already-resolved MCP servers to forward downstream
	// in session/new; the kernel drops any the downstream's advertised
	// mcpCapabilities cannot consume. Nil forwards none.
	McpServers []libacp.McpServer

	// Meta is an opaque session/new `_meta` blob forwarded verbatim; the
	// kernel neither reads nor interprets it. Nil forwards none.
	Meta json.RawMessage

	// Terminal advertises the terminal client capability to the downstream
	// at initialize, iff set — negotiated once per connection, at the first
	// OpenSession. Even when set, every terminal/* is still gated on the
	// session's controller implementing TerminalServer (else
	// MethodNotFound); left false, terminals are never advertised and
	// terminal/* always refuses.
	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

TerminalServer is an optional capability a Viewer may implement to service a downstream agent's terminal/* callbacks for the session it controls. It is routed only to the session's controller viewer; a controller that does not implement it, or a session with no controller, answers terminal/* with MethodNotFound. Callbacks run on their own goroutine and may block.

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: the replayed journal
	// backlog on attach, then every live update.
	//
	// It must not block — it runs on the fan-out path under the session
	// lock, so a blocking call stalls every other viewer and the downstream
	// read loop. Enqueue and return. The 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 session's controller
	// viewer (an observer may later be promoted to controller on detach).
	// Unlike Deliver 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.

Jump to

Keyboard shortcuts

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