Skip to content

refactor(core): centralize model stream attempt state - #9683

Draft
DragonnZhang wants to merge 5 commits into
QwenLM:mainfrom
DragonnZhang:dragon/centralize-model-stream-attempt-state
Draft

refactor(core): centralize model stream attempt state#9683
DragonnZhang wants to merge 5 commits into
QwenLM:mainfrom
DragonnZhang:dragon/centralize-model-stream-attempt-state

Conversation

@DragonnZhang

Copy link
Copy Markdown
Collaborator

What this PR does

This PR introduces one shared per-attempt state model for streamed model responses and routes every raw model-stream consumer through it. Text, thoughts, tool calls, usage, response identifiers, finish reasons, and truncation state now follow the same retry, continuation, fallback, and compression semantics across the main turn, subagents, forked queries, speculative execution, and ACP-driven flows.

Fresh retries and model fallbacks discard the failed attempt. Continuation retries preserve accumulated text and thoughts while clearing tool calls and attempt-scoped metadata. Message-display hooks and loop detection now follow the same reset decision.

Why it's needed

Stream consumers previously maintained independent copies of attempt state. Those implementations had drifted: subagents dropped the first half of continuation retries, fallback output could be appended to stale text, ACP background responses retained failed-attempt usage, and stale tool calls or response identifiers could survive a reset. Centralizing the lifecycle removes those inconsistencies and gives future protocol-boundary work one place to preserve retry semantics.

Reviewer Test Plan

How to verify

  1. Stream partial text, usage, and a tool call, then emit a fresh retry or model fallback followed by a successful response. Confirm only the successful attempt contributes final text, usage, tool execution, response identifiers, finish reason, and truncation state.
  2. Stream partial text and thoughts, then emit a continuation retry. Confirm the text and thoughts continue without duplication while pre-retry tool calls and metadata are discarded.
  3. Exercise the main turn, subagent, forked-query, speculative, foreground ACP, daemon-continuation, cron, and background-notification paths. Confirm they produce the same final-attempt state and never execute stale tool preparations.
  4. Enable the MessageDisplay hook and force a fresh retry. Confirm the final hook payload contains only the successful attempt; force a continuation retry and confirm both text segments remain.

Evidence (Before & After)

N/A — this is a non-UI stream-lifecycle refactor. Deterministic regression fixtures reproduce the stale-state behavior before the change and pass after the shared attempt state is applied.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Local macOS workspace with Node.js v24.18.0. Verified with 238 focused core tests, two client integration tests, all 669 ACP Session tests, targeted ACP reset tests, the full repository build, and workspace type checking. Targeted lint for every changed source and test file passed.

Risk & Scope

  • Main risk or tradeoff: Incorrectly classifying a retry as fresh versus continuation could drop or duplicate text; dedicated state and consumer-level regression tests cover both branches.
  • Not validated / out of scope: This does not replace the current Google-shaped request/response boundary with Pi or another provider-neutral protocol. Already-delivered streaming chunks are not retroactively retracted; final and captured attempt state is corrected.
  • Breaking changes / migration notes: None. The existing stream event contract is unchanged.

Linked Issues

N/A

中文说明

本 PR 做了什么

本 PR 为模型流式响应引入了一套共享的单次尝试状态模型,并让所有直接消费原始模型流的路径统一经过该模型。主会话、子 Agent、分叉查询、推测执行和 ACP 流程现在对文本、思考、工具调用、用量、响应标识、结束原因和截断状态采用一致的重试、续写、回退和压缩语义。

全新重试和模型回退会丢弃失败尝试;续写重试会保留已经累积的文本和思考,同时清除工具调用与本次尝试相关的元数据。MessageDisplay hook 和循环检测也遵循同一个重置判断。

为什么需要它

