Documentation
¶
Overview ¶
Package swarm is the per-space coordination core of Veronica, evva's in-process multi-agent swarm subsystem.
A SwarmSpace is one isolated "sub-cluster": a Leader plus Workers, each a long-lived agent.New(...) root agent, collaborating through a private message bus and a per-space .vero/ SQLite ledger. The Supervisor owns the space lifecycle (start/stop, dynamic membership, freeze, suspend/resume); the Scheduler turns wake sources (message / task / timer) into Controller.Run calls; the Roster is the single source of truth for "who is in this space and what are they doing", feeding both the list_members tool and the web API.
The process-singleton multi-space host (the :8888 HTTP/WS server and the SwarmSpace registry) lives one layer up in internal/swarm/service; the leaf components (bus, store, agentdef, tools) live in their own subpackages.
Invariant: the multi-agent oracle (pkg-only for agent concerns) ¶
Everything under internal/swarm/** consumes agent functionality through the public pkg/* surface ONLY (agent.New, pkg/event, pkg/tools, pkg/skill, pkg/permission, ...) and never imports internal/agent or any other evva internal package. This is enforced by scripts/depcheck.sh in CI. The single sanctioned exception is the public inbox-drainer seam added to pkg/agent in SPRD-1-12 — which is itself public, not a private reach-in. Keeping the swarm pkg-pure makes it evva's multi-agent completeness oracle: if evva's own swarm can be built on pkg/* alone, a third party's can too.
Index ¶
- Constants
- func AgeString(d time.Duration) string
- func AssignmentMail(t store.Task, sender string, auto bool) store.Message
- func BindMemberContext(cfg *config.Config, mc MemberContext)
- type MemberContext
- type MemberMetrics
- type MemberView
- type Membership
- type MemoryFile
- type Roster
- func (r *Roster) ActiveMembers() []string
- func (r *Roster) Controller(name string) (ui.Controller, bool)
- func (r *Roster) ControllerRef(ref string) (ui.Controller, bool)
- func (r *Roster) LeaderName() string
- func (r *Roster) Names() []string
- func (r *Roster) ResolveRecipient(to string) string
- func (r *Roster) Snapshot() []MemberView
- type RunPhase
- type RunStatus
- type ScheduleOrigin
- type SpacedEvent
- type SpawnedMeta
- type Supervisor
- func (s *Supervisor) AddMember(name string) error
- func (s *Supervisor) ClearMemberSession(name string) error
- func (s *Supervisor) ClearSchedule(name string) error
- func (s *Supervisor) CompactMember(name, kind string) error
- func (s *Supervisor) CreateMember(spec agentdef.MemberSpec) error
- func (s *Supervisor) Freeze(name string) error
- func (s *Supervisor) HaltAll() error
- func (s *Supervisor) ReloadAllMemberSkills() error
- func (s *Supervisor) ReloadMemberSkills(name string) error
- func (s *Supervisor) RemoveMember(name string) error
- func (s *Supervisor) Resume(name string) error
- func (s *Supervisor) RetireSpawned(name string) error
- func (s *Supervisor) SetLogger(l *slog.Logger)
- func (s *Supervisor) SetMemberPermissionMode(name, mode string) error
- func (s *Supervisor) SetSchedule(name string, sch agentdef.Schedule) error
- func (s *Supervisor) SpawnMember(base, retire string) (string, error)
- func (s *Supervisor) Start(ctx context.Context)
- func (s *Supervisor) Suspend(name string) error
- func (s *Supervisor) Unfreeze(name string) error
- func (s *Supervisor) Wait()
- type SwarmSpace
- func (sp *SwarmSpace) AddMemberSkill(member, name, description, body string) error
- func (sp *SwarmSpace) AlarmScheduler() *alarm.Scheduler
- func (sp *SwarmSpace) Blackboard() (content string, mtime time.Time, by string)
- func (sp *SwarmSpace) BudgetFor(name string) int
- func (sp *SwarmSpace) Ceilings() (tokens int, usd float64)
- func (sp *SwarmSpace) CheckCounts() (run, failed, timeout int64)
- func (sp *SwarmSpace) CheckPending(id int64) bool
- func (sp *SwarmSpace) ChecksConfigured() bool
- func (sp *SwarmSpace) ClearMemberSchedule(name string) error
- func (sp *SwarmSpace) CloneMember(base, clone string) error
- func (sp *SwarmSpace) ConfigureChecks(ctx context.Context, spec agentdef.CheckSpec)
- func (sp *SwarmSpace) DayFor(name string) memberDay
- func (sp *SwarmSpace) DiscardRuntimePermModes()
- func (sp *SwarmSpace) DiscardRuntimeSchedules() error
- func (sp *SwarmSpace) DiscardRuntimeSpawned()
- func (sp *SwarmSpace) DispatchReady() ([]store.Task, error)
- func (sp *SwarmSpace) EngineCounts() (dispatched, spawned, retired int64)
- func (sp *SwarmSpace) EnqueueCheck(id int64) bool
- func (sp *SwarmSpace) Events() <-chan SpacedEvent
- func (sp *SwarmSpace) MemberMemoryFiles(member string) ([]MemoryFile, error)
- func (sp *SwarmSpace) MemberSkills(member string) ([]agent.Skill, error)
- func (sp *SwarmSpace) MetricsSnapshot() (map[string]MemberMetrics, time.Time)
- func (sp *SwarmSpace) NotifySpec() *agentdef.NotifySpec
- func (sp *SwarmSpace) PublishSharedSkill(name, description, body string, overwrite bool) error
- func (sp *SwarmSpace) Reload()
- func (sp *SwarmSpace) RemoveMemberSkill(member, name string) error
- func (sp *SwarmSpace) RemoveSharedSkill(name string) error
- func (sp *SwarmSpace) RetentionDays() int
- func (sp *SwarmSpace) RetireWorker(name string) error
- func (sp *SwarmSpace) ScheduleFor(name string) (agentdef.Schedule, bool)
- func (sp *SwarmSpace) ScheduleInfoFor(name string) (agentdef.Schedule, ScheduleOrigin, bool)
- func (sp *SwarmSpace) SetMemberSchedule(name string, sch agentdef.Schedule) error
- func (sp *SwarmSpace) SharedSkills() []agent.Skill
- func (sp *SwarmSpace) Shutdown()
- func (sp *SwarmSpace) SpaceToday() spaceDay
- func (sp *SwarmSpace) SpawnWorker(base, retire string) (string, error)
- func (sp *SwarmSpace) SpawnedInfo(name string) (SpawnedMeta, bool)
- func (sp *SwarmSpace) TaskStaleThreshold() time.Duration
- func (sp *SwarmSpace) WebhookSecret() string
- func (sp *SwarmSpace) WorkflowStaleCounts() (tasksStale, mailboxStale int64)
- func (sp *SwarmSpace) WriteBlackboard(writer, content string) (int, error)
- type ToolSet
Constants ¶
const ( KindTaskDispatched = event.Kind("task_dispatched") KindMemberSpawned = event.Kind("member_spawned") KindMemberRetired = event.Kind("member_retired") )
DWF engine event kinds — space-level synthetics flowing through the same pump the member sinks feed, so the live console and the durable event log both carry every engine action. The Kind vocabulary is open by design (pkg/event); each event reuses TextPayload for one greppable line.
const ( // RetireOnComplete (default): the supervisor retires the clone once it has // completed at least one task, holds no incomplete ones, is idle, and has // an empty mailbox — never mid-run (the sweep runs between wakes). RetireOnComplete = "on_complete" // RetireManual: the clone lives until the leader's member_retire or an // operator removal. RetireManual = "manual" )
Retire policies for spawned members.
const EngineSender = "system"
EngineSender is the mail sender name for engine actions — the same non-member identity the ops watchdogs use (scheduler.notifyOps).
const KindBlackboardUpdated = event.Kind("blackboard_updated")
KindBlackboardUpdated is the engine event announcing one blackboard write — through the same pump as the DWF engine lines, so the live console, the timeline, and the durable event log all carry who rewrote the team picture and when. Operator disk edits produce no event (documented); the mtime-based freshness line still updates.
const KindOpsAlert = event.Kind("ops_alert")
KindOpsAlert is a system alert promoted to a first-class space event (NTF-1): every notifyOps notice — stall, budget trip, stale task, stale mailbox — emits one alongside its durable mail, so the console timeline, the chatlog, and the outbound notifier see what was previously mailbox-only. TextPayload carries "subject\nbody"; AgentID names the member the alert is about. Dedup rides the sources (one per episode/stay/ run), exactly like the mail.
const KindTaskCheckDone = event.Kind("task_check_done")
KindTaskCheckDone is the engine event announcing one finished check run — the CHK sibling of KindTaskDispatched, flowing through the same pump.
Variables ¶
This section is empty.
Functions ¶
func AgeString ¶ added in v1.11.0
AgeString humanizes a freshness age for the wake brief and the read tool: coarse on purpose ("3m ago", not "3m12s") — the reader needs "current vs stale", not a stopwatch. Negative ages (clock skew) read as just now.
func AssignmentMail ¶ added in v1.11.0
AssignmentMail composes the wake message that starts a task — one body for both dispatch paths, so a worker cannot tell manual from auto apart except by the marker. auto adds the "(auto-dispatched)" note: it tells the worker no leader run preceded the dispatch, so spec ambiguity means asking the leader, not guessing (DWF open question #3).
func BindMemberContext ¶
func BindMemberContext(cfg *config.Config, mc MemberContext)
BindMemberContext stashes mc on cfg's custom bag in memory. It writes the map directly rather than via Config.SetCustom, which persists to disk and cannot marshal the live *SwarmSpace pointer.
Types ¶
type MemberContext ¶
type MemberContext struct {
Name string
Role agentdef.Role
Space *SwarmSpace
}
MemberContext is one swarm member's runtime identity, handed to that member's custom tools (SPRD-1-7) so send_message can bake the sender and the task tools can reach the ledger/bus/roster.
It travels through the agent's Config rather than a factory closure on purpose: pkg/agent.WithCustomTool registers ONE factory per tool name process-wide (the first registration wins), and a shared factory can't carry per-agent identity in a closure — nor could a closure-captured *SwarmSpace, which would leak across spaces. So each member's tools read their identity from the per-agent Config they are built against. The Space pointer is in-memory only: a swarm agent's Config must never be serialized (the custom bag holds a live pointer).
func MemberContextFrom ¶
func MemberContextFrom(cfg *config.Config) (MemberContext, bool)
MemberContextFrom recovers the MemberContext a member's tools were built against, or false when none was bound.
type MemberMetrics ¶ added in v1.7.0
type MemberMetrics struct {
WakesMessage int64
WakesTimer int64
Runs int64
Aborts int64
// RunSeconds buckets completed runs by wall-clock duration:
// [0] <10s, [1] <1m, [2] <10m, [3] ≥10m.
RunSeconds [4]int64
// RunTokens buckets runs by token cost (input+output, the same per-run
// delta the RP-13 daily counter folds — one source, no double books):
// [0] <1k, [1] <10k, [2] <50k, [3] ≥50k. A run whose provider reported
// no usage lands in [0] with zero cost — the bucket count still equals
// Runs, so "every run metered" stays checkable (RP-28).
RunTokens [4]int64
}
MemberMetrics counts one member's scheduler lifecycle (RP-17): wakes by source, runs, non-clean exits, and a coarse run-duration histogram. Exported as a snapshot value through SwarmSpace.MetricsSnapshot.
type MemberView ¶
type MemberView struct {
Name string
Role agentdef.Role
Membership Membership
Run RunStatus
Phase RunPhase
Tool string
PhaseSince int64 // unix millis the current phase was entered
CurrentTask int64
WhenToUse string
// PermissionMode is the member's effective permission stance (manifest
// member override > space setting; RP-24) — "bypass" runs fully
// autonomous, "default" queues approvals for a human.
PermissionMode string
Usage llm.Usage // cumulative session tokens as of the last run boundary (RP-13)
LastTurnInput int // most recent turn's input tokens (context pressure)
DailyTokens int // tokens spent today (the budget breaker's counter)
}
MemberView is a read-only snapshot of one member, the shape served to the list_members tool and the web API.
func (MemberView) DisplayPhase ¶
func (v MemberView) DisplayPhase() string
DisplayPhase composes the coarse run status and the fine event-derived phase into the single label shown to operators and teammates. A suspended member reads "suspended" (the coarse status wins, since the deriver may have moved to "ready" right after the cancel); otherwise the fine phase, with the tool name appended for the executing / waiting-approval phases ("executing:bash"). An empty phase falls back to the coarse status. The web composes the same rule.
type Membership ¶
type Membership string
Membership is the first of the two orthogonal status dimensions (design §5.1): whether a member is in service (active) or cold-stored (frozen). v1 never deletes — freeze is the safe "offline".
const ( MembershipActive Membership = "active" MembershipFrozen Membership = "frozen" )
type MemoryFile ¶ added in v1.7.0
type MemoryFile struct {
Name string // dir-relative path, slash-separated (e.g. "MEMORY.md", "project_x.md")
Content string
}
MemoryFile is one file of a member's memory dir, served read-only to the web (RP-25): the User's transparency window onto the team's mind.
type Roster ¶
type Roster struct {
// contains filtered or unexported fields
}
Roster is the per-space, thread-safe member directory — the single source of truth feeding both list_members and /api/swarm/:id.
func (*Roster) ActiveMembers ¶
ActiveMembers returns the names of active (in-service) members in insertion order. It is the bus.Membership view used to expand a "to: all" broadcast; frozen members are excluded so a broadcast never reaches them (SPRD-1-5).
func (*Roster) Controller ¶
func (r *Roster) Controller(name string) (ui.Controller, bool)
Controller returns a member's handle by member name, for the supervisor/webapi to drive.
func (*Roster) ControllerRef ¶
func (r *Roster) ControllerRef(ref string) (ui.Controller, bool)
ControllerRef resolves a controller by either member name OR controller AgentID. Internal callers (supervisor, lifecycle commands) use the name; the web approval/question reply path carries the event's AgentID instead, so it must resolve too — without this, every web-driven RespondPermission misses (name-keyed lookup, AgentID ref) and the blocked tool hangs forever.
The name lookup is the O(1) common path; an AgentID falls through to a scan (AgentID() just reads a string field, the member count is small, and gate replies are human-paced — so the scan is free in practice and we avoid a second index to keep in sync). The two namespaces don't overlap (UUID hex vs human names), so name-first is unambiguous.
func (*Roster) LeaderName ¶ added in v1.7.0
LeaderName returns the unique leader's member name, or "" when the roster has none (e.g. a store-only test space). Exported for the proposal tools (RP-23), which address the leader without knowing its actual name; the budget breaker and watchdog use it via the unexported alias below.
func (*Roster) ResolveRecipient ¶
ResolveRecipient maps a send target to a concrete member name. An exact member name and the "all" broadcast pass through unchanged; otherwise the target is treated as a ROLE and resolved to the unique active member of that role — so a worker can address "leader" without knowing the leader's member name (which in practice often differs, e.g. "lead"/"pm"). An ambiguous role (more than one active member, e.g. "worker") or an unknown target is returned unchanged for the caller to reject with a helpful error.
func (*Roster) Snapshot ¶
func (r *Roster) Snapshot() []MemberView
Snapshot returns a copy of every member in insertion order — for list_members and the web API. No Controller handles cross this boundary.
type RunPhase ¶
type RunPhase string
RunPhase is the fine-grained, event-derived run sub-phase (RP-3). It refines the coarse RunStatus the supervisor sets (idle/busy/suspended) into the same vocabulary evva's TUI shows — running / thinking / executing / … — plus the swarm-critical WAITING_APPROVAL / WAITING_INPUT, so an operator can tell a long tool call from a hang from a blocked approval instead of seeing a flat "busy". It is derived purely from the member's event stream (see phaseDeriver); the coarse RunStatus stays authoritative for lifecycle (suspended) and for event-less callers. Composition for display: a suspended member shows "suspended" (coarse wins); otherwise the phase.
const ( PhaseReady RunPhase = "ready" // idle — burns no tokens PhaseRunning RunPhase = "running" // loop alive between sub-phases PhaseThinking RunPhase = "thinking" // model generating reasoning PhaseTexting RunPhase = "texting" // model generating response text PhaseExecuting RunPhase = "executing" // a tool call is in flight (Tool names it) PhaseWaitingApproval RunPhase = "waiting-approval" // blocked in the permission broker (Tool names it) PhaseWaitingInput RunPhase = "waiting-input" // blocked in the question broker PhaseDraining RunPhase = "draining" // folding async results / inbox PhaseCompacting RunPhase = "compacting" // session compaction PhasePaused RunPhase = "paused" // iteration limit PhaseError RunPhase = "error" // last run failed )
type RunStatus ¶
type RunStatus string
RunStatus is the second dimension, meaningful only while active: idle (not in a run, burning no tokens), busy (Controller.Run in flight), or suspended (the run context was cancelled).
type ScheduleOrigin ¶ added in v1.7.0
ScheduleOrigin says where a member's live schedule came from: the manifest (the zero value) or a runtime change, with the unix-milli instant of that change so list_members can render "set 2026-06-11" (RP-20 §2.5).
type SpacedEvent ¶
SpacedEvent tags an agent event with the space it came from. AgentID is already on the event (set by the agent), so (SpaceID, Event.AgentID) is the full routing key the service (SPRD-1-8) fans out on.
type SpawnedMeta ¶ added in v1.11.0
type SpawnedMeta struct {
From string `json:"from"` // base member the clone was made from
Retire string `json:"retire"` // RetireOnComplete | RetireManual
Seq int `json:"seq"` // per-base clone number; never reused
}
SpawnedMeta records one ephemeral member's provenance (DWF).
type Supervisor ¶
type Supervisor struct {
// contains filtered or unexported fields
}
Supervisor owns one space's lifecycle and run engine: it launches a recover-guarded run loop per member, turns the three wake sources (message, task, timer — §5.5) into Controller.Run calls, and exposes membership (AddMember/Freeze/Unfreeze) and run control (Suspend/Resume/HaltAll). It holds each in-flight run's cancel so a Suspend is a deterministic ctx-cancel.
Mechanically there are two wake channels — the bus mailbox (message, which also carries task-assignment messages from 1-7's task_assign) and a per-agent timer/resume poke. "Idle burns no tokens": with no wake there is no Run.
func NewSupervisor ¶
func NewSupervisor(sp *SwarmSpace) *Supervisor
NewSupervisor builds a supervisor over an assembled space. Call Start to bring the run loops up.
func (*Supervisor) AddMember ¶
func (s *Supervisor) AddMember(name string) error
AddMember hot-loads agents/sub/<name>/ into the live space — roster entry, mailbox, and run loop — with no restart (design §5.4, user-triggered). Start must have run first.
func (*Supervisor) ClearMemberSession ¶ added in v1.7.2
func (s *Supervisor) ClearMemberSession(name string) error
ClearMemberSession wipes one member's conversation to a blank slate while the rest of the space keeps running: the live agent gets a fresh session (empty history, zeroed usage, cleared todos, NEW agent id) and the member's persisted snapshots are deleted, so neither the next wake nor a service restart (Reload's latestSessionFor) resurrects the old context. Membership, run status, schedule, skills, memory dir, and the budget meter all survive — this clears the member's mind, not its seat.
Holding m.mu across the clear is what makes it race-free against the run engine: runOnce claims the run slot under the same mutex, so a wake that fires mid-clear parks until the swap is done. A member with a run in flight (cancelRun set) refuses with a "busy" error — suspend it or wait.
func (*Supervisor) ClearSchedule ¶
func (s *Supervisor) ClearSchedule(name string) error
ClearSchedule removes a member's recurring schedule from the running loop and writes a TOMBSTONE row (not a delete): "the leader cleared this crontab" must survive a restart and beat a schedule the manifest still declares (RP-20 §2.2). A member with no schedule still gets the tombstone — predictable semantics: after a clear, the member has no schedule until the next set or a fresh register.
func (*Supervisor) CompactMember ¶ added in v1.8.0
func (s *Supervisor) CompactMember(name, kind string) error
CompactMember compacts one member's live context on operator command — the web's per-member twin of the TUI's /compact. kind is "micro" (elide older tool-result bodies, no LLM call) or "full" (one summarization call that replaces the transcript with a context brief). Like ClearMemberSession it holds m.mu across the call, which is what makes it race-free against the run engine (runOnce claims its run slot under the same mutex), and refuses a member with a run in flight — suspend it or wait. An unknown kind errors (Agent.Compact validates it; the web maps it to 400).
The full path makes one LLM call, so it runs under the supervisor's long-lived rootCtx rather than the request context — the summarization must survive the operator's HTTP client disconnecting. The compaction emits the same KindCompacting/KindCompactingEnd events the TUI shows, so the member's live stream narrates it for free.
func (*Supervisor) CreateMember ¶
func (s *Supervisor) CreateMember(spec agentdef.MemberSpec) error
CreateMember authors a brand-new worker from an operator spec (RP-8): it writes the agent dir, then hot-loads it through the exact AddMember path (register→construct→startLoop→persist). A roster pre-check rejects a duplicate before touching disk; if the hot-load fails after the dir is written, the dir is rolled back so a failed create leaves no half state. The manifest rewrite (so the member survives restart) is the service's job, layered on top.
func (*Supervisor) Freeze ¶
func (s *Supervisor) Freeze(name string) error
Freeze cold-stores a member: it keeps its mailbox and history but is never scheduled again until Unfreeze. An in-flight run is left to finish (freeze stops future dispatch, it is not a kill).
func (*Supervisor) HaltAll ¶
func (s *Supervisor) HaltAll() error
HaltAll suspends every member and cancels every in-flight run — the Phase-2 kill switch. Members come back individually via Resume (or on restart).
func (*Supervisor) ReloadAllMemberSkills ¶ added in v1.7.0
func (s *Supervisor) ReloadAllMemberSkills() error
ReloadAllMemberSkills runs ReloadMemberSkills over the whole roster — the fan-out a shared-skill change needs (RP-26 Part B): every member re-scans (shared, own) and applies at its own next run boundary. Per-member failures are joined, not short-circuited, so one bad member can't strand the rest of the team on a stale catalog.
func (*Supervisor) ReloadMemberSkills ¶
func (s *Supervisor) ReloadMemberSkills(name string) error
ReloadMemberSkills re-scans a member's on-disk skills directory and re-renders its system prompt to match (RP-10-4) — the seam the web add/remove-skill path drives after writing or deleting a SKILL.md. It rebuilds the registry from disk (sidestep- ping pkg/skill's lack of a Remove: a full re-scan is the source of truth, exactly how the member was first constructed), then applies it at the member's next RUN BOUNDARY: an idle member is poked and applies on the spot; a busy one stashes the new catalog and the run loop swaps it in once the current run ends (serve), so an in-flight conversation never sees its prompt change underfoot.
func (*Supervisor) RemoveMember ¶
func (s *Supervisor) RemoveMember(name string) error
RemoveMember retires a worker from the live space (RP-8): it stops the member's run loop and any in-flight run, drops it from the roster, stops mail delivery, and tears down its agent — then persists the new membership. The LEADER can never be removed (it is unique). On-disk concerns — dropping it from the manifest and optionally deleting its dir — are the service's job (ordered so a restart never references a missing dir). The .vero ledger is untouched (v1 never deletes history).
func (*Supervisor) Resume ¶
func (s *Supervisor) Resume(name string) error
Resume un-parks a suspended member and pokes it to re-run its unread work.
func (*Supervisor) RetireSpawned ¶ added in v1.11.0
func (s *Supervisor) RetireSpawned(name string) error
RetireSpawned retires an ephemeral member by hand (leader member_retire). Manifest members are refused — their membership is the operator's contract. A busy clone is refused too: RemoveMember cancels an in-flight run, and a manual retire should never chop work; on_complete clones retire themselves when idle, and a manual-policy clone can be retried once it settles.
func (*Supervisor) SetLogger ¶
func (s *Supervisor) SetLogger(l *slog.Logger)
SetLogger swaps the supervisor's logger. The service wires its own logger in so every member's wake/run lifecycle lands in the same log stream.
func (*Supervisor) SetMemberPermissionMode ¶ added in v1.7.3
func (s *Supervisor) SetMemberPermissionMode(name, mode string) error
SetMemberPermissionMode switches one member's permission stance at runtime (the web's per-member control): the live gate reads the mode per tool call, so the switch applies immediately — mid-run included (the TUI Shift+Tab precedent; no busy guard needed). The roster's display cache follows, and the switch is recorded as a runtime override in runtime.json so a restart rebuild reapplies it (a fresh register discards it — manifest authority). An invalid mode errors (→ 400 at the web layer); unknown member → 404.
func (*Supervisor) SetSchedule ¶
func (s *Supervisor) SetSchedule(name string, sch agentdef.Schedule) error
SetSchedule puts a member on (or replaces) a recurring timer schedule and applies it to the running loop immediately (fixing the old "seeded once at startMemberLoop" gap, RP-7 §3.4). Both runtime write paths — the leader's schedule_set tool (via sp.SetMemberSchedule) and the operator's web edit (via Service.SetSchedule) — converge here, so this is where the durable row lands (RP-20): store first, live loop second; if the row can't be written the loop is left untouched and the caller sees the error. The two live representations update in separate critical sections to preserve the package lock order s.mu → sp.mu (never nested): the memberRun (read by the tick under s.mu) and sp.schedules + provenance (under sp.mu). A bad cron, empty spec, or unknown member is rejected up front — a row for a member that doesn't exist would lie dormant and apply to a future namesake.
func (*Supervisor) SpawnMember ¶ added in v1.11.0
func (s *Supervisor) SpawnMember(base, retire string) (string, error)
SpawnMember clones an existing worker into a live ephemeral member (DWF member_spawn): derive the name, re-enter the base's construction path, book provenance, start the run loop, persist. The cap gates only this — the agent-driven path; operator surfaces stay uncapped (the guardrail is on the model, not the human).
func (*Supervisor) Start ¶
func (s *Supervisor) Start(ctx context.Context)
Start launches a run loop per current member plus the one timer tick. It is idempotent. Everything lives until ctx is cancelled — pass the space's context so SwarmSpace.Shutdown also stops the supervisor.
func (*Supervisor) Suspend ¶
func (s *Supervisor) Suspend(name string) error
Suspend stops a member's current run (cancel its ctx) and parks it: further wakes do nothing until Resume. The unread messages it was working stay unread (the DB is truth), so Resume re-processes them.
func (*Supervisor) Unfreeze ¶
func (s *Supervisor) Unfreeze(name string) error
Unfreeze returns a member to service and pokes it to drain any mail that queued while it was frozen.
func (*Supervisor) Wait ¶
func (s *Supervisor) Wait()
Wait blocks until every run loop and tick goroutine started under the supervisor's context has returned. Call it AFTER cancelling that context (otherwise the loops never exit and Wait hangs) and BEFORE closing the space store, so no run loop can touch a closed DB. teardownSpace orders it exactly so; tests do the same in their cleanup.
type SwarmSpace ¶
type SwarmSpace struct {
ID string
Name string
Workdir string
Store *store.Store
Roster *Roster
Bus *bus.Bus
// contains filtered or unexported fields
}
SwarmSpace is one live, isolated sub-cluster: its own workdir, .vero store, Roster, message Bus, and constructed agent handles. Two spaces in one process share nothing — separate stores, rosters, buses, and event streams — and member names are scoped per space.
func NewSpace ¶
func NewSpace(id string, m agentdef.Manifest, loaded []agentdef.Loaded, ts ToolSet, cfg *config.Config) (*SwarmSpace, error)
NewSpace assembles a live space from a manifest and its loaded agent definitions: it opens the per-space store and message bus, constructs each member via agent.New against per-agent config clones, wires each agent's event.Sink to stamp the spaceID, registers a mailbox per member, and populates the Roster. After this returns, every member is active + idle and addressable by name — no scheduling yet (the Supervisor, SPRD-1-6, starts the run loops).
ts may be nil (no swarm tools attached yet). cfg supplies the AppConfig; each agent gets its own clone so per-agent provider/model mutations don't bleed.
func (*SwarmSpace) AddMemberSkill ¶
func (sp *SwarmSpace) AddMemberSkill(member, name, description, body string) error
AddMemberSkill authors a skill on a member (User-only, via the web) and reloads it so the new skill enters the member's prompt + skill tool at its next run boundary (RP-10). RemoveMemberSkill is the mirror. Both reject an unknown member up front.
func (*SwarmSpace) AlarmScheduler ¶ added in v1.7.0
func (sp *SwarmSpace) AlarmScheduler() *alarm.Scheduler
AlarmScheduler returns the space's shared one-shot alarm scheduler, allocating it on first use. A fired alarm is delivered by deliverAlarm as a durable bus message to its Target (the member to wake) — so it flows through the same mailbox path as a teammate message: the target's run loop wakes and drains it. Durable alarms persist beside the space store and are re-armed by the supervisor on Start.
func (*SwarmSpace) Blackboard ¶ added in v1.11.0
func (sp *SwarmSpace) Blackboard() (content string, mtime time.Time, by string)
Blackboard reads the current board: content, its mtime, and the last tool writer when the file is still that writer's version ("" for a fresh restart or after an operator disk edit — the freshness line then carries no attribution rather than a wrong one). Absent or empty file = dormant: "", zero time. Disk is the truth — read fresh per call, never cached — so an operator's hand edit is live at every member's next wake with no service hook. A hand-edited file past the hard ceiling is truncated on the read side (the write-side cap can't gate an editor).
func (*SwarmSpace) BudgetFor ¶ added in v1.7.0
func (sp *SwarmSpace) BudgetFor(name string) int
BudgetFor resolves a member's effective daily token budget: a manifest member-level override wins (>0 = own cap, <0 = unlimited even when the space sets a default), otherwise the space-wide Settings.DailyBudgetTokens. 0 means unlimited. Exported so list_members can render "today X/Y".
func (*SwarmSpace) Ceilings ¶ added in v1.11.0
func (sp *SwarmSpace) Ceilings() (tokens int, usd float64)
Ceilings returns the space-wide daily budget ceilings (CST): the In+Out token cap and the priced-USD cap, 0 = that axis off. Exported for the metrics surface.
func (*SwarmSpace) CheckCounts ¶ added in v1.11.0
func (sp *SwarmSpace) CheckCounts() (run, failed, timeout int64)
CheckCounts reports the CHK runner tallies (runs delivered, failed, timed out). Exported for the metrics endpoint.
func (*SwarmSpace) CheckPending ¶ added in v1.11.0
func (sp *SwarmSpace) CheckPending(id int64) bool
CheckPending reports whether a task's check is queued or executing — the board's RUNNING chip state (evidence only lands when a run finishes).
func (*SwarmSpace) ChecksConfigured ¶ added in v1.11.0
func (sp *SwarmSpace) ChecksConfigured() bool
ChecksConfigured reports whether this space runs verify-time checks — the task_create gate for verify:"checks".
func (*SwarmSpace) ClearMemberSchedule ¶
func (sp *SwarmSpace) ClearMemberSchedule(name string) error
ClearMemberSchedule removes a member's timer schedule via the run engine.
func (*SwarmSpace) CloneMember ¶ added in v1.11.0
func (sp *SwarmSpace) CloneMember(base, clone string) error
CloneMember builds a live duplicate of an existing member's definition under a new name: register (protocol re-derived for the clone's name) → construct (fresh config clone; same model/effort/permission resolution as the base) → mailbox + roster. Schedules deliberately do NOT clone — a duplicated cron duty is never what fan-out means; manifest budget overrides DO (a clone spends under its base's cap semantics). The caller records provenance and starts the run loop.
func (*SwarmSpace) ConfigureChecks ¶ added in v1.11.0
func (sp *SwarmSpace) ConfigureChecks(ctx context.Context, spec agentdef.CheckSpec)
ConfigureChecks installs and starts the space's check runner. NewSpace calls it when the manifest sets verify_checks; tests (this package and the tools package) call it directly on lite spaces. The runner lives until ctx (the space context) is cancelled; stopChecks drains it before the store closes.
func (*SwarmSpace) DayFor ¶ added in v1.11.0
func (sp *SwarmSpace) DayFor(name string) memberDay
DayFor reports one member's full day (all classes + USD) for the roster surfaces. The zero value means "nothing metered today".
func (*SwarmSpace) DiscardRuntimePermModes ¶ added in v1.7.3
func (sp *SwarmSpace) DiscardRuntimePermModes()
DiscardRuntimePermModes wipes every runtime permission-mode override so the manifest is authoritative again — DiscardRuntimeSchedules' sibling, called by the service on a FRESH register only; restart rebuilds keep them.
func (*SwarmSpace) DiscardRuntimeSchedules ¶ added in v1.7.0
func (sp *SwarmSpace) DiscardRuntimeSchedules() error
DiscardRuntimeSchedules wipes every runtime schedule override — the store rows and the legacy runtime.json field — so the manifest is authoritative again. The service calls it on a FRESH register (`evva swarm .`), the operator's explicit "take the manifest as written" (RP-20 §2.4); restart rebuilds (Reconcile / RunSpace / reset) never do. Must run BEFORE Reload, which is what applies the rows.
func (*SwarmSpace) DiscardRuntimeSpawned ¶ added in v1.11.0
func (sp *SwarmSpace) DiscardRuntimeSpawned()
DiscardRuntimeSpawned drops the persisted ephemeral-member records — the fresh-register path (DWF-6): an explicit `evva swarm .` means "take the manifest as written", and clones are runtime state, not manifest state. Mirrors DiscardRuntimePermModes; called BEFORE Reload would re-clone them.
func (*SwarmSpace) DispatchReady ¶ added in v1.11.0
func (sp *SwarmSpace) DispatchReady() ([]store.Task, error)
DispatchReady runs one engine turn: sweep the ledger for engine-managed tasks whose time has come (store.SweepDispatchable marks them running atomically) and mail each assignee. Returns the dispatched tasks so callers can fold them into tool results and event lines. A mail failure after the flip mirrors manual task_assign's failure surface — the task IS running, the error is logged, and the RP-22 stale-task watchdog is the backstop; the persist-before-signal bus makes anything short of a store failure durable.
func (*SwarmSpace) EngineCounts ¶ added in v1.11.0
func (sp *SwarmSpace) EngineCounts() (dispatched, spawned, retired int64)
EngineCounts reports the DWF engine tallies (auto-dispatched tasks, members spawned/retired). Exported for the metrics endpoint.
func (*SwarmSpace) EnqueueCheck ¶ added in v1.11.0
func (sp *SwarmSpace) EnqueueCheck(id int64) bool
EnqueueCheck queues a check for a task that just entered `verifying`. Called from the two verifying-entry tool paths (task_done and task_update_status) after their transition commits. No-op when the space configures no checks, the task opted out (check:"off"), or its policy is auto (auto wins — the leader explicitly declared "mechanical, don't gate"). Reports whether a check was actually queued so tool results can say so.
func (*SwarmSpace) Events ¶
func (sp *SwarmSpace) Events() <-chan SpacedEvent
Events is the space's outbound event stream — every member's events, each stamped with this space's ID. The service fans these out per (spaceID, AgentID); a consumer must drain it continuously.
func (*SwarmSpace) MemberMemoryFiles ¶ added in v1.7.0
func (sp *SwarmSpace) MemberMemoryFiles(member string) ([]MemoryFile, error)
MemberMemoryFiles lists a member's memory directory for the web's read-only Memory view (RP-25). Reads the disk fresh — the dir is the single source of truth the member itself writes. Only *.md files surface (the memdir convention); each is capped like memdir.MaxFileBytes so one runaway file cannot balloon the response. An empty dir yields an empty list, not an error.
func (*SwarmSpace) MemberSkills ¶
func (sp *SwarmSpace) MemberSkills(member string) ([]agent.Skill, error)
MemberSkills lists a member's authored skills (RP-10) by re-scanning its on-disk skills/ dir — the source of truth POST/DELETE write, so a GET right after an add reflects it immediately (the live agent registry lags until the boundary reload). A member with no skills dir yields an empty list, not an error.
func (*SwarmSpace) MetricsSnapshot ¶ added in v1.7.0
func (sp *SwarmSpace) MetricsSnapshot() (map[string]MemberMetrics, time.Time)
MetricsSnapshot copies every member's counters plus the space's start time (the uptime anchor). Exported for the service's metrics endpoint (RP-17).
func (*SwarmSpace) NotifySpec ¶ added in v1.11.0
func (sp *SwarmSpace) NotifySpec() *agentdef.NotifySpec
NotifySpec returns the space's outbound-notification config (nil = off, NTF). Exported for the service, which owns the notifier beside the event log — the space itself never sends.
func (*SwarmSpace) PublishSharedSkill ¶ added in v1.7.0
func (sp *SwarmSpace) PublishSharedSkill(name, description, body string, overwrite bool) error
PublishSharedSkill writes a skill into the space-shared dir and fans the reload out to EVERY member (RP-26 Part B) — each picks the new catalog up at its own next run boundary (an idle member on the spot, a busy one when its current run ends). The two callers are the leader's skill_publish tool and the operator's web POST; both go through here so the write+reload-all pairing can't be skipped. overwrite gates replacing an existing version (agentdef.ErrSkillExists otherwise).
func (*SwarmSpace) Reload ¶
func (sp *SwarmSpace) Reload()
Reload restores a rebuilt space to where it died:
- resume each member's most recent real transcript (its persona session),
- re-queue each member's durable unread mail onto its mailbox, and
- reapply persisted frozen membership.
Call it AFTER NewSpace (agents + mailboxes exist) and BEFORE the Supervisor starts the run loops — §6.2's ordering: requeue after the inbox exists, before the loop drains it, so no wake is lost. Tasks need no handling: they live in vero.db, so a rebuilt space already sees the same ledger (a row left `running` is still `running`). Reload is idempotent.
func (*SwarmSpace) RemoveMemberSkill ¶
func (sp *SwarmSpace) RemoveMemberSkill(member, name string) error
RemoveMemberSkill deletes a member's skill and reloads it (RP-10).
func (*SwarmSpace) RemoveSharedSkill ¶ added in v1.7.0
func (sp *SwarmSpace) RemoveSharedSkill(name string) error
RemoveSharedSkill deletes a shared skill and fans the reload out — the User's final-arbiter delete (web), so a bad publish is reversible.
func (*SwarmSpace) RetentionDays ¶ added in v1.7.0
func (sp *SwarmSpace) RetentionDays() int
RetentionDays returns the space's ledger retention window in days (0 = retention disabled). Exported for the service's manual vacuum default (RP-16).
func (*SwarmSpace) RetireWorker ¶ added in v1.11.0
func (sp *SwarmSpace) RetireWorker(name string) error
func (*SwarmSpace) ScheduleFor ¶
func (sp *SwarmSpace) ScheduleFor(name string) (agentdef.Schedule, bool)
ScheduleFor returns a member's declared timer schedule, if any. Exported so list_members (internal/swarm/tools) can render each member's crontab.
func (*SwarmSpace) ScheduleInfoFor ¶ added in v1.7.0
func (sp *SwarmSpace) ScheduleInfoFor(name string) (agentdef.Schedule, ScheduleOrigin, bool)
ScheduleInfoFor is ScheduleFor plus provenance — manifest seed vs runtime override — so the leader and the operator can tell at a glance whose hand set a cadence (RP-20 §2.5).
func (*SwarmSpace) SetMemberSchedule ¶
func (sp *SwarmSpace) SetMemberSchedule(name string, sch agentdef.Schedule) error
SetMemberSchedule / ClearMemberSchedule are the tool-facing seam onto the run engine: the leader's schedule_set/schedule_clear tools hold only the space, so they go through here. We read super under sp.mu and release BEFORE delegating — the supervisor methods take s.mu then sp.mu, and holding sp.mu across the call would invert that order and risk deadlock.
func (*SwarmSpace) SharedSkills ¶ added in v1.7.0
func (sp *SwarmSpace) SharedSkills() []agent.Skill
SharedSkills lists the space-shared skill dir (RP-26) fresh from disk — the same source-of-truth-on-disk read as MemberSkills, for the web GET and the skill_publish result. A space without the dir yields an empty list.
func (*SwarmSpace) Shutdown ¶
func (sp *SwarmSpace) Shutdown()
Shutdown tears down every agent (cancelling their background workers) and closes the store. Safe to call on a partially-constructed space.
func (*SwarmSpace) SpaceToday ¶ added in v1.11.0
func (sp *SwarmSpace) SpaceToday() spaceDay
SpaceToday reports the space-wide aggregate for the metrics/health surfaces (CST).
func (*SwarmSpace) SpawnWorker ¶ added in v1.11.0
func (sp *SwarmSpace) SpawnWorker(base, retire string) (string, error)
func (*SwarmSpace) SpawnedInfo ¶ added in v1.11.0
func (sp *SwarmSpace) SpawnedInfo(name string) (SpawnedMeta, bool)
SpawnedInfo reports whether name is an ephemeral member, and its provenance.
func (*SwarmSpace) TaskStaleThreshold ¶ added in v1.7.0
func (sp *SwarmSpace) TaskStaleThreshold() time.Duration
TaskStaleThreshold returns the space's RP-22 task-age reminder line (0 = disabled). Exported so task_list can tag over-threshold tasks as stale.
func (*SwarmSpace) WebhookSecret ¶ added in v1.7.0
func (sp *SwarmSpace) WebhookSecret() string
WebhookSecret returns the space's external-event shared secret ("" = unset, RP-9 loopback trust). Exported for the service's webhook auth check (RP-15).
func (*SwarmSpace) WorkflowStaleCounts ¶ added in v1.7.0
func (sp *SwarmSpace) WorkflowStaleCounts() (tasksStale, mailboxStale int64)
WorkflowStaleCounts reports the space's RP-22 watchdog tallies (stale-task reminders, mailbox-backlog alerts). Exported for the metrics endpoint.
func (*SwarmSpace) WriteBlackboard ¶ added in v1.11.0
func (sp *SwarmSpace) WriteBlackboard(writer, content string) (int, error)
WriteBlackboard replaces the whole board (BB §4: replace, not patch — an LLM writer is far more reliable re-emitting a bounded document than addressing patches into one, and a retried write converges). Oversize content fails loudly naming the cap and the overage — this one check is what bounds the wake-brief token cost across N members × every wake. The write is temp-file + atomic rename, so a reader (wake brief, web, operator cat) never sees a torn file. Empty content clears the board back to dormant.
type ToolSet ¶
ToolSet is the dependency-injection seam SwarmSpace uses to attach the swarm custom tools (send_message, task_*, list_members) to each agent at construction. Implemented by internal/swarm/tools (SPRD-1-7) and passed in, so this package carries no hard dependency on the concrete tools. For returns the agent.Options (typically WithCustomTool(...)) for one member.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package agentdef is the re-callable loader that turns one on-disk agent directory (agents/{main,sub}/{name}/: system_prompt.md, tools/active.yml, tools/deferr.yml, profile.yml, skills/*) into the public SDK objects needed to construct a live agent — a pkg/agent.AgentDefinition plus a *pkg/skill.Registry — using pkg/* only.
|
Package agentdef is the re-callable loader that turns one on-disk agent directory (agents/{main,sub}/{name}/: system_prompt.md, tools/active.yml, tools/deferr.yml, profile.yml, skills/*) into the public SDK objects needed to construct a live agent — a pkg/agent.AgentDefinition plus a *pkg/skill.Registry — using pkg/* only. |
|
Package bus is the per-space message bus and mailboxes.
|
Package bus is the per-space message bus and mailboxes. |
|
Package doctor is the swarm preflight (DR): `evva swarm doctor` runs the whole register-and-run ladder — manifest → member definitions → models / efforts → provider keys → .vero state → (optionally) the live service — and reports ✓/⚠/✗ per probe, so the expensive mistakes that today explode deep inside a member's first run (a typo'd model pin, a missing API key, a ledger written by a newer binary) surface before anything registers.
|
Package doctor is the swarm preflight (DR): `evva swarm doctor` runs the whole register-and-run ladder — manifest → member definitions → models / efforts → provider keys → .vero state → (optionally) the live service — and reports ✓/⚠/✗ per probe, so the expensive mistakes that today explode deep inside a member's first run (a typo'd model pin, a missing API key, a ledger written by a newer binary) surface before anything registers. |
|
Package service is the process-singleton swarm host: the 127.0.0.1:8888 HTTP/WS server that fronts one or more isolated SwarmSpaces.
|
Package service is the process-singleton swarm host: the 127.0.0.1:8888 HTTP/WS server that fronts one or more isolated SwarmSpaces. |
|
Package store is the single per-space data layer: it opens <workdir>/.vero/vero.db (WAL + busy_timeout, foreign keys on), runs migrations, and exposes a sync.RWMutex-wrapped DAO for the task ledger and the message store.
|
Package store is the single per-space data layer: it opens <workdir>/.vero/vero.db (WAL + busy_timeout, foreign keys on), runs migrations, and exposes a sync.RWMutex-wrapped DAO for the task ledger and the message store. |
|
Package tools holds Veronica's swarm-specific custom tools, written against pkg/tools.Tool and attached to agents via pkg/agent.WithCustomTool:
|
Package tools holds Veronica's swarm-specific custom tools, written against pkg/tools.Tool and attached to agents via pkg/agent.WithCustomTool: |
|
tui
|
|
|
app
Package app is the attach TUI (`evva swarm attach`): a Bubble Tea cockpit over one running space.
|
Package app is the attach TUI (`evva swarm attach`): a Bubble Tea cockpit over one running space. |
|
client
Package client is the attach TUI's service client: thin authenticated REST calls for state snapshots and one WebSocket for the live feed + the interactive gate replies — exactly the surface the web console consumes (process model A: the service builds the agents, this client only reads state and POSTs intent).
|
Package client is the attach TUI's service client: thin authenticated REST calls for state snapshots and one WebSocket for the live feed + the interactive gate replies — exactly the surface the web console consumes (process model A: the service builds the agents, this client only reads state and POSTs intent). |
|
reduce
Package reduce is the Go port of the web console's event reducers (web2/src/lib/events.ts) — the semantics that turn the swarm's wire events into renderable state.
|
Package reduce is the Go port of the web console's event reducers (web2/src/lib/events.ts) — the semantics that turn the swarm's wire events into renderable state. |
|
wire
Package wire holds the attach TUI's wire-protocol types: the JSON shapes the swarm service pushes over its WebSocket and /chatlog replay, and the inbound command envelope the socket accepts.
|
Package wire holds the attach TUI's wire-protocol types: the JSON shapes the swarm service pushes over its WebSocket and /chatlog replay, and the inbound command envelope the socket accepts. |
|
Package webapi serves the swarm workstation API: REST snapshots (/api/swarms, /api/swarm/:id, /api/tasks, /api/agents/:name/transcript, /api/messages) and a WebSocket bridge that streams each agent's pkg/event.Event out to the browser, fanned out by (spaceID, AgentID).
|
Package webapi serves the swarm workstation API: REST snapshots (/api/swarms, /api/swarm/:id, /api/tasks, /api/agents/:name/transcript, /api/messages) and a WebSocket bridge that streams each agent's pkg/event.Event out to the browser, fanned out by (spaceID, AgentID). |