subagent

package
v1.6.1 Latest Latest
Warning

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

Go to latest
Published: Jun 30, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package subagent implements parallel child-agent execution.

A subagent is a lightweight agentic loop that runs in its own goroutine with its own working directory (typically a git worktree), its own tool dispatcher, and its own message history. It communicates with the parent through a typed event channel; no shared Bubble Tea state is touched.

Index

Constants

View Source
const (
	// MaxConcurrent is the maximum number of subagents running at once.
	MaxConcurrent = 8

	// DefaultMaxTools is the per-subagent tool-call budget.
	DefaultMaxTools = 50
	// DefaultMaxTokens is the per-subagent token budget (input + output).
	DefaultMaxTokens = 50_000
	// MaxTotalSubagents is the maximum total subagents that can be spawned
	// in a single session to prevent runaway resource consumption.
	MaxTotalSubagents = 50
	// MaxSpawnRate is the maximum number of subagents that can be spawned
	// per minute to prevent rapid resource exhaustion.
	MaxSpawnRate = 10
)
View Source
const (
	ModePrimary  = "primary"
	ModeSubagent = "subagent"
	ModeAll      = "all"
)

Variables

View Source
var ErrMaxConcurrent = errors.New("subagent: max concurrent agents reached")

ErrMaxConcurrent is returned when Spawn cannot acquire a slot.

Functions

func ApplyToolFilter added in v1.3.0

func ApplyToolFilter(dispatcher ToolDispatcher, profile AgentProfile)

ApplyToolFilter removes tools from a ToolDispatcher based on the profile's AllowedTools and DeniedTools. If AllowedTools is non-empty, only listed tools are kept. DeniedTools are then removed from whatever remains.

func BuiltinProfiles added in v1.3.0

func BuiltinProfiles() map[string]AgentProfile

BuiltinProfiles returns the 6 built-in agent profiles.

func Sweep

func Sweep(ctx context.Context, parentWorkDir string) error

Sweep removes every stale m31a/agent-* branch that no longer has a worktree AND removes orphaned worktree directories that are no longer tracked by git. Call this at startup to recover from crashes.

Types

type AgentProfile added in v1.3.0

type AgentProfile struct {
	Name         string
	Description  string
	Mode         string // "primary", "subagent", "all"
	SystemPrompt string
	Model        string // override model ID; "" = inherit parent
	Hidden       bool

	// Tool scoping: AllowedTools is an allowlist (empty = all tools).
	// DeniedTools is a denylist applied after the allowlist.
	AllowedTools []string
	DeniedTools  []string

	// Resource budgets (0 = use manager defaults).
	MaxTools  int
	MaxTokens int
	MaxTurns  int
}

AgentProfile defines a named subagent type with its own system prompt, tool access, model assignment, and resource budgets. Profiles can be built-in or user-defined via config overrides.

func ListSubagentProfiles added in v1.3.0

func ListSubagentProfiles(overrides map[string]config.SubagentProfileConfig) []AgentProfile

ListSubagentProfiles returns all non-hidden profiles where Mode is "subagent" or "all", sorted by name. Used to populate the Agent tool description so the LLM knows which subagent types are available.

func ResolveProfile added in v1.3.0

func ResolveProfile(name string, overrides map[string]config.SubagentProfileConfig) (AgentProfile, bool)

ResolveProfile returns the resolved AgentProfile for a given name. It starts with the built-in default (if one exists) and applies user overrides from the config. If the profile is disabled via config, the second return value is false. If no built-in profile exists for the name, a custom profile is created from the overrides alone (Mode defaults to "all").

type Dependencies

type Dependencies struct {
	WorkDir       string
	Registry      *provider.Registry
	ActiveModel   *types.ModelInfo
	Logger        *slog.Logger
	Worktrees     WorktreeOps                             // optional; nil = always use IsolationDefault
	NewDispatcher DispatcherFactory                       // required: builds a dispatcher per workspace
	Profiles      map[string]config.SubagentProfileConfig // optional: user profile overrides
}

Dependencies are shared resources the Manager needs.

type DispatcherFactory

type DispatcherFactory func(workDir string) (ToolDispatcher, error)

DispatcherFactory builds a fresh ToolDispatcher scoped to a subagent's workspace. This lets the parent wire a real tools.Dispatcher without the subagent package importing internal/tools.

