Appearance
功能规格说明:实时内容流式传输
创建日期:2025-11-19
用户场景与测试 (必填)
用户故事:实时助手消息流式传输(优先级:P1)
用户在助手响应生成过程中体验即时、增量的更新,类似于 ChatGPT 的打字效果。
为什么是这个优先级:这是面向用户的核心体验改进,提供即时视觉反馈,使系统感觉更响应和引人入胜。
独立测试:可以通过向助手发送任何消息并观察响应文本逐字符增量出现,而不是完成后一次性出现来完整测试。
验收场景:
- 假设用户发送了消息,当助手开始生成响应时,则文本内容在 CLI 消息列表中逐字符出现
- 假设助手正在流式传输响应,当新内容块到达时,则消息内容增量更新而不刷新整个界面
- 假设流式传输响应被中断,当用户中止消息时,则部分内容保持可见且格式正确
用户故事:实时工具参数流式传输(优先级:P2)
用户在 AI 构建函数调用时看到工具调用参数被增量构建,提供对 AI 推理过程的透明度。
为什么是这个优先级:通过实时展示 AI 的决策过程来增强用户信任和理解,对具有许多参数的复杂工具调用特别有价值。
独立测试:可以通过请求触发工具调用的操作并观察参数在折叠视图中增量出现,而展开视图显示进入展开模式时参数的快照来测试。
验收场景:
- 假设助手正在生成工具调用,当参数数据流式传入时,则compactParams 显示在折叠视图中实时更新
- 假设用户启用了展开视图,当工具参数正在生成时,则显示展示进入展开模式时参数的静态快照
- 假设多个工具调用正在生成,当处于折叠视图时,则每个工具的 compactParams 独立实时更新
用户故事:无缝视图模式切换(优先级:P3)
用户可以在折叠和展开视图模式之间切换,折叠模式显示实时流式传输,展开模式显示完全静态的内容,在内容生成期间不进行任何更新。
为什么是这个优先级:确保流式传输在折叠模式下最佳工作,同时在展开模式下提供完全稳定、无干扰的阅读体验,在内容生成期间永不变化。
独立测试:可以通过触发流式传输内容并在流式传输过程中在折叠/展开模式之间切换来测试。
验收场景:
- 假设内容在折叠模式下流式传输,当用户切换到展开模式时,则流式传输停止,内容显示为切换时刻的静态快照
- 假设用户处于展开模式,当新内容开始生成时,则显示保持为进入展开模式时的静态快照
- 假设用户处于展开模式,当在生成期间切换到折叠模式时,则流式传输从当前完成点恢复
用户故事:工具块阶段更新(优先级:P2)
SDK 集成者希望通过确定性阶段(start、streaming、running、end)追踪工具执行的生命周期,以提供准确的 UI 反馈。
为什么是这个优先级:对"启动中"、"流式输出中"、"仍在运行"和"已完成"状态的清晰区分使集成者能够显示准确的状态消息和最终结果。
独立测试:订阅 onToolBlockUpdated,触发工具执行,并验证事件按正确顺序以预期的 stage 值到达。
验收场景:
- 假设工具执行开始,当
onToolBlockUpdated触发时,则收到的第一个事件包含stage="start"和工具的显示名称 - 假设工具发出流式输出,当
onToolBlockUpdated以stage="streaming"触发时,则每个事件包含最新的parametersChunk - 假设长时间运行的工具,当进度更新发生但没有新块时,则
onToolBlockUpdated发出stage="running" - 假设工具处于
running阶段,当其产生增量结果或短结果时,则每次stage="running"事件都必须携带该工具调用自始至终不变的稳定展示字段(compactParams、name),而不是只在首次 running 事件携带——消费端(如 CLI)以 last-value-wins 节流丢弃中间事件后,仍能在 running 阶段正确渲染 compactParams - 假设工具完成,当
onToolBlockUpdated发出最终更新时,则事件使用stage="end"并携带最终输出或错误摘要 - 假设任何
onToolBlockUpdated事件,则有效载荷不包含已弃用的isRunning标志
用户故事:实时工具结果流式传输(优先级:P2)
用户在长时间运行的工具(如 bash)执行时实时看到其输出,提供对工具进度的即时反馈。
为什么是这个优先级:对监控长时间运行的操作和提供响应式用户体验至关重要。
独立测试:运行长时间运行的 bash 命令并验证工具块的结果内容在 UI 中增量更新。
验收场景:
- 假设工具处于
running阶段,当它产生增量结果时,则onResultUpdate回调必须被触发,携带最新的累积结果 - 假设工具处于
running阶段,当它产生增量短结果时,则onShortResultUpdate回调必须被触发,携带最新的短结果 - 假设工具正在流式传输结果,当UI 接收更新时,则它必须相应更新工具块的 result 和 shortResult
用户故事:思考用时展示(优先级:P2)
用户在查看助手的推理(思考)块时,希望看到本次思考所花费的时间:进行中实时跳动计时,结束后显示最终耗时,历史会话重新加载后仍能看到最终耗时。
为什么是这个优先级:思考耗时让用户对 AI 的推理成本有直观感知,进行中的实时计时提供"仍在工作"的反馈,结束后的固定耗时便于回顾历史会话。
独立测试:触发一次会产生推理块的对话,观察思考块头部在进行中每秒递增,结束后锁定为固定耗时;随后刷新或重新加载该历史会话,验证仍显示相同的最终耗时。
验收场景:
- 假设推理块正在生成(
stage="streaming"),当时间流逝时,则思考块头部基于该块的开始时间每秒动态递增显示当前已用秒数(如"思考中 9s") - 假设推理块生成结束(
stage="end"),当头部更新时,则停止动态计时并显示由起止时间算出的固定最终耗时(如"思考 (用时 15s)") - 假设历史会话被重新加载或界面刷新,当推理块以
stage="end"出现时,则头部直接静态显示持久化的最终耗时,而不重新从零计时 - 假设推理块开始时间缺失或起止时间异常(如结束早于开始),当头部渲染时,则不显示耗时而非显示负值或错误值
用户故事:长会话流式输出保持流畅(优先级:P1)
作为 CLI 或插件端的使用者,我希望在积累了大量消息的会话中,助手流式输出时界面依然即时、平滑地更新,以便消息越多时交互越不卡顿。
为什么是这个优先级:当前每次流式 chunk 都会触发 onMessagesChange 全量回调,CLI 侧随之全量 setMessages、插件宿主经 stdio 全量下发消息列表,序列化与传输成本随会话长度线性增长,是长会话流式卡顿的主要来源;且它与增量回调在时间上重复(同一 chunk 同时触发 onAssistantContentUpdated 与 onMessagesChange),造成数据冗余与竞态。为此移除 onMessagesChange,使流式更新全链路走增量通道;bash 模式命令以 user 消息 + bash tool block 承载,经 userMessageAdded/toolBlockUpdated 增量定位,完整消息列表仅在"拉取"场景(初始化、clear、restore)传输——压缩与 rewind 都不进该名单:压缩对 UI 显示流是纯追加(只折叠 API 上下文、保留全量历史并把摘要块追加到显示流末尾),因此经 compactBlockAdded 通知携带摘要块那条完整 Message、由宿主 appendMessage 增量送达 webview;rewind 是截断而非追加,宿主与 webview 各自在本地按同一条规则切自己的副本(保留锚点消息最后一次出现之前的内容),宿主只回发一条不带消息载荷的回执 rewindResult(带 requestId 归属键与 ok/error)来授权这次本地截断,整份列表因此不在这条路径上重传,回滚耗时与会话总长脱钩;仅当请求未携带 requestId(旧 webview / 远程页)时才回落到 getMessages 全量拉取。进一步地,增量负载从"chunk + accumulated 并存"收敛为"纯 chunk delta"(tool block streaming 只携带 parametersChunk),SDK 回调与跨进程(stdio)通知统一不再携带 accumulated,使单个通知的序列化成本与已流式内容总长度解耦,从根源消除传输层随流式累积的内存/带宽压力;进程内消费者(CLI、print-cli)改为消费纯 chunk 并自行累积追加(详见下方"状态管理架构"与 2026-08-08 会议记录)。
独立测试:构造一个包含数百条消息的长会话并发送一条消息触发流式响应:CLI 消息内容增量就地更新、无整屏刷新闪烁;插件侧记录 CLI stdout,流式期间只出现增量通知、无全量消息推送。执行 ! bash 模式命令后消息列表正确显示命令输出。
验收场景:
- 假设会话已积累大量消息,当助手开始流式响应时,则CLI 与插件界面通过增量回调就地更新,不整屏刷新、不卡顿
- 假设助手响应正在流式传输,当每个内容 chunk 到达时,则SDK 只触发
onAssistantContentUpdated等增量回调,不再触发任何携带完整消息列表的回调 - 假设CLI 正在渲染流式响应,当新 chunk 到达时,则消息内容就地更新,不再执行全量
setMessages - 假设CLI 收到 bash 模式命令消息增量(
userMessageAdded创建 user 消息,onToolBlockUpdated更新name: "Bash"的 tool block),当需要渲染命令消息时,则按 messageId 就地创建/更新消息块,无需读取agent.messages - 假设插件 webview 需要完整会话(webviewReady / clearChat / restoreSession),当触发上述任一场景时,则宿主主动调用
getMessages请求拉取并下发当前列表(压缩后的上下文),而非订阅持续的整表推送 - 假设子代理(subagent)运行中,当其消息变更时,则
SubagentManager通过instance.messageManager.getMessages()拉取最新列表维护instance.messages与usedTools,并继续转发onSubagentMessagesChange,不再依赖子代理的onMessagesChange - 假设助手响应正在流式传输,当每个内容/推理 chunk 到达时,则增量回调与 stdio 增量通知都只携带
chunk(增量片段)+messageId+stage,不再携带accumulated累积值;进程内消费者(CLI、print-cli)与跨进程宿主(agentBridge)统一消费纯 chunk,自行累积追加 - 假设工具参数正在经 stdio 流式传输,当
stage="streaming"通知到达时,则只携带parametersChunk(增量),不再携带累积parameters;当stage="end"通知到达时,则携带全量parameters+result作为一次性权威值 - 假设宿主(VSCE/桌面/CLI)对增量通知做了节流(如 16ms/500ms 窗口),当窗口内累积了多个 chunk 时,则按到达顺序将窗口内所有 chunks 拼接为一个合并 delta 发送,而非 last-value-wins 丢弃中间值——丢弃中间 chunk 会造成内容永久缺失,只能由后续
getMessages拉取自愈 - 假设webview 正在追加流式 delta,当触发
getMessages拉取全量列表时,则全量响应整体替换消息块为权威快照,随后到达的 delta 继续追加;管道 FIFO 保证拉取响应包含其之前发出的所有 chunk,不发生重复或错乱 - 假设会话发生压缩(自动或手动),当压缩写入完成时,则宿主不发起
getMessages拉取,而是收到compactBlockAdded通知(载荷含摘要块那条完整Message)并把它经appendMessage增量追加到 webview 消息列表末尾(同时写入宿主自己的消息缓存,保证后续setInitialState不丢该块);仅当message字段缺席(版本错配)时才回落到getMessages全量拉取 - 假设用户确认
/rewind回滚到某条用户消息,当宿主完成会话文件重写时,则它按与 webview 相同的规则切掉自己缓存中该消息最后一次出现之后的部分、并回发一条不含消息载荷的rewindResult(携带请求的requestId作归属键,以及ok/error);webview 收到匹配的ok: true回执后对本地列表做同样的截断,随后的会话快照以「保留 webview 现有列表」的形式下发(列表不入载荷),整份消息列表不在这条路径上传输——回滚耗时与会话总长脱钩 - 假设
/rewind的请求缺少requestId(旧 webview 或远程页),当宿主完成重写时,则它退回getMessages拉取并下发全量列表,因为这些客户端收不到回执、只能靠快照更新;当回执为ok: false时,则两端列表都保持原样不动,当回执携带的requestId与在途请求不匹配(晚到或已被取代)时,则必须丢弃该回执、不得截断列表
用户故事:增量消费端以快照推入消息(优先级:P1)
作为 CLI 或其他进程内增量消费端,我推入自己的消息状态时使用 SDK 消息对象的快照而非活引用,以便流式增量期间 SDK 就地更新消息块不会与消费端自己的累积追加相互污染。
为什么是这个优先级:2026-08-08 起 SDK 回调只携带纯 chunk delta,消费端必须自行累积追加(content += chunk);而 SDK 内部仍将累积全文写入消息块(先写全量、再按"新值 slice 当前长度"计算并回调 chunk delta)。若消费端在 onAssistantMessageAdded/onUserMessageAdded 时直接把 agent.messages 中的消息对象活引用推入自己的状态,第一个 delta 到达时 SDK 已把共享块内容替换为完整累积值,消费端追加逻辑会读到此已更新值再拼接该 delta——首个 delta 被重复计数,表现为"reasoning 第一个单词重复"(如 LetLet me think about this.)。文本内容同理受影响,只是文本流通常在消息解耦后才出现而不易暴露。
2026-08-17 补充:快照必须在回调触发时刻立即捕获(onAssistantMessageAdded/onUserMessageAdded 被调用时同步执行 snapshotMessage(msg) 并保存结果),不能在 React state updater 内延迟执行。原因是 React 会把同一同步 tick 内的多次 setMessages 批处理,updater 函数到 flush 时才运行——而 SDK 的 addAssistantMessage() → 就地写入首个 chunk → 首个 delta 回调恰好处于同一同步 tick(aiManager.ts 的 onContentDelta/onReasoningDelta),flush 时延迟快照复制到的已是变异后的消息块,首个 delta 再追加即翻倍(HelloHello)。该竞态表现为"推理流结束后正文首个单词重复"(首个 content delta 与 messageAdded 落入同一批时),或首个 reasoning delta 重复(reasoning 折叠区,不易察觉)。
独立测试:构造一个带 reasoning 流式的会话,用与 CLI 相同的消费模式(推入活引用 + 追加 reducer)验证出现首词重复;改用快照推入({ ...m, blocks: m.blocks.map(b => ({ ...b })) })后同一断言通过。另有同批回归测试:在同一同步 tick 内连续调用 onAssistantMessageAdded → 就地变异消息块 → 首个 delta 回调(无 await 间隔,与真实 SDK 顺序一致),断言消息内容不翻倍(content Hello 非 HelloHello、reasoning Let 非 LetLet、reasoning 收尾后首个 content chunk 同批时两通道均不重复)。
验收场景:
- 假设增量消费端(CLI)收到
onAssistantMessageAdded/onUserMessageAdded,当将消息加入自己的状态时,则推入的是消息对象及其 blocks 的快照(深拷贝至少一层:消息与 blocks),而非 SDK 内部消息对象的活引用 - 假设助手消息流式输出 reasoning,当消费端按
chunkdelta 累积追加时,则追加结果与 SDK 存储的完整推理内容一致,首个 delta 不重复(首个单词不翻倍) - 假设消费端通过拉取通道(
agent.messages/getMessages)获取全量列表,当把拉取结果推入状态时,则同样以快照推入,不与 SDK 内部对象共享引用 - 假设消费端已按快照推入消息,当 SDK 继续就地更新其内部消息块时,则消费端状态不受影响,仅通过后续 delta 回调获得更新
- 假设消息创建回调与首个 delta 回调落在同一 React 批处理内(同一同步 tick:
addAssistantMessage()→ 就地写入首个 chunk → 首个 delta 回调),当消费端把快照推入状态时,则快照在回调触发时刻立即捕获(updater 外),复制的是变异前的空块;首个 delta 追加后首个单词不翻倍
用户故事:CLI 消息状态全量立即渲染,无数据层节流(优先级:P1)
作为 CLI 使用者,我希望所有消息状态更新——文本/推理流式 delta、工具全阶段(start/streaming/running/end)、消息新增与 error 块——到达即立即应用到消息状态并渲染,以便流式正文保持逐 delta 的打字机反馈、工具输出实时可见,终端写入频率由 Ink 内置 maxFps: 30 兜底,不因数据层节流引入任何显示延迟或窗口/排队复杂度。
为什么是这个优先级:节流设计经历三轮演进——2026-08-17 曾因节流器 trailing edge 冲刷与全量列表替换(refreshMessages)交错导致内容重复而移除 CLI 节流(见下方会议记录);2026-08-19 恢复单一 500ms 窗口并在竞态源头修复(快照拉取后 cancel());2026-10-15 改为按「更新是否改变卡片高度」分流(文本/推理立即、工具 running 节流)并提供实验开关 WAVE_STREAM_THROTTLE_MS。2026-10-15 用户以 WAVE_STREAM_THROTTLE_MS=0 在真实终端初步体验(长文本流式 + 工具运行输出),确认全立即渲染观感正常、无闪烁困扰,据此彻底移除数据层节流:节流的唯一存在理由是压制工具 running 短结果行数震荡(1–N 行反复推拉下方内容)的闪烁担忧,实测在 Ink log-update 只重写动态区尾部 + maxFps: 30 合帧下不成立;逐 delta 渲染的 React commit 成本对典型会话规模为亚毫秒级(2026-08-17 评估);节流只增加显示延迟与窗口/排队/冲刷/快照取消的实现复杂度。若未来真实场景出现闪烁,应在渲染层解决(如 Ink fork 增量 diff + 双缓冲虚拟屏,参照 Claude Code 的 16ms 帧节流),而非恢复数据层节流。
独立测试:构造 bash 前台任务,连续触发 running 阶段 shortResult 更新,断言每个更新立即应用(无窗口合并、无排队延迟);构造流式期间的全量刷新(refresh-interleave),验证快照替换后新 delta 立即追加、内容不重复不丢失;运行既有双计数回归测试(content/reasoning 同批 double-count、refresh-interleave duplication)继续通过。
验收场景:
- 假设 CLI 正在消费消息状态更新,当任一消息更新回调到达时,则更新立即以 updater 形式(
(prev: Message[]) => Message[])应用到消息状态——无节流窗口、无排队、无延迟;到达顺序即应用顺序 - 假设模型逐 token 产出文本/推理(如 30ms/chunk),当每个 delta 到达时,则消息内容逐 delta 立即就地更新(打字机效果),终端写入频率由 Ink 内置
maxFps: 30约束,不出现逐 chunk 整屏重绘 - 假设工具运行阶段逐 chunk 更新
shortResult/result(bash 前台任务经onToolBlockUpdatedstage="running"高频到达),当每个更新到达时,则立即应用、短结果实时增长(实测行数震荡在 Ink 合帧下无可感知闪烁) - 假设工具
stage="end"事件到达,当其应用时,则直接以权威终值收尾(无 flush 语义——无 pending 更新可冲刷) - 假设流式期间发生了全量列表替换(
refreshMessages,/clear、/compact、/rewind、Ctrl+O 折叠触发),当替换发生时,则权威快照直接覆盖当前状态(无需 cancel——无 pending 更新可复活);替换完成后到达的 delta 立即追加在快照之上,不丢失、不重复 - 假设同一工具块的
streaming参数 chunk 与running权威快照先后到达,当两者依次立即应用时,则最终stage="running"、parameters为权威值——到达顺序即应用顺序(强于原节流 FIFO 队列保证),且同一工具块的参数流在streaming阶段结束后不再产生 chunk(SDK 协议保证);即便出现协议外非法交错,也会被随后的end权威值覆盖自愈 - 假设长会话中流式输出,当消息不断积累时,则界面依然平滑即时更新,不因无节流的逐 delta commit 而卡顿(与「长会话流式输出保持流畅」验收场景 1 一致)
- 假设节流基础设施已移除,当检查消费端代码时,则
createThrottledUpdater、resolveStreamThrottleMs、DEFAULT_STREAM_THROTTLE_MS与实验开关WAVE_STREAM_THROTTLE_MS均已删除(无死代码;回滚路径由 git 历史保留),refreshMessages、isExpanded 切换与卸载清理不再有 cancel 调用 - 假设存在内容重复的回归测试,当运行现有双计数回归测试(content/reasoning 同批 double-count、refresh-interleave duplication)时,则测试继续通过
用户故事:CLI 静态块 reopen 不重复渲染冻结前缀(优先级:P1)
作为 CLI 使用者,我希望推理/正文块在一次 AI 调用内交错续写(同块 end → streaming 重新打开)时,屏幕上已显示的内容不重复,流式增量继续实时可见。
为什么是这个优先级:CLI 用 Ink <Static> 渲染历史消息,其 append-only 语义(items.slice(index) 只追加新项、已渲染项永不更新)与消息列表的"静态/动态分区"叠加产生渲染层重复:文本/推理块一旦进入静态区,其内容就冻结在终端输出中;当同一块因模型交错输出(如 T→R→T 或 R→T→R)重新打开(stage 从 end 回到 streaming)时,该块从静态区迁移回动态区,动态区以全量累积内容重新渲染——屏幕上"冻结前缀 + 全量内容"首词重复(如正文 The answer is 42 的 The answer 出现两次)。这是第三类首词重复(前两类为状态层:活引用/批处理快照、节流残留竞态),根因在渲染层而非状态层——SDK→CLI 状态始终一致,纯 delta 追加无重复,重复发生在 Ink 输出拼接。
独立测试:构造单条消息的阶段序列(推理流 → 收尾 → 正文流 → 推理 reopen 续写 → 收尾),逐阶段 rerender 并收集测试 harness(tests/helpers/inkHarness.ts)的 frames 全部帧,断言每一帧中每个块内容至多出现一次(MessageList.block-reopen-duplication.test.tsx:推理 reopen、正文 reopen=用户症状、reopen 期间增量可见三类用例)。无修复时断言失败(冻结前缀 + 动态全量 → 计数 2),修复后通过。
验收场景:
- 假设推理块已收尾(
stage="end",内容已进入静态区),当同一块重新打开流式续写(stage="streaming")时,则屏幕上该块已显示的前缀不重复出现,动态区只渲染续写增量(content超出冻结前缀的部分) - 假设正文块已收尾(如模型先输出正文、再推理、再继续正文,正文块
end→streaming),当正文续写流式输出时,则正文首词不重复(用户可感知症状:推理流结束后正文首个单词重复) - 假设reopen 块在动态区以增量渲染,当新增量到达时,则续写内容实时可见(增量 + 冻结前缀拼接后等于完整内容,视觉上无跳变)
- 假设reopen 块再次收尾,当其回到静态区时,则不因位置已被冻结项占据而重新渲染全量内容,屏幕保持稳定
- 假设reopen 块曾被静态写入过多次(多次 reopen),当后续续写流式输出时,则增量始终相对首次冻结内容计算(静态区永不更新已渲染项),多轮续写拼接正确
- 假设消息被 rewind/clear 移除,当再次渲染时,则冻结内容记录随块移除而清理,不残留过期键
边界情况
- 当网络连接较差且流式块乱序到达或延迟时会发生什么?
- 系统如何处理可能跨流式块分割的部分 UTF-8 字符?
- 如果用户在内容流式传输时从折叠切换到展开模式会发生什么?
- 如果流式传输因 API 速率限制或错误而中断,界面行为如何?
- 当非常长的内容流超过终端显示限制时会发生什么?
- 当推理块的开始时间存在但结束时间缺失(流被中止)时,耗时如何展示?
- 如果某个增量通知在传输中丢失(进程异常、节流实现错误),UI 内容如何恢复?→ 丢失的 delta 由后续
getMessages拉取的全量权威快照整体替换自愈 - 如果在追加流式 delta 期间插入了一次
getMessages拉取(如恢复会话),是否会重复或丢失内容?→ 不会:管道 FIFO 保证拉取响应按序到达且包含其之前全部 chunk;UI 以响应快照为准整体替换,后续 delta 继续追加
状态管理架构
- Agent SDK 职责:在内部管理所有消息状态并更新消息;变更通过增量回调(
onUserMessageAdded、onAssistantMessageAdded、onAssistantContentUpdated、onAssistantReasoningUpdated、onToolBlockUpdated、onNoticeAdded等)对外发布;完整列表通过agent.messagesgetter(同进程)或getMessages请求(stdio 跨进程)按需获取。onMessagesChange全量回调已移除 - 增量回调用途:为第三方集成、扩展、CLI 与示例(如
packages/code/src/print-cli.ts和packages/agent-sdk/examples)提供实时流式数据;增量回调是消息状态同步的唯一推送通道 - SDK 回调负载:
onAssistantContentUpdated/onAssistantReasoningUpdated只提供chunk(增量)+messageId+stage,不再携带accumulated累积值,进程内消费者(CLI、print-cli)自行累积追加;onToolBlockUpdated在stage="streaming"时只提供parametersChunk(增量),start/running/end阶段携带权威parameters(end 为最终值,一次性权威)。SDK 内部仍将 chunk 追加进内存toolBlock.parameters保持累积,只是累积值不再暴露到外部回调 - 跨进程 wire 负载(纯 delta):agentBridge 原样透传增量回调负载——
assistantContentUpdated/assistantReasoningUpdated只携带{messageId, chunk, stage}(SDK 回调本身已无累积字段,无需剥离);toolBlockUpdated在stage="streaming"时只携带parametersChunk,stage="end"时携带全量parameters+result作为权威值。消费端负责累积(追加),丢失的 delta 由getMessages拉取的全量快照自愈 - UI 状态流:CLI 直接订阅增量回调就地更新消息,消息状态更新按更新是否改变卡片高度分流——工具
running阶段逐 chunk 的shortResult/result高频更新经 500ms 节流入口updateMessages(updater)排队应用(leading edge 立即应用、窗口内更新按到达顺序 FIFO 排队、stage="end"收尾时flush()冲刷),其余更新(文本/推理 delta、工具start/streaming/end、消息新增与 error 块等结构性更新)直接应用、不被窗口延迟,渲染频率由节流窗口与 Ink 内置 30fps 输出节流共同兜底(见「CLI 消息状态按更新是否改变卡片高度分流渲染,全量刷新时快照安全取消」用户故事);插件/桌面经 stdio 增量通知(userMessageAdded、assistantContentUpdated、toolBlockUpdated等,纯 delta 负载)驱动 webview 增量 reducer——文本/推理块追加 chunk、工具块追加parametersChunk,end 时以权威值终结;bash 模式命令(!ls)以 user 消息 + bash tool block 承载,输出实时性由toolBlockUpdated增量通知驱动,无需拉取全量列表;需要完整列表的场景(初始化、rewind、clear)主动拉取,拉取响应整体替换为权威快照——压缩不在其中:它对 UI 显示流是纯追加,经compactBlockAdded(载荷含摘要块那条Message)→ 宿主appendMessage增量追加,不进拉取名单。进程内消费端把 SDK 消息对象推入自有状态时必须克隆(消息 + blocks 至少一层)且在回调触发时刻立即捕获(不得在 React state updater 内延迟求值——批处理 flush 晚于 SDK 就地变异,见"增量消费端以快照推入消息"用户故事),不得持有 SDK 内部活引用——SDK 会就地更新共享块,与消费端累积追加叠加会重复计数(见"增量消费端以快照推入消息"用户故事)。渲染层:CLI 消息列表的静态/动态分区必须遵守 append-only 约束——进入<Static>区的块(其内容已冻结在终端输出中)若因同块 reopen(end→streaming)回到动态区,动态区只能渲染"超出冻结前缀"的增量(content.slice(冻结长度)),否则冻结前缀与全量重渲染叠加造成首词重复(见「CLI 静态块 reopen 不重复渲染冻结前缀」用户故事);冻结内容按块 key 记录(写入静态区的时刻),多轮 reopen 恒相对首次冻结内容计算 - 节流语义:SDK 回调与 wire 通知均只携带纯 delta。CLI 进程内消费端按「更新是否改变卡片高度」分流(取代早期 content/reasoning/tool 三通道节流器与 2026-08-19 的单一窗口,见「CLI 消息状态按更新是否改变卡片高度分流渲染,全量刷新时快照安全取消」用户故事):高度敏感更新(
onToolBlockUpdated且stage="running"——工具逐 chunk 上报的shortResult/result,短结果行数在 1–N 行间震荡)以(prev: Message[]) => Message[]updater 形式提交给 500ms window-concat 节流入口updateMessages,leading edge 立即应用并开启窗口,窗口内更新按到达顺序排队,窗口关闭时以单个组合 updater 一次性应用(顺序 = 到达顺序,无更新丢失、无 last-value-wins 丢弃中间值);stage="end"/收尾事件先flush()冲刷排队更新再应用收尾信号。非高度敏感更新(文本/推理 delta——高度单调递增、工具start/streaming/end、消息新增/error 块等一发式结构性更新)直接应用、不被窗口延迟,文本流式由此保持逐 delta 的即时渲染。running 阶段shortResult/result更新仍在窗口内合并,bash 短结果行数变化不逐 chunk 触发 React commit(消除闪烁);同一工具块的参数流在streaming阶段结束后不再产生 chunk,故 running 在窗口内排队期间不会被同块的晚到 chunk 插队覆盖权威值,阶段回退在分流下由到达顺序本身杜绝(取代createToolStreamingThrottle的"running 丢弃该 tool buffered deltas"逻辑)。全量刷新与节流窗口的交互:任何把 CLI 消息状态整体替换为权威快照的操作(refreshMessages:/clear、/compact、/rewind、Ctrl+O 折叠)必须在拉取快照后cancel()节流窗口——pending 内更新已包含在快照中,丢弃无损失;拉取后的更新开启新窗口追加在快照之上,保证刷新前后内容既不重复也不丢失。为配合该语义,SDK 必须在每次 running 事件中重复携带compactParams等稳定展示字段(见"工具块阶段更新"验收场景 4),保证丢弃中间事件不丢失这些字段 - 清晰分离:增量回调是消息状态管理的正式对外通道;全量数据按需获取,避免随每次流式 chunk 序列化整个列表
澄清
2025-11-19 会议
- 问:展开模式流式行为 → 答:展开模式下永不流式传输 - 所有内容仅在完成后出现
- 问:展开模式在生成完成时的内容更新 → 答:即使内容完成也不更新
- 问:展开模式中的内容可见性 → 答:显示切换到展开模式时内容的快照
- 问:onAssistantContentUpdated 回调数据格式 → 答:两个参数:(chunk: string, accumulated: string)
- 问:onToolBlockUpdated 回调参数格式 → 答:现有签名加上用于增量更新的新
parametersChunk字段与累积参数并存 - 问:onAssistantMessageAdded 回调参数 → 答:不接收参数,内容/工具由专用流式回调处理
- 问:流式传输期间的状态管理 → 答:Agent SDK 在内部管理消息状态并通过增量回调对外发布;完整列表按需拉取(
agent.messages/getMessages)。流式回调为第三方集成、扩展和示例提供数据(2026-08-04 起onMessagesChange已移除,见下方会议记录)
2026-04-09 会议(PR #928)
- 问:TextBlock/ReasoningBlock 阶段追踪 → 答:为两种类型添加
stage字段("streaming" | "end") - 问:BangBlock 运行状态 → 答:用
stage: "running" | "end"替换isRunning: boolean以保持一致性 - 问:何时终结流式块 → 答:在
updateToolBlock和addToolBlock中添加工具块之前调用finalizeCurrentStreamingBlocks() - 问:MessageList 中的流式块渲染 → 答:过滤掉流式文本/推理块以避免流式传输期间的频繁高度变化
- 问:动态块渲染范围 → 答:如果消息中的任何块处于运行/流式状态,该消息中的所有块都是动态的
- 问:流式工具参数显示 → 答:当没有
compactParams可用时,内联显示最后 30 个字符并展平换行符(\n→\\n) - 问:流式模式默认值 → 答:在 useChat Agent 创建中将
stream: false改为stream: true - 问:Token 状态更新器 → 答:将
throttledSetTokens添加到 useCallback 依赖数组以防止渲染错误
2026-07-22 会议(思考用时)
- 问:思考耗时的数据从哪来 → 答:由 Agent SDK 记录
ReasoningBlock的起止时间并随消息持久化,作为耗时"真值";前端不自行估算 - 问:进行中如何展示 → 答:
stage="streaming"时前端基于开始时间每秒动态递增显示(如"思考中 9s") - 问:结束/历史加载如何展示 → 答:
stage="end"时停止计时并显示由起止时间算出的固定最终耗时;历史会话重新加载后直接静态显示,不重新计时 - 问:起止时间异常如何处理 → 答:缺少开始时间或结束早于开始时,不显示耗时而非显示负值
2026-08-04 会议(移除 onMessagesChange 全量回调)
- 问:移除哪个回调 → 答:仅移除
onMessagesChange(携带完整Message[]的全量回调);onTasksChange、onBackgroundTasksChange、onMcpServersChange、onQueuedMessagesChange、onConfiguredModelsChange等其他全量回调保留不动 - 问:CLI 如何更新消息 UI → 答:改为注册增量回调(
onUserMessageAdded、onAssistantMessageAdded、onAssistantContentUpdated、onAssistantReasoningUpdated、onToolBlockUpdated、onNoticeAdded、bang 三回调)就地更新;bang 三回调新增messageId参数(onAddBangMessage(command, messageId)等),CLI 按 messageId 就地创建/更新 bang 消息块,无需全量拉取 - 问:webview 如何获取全量数据 → 答:主动拉取。stdio 协议移除
messagesChange通知;宿主在 webviewReady / rewind / clearChat / restoreSession 后调用getMessages请求,以响应形式下发全量消息(压缩不在该名单——摘要块经compactBlockAdded携带该条message、由宿主appendMessage增量追加,message缺席时才回落拉取);流式期间仅流动增量通知,bang 信号经 stdio 增量通知携带 messageId - 问:子代理消息如何维护 → 答:
SubagentManager在子代理增量回调中通过instance.messageManager.getMessages()拉取最新列表,维护instance.messages/usedTools并转发onSubagentMessagesChange - 问:vsce 宿主增量注册注意事项 → 答:
onUserMessageAdded与onAssistantMessageAdded需分别映射到 webview 的用户/助手消息追加通道(修复现有onUserMessageAdded误路由到onAssistantMessageAdded的问题),避免与全量拉取叠加导致消息重复
2026-08-04 会议(流式通知纯增量负载)
- 问:为什么从"chunk + accumulated 并存"收敛为纯 delta → 答:节流只能减少通知条数,每个通知仍携带与已流式内容等长的累积 payload,总序列化/传输成本 O(n²),长流时 stdio 内存与带宽压力持续存在;改为 wire 纯 delta 后通知负载与内容总长度解耦,总成本 O(n)
- 问:SDK 回调签名是否改变 → 答:不改。SDK 回调继续同时提供 chunk 与 accumulated(进程内消费者免费使用 accumulated);剥离只发生在跨进程 wire 层(agentBridge),CLI/print-cli 进程内路径完全不动(注:2026-08-08 起此决定已被推翻——SDK 回调一并移除 accumulated,见下方会议记录)
- 问:tool block 流式通知负载 → 答:
stage="streaming"只携带parametersChunk(增量),不再携带累积parameters;stage="end"仍携带全量parameters+result(一次性权威值,UI 以此为最终值) - 问:流结束信号如何表示 → 答:与现状一致——
chunk: ""(空字符串)作为流结束标记,配合stage="end" - 问:webview 如何应用 delta → 答:reducer 追加——文本/推理块
content += chunk;工具块parameters += parametersChunk;getMessages拉取全量时整体替换为权威快照(自愈通道) - 问:节流语义如何调整 → 答:从 last-value-wins(对累积值安全)改为窗口拼接(window-concat):窗口内按到达顺序拼接所有 chunks 发送一个合并 delta;丢弃中间值会造成内容永久缺失
- 问:乱序/丢失如何保证正确性 → 答:管道 FIFO 保证通知与请求响应按序到达,"assistantMessageAdded 先于其首个 delta"、拉取响应包含其之前全部 chunk;偶发丢失由后续
getMessages全量快照自愈 - 问:受影响范围 → 答:agentBridge(剥离累积字段)、各宿主 stdioAgent(透传 delta)、各宿主 webview 转发层(VSCE chatSession、桌面 desktopHost、JetBrains WaveSession 等的节流改为窗口拼接);CLI/print-cli 进程内不变
2026-08-08 会议(SDK 回调移除 accumulated)
- 问:SDK 回调的
accumulated是否保留 → 答:移除(breaking change,不留向后兼容)。onAssistantContentUpdated/onAssistantReasoningUpdated与 wire 通知统一只携带{messageId, chunk, stage},进程内/跨进程只有一种负载模式,消除"两套语义"分裂,也避免消费者误用累积值造成 O(n²) 拷贝 - 问:SDK 内部累积逻辑是否一并移除 → 答:不移除。aiService 仍内部累积全文,messageManager 仍以"新值 slice 当前长度"计算 chunk delta——只是累积值不再暴露到外部回调
- 问:进程内消费者如何适配 → 答:CLI useChat 改为消费纯 chunk:新增窗口拼接节流器(window-concat,500ms),窗口内按到达顺序拼接 chunks 为一个合并 delta 发送,
stage="end"时先冲刷窗口内 pending 增量再转发 end 信号,最后一条 chunk 以空串 end 终结;print-cli 直接按 chunk 打印,无累积依赖 - 问:为什么
stage="end"事件以chunk: ""结尾 → 答:end 是流结束信号,权威最终值由消费端自行累积得到,end 事件不再重复携带全量值
2026-08-11 会议(SDK tool 回调 streaming 移除累积 parameters)
- 问:
onToolBlockUpdated的stage="streaming"是否继续携带累积parameters→ 答:移除(对齐 2026-08-08 content/reasoning 决策)。streaming 只携带parametersChunk(增量)+messageId+stage;start/running/end仍携带权威parameters(end 为最终值)。SDK 内部 messageOperations 仍将 chunk 追加进内存toolBlock.parameters保持累积,getMessages快照对账自愈不回归——只是累积值不再暴露到外部回调 - 问:CLI 多 tool 场景下第一个 tool 不显示 compact params 的根因 → 答:CLI useChat 对 tool 更新误用普通 throttle(last-value-wins,单 lastArgs 槽),多 tool 交错 streaming 时首 tool 的 chunks 被后续事件覆盖,trailing 只应用最后一个 tool 的累积值 → 违反"节流语义"(window-concat);修复为按 tool id 的 window-concat 节流,窗口内各 tool 的 chunks 独立累积拼接,end 时先冲刷窗口内 pending 增量再应用权威参数
- 问:受影响范围 → 答:aiService(streaming 不再发
parameters)、aiManager(parameters条件转发,避免undefined泄漏进回调载荷)、messageOperations(chunk 追加进内存parameters)、CLI useChat(按 tool 窗口拼接节流);wire/agentBridge 自 2026-08-04 起已纯 delta,webview/desktop 消费端已按parametersChunk追加,均无需改动
2026-08-12 会议(CLI reasoning 首词重复缺陷)
- 问:CLI 中所有 reasoning 第一个单词重复的根因 → 答:进程内增量消费端(CLI useChat)在
onAssistantMessageAdded时把agent.messages中的 SDK 消息对象活引用推入 React 状态;updateCurrentMessageReasoning先就地替换共享块为完整累积值、再按"新值 slice 当前长度"计算并回调 chunk delta。首个 delta 到达时消费端追加逻辑(content += chunk)读到已被 SDK 更新的完整值再拼接该 chunk → 首词翻倍(Let+Let me think about this.);首个 delta 处理完成、消息与 SDK 解耦后,后续 delta 正常 - 问:修复位置 → 答:消费端边界。CLI 在推入消息时必须克隆(
{ ...m, blocks: m.blocks.map(b => ({ ...b })) }),不得持有 SDK 内部消息对象活引用;SDK 就地更新与回调顺序(先写全量、再回 delta)不变。覆盖所有推入点:onAssistantMessageAdded、onUserMessageAdded、拉取全量(refreshMessages、初始setMessages(agent.messages)) - 问:快照放在 React state updater 里执行行不行 → 答:不行。
onAssistantMessageAdded与首个 delta 回调落在同一同步 tick(addAssistantMessage()→ 就地写入 → delta 回调),React 将同一 tick 的setMessages批处理,updater 到 flush 时才运行——此时快照复制的是已被 SDK 写入首个 chunk 的消息块,首个 delta 追加后首词翻倍(HelloHello)。快照必须在回调触发时刻(snapshotMessage(msg)在setMessages之外、onAssistantMessageAdded内立即执行)捕获变异前状态。同批回归测试:同一 tick 内依次调用 messageAdded → 变异 → 首个 delta(无 await 间隔),断言不翻倍 - 问:为什么不在 SDK 侧改 → 答:SDK 就地更新是
getMessages权威快照的基础,先写全量再回 delta 保证回调时刻 SDK 状态已一致;改为先回调再写会让拉取与回调交错时读到过期值。快照克隆成本只在消息创建时发生一次,与流式 delta 数量无关
2026-08-17 会议(CLI 移除 500ms 节流、原始频率渲染)
- 问:为什么移除 CLI 的 500ms window-concat 节流 → 答:2026-08-12 修复(快照推入)解决了首词重复的活引用根因,但调研复现了另一残留竞态:节流器 trailing edge 冲刷 pending 窗口内已合并 chunk 时,若期间发生全量列表替换(
refreshMessages,/clear、/compact、/rewind、Ctrl+O 折叠触发),pending 内旧 delta 会追加到已含全部内容的权威快照之上,造成内容重复(如Let me think about me think about this...)。移除节流后每个 delta 同步立即应用、无 pending 窗口,竞态从机制上消除——用忠实复刻 throttle+reducer 逻辑的模拟脚本验证:500ms 节流 + 流中刷新稳定复现重复,wait=0(无节流)同场景永不重复 - 问:渲染性能是否受影响 → 答:不会。Ink 7.1.1 内置输出节流(
maxFps: 30→ ~34ms,leading+trailing)约束终端写入频率,与消费端节流无关;<Static>append-only 使历史消息永不重绘(fullStaticOutput += staticOutput);renderInteractiveFrame仅在output !== lastOutput时写入,log-update 再做增量 diff。React commit 频率等于 delta 到达频率,每次 commit 对全量可见消息做flatMap+ 元素重建,典型会话规模(≤30 条可见消息)为亚毫秒级 - 问:受影响范围 → 答:仅 CLI 进程内消费端(
packages/code/src/contexts/useChat.tsx)——移除内容/reasoning 通道的createStreamingWindowThrottle(500ms)与 tool 通道的createToolStreamingThrottle(同一 pending 窗口 + 全量刷新竞态结构、同一逐 token 高频来源),每个 delta 立即经 reducer 应用。VSCE/桌面跨进程通道保留 window-concat 节流语义不变(wire 通知频率较低,非首词重复来源) - 问:原「每秒 2-3 次内容更新」假设为何移除 → 答:该假设是 500ms 节流的设计依据;移除节流后更新频率由模型产出速率决定,终端写入频率由 Ink 30fps 兜底,无需人为限频
2026-08-18 会议(CLI Static 块 reopen 渲染层首词重复)
- 问:本次(第三类)首词重复与之前两类的区别 → 答:前两类在状态层——活引用/批处理快照(2026-08-12)与节流残留竞态(2026-08-17),状态先重复再渲染;本次在渲染层——SDK→CLI 状态始终一致(纯 delta 追加、逐帧对账无重复),重复只出现在 Ink 输出拼接。根因:CLI 消息列表的静态/动态分区中,文本/推理块一旦收尾就进入
<Static>区被冻结;同块因模型交错输出(T→R→T或R→T→R)重新打开(end→streaming)时迁移回动态区,动态区以全量累积内容渲染,与屏幕上已冻结的前缀叠加 → 首词重复。复现概率低,因为需要单次调用内交错续写 - 问:为什么不能用"reopen 块永久留在静态区"修复 → 答:静态区 append-only、永不更新已渲染项,块留在静态区后续写内容永远不会显示,消息后半段的流式反馈会整体冻结(屏幕停在首个 reopen 时刻),流式体验退化
- 问:为什么不能整条消息动态化 → 答:reopen 只发生在消息运行期,但"消息运行"包含长工具执行(分钟级)——整条消息动态化会让已完成的超长文本块在每次工具状态更新时全量重建(性能回归),且冻结过的块回到动态区同样重复
- 问:修复方案 → 答:按块 key 记录"写入静态区时刻的内容"(冻结前缀,镜像
<Static>内部 index 判断哪些项真正被追加);reopen 块在动态区只渲染content.slice(冻结长度)增量——冻结前缀 + 动态增量拼接等于完整内容,无重叠、无重复、续写实时可见。多轮 reopen 恒相对首次冻结内容计算(静态区从不更新已渲染项) - 问:如何确定哪些静态项真正被写入终端 → 答:镜像
<Static>的 index 语义(useLayoutEffect后 index = 上次items.length),静态列表位置 ≥ index 的项在本 pass 被追加写入,其内容才记录为冻结;位置 < index 的项(reopen 时被顶到已占用位置的"填充块")从未写入,不记录——它们后续 reopen 时仍以全量渲染(从未显示过,无重复问题) - 问:回归测试如何捕获转瞬重复 → 答:逐阶段
rerender后断言每一帧(测试 harness 的frames)中每个块内容至多出现一次,而非只断言末帧——重复只在 reopen 流式窗口内存在,末帧(块回到静态区)已无重复。无修复时全部 3 用例失败(帧计数 2),修复后通过
2026-08-19 会议(CLI 恢复 500ms 窗口节流 + 全量刷新快照安全取消)
- 问:为什么恢复 CLI 的 500ms window-concat 节流 → 答:移除节流后模型逐 token 高频产出(如 30ms/chunk)时每次 delta 都触发 React commit,bash 等长任务运行期间界面更新过密、短结果行数变化导致布局跳动(闪烁)。恢复
createStreamingWindowThrottle(content/reasoning,500ms)与createToolStreamingThrottle(tool 参数,500ms):leading edge 立即应用、窗口内 chunk 拼接、end 立即冲刷,渲染频率被节流窗口限制 - 问:2026-08-17 记录的节流残留竞态如何解决 → 答:在竞态源头修复而非移除节流——
refreshMessages(全量快照替换的唯一入口,/clear、/compact、/rewind、Ctrl+O 折叠均经此)在拉取快照之后、应用快照之前cancel()三个节流窗口。pending 内旧 delta 在拉取前已写入 SDK 消息状态,必然已包含在权威快照中,丢弃无损失;拉取之后到达的 delta 开启全新窗口、按 React 入队顺序追加在快照之上。刷新前后内容既不重复也不丢失 - 问:为什么 cancel 必须在拉取之后 → 答:若先 cancel 再拉取,cancel 与拉取之间到达的 delta 会同时出现在新窗口与快照中 → 重复;反之(先拉取后 cancel),cancel 与拉取之间不可能插入 delta(同步代码块),拉取前到达的 delta 要么已含于快照、要么在 pending 中被丢弃,两种情况下最终内容都与 SDK 权威状态一致
- 问:tool 节流窗口的 pending 参数 chunk 被 cancel 丢弃是否安全 → 答:安全。SDK 内部始终累积
toolBlock.parameters,拉取的快照携带权威parameters;丢弃的只是 CLI 侧尚未刷新的增量,内容不丢失 - 问:受影响范围 → 答:仅 CLI 进程内消费端(
packages/code/src/contexts/useChat.tsx)——恢复三个节流器并在refreshMessages中取消;VSCE/桌面跨进程通道的 window-concat 节流语义不变。回归测试:恢复后的 refresh-interleave 场景(流中 /clear 触发全量刷新 + 冲刷残留定时器)在快照取消下内容保持Let me,无Let me me重复;同批 double-count 测试(2026-08-12 快照捕获修复)不受节流恢复影响,继续通过
2026-08-19 会议(CLI 消息状态更新统一为单一节流函数)
- 问:为什么把 content/reasoning/tool 三通道节流器统一为一个
updateMessages→ 答:initializeAgent中全部消息状态更新回调(onUserMessageAdded、onAssistantMessageAdded、onAssistantContentUpdated、onAssistantReasoningUpdated、onToolBlockUpdated全阶段、onNoticeAdded、bang 三回调)本质都是"以更新函数改写 messages 状态",此前只有文本/推理/tool 参数三通道走节流,而 bash 等前台工具逐 chunk 的 running 阶段shortResult/result更新走createToolStreamingThrottle的"running 立即应用"分支——不节流,行数(1-4 行)变化逐 chunk 触发 React commit 与 ToolDisplay 高度变化(闪烁源)。统一后全部更新经单一 500ms 窗口排队(FIFO、无 last-value-wins),running 高频更新与流式 delta 共用窗口,至多每窗口一次渲染,闪烁消除 - 问:统一窗口后如何保证 streaming→running 不产生阶段回退(原 a4d5767e 语义) → 答:由 FIFO 排队顺序从机制上保证——窗口内 streaming 参数 chunk 与 running 权威快照按到达顺序应用(streaming 先追加、running 后权威替换),组合 updater 一次性应用,最终
stage="running"、parameters为权威值,不存在"buffered stale streaming 晚于 running 冲刷"的窗口(两者同窗同 flush),原"running 丢弃该 tool buffered deltas"的特殊逻辑随之移除 - 问:一次性结构更新(消息新增、error 块、bang 完成)经节流是否引入延迟 → 答:
updateMessages保留 leading edge 立即语义——窗口关闭时首个更新立即应用,窗口内到达的更新最多延迟 500ms 至窗口关闭冲刷;结构性更新(如消息新增)同窗排队时按到达顺序与流式 delta 一起应用,不丢失、不重排 - 问:统一后
refreshMessages的取消语义是否变化 → 答:不变,只是从 cancel 三个窗口变为 cancel 单一窗口——拉取快照后cancel()丢弃排队中的 pending 更新(内容已含于权威快照),拉取后的更新开启新窗口追加在快照之上,刷新前后不重复不丢失 - 问:受影响范围 → 答:仅 CLI 进程内消费端(
packages/code/src/contexts/useChat.tsx)——以单一updateMessages取代createStreamingWindowThrottle(content/reasoning 两实例)与createToolStreamingThrottle,refreshMessages、isExpanded 切换、卸载清理均改取消单一窗口;VSCE/桌面跨进程通道的 window-concat 节流语义不变。回归测试:既有 500ms 节流、tool 参数累积、同批 double-count、refresh-interleave 测试在统一入口下继续通过;createToolStreamingThrottle单元测试改写为updateMessagesFIFO 排队语义测试(阶段回退防负断言保留)
2026-10-15 会议(CLI 节流按「更新是否改变卡片高度」分流)
- 问:为什么要从单一 500ms 窗口改为分流 → 答:统一窗口让全部消息更新(含文本/推理 delta)共享 500ms 限频,模型逐 token 产出时正文以每 500ms 成块的节奏推进(顿挫感)。复盘渲染特征发现闪烁源只有工具
running的shortResult/result(短结果行数在 1–N 行间震荡,其下方内容被反复推下/拉上);文本/推理 delta 的高度单调递增,Inklog-update只在尾部追加/重写,不产生来回位移。收窄节流范围即可同时保住防闪烁与流式即时性 - 问:分流判据是什么 → 答:
onToolBlockUpdated且stage="running"(SDK 只在该阶段逐 chunk 上报shortResult/result,见aiManager.ts的onShortResultUpdate/onResultUpdate);其余更新(文本/推理 delta、工具start/streaming/end、消息新增与 error 块)都立即应用。工具end仍先flush()节流窗口再应用,避免 pending 的running值在end之后复活 shortResult - 问:文本/推理 delta 恢复逐 delta 渲染会不会卡 → 答:不会。2026-08-17 已评估:每次 commit 对全量可见消息做 map 为亚毫秒级,终端写入频率由 Ink 内置
maxFps: 30兜底,<Static>append-only 使历史消息永不重绘;实现后用真实终端验证(bash 长任务 + 长文本流式)确认无闪烁 - 问:分流后是否引入新的交错风险(running 在窗口内排队期间,其后到达的立即更新会先于它应用)→ 答:逐一枚举插队组合均无害——① 作用于不同消息/块/字段的更新(文本/推理 delta、其他工具块的
start/streaming、error 块追加)与 running 的状态互不重叠,顺序无关;② 同一工具块的end经flush()保序(先冲刷 pending 的 running 再应用 end);③ 全量刷新(refreshMessages)拉快照后cancel()丢弃 pending;④onAssistantMessageDiscarded删除消息后,其 pending 的 running 冲刷时按 messageId 找不到目标、无操作。唯一理论违规是同一工具块running之后又到streamingchunk——SDK 生命周期上不可达(参数流先于工具执行,aiManager.ts的 running emit 之后仅剩onShortResultUpdate/onResultUpdate),即便发生也只是参数字符串重复,被随后的end权威值覆盖自愈,不崩溃、不丢消息。放弃「立即更新到达先 flush 单队列再应用」的严格 FIFO 变体:文本 delta 每到一次就冲刷一次窗口,同消息 reopen(文本流与工具运行交错,见「CLI 静态块 reopen 不重复渲染冻结前缀」用户故事)时 running 合并被反复打断、闪烁回归 - 问:
refreshMessages的取消语义是否变化 → 答:不变。拉取权威快照后cancel()节流窗口(立即通道无 pending,无需取消);pending 内的旧running更新已包含在快照中,丢弃无损失 - 问:被节流延迟的 running 更新会不会在消息已写入
<Static>后才应用、在终端留下冻结痕迹 → 答:不会。静态/动态分区按渲染时刻的 state 判定(MessageList.tsx的runningMessageIndices:含 running/streaming/start 块的消息整体动态);running 更新 pending 的唯一前提是窗口已被前一个 running 打开(首个 running 走 leading 立即应用),故 pending 期间 state 中该块 stage 必为running、消息必在动态区。它被应用的三条路径均无「先冻结、后应用」窗口:窗口 trailing 应用后块仍running(消息仍动态,动态区由 Ink 每帧重绘、中间态不残留);同块end先flush()再应用(R 值先于 end 落定,之后消息才可能整体迁入 Static,冻结内容已含最终值);全量刷新cancel()丢弃(以含该内容的权威快照重建)。reopen 的frozenContentRef机制只记录 text/reasoning 块内容,与工具块shortResult的更新路径互不相交 - 问:受影响范围 → 答:仅 CLI 进程内消费端(
packages/code/src/contexts/useChat.tsx)——initializeAgent的回调按分流提交;SDK 回调、wire 通知、VSCE/桌面跨进程通道的 window-concat 节流语义均不变。回归测试:既有 500ms 节流、tool 参数累积、同批 double-count、refresh-interleave 测试改写为分流语义后继续通过;新增「文本 delta 立即渲染、不被窗口延迟」断言 - 问:如何观察「彻底移除节流」的效果 → 答:实验环境变量
WAVE_STREAM_THROTTLE_MS(毫秒):未设置/空白/非数字 = 500 默认(行为不变),<=0= 彻底禁用节流(高度敏感更新也逐条立即应用,观察全立即渲染),正数 = 自定义窗口。实现为createThrottledUpdater的wait <= 0旁路——直接应用、不开窗口、不排队,flush()/cancel()天然为 no-op(timer 永不创建、队列永为空);仅在 CLI 消费端updateMessages创建处读取(挂载时读一次,进程内 env 不变),VSCE/桌面跨进程通道不受影响;InkmaxFps: 30渲染层兜底保留。定位:观察/实验开关,用于在真实终端验证「数据层节流是否必要」——若全立即渲染无闪烁,后续可考虑收窄或移除高度敏感节流;不改变未设置时的默认行为
2026-10-15 会议(移除 CLI 数据层节流)
- 问:为什么彻底移除而非保留分流或开关 → 答:分流实现后提供实验开关
WAVE_STREAM_THROTTLE_MS(0= 彻底禁用),用户在真实终端以=0初步体验长文本流式与工具运行输出,确认全立即渲染观感正常、无闪烁困扰——节流的唯一存在理由(压制工具running短结果行数震荡的闪烁担忧)实测不成立:Inklog-update只重写动态区尾部、maxFps: 30合帧吸收行数震荡。数据层节流只增加显示延迟与复杂度(窗口/排队/FIFO/flush/cancel/快照取消语义 + 相应测试矩阵),移除后updateMessages退化为直接setMessages - 问:无节流后还有顺序/竞态风险吗 → 答:到达顺序即应用顺序,强于此前节流 FIFO 队列提供的保证。原分流插队矩阵中所有「queued vs immediate」组合随队列消失;同块
streaming→running阶段回退由 SDK 协议保证(参数流先于工具执行,runningemit 之后仅剩onShortResultUpdate/onResultUpdate)+end权威值覆盖自愈;refreshMessages拉快照后无需 cancel(无 pending 可复活),刷新后新 delta 直接追加在快照之上 - 问:移除范围 → 答:删除
createThrottledUpdater、resolveStreamThrottleMs、DEFAULT_STREAM_THROTTLE_MS与实验开关WAVE_STREAM_THROTTLE_MS(一次性实验设施,设计定型后无存在意义,git 历史保留回滚路径);onToolBlockUpdated全 stage 直接应用;refreshMessages、isExpanded effect 与卸载清理中的 cancel 移除。渲染层 InkmaxFps: 30兜底不变;VSCE/桌面跨进程通道的 window-concat 节流语义不受影响 - 问:如何防闪烁回归 → 答:假设部分记录观察条件(长 bash 任务短结果行数震荡 + 长文本流式,真实终端);若未来真实场景出现闪烁,应在渲染层解决(Ink fork 增量 diff / 双缓冲虚拟屏,参照 Claude Code 的 16ms 帧节流),而非恢复数据层节流
2026-10-27 会议(内层回调也只保留 delta,所有消费端改为拼接)
- 问:为什么连
aiService.callAgent的快照回调(onContentUpdate/onReasoningUpdate)也移除 → 答:同一份流有两个视图时,取快照的那条路必须自己再算一次差分才拿到 chunk(原实现slice(当前长度)),而这个差分基准就是消息块里已存的内容——状态存了两份(流里的累积变量、块里的 content),任何一处错位都表现为重复或丢字。收掉快照后只剩onContentDelta/onReasoningDelta,累积由消费端自己做(消息块 append),一份状态一处写 - 问:消息块怎么改 → 答:
MessageManager.updateCurrentMessageContent/updateCurrentMessageReasoning改为appendCurrentMessageContent/appendCurrentMessageReasoning:入参是新片段,块存在就content + chunk、不存在就新建块,回调仍按{messageId, chunk, stage}发。非流式落地(!createdByStreaming保护)调用同一方法,此时块为空、等于整体写入一次 - 问:CLI 的
/btw覆盖层原来按替换写,改成什么 → 答:拼接(新增 reducer 动作APPEND_BTW_ANSWER)。它在 SDK 侧拿到的onContent现在就是片段,继续替换只会显示最后一片 - 问:
stream_fallback丢弃重发会不会受影响 → 答:不受。降级路径整条消息discardAssistantMessage后重建,不依赖快照幂等 - 问:受影响范围 → 答:
aiService(回调收窄、isStreaming判定与processStreamingResponse参数同步收窄)、aiManager(主循环、/btwfork、非流式落地三处改 delta)、messageManager(两个 append 方法)、CLIuseInputManager+inputReducer(APPEND_BTW_ANSWER)。wire 通知、宿主转发层、webview 消费端原本就是 delta 口径,无需改动
假设 (必填)
- 底层 AI 服务支持流式响应(增量内容传递)
- 网络连接对大多数用户通常稳定
- 终端/CLI 界面可以处理逐 delta 的高频文本更新(终端写入频率由 Ink 内置
maxFps: 30输出节流兜底);工具running短结果的行数震荡实测在log-update尾部重写 +maxFps: 30合帧下无可感知闪烁(2026-10-15 真实终端以全立即渲染初步验证),CLI 数据层无需节流 - 用户通常使用支持实时文本渲染的标准终端模拟器
- 在正常网络条件下内容流按时间顺序到达
- 消息变更通过增量回调对外发布,完整列表按需拉取(
agent.messages/getMessages请求);每次流式 chunk 不触发全量列表推送 - 增量回调与跨进程通知一律只携带增量负载(chunk / parametersChunk),SDK 不再对外提供累积值;消费端负责累积追加,
getMessages全量快照负责对账自愈 - 工具参数流包含有效的 JSON 或结构化数据,可以使用新的
extractStreamingParams工具函数(待实现)增量解析,该函数将验证 JSON 完整性并从部分流中提取有效的参数对象 - Agent SDK 在内部管理消息状态,并通过增量回调与按需读取(
agent.messages)对外提供消息数据