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 context, which only Close ends. A run is one or more
27 // request chains. The first request of each chain holds the view of the agent
28 // at the start of the run, then the state of the turn, then the whole log of
29 // the subagent. A message of Tell that comes during a chain joins its next
30 // request after the tool results. One that comes after the final answer starts
31 // one more chain, which reads the log again. The report of the run goes to
32 // report: the final answer of each chain, or the error of a failed call. At the
33 // stop of the daemon the runs end with no report.
35 // Spawn and Tell split the view with the layout of the agent, which has no
36 // lock. They are tools, so they run in the goroutine of the turns, as each
37 // split of a turn does.
38 type SubagentRuns struct {
45 report func(text string, stopped bool)
49 running map[string]*SubagentRun
51 workers sync.WaitGroup
54 // Spawn starts one subagent for each task and returns their names at once. It
55 // waits until the view has no pending summary, and gives the view to each
56 // subagent. A nil effort is the effort of the agent.
57 func (r *SubagentRuns) Spawn(ctx context.Context, tasks []string, effort *string) (string, error) {
59 return "", errors.New("no tasks")
67 return "", errSubagentClosed
71 runEffort := r.agent.config.Effort
72 if effort != nil && !slices.Contains(promptEfforts, *effort) {
73 return "", fmt.Errorf("no effort %q: one of %s", *effort, strings.Join(promptEfforts, ", "))
76 if effort != nil && *effort != runEffort {
77 note += subagentNoteEffort + *effort
81 err := r.memory.Settle(ctx)
83 return "", fmt.Errorf("wait for the summaries: %w", err)
86 view, err := r.layout.Split(r.memory.ViewLines(nil))
91 names := make([]string, 0, len(tasks))
92 for _, task := range tasks {
93 name, err := r.create(ctx, note, task, view, runEffort)
94 if err != nil && len(names) > 0 {
95 return "", fmt.Errorf("started %s, then: %w", strings.Join(names, ", "), err)
102 names = append(names, name)
105 return "Started " + strings.Join(names, ", ") + ". Each one's report will reach you as a message when it finishes.", nil
108 // Tell sends message to the subagent name. A running subagent reads it between
109 // its tool calls, and its report answers it. A finished subagent goes on from
110 // its log with a new run, with the view of now and the effort of its note.
111 func (r *SubagentRuns) Tell(ctx context.Context, name, message string) (string, error) {
113 run, found := r.running[name]
115 defer r.mutex.Unlock()
117 told, err := run.subagent.Append("user", message)
122 run.pending = append(run.pending, told)
124 return "Sent. " + name + " reads it between its tool calls, and its report answers it.", nil
131 return "", errSubagentClosed
134 subagent, err := r.subagents.Get(ctx, name)
139 effort, err := r.effort(subagent)
144 err = r.memory.Settle(ctx)
146 return "", fmt.Errorf("wait for the summaries: %w", err)
149 view, err := r.layout.Split(r.memory.ViewLines(nil))
154 _, err = subagent.Append("user", message)
156 err = subagent.Flush(ctx)
163 err = r.start(ctx, &SubagentRun{
172 return "Sent. " + name + "'s reply will reach you as a message.", nil
175 // Close ends every run and waits for them. No subagent starts after Close.
176 func (r *SubagentRuns) Close() {
179 for _, run := range r.running {
187 // create makes a new subagent with its note, its task, and its work directory,
188 // and starts its run.
189 func (r *SubagentRuns) create(ctx context.Context, note, task string, view []string, effort string) (string, error) {
190 subagent, err := r.subagents.Create(ctx)
195 for _, message := range [][2]string{{"note", note}, {"user", task}} {
196 _, err = subagent.Append(message[0], message[1])
202 err = subagent.Flush(ctx)
207 err = os.MkdirAll(subagent.WorkDir(), 0o700)
209 return "", fmt.Errorf("create work directory: %w", err)
212 return subagent.name, r.start(ctx, &SubagentRun{
219 // effort returns the effort of subagent from the note at the start of its log:
220 // the effort of the agent, or the effort that the note names.
221 func (r *SubagentRuns) effort(subagent *Subagent) (string, error) {
222 note, err := subagent.First()
227 effort, own := strings.CutPrefix(note.Text, subagentNote+subagentNoteEffort)
229 case note.Kind != "note":
232 case note.Text == subagentNote:
233 return r.agent.config.Effort, nil
236 return "", fmt.Errorf("the log of %s does not start with a subagent note", subagent.name)
239 // start runs run in the background. Its context keeps the values of ctx, but
240 // only Close ends it.
241 func (r *SubagentRuns) start(ctx context.Context, run *SubagentRun) error {
242 ctx, run.cancel = context.WithCancel(context.WithoutCancel(ctx))
245 _, found := r.running[run.subagent.name]
246 if r.closed || found {
251 return fmt.Errorf("%s runs already", run.subagent.name)
254 return errSubagentClosed
257 r.running[run.subagent.name] = run
263 Text: run.subagent.name,
271 // run runs the chains of run and reports its end, unless Close ended it. A
272 // chain follows while messages wait after the final answer of the chain before
273 // it, and no chain failed.
274 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
275 defer r.workers.Done()
279 for more := true; more; {
280 reply, err := r.chain(ctx, run)
282 reply = "failed: " + err.Error()
285 replies = append(replies, reply)
288 more = err == nil && len(run.pending) > 0
291 delete(r.running, run.subagent.name)
296 if ctx.Err() != nil {
300 r.report(subagentReport(run.subagent.name, replies), false)
303 // chain runs one request chain of run and returns its final answer.
304 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) (string, error) {
305 instructions, err := r.agent.SubagentPrompt()
310 log, err := run.subagent.Show()
315 input := make([]any, 0, len(run.view)+2)
316 for _, text := range run.view {
317 input = append(input, modelUserMessage(text))
320 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
326 subagents: r.subagents,
327 workdir: run.subagent.WorkDir(),
329 request: ModelRequest{
330 Model: r.agent.config.Model,
332 Instructions: instructions,
333 Tools: promptSubagentTools(),
334 Agent: run.subagent.name,
336 take: func() []Message {
341 return chain.Run(ctx, input)
344 // take returns the messages of Tell that wait for a request of run, and
346 func (r *SubagentRuns) take(run *SubagentRun) []Message {
348 defer r.mutex.Unlock()
350 messages := run.pending
356 // SubagentRun is one run of a subagent: the view that its requests get, its
357 // effort, the end of its context, and the messages of Tell that wait for a
358 // request. The mutex of SubagentRuns protects the messages.
359 type SubagentRun struct {
363 cancel context.CancelFunc
367 // subagentReport returns the report of the subagent name with its replies: the
368 // name in brackets, a new line, the replies, and the zoom that gives its whole
370 func subagentReport(name string, replies []string) string {
371 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"