Documentation
¶
Overview ¶
Package agentproc owns the lifecycle of the long-lived agent CLI processes that external agent runtimes keep in a bot's workspace. Drivers only say how to start a process and which configuration it starts from; when a process is started, shared, replaced, closed for idleness, or torn down at shutdown is decided here, in one place.
The rules:
- Every use leases the key's process and releases it when done. A leased process is never closed for idleness.
- A lease names the fingerprint of the configuration it would start a process from. A live process started from another one is retired: it takes no new leases and closes when its last lease is released.
- One start runs per key at a time; everyone waiting on it gets its process or its error.
- A process that has exited is replaced on the next lease. Dying is normal: drivers resume their sessions on the new process.
- A reconcile loop closes idle and dead processes, so nothing depends on an exit being noticed the moment it happens.
- Close tears every process down at once and refuses later leases.
A process generation follows the lifecycle model in AGENTS.md: starting, live, retired, closing, gone, one way, each transition in one function.
Index ¶
- Constants
- Variables
- func BaseEnv(extra ...string) []string
- func ObserveVersion(ctx context.Context, resolver external.LauncherResolver, ...)
- func ResolveLauncher(ctx context.Context, resolver external.LauncherResolver, ...) (external.Launcher, error)
- func ShellQuote(value string) string
- type Child
- func (p *Child) Close() error
- func (p *Child) CloseStdin()
- func (p *Child) Done() <-chan struct{}
- func (p *Child) Err() error
- func (p *Child) Exited() <-chan struct{}
- func (p *Child) Kill()
- func (p *Child) Read(b []byte) (int, error)
- func (p *Child) StderrTail() string
- func (p *Child) Write(b []byte) (int, error)
- type Key
- type Options
- type Process
- type Starter
- type Supervisor
- func (s *Supervisor) Close(ctx context.Context)
- func (s *Supervisor) Drain(ctx context.Context, match func(Key) bool, busy func(Process) bool) (func(), error)
- func (s *Supervisor) DrainConversations(ctx context.Context, botID, conversation string) (func(), error)
- func (s *Supervisor) Lease(ctx context.Context, key Key, fingerprint string, start Starter) (Process, func(), error)
- func (s *Supervisor) MarkBroken(key Key, proc Process)
- func (s *Supervisor) Peek(key Key) Process
- func (s *Supervisor) Retire(match func(Key) bool)
- func (s *Supervisor) SignOut(ctx context.Context, workspaces bridge.Provider, ...) error
Constants ¶
const ContainerPath = vpath.DataMount + "/.memoh/deps/bin:/opt/memoh/toolkit/bin:/usr/local/bin:/usr/bin:/bin"
ContainerPath is the PATH an agent CLI and every command it spawns see. The managed dependency shims come first, so a managed overlay of an image runtime (node, python, uv) wins over the toolkit copy.
Variables ¶
var ( // ErrClosed answers a lease after Close. ErrClosed = errors.New("agent process supervisor is closed") // ErrBusy answers a drain while a process of the key is at work. ErrBusy = errors.New("agent process is in use") )
var ErrNotReading = errors.New("agent process is not reading its input")
ErrNotReading reports a write to a command that stopped taking input; the command is killed.
Functions ¶
func BaseEnv ¶
BaseEnv is the environment every agent CLI starts from: the container PATH and the bot's data volume as HOME. Drivers append their own variables.
func ObserveVersion ¶
func ObserveVersion(ctx context.Context, resolver external.LauncherResolver, botID, depID, version string)
ObserveVersion feeds the version a CLI reported in its handshake back to a resolver that caches versions, so discovery needs no second probe.
func ResolveLauncher ¶
func ResolveLauncher(ctx context.Context, resolver external.LauncherResolver, botID, depID, fallbackPath string) (external.Launcher, error)
ResolveLauncher finds the CLI copy a runtime runs for a bot, never installing one. Without a resolver it is the toolkit copy at fallbackPath. A missing dependency comes back as an *external.DependencyMissingError, which callers must pass through so the application can tell the user what to do; any other failure is the runtime being unavailable.
func ShellQuote ¶
ShellQuote single-quotes value for a POSIX shell. Launcher paths live under per-bot data directories whose names Memoh does not control.
Types ¶
type Child ¶
type Child struct {
// contains filtered or unexported fields
}
Child is one long-lived command running through a workspace bridge exec stream and exposed as an io.ReadWriter.
Writes never wait on the command: they are queued for one sender, and a command that leaves inputQueue writes unread has stopped taking input and is killed. Done closes as soon as the command can serve no more, on exit or on Kill, so nothing hands it out while it is still being torn down.
func Spawn ¶
func Spawn(ctx context.Context, client *bridge.Client, command, workDir string, env []string) (*Child, error)
Spawn launches command on env alone, without a bridge-side timeout. Closing the returned process cancels the exec stream and terminates the command.
func SpawnWith ¶
func SpawnWith(ctx context.Context, client *bridge.Client, command, workDir string, opts bridge.ExecOptions) (*Child, error)
SpawnWith is Spawn for a caller that shapes the environment itself: a user supplied agent inherits the workspace environment minus what it must not see.
func (*Child) Close ¶
Close kills the process and returns once its exec stream has ended: the stream is cancelled locally, so its end does not wait on the command, and the bridge kills the command's process group when its stream ends.
func (*Child) CloseStdin ¶
func (p *Child) CloseStdin()
CloseStdin ends input without stopping the process: the command sees EOF once what was queued is sent.
func (*Child) Done ¶
func (p *Child) Done() <-chan struct{}
Done is closed once the process can serve no more: it exited, or Kill began tearing it down.
func (*Child) Exited ¶
func (p *Child) Exited() <-chan struct{}
Exited is closed once the exec stream has ended: the command no longer runs.
func (*Child) Kill ¶
func (p *Child) Kill()
Kill takes the process out of service at once and terminates it: Done closes before Kill returns, and the bridge kills the command when its stream is cancelled. Kill never waits on the command.
func (*Child) StderrTail ¶
StderrTail returns the most recent stderr output for diagnostics.
type Key ¶
Key names one process a runtime keeps for a Bot Agent. Scope is empty for runtimes that share one process between the Agent's conversations and the session ID for those that run one per conversation.
type Options ¶
type Options struct {
IdleTTL time.Duration // close a process unleased this long
StartTimeout time.Duration // bound one start
ReconcileEvery time.Duration
Logger *slog.Logger
}
Options configures a Supervisor. Zero values take the defaults.
type Process ¶
type Process interface {
Done() <-chan struct{}
Close() error
}
Process is a running agent CLI. Close must be safe to call more than once.
type Starter ¶
Starter starts a process. Its context bounds the start, not the process. It may return the fingerprint the process actually started from, when the start learned what the lease could not know (the CLI version the handshake reported); empty keeps the lease's.
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor holds every process of every key: the current one and those retired but still leased.
func New ¶
func New(opts Options) *Supervisor
New returns a Supervisor whose reconcile loop runs until Close.
func (*Supervisor) Close ¶
func (s *Supervisor) Close(ctx context.Context)
Close tears every process down at once and refuses later leases. It waits until every generation is gone, one still starting included, or ctx ends.
func (*Supervisor) Drain ¶
func (s *Supervisor) Drain(ctx context.Context, match func(Key) bool, busy func(Process) bool) (func(), error)
Drain closes every process of the matching keys and holds new starts on them off until the returned unblock is called, so the caller can change what the next process starts from (delete a login file, clear a conversation). It refuses with ErrBusy when busy reports one of them at work, or another drain holds a matching key, and returns once they have exited. A nil busy closes processes at work too.
func (*Supervisor) DrainConversations ¶
func (s *Supervisor) DrainConversations(ctx context.Context, botID, conversation string) (func(), error)
DrainConversations closes every process of the bot whose scope is conversation, or every process of the bot, of any runtime, when conversation is empty, and holds new ones off until the returned unblock. A process an Agent shares between its conversations stays for a single conversation: the conversation's state lives in what the reset clears, not in the process. It waits out another drain of the same processes.
func (*Supervisor) Lease ¶
func (s *Supervisor) Lease(ctx context.Context, key Key, fingerprint string, start Starter) (Process, func(), error)
Lease returns the key's live process started from fingerprint, starting one when there is none, and a release the caller must call when done.
func (*Supervisor) MarkBroken ¶
func (s *Supervisor) MarkBroken(key Key, proc Process)
MarkBroken retires proc if it is still the key's current process.
func (*Supervisor) Peek ¶
func (s *Supervisor) Peek(key Key) Process
Peek returns the key's current live process without leasing or starting one; callers may only read state the process already holds.
func (*Supervisor) Retire ¶
func (s *Supervisor) Retire(match func(Key) bool)
Retire retires the current process of every matching key; a leased one closes when its last lease is released.
func (*Supervisor) SignOut ¶
func (s *Supervisor) SignOut(ctx context.Context, workspaces bridge.Provider, runtime, botID, botAgentID, loginFile string, beforeDelete func()) error
SignOut removes the login an Agent's runtime keeps in loginFile in the bot's workspace. Every process of the Agent closes first and none starts until the file is gone: a running one could write a refreshed login back. A process at work refuses it as external.FailureCredentialBusy. beforeDelete, when set, runs with the processes gone (to end a sign-in that would put a login back).