14 // subagentNote is the first message of the log of a subagent.
15 subagentNote = "subagent"
17 // subagentNoteEffort follows subagentNote when the subagent has its own
19 subagentNoteEffort = " at effort "
22 // errSubagentClosed tells that the runs of the subagents ended with the loop.
23 var errSubagentClosed = errors.New("the daemon stops, so no subagent starts")
25 // SubagentRuns runs the subagents of one agent in the background. Each run is
26 // a goroutine with its own Chain and its own context, which only Close ends.
27 // The first request of each chain holds the view of the agent at the spawn,
28 // then the state of the turn, then the whole log of the subagent. The report of
29 // the run goes to report: its final answer, or the error of a failed call. At
30 // the stop of the daemon the runs end with no report.
32 // Spawn splits the view with the layout of the agent, which has no lock. Spawn
33 // is a tool, so it runs in the goroutine of the turns, as each split of a turn
35 type SubagentRuns struct {
42 report func(text string, stopped bool)
46 running map[string]*SubagentRun
48 workers sync.WaitGroup
51 // Spawn starts one subagent for each task and returns their names at once. It
52 // waits until the view has no pending summary, and gives the view to each
53 // subagent. A nil effort is the effort of the agent.
54 func (r *SubagentRuns) Spawn(ctx context.Context, tasks []string, effort *string) (string, error) {
56 return "", errors.New("no tasks")
64 return "", errSubagentClosed
68 runEffort := r.agent.config.Effort
69 if effort != nil && !slices.Contains(promptEfforts, *effort) {
70 return "", fmt.Errorf("no effort %q: one of %s", *effort, strings.Join(promptEfforts, ", "))
73 if effort != nil && *effort != runEffort {
74 note += subagentNoteEffort + *effort
78 err := r.memory.Settle(ctx)
80 return "", fmt.Errorf("wait for the summaries: %w", err)
83 view, err := r.layout.Split(r.memory.ViewLines(nil))
88 names := make([]string, 0, len(tasks))
89 for _, task := range tasks {
90 name, err := r.create(ctx, note, task, view, runEffort)
91 if err != nil && len(names) > 0 {
92 return "", fmt.Errorf("started %s, then: %w", strings.Join(names, ", "), err)
99 names = append(names, name)
102 return "Started " + strings.Join(names, ", ") + ". Each one's report will reach you as a message when it finishes.", nil
105 // create makes a new subagent with its note, its task, and its work directory,
106 // and starts its run.
107 func (r *SubagentRuns) create(ctx context.Context, note, task string, view []string, effort string) (string, error) {
108 subagent, err := r.subagents.Create(ctx)
113 for _, message := range [][2]string{{"note", note}, {"user", task}} {
114 _, err = subagent.Append(message[0], message[1])
120 err = subagent.Flush(ctx)
125 err = os.MkdirAll(subagent.WorkDir(), 0o700)
127 return "", fmt.Errorf("create work directory: %w", err)
130 return subagent.name, r.start(ctx, &SubagentRun{
137 // start runs run in the background. Its context keeps the values of ctx, but
138 // only Close ends it.
139 func (r *SubagentRuns) start(ctx context.Context, run *SubagentRun) error {
140 ctx, run.cancel = context.WithCancel(context.WithoutCancel(ctx))
147 return errSubagentClosed
150 r.running[run.subagent.name] = run
156 Text: run.subagent.name,
164 // run runs the chain of run and reports its end, unless Close ended it.
165 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
166 defer r.workers.Done()
169 reply := r.chain(ctx, run)
172 delete(r.running, run.subagent.name)
175 if ctx.Err() != nil {
179 r.report(subagentReport(run.subagent.name, []string{reply}), false)
182 // chain runs one request chain of run and returns its final answer, or the
183 // error of the chain after "failed: ".
184 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) string {
185 instructions, err := r.agent.SubagentPrompt()
187 return "failed: " + err.Error()
190 log, err := run.subagent.Show()
192 return "failed: " + err.Error()
195 input := make([]any, 0, len(run.view)+2)
196 for _, text := range run.view {
197 input = append(input, modelUserMessage(text))
200 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
206 subagents: r.subagents,
207 workdir: run.subagent.WorkDir(),
209 request: ModelRequest{
210 Model: r.agent.config.Model,
212 Instructions: instructions,
213 Tools: promptSubagentTools(),
214 Agent: run.subagent.name,
216 take: func() []Message {
221 reply, err := chain.Run(ctx, input)
223 return "failed: " + err.Error()
229 // Close ends every run and waits for them. No subagent starts after Close.
230 func (r *SubagentRuns) Close() {
233 for _, run := range r.running {
241 // SubagentRun is one run of a subagent: the view that its requests get, its
242 // effort, and the end of its context.
243 type SubagentRun struct {
247 cancel context.CancelFunc
250 // subagentReport returns the report of the subagent name with its replies: the
251 // name in brackets, a new line, the replies, and the zoom that gives its whole
253 func subagentReport(name string, replies []string) string {
254 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"