]> git.giacomolli.uk - onechat/blob - subagent_run_test.go
Log each schedule run by its ID
[onechat] / subagent_run_test.go
1 package main
2
3 import (
4         "context"
5         "io"
6         "log"
7         "net/http"
8         "os"
9         "path/filepath"
10         "strconv"
11         "strings"
12         "sync/atomic"
13         "testing"
14         "time"
15
16         "github.com/stretchr/testify/require"
17 )
18
19 // subagentTestFullContext is the error body of a call whose input does not fit
20 // in the context of the model.
21 const subagentTestFullContext = `{"error": {"message": "Your input exceeds the context window of this model.", "type": "invalid_request_error", "code": "context_length_exceeded"}}`
22
23 func TestSpawnRunsASubagentInTheBackground(t *testing.T) {
24         server := newModelFake(t)
25         var test *TurnTestLoop
26         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
27                 last := subagentTestLastItem(call)
28                 switch {
29                 case subagentTestCall(call) && last["type"] == "function_call_output":
30                         // The report comes after the reply of the turn of the spawn.
31                         test.events.WaitFor(t, "reply", 1)
32                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("There are 3 files.")}, 100, 0, 5))
33                 case subagentTestCall(call):
34                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"pwd"}`)}, 100, 0, 5))
35                 case last["type"] == "function_call_output":
36                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Started.")}, 100, 0, 5))
37                 case strings.HasPrefix(subagentTestText(last), "["):
38                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
39                 default:
40                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "spawn", `{"tasks":["Count the files."],"effort":null}`)}, 100, 0, 5))
41                 }
42         }
43
44         test = newTestTurnLoop(t, server)
45         _, err := test.loop.Submit(terminalSource, "Count the files in a subagent.")
46         require.NoError(t, err)
47         test.events.WaitFor(t, "idle", 2)
48
49         name := test.events.Last("subagent")
50         require.True(t, subagentNameValid(name), name)
51         work := filepath.Join(test.loop.runs.subagents.dir, name, "work")
52         require.DirExists(t, work)
53
54         report := "[" + name + "]\nThere are 3 files.\n\nFull chat: zoom(\"" + name + "\")"
55         require.Equal(t, []string{
56                 "user: Count the files in a subagent.",
57                 `tool: spawn {"tasks":["Count the files."],"effort":null}`,
58                 "echo: Started " + name + ". Each one's report will reach you as a message when it finishes.",
59                 "reply: Started.",
60                 "work: " + report,
61                 "reply: Noted.",
62         }, test.Log(t))
63         require.Equal(t, Event{
64                 Kind:   "work",
65                 ID:     4,
66                 Text:   report,
67                 Source: name,
68                 Status: subagentFinished,
69         }, test.events.LastEvent("work"))
70         require.Equal(t, "subagent "+name+" is done, and its report goes to the agent\n", test.logs.String())
71
72         var first, second *ModelFakeCall
73         for n := range server.Calls() {
74                 call := server.Call(n)
75                 if !subagentTestCall(call) {
76                         continue
77                 }
78
79                 if first == nil {
80                         first = &call
81                 } else {
82                         second = &call
83                 }
84         }
85
86         require.NotNil(t, second)
87
88         // The first request of the subagent holds the view, the state, and its
89         // whole log, with its own prompt and tools.
90         texts := turnInputTexts(t, *first)
91         view := strings.Join(texts[:len(texts)-2], "")
92         require.True(t, strings.HasPrefix(view, "<chat>\n0+1|user: Count the files in a subagent.\n1+1|tool: spawn"), view)
93         require.Equal(t, "</chat>\n\nNow: Thursday 2026-01-01 12:00 +00:00", texts[len(texts)-2])
94         require.Equal(t, "0|note: subagent\n\n1|user: Count the files.", texts[len(texts)-1])
95
96         instructions, err := test.agent.SubagentPrompt()
97         require.NoError(t, err)
98         require.Equal(t, instructions, first.body["instructions"])
99
100         var tools []string
101         for _, tool := range first.body["tools"].([]any) {
102                 tools = append(tools, tool.(map[string]any)["name"].(string))
103         }
104
105         require.Equal(t, []string{"zoom", "date", "shell", "read", "write", "edit"}, tools)
106         require.Equal(t, work+"\n", subagentTestLastItem(*second)["output"])
107
108         subagent, err := test.loop.runs.subagents.Get(t.Context(), name)
109         require.NoError(t, err)
110
111         log, err := subagent.Show()
112         require.NoError(t, err)
113         require.Equal(t, "0|note: subagent\n\n1|user: Count the files.\n\n2|tool: shell {\"command\":\"pwd\"}\n\n3|echo: "+work+"\n\n\n4|reply: There are 3 files.", log)
114
115         var agents []string
116         for _, record := range modelReadUsage(t, test.usagePath) {
117                 agents = append(agents, record.Agent)
118         }
119
120         require.ElementsMatch(t, []string{"", "", "", name, name}, agents)
121 }
122
123 func TestReportJoinsTheNextRequestOfARunningTurn(t *testing.T) {
124         server := newModelFake(t)
125         arrived := make(chan struct{})
126         release := make(chan struct{})
127         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
128                 if call.number == 0 {
129                         close(arrived)
130                         <-release
131                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "date", `{"id":0}`)}, 100, 0, 5))
132
133                         return
134                 }
135
136                 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("done")}, 100, 0, 5))
137         }
138
139         test := newTestTurnLoop(t, server)
140         _, err := test.loop.Submit(terminalSource, "first")
141         require.NoError(t, err)
142
143         <-arrived
144         report := subagentReport("GreenOtter", []string{"It is done."})
145         test.loop.Report("GreenOtter", report, subagentFinished)
146         close(release)
147
148         test.events.WaitFor(t, "idle", 1)
149         require.Equal(t, report, subagentTestText(subagentTestLastItem(server.Call(1))))
150
151         time.Sleep(50 * time.Millisecond)
152         require.Equal(t, 2, server.Calls())
153         require.Equal(t, 1, test.events.Count("busy"))
154 }
155
156 func TestReportOfAStoppedSubagentStartsNoTurn(t *testing.T) {
157         server := newModelFake(t)
158         test := newTestTurnLoop(t, server)
159         report := subagentReport("GreenOtter", []string{"failed: context canceled"})
160         test.loop.Report("GreenOtter", report, subagentStopped)
161
162         test.events.WaitFor(t, "work", 1)
163         time.Sleep(50 * time.Millisecond)
164         require.Equal(t, []string{"work: " + report}, test.Log(t))
165         require.Zero(t, server.Calls())
166         require.Zero(t, test.events.Count("busy"))
167 }
168
169 func TestFullContextGivesAFailedReport(t *testing.T) {
170         server := newModelFake(t)
171         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
172                 if subagentTestCall(call) {
173                         w.WriteHeader(http.StatusBadRequest)
174                         _, _ = io.WriteString(w, subagentTestFullContext)
175
176                         return
177                 }
178
179                 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("It failed.")}, 100, 0, 5))
180         }
181
182         test := newTestTurnLoop(t, server)
183         _, err := test.loop.runs.Spawn(t.Context(), []string{"Read the big file."}, nil)
184         require.NoError(t, err)
185         test.events.WaitFor(t, "idle", 1)
186
187         name := test.events.Last("subagent")
188         require.Equal(t, []string{
189                 "work: [" + name + "]\nfailed: model call: HTTP 400: " + subagentTestFullContext + "\n\nFull chat: zoom(\"" + name + "\")",
190                 "reply: It failed.",
191         }, test.Log(t))
192         require.Equal(t, subagentFailed, test.events.LastEvent("work").Status)
193         require.Equal(t, "subagent "+name+" failed, and its report tells the agent: model call: HTTP 400: "+subagentTestFullContext+"\n", test.logs.String())
194 }
195
196 func TestSubagentHasItsOwnEffortAndNoSpawn(t *testing.T) {
197         server := newModelFake(t)
198         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
199                 if subagentTestCall(call) && subagentTestLastItem(call)["type"] != "function_call_output" {
200                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "spawn", `{"tasks":["More."],"effort":null}`)}, 100, 0, 5))
201
202                         return
203                 }
204
205                 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("ok")}, 100, 0, 5))
206         }
207
208         test := newTestTurnLoop(t, server)
209         runs := test.loop.runs
210         _, err := runs.Spawn(t.Context(), nil, nil)
211         require.EqualError(t, err, "no tasks")
212
213         effort := "max"
214         _, err = runs.Spawn(t.Context(), []string{"Think."}, &effort)
215         require.EqualError(t, err, `no effort "max": one of low, medium, high, xhigh`)
216
217         effort = "high"
218         _, err = runs.Spawn(t.Context(), []string{"Think hard."}, &effort)
219         require.NoError(t, err)
220         test.events.WaitFor(t, "idle", 1)
221
222         for n := range server.Calls() {
223                 call := server.Call(n)
224                 if subagentTestCall(call) {
225                         require.Equal(t, "high", call.body["reasoning"].(map[string]any)["effort"])
226                 }
227         }
228
229         subagent, err := runs.subagents.Get(t.Context(), test.events.Last("subagent"))
230         require.NoError(t, err)
231
232         log, err := subagent.Show()
233         require.NoError(t, err)
234         require.Equal(t, "0|note: subagent at effort high\n\n1|user: Think hard.\n\n2|tool: spawn {\"tasks\":[\"More.\"],\"effort\":null}\n\n3|echo: error: unknown tool \"spawn\"\n\n4|reply: ok", log)
235
236         entries, err := os.ReadDir(runs.subagents.dir)
237         require.NoError(t, err)
238         require.Len(t, entries, 1)
239 }
240
241 func TestTellReachesARunningSubagentBetweenItsToolCalls(t *testing.T) {
242         server := newModelFake(t)
243         arrived := make(chan struct{})
244         told := make(chan struct{})
245         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
246                 switch {
247                 case !subagentTestCall(call):
248                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
249                 case call.number == 0:
250                         close(arrived)
251                         <-told
252                         modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"true"}`)}, 100, 0, 5))
253                 default:
254                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Used the second disk.")}, 100, 0, 5))
255                 }
256         }
257
258         test := newTestTurnLoop(t, server)
259         runs := test.loop.runs
260         _, err := runs.Spawn(t.Context(), []string{"Copy the files."}, nil)
261         require.NoError(t, err)
262
263         <-arrived
264         name := test.events.Last("subagent")
265         output, err := runs.Tell(t.Context(), name, "Use the second disk.")
266         require.NoError(t, err)
267         require.Equal(t, "Sent. "+name+" reads it between its tool calls, and its report answers it.", output)
268         close(told)
269
270         test.events.WaitFor(t, "idle", 1)
271         require.Equal(t, "Use the second disk.", subagentTestText(subagentTestLastItem(server.Call(1))))
272         require.Equal(t, "work: "+subagentReport(name, []string{"Used the second disk."}), test.Log(t)[0])
273 }
274
275 func TestTellAfterTheFinalAnswerStartsAnotherChain(t *testing.T) {
276         server := newModelFake(t)
277         arrived := make(chan struct{})
278         told := make(chan struct{})
279         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
280                 switch {
281                 case !subagentTestCall(call):
282                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
283                 case call.number == 0:
284                         close(arrived)
285                         <-told
286                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("First.")}, 100, 0, 5))
287                 default:
288                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Second.")}, 100, 0, 5))
289                 }
290         }
291
292         test := newTestTurnLoop(t, server)
293         runs := test.loop.runs
294         _, err := runs.Spawn(t.Context(), []string{"Count."}, nil)
295         require.NoError(t, err)
296
297         <-arrived
298         name := test.events.Last("subagent")
299         _, err = runs.Tell(t.Context(), name, "Count again.")
300         require.NoError(t, err)
301         close(told)
302
303         test.events.WaitFor(t, "idle", 1)
304         require.Equal(t, "0|note: subagent\n\n1|user: Count.\n\n2|user: Count again.\n\n3|reply: First.", subagentTestText(subagentTestLastItem(server.Call(1))))
305         require.Equal(t, "work: "+subagentReport(name, []string{"First.", "Second."}), test.Log(t)[0])
306 }
307
308 func TestTellToAFinishedSubagentGoesOnFromItsLog(t *testing.T) {
309         server := newModelFake(t)
310         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
311                 text := "Noted."
312                 if subagentTestCall(call) {
313                         text = "Reply " + strconv.Itoa(call.number) + "."
314                 }
315
316                 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
317         }
318
319         test := newTestTurnLoop(t, server)
320         runs := test.loop.runs
321         effort := "high"
322         _, err := runs.Spawn(t.Context(), []string{"Count."}, &effort)
323         require.NoError(t, err)
324         test.events.WaitFor(t, "idle", 1)
325
326         name := test.events.Last("subagent")
327         output, err := runs.Tell(t.Context(), name, "And the hidden files?")
328         require.NoError(t, err)
329         require.Equal(t, "Sent. "+name+"'s reply will reach you as a message.", output)
330         test.events.WaitFor(t, "idle", 2)
331
332         call := server.Call(2)
333         require.True(t, subagentTestCall(call))
334         require.Equal(t, "high", call.body["reasoning"].(map[string]any)["effort"])
335         require.Equal(t, "0|note: subagent at effort high\n\n1|user: Count.\n\n2|reply: Reply 0.\n\n3|user: And the hidden files?", subagentTestText(subagentTestLastItem(call)))
336
337         // The view of the new run holds the chat of now, with the first report.
338         texts := turnInputTexts(t, call)
339         require.Contains(t, strings.Join(texts[:len(texts)-2], ""), "work: ["+name+"]")
340         require.Equal(t, "work: "+subagentReport(name, []string{"Reply 2."}), test.Log(t)[2])
341
342         _, err = runs.Tell(t.Context(), "BlueFox", "Hello.")
343         require.EqualError(t, err, "no agent BlueFox")
344 }
345
346 func TestStopEndsASubagentAndItsCommand(t *testing.T) {
347         server := newModelFake(t)
348         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
349                 modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"touch started && sleep 30"}`)}, 100, 0, 5))
350         }
351
352         test := newTestTurnLoop(t, server)
353         runs := test.loop.runs
354         _, err := runs.Spawn(t.Context(), []string{"Wait long."}, nil)
355         require.NoError(t, err)
356
357         name := test.events.Last("subagent")
358         subagent, err := runs.subagents.Get(t.Context(), name)
359         require.NoError(t, err)
360
361         require.Eventually(t, func() bool {
362                 _, err := os.Stat(filepath.Join(subagent.WorkDir(), "started"))
363                 return err == nil
364         }, 10*time.Second, time.Millisecond)
365
366         start := time.Now()
367         found, err := test.loop.StopSubagent(name)
368         require.NoError(t, err)
369         require.True(t, found)
370
371         test.events.WaitFor(t, "work", 1)
372         require.Less(t, time.Since(start), 10*time.Second)
373         require.Equal(t, []string{
374                 "note: Stopped by the user: " + name + ".",
375                 "work: " + subagentReport(name, []string{"stopped"}),
376         }, test.Log(t))
377         require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
378         require.Equal(t, "subagent "+name+" is stopped by the user\n", test.logs.String())
379
380         log, err := subagent.Show()
381         require.NoError(t, err)
382         require.Contains(t, log, "echo: \n[interrupted]")
383
384         found, err = test.loop.StopSubagent(name)
385         require.NoError(t, err)
386         require.False(t, found)
387
388         time.Sleep(50 * time.Millisecond)
389         require.Zero(t, test.events.Count("busy"))
390 }
391
392 func TestCloseEndsTheSubagentsWithNoReport(t *testing.T) {
393         server := newModelFake(t)
394         arrived := make(chan struct{})
395         release := make(chan struct{})
396         t.Cleanup(func() {
397                 close(release)
398         })
399
400         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
401                 close(arrived)
402                 <-release
403         }
404
405         test := newTestTurnLoop(t, server)
406         runs := test.loop.runs
407         _, err := runs.Spawn(t.Context(), []string{"Wait."}, nil)
408         require.NoError(t, err)
409
410         <-arrived
411         runs.Close()
412         require.Empty(t, runs.running)
413         require.Empty(t, test.Log(t))
414
415         _, err = runs.Spawn(t.Context(), []string{"Again."}, nil)
416         require.ErrorIs(t, err, errSubagentClosed)
417
418         entries, err := os.ReadDir(runs.subagents.dir)
419         require.NoError(t, err)
420         require.Len(t, entries, 1)
421 }
422
423 func TestRestartResumesTheCutSubagents(t *testing.T) {
424         server := newModelFake(t)
425         arrived := make(chan struct{}, 1)
426         release := make(chan struct{})
427         t.Cleanup(func() {
428                 close(release)
429         })
430
431         var restarted atomic.Bool
432         server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
433                 switch {
434                 case subagentTestCall(call) && !restarted.Load():
435                         arrived <- struct{}{}
436                         <-release
437                 case subagentTestCall(call):
438                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Built.")}, 100, 0, 5))
439                 default:
440                         modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
441                 }
442         }
443
444         agent, err := openAgent(t.TempDir())
445         require.NoError(t, err)
446
447         first := &TurnEventRecorder{}
448         loop, _, stop := subagentTestStart(t, agent, server, first)
449         _, err = loop.runs.Spawn(t.Context(), []string{"Build the project."}, nil)
450         require.NoError(t, err)
451
452         <-arrived
453         name := first.Last("subagent")
454         stop()
455         require.Zero(t, first.Count("work"))
456
457         data, err := os.ReadFile(filepath.Join(agent.dir, subagentWorkFileName))
458         require.NoError(t, err)
459         require.JSONEq(t, `{"running": ["`+name+`"]}`, string(data))
460
461         restarted.Store(true)
462         second := &TurnEventRecorder{}
463         loop, memory, stop := subagentTestStart(t, agent, server, second)
464         defer stop()
465
466         second.WaitFor(t, "idle", 1)
467         note := "The daemon restarted, cutting " + name + "; nothing was lost: go on."
468         log := subagentTestChat(t, memory)
469         require.Equal(t, []string{
470                 "note: " + note,
471                 "work: " + subagentReport(name, []string{"Built."}),
472                 "reply: Noted.",
473         }, log[len(log)-3:])
474
475         subagent, err := loop.runs.subagents.Get(t.Context(), name)
476         require.NoError(t, err)
477
478         text, err := subagent.Show()
479         require.NoError(t, err)
480         require.Equal(t, "0|note: subagent\n\n1|user: Build the project.\n\n2|user: "+note+"\n\n3|reply: Built.", text)
481
482         data, err = os.ReadFile(filepath.Join(agent.dir, subagentWorkFileName))
483         require.NoError(t, err)
484         require.JSONEq(t, `{"running": []}`, string(data))
485 }
486
487 func TestRestartReportsASubagentThatCannotGoOn(t *testing.T) {
488         server := newModelFake(t)
489         agent, err := openAgent(t.TempDir())
490         require.NoError(t, err)
491         require.NoError(t, os.WriteFile(filepath.Join(agent.dir, subagentWorkFileName), []byte(`{"running": ["BlueFox"]}`), 0o600))
492
493         events := &TurnEventRecorder{}
494         _, memory, stop := subagentTestStart(t, agent, server, events)
495         defer stop()
496
497         events.WaitFor(t, "idle", 1)
498         require.Equal(t, []string{
499                 "note: The daemon restarted, cutting BlueFox; nothing was lost: go on.",
500                 "work: " + subagentReport("BlueFox", []string{"failed: no agent BlueFox"}),
501                 "reply: a line",
502         }, subagentTestChat(t, memory))
503         require.Equal(t, subagentFailed, events.LastEvent("work").Status)
504
505         data, err := os.ReadFile(filepath.Join(agent.dir, subagentWorkFileName))
506         require.NoError(t, err)
507         require.JSONEq(t, `{"running": []}`, string(data))
508 }
509
510 // subagentTestStart runs a loop of agent with its memory and subagents in the
511 // agent directory, as the daemon does. The returned function stops the loop
512 // and closes the files.
513 func subagentTestStart(t *testing.T, agent *Agent, server *ModelFake, events *TurnEventRecorder) (*TurnLoop, *Memory, func()) {
514         t.Helper()
515
516         agent.config.Workdir = t.TempDir()
517         clock := newTestClock()
518         memory, err := openMemory(t.Context(), agent.dir, clock.Now, newFakeSummarizer())
519         require.NoError(t, err)
520
521         subagents := newSubagents(filepath.Join(agent.dir, subagentDirName), clock.Now, subagentNameRandom)
522         client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(agent.dir, "usage.jsonl"))
523         whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), clock)
524         loop, err := newTurnLoop(agent, memory, subagents, whatsapp, newTestSchedules(t, clock.Now), client, clock.Now, log.New(io.Discard, "", 0), events.Add)
525         require.NoError(t, err)
526
527         ctx, cancel := context.WithCancel(t.Context())
528         done := make(chan error)
529         go func() {
530                 done <- loop.Run(ctx)
531         }()
532
533         return loop, memory, func() {
534                 cancel()
535                 require.ErrorIs(t, <-done, context.Canceled)
536                 require.NoError(t, subagents.Close())
537                 require.NoError(t, memory.Close())
538         }
539 }
540
541 // subagentTestChat returns each message of the chat in memory as
542 // "kind: text".
543 func subagentTestChat(t *testing.T, memory *Memory) []string {
544         t.Helper()
545
546         var lines []string
547         for id := range memory.Len() {
548                 message, err := memory.Message(id)
549                 require.NoError(t, err)
550
551                 lines = append(lines, message.Kind+": "+message.Text)
552         }
553
554         return lines
555 }
556
557 // subagentTestCall tells if call is a call of a subagent.
558 func subagentTestCall(call ModelFakeCall) bool {
559         instructions, _ := call.body["instructions"].(string)
560
561         return strings.HasPrefix(instructions, "You are a subagent")
562 }
563
564 // subagentTestLastItem returns the last input item of call.
565 func subagentTestLastItem(call ModelFakeCall) map[string]any {
566         input := call.body["input"].([]any)
567
568         return input[len(input)-1].(map[string]any)
569 }
570
571 // subagentTestText returns the text of the user message item, or "" for
572 // another item.
573 func subagentTestText(item map[string]any) string {
574         content, _ := item["content"].([]any)
575         var text strings.Builder
576         for _, part := range content {
577                 value, _ := part.(map[string]any)["text"].(string)
578                 text.WriteString(value)
579         }
580
581         return text.String()
582 }