fix(agent): stop running LLM loop for subagent completions

processSystemMessage was calling runAgentLoop for every subagent
completion, which caused:
- Chat spam: each completion triggered a full LLM response sent to user
- Token waste: unnecessary LLM iterations for each result
- Placeholder corruption: responses consumed Telegram status messages

Replace with a lightweight path: inject the subagent result into
session history and send a brief SkipPlaceholder notification. The
conductor sees accumulated results on its next turn.

Remove the now-unused SystemMessage field from processOptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-25 17:01:09 +09:00
parent d2801e3651
commit 5957dcb68c

View file

@ -115,7 +115,7 @@ type processOptions struct {
NoHistory bool // If true, don't load session history (for heartbeat) NoHistory bool // If true, don't load session history (for heartbeat)
TaskID string // Unique task ID for background task status tracking TaskID string // Unique task ID for background task status tracking
Background bool // If true, this is a background task (cron/heartbeat) — enables live task notifications Background bool // If true, this is a background task (cron/heartbeat) — enables live task notifications
SystemMessage bool // If true, this is a system/subagent message — suppress plan nudge, use SkipPlaceholder SystemMessage bool // If true, this is a system message (subagent result) — skip placeholder and plan nudge
} }
const defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json." const defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json."
@ -800,25 +800,41 @@ func (al *AgentLoop) processSystemMessage(ctx context.Context, msg bus.InboundMe
return "", nil return "", nil
} }
// Use default agent for system messages // Inject subagent result into session history without running a full LLM loop.
// The conductor will see the result on its next turn. This avoids:
// - Flooding the chat with a response for every subagent completion
// - Consuming the Telegram "Thinking..." placeholder
// - Wasting LLM tokens on processing each result individually
agent := al.registry.GetDefaultAgent() agent := al.registry.GetDefaultAgent()
if agent == nil { if agent == nil {
return "", fmt.Errorf("no default agent for system message") return "", fmt.Errorf("no default agent for system message")
} }
// Use the origin session for context
sessionKey := routing.BuildAgentMainSessionKey(agent.ID) sessionKey := routing.BuildAgentMainSessionKey(agent.ID)
historyMsg := fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content)
agent.Sessions.AddMessage(sessionKey, "user", historyMsg)
agent.Sessions.MarkDirty(sessionKey)
return al.runAgentLoop(ctx, agent, processOptions{ // Send a brief notification (SkipPlaceholder to avoid corrupting status messages)
SessionKey: sessionKey, label := msg.SenderID
if idx := strings.LastIndex(label, ":"); idx >= 0 {
label = label[idx+1:]
}
_ = al.bus.PublishOutbound(ctx, bus.OutboundMessage{
Channel: originChannel, Channel: originChannel,
ChatID: originChatID, ChatID: originChatID,
UserMessage: fmt.Sprintf("[System: %s] %s", msg.SenderID, msg.Content), Content: fmt.Sprintf("📋 %s completed.", label),
DefaultResponse: "Background task completed.", SkipPlaceholder: true,
EnableSummary: false,
SendResponse: true,
SystemMessage: true,
}) })
logger.InfoCF("agent", "Subagent result injected into session history",
map[string]any{
"sender_id": msg.SenderID,
"session_key": sessionKey,
"content_len": len(content),
})
return "", nil
} }
// acquireSessionLock gets or creates a per-session semaphore and acquires it. // acquireSessionLock gets or creates a per-session semaphore and acquires it.
@ -1230,7 +1246,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
Channel: opts.Channel, Channel: opts.Channel,
ChatID: opts.ChatID, ChatID: opts.ChatID,
Content: finalContent, Content: finalContent,
SkipPlaceholder: opts.SystemMessage, SkipPlaceholder: opts.SystemMessage, // suppress Telegram "Thinking..." for system messages
}) })
} }
@ -2285,7 +2301,7 @@ func (al *AgentLoop) runLLMIteration(
curUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]") curUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]")
} }
if curUnchecked > 0 && !planMarkNudged && if curUnchecked > 0 && !planMarkNudged &&
planSnapshot == "executing" && !opts.SystemMessage { planSnapshot == "executing" {
planMarkNudged = true planMarkNudged = true
messages = append(messages, providers.Message{ messages = append(messages, providers.Message{
Role: "assistant", Role: "assistant",