factory

package
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 7, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package factory supervises chat-spawned subagents. The public model-facing surface is the agent tool: the coordinator writes a self-contained prompt, the supervisor enforces depth/fanout/agent/time budgets, and the UI receives an event tree for live progress. Legacy project-local step resolution remains internal support code, but default named steps are no longer advertised.

Division of labor:

agents decide    — planning, sequencing, triage, judgment
code  constrains — depth, fanout, agent count, time
code  observes   — events, attribution, rendering
code  never interprets — no state machines over agent outcomes

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDepth    = errors.New("denied: max depth reached — do the work in this step or narrow it")
	ErrChildren = errors.New("denied: max children for this step")
	ErrAgents   = errors.New("denied: tree-wide agent cap reached")
)

Functions

func DefaultStepPaths

func DefaultStepPaths(dir string) []string

DefaultStepPaths returns the standard search path for a session rooted at dir.

func RenderTree

func RenderTree(nodes []NodeView) string

RenderTree is a pure function: Snapshot -> one text frame. Stdlib only, so it drops into any view layer with one call.

● agent  work the backlog: owainlewis/app                  3m12s
├─ ● agent  #12 invite teammate by email                    2m07s
│  │  bash: just test
│  └─ ✓ agent  PR #34 vs acceptance criteria                   31s
└─ ✗ agent  #13 rate limiting (timeout after 10m)           10m00s

Types

type AgentEvent

type AgentEvent struct {
	Kind string `json:"kind"` // "start" | "tool" | "text" | "error" | "usage" | "done" | "fail"
	Body string `json:"body,omitempty"`
}

AgentEvent is one observation from a running agent. Lifecycle kinds frame each node: "start" when it registers, then "done" (completed) or "fail" (errored, timed out, denied) exactly once at the end. Everything in between ("tool", "text", "error", "usage") is status.

type AgentRunner

type AgentRunner struct {
	Provider     llm.Provider
	DefaultModel string
	Root         string // workspace root; bounds file tools via permission policy
	BashTimeout  time.Duration

	// Mode is the permission mode child agents run under. Steps execute
	// autonomously (there is no approver inside a step), so "ask" cannot be
	// honored mid-step; but "readonly" must propagate — a readonly session
	// delegating a step must not gain write access through the side door.
	// Empty defaults to trusted (the standalone step CLI case).
	Mode permission.Mode

	// Sup is set after NewSupervisor. It is used only when a step explicitly
	// opts into the agent tool; dynamic chat subagents do not get nested
	// delegation by default.
	Sup *Supervisor
}

AgentRunner runs agent steps on neo's core agent loop. Each step gets a fresh agent (amnesiac by design) with a registry filtered to the step's frontmatter tool list — role enforcement by construction, not prose.

func (*AgentRunner) RunAgentStep

func (r *AgentRunner) RunAgentStep(ctx context.Context, step Step, dir, input string, nodeID int, events chan<- AgentEvent) (string, error)

type AgentTool

type AgentTool struct {
	Sup        *Supervisor
	CallerNode int
	Dir        string
}

AgentTool exposes subagent delegation to the model through neo's tool registry. CallerNode is bound per agent-loop instance so children attribute correctly.

func (AgentTool) Name

func (AgentTool) Name() string

func (AgentTool) Run

func (t AgentTool) Run(ctx context.Context, input map[string]any) (string, error)

func (AgentTool) Spec

func (AgentTool) Spec() llm.ToolSpec

type Budget

type Budget struct {
	MaxDepth      int           // coordinator=0, children=1, grandchildren=2
	MaxChildren   int           // per node
	MaxAgents     int           // tree-wide cap on agent steps
	MaxWall       time.Duration // per agent step
	ScriptTimeout time.Duration // per script step
}

Budget is the cage. Enforced by the runtime regardless of what any agent asks for. Agent-count is tree-wide; the rest are per node.

func DefaultBudget

func DefaultBudget() Budget

DefaultBudget is a short leash suitable for early supervised runs.

type Console

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

Console renders the supervisor's tree to a terminal as a live, in-place frame (redrawn on a ticker) and tees every event to events.jsonl. It is a consumer of the event stream only — agents never wait on it (the supervisor's send is non-blocking by construction).

func NewConsole

func NewConsole(sup *Supervisor, w io.Writer) *Console

NewConsole prepares a console over w. Live in-place redraw is enabled only when w is the process's terminal.

func (*Console) Repaint

func (c *Console) Repaint()

Repaint draws the current tree. On a TTY it rewrites the previous frame in place; otherwise it is a no-op (the event log lines are the output).

func (*Console) Watch

func (c *Console) Watch(jsonlPath string, interval time.Duration)

Watch consumes events until the channel closes: tees them to jsonlPath (best-effort; "" disables) and repaints the tree at most every interval. Call it in a goroutine; it returns when the supervisor closes Events.

type Event

