minimal-agent 0.18.13 → 0.18.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/package.json +1 -3
  2. package/plugins/workflow-runner/src/runtime/context.js +1 -1
  3. package/plugins/workflow-runner/src/runtime/context.ts +1 -1
  4. package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
  5. package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
  6. package/skills/ssh-remote-dev/SKILL.md +61 -0
  7. package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
  8. package/src/advisor/README.md +62 -24
  9. package/src/advisor/advisorNudge.js +199 -0
  10. package/src/advisor/advisorNudge.ts +232 -0
  11. package/src/advisor/deliveryReview.js +35 -12
  12. package/src/advisor/deliveryReview.ts +35 -11
  13. package/src/advisor/presentation.js +7 -10
  14. package/src/advisor/presentation.ts +10 -13
  15. package/src/advisor/reviewContext.js +22 -58
  16. package/src/advisor/reviewContext.ts +24 -67
  17. package/src/cli/configMenu.js +17 -128
  18. package/src/cli/configMenu.tsx +17 -193
  19. package/src/cli/print.js +2 -0
  20. package/src/cli/print.ts +2 -0
  21. package/src/cli/streamJson.js +2 -0
  22. package/src/cli/streamJson.ts +2 -0
  23. package/src/cli/wizards/advisorWizard.js +9 -7
  24. package/src/cli/wizards/advisorWizard.tsx +9 -7
  25. package/src/cli/wizards/contextAdvisorWizard.js +9 -7
  26. package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
  27. package/src/cli/wizards/providerPresets.js +27 -1
  28. package/src/cli/wizards/providerPresets.ts +44 -2
  29. package/src/cli/wizards/providerWizard.js +51 -10
  30. package/src/cli/wizards/providerWizard.tsx +59 -9
  31. package/src/config/configDefaults.js +1 -1
  32. package/src/config/configDefaults.ts +1 -1
  33. package/src/config/configDiagnostics.js +1 -21
  34. package/src/config/configDiagnostics.ts +3 -23
  35. package/src/config/configFile.js +11 -4
  36. package/src/config/configFile.ts +27 -7
  37. package/src/config/configManagement.js +1 -0
  38. package/src/config/configManagement.ts +2 -0
  39. package/src/config/configService.js +12 -8
  40. package/src/config/configService.ts +16 -11
  41. package/src/config/numericLimits.js +0 -4
  42. package/src/config/numericLimits.ts +0 -4
  43. package/src/config/reasoningEffort.js +16 -0
  44. package/src/config/reasoningEffort.ts +18 -0
  45. package/src/context/budgetPlan.js +1 -1
  46. package/src/context/budgetPlan.ts +1 -1
  47. package/src/context/optimizeConversation.js +1 -1
  48. package/src/context/optimizeConversation.ts +1 -1
  49. package/src/harness/stallGuard.js +8 -13
  50. package/src/harness/stallGuard.ts +8 -12
  51. package/src/llm/client.js +64 -17
  52. package/src/llm/client.ts +62 -7
  53. package/src/llm/providers/openaiFormat.js +14 -3
  54. package/src/llm/providers/openaiFormat.ts +17 -3
  55. package/src/llm/providers/responsesFormat.js +275 -0
  56. package/src/llm/providers/responsesFormat.ts +306 -0
  57. package/src/loop/README.md +18 -3
  58. package/src/loop/liveDiagramGate.js +63 -0
  59. package/src/loop/liveDiagramGate.ts +61 -0
  60. package/src/loop/messages.js +12 -0
  61. package/src/loop/messages.ts +13 -0
  62. package/src/loop/options.ts +7 -2
  63. package/src/loop/toolBatch.js +0 -1
  64. package/src/loop/toolBatch.ts +0 -1
  65. package/src/loop/toolExecutor.js +11 -46
  66. package/src/loop/toolExecutor.ts +12 -55
  67. package/src/loop/toolPresentation.js +2 -13
  68. package/src/loop/toolPresentation.ts +2 -13
  69. package/src/loop/toolSemantics.js +3 -4
  70. package/src/loop/toolSemantics.ts +4 -6
  71. package/src/loop.js +222 -214
  72. package/src/loop.ts +226 -232
  73. package/src/memory/dependencies.js +11 -3
  74. package/src/memory/dependencies.ts +11 -3
  75. package/src/memory/episodicMemory.js +59 -18
  76. package/src/memory/episodicMemory.ts +58 -18
  77. package/src/memory/experienceStore.js +4 -3
  78. package/src/memory/experienceStore.ts +4 -3
  79. package/src/memory/factCandidates.js +53 -1
  80. package/src/memory/factCandidates.ts +54 -1
  81. package/src/memory/factLearning.js +2 -2
  82. package/src/memory/factLearning.ts +2 -2
  83. package/src/memory/jobPolicy.js +24 -5
  84. package/src/memory/jobPolicy.ts +25 -5
  85. package/src/memory/learning.js +2 -2
  86. package/src/memory/learning.ts +2 -2
  87. package/src/memory/learningModel.js +7 -0
  88. package/src/memory/learningModel.ts +7 -0
  89. package/src/memory/managementService.js +8 -7
  90. package/src/memory/managementService.ts +16 -8
  91. package/src/memory/memoryService.js +104 -33
  92. package/src/memory/memoryService.ts +132 -28
  93. package/src/memory/projectKnowledge.js +39 -3
  94. package/src/memory/projectKnowledge.ts +45 -3
  95. package/src/memory/recallQuery.js +56 -0
  96. package/src/memory/recallQuery.ts +58 -0
  97. package/src/memory/retrieval.js +39 -10
  98. package/src/memory/retrieval.ts +46 -11
  99. package/src/memory/turnJournal.js +75 -10
  100. package/src/memory/turnJournal.ts +66 -10
  101. package/src/memory/types.ts +7 -1
  102. package/src/prompts/delegation.js +2 -1
  103. package/src/prompts/delegation.ts +3 -1
  104. package/src/prompts/policyRegistry.js +9 -12
  105. package/src/prompts/policyRegistry.ts +9 -12
  106. package/src/prompts/promptAssembly.js +6 -5
  107. package/src/prompts/promptAssembly.ts +8 -6
  108. package/src/prompts/visualPresentation.js +39 -10
  109. package/src/prompts/visualPresentation.ts +38 -9
  110. package/src/runtime/answerPreparation.js +1 -1
  111. package/src/runtime/answerPreparation.ts +1 -1
  112. package/src/runtime/fileLock.js +19 -3
  113. package/src/runtime/fileLock.ts +19 -3
  114. package/src/runtime/invocationPolicy.js +1 -1
  115. package/src/runtime/invocationPolicy.ts +1 -1
  116. package/src/runtime/outputContract.js +0 -4
  117. package/src/runtime/outputContract.ts +0 -7
  118. package/src/session/runAgentTurn.js +3 -0
  119. package/src/session/runAgentTurn.ts +3 -0
  120. package/src/team/onlineToolContract.js +0 -1
  121. package/src/team/onlineToolContract.ts +0 -1
  122. package/src/tools/advisor/advisor.js +207 -365
  123. package/src/tools/advisor/advisor.ts +211 -383
  124. package/src/tools/agent/agentTypes.js +0 -1
  125. package/src/tools/agent/agentTypes.ts +0 -1
  126. package/src/tools/agent/coordinator.js +4 -1
  127. package/src/tools/agent/coordinator.ts +4 -1
  128. package/src/tools/bash/semantics.js +4 -5
  129. package/src/tools/bash/semantics.ts +4 -5
  130. package/src/tools/capabilities.js +2 -3
  131. package/src/tools/capabilities.ts +2 -4
  132. package/src/tools/index.js +0 -16
  133. package/src/tools/index.ts +0 -16
  134. package/src/tools/memory/maintenance.js +8 -9
  135. package/src/tools/memory/maintenance.ts +13 -11
  136. package/src/tools/memory/memory.js +5 -1
  137. package/src/tools/memory/memory.ts +5 -0
  138. package/src/tools/optimizeConversation/README.md +4 -6
  139. package/src/tools/toolPromptGuides.js +2 -9
  140. package/src/tools/toolPromptGuides.ts +2 -9
  141. package/src/types.ts +28 -105
  142. package/src/ui/App.js +2 -0
  143. package/src/ui/App.tsx +1 -0
  144. package/src/ui/MessageList.js +8 -44
  145. package/src/ui/MessageList.tsx +3 -81
  146. package/src/ui/hooks/useChat.js +2 -0
  147. package/src/ui/hooks/useChat.ts +2 -0
  148. package/src/ui/toolProgress.js +1 -1
  149. package/src/ui/toolProgress.ts +1 -1
  150. package/src/web/protocol.ts +10 -3
  151. package/src/advisor/advisorCoordinator.js +0 -734
  152. package/src/advisor/advisorCoordinator.ts +0 -825
  153. package/src/cli/wizards/sshHostWizard.js +0 -156
  154. package/src/cli/wizards/sshHostWizard.tsx +0 -214
  155. package/src/context/sshHosts.js +0 -290
  156. package/src/context/sshHosts.ts +0 -312
  157. package/src/loop/publication.js +0 -94
  158. package/src/loop/publication.ts +0 -124
  159. package/src/runtime/finalAnswer.js +0 -9
  160. package/src/runtime/finalAnswer.ts +0 -13
  161. package/src/tools/advisor/advisorDecision.js +0 -79
  162. package/src/tools/advisor/advisorDecision.ts +0 -92
  163. package/src/tools/ssh/ssh.js +0 -152
  164. package/src/tools/ssh/ssh.ts +0 -167
  165. package/src/tools/ssh/sshAvailability.js +0 -35
  166. package/src/tools/ssh/sshAvailability.ts +0 -39
  167. package/src/tools/ssh/sshClient.js +0 -177
  168. package/src/tools/ssh/sshClient.ts +0 -189
  169. package/src/tools/ssh/sshDownload.js +0 -77
  170. package/src/tools/ssh/sshDownload.ts +0 -92
  171. package/src/tools/ssh/sshHostsTool.js +0 -36
  172. package/src/tools/ssh/sshHostsTool.ts +0 -48
  173. package/src/tools/ssh/sshUpload.js +0 -72
  174. package/src/tools/ssh/sshUpload.ts +0 -85
