fix(agents): preserve origin context for async follow-ups

This commit is contained in:
Anton Bogdanovich 2026-05-06 21:24:21 -07:00
parent 01280eaa53
commit 77c1890765
3 changed files with 136 additions and 4 deletions

View file

@ -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<actual content>"
@ -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,
}
}

View file

@ -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 {

View file

@ -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,
})