type EventType

type EventType string

EventType discriminates the SubagentEvent variants below.

const (
	EventSpawned   EventType = "spawned"
	EventToolStart EventType = "tool_start"
	EventToolDone  EventType = "tool_done"
	EventTextDelta EventType = "text_delta"
	EventThinking  EventType = "thinking"
	EventDone      EventType = "done"
	EventError     EventType = "error"
	EventCancelled EventType = "cancelled"
)

type GitWorktrees

type GitWorktrees struct {
	// Root is the directory under which worktree subdirectories are created.
	// Defaults to filepath.Join(parentWorkDir, ".m31a-worktrees") when empty.
	Root string
}

GitWorktrees is a WorktreeOps implementation backed by the local git binary via internal/git. It creates worktrees under a sibling directory of the main repo so they share the same .git object store.

func (*GitWorktrees) Create

func (g *GitWorktrees) Create(ctx context.Context, parentWorkDir, agentID, branchSuffix string) (string, error)

Create provisions a git worktree at <root>/<agentID> on branch m31a/agent-<agentID>[-<suffix>], starting from the current HEAD.

func (*GitWorktrees) IsRepo

func (g *GitWorktrees) IsRepo(parentWorkDir string) bool

IsRepo reports whether parentWorkDir lives inside a git repository.

func (*GitWorktrees) Remove

func (g *GitWorktrees) Remove(ctx context.Context, path string) error

Remove deletes the worktree at path and its branch. Idempotent.

type Isolation

type Isolation string

Isolation controls workspace isolation for a subagent.

const (
	// IsolationWorktree creates a git worktree + branch for the subagent.
	IsolationWorktree Isolation = "worktree"
	// IsolationDefault shares the parent working directory.
	IsolationDefault Isolation = "default"
)

type Manager

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

Manager orchestrates subagent lifecycles.

func NewManager

func NewManager(deps Dependencies) *Manager

NewManager creates a Manager ready to accept spawns.

func (*Manager) Cancel

func (m *Manager) Cancel(id string)

Cancel requests cancellation of a specific subagent.

func (*Manager) CancelAll

func (m *Manager) CancelAll()

CancelAll requests cancellation of every running subagent.

func (*Manager) Cleanup

func (m *Manager) Cleanup(ctx context.Context, id string) error

Cleanup removes a subagent's workspace (best-effort) and forgets it.

func (*Manager) Events

func (m *Manager) Events() <-chan SubagentEvent

Events returns the read-only event stream consumed by the TUI or tests.

func (*Manager) Get

func (m *Manager) Get(id string) *Subagent

Get returns a subagent by ID, or nil if unknown.

func (*Manager) List

func (m *Manager) List() []SubagentInfo

List returns a snapshot of every known subagent (any status).

func (*Manager) Shutdown

func (m *Manager) Shutdown(ctx context.Context)

Shutdown cancels all subagents, waits for completion, cleans up their worktrees, and closes the event channel. Safe to call once at application exit.

func (*Manager) Spawn

func (m *Manager) Spawn(parentCtx context.Context, req SpawnRequest) (string, *Subagent, error)

Spawn starts a new subagent and returns its ID. If req.Background is false, Spawn blocks until the subagent finishes.

type SpawnRequest

type SpawnRequest struct {
	Description  string    // short 3-5 word label
	Prompt       string    // full task description
	Name         string    // optional stable name
	SubagentType string    // agent profile name ("explore", "general", etc.); empty = "general"
	Isolation    Isolation // worktree or default
	Background   bool      // if true, return immediately; if false, block until done
	ModelID      string    // empty = inherit parent
	MaxTools     int       // 0 = default (50)
	MaxTokens    int       // 0 = default (50_000)
}

SpawnRequest is the input to Manager.Spawn.

type Status

type Status string

Status describes a subagent's lifecycle state.

const (
	StatusPending Status = "pending"
	StatusRunning Status = "running"
	StatusDone    Status = "done"
	StatusError   Status = "error"
	StatusCancel  Status = "cancelled"
)

type Subagent

type Subagent struct {
	Info    SubagentInfo
	Profile AgentProfile // resolved agent profile
	// contains filtered or unexported fields
}

Subagent is the live handle for a running child agent.

type SubagentEvent

