pool

package
v0.10.2 Latest Latest
Warning

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

Go to latest
Published: May 12, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package pool manages the global agent subprocess slot count + FIFO queue. Files:

  • buffer.go — per-session message buffer (drain on slot grant)
  • pool.go — slot allocation, FIFO queue, factory hookup

Buffer rationale: when a session is queued (no slot), incoming user messages must not vanish — they're appended to the on-disk PendingInput list (so a wick restart preserves them) AND held in a transient buffer here. When the slot is granted, the entire buffer is drained as one combined input to the spawned agent. See agents-design.md §5.1.1.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ActiveEntry

type ActiveEntry struct {
	SessionID  string
	AgentName  string
	CWD        string // resolved workspace path, used by RouteByCWD
	PID        int
	Lifecycle  string
	Substate   string
	LastActive time.Time
}

ActiveEntry is the public snapshot view of one running agent. Lifecycle / Substate / PID / LastActive are populated when the pool can read them; older callers that only check SessionID + AgentName keep working.

type AgentFactory

type AgentFactory interface {
	Build(opt FactoryOptions) (BuildResult, error)
}

AgentFactory builds an agent ready to Start. The pool wires the OnExit hook itself (so it can free the slot); the factory should not.

BuildResult.OnStarted is called by the pool right after a.Start succeeds — that's when the OS pid is known and the first user message has been drained from the buffer. Factories use it to finish writing the spawn `start` event with both fields. Optional; nil = nothing to record.

type Buffer

type Buffer struct {
	// contains filtered or unexported fields
}

Buffer is a small per-session message queue. Operations are idempotent w.r.t. the on-disk meta.json so a crash between in-memory append and disk persist doesn't lose user input.

func NewBuffer

func NewBuffer(layout config.Layout, sessionID string) (*Buffer, error)

NewBuffer reads any pending_input persisted on disk so we resume queued messages after a wick restart.

func (*Buffer) Append

func (b *Buffer) Append(text string) error

Append adds a new line and persists it to meta.PendingInput so a crash before drain doesn't drop it.

func (*Buffer) Drain

func (b *Buffer) Drain() (string, error)

Drain returns all buffered lines joined by newline and clears the buffer (both in-memory and on-disk PendingInput). Returns "" if empty.

func (*Buffer) Len

func (b *Buffer) Len() int

Len reports the buffered line count without modifying state.

type BuildResult

type BuildResult struct {
	Agent     *provider.Agent
	State     *state.Machine
	Store     *store.Store
	OnStarted func(meta SpawnStartMeta)
}

BuildResult bundles everything Build returns. New code should pull fields from here; the bare-tuple shape is gone so we don't have to thread one more channel through every callsite when we add another hook later.

type ClaudeFactory

type ClaudeFactory struct {
	Layout    config.Layout
	Spawner   provider.Spawner // optional override; nil = real claude
	RecordRaw bool
	OnEvent   func(sessionID, agentName string, ev event.AgentEvent)
	OnExit    func(sessionID, agentName string, reason provider.ExitReason)

	// Gate (optional) attaches a static command whitelist to every spawn.
	// When non-nil, Build writes a per-session settings.json + spec
	// file to a temp dir, points the spawner at the settings file,
	// and injects WICK_GATE_SPEC into ExtraEnv so wick-gate finds
	// its config. nil = no gate (fail-open, only safe for tests).
	Gate *GateConfig
	// GateLoader (optional) is called on every Build to fetch the
	// current gate config from the live config store. Takes precedence
	// over Gate when non-nil. This lets operators toggle gate_enabled
	// or edit AllowedCmds in the UI without restarting the server.
	GateLoader func() *GateConfig
	// BypassPermissionsLoader (optional) is called on every Build to
	// check whether --permission-mode bypassPermissions should be added
	// when no gate is active. Useful for non-interactive channels
	// (Slack, HTTP) where the operator wants to skip prompts without
	// enabling the full command gate.
	BypassPermissionsLoader func() bool

	// SpawnLogger (optional) writes one jsonl per spawn under
	// `<base>/backends/spawns/`. Each spawn emits `start` on Build +
	// `exit` from the OnExit hook so the Backends UI can list spawn
	// history per backend by `ls`-ing the directory. nil = no logging.
	SpawnLogger *provider.SpawnLogger
}

