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