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 err = r.start(ctx, &SubagentRun{
186 return subagent.name, fmt.Errorf("start created worker: %w", err)
189 return subagent.name, nil
192 // start runs run in the background and logs its start. Its context keeps the
193 // values of ctx, but only Stop and Close end it.
194 func (r *SubagentRuns) start(ctx context.Context, run *SubagentRun) error {
195 ctx, run.cancel = context.WithCancel(context.WithoutCancel(ctx))
199 _, found := r.running[run.subagent.name]
200 if r.closed || found {
205 return fmt.Errorf("%s runs already", run.subagent.name)
208 return errSubagentClosed
211 r.running[run.subagent.name] = run
214 delete(r.running, run.subagent.name)
218 return fmt.Errorf("save running workers: %w", err)
224 run.started = r.subagents.clock()
225 r.logger.Info("subagent started", "name", run.subagent.name)
228 Text: run.subagent.name,
236 // save writes the names of the running subagents to work.json. The caller holds
238 func (r *SubagentRuns) save() error {
239 file := SubagentWorkFile{
240 Running: append([]string{}, slices.Sorted(maps.Keys(r.running))...),
243 data, err := json.Marshal(file)
245 return fmt.Errorf("encode running subagents: %w", err)
248 err = writeFileAtomic(r.workPath(), append(data, '\n'))
250 return fmt.Errorf("save running subagents: %w", err)
256 // run runs the chains of run and reports and logs its end with the length of
257 // the run, unless Close ended it. A chain follows while messages wait after the
258 // final answer of the chain before it, and no chain failed. A chain that Stop
259 // ends gives the reply "stopped".
260 func (r *SubagentRuns) run(ctx context.Context, run *SubagentRun) {
261 defer r.workers.Done()
270 status := subagentFinished
271 for more := true; more; {
272 reply, err := r.chain(ctx, run)
276 stopped = run.stopped
278 case err != nil && stopped:
281 reply = "failed: " + err.Error()
282 status = subagentFailed
286 more = err == nil && ctx.Err() == nil && len(run.pending) > 0
292 delete(r.running, run.subagent.name)
305 replies = append(replies, reply)
308 if ctx.Err() != nil && !stopped {
313 status = subagentStopped
316 length := r.subagents.clock().Sub(run.started)
318 case subagentStopped:
319 r.logger.Info("subagent stopped", "name", run.subagent.name, "after", length)
321 r.logger.Error("subagent failed", "name", run.subagent.name, "after", length, "err", failure)
323 r.logger.Info("subagent done", "name", run.subagent.name, "after", length)
326 r.report(run.subagent.name, subagentReport(run.subagent.name, replies), status)
329 func (r *SubagentRuns) workPath() string {
330 return filepath.Join(r.agent.dir, subagentWorkFileName)
333 // chain runs one request chain of run and returns its final answer.
334 func (r *SubagentRuns) chain(ctx context.Context, run *SubagentRun) (string, error) {
335 instructions, err := r.agent.SubagentPrompt()
337 return "", fmt.Errorf("read worker instructions: %w", err)
340 log, err := run.subagent.Show()
342 return "", fmt.Errorf("read worker log: %w", err)
345 input := make([]any, 0, len(run.view)+2)
346 for _, text := range run.view {
347 input = append(input, modelUserMessage(text))
350 input = append(input, modelUserMessage(r.state()), modelUserMessage(log))
353 images: r.agent.Images(),
357 subagents: r.subagents,
358 workdir: run.subagent.WorkDir(),
359 images: r.agent.Images(),
361 request: ModelRequest{
362 Model: r.agent.config.Model,
364 Instructions: instructions,
365 Tools: promptSubagentTools(),
366 Agent: run.subagent.name,
368 take: func() []Message {
373 text, err := chain.Run(ctx, input)
375 return text, fmt.Errorf("run worker chain: %w", err)
381 // take returns the messages of Tell that wait for a request of run, and removes
383 func (r *SubagentRuns) take(run *SubagentRun) []Message {
385 defer r.mutex.Unlock()
387 messages := run.pending
393 // Stop ends the run of the subagent name and tells if it ran. The chat gets the
394 // note "Stopped by the user: NAME." under the mutex, so the note comes before
395 // the report of the run.
396 func (r *SubagentRuns) Stop(name string) (bool, error) {
398 defer r.mutex.Unlock()
400 run, found := r.running[name]
405 err := r.note("Stopped by the user: " + name + ".")
416 // Resume goes on with the runs that a stop of the daemon cut, as work.json
417 // names them. The chat gets the note "The daemon restarted, cutting NAMES;
418 // nothing was lost: go on.", and each cut subagent gets the same text as a
419 // message of Tell. A subagent that cannot go on gets a failed report.
420 func (r *SubagentRuns) Resume(ctx context.Context) error {
421 names, err := r.load()
422 if err != nil || len(names) == 0 {
424 return fmt.Errorf("load interrupted workers: %w", err)
430 text := "The daemon restarted, cutting " + strings.Join(names, ", ") + "; nothing was lost: go on."
433 return fmt.Errorf("record interrupted workers: %w", err)
436 for _, name := range names {
437 _, err = r.Tell(ctx, name, text)
439 r.logger.Error("subagent lost at restart", "name", name, "err", err)
440 r.report(name, subagentReport(name, []string{"failed: " + err.Error()}), subagentFailed)
444 // A subagent that cannot go on does not run, so work.json no longer
445 // names it, and the next start does not try it again.
447 defer r.mutex.Unlock()
451 return fmt.Errorf("save resumed workers: %w", err)
457 // load reads the names of work.json. A missing file names none.
458 func (r *SubagentRuns) load() ([]string, error) {
459 data, err := os.ReadFile(r.workPath())
460 if errors.Is(err, os.ErrNotExist) {
465 return nil, fmt.Errorf("read running subagents: %w", err)
468 var file SubagentWorkFile
470 err = json.Unmarshal(data, &file)
472 return nil, fmt.Errorf("decode running subagents: %w", err)
475 return file.Running, nil
478 // Tell sends message to the subagent name. A running subagent reads it between
479 // its tool calls, and its report answers it. A finished subagent goes on from
480 // its log with a new run, with the view of now and the effort of its note.
481 func (r *SubagentRuns) Tell(ctx context.Context, name, message string) (string, error) {
484 run, found := r.running[name]
486 defer r.mutex.Unlock()
488 told, err := run.subagent.Append("user", message)
490 return "", fmt.Errorf("append running worker message: %w", err)
493 run.pending = append(run.pending, told)
495 return "Sent. " + name + " reads it between its tool calls, and its report answers it.", nil
502 return "", errSubagentClosed
505 subagent, err := r.subagents.Get(ctx, name)
507 return "", fmt.Errorf("open worker log: %w", err)
510 effort, err := r.effort(subagent)
512 return "", fmt.Errorf("read worker effort: %w", err)
515 err = r.memory.Settle(ctx)
517 return "", fmt.Errorf("wait for the summaries: %w", err)
520 view, err := r.layout.Split(r.memory.ViewLines(nil))
522 return "", fmt.Errorf("split worker view: %w", err)
525 _, err = subagent.Append("user", message)
527 err = subagent.Flush(ctx)
531 return "", fmt.Errorf("save worker message: %w", err)
534 err = r.start(ctx, &SubagentRun{
540 return "", fmt.Errorf("start told worker: %w", err)
543 return "Sent. " + name + "'s reply will reach you as a message.", nil
546 // effort returns the effort of subagent from the note at the start of its log:
547 // the effort of the agent, or the effort that the note names.
548 func (r *SubagentRuns) effort(subagent *Subagent) (string, error) {
549 note, err := subagent.First()
551 return "", fmt.Errorf("read worker note: %w", err)
554 effort, own := strings.CutPrefix(note.Text, subagentNote+subagentNoteEffort)
556 case note.Kind != "note":
559 case note.Text == subagentNote:
560 return r.agent.config.Effort, nil
563 return "", fmt.Errorf("the log of %s does not start with a subagent note", subagent.name)
566 // Snapshot locks the run state, memory, and subagent logs while snapshot reads
567 // the agent files. The lock order agrees with Stop, which logs under the run
569 func (r *SubagentRuns) Snapshot(ctx context.Context, snapshot func() error) error {
571 defer r.mutex.Unlock()
573 return r.memory.Snapshot(ctx, func() error {
574 return r.subagents.Snapshot(ctx, snapshot)
578 // Close ends every run and waits for them. No subagent starts after Close.
579 func (r *SubagentRuns) Close() {
583 for _, run := range r.running {
592 // subagentReport returns the report of the subagent name with its replies: the
593 // name in brackets, a new line, the replies, and the zoom that gives its whole
595 func subagentReport(name string, replies []string) string {
596 return "[" + name + "]\n" + strings.Join(replies, "\n\n") + "\n\nFull chat: zoom(\"" + name + "\")"