package subagent
import (
"context"
"fmt"
"strings"
"codey/internal/tools"
)
// spawnArgs is what the model sends to start an agent.
type spawnArgs struct {
Preset string `json:"preset" jsonschema:"description=Which kind of agent to start. See the list in this tool's description."`
Task string `json:"task" jsonschema:"description=The complete brief for the agent: what to do and what to report back. It cannot ask you questions and it cannot see this conversation, so everything it needs has to be in here — the paths you already know, what you have tried, what the answer should look like."`
Effort string `json:"effort,omitempty" jsonschema:"description=How hard it should think: low, medium, high, xhigh or max. Low is right for mechanical work — searching, listing, reading something back; reach higher when the answer has to be right. Leave it out to use this session's level. Some models take no effort level at all, and then this is ignored and the answer says so."`
Model string `json:"model,omitempty" jsonschema:"description=Which model to run it on. Pick one that suits the job — a small fast model for mechanical search, a strong one for design or code that has to be right. Leave it out to use this session's model, which is the safe answer when you do not know what this endpoint serves."`
}
type waitArgs struct {
ID string `json:"id" jsonschema:"description=The agent id returned by spawn_agent, such as a1."`
}
// Tools are the tools the parent agent gets. Subagents are built from the
// caller's own registry factory and never see these, which is what stops an
// agent from spawning agents that spawn agents.
func (s *Supervisor) Tools() []tools.Tool {
return []tools.Tool{
tools.New("spawn_agent", spawnDescription(), func(_ context.Context, in spawnArgs) (string, error) {
// The context is deliberately ignored: it belongs to the turn that
// is spawning, and the agent has to outlive that turn. Children
// descend from the supervisor's own context instead.
id, err := s.Spawn(Spec{Preset: in.Preset, Task: in.Task, Model: in.Model, Effort: in.Effort})
if err != nil {
return "", err
}
info, _ := s.Info(id)
var note string
switch {
case info.Effort != "":
note = " at " + info.Effort + " effort"
case in.Effort != "":
// Asked for, and dropped: that model takes no effort level,
// and letting the parent believe otherwise would be worse
// than refusing the spawn.
note = fmt.Sprintf(" — %s takes no effort level, so %q was not applied", info.Model, in.Effort)
}
return fmt.Sprintf("started %s (%s) on %s%s, status %s. It runs in the background: carry on, "+
"and its report will arrive as a message when it finishes. Call agent_wait(%q) only if "+
"you cannot do anything useful until then.", id, info.Preset, info.Model, note, info.Status, id), nil
}),
tools.New("agent_wait", "Block until a subagent finishes and return its report. Use this only when "+
"you genuinely cannot proceed without the answer — a finished agent's report reaches you on its "+
"own, and waiting spends this turn doing nothing.",
func(ctx context.Context, in waitArgs) (string, error) {
info, err := s.Wait(ctx, in.ID)
if err != nil {
return "", err
}
// Whoever hands the report over claims it. Losing the claim
// means the front end has already put it into the
// conversation, so all this has to do is point at it.
if !s.ClaimReport(info.ID) {
return info.AlreadyDelivered(), nil
}
return info.Notification(), nil
}),
}
}
// spawnDescription is built from the presets so the two cannot drift apart.
func spawnDescription() string {
var b strings.Builder
b.WriteString(`Start a subagent to do a piece of work on its own, in the background.
A subagent has its own context window and its own conversation: it sees the
task you give it and nothing else, and you see its final report and nothing
else. That is what makes it worth using — a search that would fill your context
with file listings fills its instead, and comes back a paragraph long. Spawn
one for work that is self-contained and describable in writing; do the rest
yourself, because a task you have to explain at length is quicker to do.
Several can run at once, and you can keep working while they do. Their reports
arrive as messages when they finish. Subagents cannot spawn subagents, and each
spawn is shown to the user to approve before it starts.
You choose the model and the effort. The preset says what kind of job it is;
what to spend on it is your call, and those two are what decide what a subagent
costs.
Use the newest model listed in your system prompt unless the user asked for a
different one, or that model has already failed for you this session. Omit the
model and it runs on this session's, which is the right answer when nothing was
listed; an id this endpoint does not serve fails that agent with the API's own
error before its first turn.
Effort is the other half: low for mechanical work, higher when the answer has
to be right rather than quick. Omit it and the agent runs at this session's
level. Not every model has one — Haiku 4.5 takes none at all — and where it
does not, the level is dropped and the answer tells you so.
The presets:`)
for _, p := range presets {
fmt.Fprintf(&b, "\n %s — %s", p.Name, p.Description)
}
return b.String()
}