diff --git a/pkg/agent/agent_message.go b/pkg/agent/agent_message.go index 96b0b0817..f884d4cfb 100644 --- a/pkg/agent/agent_message.go +++ b/pkg/agent/agent_message.go @@ -15,6 +15,15 @@ import ( "github.com/sipeed/picoclaw/pkg/utils" ) +const ( + systemFollowUpOriginChannelKey = "origin_channel" + systemFollowUpOriginChatIDKey = "origin_chat_id" + systemFollowUpOriginChatTypeKey = "origin_chat_type" + systemFollowUpOriginTopicIDKey = "origin_topic_id" + systemFollowUpOriginMessageIDKey = "origin_message_id" + systemFollowUpOriginReplyToMessageIDKey = "origin_reply_to_message_id" +) + func (al *AgentLoop) buildContinuationTarget(msg bus.InboundMessage) (*continuationTarget, error) { if msg.Channel == "system" { return nil, nil @@ -227,6 +236,42 @@ func (al *AgentLoop) allocateRouteSession(route routing.ResolvedRoute, msg bus.I }) } +func originTopicID(origin *bus.InboundContext) string { + if origin == nil { + return "" + } + return strings.TrimSpace(origin.TopicID) +} + +func systemFollowUpOriginRaw(origin *bus.InboundContext, channel, chatID string) map[string]string { + raw := map[string]string{ + systemFollowUpOriginChannelKey: strings.TrimSpace(channel), + systemFollowUpOriginChatIDKey: strings.TrimSpace(chatID), + } + if origin == nil { + return raw + } + if origin.Channel != "" { + raw[systemFollowUpOriginChannelKey] = strings.TrimSpace(origin.Channel) + } + if origin.ChatID != "" { + raw[systemFollowUpOriginChatIDKey] = strings.TrimSpace(origin.ChatID) + } + if origin.ChatType != "" { + raw[systemFollowUpOriginChatTypeKey] = strings.TrimSpace(origin.ChatType) + } + if origin.TopicID != "" { + raw[systemFollowUpOriginTopicIDKey] = strings.TrimSpace(origin.TopicID) + } + if origin.MessageID != "" { + raw[systemFollowUpOriginMessageIDKey] = strings.TrimSpace(origin.MessageID) + } + if origin.ReplyToMessageID != "" { + raw[systemFollowUpOriginReplyToMessageIDKey] = strings.TrimSpace(origin.ReplyToMessageID) + } + return raw +} + func (al *AgentLoop) processSystemMessage( ctx context.Context, msg bus.InboundMessage, @@ -253,6 +298,30 @@ func (al *AgentLoop) processSystemMessage( originChannel = "cli" originChatID = msg.ChatID } + originChatType := "direct" + originTopicID := strings.TrimSpace(msg.Context.TopicID) + originMessageID := strings.TrimSpace(msg.Context.MessageID) + originReplyToMessageID := strings.TrimSpace(msg.Context.ReplyToMessageID) + if raw := msg.Context.Raw; len(raw) > 0 { + if value := strings.TrimSpace(raw[systemFollowUpOriginChannelKey]); value != "" { + originChannel = value + } + if value := strings.TrimSpace(raw[systemFollowUpOriginChatIDKey]); value != "" { + originChatID = value + } + if value := strings.TrimSpace(raw[systemFollowUpOriginChatTypeKey]); value != "" { + originChatType = value + } + if value := strings.TrimSpace(raw[systemFollowUpOriginTopicIDKey]); value != "" { + originTopicID = value + } + if value := strings.TrimSpace(raw[systemFollowUpOriginMessageIDKey]); value != "" { + originMessageID = value + } + if value := strings.TrimSpace(raw[systemFollowUpOriginReplyToMessageIDKey]); value != "" { + originReplyToMessageID = value + } + } // Extract subagent result from message content // Format: "Task 'label' completed.\n\nResult:\n" @@ -286,10 +355,13 @@ func (al *AgentLoop) processSystemMessage( } if originChannel != "" || originChatID != "" { dispatch.InboundContext = &bus.InboundContext{ - Channel: originChannel, - ChatID: originChatID, - ChatType: "direct", - SenderID: msg.SenderID, + Channel: originChannel, + ChatID: originChatID, + ChatType: originChatType, + TopicID: originTopicID, + SenderID: msg.SenderID, + MessageID: originMessageID, + ReplyToMessageID: originReplyToMessageID, } } diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index a75919912..914bea682 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -2518,6 +2518,64 @@ func TestProcessMessage_UsesRouteSessionKey(t *testing.T) { } } +func TestProcessSystemMessage_PreservesOriginTopicOnFinalResponse(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 := &simpleMockProvider{response: "follow-up response"} + al := NewAgentLoop(cfg, msgBus, provider) + + msg := testInboundMessage(bus.InboundMessage{ + Context: bus.InboundContext{ + Channel: "system", + ChatID: "telegram:-1001234567890", + ChatType: "direct", + TopicID: "42", + SenderID: "async:spawn", + Raw: map[string]string{ + systemFollowUpOriginChannelKey: "telegram", + systemFollowUpOriginChatIDKey: "-1001234567890", + systemFollowUpOriginChatTypeKey: "group", + systemFollowUpOriginTopicIDKey: "42", + }, + }, + Content: "Task 'deep-research' completed.\n\nResult:\nreport URL", + }) + + if _, err := al.processSystemMessage(context.Background(), msg); err != nil { + t.Fatalf("processSystemMessage() error = %v", err) + } + + select { + case outbound := <-msgBus.OutboundChan(): + if outbound.Content != "follow-up response" { + t.Fatalf("outbound content = %q, want follow-up response", outbound.Content) + } + if outbound.Channel != "telegram" || outbound.ChatID != "-1001234567890" { + t.Fatalf("outbound route = %s/%s, want telegram/-1001234567890", outbound.Channel, outbound.ChatID) + } + if outbound.Context.ChatType != "group" { + t.Fatalf("outbound chat type = %q, want group; context=%+v", outbound.Context.ChatType, outbound.Context) + } + if outbound.Context.TopicID != "42" { + t.Fatalf("outbound topic = %q, want 42; context=%+v", outbound.Context.TopicID, outbound.Context) + } + case <-time.After(responseTimeout): + t.Fatal("timed out waiting for outbound response") + } +} + func TestProcessMessage_CommandOutcomes(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") if err != nil { diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 0f71c7432..d792c5ab2 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -417,6 +417,8 @@ toolLoop: ChatID: fmt.Sprintf("%s:%s", ts.channel, ts.chatID), ChatType: "direct", SenderID: fmt.Sprintf("async:%s", asyncToolName), + TopicID: originTopicID(ts.opts.Dispatch.InboundContext), + Raw: systemFollowUpOriginRaw(ts.opts.Dispatch.InboundContext, ts.channel, ts.chatID), }, Content: content, })