19 // subagentNote is the first message of the log of a subagent.
20 subagentNote = "subagent"
22 // subagentNoteEffort follows subagentNote when the subagent has its own
24 subagentNoteEffort = " at effort "
26 // subagentWorkFileName is the file of the names of the running
27 // subagents in the directory of an agent.
28 subagentWorkFileName = "work.json"
31 // The statuses of the end of a run, in the work event of its report.
33 subagentFinished = "finished"
34 subagentStopped = "stopped"
35 subagentFailed = "failed"
38 // errSubagentClosed tells that the runs of the subagents ended with the loop.
39 var errSubagentClosed = errors.New("the daemon stops, so no subagent starts")
41 // SubagentRuns runs the subagents of one agent in the background. Each run is a
42 // goroutine with its own context, which only Stop and Close end. A run is one
43 // or more request chains. The first request of each chain holds the view of the
44 // agent at the start of the run, then the state of the turn, then the whole log
45 // of the subagent. A message of Tell that comes during a chain joins its next
46 // request after the tool results. One that comes after the final answer starts
47 // one more chain, which reads the log again. The report of the run goes to
48 // report: the final answer of each chain, or the error of a failed call. The
49 // logger gets one line at the end of a run: done, failed with the cause, or
50 // stopped. Stop ends one run, and its report says that it stopped. At the stop
51 // of the daemon the runs end with no report.
53 // work.json in the directory of the agent holds the names of the running
54 // subagents. Each start and each end of a run writes it whole, but Close does
55 // not, so after a stop of the daemon it names the runs that the stop cut, and
56 // Resume goes on with them. The logs alone cannot tell this, because a stop can
57 // cut a run just after its final reply.
59 // Spawn and Tell split the view with the layout of the agent, which has no
60 // lock. They are tools, so they run in the goroutine of the turns, as each
61 // split of a turn does.
62 type SubagentRuns struct {
69 report func(name, text, status string)
70 note func(text string) error
76 running map[string]*SubagentRun
78 workers sync.WaitGroup
81 // SubagentRun is one run of a subagent: the view that its requests get, its
82 // effort, the end of its context, the messages of Tell that wait for a request,
83 // whether Stop ended it, and the time it started. The mutex of SubagentRuns
84 // protects the messages and the stop.
85 type SubagentRun struct {
89 cancel context.CancelFunc
95 // SubagentWorkFile is the content of work.json.
96 type SubagentWorkFile struct {
97 Running []string `json:"running"`
100 // Spawn starts one subagent for each task and returns their names at once. It
101 // waits until the view has no pending summary, and gives the view to each
102 // subagent. A nil effort is the effort of the agent.
103 func (r *SubagentRuns) Spawn(ctx context.Context, tasks []string, effort *string) (string, error) {
105 return "", errors.New("no tasks")
114 return "", errSubagentClosed
118 runEffort := r.agent.config.Effort
119 if effort != nil && !slices.Contains(promptEfforts, *effort) {
120 return "", fmt.Errorf("no effort %q: one of %s", *effort, strings.Join(promptEfforts, ", "))
123 if effort != nil && *effort != runEffort {
124 note += subagentNoteEffort + *effort
128 err := r.memory.Settle(ctx)
130 return "", fmt.Errorf("wait for the summaries: %w", err)
133 view, err := r.layout.Split(r.memory.ViewLines(nil))
138 names := make([]string, 0, len(tasks))
139 for _, task := range tasks {
140 name, err := r.create(ctx, note, task, view, runEffort)
141 if err != nil && len(names) > 0 {
142 return "", fmt.Errorf("started %s, then: %w", strings.Join(names, ", "), err)
149 names = append(names, name)
152 return "Started " + strings.Join(names, ", ") + ". Each one's report will reach you as a message when it finishes.", nil
155 // create makes a new subagent with its note, its task, and its work directory,
156 // and starts its run.
157 func (r *SubagentRuns) create(ctx context.Context, note, task string, view []string, effort string) (string, error) {
158 subagent, err := r.subagents.Create(ctx)
163 for _, message := range [][2]string{{"note", note}, {"user", task}} {
164 _, err = subagent.Append(message[0], message[1])
170 err = subagent.Flush(ctx)
175 err = os.MkdirAll(subagent.WorkDir(), 0o700)
177 return "", fmt.Errorf("create work directory: %w", err)
180 return subagent.name, r.start(ctx, &SubagentRun{
187 // start runs run in the background and logs its start. Its context keeps the
188 // values of ctx, but only Stop and Close end it.
189 func (r *SubagentRuns) start(ctx context.Context, run *SubagentRun) error {
190 ctx, run.cancel = context.WithCancel(context.WithoutCancel(ctx))
194 _, found := r.running[run.subagent.name]
195 if r.closed || found {
200 return fmt.Errorf("%s runs already", run.subagent.name)
203 return errSubagentClosed
206 r.running[run.subagent.name] = run
209 delete(r.running, run.subagent.name)
219 run.started = r.subagents.clock()
220 r.logger.Info("subagent started", "name", run.subagent.name)
223 Text: run.subagent.name,
231 // save writes the names of the running subagents to work.json. The caller holds
233 func (r *SubagentRuns) save() error {
234 file := SubagentWorkFile{
235 Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
238 data, err := json.Marshal(file)
240 return fmt.Errorf("encode running subagents: %w", err)
243 err = writeFileAtomic(r.workPath(), append(data, '\n'))
245 return fmt.Errorf("save running subagents: %w", err)
251 // run runs the chains of run and reports and logs its end with the length of
252 // the run, unless Close ended it. A chain follows while messages wait after the
253 // final answer of the chain before it, and no chain failed. A chain that Stop
254 // ends gives the reply "stopped".
255 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
256 defer r.workers.Done()
265 status := subagentFinished
266 for more := true; more; {
267 reply, err := r.chain(ctx, run)
271 stopped = run.stopped
273 case err != nil && stopped:
276 reply = "failed: " + err.Error()
277 status = subagentFailed
281 more = err == nil && ctx.Err() == nil && len(run.pending) > 0
287 delete(r.running, run.subagent.name)
300 replies = append(replies, reply)
303 if ctx.Err() != nil && !stopped {
308 status = subagentStopped
311 length := r.subagents.clock().Sub(run.started)
313 case subagentStopped:
314 r.logger.Info("subagent stopped", "name", run.subagent.name, "after", length)
316 r.logger.Error("subagent failed", "name", run.subagent.name, "after", length, "err", failure)
318 r.logger.Info("subagent done", "name", run.subagent.name, "after", length)
321 r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
324 func (r *SubagentRuns) workPath() string {
325 return filepath.Join(r.agent.dir, subagentWorkFileName)
328 // chain runs one request chain of run and returns its final answer.
329 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) (string, error) {
330 instructions, err := r.agent.SubagentPrompt()
335 log, err := run.subagent.Show()
340 input := make([]any, 0, len(run.view)+2)
341 for _, text := range run.view {
342 input = append(input, modelUserMessage(text))
345 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
348 images: r.agent.Images(),
352 subagents: r.subagents,
353 workdir: run.subagent.WorkDir(),
354 images: r.agent.Images(),
356 request: ModelRequest{
357 Model: r.agent.config.Model,
359 Instructions: instructions,
360 Tools: promptSubagentTools(),
361 Agent: run.subagent.name,
363 take: func() []Message {
368 return chain.Run(ctx, input)
371 // take returns the messages of Tell that wait for a request of run, and removes
373 func (r *SubagentRuns) take(run *SubagentRun) []Message {
375 defer r.mutex.Unlock()
377 messages := run.pending
383 // Stop ends the run of the subagent name and tells if it ran. The chat gets the
384 // note "Stopped by the user: NAME." under the mutex, so the note comes before
385 // the report of the run.
386 func (r *SubagentRuns) Stop(name string) (bool, error) {
388 defer r.mutex.Unlock()
390 run, found := r.running[name]
395 err := r.note("Stopped by the user: " + name + ".")
406 // Resume goes on with the runs that a stop of the daemon cut, as work.json
407 // names them. The chat gets the note "The daemon restarted, cutting NAMES;
408 // nothing was lost: go on.", and each cut subagent gets the same text as a
409 // message of Tell. A subagent that cannot go on gets a failed report.
410 func (r *SubagentRuns) Resume(ctx context.Context) error {
411 names, err := r.load()
412 if err != nil || len(names) == 0 {
416 text := "The daemon restarted, cutting " + strings.Join(names, ", ") + "; nothing was lost: go on."
422 for _, name := range names {
423 _, err = r.Tell(ctx, name, text)
425 r.logger.Error("subagent lost at restart", "name", name, "err", err)
426 r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
430 // A subagent that cannot go on does not run, so work.json no longer
431 // names it, and the next start does not try it again.
433 defer r.mutex.Unlock()
438 // load reads the names of work.json. A missing file names none.
439 func (r *SubagentRuns) load() ([]string, error) {
440 data, err := os.ReadFile(r.workPath())
441 if errors.Is(err, os.ErrNotExist) {
446 return nil, fmt.Errorf("read running subagents: %w", err)
449 var file SubagentWorkFile
451 err = json.Unmarshal(data, &file)
453 return nil, fmt.Errorf("decode running subagents: %w", err)
456 return file.Running, nil
459 // Tell sends message to the subagent name. A running subagent reads it between
460 // its tool calls, and its report answers it. A finished subagent goes on from
461 // its log with a new run, with the view of now and the effort of its note.
462 func (r *SubagentRuns) Tell(ctx context.Context, name, message string) (string, error) {
465 run, found := r.running[name]
467 defer r.mutex.Unlock()
469 told, err := run.subagent.Append("user", message)
474 run.pending = append(run.pending, told)
476 return "Sent. " + name + " reads it between its tool calls, and its report answers it.", nil
483 return "", errSubagentClosed
486 subagent, err := r.subagents.Get(ctx, name)
491 effort, err := r.effort(subagent)
496 err = r.memory.Settle(ctx)
498 return "", fmt.Errorf("wait for the summaries: %w", err)
501 view, err := r.layout.Split(r.memory.ViewLines(nil))
506 _, err = subagent.Append("user", message)
508 err = subagent.Flush(ctx)
515 err = r.start(ctx, &SubagentRun{
524 return "Sent. " + name + "'s reply will reach you as a message.", nil
527 // effort returns the effort of subagent from the note at the start of its log:
528 // the effort of the agent, or the effort that the note names.
529 func (r *SubagentRuns) effort(subagent *Subagent) (string, error) {
530 note, err := subagent.First()
535 effort, own := strings.CutPrefix(note.Text, subagentNote+subagentNoteEffort)
537 case note.Kind != "note":
540 case note.Text == subagentNote:
541 return r.agent.config.Effort, nil
544 return "", fmt.Errorf("the log of %s does not start with a subagent note", subagent.name)
547 // Snapshot locks the run state, memory, and subagent logs while snapshot reads
548 // the agent files. The lock order agrees with Stop, which logs under the run
550 func (r *SubagentRuns) Snapshot(ctx context.Context, snapshot func() error) error {
552 defer r.mutex.Unlock()
554 return r.memory.Snapshot(ctx, func() error {
555 return r.subagents.Snapshot(ctx, snapshot)
559 // Close ends every run and waits for them. No subagent starts after Close.
560 func (r *SubagentRuns) Close() {
564 for _, run := range r.running {
573 // subagentReport returns the report of the subagent name with its replies: the
574 // name in brackets, a new line, the replies, and the zoom that gives its whole
576 func subagentReport(name string, replies []string) string {
577 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"