18 // subagentNote is the first message of the log of a subagent.
19 subagentNote = "subagent"
21 // subagentNoteEffort follows subagentNote when the subagent has its own
23 subagentNoteEffort = " at effort "
25 // subagentWorkFileName is the file of the names of the running
26 // subagents in the directory of an agent.
27 subagentWorkFileName = "work.json"
30 // The statuses of the end of a run, in the work event of its report.
32 subagentFinished = "finished"
33 subagentStopped = "stopped"
34 subagentFailed = "failed"
37 // errSubagentClosed tells that the runs of the subagents ended with the loop.
38 var errSubagentClosed = errors.New("the daemon stops, so no subagent starts")
40 // SubagentRuns runs the subagents of one agent in the background. Each run is a
41 // goroutine with its own context, which only Stop and Close end. A run is one
42 // or more request chains. The first request of each chain holds the view of the
43 // agent at the start of the run, then the state of the turn, then the whole log
44 // of the subagent. A message of Tell that comes during a chain joins its next
45 // request after the tool results. One that comes after the final answer starts
46 // one more chain, which reads the log again. The report of the run goes to
47 // report: the final answer of each chain, or the error of a failed call. The
48 // logger gets one line at the end of a run: done, failed with the cause, or
49 // stopped. Stop ends one run, and its report says that it stopped. At the stop
50 // of the daemon the runs end with no report.
52 // work.json in the directory of the agent holds the names of the running
53 // subagents. Each start and each end of a run writes it whole, but Close does
54 // not, so after a stop of the daemon it names the runs that the stop cut, and
55 // Resume goes on with them. The logs alone cannot tell this, because a stop
56 // can cut a run just after its final reply.
58 // Spawn and Tell split the view with the layout of the agent, which has no
59 // lock. They are tools, so they run in the goroutine of the turns, as each
60 // split of a turn does.
61 type SubagentRuns struct {
68 report func(name, text, status string)
69 note func(text string) error
75 running map[string]*SubagentRun
77 workers sync.WaitGroup
80 // Spawn starts one subagent for each task and returns their names at once. It
81 // waits until the view has no pending summary, and gives the view to each
82 // subagent. A nil effort is the effort of the agent.
83 func (r *SubagentRuns) Spawn(ctx context.Context, tasks []string, effort *string) (string, error) {
85 return "", errors.New("no tasks")
93 return "", errSubagentClosed
97 runEffort := r.agent.config.Effort
98 if effort != nil && !slices.Contains(promptEfforts, *effort) {
99 return "", fmt.Errorf("no effort %q: one of %s", *effort, strings.Join(promptEfforts, ", "))
102 if effort != nil && *effort != runEffort {
103 note += subagentNoteEffort + *effort
107 err := r.memory.Settle(ctx)
109 return "", fmt.Errorf("wait for the summaries: %w", err)
112 view, err := r.layout.Split(r.memory.ViewLines(nil))
117 names := make([]string, 0, len(tasks))
118 for _, task := range tasks {
119 name, err := r.create(ctx, note, task, view, runEffort)
120 if err != nil && len(names) > 0 {
121 return "", fmt.Errorf("started %s, then: %w", strings.Join(names, ", "), err)
128 names = append(names, name)
131 return "Started " + strings.Join(names, ", ") + ". Each one's report will reach you as a message when it finishes.", nil
134 // Tell sends message to the subagent name. A running subagent reads it between
135 // its tool calls, and its report answers it. A finished subagent goes on from
136 // its log with a new run, with the view of now and the effort of its note.
137 func (r *SubagentRuns) Tell(ctx context.Context, name, message string) (string, error) {
139 run, found := r.running[name]
141 defer r.mutex.Unlock()
143 told, err := run.subagent.Append("user", message)
148 run.pending = append(run.pending, told)
150 return "Sent. " + name + " reads it between its tool calls, and its report answers it.", nil
157 return "", errSubagentClosed
160 subagent, err := r.subagents.Get(ctx, name)
165 effort, err := r.effort(subagent)
170 err = r.memory.Settle(ctx)
172 return "", fmt.Errorf("wait for the summaries: %w", err)
175 view, err := r.layout.Split(r.memory.ViewLines(nil))
180 _, err = subagent.Append("user", message)
182 err = subagent.Flush(ctx)
189 err = r.start(ctx, &SubagentRun{
198 return "Sent. " + name + "'s reply will reach you as a message.", nil
201 // Stop ends the run of the subagent name and tells if it ran. The chat gets the
202 // note "Stopped by the user: NAME." under the mutex, so the note comes before
203 // the report of the run.
204 func (r *SubagentRuns) Stop(name string) (bool, error) {
206 defer r.mutex.Unlock()
208 run, found := r.running[name]
213 err := r.note("Stopped by the user: " + name + ".")
224 // Resume goes on with the runs that a stop of the daemon cut, as work.json
225 // names them. The chat gets the note "The daemon restarted, cutting NAMES;
226 // nothing was lost: go on.", and each cut subagent gets the same text as a
227 // message of Tell. A subagent that cannot go on gets a failed report.
228 func (r *SubagentRuns) Resume(ctx context.Context) error {
229 names, err := r.load()
230 if err != nil || len(names) == 0 {
234 text := "The daemon restarted, cutting " + strings.Join(names, ", ") + "; nothing was lost: go on."
240 for _, name := range names {
241 _, err = r.Tell(ctx, name, text)
243 r.logger.Printf("subagent %s lost at restart: %v", name, err)
244 r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
248 // A subagent that cannot go on does not run, so work.json no longer
249 // names it, and the next start does not try it again.
251 defer r.mutex.Unlock()
256 // Close ends every run and waits for them. No subagent starts after Close.
257 func (r *SubagentRuns) Close() {
260 for _, run := range r.running {
268 // create makes a new subagent with its note, its task, and its work directory,
269 // and starts its run.
270 func (r *SubagentRuns) create(ctx context.Context, note, task string, view []string, effort string) (string, error) {
271 subagent, err := r.subagents.Create(ctx)
276 for _, message := range [][2]string{{"note", note}, {"user", task}} {
277 _, err = subagent.Append(message[0], message[1])
283 err = subagent.Flush(ctx)
288 err = os.MkdirAll(subagent.WorkDir(), 0o700)
290 return "", fmt.Errorf("create work directory: %w", err)
293 return subagent.name, r.start(ctx, &SubagentRun{
300 // effort returns the effort of subagent from the note at the start of its log:
301 // the effort of the agent, or the effort that the note names.
302 func (r *SubagentRuns) effort(subagent *Subagent) (string, error) {
303 note, err := subagent.First()
308 effort, own := strings.CutPrefix(note.Text, subagentNote+subagentNoteEffort)
310 case note.Kind != "note":
313 case note.Text == subagentNote:
314 return r.agent.config.Effort, nil
317 return "", fmt.Errorf("the log of %s does not start with a subagent note", subagent.name)
320 // start runs run in the background. Its context keeps the values of ctx, but
321 // only Stop and Close end it.
322 func (r *SubagentRuns) start(ctx context.Context, run *SubagentRun) error {
323 ctx, run.cancel = context.WithCancel(context.WithoutCancel(ctx))
326 _, found := r.running[run.subagent.name]
327 if r.closed || found {
332 return fmt.Errorf("%s runs already", run.subagent.name)
335 return errSubagentClosed
338 r.running[run.subagent.name] = run
341 delete(r.running, run.subagent.name)
353 Text: run.subagent.name,
361 // load reads the names of work.json. A missing file names none.
362 func (r *SubagentRuns) load() ([]string, error) {
363 data, err := os.ReadFile(r.workPath())
364 if errors.Is(err, os.ErrNotExist) {
369 return nil, fmt.Errorf("read running subagents: %w", err)
372 var file SubagentWorkFile
373 err = json.Unmarshal(data, &file)
375 return nil, fmt.Errorf("decode running subagents: %w", err)
378 return file.Running, nil
381 // save writes the names of the running subagents to work.json. The caller
383 func (r *SubagentRuns) save() error {
384 file := SubagentWorkFile{
385 Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
388 data, err := json.Marshal(file)
390 return fmt.Errorf("encode running subagents: %w", err)
393 err = writeFileAtomic(r.workPath(), append(data, '\n'))
395 return fmt.Errorf("save running subagents: %w", err)
401 func (r *SubagentRuns) workPath() string {
402 return filepath.Join(r.agent.dir, subagentWorkFileName)
405 // run runs the chains of run and reports its end, unless Close ended it. A
406 // chain follows while messages wait after the final answer of the chain before
407 // it, and no chain failed. A chain that Stop ends gives the reply "stopped".
408 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
409 defer r.workers.Done()
415 status := subagentFinished
416 for more := true; more; {
417 reply, err := r.chain(ctx, run)
420 stopped = run.stopped
422 case err != nil && stopped:
425 reply = "failed: " + err.Error()
426 status = subagentFailed
430 more = err == nil && ctx.Err() == nil && len(run.pending) > 0
434 delete(r.running, run.subagent.name)
445 replies = append(replies, reply)
448 if ctx.Err() != nil && !stopped {
453 status = subagentStopped
457 case subagentStopped:
458 r.logger.Printf("subagent %s stopped", run.subagent.name)
460 r.logger.Printf("subagent %s failed: %v", run.subagent.name, failure)
462 r.logger.Printf("subagent %s done", run.subagent.name)
465 r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
468 // chain runs one request chain of run and returns its final answer.
469 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) (string, error) {
470 instructions, err := r.agent.SubagentPrompt()
475 log, err := run.subagent.Show()
480 input := make([]any, 0, len(run.view)+2)
481 for _, text := range run.view {
482 input = append(input, modelUserMessage(text))
485 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
491 subagents: r.subagents,
492 workdir: run.subagent.WorkDir(),
494 request: ModelRequest{
495 Model: r.agent.config.Model,
497 Instructions: instructions,
498 Tools: promptSubagentTools(),
499 Agent: run.subagent.name,
501 take: func() []Message {
506 return chain.Run(ctx, input)
509 // take returns the messages of Tell that wait for a request of run, and
511 func (r *SubagentRuns) take(run *SubagentRun) []Message {
513 defer r.mutex.Unlock()
515 messages := run.pending
521 // SubagentRun is one run of a subagent: the view that its requests get, its
522 // effort, the end of its context, the messages of Tell that wait for a request,
523 // and whether Stop ended it. The mutex of SubagentRuns protects the messages
525 type SubagentRun struct {
529 cancel context.CancelFunc
534 // SubagentWorkFile is the content of work.json.
535 type SubagentWorkFile struct {
536 Running []string `json:"running"`
539 // subagentReport returns the report of the subagent name with its replies: the
540 // name in brackets, a new line, the replies, and the zoom that gives its whole
542 func subagentReport(name string, replies []string) string {
543 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"