-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(core): make loop detection result-aware for task_list polls #9492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c48b84e
91256db
83d2bbb
b0db430
2ea0eea
8b5b610
96eeb5c
164a9a8
0b619f5
e1e61d7
f4e85c7
167b0af
77a4c88
76621a1
b0b2558
61dd8c5
1ac2a81
e7085ec
dd2510d
7cc2bff
20ce795
edb1f30
dd309b3
00aa71b
71630d8
660cc46
0f8abb8
a08fac3
0ca1aa3
da1d1c3
395420a
b7d8386
e754027
95f2447
b1a8e66
5746008
c4db73a
a91b6b2
4201cd7
88edb22
3ca2fdd
429881f
f6199ae
342ea34
7f49db3
372e4b7
27f12a2
9ee8300
23b6f8a
c30eadb
d603092
9b5089f
ccdb89d
dfadc01
f34098e
4bbf8ea
fc76bc2
2351563
1d515ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,7 @@ import { | |
| type ToolCallRequestInfo, | ||
| } from '../../core/turn.js'; | ||
| import { LoopDetectionService } from '../../services/loopDetectionService.js'; | ||
| import type { LoopType } from '../../telemetry/types.js'; | ||
| import { | ||
| CoreToolScheduler, | ||
| type ToolCall, | ||
|
|
@@ -317,6 +318,12 @@ export interface ReasoningLoopResult { | |
| terminateMode: AgentTerminateMode | null; | ||
| /** Number of model round-trips completed. */ | ||
| turnsUsed: number; | ||
| /** | ||
| * Which loop detector fired, when terminateMode is LOOP_DETECTED (issue | ||
| * #9450 — attribution for stops that all render as one generic message | ||
| * otherwise). null otherwise. | ||
| */ | ||
| loopType?: LoopType | null; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -993,6 +1000,13 @@ export class AgentCore { | |
| } as AgentRoundEvent); | ||
|
|
||
| const functionCalls: FunctionCall[] = []; | ||
| // callIds already streamed to the loop guard this attempt. Mirrors | ||
| // dedupeToolCallsById (which collapses execution to one call per | ||
| // id): a provider can emit the same call id twice in one response, | ||
| // and counting both emissions would leave the request counters one | ||
| // ahead of the executed result evidence (one recordToolResult per | ||
| // executed call), fail-safe-halting a productive stateful poller. | ||
| const loopGuardStreamedCallIds = new Set<string>(); | ||
| let roundText = ''; | ||
| let roundThoughtText = ''; | ||
| let lastUsage: GenerateContentResponseUsageMetadata | undefined = | ||
|
|
@@ -1030,6 +1044,7 @@ export class AgentCore { | |
| stickyMaxOutputTokens = streamEvent.maxOutputTokensEscalated; | ||
| } | ||
| functionCalls.length = 0; | ||
| loopGuardStreamedCallIds.clear(); | ||
| roundText = ''; | ||
| roundThoughtText = ''; | ||
| lastUsage = undefined; | ||
|
|
@@ -1111,6 +1126,17 @@ export class AgentCore { | |
|
|
||
| for (const fc of chunkFunctionCalls) { | ||
| const toolName = String(fc.name); | ||
| // Provider-duplicate emissions of an already-streamed call id | ||
| // execute once (dedupeToolCallsById collapses them), so feed | ||
| // the loop guard once — request counts and result evidence | ||
| // must stay the same population. Id-less calls are never | ||
| // deduped, mirroring dedupeToolCallsById. | ||
| if (fc.id) { | ||
| if (loopGuardStreamedCallIds.has(fc.id)) { | ||
| continue; | ||
| } | ||
| loopGuardStreamedCallIds.add(fc.id); | ||
| } | ||
| if ( | ||
| checkSubagentLoop({ | ||
| type: LlmEventType.ToolCallRequest, | ||
|
|
@@ -1200,6 +1226,24 @@ export class AgentCore { | |
| terminateMode = AgentTerminateMode.LOOP_DETECTED; | ||
| break; | ||
| } | ||
| // Result-aware loop guards (issue #9450): stateful reads like | ||
| // task_list may legitimately repeat with identical arguments while | ||
| // the shared task board changes, so the detector must see each | ||
| // executed result before the next round re-emits the call. | ||
| for (const toolResult of toolCallResult.results) { | ||
| if ( | ||
| loopDetector.recordToolResult( | ||
|
Comment on lines
+1233
to
+1235
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R19-1: The result-aware exemption assumes "by the Nth identical request, the prior N−1 results have been recorded", but agent-core feeds results only after the whole batch executes (this loop runs after Witness — probe driving the real Fix: account for in-flight requests in the exemption — track outstanding requests per streak (requests streamed − results recorded) and evaluate Fix witness: add a production-order test — send 3 task_list requests, feed 3 changed results, send 2 more requests, assert the 5th 中文说明结果感知豁免依赖不变量"第 N 次相同请求时前 N−1 个结果已记录",但 agent-core 在整批执行完毕后才喂送结果(此循环位于 修复:让豁免计入在途请求(按连续段跟踪未决请求数,判定 修复见证:新增生产顺序测试——连发 3 次 task_list 请求、喂入 3 个已变化结果、再连发 2 次请求,断言第 5 次 — qwen3.8-max via Qwen Code /review (v0.22.2)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified REAL at head 2351563 (read-only code trace; no code executed or changed):
Trace: round 1 = 3 identical task_list({}) requests -> counts 1-3 (< TOOL_CALL_LOOP_THRESHOLD=5, :41), then 3 changed boards fed post-batch -> resultsObserved=3, unchangedStreak=0. Round 2: request 4 -> count 4; request 5 -> count 5 >= threshold: expectedResults=4 but resultsObserved=3 (round 2's batch has not executed yet), exemption unreachable -> halt with consecutive_identical_tool_calls despite every recorded board having changed. Any split where the 5th request does not open its round (3+2, 2+3, 1+4) hits this. This PR is scope-fused and has been non-converging for 19 patrol rounds; recorded as needs-human-decision. No code changes this round. Leaving unresolved.
yiliang114 marked this conversation as resolved.
|
||
| { name: toolResult.toolName, args: toolResult.args }, | ||
| toolResult.responseParts, | ||
| ) | ||
| ) { | ||
|
yiliang114 marked this conversation as resolved.
|
||
| terminateMode = AgentTerminateMode.LOOP_DETECTED; | ||
| break; | ||
| } | ||
| } | ||
| if (terminateMode === AgentTerminateMode.LOOP_DETECTED) { | ||
| break; | ||
| } | ||
| currentMessages = toolCallResult.messages; | ||
|
|
||
| const externalInputs = this.drainExternalInputs(options); | ||
|
|
@@ -1317,6 +1361,9 @@ export class AgentCore { | |
| text: finalText, | ||
| terminateMode, | ||
| turnsUsed: turnCounter, | ||
| ...(terminateMode === AgentTerminateMode.LOOP_DETECTED | ||
| ? { loopType: loopDetector.getLastLoopType() } | ||
| : {}), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -1625,6 +1672,14 @@ export class AgentCore { | |
| ): Promise<{ | ||
| messages: Content[]; | ||
| repeatedDuplicateProviderToolCall: boolean; | ||
| /** Executed calls with their model-visible results, in call order. | ||
| * Consumed by the loop detector for result-aware stateful-read guards | ||
| * (issue #9450). */ | ||
| results: Array<{ | ||
| toolName: string; | ||
| args: Record<string, unknown>; | ||
| responseParts: Part[]; | ||
| }>; | ||
| }> { | ||
| const responseByCallId = new Map< | ||
| string, | ||
|
|
@@ -1678,6 +1733,7 @@ export class AgentCore { | |
| return { | ||
| messages: [{ role: 'user', parts: [] }], | ||
| repeatedDuplicateProviderToolCall: true, | ||
| results: [], | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -2267,9 +2323,31 @@ export class AgentCore { | |
| timestamp: Date.now(), | ||
| }); | ||
|
|
||
| // Pair each executed call with its model-visible (finalized) result so | ||
| // the reasoning loop can feed the loop detector's result-aware guards. | ||
| const finalizedByCallId = new Map( | ||
|
yiliang114 marked this conversation as resolved.
yiliang114 marked this conversation as resolved.
|
||
| finalizedResponses.map((response) => [response.callId, response]), | ||
| ); | ||
| const results: Array<{ | ||
| toolName: string; | ||
| args: Record<string, unknown>; | ||
| responseParts: Part[]; | ||
| }> = []; | ||
| for (const fc of uniqueFunctionCalls) { | ||
| const callId = callIdByFunctionCall.get(fc) ?? fc.id ?? ''; | ||
| const finalized = finalizedByCallId.get(callId); | ||
|
yiliang114 marked this conversation as resolved.
yiliang114 marked this conversation as resolved.
|
||
| if (!finalized) continue; | ||
|
yiliang114 marked this conversation as resolved.
|
||
| results.push({ | ||
| toolName: String(fc.name ?? ''), | ||
| args: (fc.args ?? {}) as Record<string, unknown>, | ||
| responseParts: finalized.responseParts, | ||
| }); | ||
|
yiliang114 marked this conversation as resolved.
Comment on lines
+2340
to
+2344
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] R5-5: Still stands — re-proven at this head (also carries the round-7 R7-1 entrance). The guards fingerprint these post-finalization parts, and the batch-budget finalizer's Fix: embed the 中文说明[Critical] R5-5:仍然成立 —— 已在本 head 上重新证实(同时承载第 7 轮 R7-1 入口)。守卫对这些 finalize 后的部件做指纹,而批预算终结器的 修复:在 — qwen3.8-max via Qwen Code /review (v0.22.2)
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Patrol note (automated sweep): this PR has hit the scope fuse — cumulative additions are +1853 (>1500-line cap) and this finding shape has recurred across five-plus review rounds without converging. Per patrol policy no more code is added from the sweep; this thread stays open for a maintainer decision on direction (the digest/fingerprint gaps are real, but each round mints new entrance variants faster than they close). |
||
| } | ||
|
|
||
| return { | ||
| messages: [{ role: 'user', parts: toolResponseParts }], | ||
| repeatedDuplicateProviderToolCall: false, | ||
| results, | ||
| }; | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.