ClaudeFactory is the production AgentFactory: wires a ClaudeParser + ClaudeSpawner into a fresh provider.Agent for each Build call.

The factory owns no per-spawn state; the pool calls Build once per session activation.

func (*ClaudeFactory) Build

func (f *ClaudeFactory) Build(opt FactoryOptions) (BuildResult, error)

Build returns a fresh agent + state machine + store wired for one session+agent. Caller (the pool) is responsible for calling agent.Start.

type FactoryOptions

type FactoryOptions struct {
	SessionID     string
	AgentName     string
	ProviderType  string
	ProviderName  string
	Workspace     string
	ResumeID      string
	IdleTimeout   time.Duration
	KillAfterIdle time.Duration
	OnEvent       func(event.AgentEvent)
}

FactoryOptions is what the pool hands to the factory. ResumeID is pulled from the session's agents.json by the pool. ProviderType / ProviderName identify which provider runtime instance to spawn against — empty ProviderName resolves to the per-type default whose name equals the type itself ("claude" / "codex" / "gemini"). Both are forwarded to the spawn logger so /tools/agents/providers can surface per-provider history without re-parsing files.

type GateConfig

type GateConfig struct {
	// GateBinary is the absolute path to the wick-gate binary. Required.
	GateBinary string
	// Rules is the whitelist enforced for every spawn under this factory.
	Rules []gate.CommandRule
	// AppName drives the shared spec path (~/.<app>/agents/gate/spec.json).
	// Falls back to "wick" when empty.
	AppName string
	// DefaultScope is written into spec.json as the fallback scope for
	// rules that have an empty Scope field. Typically the default
	// workspace directory so no-scope rules are still path-restricted.
	DefaultScope string
	// TempDirRoot is where per-spawn gate artifacts live. If empty,
	// `<Layout.SessionDir(id)>/gate` is used.
	TempDirRoot string
}

GateConfig describes the gate plumbing: where the wick-gate binary lives + what rules it enforces. The factory writes the shared spec.json from Rules on every spawn so UI changes propagate immediately without restarting the server.

type LifecycleEvent

type LifecycleEvent struct {
	SessionID string
	AgentName string
	Lifecycle string // "spawning" | "killed"
	PID       int
	At        time.Time
}

LifecycleEvent is emitted for the two transitions the pool drives directly (no parser event triggers them): a fresh spawn coming online, or a subprocess dying. PID is populated for spawning → working; 0 for killed.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool is the global slot manager. It tracks how many agent subprocesses are alive across all sessions, FIFO-queues sessions that arrive while full, and grants slots when one frees up.

Pool deliberately knows nothing about CLI specifics — it asks an AgentFactory to build an *provider.Agent for a given session+agent name. Tests inject a factory that returns agents wired to the fakeSpawner; production wires ClaudeSpawner.

func New

func New(cfg PoolConfig) *Pool

New returns an empty pool.

func (*Pool) Active

func (p *Pool) Active() int

Active returns the number of running agents.

func (*Pool) ActiveSnapshot

func (p *Pool) ActiveSnapshot() []ActiveEntry

ActiveSnapshot returns a defensive copy of every running agent in the pool. Used by the Backends UI to show what's eating each slot.

func (*Pool) Dequeue

func (p *Pool) Dequeue(sessionID, agentName string) int

Dequeue drops every queued request matching sessionID+agentName. Returns the number of removed entries — operators use this to cancel a session that has been waiting too long without ever getting a slot. Active spawns are NOT touched; use Kill for that.

func (*Pool) HandleExit

func (p *Pool) HandleExit(sessionID, agentName string, _ provider.ExitReason)