package/src/loop.ts CHANGED
@@ -5,10 +5,10 @@ import {
5
5
  injectVisionFollowups,
6
6
  mergeToolCallDelta,
7
7
  pushManualInterruptMarker,
8
+ pushAdvisorFollowUpMarker,
8
9
  pushQueuedInputMarker,
9
10
  } from './loop/messages.js';
10
11
  import type { RunQueryOptions } from './loop/options.js';
11
- import { publishFinalAnswer } from './loop/publication.js';
12
12
  import { prepareModelRequest } from './loop/requestPreparation.js';
13
13
  import { executeToolBatch } from './loop/toolBatch.js';
14
14
  import { createToolExecutor, type WorkerOutcome } from './loop/toolExecutor.js';
@@ -25,15 +25,17 @@ export { friendlyToolDescription } from './loop/toolPresentation.js';
25
25
  export { advisorSemanticsFor } from './loop/toolSemantics.js';
26
26
  /**
27
27
  * Agent turn orchestrator. Owns conversation state and continuation decisions.
28
- * Request preparation, tool execution/scheduling and final publication live in
29
- * ./loop/. Host options and LoopEvent remain the public compatibility boundary.
28
+ * Request preparation and tool execution/scheduling live in ./loop/; the
29
+ * final answer is published here, verbatim, once every delivery gate passes.
30
+ * Host options and LoopEvent remain the public compatibility boundary.
30
31
  * See ./loop/README.md for phase ownership and invariants.
31
32
  */
