Files
zero/internal/swarm/definition.go
KRATOS 9ddef0e957 feat(swarm): multi-agent swarm over the specialist sub-agent (#207)
* feat(swarm): multi-agent swarm over the specialist sub-agent

Add internal/swarm so an orchestrator can spawn and coordinate MULTIPLE
specialist members concurrently under one team, communicate via per-agent
mailboxes, hand work off, and adopt orphaned tasks. Additive and opt-in: the
single Task tool is unchanged; swarm tools only act when invoked.

- Registry/Definition: agent roster (built-in teammate/subagent + user-defined).
- Mailbox: per-team/per-agent JSON inbox, owner-only 0600, file-locked with
  stale-break, size-capped, path-confined, and malformed-fails-closed.
- Coordinator: in-memory task registry with terminal-state guards and stable
  per-agent colors.
- Team: bounded concurrent members per team with a FIFO overflow queue that
  drains one-per-slot.
- Lifecycle: spawn / handoff / orphan-adoption with bounded relaunch on
  temporary failures; members run under the Swarm's base context so they
  outlive the spawn call.
- Tools: swarm_spawn, swarm_send, swarm_inbox, swarm_status, swarm_handoff,
  swarm_collect.
- Wiring: each member is launched through specialist.Executor and inherits the
  orchestrator's model + permission mode (never widened) and runs under the same
  sandbox/policy. Mailbox state lives under the workspace so writes fall within
  the sandbox write rules. Swarm lifetime is paired with the specialist runtime.

Scheduling (cron/wakeup) and daemon-pool-backed members are deferred follow-ups.

* swarm: address CodeRabbit review (7 findings)

- coordinator: SetStatus rejects unknown TaskStatus values (fail closed) so a
  task can never enter a state Summarize/lifecycle don't reason about.
- permission inheritance ENFORCED end-to-end (was resolved but discarded):
  thread PermissionMode through specialist.TaskRunOptions -> BuildArgs/
  BuildResumeArgs. A headless specialist child now runs "--auto high" (unsafe)
  ONLY when the parent is unsafe or no mode is given (preserves the Task tool's
  behavior); any other parent mode yields non-unsafe "--auto low", so a member
  never gains more authority than its orchestrator. Also fixed the swarm
  permission vocabulary to the real agent runtime modes (ask/spec-draft/auto/
  unsafe) instead of TUI display names, and the launcher substitutes "auto" for
  an empty mode so a member can't fall into the empty-means-high path.
- mailbox: symlink-aware confinement (EvalSymlinks) rejects an inbox dir that
  resolves outside BaseDir; dirs tightened to 0700 even if pre-existing with
  broad perms; lock release is ownership-token-aware so a stale-broken writer
  can't delete a newer writer's lock (split-brain).
- member: recover panics inside a member goroutine -> fails that member only,
  never crashes the orchestrator.
- tools: collapse() truncates on rune boundaries (no invalid UTF-8).

Tests added for every fix. Existing specialist behavior is unchanged (empty
PermissionMode keeps --auto high). Gates: gofmt/vet/build (host+linux)/test
-race/staticcheck(no new)/govulncheck(0)/deadcode(no new) all pass.

* swarm: address review round 2 (Windows lock + non-blocking notes)

Blocking (Vasanth): mailbox acquireLock now treats Windows ERROR_ACCESS_DENIED
/ ERROR_SHARING_VIOLATION as lock-contended (retry/wait) instead of a fatal
error, via a platform-aware isLockContended (lock_windows.go / lock_other.go).
A contended lock under Windows file-sharing semantics no longer fails a
send/read spuriously. TestMailboxConcurrentSends raised to 200 goroutines with a
10s lock timeout and now asserts zero send failures (regression guard).

Non-blocking notes addressed:
- Removed third-party reference-project names from doc comments (clean-room +
  provider-neutral); the Go code is original.
- Documented the inbox-file size bound's ×2 (pretty-print overhead) constant.
- Documented Send's O(N) rewrite cost + when to switch to append-only.
- permissionRank: spec-draft now ranks below ask (it never widens authority).
- Documented watch's per-member-id retry binding (Member.ID == MemberSpec.ID).
- handoff Safety.Reason now also mentions the inbox write.

Gates: gofmt · vet · build (host+linux+windows) · test -race · staticcheck
(no new) · govulncheck (0) · deadcode (no new) all pass.
2026-06-15 15:10:21 +05:30

134 lines
4.4 KiB
Go

// Package swarm adds a multi-agent SWARM on top of zero's single sub-agent
// mechanism (internal/specialist): an orchestrator can spawn and coordinate
// MULTIPLE specialist members that run concurrently, communicate via per-agent
// mailboxes, and hand work off. It composes internal/specialist (to launch each
// member), internal/daemon (pooled workers when a daemon is running) and
// internal/background. Everything is additive and opt-in — the existing single
// "task" tool is unchanged; swarm tools are only active when invoked.
//
// Every member runs under the SAME sandbox + risk/autonomy policy as the
// orchestrator: the swarm never grants a member more authority than its parent.
package swarm
import (
"errors"
"sort"
"strings"
"sync"
)
// ErrUnknownAgentType is returned when a definition lookup misses.
var ErrUnknownAgentType = errors.New("swarm: unknown agent type")
// modelInherit is the sentinel meaning "use the orchestrator's model" — members
// never silently pick a different (e.g. more capable) model.
const modelInherit = "inherit"
// Definition is one entry in the agent roster: how a member of a given type
// behaves (agent type, when to use it, model — "inherit" by default —,
// permission mode, and a system-prompt builder).
type Definition struct {
AgentType string
WhenToUse string
Model string // "inherit" => use the orchestrator's model
PermissionMode string
// SystemPrompt returns the member's system prompt for the given task context.
// It is a func so a definition can fold the task briefing in.
SystemPrompt func(ctx PromptContext) string
}
// PromptContext is the minimal context handed to a definition's SystemPrompt.
type PromptContext struct {
Team string
Task string
}
// Registry is the agent roster: definitions looked up by agent type. Built-ins
// are seeded at construction; user-defined definitions extend or override them.
type Registry struct {
mu sync.RWMutex
defs map[string]Definition
}
// NewRegistry builds a registry seeded with the built-in roster.
func NewRegistry() *Registry {
r := &Registry{defs: map[string]Definition{}}
for _, def := range builtinDefinitions() {
r.defs[def.AgentType] = def
}
return r
}
// Register adds or overrides a definition (user-defined agents extend the
// built-ins). An empty AgentType is rejected.
func (r *Registry) Register(def Definition) error {
agentType := strings.TrimSpace(def.AgentType)
if agentType == "" {
return errors.New("swarm: definition requires an agentType")
}
if def.Model == "" {
def.Model = modelInherit
}
def.AgentType = agentType
r.mu.Lock()
r.defs[agentType] = def
r.mu.Unlock()
return nil
}
// Lookup returns the definition for agentType, or ErrUnknownAgentType.
func (r *Registry) Lookup(agentType string) (Definition, error) {
r.mu.RLock()
def, ok := r.defs[strings.TrimSpace(agentType)]
r.mu.RUnlock()
if !ok {
return Definition{}, ErrUnknownAgentType
}
return def, nil
}
// AgentTypes returns the registered agent types, sorted, for status/help.
func (r *Registry) AgentTypes() []string {
r.mu.RLock()
types := make([]string, 0, len(r.defs))
for t := range r.defs {
types = append(types, t)
}
r.mu.RUnlock()
sort.Strings(types)
return types
}
// builtinDefinitions returns the seeded roster (teammate, subagent) — kept
// minimal; user-defined agents extend it via Register.
func builtinDefinitions() []Definition {
return []Definition{
{
AgentType: "teammate",
WhenToUse: "In-process teammate for parallel task execution; delegate work to run alongside the orchestrator.",
Model: modelInherit,
// Empty PermissionMode => inherit the orchestrator's mode (never widened).
SystemPrompt: func(ctx PromptContext) string {
return "You are a teammate agent collaborating with an orchestrator on team " +
displayTeam(ctx.Team) + ". Complete your assigned task and report results.\n\nTask: " + ctx.Task
},
},
{
AgentType: "subagent",
WhenToUse: "General-purpose subagent for an isolated, delegated task; starts with zero prior context.",
Model: modelInherit,
SystemPrompt: func(ctx PromptContext) string {
return "You are a subagent spawned to complete a specific task. You start with zero context — " +
"the briefing below is all you know.\n\nTask: " + ctx.Task
},
},
}
}
func displayTeam(team string) string {
if strings.TrimSpace(team) == "" {
return "default"
}
return team
}