type Event struct {
	At     time.Time  `json:"at"`
	Node   int        `json:"node"`
	Parent int        `json:"parent,omitempty"`
	Depth  int        `json:"depth,omitempty"`
	Step   string     `json:"step"`
	Task   string     `json:"task,omitempty"`
	Ev     AgentEvent `json:"ev"`
}

Event is the attributed stream: every agent event tagged with its node and the node's place in the tree, so a consumer can reconstruct the whole hierarchy from the stream alone. One channel; the UI is a fold over it; events.jsonl is a tee of it.

type Node

type Node struct {
	ID      int
	Parent  int // 0 = the root's virtual parent
	Step    string
	Kind    string // "agent" | "script"
	Task    string // clipped, for the UI
	Depth   int
	Started time.Time
	// contains filtered or unexported fields
}

Node is one subagent/script execution in the tree.

type NodeView

type NodeView struct {
	ID, Parent, Depth int
	Step, Kind, Task  string
	Done              bool
	Err, LastLine     string
	Elapsed           time.Duration
}

NodeView is an immutable snapshot of a Node for rendering.

type Resolver

type Resolver struct {
	Paths []string
}

Resolver locates steps by bare name. Search order: each path in Paths (project ./steps first, then ~/.neo/steps), then the embedded defaults. In a directory, <name>.md (agent step) wins over an executable <name> (script step).

func (Resolver) Catalog

func (r Resolver) Catalog() []Step

Catalog enumerates every available step across the search path and embedded defaults, deduplicated by name (earlier paths win, mirroring Resolve precedence), sorted by name. Used to advertise steps in the chat system prompt; scripts carry name only.

func (Resolver) List

func (r Resolver) List() []string

List enumerates available step names, deduplicated, sorted.

func (Resolver) Resolve

func (r Resolver) Resolve(name string) (Step, error)

Resolve finds a step by bare name. Names are bare identifiers — no path separators or dots — so the model cannot traverse the filesystem. A step's name is its frontmatter `name`, falling back to its filename, so steps/step1.md with `name: first` is invoked as "first".

type Step

type Step struct {
	Name string
	Kind string // "agent" | "script"
	Path string // executable path for scripts; source path for agent steps ("" if embedded)

	// Agent-step fields, parsed from YAML frontmatter.
	Description string   // one-line summary, surfaced in the chat catalog
	Prompt      string   // system prompt body
	Tools       []string // allowed tool names; empty = read-only default (bash, read_file, grep, glob)
	Model       string   // pinned model; empty = inherit the session default
	MaxTurns    int      // agent loop turn cap; 0 = factory default
}

Step is a resolved step definition. Agent steps carry a prompt (markdown body) plus frontmatter restrictions; script steps carry the executable path.

type StepAgent

type StepAgent interface {
	RunAgentStep(ctx context.Context, step Step, dir, input string, nodeID int, events chan<- AgentEvent) (string, error)
}

StepAgent runs a resolved agent prompt against an input in dir, streaming events, returning the final message. NodeID identifies the execution so child agent calls attribute correctly.

type StepResult

type StepResult struct {
	Ok     bool   `json:"ok"`
	Output string `json:"output"`
	Kind   string `json:"kind"`
	Took   string `json:"took"`
}

StepResult is the uniform envelope returned to the calling agent. Ok means "the step completed", NOT "the answer is yes": scripts conflate the two via exit code (fine — they hold invariants); for agent steps the caller judges the output's content itself. Collapsing agent judgment into a boolean is the classic mistake this design exists to avoid.

type Supervisor

type Supervisor struct {
	Events chan Event
	// contains filtered or unexported fields
}

Supervisor enforces budgets, owns the node tree, and tags every agent event with its node into one stream. It never interprets agent decisions.

func NewSupervisor

func NewSupervisor(agent StepAgent, b Budget, resolver Resolver) *Supervisor

func (*Supervisor) Run

func (s *Supervisor) Run(ctx context.Context, dir, rootStep, goal string) (string, error)

Run starts the root step with the user's goal and blocks until the tree finishes. Close(Events) afterwards is the caller's choice.

func (*Supervisor) RunAgentPrompt

func (s *Supervisor) RunAgentPrompt(ctx context.Context, caller int, dir, prompt string) StepResult

RunAgentPrompt starts a fresh subagent with a self-contained prompt. This is the chat-native delegation path: no named/static step file is involved, but the execution still participates in the supervisor tree and budgets.

func (*Supervisor) RunStep

func (s *Supervisor) RunStep(ctx context.Context, caller int, dir, name, input string) StepResult

RunStep resolves and executes a named legacy step on behalf of caller node. Both kinds become nodes in the tree (so the UI shows everything); only agent steps consume the agent budget. Denials and failures return as results with the reason — the calling agent reads why and re-plans.

func (*Supervisor) Snapshot

func (s *Supervisor) Snapshot() []NodeView

Snapshot returns the tree for rendering; the UI calls it per frame.

Jump to

Keyboard shortcuts

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