32
33
 
33
34
  import crypto from 'node:crypto';
34
35
  import { resolve } from 'node:path';
35
- import { AdvisorCoordinator } from './advisor/advisorCoordinator.js';
36
- import { collectReviewContext } from './advisor/reviewContext.js';
36
+ import { AdvisorNudge } from './advisor/advisorNudge.js';
37
+ import { collectAdvisorBackground } from './advisor/reviewContext.js';
38
+ import { previousUserRequest } from './memory/recallQuery.js';
37
39
  import { configService } from './config/configService.js';
38
40
  import { buildContextBudgetPlan } from './context/budgetPlan.js';
39
41
  import { getCompactThreshold } from './context/compact.js';
@@ -58,12 +60,13 @@ import { buildUserContent } from './multimodal.js';
58
60
  import { delegationRuntimeGuidance } from './prompts/delegation.js';
59
61
  import { capabilitiesForTools, enabledPolicyIds } from './prompts/policyRegistry.js';
60
62
  import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
61
- import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
63
+ import { needsDiagramPreparation, requestKeepsTextDiagrams, visualPresentation } from './prompts/visualPresentation.js';
64
+ import { LiveDiagramGate } from './loop/liveDiagramGate.js';
62
65
  import { createAgentRuntimeContext } from './runtime/agentRuntimeContext.js';
63
66
  import { answerPreparationNotice } from './runtime/answerPreparation.js';
64
67
  import { invocationInstructions, invocationToolAllowed } from './runtime/invocationPolicy.js';
65
68
  import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
66
- import { resolveOutputContract, toolsForResponsePhase } from './runtime/outputContract.js';
69
+ import { resolveOutputContract } from './runtime/outputContract.js';
67
70
  import { filterToolsForProvider } from './team/toolPolicy.js';
68
71
  import { agentCoordinationContext, takeAgentNotifications } from './tools/agent/agentTaskService.js';
69
72
  import { needsInteractiveQuestion } from './tools/ask/routing.js';
@@ -80,6 +83,25 @@ import { stripThinkBlocks } from './utils/stripThink.js';
80
83
  /** 主工具循环的默认轮数上限。 */
81
84
  export const DEFAULT_MAX_TURNS = 200;
82
85
 
