mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 15:02:21 -10:00
* 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.
134 lines
4.4 KiB
Go
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
|
|
}
|