+func TestTellReachesARunningSubagentBetweenItsToolCalls(t *testing.T) {
+ server := newModelFake(t)
+ arrived := make(chan struct{})
+ told := make(chan struct{})
+ server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+ switch {
+ case !subagentTestCall(call):
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
+ case call.number == 0:
+ close(arrived)
+ <-told
+ modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"true"}`)}, 100, 0, 5))
+ default:
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Used the second disk.")}, 100, 0, 5))
+ }
+ }
+
+ test := newTestTurnLoop(t, server)
+ runs := test.loop.runs
+ _, err := runs.Spawn(t.Context(), []string{"Copy the files."}, nil)
+ require.NoError(t, err)
+
+ <-arrived
+ name := test.events.Last("subagent")
+ output, err := runs.Tell(t.Context(), name, "Use the second disk.")
+ require.NoError(t, err)
+ require.Equal(t, "Sent. "+name+" reads it between its tool calls, and its report answers it.", output)
+ close(told)
+
+ test.events.WaitFor(t, "idle", 1)
+ require.Equal(t, "Use the second disk.", subagentTestText(subagentTestLastItem(server.Call(1))))
+ require.Equal(t, "work: "+subagentReport(name, []string{"Used the second disk."}), test.Log(t)[0])
+}
+
+func TestTellAfterTheFinalAnswerStartsAnotherChain(t *testing.T) {
+ server := newModelFake(t)
+ arrived := make(chan struct{})
+ told := make(chan struct{})
+ server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+ switch {
+ case !subagentTestCall(call):
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Noted.")}, 100, 0, 5))
+ case call.number == 0:
+ close(arrived)
+ <-told
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("First.")}, 100, 0, 5))
+ default:
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("Second.")}, 100, 0, 5))
+ }
+ }
+
+ test := newTestTurnLoop(t, server)
+ runs := test.loop.runs
+ _, err := runs.Spawn(t.Context(), []string{"Count."}, nil)
+ require.NoError(t, err)
+
+ <-arrived
+ name := test.events.Last("subagent")
+ _, err = runs.Tell(t.Context(), name, "Count again.")
+ require.NoError(t, err)
+ close(told)
+
+ test.events.WaitFor(t, "idle", 1)
+ require.Equal(t, "0|note: subagent\n\n1|user: Count.\n\n2|user: Count again.\n\n3|reply: First.", subagentTestText(subagentTestLastItem(server.Call(1))))
+ require.Equal(t, "work: "+subagentReport(name, []string{"First.", "Second."}), test.Log(t)[0])
+}
+
+func TestTellToAFinishedSubagentGoesOnFromItsLog(t *testing.T) {
+ server := newModelFake(t)
+ server.reply = func(w http.ResponseWriter, call ModelFakeCall) {
+ text := "Noted."
+ if subagentTestCall(call) {
+ text = "Reply " + strconv.Itoa(call.number) + "."
+ }
+
+ modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem(text)}, 100, 0, 5))
+ }
+
+ test := newTestTurnLoop(t, server)
+ runs := test.loop.runs
+ effort := "high"
+ _, err := runs.Spawn(t.Context(), []string{"Count."}, &effort)
+ require.NoError(t, err)
+ test.events.WaitFor(t, "idle", 1)
+
+ name := test.events.Last("subagent")
+ output, err := runs.Tell(t.Context(), name, "And the hidden files?")
+ require.NoError(t, err)
+ require.Equal(t, "Sent. "+name+"'s reply will reach you as a message.", output)
+ test.events.WaitFor(t, "idle", 2)
+
+ call := server.Call(2)
+ require.True(t, subagentTestCall(call))
+ require.Equal(t, "high", call.body["reasoning"].(map[string]any)["effort"])
+ 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)))
+
+ // The view of the new run holds the chat of now, with the first report.
+ texts := turnInputTexts(t, call)
+ require.Contains(t, strings.Join(texts[:len(texts)-2], ""), "work: ["+name+"]")
+ require.Equal(t, "work: "+subagentReport(name, []string{"Reply 2."}), test.Log(t)[2])
+
+ _, err = runs.Tell(t.Context(), "BlueFox", "Hello.")
+ require.EqualError(t, err, "no agent BlueFox")
+}
+
+func TestStopEndsASubagentAndItsCommand(t *testing.T) {
+ server := newModelFake(t)
+ server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
+ modelWriteEvents(w, modelCompletedEvent([]any{modelFunctionCallItem("fc_1", "call_1", "shell", `{"command":"touch started && sleep 30"}`)}, 100, 0, 5))
+ }
+
+ test := newTestTurnLoop(t, server)
+ runs := test.loop.runs
+ _, err := runs.Spawn(t.Context(), []string{"Wait long."}, nil)
+ require.NoError(t, err)
+
+ name := test.events.Last("subagent")
+ subagent, err := runs.subagents.Get(t.Context(), name)
+ require.NoError(t, err)
+
+ require.Eventually(t, func() bool {
+ _, err := os.Stat(filepath.Join(subagent.WorkDir(), "started"))
+ return err == nil
+ }, 10*time.Second, time.Millisecond)
+
+ start := time.Now()
+ found, err := test.loop.StopSubagent(name)
+ require.NoError(t, err)
+ require.True(t, found)
+
+ test.events.WaitFor(t, "work", 1)
+ require.Less(t, time.Since(start), 10*time.Second)
+ require.Equal(t, []string{
+ "note: Stopped by the user: " + name + ".",
+ "work: " + subagentReport(name, []string{"stopped"}),
+ }, test.Log(t))
+ require.Equal(t, subagentStopped, test.events.LastEvent("work").Status)
+ require.Equal(t, "subagent "+name+" started\nsubagent "+name+" stopped after 0s\n", test.logs.String())
+
+ log, err := subagent.Show()
+ require.NoError(t, err)
+ require.Contains(t, log, "echo: \n[interrupted]")
+
+ found, err = test.loop.StopSubagent(name)
+ require.NoError(t, err)
+ require.False(t, found)
+
+ time.Sleep(50 * time.Millisecond)
+ require.Zero(t, test.events.Count("busy"))
+}
+