agentproc

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

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

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

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

func BaseEnv(extra ...string) []string

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

func ShellQuote(value string) string

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

func (p *Child) Close() error

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) Err

func (p *Child) Err() error

Err reports a transport failure or nonzero exit after the process exits.

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) Read

func (p *Child) Read(b []byte) (int, error)

func (*Child) StderrTail

func (p *Child) StderrTail() string

StderrTail returns the most recent stderr output for diagnostics.

func (*Child) Write

func (p *Child) Write(b []byte) (int, error)

Write queues b for the command and never waits; a command that stopped taking input is killed.

type Key

type Key struct {
	Runtime    string
	BotID      string
	BotAgentID string
	Scope      string
}

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

type Starter func(ctx context.Context) (Process, string, error)

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).

Jump to

Keyboard shortcuts

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