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.
- package/package.json +1 -3
- package/plugins/workflow-runner/src/runtime/context.js +1 -1
- package/plugins/workflow-runner/src/runtime/context.ts +1 -1
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +2 -5
- package/plugins/workflow-runner/test/execLifetime.test.ts +15 -2
- package/skills/ssh-remote-dev/SKILL.md +61 -0
- package/skills/ssh-remote-dev/agents/openai.yaml +4 -0
- package/src/advisor/README.md +62 -24
- package/src/advisor/advisorNudge.js +199 -0
- package/src/advisor/advisorNudge.ts +232 -0
- package/src/advisor/deliveryReview.js +35 -12
- package/src/advisor/deliveryReview.ts +35 -11
- package/src/advisor/presentation.js +7 -10
- package/src/advisor/presentation.ts +10 -13
- package/src/advisor/reviewContext.js +22 -58
- package/src/advisor/reviewContext.ts +24 -67
- package/src/cli/configMenu.js +17 -128
- package/src/cli/configMenu.tsx +17 -193
- package/src/cli/print.js +2 -0
- package/src/cli/print.ts +2 -0
- package/src/cli/streamJson.js +2 -0
- package/src/cli/streamJson.ts +2 -0
- package/src/cli/wizards/advisorWizard.js +9 -7
- package/src/cli/wizards/advisorWizard.tsx +9 -7
- package/src/cli/wizards/contextAdvisorWizard.js +9 -7
- package/src/cli/wizards/contextAdvisorWizard.tsx +9 -7
- package/src/cli/wizards/providerPresets.js +27 -1
- package/src/cli/wizards/providerPresets.ts +44 -2
- package/src/cli/wizards/providerWizard.js +51 -10
- package/src/cli/wizards/providerWizard.tsx +59 -9
- package/src/config/configDefaults.js +1 -1
- package/src/config/configDefaults.ts +1 -1
- package/src/config/configDiagnostics.js +1 -21
- package/src/config/configDiagnostics.ts +3 -23
- package/src/config/configFile.js +11 -4
- package/src/config/configFile.ts +27 -7
- package/src/config/configManagement.js +1 -0
- package/src/config/configManagement.ts +2 -0
- package/src/config/configService.js +12 -8
- package/src/config/configService.ts +16 -11
- package/src/config/numericLimits.js +0 -4
- package/src/config/numericLimits.ts +0 -4
- package/src/config/reasoningEffort.js +16 -0
- package/src/config/reasoningEffort.ts +18 -0
- package/src/context/budgetPlan.js +1 -1
- package/src/context/budgetPlan.ts +1 -1
- package/src/context/optimizeConversation.js +1 -1
- package/src/context/optimizeConversation.ts +1 -1
- package/src/harness/stallGuard.js +8 -13
- package/src/harness/stallGuard.ts +8 -12
- package/src/llm/client.js +64 -17
- package/src/llm/client.ts +62 -7
- package/src/llm/providers/openaiFormat.js +14 -3
- package/src/llm/providers/openaiFormat.ts +17 -3
- package/src/llm/providers/responsesFormat.js +275 -0
- package/src/llm/providers/responsesFormat.ts +306 -0
- package/src/loop/README.md +18 -3
- package/src/loop/liveDiagramGate.js +63 -0
- package/src/loop/liveDiagramGate.ts +61 -0
- package/src/loop/messages.js +12 -0
- package/src/loop/messages.ts +13 -0
- package/src/loop/options.ts +7 -2
- package/src/loop/toolBatch.js +0 -1
- package/src/loop/toolBatch.ts +0 -1
- package/src/loop/toolExecutor.js +11 -46
- package/src/loop/toolExecutor.ts +12 -55
- package/src/loop/toolPresentation.js +2 -13
- package/src/loop/toolPresentation.ts +2 -13
- package/src/loop/toolSemantics.js +3 -4
- package/src/loop/toolSemantics.ts +4 -6
- package/src/loop.js +222 -214
- package/src/loop.ts +226 -232
- package/src/memory/dependencies.js +11 -3
- package/src/memory/dependencies.ts +11 -3
- package/src/memory/episodicMemory.js +59 -18
- package/src/memory/episodicMemory.ts +58 -18
- package/src/memory/experienceStore.js +4 -3
- package/src/memory/experienceStore.ts +4 -3
- package/src/memory/factCandidates.js +53 -1
- package/src/memory/factCandidates.ts +54 -1
- package/src/memory/factLearning.js +2 -2
- package/src/memory/factLearning.ts +2 -2
- package/src/memory/jobPolicy.js +24 -5
- package/src/memory/jobPolicy.ts +25 -5
- package/src/memory/learning.js +2 -2
- package/src/memory/learning.ts +2 -2
- package/src/memory/learningModel.js +7 -0
- package/src/memory/learningModel.ts +7 -0
- package/src/memory/managementService.js +8 -7
- package/src/memory/managementService.ts +16 -8
- package/src/memory/memoryService.js +104 -33
- package/src/memory/memoryService.ts +132 -28
- package/src/memory/projectKnowledge.js +39 -3
- package/src/memory/projectKnowledge.ts +45 -3
- package/src/memory/recallQuery.js +56 -0
- package/src/memory/recallQuery.ts +58 -0
- package/src/memory/retrieval.js +39 -10
- package/src/memory/retrieval.ts +46 -11
- package/src/memory/turnJournal.js +75 -10
- package/src/memory/turnJournal.ts +66 -10
- package/src/memory/types.ts +7 -1
- package/src/prompts/delegation.js +2 -1
- package/src/prompts/delegation.ts +3 -1
- package/src/prompts/policyRegistry.js +9 -12
- package/src/prompts/policyRegistry.ts +9 -12
- package/src/prompts/promptAssembly.js +6 -5
- package/src/prompts/promptAssembly.ts +8 -6
- package/src/prompts/visualPresentation.js +39 -10
- package/src/prompts/visualPresentation.ts +38 -9
- package/src/runtime/answerPreparation.js +1 -1
- package/src/runtime/answerPreparation.ts +1 -1
- package/src/runtime/fileLock.js +19 -3
- package/src/runtime/fileLock.ts +19 -3
- package/src/runtime/invocationPolicy.js +1 -1
- package/src/runtime/invocationPolicy.ts +1 -1
- package/src/runtime/outputContract.js +0 -4
- package/src/runtime/outputContract.ts +0 -7
- package/src/session/runAgentTurn.js +3 -0
- package/src/session/runAgentTurn.ts +3 -0
- package/src/team/onlineToolContract.js +0 -1
- package/src/team/onlineToolContract.ts +0 -1
- package/src/tools/advisor/advisor.js +207 -365
- package/src/tools/advisor/advisor.ts +211 -383
- package/src/tools/agent/agentTypes.js +0 -1
- package/src/tools/agent/agentTypes.ts +0 -1
- package/src/tools/agent/coordinator.js +4 -1
- package/src/tools/agent/coordinator.ts +4 -1
- package/src/tools/bash/semantics.js +4 -5
- package/src/tools/bash/semantics.ts +4 -5
- package/src/tools/capabilities.js +2 -3
- package/src/tools/capabilities.ts +2 -4
- package/src/tools/index.js +0 -16
- package/src/tools/index.ts +0 -16
- package/src/tools/memory/maintenance.js +8 -9
- package/src/tools/memory/maintenance.ts +13 -11
- package/src/tools/memory/memory.js +5 -1
- package/src/tools/memory/memory.ts +5 -0
- package/src/tools/optimizeConversation/README.md +4 -6
- package/src/tools/toolPromptGuides.js +2 -9
- package/src/tools/toolPromptGuides.ts +2 -9
- package/src/types.ts +28 -105
- package/src/ui/App.js +2 -0
- package/src/ui/App.tsx +1 -0
- package/src/ui/MessageList.js +8 -44
- package/src/ui/MessageList.tsx +3 -81
- package/src/ui/hooks/useChat.js +2 -0
- package/src/ui/hooks/useChat.ts +2 -0
- package/src/ui/toolProgress.js +1 -1
- package/src/ui/toolProgress.ts +1 -1
- package/src/web/protocol.ts +10 -3
- package/src/advisor/advisorCoordinator.js +0 -734
- package/src/advisor/advisorCoordinator.ts +0 -825
- package/src/cli/wizards/sshHostWizard.js +0 -156
- package/src/cli/wizards/sshHostWizard.tsx +0 -214
- package/src/context/sshHosts.js +0 -290
- package/src/context/sshHosts.ts +0 -312
- package/src/loop/publication.js +0 -94
- package/src/loop/publication.ts +0 -124
- package/src/runtime/finalAnswer.js +0 -9
- package/src/runtime/finalAnswer.ts +0 -13
- package/src/tools/advisor/advisorDecision.js +0 -79
- package/src/tools/advisor/advisorDecision.ts +0 -92
- package/src/tools/ssh/ssh.js +0 -152
- package/src/tools/ssh/ssh.ts +0 -167
- package/src/tools/ssh/sshAvailability.js +0 -35
- package/src/tools/ssh/sshAvailability.ts +0 -39
- package/src/tools/ssh/sshClient.js +0 -177
- package/src/tools/ssh/sshClient.ts +0 -189
- package/src/tools/ssh/sshDownload.js +0 -77
- package/src/tools/ssh/sshDownload.ts +0 -92
- package/src/tools/ssh/sshHostsTool.js +0 -36
- package/src/tools/ssh/sshHostsTool.ts +0 -48
- package/src/tools/ssh/sshUpload.js +0 -72
- package/src/tools/ssh/sshUpload.ts +0 -85
package/src/loop.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { DELIVERY_CORRECTION_REMINDER, expectsFormalDelivery } from './loop/deliveryIntent.js';
|
|
2
|
-
import { buildModelMessages, evictPriorVisionShots, injectVisionFollowups, mergeToolCallDelta, pushManualInterruptMarker, pushQueuedInputMarker, } from './loop/messages.js';
|
|
3
|
-
import { publishFinalAnswer } from './loop/publication.js';
|
|
2
|
+
import { buildModelMessages, evictPriorVisionShots, injectVisionFollowups, mergeToolCallDelta, pushManualInterruptMarker, pushAdvisorFollowUpMarker, pushQueuedInputMarker, } from './loop/messages.js';
|
|
4
3
|
import { prepareModelRequest } from './loop/requestPreparation.js';
|
|
5
4
|
import { executeToolBatch } from './loop/toolBatch.js';
|
|
6
5
|
import { createToolExecutor } from './loop/toolExecutor.js';
|
|
@@ -11,14 +10,16 @@ export { friendlyToolDescription } from './loop/toolPresentation.js';
|
|
|
11
10
|
export { advisorSemanticsFor } from './loop/toolSemantics.js';
|
|
12
11
|
/**
|
|
13
12
|
* Agent turn orchestrator. Owns conversation state and continuation decisions.
|
|
14
|
-
* Request preparation
|
|
15
|
-
*
|
|
13
|
+
* Request preparation and tool execution/scheduling live in ./loop/; the
|
|
14
|
+
* final answer is published here, verbatim, once every delivery gate passes.
|
|
15
|
+
* Host options and LoopEvent remain the public compatibility boundary.
|
|
16
16
|
* See ./loop/README.md for phase ownership and invariants.
|
|
17
17
|
*/
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
19
|
import { resolve } from 'node:path';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { AdvisorNudge } from './advisor/advisorNudge.js';
|
|
21
|
+
import { collectAdvisorBackground } from './advisor/reviewContext.js';
|
|
22
|
+
import { previousUserRequest } from './memory/recallQuery.js';
|
|
22
23
|
import { configService } from './config/configService.js';
|
|
23
24
|
import { buildContextBudgetPlan } from './context/budgetPlan.js';
|
|
24
25
|
import { getCompactThreshold } from './context/compact.js';
|
|
@@ -36,12 +37,13 @@ import { buildUserContent } from './multimodal.js';
|
|
|
36
37
|
import { delegationRuntimeGuidance } from './prompts/delegation.js';
|
|
37
38
|
import { capabilitiesForTools, enabledPolicyIds } from './prompts/policyRegistry.js';
|
|
38
39
|
import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
|
|
39
|
-
import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
|
|
40
|
+
import { needsDiagramPreparation, requestKeepsTextDiagrams, visualPresentation } from './prompts/visualPresentation.js';
|
|
41
|
+
import { LiveDiagramGate } from './loop/liveDiagramGate.js';
|
|
40
42
|
import { createAgentRuntimeContext } from './runtime/agentRuntimeContext.js';
|
|
41
43
|
import { answerPreparationNotice } from './runtime/answerPreparation.js';
|
|
42
44
|
import { invocationInstructions, invocationToolAllowed } from './runtime/invocationPolicy.js';
|
|
43
45
|
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
44
|
-
import { resolveOutputContract
|
|
46
|
+
import { resolveOutputContract } from './runtime/outputContract.js';
|
|
45
47
|
import { filterToolsForProvider } from './team/toolPolicy.js';
|
|
46
48
|
import { agentCoordinationContext, takeAgentNotifications } from './tools/agent/agentTaskService.js';
|
|
47
49
|
import { needsInteractiveQuestion } from './tools/ask/routing.js';
|
|
@@ -55,6 +57,26 @@ import { hydrateSessionWorktree } from './tools/worktree/sessionWorktree.js';
|
|
|
55
57
|
import { stripThinkBlocks } from './utils/stripThink.js';
|
|
56
58
|
/** 主工具循环的默认轮数上限。 */
|
|
57
59
|
export const DEFAULT_MAX_TURNS = 200;
|
|
60
|
+
/** Live streaming starts once real answer text exists. A leading inline
|
|
61
|
+
* <think> block (MiniMax and similar), even a tag still arriving in pieces,
|
|
62
|
+
* is not the answer and must not be shown as if it were. */
|
|
63
|
+
/** 显式选项优先;否则读配置 `advisor.enforcement`(只在需要补问时才读一次盘,读失败当作关闭)。 */
|
|
64
|
+
async function advisorFollowUpEnabled(options) {
|
|
65
|
+
if (options.advisorFollowUp !== undefined)
|
|
66
|
+
return options.advisorFollowUp;
|
|
67
|
+
try {
|
|
68
|
+
return (await configService.advisor())?.enforcement === 'followup';
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function answerTextStarted(text) {
|
|
75
|
+
const lead = text.trimStart();
|
|
76
|
+
if (lead.length < '<think>'.length && '<think>'.startsWith(lead.toLowerCase()))
|
|
77
|
+
return false;
|
|
78
|
+
return stripThinkBlocks(text).length > 0;
|
|
79
|
+
}
|
|
58
80
|
/**
|
|
59
81
|
* 执行一次"用户输入 → 模型回答完成"的完整流程。
|
|
60
82
|
*
|
|
@@ -128,7 +150,13 @@ export async function* runQuery(userInput, options) {
|
|
|
128
150
|
let emptyResponseRecoveryAttempted = false;
|
|
129
151
|
let emptyResponseRecoveryReason = '';
|
|
130
152
|
let askCorrectionAttempted = false;
|
|
131
|
-
let
|
|
153
|
+
let diagramCorrectionAttempted = false;
|
|
154
|
+
// The retired draft rides along with the next request only, as private
|
|
155
|
+
// revision material: never shown to the user, never stored in history.
|
|
156
|
+
let diagramCorrectionDraft = '';
|
|
157
|
+
// Part of that draft already streamed live (the text before the box): the
|
|
158
|
+
// model continues from there instead of rewriting what the user has read.
|
|
159
|
+
let diagramContinuation = false;
|
|
132
160
|
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
133
161
|
const subagents = await configService.subagents();
|
|
134
162
|
await hydrateSessionWorktree({
|
|
@@ -148,35 +176,12 @@ export async function* runQuery(userInput, options) {
|
|
|
148
176
|
},
|
|
149
177
|
history,
|
|
150
178
|
sessionState,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
authority: 'host',
|
|
158
|
-
content: JSON.stringify({
|
|
159
|
-
cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
160
|
-
conversationMode: toolCallContext.conversationMode,
|
|
161
|
-
profile: runtimeContext.profile,
|
|
162
|
-
availableTools: [...currentEffectiveToolSet.names],
|
|
163
|
-
invocation: invocationInstructions(options.invocationPolicy),
|
|
164
|
-
}),
|
|
165
|
-
},
|
|
166
|
-
],
|
|
167
|
-
omittedIds: [
|
|
168
|
-
...new Set([
|
|
169
|
-
...(currentTransientBundle?.diagnostics.omittedIds ?? []),
|
|
170
|
-
...(advisorContextBundle?.diagnostics.omittedIds ?? []),
|
|
171
|
-
]),
|
|
172
|
-
...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : []),
|
|
173
|
-
],
|
|
174
|
-
clippedIds: [
|
|
175
|
-
...new Set([
|
|
176
|
-
...(currentTransientBundle?.diagnostics.clippedIds ?? []),
|
|
177
|
-
...(advisorContextBundle?.diagnostics.clippedIds ?? []),
|
|
178
|
-
]),
|
|
179
|
-
],
|
|
179
|
+
// Project constraints and memory are not in the conversation history, so the
|
|
180
|
+
// advisor gets them as one background block (see tools/advisor/advisor.ts).
|
|
181
|
+
advisorBackground: () => collectAdvisorBackground(advisorContextBundle, {
|
|
182
|
+
cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
183
|
+
availableTools: [...currentEffectiveToolSet.names],
|
|
184
|
+
invocation: invocationInstructions(options.invocationPolicy),
|
|
180
185
|
}),
|
|
181
186
|
// Statistics only; consultations have no per-turn quota.
|
|
182
187
|
advisorCalls: { used: 0 },
|
|
@@ -222,20 +227,20 @@ export async function* runQuery(userInput, options) {
|
|
|
222
227
|
options.invocationPolicy?.delivery !== 'caller' &&
|
|
223
228
|
effectiveToolSet.names.has('PresentResult');
|
|
224
229
|
let currentEffectiveToolSet = effectiveToolSet;
|
|
225
|
-
|
|
226
|
-
|
|
230
|
+
// The host only reminds; whether to consult the advisor is the model's call
|
|
231
|
+
// (see advisor/advisorNudge.ts). Nothing here blocks a tool or holds an answer.
|
|
232
|
+
const advisorNudge = new AdvisorNudge({
|
|
227
233
|
currentRequest: userInput,
|
|
228
|
-
|
|
229
|
-
advisorAvailable: effectiveToolSet.names.has('Advisor'),
|
|
234
|
+
previousRequest: previousUserRequest(history),
|
|
235
|
+
advisorAvailable: effectiveToolSet.names.has('Advisor') && options.advisorReminders !== false,
|
|
230
236
|
});
|
|
231
|
-
toolCallContext.advisorRuntime = advisorCoordinator;
|
|
232
237
|
let coordinationText = '';
|
|
233
238
|
let delegationCorrectionAttempted = false;
|
|
234
239
|
// 组装每轮请求的易变上下文。三个构建点(首轮 / autoCompact 后 / snip 后)共用,
|
|
235
240
|
// 避免 Advisor 相关提醒在复制粘贴中三处漂移。提醒均为 transient:只进当轮请求副本,
|
|
236
241
|
// 不进会话历史(与 DELIVERY_CORRECTION_REMINDER 同构,见 buildModelMessages)。
|
|
237
242
|
const buildRequestTransientContext = () => {
|
|
238
|
-
const advisorAvailable = currentEffectiveToolSet.names.has('Advisor');
|
|
243
|
+
const advisorAvailable = currentEffectiveToolSet.names.has('Advisor') && options.advisorReminders !== false;
|
|
239
244
|
const parts = [
|
|
240
245
|
transientFor(activeTools),
|
|
241
246
|
coordinationText,
|
|
@@ -245,7 +250,11 @@ export async function* runQuery(userInput, options) {
|
|
|
245
250
|
callsUsed: toolCallContext.agentCalls?.used ?? 0,
|
|
246
251
|
maxCalls: toolCallContext.agentCalls?.max ?? 0,
|
|
247
252
|
}),
|
|
248
|
-
|
|
253
|
+
diagramCorrectionDraft
|
|
254
|
+
? `${visualPresentation(output)}\n${diagramContinuation
|
|
255
|
+
? `你这版回答已向用户显示到字符框图之前(已保存为上一条消息,不能修改或撤回),框图及其后内容用户尚未看到。请紧接着已显示的内容继续写:先用 ${output.diagrams.join('/')} 重画那张框图,再按下方草稿写完其余部分,保留结论、数字、限定条件和链接;不要重复已显示的内容。`
|
|
256
|
+
: `你上一版回答(下方内部草稿,用户尚未看到)用字符画了框图,而当前载体可渲染 ${output.diagrams.join('/')}。请在这版草稿的基础上修订:只把字符框图改成这些格式,保留其余结论、数字、限定条件和链接。`}技能目录里有图形技能时可先加载。\n<internal-draft>\n${diagramCorrectionDraft}\n</internal-draft>`
|
|
257
|
+
: '',
|
|
249
258
|
delegationCorrectionAttempted
|
|
250
259
|
? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。'
|
|
251
260
|
: '',
|
|
@@ -256,8 +265,7 @@ export async function* runQuery(userInput, options) {
|
|
|
256
265
|
? `上一次模型响应没有可交付正文(${emptyResponseRecoveryReason})。请基于已有历史和工具结果继续完成任务:信息充分则给出实质结论,不充分则继续必要操作或明确缺口。不要重复已经完成的调查或有副作用的操作;不要把未完成的任务说成完成。已有顾问意见继续有效,无需仅因恢复而再次咨询。`
|
|
257
266
|
: '',
|
|
258
267
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
259
|
-
advisorAvailable ?
|
|
260
|
-
advisorAvailable ? advisorCoordinator.usageLine(toolCallContext.advisorCalls.used) : '',
|
|
268
|
+
advisorAvailable ? advisorNudge.reminder() : '',
|
|
261
269
|
];
|
|
262
270
|
return parts.filter(Boolean).join('\n\n');
|
|
263
271
|
};
|
|
@@ -524,7 +532,7 @@ export async function* runQuery(userInput, options) {
|
|
|
524
532
|
replaceHistory,
|
|
525
533
|
buildRequestTransientContext: (tools) => {
|
|
526
534
|
currentEffectiveToolSet = tools;
|
|
527
|
-
|
|
535
|
+
advisorNudge.setAvailable(tools.names.has('Advisor') && options.advisorReminders !== false);
|
|
528
536
|
lastRequestTransientContext = buildRequestTransientContext();
|
|
529
537
|
return lastRequestTransientContext;
|
|
530
538
|
},
|
|
@@ -545,7 +553,7 @@ export async function* runQuery(userInput, options) {
|
|
|
545
553
|
let assistantText = '';
|
|
546
554
|
// Candidate text is private until the delivery gates allow publication.
|
|
547
555
|
// Keep the compatibility event as a phase notice, never as a text retraction.
|
|
548
|
-
function*
|
|
556
|
+
function* preparationNotice(reason) {
|
|
549
557
|
const message = {
|
|
550
558
|
role: 'system',
|
|
551
559
|
content: answerPreparationNotice(reason),
|
|
@@ -556,6 +564,9 @@ export async function* runQuery(userInput, options) {
|
|
|
556
564
|
};
|
|
557
565
|
history.push(message);
|
|
558
566
|
yield { type: 'injected_message', message };
|
|
567
|
+
}
|
|
568
|
+
function* retireDraft(reason) {
|
|
569
|
+
yield* preparationNotice(reason);
|
|
559
570
|
if (reason !== 'interrupted')
|
|
560
571
|
yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
|
|
561
572
|
}
|
|
@@ -568,22 +579,80 @@ export async function* runQuery(userInput, options) {
|
|
|
568
579
|
let reasoningContent = '';
|
|
569
580
|
let reasoningString = '';
|
|
570
581
|
const reasoningDetails = [];
|
|
582
|
+
/** Visible text is never retracted: whatever already streamed live is
|
|
583
|
+
* committed before the turn moves on (interruption, error, new messages). */
|
|
584
|
+
function* commitLiveAnswer(presentation) {
|
|
585
|
+
const message = {
|
|
586
|
+
role: 'assistant',
|
|
587
|
+
// Only what the user actually saw: a box held back by the diagram gate
|
|
588
|
+
// must not slip into history through an interruption.
|
|
589
|
+
content: assistantText.slice(0, shownLength),
|
|
590
|
+
...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
|
|
591
|
+
...(reasoningString ? { reasoning: reasoningString } : {}),
|
|
592
|
+
...(reasoningDetails.length > 0 ? { reasoning_details: reasoningDetails } : {}),
|
|
593
|
+
timestamp: Date.now(),
|
|
594
|
+
id: crypto.randomUUID(),
|
|
595
|
+
presentation,
|
|
596
|
+
...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
|
|
597
|
+
};
|
|
598
|
+
history.push(message);
|
|
599
|
+
yield { type: 'assistant_message', message };
|
|
600
|
+
}
|
|
571
601
|
/**
|
|
572
602
|
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
573
603
|
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
574
604
|
* 避免「半截答案」被静默当成正常 end_turn。
|
|
575
605
|
*/
|
|
576
606
|
let lastFinishReason = 'unknown';
|
|
607
|
+
// Decided before the model writes anything: can a check still hold this
|
|
608
|
+
// answer back? If not, it is irrevocable by construction and streams live
|
|
609
|
+
// (visible text is never retracted). If one can — a required formal delivery
|
|
610
|
+
// or an unresolved delegation — the candidate stays private until it passes
|
|
611
|
+
// and is then published verbatim. Streaming everything and retracting on a
|
|
612
|
+
// later check showed users drafts that vanished; buffering everything made
|
|
613
|
+
// every answer appear as one block. The advisor is deliberately not a holder
|
|
614
|
+
// any more: it is a reminder, so an answer never waits for it.
|
|
615
|
+
const liveEligible = !options.responseFormat &&
|
|
616
|
+
runtimeContext.profile === 'main' &&
|
|
617
|
+
output.finalDelivery === 'stream' &&
|
|
618
|
+
!(formalDeliveryRequired && !resultPresented && !deliveryCorrectionAttempted);
|
|
619
|
+
// Only decides how the answer is shown: unreadable delegation state means
|
|
620
|
+
// "hold it back", never a failed turn (the final gate re-reads it anyway).
|
|
621
|
+
const delegationMayHold = liveEligible &&
|
|
622
|
+
!delegationCorrectionAttempted &&
|
|
623
|
+
currentEffectiveToolSet.names.has('Agent') &&
|
|
624
|
+
(await agentCoordinationContext(toolCallContext).then((coordination) => coordination.unresolved.some((record) => record.parentMessageId === currentUserMessageId), () => true));
|
|
625
|
+
const liveFinal = liveEligible && !delegationMayHold;
|
|
626
|
+
let liveStarted = false;
|
|
627
|
+
let shownLength = 0;
|
|
628
|
+
// Live answers on a diagram-rendering surface hold back a fence that could
|
|
629
|
+
// be a text drawing until it closes, so a box can be corrected without
|
|
630
|
+
// retracting anything (see loop/liveDiagramGate.ts).
|
|
631
|
+
const diagramGate = liveFinal && output.diagrams.length > 0 && !diagramCorrectionAttempted && !requestKeepsTextDiagrams(userInput)
|
|
632
|
+
? new LiveDiagramGate()
|
|
633
|
+
: undefined;
|
|
634
|
+
function* releaseLive(final = false) {
|
|
635
|
+
const end = diagramGate ? diagramGate.releasable(assistantText, final) : assistantText.length;
|
|
636
|
+
if (!liveStarted) {
|
|
637
|
+
if (!answerTextStarted(assistantText.slice(0, end)))
|
|
638
|
+
return;
|
|
639
|
+
liveStarted = true;
|
|
640
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
641
|
+
}
|
|
642
|
+
if (end > shownLength) {
|
|
643
|
+
yield { type: 'text', delta: assistantText.slice(shownLength, end) };
|
|
644
|
+
shownLength = end;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
577
647
|
// 进入 LLM 流前:等首 token / reasoning,本质是 thinking
|
|
578
648
|
yield { type: 'stage_change', stage: 'thinking' };
|
|
579
649
|
workStarted = true;
|
|
580
650
|
try {
|
|
581
651
|
const modelMessages = buildModelMessages(history, requestTransientContext, userMessage.id);
|
|
582
|
-
advisorCoordinator.markTransientDelivered();
|
|
583
652
|
for await (const ev of chatFn({
|
|
584
653
|
provider,
|
|
585
654
|
messages: modelMessages,
|
|
586
|
-
tools:
|
|
655
|
+
tools: currentEffectiveToolSet.tools,
|
|
587
656
|
signal,
|
|
588
657
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
589
658
|
promptCache: options.promptCache,
|
|
@@ -594,6 +663,8 @@ export async function* runQuery(userInput, options) {
|
|
|
594
663
|
})) {
|
|
595
664
|
if (ev.type === 'text_delta') {
|
|
596
665
|
assistantText += ev.delta;
|
|
666
|
+
if (liveFinal)
|
|
667
|
+
yield* releaseLive();
|
|
597
668
|
}
|
|
598
669
|
else if (ev.type === 'tool_call_delta') {
|
|
599
670
|
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
@@ -648,14 +719,19 @@ export async function* runQuery(userInput, options) {
|
|
|
648
719
|
catch (e) {
|
|
649
720
|
trace('model_error');
|
|
650
721
|
if (e.name === 'AbortError') {
|
|
651
|
-
|
|
722
|
+
if (liveStarted) {
|
|
723
|
+
yield* commitLiveAnswer('final');
|
|
724
|
+
yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
|
|
725
|
+
}
|
|
726
|
+
else
|
|
727
|
+
yield* retireDraft('interrupted');
|
|
652
728
|
yield interruption();
|
|
653
729
|
return;
|
|
654
730
|
}
|
|
655
731
|
// prompt_too_long 反应式自救:压缩历史 → 同 turn 重发 LLM
|
|
656
732
|
// LLM 看到压缩后的 9 段摘要 + 近期 verbatim 消息,能接着干活,
|
|
657
|
-
//
|
|
658
|
-
if (isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
|
|
733
|
+
// 不会丢失中途的工具调用上下文。已实时显示过文字时不重发(会重复输出)。
|
|
734
|
+
if (!liveStarted && isPromptTooLongError(e) && reactiveCount < MAX_REACTIVE_PER_QUERY) {
|
|
659
735
|
reactiveCount++;
|
|
660
736
|
yield { type: 'compact_start' };
|
|
661
737
|
yield { type: 'stage_change', stage: 'compacting' };
|
|
@@ -695,7 +771,12 @@ export async function* runQuery(userInput, options) {
|
|
|
695
771
|
// reactive 没压动或未恢复 → 退回正常错误路径
|
|
696
772
|
}
|
|
697
773
|
// code='llm_error':终结性错误,stop_reason='error'
|
|
698
|
-
|
|
774
|
+
if (liveStarted) {
|
|
775
|
+
yield* commitLiveAnswer('final');
|
|
776
|
+
yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
|
|
777
|
+
}
|
|
778
|
+
else
|
|
779
|
+
yield* retireDraft('interrupted');
|
|
699
780
|
yield {
|
|
700
781
|
type: 'error',
|
|
701
782
|
error: `LLM 调用失败:${e.message}`,
|
|
@@ -704,8 +785,20 @@ export async function* runQuery(userInput, options) {
|
|
|
704
785
|
return;
|
|
705
786
|
}
|
|
706
787
|
trace('model_end');
|
|
788
|
+
// A tool round (e.g. loading a graphics skill) is part of the revision;
|
|
789
|
+
// the draft is dropped only once the revised answer itself has arrived.
|
|
790
|
+
if (toolCallsByIndex.length === 0) {
|
|
791
|
+
diagramCorrectionDraft = '';
|
|
792
|
+
diagramContinuation = false;
|
|
793
|
+
}
|
|
707
794
|
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
708
|
-
|
|
795
|
+
if (liveStarted) {
|
|
796
|
+
yield* commitLiveAnswer('final');
|
|
797
|
+
if (lastFinishReason !== 'stop')
|
|
798
|
+
yield { type: 'warning', message: '回答未完整生成,已保留已显示的内容;未撤回或重新生成。' };
|
|
799
|
+
}
|
|
800
|
+
else
|
|
801
|
+
yield* retireDraft('interrupted');
|
|
709
802
|
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
710
803
|
pushQueuedInputMarker(history);
|
|
711
804
|
yield { type: 'interrupted', reason: 'queued_input' };
|
|
@@ -725,35 +818,11 @@ export async function* runQuery(userInput, options) {
|
|
|
725
818
|
toolCallsByIndex.push(...dense);
|
|
726
819
|
}
|
|
727
820
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
toolCallsByIndex.length > 0 &&
|
|
734
|
-
!toolCallsByIndex.some((tc) => ['Advisor', 'AskUserQuestion'].includes(tc.function.name)) &&
|
|
735
|
-
toolCallsByIndex.some((tc) => tc.function.name === 'PresentResult' ||
|
|
736
|
-
advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find((t) => t.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace ||
|
|
737
|
-
stallGuard.shouldReject(tc))) {
|
|
738
|
-
yield* retireDraft('advisor_checkpoint');
|
|
739
|
-
toolCallsByIndex.splice(0, toolCallsByIndex.length, {
|
|
740
|
-
id: crypto.randomUUID(),
|
|
741
|
-
type: 'function',
|
|
742
|
-
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) },
|
|
743
|
-
});
|
|
744
|
-
assistantText = '';
|
|
745
|
-
}
|
|
746
|
-
// Count the calls that survived review scheduling, including failed loads.
|
|
747
|
-
for (const call of toolCallsByIndex) {
|
|
748
|
-
if (call.function.name !== 'Skill')
|
|
749
|
-
continue;
|
|
750
|
-
try {
|
|
751
|
-
if (JSON.parse(call.function.arguments).name === 'dynamic-ui')
|
|
752
|
-
diagramPreparationAttempted = true;
|
|
753
|
-
}
|
|
754
|
-
catch {
|
|
755
|
-
/* Ordinary tool validation reports malformed arguments. */
|
|
756
|
-
}
|
|
821
|
+
if (liveFinal) {
|
|
822
|
+
// Narration before a tool round, or no turn left to revise: show it all.
|
|
823
|
+
if (diagramGate?.hit !== undefined && (toolCallsByIndex.length > 0 || turn >= maxTurns))
|
|
824
|
+
diagramGate.release();
|
|
825
|
+
yield* releaseLive(true);
|
|
757
826
|
}
|
|
758
827
|
const assistantMsg = {
|
|
759
828
|
role: 'assistant',
|
|
@@ -768,29 +837,41 @@ export async function* runQuery(userInput, options) {
|
|
|
768
837
|
};
|
|
769
838
|
// 4. 没有工具调用 → 整轮交互结束
|
|
770
839
|
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
840
|
+
if (diagramGate?.hit !== undefined) {
|
|
841
|
+
diagramCorrectionAttempted = true;
|
|
842
|
+
diagramCorrectionDraft = assistantText;
|
|
843
|
+
if (liveStarted) {
|
|
844
|
+
// The text before the box stays exactly as shown, as its own message.
|
|
845
|
+
diagramContinuation = true;
|
|
846
|
+
yield* commitLiveAnswer('progress');
|
|
847
|
+
yield* preparationNotice('diagram_preparation');
|
|
848
|
+
}
|
|
849
|
+
else
|
|
850
|
+
yield* retireDraft('diagram_preparation');
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
// A boxed ASCII diagram on a surface that renders real diagrams: retire the
|
|
854
|
+
// private draft once and let the model revise it, with the draft passed
|
|
855
|
+
// back as internal material (asking it to "keep the rest" without showing
|
|
856
|
+
// the rest made it answer from scratch). The kernel names no skill and
|
|
857
|
+
// never writes a tool call the model did not make — it used to inject
|
|
858
|
+
// Skill("dynamic-ui") into history under the model's name. Content checks
|
|
859
|
+
// like this one and the ask correction apply only to private drafts; an
|
|
860
|
+
// answer that already streamed live is never retracted.
|
|
861
|
+
if (!liveStarted &&
|
|
862
|
+
!diagramCorrectionAttempted &&
|
|
774
863
|
!options.responseFormat &&
|
|
775
864
|
runtimeContext.profile === 'main' &&
|
|
776
865
|
output.diagrams.length > 0 &&
|
|
777
|
-
currentEffectiveToolSet.names.has('Skill') &&
|
|
778
866
|
turn < maxTurns &&
|
|
779
867
|
needsDiagramPreparation(assistantText, userInput)) {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
function: {
|
|
785
|
-
name: 'Skill',
|
|
786
|
-
arguments: JSON.stringify({ name: 'dynamic-ui' }),
|
|
787
|
-
},
|
|
788
|
-
});
|
|
789
|
-
assistantMsg.content = null;
|
|
790
|
-
assistantMsg.tool_calls = toolCallsByIndex;
|
|
791
|
-
break finalResponse;
|
|
868
|
+
diagramCorrectionAttempted = true;
|
|
869
|
+
diagramCorrectionDraft = assistantText;
|
|
870
|
+
yield* retireDraft('diagram_preparation');
|
|
871
|
+
continue;
|
|
792
872
|
}
|
|
793
|
-
if (!
|
|
873
|
+
if (!liveStarted &&
|
|
874
|
+
!askCorrectionAttempted &&
|
|
794
875
|
runtimeContext.interactiveAsk &&
|
|
795
876
|
currentEffectiveToolSet.names.has('AskUserQuestion') &&
|
|
796
877
|
needsInteractiveQuestion(stripThinkBlocks(assistantText)) &&
|
|
@@ -802,7 +883,7 @@ export async function* runQuery(userInput, options) {
|
|
|
802
883
|
const emptyResponse = stripThinkBlocks(assistantText).length === 0;
|
|
803
884
|
const coordination = runtimeContext.profile === 'main' ? await agentCoordinationContext(toolCallContext) : undefined;
|
|
804
885
|
const pendingDelegations = coordination?.unresolved.filter((record) => record.parentMessageId === currentUserMessageId) ?? [];
|
|
805
|
-
if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
886
|
+
if (!liveStarted && pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
806
887
|
delegationCorrectionAttempted = true;
|
|
807
888
|
coordinationText = coordination.text;
|
|
808
889
|
yield* retireDraft('formal_delivery');
|
|
@@ -813,13 +894,18 @@ export async function* runQuery(userInput, options) {
|
|
|
813
894
|
...(await takeAgentNotifications(toolCallContext)),
|
|
814
895
|
];
|
|
815
896
|
if (pendingMessages.length) {
|
|
897
|
+
// A private draft is simply superseded; a live one was already read,
|
|
898
|
+
// so it stays in the conversation as this step's progress.
|
|
899
|
+
if (liveStarted)
|
|
900
|
+
yield* commitLiveAnswer('progress');
|
|
816
901
|
history.push(...pendingMessages);
|
|
817
902
|
continue;
|
|
818
903
|
}
|
|
819
904
|
// A model occasionally writes a path/link in prose and stops without formally
|
|
820
905
|
// presenting it. Give it one model-only corrective pass; the reminder is
|
|
821
906
|
// transient and therefore never pollutes persisted conversation history.
|
|
822
|
-
if (!
|
|
907
|
+
if (!liveStarted &&
|
|
908
|
+
!emptyResponse &&
|
|
823
909
|
formalDeliveryRequired &&
|
|
824
910
|
!resultPresented &&
|
|
825
911
|
!deliveryCorrectionAttempted &&
|
|
@@ -828,37 +914,21 @@ export async function* runQuery(userInput, options) {
|
|
|
828
914
|
yield* retireDraft('formal_delivery');
|
|
829
915
|
continue;
|
|
830
916
|
}
|
|
831
|
-
|
|
832
|
-
? { allow: true }
|
|
833
|
-
: advisorCoordinator.beforeDelivery(assistantText);
|
|
834
|
-
const fallbackReview = !emptyResponse && options.advisorCheckpoints !== false
|
|
835
|
-
? advisorCoordinator.pendingReview(assistantText)
|
|
836
|
-
: undefined;
|
|
837
|
-
if (fallbackReview && turn < maxTurns) {
|
|
838
|
-
yield* retireDraft('advisor_checkpoint');
|
|
839
|
-
// Reuse the effective tool boundary, permissions, cancellation, events and
|
|
840
|
-
// persistence. Never call a provider behind the host's tool pipeline.
|
|
841
|
-
toolCallsByIndex.push({
|
|
842
|
-
id: crypto.randomUUID(),
|
|
843
|
-
type: 'function',
|
|
844
|
-
function: {
|
|
845
|
-
name: 'Advisor',
|
|
846
|
-
arguments: JSON.stringify(fallbackReview),
|
|
847
|
-
},
|
|
848
|
-
});
|
|
849
|
-
assistantMsg.content = null;
|
|
850
|
-
assistantMsg.tool_calls = toolCallsByIndex;
|
|
851
|
-
break finalResponse;
|
|
852
|
-
}
|
|
853
|
-
if (!deliveryCheckpoint.allow && turn < maxTurns) {
|
|
854
|
-
yield* retireDraft('advisor_checkpoint');
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
917
|
+
let advisorFollowUp = false;
|
|
857
918
|
if (!emptyResponse) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
919
|
+
// A reminder, never a gate: the answer is already on its way to the user.
|
|
920
|
+
// With enforcement 'followup' (weak models that never ask on their own) an
|
|
921
|
+
// unreviewed substantive answer earns one more request in which the model is
|
|
922
|
+
// asked to call Advisor itself; the warning is then left for that pass.
|
|
923
|
+
if (turn < maxTurns && advisorNudge.followUpDue(assistantText) && (await advisorFollowUpEnabled(options))) {
|
|
924
|
+
advisorNudge.markFollowedUp();
|
|
925
|
+
advisorFollowUp = true;
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
const unreviewed = advisorNudge.unreviewedWarning(assistantText);
|
|
929
|
+
if (unreviewed)
|
|
930
|
+
yield { type: 'warning', message: unreviewed };
|
|
931
|
+
}
|
|
862
932
|
}
|
|
863
933
|
if (emptyResponse) {
|
|
864
934
|
// Recover once within the existing turn budget, retaining tool receipts and
|
|
@@ -886,86 +956,26 @@ export async function* runQuery(userInput, options) {
|
|
|
886
956
|
yield* retireDraft('empty_response');
|
|
887
957
|
}
|
|
888
958
|
else {
|
|
889
|
-
// Publication is a
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (!options.responseFormat && runtimeContext.profile === 'main' && output.finalDelivery === 'stream')
|
|
895
|
-
|
|
896
|
-
provider,
|
|
897
|
-
messages: buildModelMessages(history, requestTransientContext, userMessage.id),
|
|
898
|
-
candidate: assistantText,
|
|
899
|
-
candidateFinishReason: lastFinishReason,
|
|
900
|
-
output,
|
|
901
|
-
chatFn,
|
|
902
|
-
options,
|
|
903
|
-
signal,
|
|
904
|
-
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
905
|
-
turnUsage,
|
|
906
|
-
});
|
|
907
|
-
const publishedText = publication.text;
|
|
908
|
-
({ publicationError, publicationInterrupted, streamed, lastFinishReason } = publication);
|
|
909
|
-
assistantText = publishedText;
|
|
910
|
-
assistantMsg.content = publishedText || null;
|
|
911
|
-
if (!publishedText) {
|
|
912
|
-
if (publicationInterrupted)
|
|
913
|
-
yield {
|
|
914
|
-
...interruption(),
|
|
915
|
-
usage: {
|
|
916
|
-
...turnUsage,
|
|
917
|
-
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
918
|
-
},
|
|
919
|
-
};
|
|
920
|
-
else
|
|
921
|
-
yield {
|
|
922
|
-
type: 'error',
|
|
923
|
-
code: 'llm_error',
|
|
924
|
-
error: `最终答案生成失败:${String(publicationError)}`,
|
|
925
|
-
usage: {
|
|
926
|
-
...turnUsage,
|
|
927
|
-
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
928
|
-
},
|
|
929
|
-
};
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
959
|
+
// Publication is a one-way boundary: the text that passed every gate is
|
|
960
|
+
// exactly what the user sees and what history keeps — streamed live
|
|
961
|
+
// when nothing could hold it back, otherwise published verbatim now.
|
|
962
|
+
// It used to be regenerated by a second tool-free request: a third
|
|
963
|
+
// text no gate had seen, at the cost of a full-context call.
|
|
964
|
+
if (!liveStarted && !options.responseFormat && runtimeContext.profile === 'main' && output.finalDelivery === 'stream')
|
|
965
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
933
966
|
assistantMsg.presentation = 'final';
|
|
934
967
|
trace('final_published');
|
|
935
|
-
if (assistantMsg.role === 'assistant')
|
|
936
|
-
assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
|
|
937
|
-
assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
|
|
938
968
|
history.push(assistantMsg);
|
|
939
|
-
|
|
940
|
-
// already received real deltas; commit the identical text only once.
|
|
941
|
-
if (!streamed)
|
|
969
|
+
if (!liveStarted)
|
|
942
970
|
yield { type: 'text', delta: assistantText };
|
|
943
971
|
yield {
|
|
944
972
|
type: 'assistant_message',
|
|
945
973
|
message: assistantMsg,
|
|
946
974
|
};
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
...interruption(),
|
|
952
|
-
usage: {
|
|
953
|
-
...turnUsage,
|
|
954
|
-
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
955
|
-
},
|
|
956
|
-
};
|
|
957
|
-
else
|
|
958
|
-
yield {
|
|
959
|
-
type: 'error',
|
|
960
|
-
code: 'llm_error',
|
|
961
|
-
error: `最终答案流中断:${String(publicationError)}`,
|
|
962
|
-
usage: {
|
|
963
|
-
...turnUsage,
|
|
964
|
-
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
965
|
-
},
|
|
966
|
-
};
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
975
|
+
}
|
|
976
|
+
if (advisorFollowUp) {
|
|
977
|
+
pushAdvisorFollowUpMarker(history);
|
|
978
|
+
continue;
|
|
969
979
|
}
|
|
970
980
|
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
971
981
|
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
@@ -1077,7 +1087,7 @@ export async function* runQuery(userInput, options) {
|
|
|
1077
1087
|
runtimeContext,
|
|
1078
1088
|
toolCallContext,
|
|
1079
1089
|
currentEffectiveToolSet,
|
|
1080
|
-
|
|
1090
|
+
advisorNudge,
|
|
1081
1091
|
stallGuard,
|
|
1082
1092
|
modelEnvironmentCwd,
|
|
1083
1093
|
observedRuleScopes,
|
|
@@ -1090,9 +1100,7 @@ export async function* runQuery(userInput, options) {
|
|
|
1090
1100
|
// latest evidence and versions. Parallel batches commit in call order once
|
|
1091
1101
|
// settled. Both paths use this single writer, avoiding duplicate observation.
|
|
1092
1102
|
const commitOutcome = async function* ({ tc, result }) {
|
|
1093
|
-
|
|
1094
|
-
advisorCoordinator.observeToolResult(tc, result, advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name), { treatVerificationAsReadOnly: true }));
|
|
1095
|
-
}
|
|
1103
|
+
advisorNudge.observeToolResult(tc, result, advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name), { treatVerificationAsReadOnly: true }));
|
|
1096
1104
|
if (result.ok || result.disposition !== 'deferred') {
|
|
1097
1105
|
evidenceLedger.record(tc.function.name, tc.function.arguments, result);
|
|
1098
1106
|
}
|
|
@@ -1108,6 +1116,7 @@ export async function* runQuery(userInput, options) {
|
|
|
1108
1116
|
const manifest = result.metadata?.kind === 'result_manifest' ? result.metadata.manifest : browserArtifact;
|
|
1109
1117
|
presentedResultUnverified ||= manifest?.files.some((file) => file.verification === 'failed') ?? false;
|
|
1110
1118
|
}
|
|
1119
|
+
const actionHint = advisorNudge.takeActionHint();
|
|
1111
1120
|
const rawContent = result.ok ? result.content : `Error: ${result.error}`;
|
|
1112
1121
|
const basePolicy = result.policy ?? getToolResultPolicy(tc.function.name);
|
|
1113
1122
|
// 超大结果先归档再压缩:归档失败时保留原文,绝不因为磁盘问题破坏上下文。
|
|
@@ -1129,9 +1138,10 @@ export async function* runQuery(userInput, options) {
|
|
|
1129
1138
|
? { ...basePolicy, archiveRef: archived.archiveRef, originalChars: archived.originalChars }
|
|
1130
1139
|
: basePolicy;
|
|
1131
1140
|
// 微压缩仅对成功结果走;policy 会保护高价值结果,避免不可逆中段丢失。
|
|
1132
|
-
const
|
|
1141
|
+
const compacted = result.ok
|
|
1133
1142
|
? microCompact(tc.function.name, archived.content, sessionState?.microCompact, policy)
|
|
1134
1143
|
: rawContent;
|
|
1144
|
+
const content = actionHint ? `${compacted}\n\n${actionHint}` : compacted;
|
|
1135
1145
|
history.push({
|
|
1136
1146
|
role: 'tool',
|
|
1137
1147
|
display_ok: result.ok,
|
|
@@ -1139,8 +1149,6 @@ export async function* runQuery(userInput, options) {
|
|
|
1139
1149
|
tool_call_id: tc.id,
|
|
1140
1150
|
tool_result_policy: policy,
|
|
1141
1151
|
...(!result.ok && result.disposition === 'deferred' ? { toolResultDisposition: 'deferred' } : {}),
|
|
1142
|
-
...(tc === toolCallsByIndex.at(-1) ? { advisorCheckpoints: advisorCoordinator.drainDegradations() } : {}),
|
|
1143
|
-
advisorMutatesWorkspace: advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find((tool) => tool.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace,
|
|
1144
1152
|
...(result.metadata ? { display_metadata: result.metadata } : {}),
|
|
1145
1153
|
timestamp: Date.now(),
|
|
1146
1154
|
id: crypto.randomUUID(),
|
|
@@ -1182,7 +1190,7 @@ export async function* runQuery(userInput, options) {
|
|
|
1182
1190
|
? '同一问题两次尝试失败,请停止原样重试;顾问可用时,继续解决或交付前先咨询,由主 Agent 自行核验和决策。只读诊断仍可继续。'
|
|
1183
1191
|
: '检测到连续工具轮次没有产生新文本或新结果;请切换路径或咨询 Advisor。';
|
|
1184
1192
|
if (stall.reason !== 'repeated_failure')
|
|
1185
|
-
|
|
1193
|
+
advisorNudge.notifyStalled(stall.signature ?? stall.reason ?? 'no_progress');
|
|
1186
1194
|
yield { type: 'warning', message };
|
|
1187
1195
|
}
|
|
1188
1196
|
// 关键节点截图回填:把工具附带的实况截图作为一条带图 user 消息喂给(视觉)模型自我核验,
|