此前各个流消费者分别维护自己的尝试状态,具体实现已经发生偏移:子 Agent 会丢失续写重试的前半段,模型回退输出可能拼接到旧文本后面,ACP 后台响应会保留失败尝试的用量,重置后还可能残留旧工具调用或响应标识。集中管理生命周期可以消除这些不一致,并为未来调整协议边界提供一个统一保留重试语义的位置。

Reviewer 测试计划

如何验证

  1. 先流式返回部分文本、用量和一个工具调用,再触发全新重试或模型回退,随后返回成功响应。确认最终文本、用量、工具执行、响应标识、结束原因和截断状态只来自成功尝试。
  2. 先流式返回部分文本和思考,再触发续写重试。确认文本和思考能够无重复地继续,同时重试前的工具调用和元数据已被丢弃。
  3. 覆盖主会话、子 Agent、分叉查询、推测执行、前台 ACP、daemon continuation、cron 和后台通知路径。确认它们得到一致的最终尝试状态,并且不会执行过期的工具准备。
  4. 启用 MessageDisplay hook 并触发全新重试。确认最终 hook 载荷只包含成功尝试;再触发续写重试,确认两段文本都会保留。

证据(修改前与修改后)

N/A——这是非 UI 的流生命周期重构。确定性的回归夹具可以在修改前复现状态残留问题,并在应用共享尝试状态后通过。

测试平台

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

环境(可选)

本地 macOS 工作区,Node.js v24.18.0。已验证 238 个核心定向测试、2 个客户端集成测试、全部 669 个 ACP Session 测试、ACP 重置定向测试、全仓构建和工作区类型检查。本次所有变更源文件与测试文件的定向 lint 均通过。

风险与范围

  • 主要风险或取舍:如果把重试错误分类为全新重试或续写重试,可能丢失或重复文本;独立状态测试和消费者级回归测试覆盖了两个分支。
  • 未验证或不在范围内:本 PR 不会把当前 Google 形态的请求/响应边界替换为 Pi 或其他供应商无关协议。已经发送出去的流式 chunk 不会被追溯撤回;最终状态和捕获状态会被正确修正。
  • 破坏性变更或迁移说明:无。现有流事件协议保持不变。

关联 Issue

N/A

DragonnZhang added a commit to DragonnZhang/qwen-code that referenced this pull request Aug 23, 2026
Same latent main-side violation as QwenLM#9682/QwenLM#9683-era fixes: QwenLM#9587 grew the
workflow without a baseline update; record the new size as the check
message directs (precedent QwenLM#9747).
Conflict resolution notes:

- agent-core.ts: kept the centralized attempt-state path. Upstream added
  isContinuation propagation on the legacy inline retry handling; the
  state machine already covers it — ModelStreamAttemptState.accept()
  captures the whole retry event (incl. isContinuation) into
  transition.retryInfo, and the Retry resetEvent built from it carries
  isContinuation into checkSubagentLoop/LoopDetectionService.
- loopDetectionService.ts: adopted upstream's separate Retry /
  ModelFallback heuristic cases (semantically identical to this PR's
  grouped case: continuation retries keep content state, replays and
  fallbacks reset it) and kept this PR's always-on cap-rollback
  extension to ModelFallback.
- agent-core.test.ts / speculation.test.ts: kept both new suites
  (model stream attempts here; approval-response deduplication and the
  session-scoped cache-safe-params test upstream). Added getSessionId
  to the fallback test's config mock for upstream's session-scoped
  getCacheSafeParams lookup.

Verified: tsc --build (packages/core) clean; vitest green for
agent-core, speculation, loopDetectionService, model-stream-attempt-state,
and turn suites.
The attempt-state centralization normalized the Retry event emitted in
AgentCore to always carry isContinuation (false for fresh retries).
Upstream's subagent loop-detection contract (agent-headless.test.ts,
'forwards retry events to subagent loop detection') requires fresh
retries to omit the property entirely, matching the pre-refactor
conditional forwarding. Restore the original event shape by only
spreading isContinuation when the upstream retry event carries it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant