minimal-agent 0.18.6 → 0.18.8
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 -1
- package/plugins/workflow-runner/src/runtime/context.js +5 -17
- package/plugins/workflow-runner/src/runtime/context.ts +4 -15
- package/plugins/workflow-runner/src/runtime/model.js +4 -2
- package/plugins/workflow-runner/src/runtime/model.ts +3 -2
- package/plugins/workflow-runner/test/api.test.ts +4 -2
- package/plugins/workflow-runner/test/embeddedExecution.test.ts +5 -6
- package/plugins/workflow-runner/test/modelOutcome.test.ts +36 -0
- package/skills/dynamic-ui/SKILL.md +38 -278
- package/skills/dynamic-ui/mermaid.md +32 -0
- package/skills/dynamic-ui/scenes/architecture-and-flow.md +4 -1
- package/skills/dynamic-ui/scenes/data-visualization.md +5 -2
- package/skills/dynamic-ui/svg-design.md +251 -0
- package/skills/dynamic-ui/templates/README.md +2 -1
- package/skills/dynamic-ui/tokens/visual-tokens.md +4 -5
- package/src/advisor/README.md +24 -0
- package/src/advisor/advisorCoordinator.js +101 -45
- package/src/advisor/advisorCoordinator.ts +90 -51
- package/src/advisor/deliveryReview.js +35 -0
- package/src/advisor/deliveryReview.ts +30 -0
- package/src/advisor/presentation.js +10 -0
- package/src/advisor/presentation.ts +12 -0
- package/src/advisor/reviewContext.js +60 -0
- package/src/advisor/reviewContext.ts +72 -0
- package/src/context/persistContext.js +5 -1
- package/src/context/persistContext.ts +5 -2
- package/src/context/tokenCounter.js +3 -0
- package/src/context/tokenCounter.ts +2 -0
- package/src/llm/messageProvenance.js +1 -1
- package/src/llm/messageProvenance.ts +1 -1
- package/src/loop.js +226 -31
- package/src/loop.ts +205 -31
- package/src/memory/fileStore.js +2 -34
- package/src/memory/fileStore.ts +2 -26
- package/src/prompts/policyRegistry.js +13 -9
- package/src/prompts/policyRegistry.ts +15 -9
- package/src/prompts/promptAssembly.js +16 -23
- package/src/prompts/promptAssembly.ts +22 -27
- package/src/prompts/visualPresentation.js +28 -0
- package/src/prompts/visualPresentation.ts +26 -0
- package/src/runtime/agentRuntimeContext.js +3 -0
- package/src/runtime/agentRuntimeContext.ts +4 -0
- package/src/runtime/answerPreparation.js +9 -0
- package/src/runtime/answerPreparation.ts +11 -0
- package/src/runtime/eventSemantics.js +2 -0
- package/src/runtime/eventSemantics.ts +2 -0
- package/src/runtime/fileLock.js +75 -0
- package/src/runtime/fileLock.ts +73 -0
- package/src/runtime/finalAnswer.js +9 -0
- package/src/runtime/finalAnswer.ts +13 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- package/src/runtime/outputContract.js +17 -0
- package/src/runtime/outputContract.ts +34 -0
- package/src/session/runAgentTurn.js +7 -2
- package/src/session/runAgentTurn.ts +7 -2
- package/src/session/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- package/src/session/runtimeStore.js +11 -28
- package/src/session/runtimeStore.ts +9 -23
- package/src/tools/advisor/advisor.js +56 -40
- package/src/tools/advisor/advisor.ts +57 -39
- package/src/tools/advisor/advisorDecision.js +6 -3
- package/src/tools/advisor/advisorDecision.ts +6 -3
- package/src/tools/agent/agent.js +14 -6
- package/src/tools/agent/agent.ts +14 -5
- package/src/tools/bash/semantics.js +91 -26
- package/src/tools/bash/semantics.ts +63 -33
- package/src/tools/index.js +4 -1
- package/src/tools/index.ts +4 -1
- package/src/tools/skill/searchSkills.js +2 -2
- package/src/tools/skill/searchSkills.ts +2 -2
- package/src/tools/skill/skill.js +4 -0
- package/src/tools/skill/skill.ts +3 -0
- package/src/types.ts +19 -6
- package/src/ui/LiveArea.js +2 -1
- package/src/ui/LiveArea.tsx +2 -1
- package/src/ui/MessageList.js +18 -5
- package/src/ui/MessageList.tsx +25 -3
- package/src/ui/draftNotice.js +1 -8
- package/src/ui/draftNotice.ts +1 -10
- package/src/ui/hooks/useChat.js +25 -1
- package/src/ui/hooks/useChat.ts +22 -1
- package/src/web/protocol.ts +3 -1
package/src/loop.js
CHANGED
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
* ============================================================
|
|
28
28
|
*/
|
|
29
29
|
import crypto from 'node:crypto';
|
|
30
|
+
import { answerPreparationNotice } from './runtime/answerPreparation.js';
|
|
31
|
+
import { finalAnswerMessages } from './runtime/finalAnswer.js';
|
|
32
|
+
import { resolveOutputContract, toolsForResponsePhase } from './runtime/outputContract.js';
|
|
33
|
+
import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
|
|
34
|
+
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
30
35
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
31
36
|
import { dirname, resolve } from 'node:path';
|
|
32
37
|
import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
|
|
@@ -59,6 +64,7 @@ import { validateFinalClaims } from './harness/claimValidation.js';
|
|
|
59
64
|
import { AdvisorCoordinator, } from './advisor/advisorCoordinator.js';
|
|
60
65
|
import { toolEffectFor } from './tools/capabilities.js';
|
|
61
66
|
import { isReadOnlyShellCommand, isVerificationShellCommand } from './tools/bash/semantics.js';
|
|
67
|
+
import { collectReviewContext } from './advisor/reviewContext.js';
|
|
62
68
|
import { createAgentRuntimeContext, } from './runtime/agentRuntimeContext.js';
|
|
63
69
|
import { stripThinkBlocks } from './utils/stripThink.js';
|
|
64
70
|
import { orderedPool } from './runtime/orderedPool.js';
|
|
@@ -203,10 +209,12 @@ export async function* runQuery(userInput, options) {
|
|
|
203
209
|
delete sessionState.computerAutomation;
|
|
204
210
|
// DI fallback:测试可传 stub,生产走真实模块单例
|
|
205
211
|
const chatFn = options.chat ?? defaultChat;
|
|
212
|
+
const trace = createLifecycleTrace('loop');
|
|
206
213
|
const executeToolFn = options.executeTool ?? defaultExecuteTool;
|
|
207
214
|
const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
|
|
208
215
|
const currentUserMessageId = options.userMessageId ?? crypto.randomUUID();
|
|
209
216
|
let currentTransientBundle = options.transientBundle;
|
|
217
|
+
let advisorContextBundle = currentTransientBundle;
|
|
210
218
|
let modelEnvironmentCwd = runtimeContext.cwd;
|
|
211
219
|
const observedRuleScopes = new Map();
|
|
212
220
|
const pendingRuleScopes = new Set();
|
|
@@ -225,12 +233,15 @@ export async function* runQuery(userInput, options) {
|
|
|
225
233
|
const selected = availableForBundle >= bundle.diagnostics.usedTokens
|
|
226
234
|
? bundle
|
|
227
235
|
: resizeContextBundle(bundle, availableForBundle);
|
|
236
|
+
advisorContextBundle = selected;
|
|
228
237
|
return [selected.text, opaque].filter(Boolean).join('\n\n');
|
|
229
238
|
};
|
|
230
239
|
let resultPresented = false;
|
|
231
240
|
let presentedResultUnverified = false;
|
|
232
241
|
let deliveryCorrectionAttempted = false;
|
|
233
242
|
let askCorrectionAttempted = false;
|
|
243
|
+
let diagramPreparationAttempted = false;
|
|
244
|
+
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
234
245
|
const subagents = await configService.subagents();
|
|
235
246
|
await hydrateSessionWorktree({
|
|
236
247
|
sessionState,
|
|
@@ -239,9 +250,21 @@ export async function* runQuery(userInput, options) {
|
|
|
239
250
|
cwd: runtimeContext.cwd,
|
|
240
251
|
});
|
|
241
252
|
const toolCallContext = {
|
|
253
|
+
output,
|
|
254
|
+
availableToolNames: () => currentEffectiveToolSet.names,
|
|
242
255
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
243
256
|
history,
|
|
244
257
|
sessionState,
|
|
258
|
+
advisorReviewContext: () => ({
|
|
259
|
+
...collectReviewContext(advisorContextBundle),
|
|
260
|
+
constraints: [...collectReviewContext(advisorContextBundle).constraints,
|
|
261
|
+
{ id: 'execution.boundary', authority: 'host', content: JSON.stringify({ cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
262
|
+
conversationMode: toolCallContext.conversationMode, profile: runtimeContext.profile,
|
|
263
|
+
availableTools: [...currentEffectiveToolSet.names], invocation: invocationInstructions(options.invocationPolicy) }) }],
|
|
264
|
+
omittedIds: [...new Set([...(currentTransientBundle?.diagnostics.omittedIds ?? []), ...(advisorContextBundle?.diagnostics.omittedIds ?? [])]),
|
|
265
|
+
...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : [])],
|
|
266
|
+
clippedIds: [...new Set([...(currentTransientBundle?.diagnostics.clippedIds ?? []), ...(advisorContextBundle?.diagnostics.clippedIds ?? [])])],
|
|
267
|
+
}),
|
|
245
268
|
// Statistics only; consultations have no per-turn quota.
|
|
246
269
|
advisorCalls: { used: 0 },
|
|
247
270
|
advisorInvocation: 'agent',
|
|
@@ -303,6 +326,7 @@ export async function* runQuery(userInput, options) {
|
|
|
303
326
|
const parts = [
|
|
304
327
|
transientFor(activeTools),
|
|
305
328
|
coordinationText,
|
|
329
|
+
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
306
330
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
307
331
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
308
332
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
@@ -684,7 +708,18 @@ export async function* runQuery(userInput, options) {
|
|
|
684
708
|
return;
|
|
685
709
|
}
|
|
686
710
|
// 3. 调 LLM 并组装 assistant 消息
|
|
711
|
+
trace('model_start');
|
|
687
712
|
let assistantText = '';
|
|
713
|
+
// Candidate text is private until the delivery gates allow publication.
|
|
714
|
+
// Keep the compatibility event as a phase notice, never as a text retraction.
|
|
715
|
+
function* retireDraft(reason) {
|
|
716
|
+
const message = { role: 'system', content: answerPreparationNotice(reason), id: crypto.randomUUID(),
|
|
717
|
+
timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
718
|
+
history.push(message);
|
|
719
|
+
yield { type: 'injected_message', message };
|
|
720
|
+
if (reason !== 'interrupted')
|
|
721
|
+
yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
|
|
722
|
+
}
|
|
688
723
|
/** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
|
|
689
724
|
const toolCallsByIndex = [];
|
|
690
725
|
/**
|
|
@@ -694,8 +729,6 @@ export async function* runQuery(userInput, options) {
|
|
|
694
729
|
let reasoningContent = '';
|
|
695
730
|
let reasoningString = '';
|
|
696
731
|
const reasoningDetails = [];
|
|
697
|
-
/** 首个 text_delta 时切到 streaming,再来不重复 yield */
|
|
698
|
-
let stageStreamingYielded = false;
|
|
699
732
|
/**
|
|
700
733
|
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
701
734
|
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
@@ -711,7 +744,7 @@ export async function* runQuery(userInput, options) {
|
|
|
711
744
|
for await (const ev of chatFn({
|
|
712
745
|
provider,
|
|
713
746
|
messages: modelMessages,
|
|
714
|
-
tools: currentEffectiveToolSet.tools,
|
|
747
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'preparation'),
|
|
715
748
|
signal,
|
|
716
749
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
717
750
|
promptCache: options.promptCache,
|
|
@@ -725,12 +758,7 @@ export async function* runQuery(userInput, options) {
|
|
|
725
758
|
: {}),
|
|
726
759
|
})) {
|
|
727
760
|
if (ev.type === 'text_delta') {
|
|
728
|
-
if (!stageStreamingYielded) {
|
|
729
|
-
stageStreamingYielded = true;
|
|
730
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
731
|
-
}
|
|
732
761
|
assistantText += ev.delta;
|
|
733
|
-
yield { type: 'text', delta: ev.delta };
|
|
734
762
|
}
|
|
735
763
|
else if (ev.type === 'tool_call_delta') {
|
|
736
764
|
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
@@ -783,7 +811,9 @@ export async function* runQuery(userInput, options) {
|
|
|
783
811
|
}
|
|
784
812
|
}
|
|
785
813
|
catch (e) {
|
|
814
|
+
trace('model_error');
|
|
786
815
|
if (e.name === 'AbortError') {
|
|
816
|
+
yield* retireDraft('interrupted');
|
|
787
817
|
yield interruption();
|
|
788
818
|
return;
|
|
789
819
|
}
|
|
@@ -799,6 +829,7 @@ export async function* runQuery(userInput, options) {
|
|
|
799
829
|
result = await reactiveCompactIfApplicable(history, provider, e, sessionState?.reactive, signal, { chat: chatFn }, options.promptCache);
|
|
800
830
|
}
|
|
801
831
|
catch (error) {
|
|
832
|
+
yield* retireDraft('interrupted');
|
|
802
833
|
if (signal?.aborted || error.name === 'AbortError') {
|
|
803
834
|
yield interruption();
|
|
804
835
|
return;
|
|
@@ -829,6 +860,7 @@ export async function* runQuery(userInput, options) {
|
|
|
829
860
|
// reactive 没压动或未恢复 → 退回正常错误路径
|
|
830
861
|
}
|
|
831
862
|
// code='llm_error':终结性错误,stop_reason='error'
|
|
863
|
+
yield* retireDraft('interrupted');
|
|
832
864
|
yield {
|
|
833
865
|
type: 'error',
|
|
834
866
|
error: `LLM 调用失败:${e.message}`,
|
|
@@ -836,7 +868,9 @@ export async function* runQuery(userInput, options) {
|
|
|
836
868
|
};
|
|
837
869
|
return;
|
|
838
870
|
}
|
|
871
|
+
trace('model_end');
|
|
839
872
|
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
873
|
+
yield* retireDraft('interrupted');
|
|
840
874
|
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
841
875
|
pushQueuedInputMarker(history);
|
|
842
876
|
yield { type: 'interrupted', reason: 'queued_input' };
|
|
@@ -856,6 +890,29 @@ export async function* runQuery(userInput, options) {
|
|
|
856
890
|
toolCallsByIndex.push(...dense);
|
|
857
891
|
}
|
|
858
892
|
}
|
|
893
|
+
// A pending review must run before another solving/publishing attempt.
|
|
894
|
+
// Read-only diagnosis and human questions remain available while stalled.
|
|
895
|
+
const pendingReview = options.advisorCheckpoints === false ? undefined : advisorCoordinator.pendingReview();
|
|
896
|
+
if (pendingReview && turn < maxTurns && toolCallsByIndex.length > 0
|
|
897
|
+
&& !toolCallsByIndex.some(tc => ['Advisor', 'AskUserQuestion'].includes(tc.function.name))
|
|
898
|
+
&& toolCallsByIndex.some(tc => tc.function.name === 'PresentResult'
|
|
899
|
+
|| advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find(t => t.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace
|
|
900
|
+
|| stallGuard.shouldReject(tc))) {
|
|
901
|
+
yield* retireDraft('advisor_checkpoint');
|
|
902
|
+
toolCallsByIndex.splice(0, toolCallsByIndex.length, { id: crypto.randomUUID(), type: 'function',
|
|
903
|
+
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
904
|
+
assistantText = '';
|
|
905
|
+
}
|
|
906
|
+
// Count the calls that survived review scheduling, including failed loads.
|
|
907
|
+
for (const call of toolCallsByIndex) {
|
|
908
|
+
if (call.function.name !== 'Skill')
|
|
909
|
+
continue;
|
|
910
|
+
try {
|
|
911
|
+
if (JSON.parse(call.function.arguments).name === 'dynamic-ui')
|
|
912
|
+
diagramPreparationAttempted = true;
|
|
913
|
+
}
|
|
914
|
+
catch { /* Ordinary tool validation reports malformed arguments. */ }
|
|
915
|
+
}
|
|
859
916
|
const assistantMsg = {
|
|
860
917
|
role: 'assistant',
|
|
861
918
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -868,12 +925,26 @@ export async function* runQuery(userInput, options) {
|
|
|
868
925
|
...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
|
|
869
926
|
};
|
|
870
927
|
// 4. 没有工具调用 → 整轮交互结束
|
|
871
|
-
if (toolCallsByIndex.length === 0) {
|
|
928
|
+
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
929
|
+
// Repair only private preparation, through the ordinary tool pipeline.
|
|
930
|
+
// One attempt per turn, including missing/failed skills; never rewind a reply.
|
|
931
|
+
if (!diagramPreparationAttempted && !options.responseFormat
|
|
932
|
+
&& runtimeContext.profile === 'main' && output.diagrams.length > 0
|
|
933
|
+
&& currentEffectiveToolSet.names.has('Skill') && turn < maxTurns
|
|
934
|
+
&& needsDiagramPreparation(assistantText, userInput)) {
|
|
935
|
+
diagramPreparationAttempted = true;
|
|
936
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
937
|
+
name: 'Skill', arguments: JSON.stringify({ name: 'dynamic-ui' }),
|
|
938
|
+
} });
|
|
939
|
+
assistantMsg.content = null;
|
|
940
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
941
|
+
break finalResponse;
|
|
942
|
+
}
|
|
872
943
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
873
944
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
874
945
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
875
946
|
askCorrectionAttempted = true;
|
|
876
|
-
yield
|
|
947
|
+
yield* retireDraft('interactive_question');
|
|
877
948
|
continue;
|
|
878
949
|
}
|
|
879
950
|
const emptyResponse = stripThinkBlocks(assistantText).length === 0;
|
|
@@ -882,13 +953,11 @@ export async function* runQuery(userInput, options) {
|
|
|
882
953
|
if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
883
954
|
delegationCorrectionAttempted = true;
|
|
884
955
|
coordinationText = coordination.text;
|
|
885
|
-
yield
|
|
956
|
+
yield* retireDraft('formal_delivery');
|
|
886
957
|
continue;
|
|
887
958
|
}
|
|
888
959
|
const pendingMessages = [...(options.takePendingMessages?.() ?? []), ...await takeAgentNotifications(toolCallContext)];
|
|
889
960
|
if (pendingMessages.length) {
|
|
890
|
-
if (!emptyResponse)
|
|
891
|
-
history.push(assistantMsg);
|
|
892
961
|
history.push(...pendingMessages);
|
|
893
962
|
continue;
|
|
894
963
|
}
|
|
@@ -900,22 +969,27 @@ export async function* runQuery(userInput, options) {
|
|
|
900
969
|
&& !deliveryCorrectionAttempted
|
|
901
970
|
&& turn < maxTurns) {
|
|
902
971
|
deliveryCorrectionAttempted = true;
|
|
903
|
-
yield
|
|
904
|
-
type: 'assistant_draft_discarded',
|
|
905
|
-
reason: 'formal_delivery',
|
|
906
|
-
textLength: assistantText.length,
|
|
907
|
-
};
|
|
972
|
+
yield* retireDraft('formal_delivery');
|
|
908
973
|
continue;
|
|
909
974
|
}
|
|
910
|
-
const deliveryCheckpoint = emptyResponse
|
|
975
|
+
const deliveryCheckpoint = emptyResponse || options.advisorCheckpoints === false
|
|
911
976
|
? { allow: true }
|
|
912
977
|
: advisorCoordinator.beforeDelivery(assistantText);
|
|
978
|
+
const fallbackReview = !emptyResponse && options.advisorCheckpoints !== false
|
|
979
|
+
? advisorCoordinator.pendingReview(assistantText) : undefined;
|
|
980
|
+
if (fallbackReview && turn < maxTurns) {
|
|
981
|
+
yield* retireDraft('advisor_checkpoint');
|
|
982
|
+
// Reuse the effective tool boundary, permissions, cancellation, events and
|
|
983
|
+
// persistence. Never call a provider behind the host's tool pipeline.
|
|
984
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
985
|
+
name: 'Advisor', arguments: JSON.stringify(fallbackReview),
|
|
986
|
+
} });
|
|
987
|
+
assistantMsg.content = null;
|
|
988
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
989
|
+
break finalResponse;
|
|
990
|
+
}
|
|
913
991
|
if (!deliveryCheckpoint.allow && turn < maxTurns) {
|
|
914
|
-
yield
|
|
915
|
-
type: 'assistant_draft_discarded',
|
|
916
|
-
reason: 'advisor_checkpoint',
|
|
917
|
-
textLength: assistantText.length,
|
|
918
|
-
};
|
|
992
|
+
yield* retireDraft('advisor_checkpoint');
|
|
919
993
|
continue;
|
|
920
994
|
}
|
|
921
995
|
if (!emptyResponse) {
|
|
@@ -927,21 +1001,118 @@ export async function* runQuery(userInput, options) {
|
|
|
927
1001
|
if (emptyResponse) {
|
|
928
1002
|
// A no-tool response is already the model's terminal signal. Do not turn it
|
|
929
1003
|
// into another model round, and do not persist/broadcast an empty bubble.
|
|
930
|
-
yield
|
|
931
|
-
type: 'assistant_draft_discarded',
|
|
932
|
-
reason: 'empty_response',
|
|
933
|
-
textLength: assistantText.length,
|
|
934
|
-
};
|
|
1004
|
+
yield* retireDraft('empty_response');
|
|
935
1005
|
}
|
|
936
1006
|
else {
|
|
1007
|
+
// Publication is a separate tool-free request. Nothing from preparation
|
|
1008
|
+
// has been streamed, and no gate may retract text after this boundary.
|
|
1009
|
+
let publicationError;
|
|
1010
|
+
let publicationInterrupted = false;
|
|
1011
|
+
let streamed = false;
|
|
1012
|
+
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
1013
|
+
&& output.finalDelivery === 'stream') {
|
|
1014
|
+
const candidate = assistantText;
|
|
1015
|
+
const candidateFinishReason = lastFinishReason;
|
|
1016
|
+
let publishedText = '';
|
|
1017
|
+
let publicationDone = false;
|
|
1018
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
1019
|
+
try {
|
|
1020
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText, output);
|
|
1021
|
+
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
1022
|
+
if (publicationPlan.overBudget)
|
|
1023
|
+
throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
1024
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1025
|
+
publicationInterrupted = true;
|
|
1026
|
+
throw new Error('最终发布已取消。');
|
|
1027
|
+
}
|
|
1028
|
+
turnUsage.totalCalls += 1;
|
|
1029
|
+
for await (const event of chatFn({
|
|
1030
|
+
provider,
|
|
1031
|
+
messages: publicationMessages,
|
|
1032
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'publication'), signal,
|
|
1033
|
+
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1034
|
+
promptCache: options.promptCache,
|
|
1035
|
+
})) {
|
|
1036
|
+
if (event.type === 'done' && !publicationDone) {
|
|
1037
|
+
publicationDone = true;
|
|
1038
|
+
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
1039
|
+
turnUsage.reportedCalls += 1;
|
|
1040
|
+
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
1041
|
+
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1045
|
+
publicationInterrupted = true;
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
if (event.type === 'text_delta') {
|
|
1049
|
+
// Final expression is live generation, not an exact-copy replay.
|
|
1050
|
+
// Once visible, text is never replaced or joined with a different draft.
|
|
1051
|
+
publishedText += event.delta;
|
|
1052
|
+
streamed = true;
|
|
1053
|
+
yield { type: 'text', delta: event.delta };
|
|
1054
|
+
}
|
|
1055
|
+
else if (event.type === 'tool_call_delta') {
|
|
1056
|
+
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
1057
|
+
}
|
|
1058
|
+
else if (event.type === 'done') {
|
|
1059
|
+
publicationDone = true;
|
|
1060
|
+
lastFinishReason = event.stopReason;
|
|
1061
|
+
if (event.stopReason === 'aborted')
|
|
1062
|
+
publicationInterrupted = true;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (!publicationDone && !publicationInterrupted)
|
|
1066
|
+
throw new Error('最终答案流未正常结束。');
|
|
1067
|
+
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length')
|
|
1068
|
+
throw new Error('最终答案流缺少有效的结束状态。');
|
|
1069
|
+
if (!publishedText.trim() && !publicationInterrupted)
|
|
1070
|
+
throw new Error('最终表达未返回正文。');
|
|
1071
|
+
}
|
|
1072
|
+
catch (error) {
|
|
1073
|
+
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || error?.name === 'AbortError';
|
|
1074
|
+
publicationError = error;
|
|
1075
|
+
}
|
|
1076
|
+
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
1077
|
+
yield { type: 'warning', message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。' };
|
|
1078
|
+
yield { type: 'text', delta: candidate };
|
|
1079
|
+
streamed = true;
|
|
1080
|
+
publishedText = candidate;
|
|
1081
|
+
lastFinishReason = candidateFinishReason;
|
|
1082
|
+
publicationError = undefined;
|
|
1083
|
+
}
|
|
1084
|
+
assistantText = publishedText;
|
|
1085
|
+
assistantMsg.content = publishedText || null;
|
|
1086
|
+
if (!publishedText) {
|
|
1087
|
+
if (publicationInterrupted)
|
|
1088
|
+
yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1089
|
+
else
|
|
1090
|
+
yield { type: 'error', code: 'llm_error', error: `最终答案生成失败:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
assistantMsg.presentation = 'final';
|
|
1095
|
+
trace('final_published');
|
|
937
1096
|
if (assistantMsg.role === 'assistant')
|
|
938
1097
|
assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
|
|
939
1098
|
assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
|
|
940
1099
|
history.push(assistantMsg);
|
|
1100
|
+
// Non-interactive consumers retain atomic output. Interactive hosts
|
|
1101
|
+
// already received real deltas; commit the identical text only once.
|
|
1102
|
+
if (!streamed)
|
|
1103
|
+
yield { type: 'text', delta: assistantText };
|
|
941
1104
|
yield {
|
|
942
1105
|
type: 'assistant_message',
|
|
943
1106
|
message: assistantMsg,
|
|
944
1107
|
};
|
|
1108
|
+
if (publicationInterrupted || publicationError) {
|
|
1109
|
+
yield { type: 'warning', message: '最终答案未完整生成,已保留已输出内容;未撤回或重新生成。' };
|
|
1110
|
+
if (publicationInterrupted)
|
|
1111
|
+
yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1112
|
+
else
|
|
1113
|
+
yield { type: 'error', code: 'llm_error', error: `最终答案流中断:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
945
1116
|
}
|
|
946
1117
|
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
947
1118
|
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
@@ -990,7 +1161,12 @@ export async function* runQuery(userInput, options) {
|
|
|
990
1161
|
...turnUsage,
|
|
991
1162
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
992
1163
|
};
|
|
993
|
-
if (
|
|
1164
|
+
if (emptyResponse && !resultPresented) {
|
|
1165
|
+
const warning = '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1166
|
+
yield { type: 'warning', message: warning };
|
|
1167
|
+
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1168
|
+
}
|
|
1169
|
+
else if (pendingDelegations.length) {
|
|
994
1170
|
const warning = `必要子任务尚未收口:${pendingDelegations.map((r) => r.id).join(', ')}。本轮不能视为全部完成。`;
|
|
995
1171
|
yield { type: 'warning', message: warning };
|
|
996
1172
|
yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning, usage };
|
|
@@ -1014,10 +1190,13 @@ export async function* runQuery(userInput, options) {
|
|
|
1014
1190
|
};
|
|
1015
1191
|
}
|
|
1016
1192
|
else {
|
|
1017
|
-
yield { type: 'turn_done', completionStatus: 'completed', usage
|
|
1193
|
+
yield { type: 'turn_done', completionStatus: 'completed', usage,
|
|
1194
|
+
...(emptyResponse ? { warning: '成果已提交,未生成文字总结;请查看已提交的成果。' } : {}) };
|
|
1018
1195
|
}
|
|
1019
1196
|
return;
|
|
1020
1197
|
}
|
|
1198
|
+
assistantMsg.presentation = 'progress';
|
|
1199
|
+
trace('progress_published');
|
|
1021
1200
|
history.push(assistantMsg);
|
|
1022
1201
|
yield {
|
|
1023
1202
|
type: 'assistant_message',
|
|
@@ -1231,7 +1410,22 @@ export async function* runQuery(userInput, options) {
|
|
|
1231
1410
|
}
|
|
1232
1411
|
};
|
|
1233
1412
|
const runOne = async (tc, reportProgress) => {
|
|
1413
|
+
// Also settle denied/invalid/unavailable calls that never reached the
|
|
1414
|
+
// provider adapter. This records a real attempt, not an ignored reminder.
|
|
1415
|
+
let consultation;
|
|
1416
|
+
if (tc.function.name === 'Advisor') {
|
|
1417
|
+
let reason;
|
|
1418
|
+
try {
|
|
1419
|
+
reason = JSON.parse(tc.function.arguments).reason;
|
|
1420
|
+
}
|
|
1421
|
+
catch { /* validation handles malformed input */ }
|
|
1422
|
+
consultation = advisorCoordinator.beginConsultation(reason);
|
|
1423
|
+
}
|
|
1234
1424
|
const outcome = await runOneImpl(tc, reportProgress);
|
|
1425
|
+
if (consultation && outcome.result.metadata?.kind !== 'advisor_consultation'
|
|
1426
|
+
&& !(!outcome.result.ok && outcome.result.error === 'aborted')) {
|
|
1427
|
+
outcome.result = { ...outcome.result, metadata: advisorCoordinator.finishConsultation(consultation, outcome.result.ok) };
|
|
1428
|
+
}
|
|
1235
1429
|
const metadata = outcome.result.metadata;
|
|
1236
1430
|
if (metadata?.kind === 'advisor_decision' && outcome.result.ok
|
|
1237
1431
|
|| metadata?.kind === 'advisor_consultation') {
|
|
@@ -1521,6 +1715,7 @@ export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
|
|
|
1521
1715
|
* serialized request a strict prefix extension of the previous one.
|
|
1522
1716
|
*/
|
|
1523
1717
|
export function buildModelMessages(history, transientContext, currentUserMessageId) {
|
|
1718
|
+
history = history.filter(message => !(message.role === 'system' && message._ephemeral));
|
|
1524
1719
|
const transient = transientContext?.trim();
|
|
1525
1720
|
if (!transient)
|
|
1526
1721
|
return history;
|