diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 89e92aa14..ac230aa86 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -88,6 +88,7 @@ type processOptions struct { DefaultResponse string // Response when LLM returns empty EnableSummary bool // Whether to trigger summarization SendResponse bool // Whether to send response via bus + AllowInterimPicoPublish bool // Whether pico tool-call interim text can be published when SendResponse is false SuppressToolFeedback bool // Whether to suppress inline tool feedback messages NoHistory bool // If true, don't load session history (for heartbeat) SkipInitialSteeringPoll bool // If true, skip the steering poll at loop start (used by Continue) @@ -1398,18 +1399,19 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) }) opts := processOptions{ - SessionKey: sessionKey, - Channel: msg.Channel, - ChatID: msg.ChatID, - MessageID: msg.MessageID, - ReplyToMessageID: inboundMetadata(msg, metadataKeyReplyToMessage), - SenderID: msg.SenderID, - SenderDisplayName: msg.Sender.DisplayName, - UserMessage: msg.Content, - Media: msg.Media, - DefaultResponse: defaultResponse, - EnableSummary: true, - SendResponse: false, + SessionKey: sessionKey, + Channel: msg.Channel, + ChatID: msg.ChatID, + MessageID: msg.MessageID, + ReplyToMessageID: inboundMetadata(msg, metadataKeyReplyToMessage), + SenderID: msg.SenderID, + SenderDisplayName: msg.Sender.DisplayName, + UserMessage: msg.Content, + Media: msg.Media, + DefaultResponse: defaultResponse, + EnableSummary: true, + SendResponse: false, + AllowInterimPicoPublish: true, } // context-dependent commands check their own Runtime fields and report @@ -2253,7 +2255,7 @@ turnLoop: } logger.DebugCF("agent", "LLM response", llmResponseFields) - if al.bus != nil && ts.channel == "pico" && len(response.ToolCalls) > 0 { + if al.bus != nil && ts.channel == "pico" && len(response.ToolCalls) > 0 && ts.opts.AllowInterimPicoPublish { if strings.TrimSpace(response.Content) != "" { outCtx, outCancel := context.WithTimeout(turnCtx, 3*time.Second) err := al.bus.PublishOutbound(outCtx, bus.OutboundMessage{ diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 371f91d89..a67c8d040 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -2844,6 +2844,55 @@ func TestRun_PicoPublishesAssistantContentDuringToolCallsWithoutFinalDuplicate(t } } +func TestRunAgentLoop_PicoSkipsInterimPublishWhenNotAllowed(t *testing.T) { + tmpDir := t.TempDir() + + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: tmpDir, + ModelName: "test-model", + MaxTokens: 4096, + MaxToolIterations: 10, + }, + }, + } + + msgBus := bus.NewMessageBus() + provider := &picoInterleavedContentProvider{} + al := NewAgentLoop(cfg, msgBus, provider) + + agent := al.GetRegistry().GetDefaultAgent() + if agent == nil { + t.Fatal("expected default agent") + } + agent.Tools.Register(&toolLimitTestTool{}) + + response, err := al.runAgentLoop(context.Background(), agent, processOptions{ + SessionKey: "agent:main:pico:session-1", + Channel: "pico", + ChatID: "session-1", + UserMessage: "run with tools", + DefaultResponse: defaultResponse, + EnableSummary: false, + SendResponse: false, + AllowInterimPicoPublish: false, + SuppressToolFeedback: true, + }) + if err != nil { + t.Fatalf("runAgentLoop() error = %v", err) + } + if response != "final model text" { + t.Fatalf("runAgentLoop() response = %q, want %q", response, "final model text") + } + + select { + case outbound := <-msgBus.OutboundChan(): + t.Fatalf("unexpected outbound message when interim publish disabled: %+v", outbound) + case <-time.After(200 * time.Millisecond): + } +} + func TestResolveMediaRefs_ResolvesToBase64(t *testing.T) { store := media.NewFileMediaStore() dir := t.TempDir()