HandleExit is the public hook the factory wires into agent.OnExit. It defers to the unexported onAgentExit but accepts the reason so future code can branch (e.g. don't grant queue if the previous exit was an error).

func (*Pool) IdleTimeout

func (p *Pool) IdleTimeout() time.Duration

IdleTimeout returns the configured idle timeout. UI consumers use it to render the auto-kill countdown alongside LastActive.

func (*Pool) Kill

func (p *Pool) Kill(sessionID, agentName string) error

Kill stops the running agent for sessionID+agentName. Idempotent if the agent is not currently active — returns nil in that case. The normal onAgentExit hook still fires, releasing the slot and draining the queue.

func (*Pool) MaxConcurrent

func (p *Pool) MaxConcurrent() int

MaxConcurrent surfaces the configured slot cap for the Backends UI. Read-only — change via PoolConfig at construction time.

func (*Pool) QueueLen

func (p *Pool) QueueLen() int

QueueLen returns the number of queued requests.

func (*Pool) QueueSnapshot

func (p *Pool) QueueSnapshot() []QueueEntry

QueueSnapshot returns a defensive copy of the current FIFO queue (oldest first). Used by the Backends UI to show what's waiting.

func (*Pool) Send

func (p *Pool) Send(ctx context.Context, sessionID, agentName, source, role, text string) error

func (*Pool) SendWithWorkspace

func (p *Pool) SendWithWorkspace(ctx context.Context, sessionID, agentName, source, role, text, workspace string) error

SendWithWorkspace is like Send but binds sessionID to the named workspace when auto-creating the session. Pass an empty string for the default.

func (*Pool) SessionExists added in v0.9.4

func (p *Pool) SessionExists(sessionID string) bool

Send routes a user message into the right session. If a slot is free the agent is spawned and the message sent immediately; else the message is appended to the session's buffer and the request is queued. The on-disk session meta status is updated to reflect running/queued so UI listings stay correct. SessionExists reports whether sessionID already has on-disk state. Cheap stat — no JSON parse. Used by channels (Slack, Telegram) to decide whether the next inbound message starts a brand-new session and needs a one-time origin-context turn injected before the user message.

Implements channels.SessionChecker.

func (*Pool) Stop

func (p *Pool) Stop()

Stop tears down all active agents and waits for trailing post-exit work (markStatus, queue drain). Used on graceful shutdown and by tests to flush goroutines before TempDir cleanup.

type PoolConfig

type PoolConfig struct {
	MaxConcurrent    int
	IdleTimeout      time.Duration
	KillAfterIdle    time.Duration
	Layout           config.Layout
	Factory          AgentFactory
	DefaultWorkspace string
	// OnSessionCreated is called after the pool auto-creates a session for a
	// channel message (e.g. Slack thread_ts). Wire this to
	// manager.Register so the dashboard sees the session immediately.
	OnSessionCreated func(s session.Session)
	// OnLifecycle fires when the pool transitions a session+agent's
	// lifecycle (Spawning, Killed). Idle/Working transitions are
	// implicit from event flow and are NOT routed here — UIs that
	// want every transition should subscribe to AgentEvent via
	// the factory's OnEvent. Optional; nil = no callback.
	OnLifecycle func(LifecycleEvent)
}

PoolConfig knobs.

DefaultWorkspace is the workspace name used when a session has no workspace bound. Empty = no default; the pool falls back to a per-session temp dir so claude still has a stable cwd. See agents-design.md §0.2 D4.

type QueueEntry

type QueueEntry struct {
	SessionID string
	AgentName string
	Enqueued  time.Time
}

QueueEntry is the public snapshot view of one queued request.

type SpawnStartMeta

type SpawnStartMeta struct {
	PID              int
	Binary           string
	Argv             []string
	FirstUserMessage string
}

SpawnStartMeta is the post-Start snapshot the pool feeds back to the factory so the spawn log gets a complete `start` record. PID, argv, and binary path are only knowable after Spawner.Spawn returns; FirstUserMessage comes from the buffer drain.

Jump to

Keyboard shortcuts

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