diff --git a/internal/agent/loop.go b/internal/agent/loop.go index a2c182ad..4e5ee449 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -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 {