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 ¶
- type ActiveEntry
- type AgentFactory
- type Buffer
- type BuildResult
- type ClaudeFactory
- type FactoryOptions
- type GateConfig
- type LifecycleEvent
- type Pool
- func (p *Pool) Active() int
- func (p *Pool) ActiveSnapshot() []ActiveEntry
- func (p *Pool) Dequeue(sessionID, agentName string) int
- func (p *Pool) HandleExit(sessionID, agentName string, _ provider.ExitReason)
- func (p *Pool) IdleTimeout() time.Duration
- func (p *Pool) Kill(sessionID, agentName string) error
- func (p *Pool) MaxConcurrent() int
- func (p *Pool) QueueLen() int
- func (p *Pool) QueueSnapshot() []QueueEntry
- func (p *Pool) Send(ctx context.Context, sessionID, agentName, source, role, text string) error
- func (p *Pool) SendWithWorkspace(ctx context.Context, ...) error
- func (p *Pool) SessionExists(sessionID string) bool
- func (p *Pool) Stop()
- type PoolConfig
- type QueueEntry
- type SpawnStartMeta
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 ¶
NewBuffer reads any pending_input persisted on disk so we resume queued messages after a wick restart.
func (*Buffer) Append ¶
Append adds a new line and persists it to meta.PendingInput so a crash before drain doesn't drop it.
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 (*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 ¶
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 ¶
IdleTimeout returns the configured idle timeout. UI consumers use it to render the auto-kill countdown alongside LastActive.
func (*Pool) Kill ¶
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 ¶
MaxConcurrent surfaces the configured slot cap for the Backends UI. Read-only — change via PoolConfig at construction time.
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) 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
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.
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 ¶
QueueEntry is the public snapshot view of one queued request.
type SpawnStartMeta ¶
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.