86
+ /** Live streaming starts once real answer text exists. A leading inline
87
+ * <think> block (MiniMax and similar), even a tag still arriving in pieces,
88
+ * is not the answer and must not be shown as if it were. */
89
+ /** 显式选项优先;否则读配置 `advisor.enforcement`(只在需要补问时才读一次盘,读失败当作关闭)。 */
90
+ async function advisorFollowUpEnabled(options: RunQueryOptions): Promise<boolean> {
91
+ if (options.advisorFollowUp !== undefined) return options.advisorFollowUp;
92
+ try {
93
+ return (await configService.advisor())?.enforcement === 'followup';
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function answerTextStarted(text: string): boolean {
100
+ const lead = text.trimStart();
101
+ if (lead.length < '<think>'.length && '<think>'.startsWith(lead.toLowerCase())) return false;
102
+ return stripThinkBlocks(text).length > 0;
103
+ }
104
+
83
105
  /**
84
106
  * 执行一次"用户输入 → 模型回答完成"的完整流程。
85
107
  *
@@ -158,7 +180,13 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
158
180
  let emptyResponseRecoveryAttempted = false;
159
181
  let emptyResponseRecoveryReason = '';
160
182
  let askCorrectionAttempted = false;
161
- let diagramPreparationAttempted = false;
183
+ let diagramCorrectionAttempted = false;
184
+ // The retired draft rides along with the next request only, as private
185
+ // revision material: never shown to the user, never stored in history.
186
+ let diagramCorrectionDraft = '';
187
+ // Part of that draft already streamed live (the text before the box): the
188
+ // model continues from there instead of rewriting what the user has read.
189
+ let diagramContinuation = false;
162
190
  const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
163
191
  const subagents = await configService.subagents();
164
192
  await hydrateSessionWorktree({
@@ -178,35 +206,12 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
178
206
  },
179
207
  history,
180
208
  sessionState,
181
- advisorReviewContext: () => ({
182
- ...collectReviewContext(advisorContextBundle),
183
- constraints: [
184
- ...collectReviewContext(advisorContextBundle).constraints,
185
- {
186
- id: 'execution.boundary',
187
- authority: 'host',
188
- content: JSON.stringify({
189
- cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
190
- conversationMode: toolCallContext.conversationMode,
191
- profile: runtimeContext.profile,
192
- availableTools: [...currentEffectiveToolSet.names],
193
- invocation: invocationInstructions(options.invocationPolicy),
194
- }),
195
- },
196
- ],
197
- omittedIds: [
198
- ...new Set([
199
- ...(currentTransientBundle?.diagnostics.omittedIds ?? []),
200
- ...(advisorContextBundle?.diagnostics.omittedIds ?? []),
201
- ]),
202
- ...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : []),
203
- ],
204
- clippedIds: [
205
- ...new Set([
206
- ...(currentTransientBundle?.diagnostics.clippedIds ?? []),
207
- ...(advisorContextBundle?.diagnostics.clippedIds ?? []),
208
- ]),
209
- ],
209
+ // Project constraints and memory are not in the conversation history, so the
210
+ // advisor gets them as one background block (see tools/advisor/advisor.ts).
211
+ advisorBackground: () => collectAdvisorBackground(advisorContextBundle, {
212
+ cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
213
+ availableTools: [...currentEffectiveToolSet.names],
214
+ invocation: invocationInstructions(options.invocationPolicy),
210
215
  }),
211
216
  // Statistics only; consultations have no per-turn quota.
212
217
  advisorCalls: { used: 0 },
@@ -257,20 +262,20 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
257
262
  options.invocationPolicy?.delivery !== 'caller' &&
258
263
  effectiveToolSet.names.has('PresentResult');
259
264
  let currentEffectiveToolSet = effectiveToolSet;
260
- const advisorCoordinator = new AdvisorCoordinator({
261
- history,
265
+ // The host only reminds; whether to consult the advisor is the model's call
266
+ // (see advisor/advisorNudge.ts). Nothing here blocks a tool or holds an answer.
267
+ const advisorNudge = new AdvisorNudge({
262
268
  currentRequest: userInput,
263
- currentRequestMessageId: currentUserMessageId,
264
- advisorAvailable: effectiveToolSet.names.has('Advisor'),
269
+ previousRequest: previousUserRequest(history),
270
+ advisorAvailable: effectiveToolSet.names.has('Advisor') && options.advisorReminders !== false,
265
271
  });
266
- toolCallContext.advisorRuntime = advisorCoordinator;
267
272
  let coordinationText = '';
268
273
  let delegationCorrectionAttempted = false;
269
274
  // 组装每轮请求的易变上下文。三个构建点(首轮 / autoCompact 后 / snip 后)共用,
270
275
  // 避免 Advisor 相关提醒在复制粘贴中三处漂移。提醒均为 transient:只进当轮请求副本,
271
276
  // 不进会话历史(与 DELIVERY_CORRECTION_REMINDER 同构,见 buildModelMessages)。
272
277
  const buildRequestTransientContext = (): string => {
273
- const advisorAvailable = currentEffectiveToolSet.names.has('Advisor');
278
+ const advisorAvailable = currentEffectiveToolSet.names.has('Advisor') && options.advisorReminders !== false;
274
279
  const parts = [
275
280
  transientFor(activeTools),
276
281
  coordinationText,
@@ -280,7 +285,13 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
280
285
  callsUsed: toolCallContext.agentCalls?.used ?? 0,
281
286
  maxCalls: toolCallContext.agentCalls?.max ?? 0,
282
287
  }),
283
- diagramPreparationAttempted ? visualPresentation(output) : '',
288
+ diagramCorrectionDraft
289
+ ? `${visualPresentation(output)}\n${
290
+ diagramContinuation
291
+ ? `你这版回答已向用户显示到字符框图之前(已保存为上一条消息,不能修改或撤回),框图及其后内容用户尚未看到。请紧接着已显示的内容继续写:先用 ${output.diagrams.join('/')} 重画那张框图,再按下方草稿写完其余部分,保留结论、数字、限定条件和链接;不要重复已显示的内容。`
292
+ : `你上一版回答(下方内部草稿,用户尚未看到)用字符画了框图,而当前载体可渲染 ${output.diagrams.join('/')}。请在这版草稿的基础上修订:只把字符框图改成这些格式,保留其余结论、数字、限定条件和链接。`
293
+ }技能目录里有图形技能时可先加载。\n<internal-draft>\n${diagramCorrectionDraft}\n</internal-draft>`
294
+ : '',
284
295
  delegationCorrectionAttempted
285
296
  ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。'
286
297
  : '',
@@ -291,8 +302,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
291
302
  ? `上一次模型响应没有可交付正文(${emptyResponseRecoveryReason})。请基于已有历史和工具结果继续完成任务:信息充分则给出实质结论,不充分则继续必要操作或明确缺口。不要重复已经完成的调查或有副作用的操作;不要把未完成的任务说成完成。已有顾问意见继续有效,无需仅因恢复而再次咨询。`
292
303
  : '',
293
304
  deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
294
- advisorAvailable ? advisorCoordinator.transientReminder() : '',
295
- advisorAvailable ? advisorCoordinator.usageLine(toolCallContext.advisorCalls!.used) : '',
305
+ advisorAvailable ? advisorNudge.reminder() : '',
296
306
  ];
297
307
  return parts.filter(Boolean).join('\n\n');
298
308
  };
@@ -577,7 +587,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
577
587
  replaceHistory,
578
588
  buildRequestTransientContext: (tools) => {
579
589
  currentEffectiveToolSet = tools;
580
- advisorCoordinator.setAvailable(tools.names.has('Advisor'));
590
+ advisorNudge.setAvailable(tools.names.has('Advisor') && options.advisorReminders !== false);
581
591
  lastRequestTransientContext = buildRequestTransientContext();
582
592
  return lastRequestTransientContext;
583
593
  },
@@ -599,7 +609,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
599
609
  let assistantText = '';
600
610
  // Candidate text is private until the delivery gates allow publication.
601
611
  // Keep the compatibility event as a phase notice, never as a text retraction.
602
- function* retireDraft(
612
+ function* preparationNotice(
603
613
  reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted',
604
614
  ): Generator<LoopEvent> {
605
615
  const message: Message = {
@@ -612,6 +622,11 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
612
622
  };
613
623
  history.push(message);
614
624
  yield { type: 'injected_message', message };
625
+ }
626
+ function* retireDraft(
627
+ reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted',
628
+ ): Generator<LoopEvent> {
629
+ yield* preparationNotice(reason);
615
630
  if (reason !== 'interrupted') yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
616
631
  }
617
632
  /** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
@@ -623,6 +638,25 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
623
638
  let reasoningContent = '';
624
639
  let reasoningString = '';
625
640
  const reasoningDetails: unknown[] = [];
641
+ /** Visible text is never retracted: whatever already streamed live is
642
+ * committed before the turn moves on (interruption, error, new messages). */
643
+ function* commitLiveAnswer(presentation: 'final' | 'progress'): Generator<LoopEvent> {
644
+ const message: Extract<Message, { role: 'assistant' }> = {
645
+ role: 'assistant',
646
+ // Only what the user actually saw: a box held back by the diagram gate
647
+ // must not slip into history through an interruption.
648
+ content: assistantText.slice(0, shownLength),
649
+ ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
650
+ ...(reasoningString ? { reasoning: reasoningString } : {}),
651
+ ...(reasoningDetails.length > 0 ? { reasoning_details: reasoningDetails } : {}),
652
+ timestamp: Date.now(),
653
+ id: crypto.randomUUID(),
654
+ presentation,
655
+ ...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
656
+ };
657
+ history.push(message);
658
+ yield { type: 'assistant_message', message };
659
+ }
626
660
 
627
661
  /**
628
662
  * R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
@@ -631,17 +665,63 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
631
665
  */
632
666
  let lastFinishReason: 'stop' | 'tool_calls' | 'length' | 'unknown' | 'aborted' = 'unknown';
633
667
 
668
+ // Decided before the model writes anything: can a check still hold this
669
+ // answer back? If not, it is irrevocable by construction and streams live
670
+ // (visible text is never retracted). If one can — a required formal delivery
671
+ // or an unresolved delegation — the candidate stays private until it passes
672
+ // and is then published verbatim. Streaming everything and retracting on a
673
+ // later check showed users drafts that vanished; buffering everything made
674
+ // every answer appear as one block. The advisor is deliberately not a holder
675
+ // any more: it is a reminder, so an answer never waits for it.
676
+ const liveEligible =
677
+ !options.responseFormat &&
678
+ runtimeContext.profile === 'main' &&
679
+ output.finalDelivery === 'stream' &&
680
+ !(formalDeliveryRequired && !resultPresented && !deliveryCorrectionAttempted);
681
+ // Only decides how the answer is shown: unreadable delegation state means
682
+ // "hold it back", never a failed turn (the final gate re-reads it anyway).
683
+ const delegationMayHold =
684
+ liveEligible &&
685
+ !delegationCorrectionAttempted &&
686
+ currentEffectiveToolSet.names.has('Agent') &&
687
+ (await agentCoordinationContext(toolCallContext).then(
688
+ (coordination) =>
689
+ coordination.unresolved.some((record) => record.parentMessageId === currentUserMessageId),
690
+ () => true,
691
+ ));
692
+ const liveFinal = liveEligible && !delegationMayHold;
693
+ let liveStarted = false;
694
+ let shownLength = 0;
695
+ // Live answers on a diagram-rendering surface hold back a fence that could
696
+ // be a text drawing until it closes, so a box can be corrected without
697
+ // retracting anything (see loop/liveDiagramGate.ts).
698
+ const diagramGate =
699
+ liveFinal && output.diagrams.length > 0 && !diagramCorrectionAttempted && !requestKeepsTextDiagrams(userInput)
700
+ ? new LiveDiagramGate()
701
+ : undefined;
702
+ function* releaseLive(final = false): Generator<LoopEvent> {
703
+ const end = diagramGate ? diagramGate.releasable(assistantText, final) : assistantText.length;
704
+ if (!liveStarted) {
705
+ if (!answerTextStarted(assistantText.slice(0, end))) return;
706
+ liveStarted = true;
707
+ yield { type: 'stage_change', stage: 'streaming' };
708
+ }
709
+ if (end > shownLength) {
710
+ yield { type: 'text', delta: assistantText.slice(shownLength, end) };
711
+ shownLength = end;
712
+ }
713
+ }
714
+
634
715
  // 进入 LLM 流前:等首 token / reasoning,本质是 thinking
635
716
  yield { type: 'stage_change', stage: 'thinking' };
636
717
  workStarted = true;
637
718
 
638
719
  try {
639
720
  const modelMessages = buildModelMessages(history, requestTransientContext, userMessage.id);
640
- advisorCoordinator.markTransientDelivered();
641
721
  for await (const ev of chatFn({
642
722
  provider,
643
723
  messages: modelMessages,
644
- tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'preparation'),
724
+ tools: currentEffectiveToolSet.tools,
645
725
  signal,
646
726
  conversationMode: runtimeContext.conversationMode ?? 'private',
647
727
  promptCache: options.promptCache,
@@ -652,6 +732,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
652
732
  })) {
653
733
  if (ev.type === 'text_delta') {
654
734
  assistantText += ev.delta;
735
+ if (liveFinal) yield* releaseLive();
655
736
  } else if (ev.type === 'tool_call_delta') {
656
737
  mergeToolCallDelta(toolCallsByIndex, ev);
657
738
  } else if (ev.type === 'reasoning_delta') {
@@ -694,15 +775,18 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
694
775
  } catch (e) {
695
776
  trace('model_error');
696
777
  if ((e as Error).name === 'AbortError') {
697
- yield* retireDraft('interrupted');
778
+ if (liveStarted) {
779
+ yield* commitLiveAnswer('final');
780
+ yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
781
+ } else yield* retireDraft('interrupted');
698
782
  yield interruption();
699
783
  return;
700
784
  }
701
785
 
702
786
  // prompt_too_long 反应式自救:压缩历史 → 同 turn 重发 LLM
703
787
  // LLM 看到压缩后的 9 段摘要 + 近期 verbatim 消息,能接着干活,
704
- // 不会丢失中途的工具调用上下文。
705
- if (isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
788
+ // 不会丢失中途的工具调用上下文。已实时显示过文字时不重发(会重复输出)。
789
+ if (!liveStarted && isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
706
790
  reactiveCount++;
707
791
  yield { type: 'compact_start' };
708
792
  yield { type: 'stage_change', stage: 'compacting' };
@@ -750,7 +834,10 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
750
834
  }
751
835
 
752
836
  // code='llm_error':终结性错误,stop_reason='error'
753
- yield* retireDraft('interrupted');
837
+ if (liveStarted) {
838
+ yield* commitLiveAnswer('final');
839
+ yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
840
+ } else yield* retireDraft('interrupted');
754
841
  yield {
755
842
  type: 'error',
756
843
  error: `LLM 调用失败:${(e as Error).message}`,
@@ -760,8 +847,18 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
760
847
  }
761
848
 
762
849
  trace('model_end');
850
+ // A tool round (e.g. loading a graphics skill) is part of the revision;
851
+ // the draft is dropped only once the revised answer itself has arrived.
852
+ if (toolCallsByIndex.length === 0) {
853
+ diagramCorrectionDraft = '';
854
+ diagramContinuation = false;
855
+ }
763
856
  if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
764
- yield* retireDraft('interrupted');
857
+ if (liveStarted) {
858
+ yield* commitLiveAnswer('final');
859
+ if (lastFinishReason !== 'stop')
860
+ yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
861
+ } else yield* retireDraft('interrupted');
765
862
  if (options.hasQueuedInput?.() && !signal?.aborted) {
766
863
  pushQueuedInputMarker(history);
767
864
  yield { type: 'interrupted', reason: 'queued_input' };
@@ -782,43 +879,12 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
782
879
  }
783
880
  }
784
881
 
785
- // A pending review must run before another solving/publishing attempt.
786
- // Read-only diagnosis and human questions remain available while stalled.
787
- const pendingReview = options.advisorCheckpoints === false ? undefined : advisorCoordinator.pendingReview();
788
- if (
789
- pendingReview &&
790
- turn < maxTurns &&
791
- toolCallsByIndex.length > 0 &&
792
- !toolCallsByIndex.some((tc) => ['Advisor', 'AskUserQuestion'].includes(tc.function.name)) &&
793
- toolCallsByIndex.some(
794
- (tc) =>
795
- tc.function.name === 'PresentResult' ||
796
- advisorSemanticsFor(
797
- tc,
798
- toolCallContext,
799
- currentEffectiveToolSet.tools.find((t) => t.name === tc.function.name),
800
- { treatVerificationAsReadOnly: true },
801
- ).mutatesWorkspace ||
802
- stallGuard.shouldReject(tc),
803
- )
804
- ) {
805
- yield* retireDraft('advisor_checkpoint');
806
- toolCallsByIndex.splice(0, toolCallsByIndex.length, {
807
- id: crypto.randomUUID(),
808
- type: 'function',
809
- function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) },
810
- });
811
- assistantText = '';
812
- }
813
- // Count the calls that survived review scheduling, including failed loads.
814
- for (const call of toolCallsByIndex) {
815
- if (call.function.name !== 'Skill') continue;
816
- try {
817
- if (JSON.parse(call.function.arguments).name === 'dynamic-ui') diagramPreparationAttempted = true;
818
- } catch {
819
- /* Ordinary tool validation reports malformed arguments. */
820
- }
882
+ if (liveFinal) {
883
+ // Narration before a tool round, or no turn left to revise: show it all.
884
+ if (diagramGate?.hit !== undefined && (toolCallsByIndex.length > 0 || turn >= maxTurns)) diagramGate.release();
885
+ yield* releaseLive(true);
821
886
  }
887
+
822
888
  const assistantMsg: Message = {
823
889
  role: 'assistant',
824
890
  content: assistantText.length > 0 ? assistantText : null,
@@ -832,31 +898,41 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
832
898
  };
833
899
  // 4. 没有工具调用 → 整轮交互结束
834
900
  finalResponse: if (toolCallsByIndex.length === 0) {
835
- // Repair only private preparation, through the ordinary tool pipeline.
836
- // One attempt per turn, including missing/failed skills; never rewind a reply.
901
+ if (diagramGate?.hit !== undefined) {
902
+ diagramCorrectionAttempted = true;
903
+ diagramCorrectionDraft = assistantText;
904
+ if (liveStarted) {
905
+ // The text before the box stays exactly as shown, as its own message.
906
+ diagramContinuation = true;
907
+ yield* commitLiveAnswer('progress');
908
+ yield* preparationNotice('diagram_preparation');
909
+ } else yield* retireDraft('diagram_preparation');
910
+ continue;
911
+ }
912
+ // A boxed ASCII diagram on a surface that renders real diagrams: retire the
913
+ // private draft once and let the model revise it, with the draft passed
914
+ // back as internal material (asking it to "keep the rest" without showing
915
+ // the rest made it answer from scratch). The kernel names no skill and
916
+ // never writes a tool call the model did not make — it used to inject
917
+ // Skill("dynamic-ui") into history under the model's name. Content checks
918
+ // like this one and the ask correction apply only to private drafts; an
919
+ // answer that already streamed live is never retracted.
837
920
  if (
838
- !diagramPreparationAttempted &&
921
+ !liveStarted &&
922
+ !diagramCorrectionAttempted &&
839
923
  !options.responseFormat &&
840
924
  runtimeContext.profile === 'main' &&
841
925
  output.diagrams.length > 0 &&
842
- currentEffectiveToolSet.names.has('Skill') &&
843
926
  turn < maxTurns &&
844
927
  needsDiagramPreparation(assistantText, userInput)
845
928
  ) {
846
- diagramPreparationAttempted = true;
847
- toolCallsByIndex.push({
848
- id: crypto.randomUUID(),
849
- type: 'function',
850
- function: {
851
- name: 'Skill',
852
- arguments: JSON.stringify({ name: 'dynamic-ui' }),
853
- },
854
- });
855
- assistantMsg.content = null;
856
- assistantMsg.tool_calls = toolCallsByIndex;
857
- break finalResponse;
929
+ diagramCorrectionAttempted = true;
930
+ diagramCorrectionDraft = assistantText;
931
+ yield* retireDraft('diagram_preparation');
932
+ continue;
858
933
  }
859
934
  if (
935
+ !liveStarted &&
860
936
  !askCorrectionAttempted &&
861
937
  runtimeContext.interactiveAsk &&
862
938
  currentEffectiveToolSet.names.has('AskUserQuestion') &&
@@ -872,7 +948,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
872
948
  runtimeContext.profile === 'main' ? await agentCoordinationContext(toolCallContext) : undefined;
873
949
  const pendingDelegations =
874
950
  coordination?.unresolved.filter((record) => record.parentMessageId === currentUserMessageId) ?? [];
875
- if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
951
+ if (!liveStarted && pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
876
952
  delegationCorrectionAttempted = true;
877
953
  coordinationText = coordination!.text;
878
954
  yield* retireDraft('formal_delivery');
@@ -883,6 +959,9 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
883
959
  ...(await takeAgentNotifications(toolCallContext)),
884
960
  ];
885
961
  if (pendingMessages.length) {
962
+ // A private draft is simply superseded; a live one was already read,
963
+ // so it stays in the conversation as this step's progress.
964
+ if (liveStarted) yield* commitLiveAnswer('progress');
886
965
  history.push(...pendingMessages);
887
966
  continue;
888
967
  }
@@ -890,6 +969,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
890
969
  // presenting it. Give it one model-only corrective pass; the reminder is
891
970
  // transient and therefore never pollutes persisted conversation history.
892
971
  if (
972
+ !liveStarted &&
893
973
  !emptyResponse &&
894
974
  formalDeliveryRequired &&
895
975
  !resultPresented &&
@@ -900,38 +980,19 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
900
980
  yield* retireDraft('formal_delivery');
901
981
  continue;
902
982
  }
903
- const deliveryCheckpoint =
904
- emptyResponse || options.advisorCheckpoints === false
905
- ? { allow: true as const }
906
- : advisorCoordinator.beforeDelivery(assistantText);
907
- const fallbackReview =
908
- !emptyResponse && options.advisorCheckpoints !== false
909
- ? advisorCoordinator.pendingReview(assistantText)
910
- : undefined;
911
- if (fallbackReview && turn < maxTurns) {
912
- yield* retireDraft('advisor_checkpoint');
913
- // Reuse the effective tool boundary, permissions, cancellation, events and
914
- // persistence. Never call a provider behind the host's tool pipeline.
915
- toolCallsByIndex.push({
916
- id: crypto.randomUUID(),
917
- type: 'function',
918
- function: {
919
- name: 'Advisor',
920
- arguments: JSON.stringify(fallbackReview),
921
- },
922
- });
923
- assistantMsg.content = null;
924
- assistantMsg.tool_calls = toolCallsByIndex;
925
- break finalResponse;
926
- }
927
- if (!deliveryCheckpoint.allow && turn < maxTurns) {
928
- yield* retireDraft('advisor_checkpoint');
929
- continue;
930
- }
983
+ let advisorFollowUp = false;
931
984
  if (!emptyResponse) {
932
- for (const message of advisorCoordinator.deliveryWarnings()) yield { type: 'warning', message };
933
- if (!deliveryCheckpoint.allow)
934
- yield { type: 'warning', message: '已达到本轮执行上限,顾问交付检查尚未完成;不能把此次输出视为已复核。' };
985
+ // A reminder, never a gate: the answer is already on its way to the user.
986
+ // With enforcement 'followup' (weak models that never ask on their own) an
987
+ // unreviewed substantive answer earns one more request in which the model is
988
+ // asked to call Advisor itself; the warning is then left for that pass.
989
+ if (turn < maxTurns && advisorNudge.followUpDue(assistantText) && (await advisorFollowUpEnabled(options))) {
990
+ advisorNudge.markFollowedUp();
991
+ advisorFollowUp = true;
992
+ } else {
993
+ const unreviewed = advisorNudge.unreviewedWarning(assistantText);
994
+ if (unreviewed) yield { type: 'warning', message: unreviewed };
995
+ }
935
996
  }
936
997
  if (emptyResponse) {
937
998
  // Recover once within the existing turn budget, retaining tool receipts and
@@ -958,85 +1019,25 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
958
1019
  }
959
1020
  yield* retireDraft('empty_response');
960
1021
  } else {
961
- // Publication is a separate tool-free request. Nothing from preparation
962
- // has been streamed, and no gate may retract text after this boundary.
963
- let publicationError: unknown;
964
- let publicationInterrupted = false;
965
- let streamed = false;
966
- if (!options.responseFormat && runtimeContext.profile === 'main' && output.finalDelivery === 'stream') {
967
- const publication = yield* publishFinalAnswer({
968
- provider,
969
- messages: buildModelMessages(history, requestTransientContext, userMessage.id),
970
- candidate: assistantText,
971
- candidateFinishReason: lastFinishReason,
972
- output,
973
- chatFn,
974
- options,
975
- signal,
976
- conversationMode: runtimeContext.conversationMode ?? 'private',
977
- turnUsage,
978
- });
979
- const publishedText = publication.text;
980
- ({ publicationError, publicationInterrupted, streamed, lastFinishReason } = publication);
981
- assistantText = publishedText;
982
- assistantMsg.content = publishedText || null;
983
- if (!publishedText) {
984
- if (publicationInterrupted)
985
- yield {
986
- ...interruption(),
987
- usage: {
988
- ...turnUsage,
989
- trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
990
- },
991
- };
992
- else
993
- yield {
994
- type: 'error',
995
- code: 'llm_error',
996
- error: `最终答案生成失败:${String(publicationError)}`,
997
- usage: {
998
- ...turnUsage,
999
- trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
1000
- },
1001
- };
1002
- return;
1003
- }
1004
- }
1022
+ // Publication is a one-way boundary: the text that passed every gate is
1023
+ // exactly what the user sees and what history keeps — streamed live
1024
+ // when nothing could hold it back, otherwise published verbatim now.
1025
+ // It used to be regenerated by a second tool-free request: a third
1026
+ // text no gate had seen, at the cost of a full-context call.
1027
+ if (!liveStarted && !options.responseFormat && runtimeContext.profile === 'main' && output.finalDelivery === 'stream')
1028
+ yield { type: 'stage_change', stage: 'streaming' };
1005
1029
  assistantMsg.presentation = 'final';
1006
1030
  trace('final_published');
1007
- if (assistantMsg.role === 'assistant')
1008
- assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
1009
- assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
1010
1031
  history.push(assistantMsg);
1011
- // Non-interactive consumers retain atomic output. Interactive hosts
1012
- // already received real deltas; commit the identical text only once.
1013
- if (!streamed) yield { type: 'text', delta: assistantText };
1032
+ if (!liveStarted) yield { type: 'text', delta: assistantText };
1014
1033
  yield {
1015
1034
  type: 'assistant_message',
1016
1035
  message: assistantMsg as Extract<Message, { role: 'assistant' }>,
1017
1036
  };
1018
- if (publicationInterrupted || publicationError) {
1019
- yield { type: 'warning', message: '最终答案未完整生成,已保留已输出内容;未撤回或重新生成。' };
1020
- if (publicationInterrupted)
1021
- yield {
1022
- ...interruption(),
1023
- usage: {
1024
- ...turnUsage,
1025
- trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
1026
- },
1027
- };
1028
- else
1029
- yield {
1030
- type: 'error',
1031
- code: 'llm_error',
1032
- error: `最终答案流中断:${String(publicationError)}`,
1033
- usage: {
1034
- ...turnUsage,
1035
- trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
1036
- },
1037
- };
1038
- return;
1039
- }
1037
+ }
1038
+ if (advisorFollowUp) {
1039
+ pushAdvisorFollowUpMarker(history);
1040
+ continue;
1040
1041
  }
1041
1042
  // R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
1042
1043
  // 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
@@ -1146,7 +1147,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1146
1147
  runtimeContext,
1147
1148
  toolCallContext,
1148
1149
  currentEffectiveToolSet,
1149
- advisorCoordinator,
1150
+ advisorNudge,
1150
1151
  stallGuard,
1151
1152
  modelEnvironmentCwd,
1152
1153
  observedRuleScopes,
@@ -1160,18 +1161,16 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1160
1161
  // latest evidence and versions. Parallel batches commit in call order once
1161
1162
  // settled. Both paths use this single writer, avoiding duplicate observation.
1162
1163
  const commitOutcome = async function* ({ tc, result }: WorkerOutcome): AsyncGenerator<LoopEvent> {
1163
- if (tc.function.name !== 'Advisor' && tc.function.name !== 'AdvisorDecision') {
1164
- advisorCoordinator.observeToolResult(
1164
+ advisorNudge.observeToolResult(
1165
+ tc,
1166
+ result,
1167
+ advisorSemanticsFor(
1165
1168
  tc,
1166
- result,
1167
- advisorSemanticsFor(
1168
- tc,
1169
- toolCallContext,
1170
- currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name),
1171
- { treatVerificationAsReadOnly: true },
1172
- ),
1173
- );
1174
- }
1169
+ toolCallContext,
1170
+ currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name),
1171
+ { treatVerificationAsReadOnly: true },
1172
+ ),
1173
+ );
1175
1174
  if (result.ok || result.disposition !== 'deferred') {
1176
1175
  evidenceLedger.record(tc.function.name, tc.function.arguments, result);
1177
1176
  }
@@ -1189,6 +1188,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1189
1188
  const manifest = result.metadata?.kind === 'result_manifest' ? result.metadata.manifest : browserArtifact;
1190
1189
  presentedResultUnverified ||= manifest?.files.some((file) => file.verification === 'failed') ?? false;
1191
1190
  }
1191
+ const actionHint = advisorNudge.takeActionHint();
1192
1192
  const rawContent = result.ok ? result.content : `Error: ${result.error}`;
1193
1193
  const basePolicy = result.policy ?? getToolResultPolicy(tc.function.name);
1194
1194
  // 超大结果先归档再压缩:归档失败时保留原文,绝不因为磁盘问题破坏上下文。
@@ -1212,9 +1212,10 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1212
1212
  ? { ...basePolicy, archiveRef: archived.archiveRef, originalChars: archived.originalChars }
1213
1213
  : basePolicy;
1214
1214
  // 微压缩仅对成功结果走;policy 会保护高价值结果,避免不可逆中段丢失。
1215
- const content = result.ok
1215
+ const compacted = result.ok
1216
1216
  ? microCompact(tc.function.name, archived.content, sessionState?.microCompact, policy)
1217
1217
  : rawContent;
1218
+ const content = actionHint ? `${compacted}\n\n${actionHint}` : compacted;
1218
1219
  history.push({
1219
1220
  role: 'tool',
1220
1221
  display_ok: result.ok,
@@ -1222,13 +1223,6 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1222
1223
  tool_call_id: tc.id,
1223
1224
  tool_result_policy: policy,
1224
1225
  ...(!result.ok && result.disposition === 'deferred' ? { toolResultDisposition: 'deferred' as const } : {}),
1225
- ...(tc === toolCallsByIndex.at(-1) ? { advisorCheckpoints: advisorCoordinator.drainDegradations() } : {}),
1226
- advisorMutatesWorkspace: advisorSemanticsFor(
1227
- tc,
1228
- toolCallContext,
1229
- currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name),
1230
- { treatVerificationAsReadOnly: true },
1231
- ).mutatesWorkspace,
1232
1226
  ...(result.metadata ? { display_metadata: result.metadata } : {}),
1233
1227
  timestamp: Date.now(),
1234
1228
  id: crypto.randomUUID(),
@@ -1277,7 +1271,7 @@ export async function* runQuery(userInput: string, options: RunQueryOptions): As
1277
1271
  stall.reason === 'repeated_failure'
1278
1272
  ? '同一问题两次尝试失败,请停止原样重试;顾问可用时,继续解决或交付前先咨询,由主 Agent 自行核验和决策。只读诊断仍可继续。'
1279
1273
  : '检测到连续工具轮次没有产生新文本或新结果;请切换路径或咨询 Advisor。';
1280
- if (stall.reason !== 'repeated_failure') advisorCoordinator.notifyStalled(stall.signature ?? stall.reason);
1274
+ if (stall.reason !== 'repeated_failure') advisorNudge.notifyStalled(stall.signature ?? stall.reason ?? 'no_progress');
1281
1275
  yield { type: 'warning', message };
1282
1276
  }
1283
1277