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))
135 return "", fmt.Errorf("split worker view: %w", err)
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)
146 return "", fmt.Errorf("create worker: %w", 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)
160 return "", fmt.Errorf("create worker log: %w", err)
163 for _, message := range [][2]string{{"note", note}, {"user", task}} {
164 _, err = subagent.Append(message[0], message[1])
166 return "", fmt.Errorf("append worker message: %w", err)
170 err = subagent.Flush(ctx)
172 return "", fmt.Errorf("flush worker log: %w", err)
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)
214 return fmt.Errorf("save running workers: %w", err)
223 run.started = r.subagents.clock()
224 r.logger.Info("subagent started", "name", run.subagent.name)
227 Text: run.subagent.name,
235 // save writes the names of the running subagents to work.json. The caller holds
237 func (r *SubagentRuns) save() error {
238 file := SubagentWorkFile{
239 Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
242 data, err := json.Marshal(file)
244 return fmt.Errorf("encode running subagents: %w", err)
247 err = writeFileAtomic(r.workPath(), append(data, '\n'))
249 return fmt.Errorf("save running subagents: %w", err)
255 // run runs the chains of run and reports and logs its end with the length of
256 // the run, unless Close ended it. A chain follows while messages wait after the
257 // final answer of the chain before it, and no chain failed. A chain that Stop
258 // ends gives the reply "stopped".
259 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
260 defer r.workers.Done()
269 status := subagentFinished
270 for more := true; more; {
271 reply, err := r.chain(ctx, run)
275 stopped = run.stopped
277 case err != nil && stopped:
280 reply = "failed: " + err.Error()
281 status = subagentFailed
285 more = err == nil && ctx.Err() == nil && len(run.pending) > 0
291 delete(r.running, run.subagent.name)
304 replies = append(replies, reply)
307 if ctx.Err() != nil && !stopped {
312 status = subagentStopped
315 length := r.subagents.clock().Sub(run.started)
317 case subagentStopped:
318 r.logger.Info("subagent stopped", "name", run.subagent.name, "after", length)
320 r.logger.Error("subagent failed", "name", run.subagent.name, "after", length, "err", failure)
322 r.logger.Info("subagent done", "name", run.subagent.name, "after", length)
325 r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
328 func (r *SubagentRuns) workPath() string {
329 return filepath.Join(r.agent.dir, subagentWorkFileName)
332 // chain runs one request chain of run and returns its final answer.
333 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) (string, error) {
334 instructions, err := r.agent.SubagentPrompt()
336 return "", fmt.Errorf("read worker instructions: %w", err)
339 log, err := run.subagent.Show()
341 return "", fmt.Errorf("read worker log: %w", err)
344 input := make([]any, 0, len(run.view)+2)
345 for _, text := range run.view {
346 input = append(input, modelUserMessage(text))
349 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
352 images: r.agent.Images(),
356 subagents: r.subagents,
357 workdir: run.subagent.WorkDir(),
358 images: r.agent.Images(),
360 request: ModelRequest{
361 Model: r.agent.config.Model,
363 Instructions: instructions,
364 Tools: promptSubagentTools(),
365 Agent: run.subagent.name,
367 take: func() []Message {
372 return chain.Run(ctx, input)
375 // take returns the messages of Tell that wait for a request of run, and removes
377 func (r *SubagentRuns) take(run *SubagentRun) []Message {
379 defer r.mutex.Unlock()
381 messages := run.pending
387 // Stop ends the run of the subagent name and tells if it ran. The chat gets the
388 // note "Stopped by the user: NAME." under the mutex, so the note comes before
389 // the report of the run.
390 func (r *SubagentRuns) Stop(name string) (bool, error) {
392 defer r.mutex.Unlock()
394 run, found := r.running[name]
399 err := r.note("Stopped by the user: " + name + ".")
410 // Resume goes on with the runs that a stop of the daemon cut, as work.json
411 // names them. The chat gets the note "The daemon restarted, cutting NAMES;
412 // nothing was lost: go on.", and each cut subagent gets the same text as a
413 // message of Tell. A subagent that cannot go on gets a failed report.
414 func (r *SubagentRuns) Resume(ctx context.Context) error {
415 names, err := r.load()
416 if err != nil || len(names) == 0 {
418 return fmt.Errorf("load interrupted workers: %w", err)
424 text := "The daemon restarted, cutting " + strings.Join(names, ", ") + "; nothing was lost: go on."
427 return fmt.Errorf("record interrupted workers: %w", err)
430 for _, name := range names {
431 _, err = r.Tell(ctx, name, text)
433 r.logger.Error("subagent lost at restart", "name", name, "err", err)
434 r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
438 // A subagent that cannot go on does not run, so work.json no longer
439 // names it, and the next start does not try it again.
441 defer r.mutex.Unlock()
446 // load reads the names of work.json. A missing file names none.
447 func (r *SubagentRuns) load() ([]string, error) {
448 data, err := os.ReadFile(r.workPath())
449 if errors.Is(err, os.ErrNotExist) {
454 return nil, fmt.Errorf("read running subagents: %w", err)
457 var file SubagentWorkFile
459 err = json.Unmarshal(data, &file)
461 return nil, fmt.Errorf("decode running subagents: %w", err)
464 return file.Running, nil
467 // Tell sends message to the subagent name. A running subagent reads it between
468 // its tool calls, and its report answers it. A finished subagent goes on from
469 // its log with a new run, with the view of now and the effort of its note.
470 func (r *SubagentRuns) Tell(ctx context.Context, name, message string) (string, error) {
473 run, found := r.running[name]
475 defer r.mutex.Unlock()
477 told, err := run.subagent.Append("user", message)
479 return "", fmt.Errorf("append running worker message: %w", err)
482 run.pending = append(run.pending, told)
484 return "Sent. " + name + " reads it between its tool calls, and its report answers it.", nil
491 return "", errSubagentClosed
494 subagent, err := r.subagents.Get(ctx, name)
496 return "", fmt.Errorf("open worker log: %w", err)
499 effort, err := r.effort(subagent)
501 return "", fmt.Errorf("read worker effort: %w", err)
504 err = r.memory.Settle(ctx)
506 return "", fmt.Errorf("wait for the summaries: %w", err)
509 view, err := r.layout.Split(r.memory.ViewLines(nil))
511 return "", fmt.Errorf("split worker view: %w", err)
514 _, err = subagent.Append("user", message)
516 err = subagent.Flush(ctx)
520 return "", fmt.Errorf("save worker message: %w", err)
523 err = r.start(ctx, &SubagentRun{
529 return "", fmt.Errorf("start told worker: %w", err)
532 return "Sent. " + name + "'s reply will reach you as a message.", nil
535 // effort returns the effort of subagent from the note at the start of its log:
536 // the effort of the agent, or the effort that the note names.
537 func (r *SubagentRuns) effort(subagent *Subagent) (string, error) {
538 note, err := subagent.First()
540 return "", fmt.Errorf("read worker note: %w", err)
543 effort, own := strings.CutPrefix(note.Text, subagentNote+subagentNoteEffort)
545 case note.Kind != "note":
548 case note.Text == subagentNote:
549 return r.agent.config.Effort, nil
552 return "", fmt.Errorf("the log of %s does not start with a subagent note", subagent.name)
555 // Snapshot locks the run state, memory, and subagent logs while snapshot reads
556 // the agent files. The lock order agrees with Stop, which logs under the run
558 func (r *SubagentRuns) Snapshot(ctx context.Context, snapshot func() error) error {
560 defer r.mutex.Unlock()
562 return r.memory.Snapshot(ctx, func() error {
563 return r.subagents.Snapshot(ctx, snapshot)
567 // Close ends every run and waits for them. No subagent starts after Close.
568 func (r *SubagentRuns) Close() {
572 for _, run := range r.running {
581 // subagentReport returns the report of the subagent name with its replies: the
582 // name in brackets, a new line, the replies, and the zoom that gives its whole
584 func subagentReport(name string, replies []string) string {
585 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"