fix(agent): defer warning messages after parallel tool results

When parallel tool calls trigger loop detection warnings, the warning
messages (role="user") were inserted between tool result messages
(role="tool"). This breaks the Anthropic API when routed through
OpenAI-compatible proxies (e.g. LiteLLM): the proxy groups consecutive
tool messages into a single user message with tool_result blocks, but
an intervening user warning splits the group, causing orphaned
tool_results and HTTP 400 "tool_use ids without tool_result blocks".

Fix: accumulate warning messages during parallel result processing and
append them after all tool results, preserving the consecutive grouping.

Closes #642
This commit is contained in:
viettranx committed 2026-04-02 16:43:53 +07:00
1 parent bfd65828a6
commit 4e4a835795
1 file changed
+9 -1
+9 -1
View File
@@ -699,7 +699,13 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (result *RunResult,
// 5. Process results sequentially: emit events, append messages, save to session
// Note: tool span start/end already emitted inside goroutines above.
// IMPORTANT: warning messages (role="user") must be deferred until AFTER all
// tool results are appended. Inserting a user message between tool results
// breaks the Anthropic API requirement that all tool_results for a set of
// tool_uses must be consecutive (causes "tool_use ids without tool_result"
// errors when routed through LiteLLM OpenAI→Anthropic conversion).
var loopStuck bool
var deferredWarnings []providers.Message
for _, r := range collected {
// Record tool execution time for adaptive thresholds.
toolTiming.Record(r.tc.Name, time.Since(r.spanStart).Milliseconds())
@@ -708,12 +714,14 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (result *RunResult,
toolMsg, warningMsgs, action := l.processToolResult(ctx, rs, &req, emitRun, r.tc, r.registryName, r.result, hadBootstrap)
messages = append(messages, toolMsg)
rs.pendingMsgs = append(rs.pendingMsgs, toolMsg)
messages = append(messages, warningMsgs...)
deferredWarnings = append(deferredWarnings, warningMsgs...)
if action == toolResultBreak {
loopStuck = true
break
}
}
// Append deferred warnings after all tool results to preserve consecutive grouping.
messages = append(messages, deferredWarnings...)
// Check read-only streak after processing all parallel results.
if !loopStuck {