Files
esengine ad2c963475 refactor(delegation): replace the task family with spawn_agents
Rewrite internal/runtime/delegation around one Runner and one tool.
spawn_agents runs 1 to 8 independent tasks in parallel, each in its own
session with three base tools (read_file, write_file, bash); plan mode and
read-only skills keep a read-only child. A child's full answer is archived;
the caller gets the opening, a reference, and the files the child read and
wrote, and pages the rest through read_subagent_result.

Removed with it: task, read_only_task, parallel_tasks, fleet,
list_subagents, background sub-tasks, steering and its limits and settings
UI, continue_from, write_paths claims, isolated worktrees, best-of-n, the
run graph and the runtime-resume benchmark. Stale writes by children stay
refused through the per-agent file views.

The use_capability description no longer names task:fleet, so the stable
prefix and its golden files change once.
2026-11-02 08:52:45 -08:00

123 lines
4.6 KiB
Go

package boot
import (
"context"
"fmt"
"strings"
"reasonix/internal/contract/config"
"reasonix/internal/contract/provider"
"reasonix/internal/contract/tool"
"reasonix/internal/ext/skill"
"reasonix/internal/platform/environment"
"reasonix/internal/runtime/agent"
"reasonix/internal/runtime/delegation"
"reasonix/internal/runtime/usecap"
"reasonix/internal/runtime/writeclaim"
)
// skillSubagents runs a skill inside its own sub-agent loop: an isolated turn
// with the skill body as system prompt, a tool set scoped to the skill's
// allowed tools minus recursive meta-tools, and an optional per-skill model.
// capRuntime is filled in after the capability runtime exists; nothing calls a
// runner before then, because the tools holding them only fire on a model request.
type skillSubagents struct {
root string
cfg *config.Config
registry *tool.Registry
// tasks is the one runner every delegated execution goes through. A skill
// that kept its own would be a second owner of admission, of the child's
// loop and of how a run ended, and the three drifted apart once already.
runner *delegation.Runner
scheduler *writeclaim.SubagentScheduler
provider provider.Provider
entry *config.ProviderEntry
capRuntime *usecap.MCPCapabilityRuntime
maxDepth int
maxSteps int
resolveProvider func(modelRef, effort string) (provider.Provider, *provider.Pricing, int, error)
identity func(modelRef, effort string) (string, string)
inheritedFor func(modelRef string) string
runOptions func(ctx context.Context, steps int, price *provider.Pricing, ctxWin, childDepth int) agent.Options
}
// systemPrompt is the child's whole prefix: its body, then the workspace facts
// every child needs and none can discover for free. One accessor, so a fresh
// run and a continued one hash the same prompt.
func (r *skillSubagents) systemPrompt(sk skill.Skill) string {
body := strings.TrimSpace(sk.Body)
if body == "" {
body = delegation.DefaultReadOnlyTaskSystemPrompt
}
return body + r.workspaceFacts()
}
// workspaceFacts is the slice of the parent's Environment section a child must
// be told rather than left to find out: unsaid, it reaches for `git diff` in a
// workspace that is no repository and burns a round finding out. Filesystem-
// derived and stable, so it stays in the child's cached prefix.
func (r *skillSubagents) workspaceFacts() string {
vcs := environment.WorkspaceVCS(r.root)
if vcs == "" {
vcs = "none (not a repository)"
}
return "\n\n## Workspace\n\n- Version control: " + vcs + "\n"
}
// halfSteps gives a child half the parent's step budget, never below five:
// enough to finish a small job, not enough to spend the parent's turn.
func (r *skillSubagents) halfSteps() int {
steps := r.maxSteps
if steps > 0 {
if steps /= 2; steps < 5 {
steps = 5
}
}
return steps
}
// stepsFor is the tighter of the default budget and the run's own cap; zero on
// either side means that side sets no limit.
func (r *skillSubagents) stepsFor(runOpts skill.SubagentRunOptions) int {
steps := r.halfSteps()
if runOpts.MaxSteps > 0 && (steps <= 0 || runOpts.MaxSteps < steps) {
return runOpts.MaxSteps
}
return steps
}
// resolveModel picks the child's provider: the parent's unless the skill or
// config names one of its own, or the images the child is to look at need the
// vision role.
func (r *skillSubagents) runReadOnly(sctx context.Context, sk skill.Skill, task string, runOpts skill.SubagentRunOptions) (string, error) {
return r.runWith(sctx, sk, task, runOpts, true)
}
func (r *skillSubagents) run(sctx context.Context, sk skill.Skill, task string, runOpts skill.SubagentRunOptions) (string, error) {
return r.runWith(sctx, sk, task, runOpts, false)
}
func (r *skillSubagents) runWith(sctx context.Context, sk skill.Skill, task string, runOpts skill.SubagentRunOptions, readOnly bool) (string, error) {
if strings.TrimSpace(runOpts.ContinueFrom) != "" || strings.TrimSpace(runOpts.ForkFrom) != "" {
return "", fmt.Errorf("subagent skill %q does not support continue_from or fork_from", sk.Name)
}
if runOpts.MaxSteps > 0 {
sctx = agent.WithRunStepLimit(sctx, r.stepsFor(runOpts), sk.Name+" rounds")
}
sk = skill.WithCodeGraphTools(sk, skill.CodeGraphReadTools(r.registry))
out, err := r.runner.Run(sctx, delegation.RunSpec{
Title: sk.Name,
Task: task,
Kind: "skill",
Name: sk.Name,
SystemPrompt: r.systemPrompt(sk),
Tools: sk.AllowedTools,
Model: subagentModelRef(r.cfg, sk),
Effort: subagentEffortRef(r.cfg, sk, r.inheritedFor),
MaxSteps: r.stepsFor(runOpts),
ReadOnly: readOnly || sk.ReadOnly,
})
return out.Answer, err
}