type SubagentEvent struct {
	Type         EventType `json:"type"`
	AgentID      string    `json:"agent_id"`
	Name         string    `json:"name,omitempty"`
	SubagentType string    `json:"subagent_type,omitempty"`
	Timestamp    time.Time `json:"ts"`

	// Spawned
	Worktree string `json:"worktree,omitempty"`

	// ToolStart / ToolDone
	ToolCallID string `json:"tool_call_id,omitempty"`
	ToolName   string `json:"tool_name,omitempty"`
	ToolInput  string `json:"tool_input,omitempty"` // abbreviated for display
	ToolOutput string `json:"tool_output,omitempty"`
	ToolError  string `json:"tool_error,omitempty"`
	DurationMs int64  `json:"duration_ms,omitempty"`

	// TextDelta / Thinking
	Delta string `json:"delta,omitempty"`

	// Done
	Summary    string       `json:"summary,omitempty"`
	ToolCalls  int          `json:"tool_calls,omitempty"`
	InputToks  int          `json:"input_tokens,omitempty"`
	OutputToks int          `json:"output_tokens,omitempty"`
	Usage      *types.Usage `json:"usage,omitempty"`

	// Error
	Error string `json:"error,omitempty"`
}

SubagentEvent is emitted by a running subagent. Consumers (the TUI, the parent tool result collector) read these from Manager.Events().

Tagged union — interpret fields based on Type.

func (SubagentEvent) MarshalJSON

func (e SubagentEvent) MarshalJSON() ([]byte, error)

MarshalJSON guards against nil slices and ensures stable output.

type SubagentInfo

type SubagentInfo struct {
	ID           string    `json:"id"`
	Name         string    `json:"name,omitempty"`
	SubagentType string    `json:"subagent_type,omitempty"`
	Description  string    `json:"description"`
	Status       Status    `json:"status"`
	Isolation    Isolation `json:"isolation"`
	Worktree     string    `json:"worktree,omitempty"`
	ModelID      string    `json:"model_id"`
	StartedAt    time.Time `json:"started_at"`
	FinishedAt   time.Time `json:"finished_at,omitempty"`

	// Counters
	ToolCalls  int `json:"tool_calls"`
	InputToks  int `json:"input_tokens"`
	OutputToks int `json:"output_tokens"`

	// Last activity for the expanded card
	LastToolName   string `json:"last_tool_name,omitempty"`
	LastToolStatus string `json:"last_tool_status,omitempty"` // "running" | "done"
	LastSummary    string `json:"last_summary,omitempty"`
	LastError      string `json:"last_error,omitempty"`
}

SubagentInfo is a snapshot for TUI rendering and /agent listing.

type ToolCallInput

type ToolCallInput struct {
	ID    string          `json:"id"`
	Name  string          `json:"name"`
	Input json.RawMessage `json:"input"`
}

ToolCallInput is the minimum shape the subagent loop needs from a tool dispatcher. It mirrors types.ToolCall/types.ToolResult so the subagent package does not import internal/tools (which would create a cycle).

type ToolCallOutput

type ToolCallOutput struct {
	ToolCallID string `json:"tool_call_id"`
	Output     string `json:"output"`
	Error      string `json:"error,omitempty"`
	DurationMs int64  `json:"duration_ms"`
}

type ToolDescriptor

type ToolDescriptor struct {
	Name        string
	Description string
	// ParameterSchema is the JSON Schema string; empty means "{}".
	ParameterSchema string
}

ToolDescriptor describes a single tool for the provider tool-definition payload. The loop uses these to advertise available tools to the LLM.

type ToolDispatcher

type ToolDispatcher interface {
	Execute(ctx context.Context, call ToolCallInput) (ToolCallOutput, error)
	ListTools() []ToolDescriptor
	UnregisterTool(name string)
	Stop()
}

ToolDispatcher abstracts the subset of tools.Dispatcher that the subagent loop uses. The concrete Dispatcher satisfies this interface without modification.

type WorktreeOps

type WorktreeOps interface {
	Create(ctx context.Context, parentWorkDir, agentID, branchSuffix string) (path string, err error)
	Remove(ctx context.Context, path string) error
	IsRepo(parentWorkDir string) bool
}

WorktreeOps abstracts git worktree operations so the manager can be tested without a real git repo. The production implementation lives in internal/tools/subagent/worktree.go.

Jump to

Keyboard shortcuts

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