minimal-agent 0.18.5 → 0.18.7
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/contextBundle.js +23 -3
- package/src/context/contextBundle.ts +28 -7
- 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 +203 -30
- package/src/loop.ts +183 -30
- package/src/memory/experienceStore.js +1 -0
- package/src/memory/experienceStore.ts +1 -0
- package/src/memory/memoryService.js +3 -2
- package/src/memory/memoryService.ts +3 -0
- package/src/memory/projectKnowledge.js +1 -0
- package/src/memory/projectKnowledge.ts +1 -0
- package/src/memory/retrieval.js +4 -4
- package/src/memory/retrieval.ts +4 -4
- package/src/memory/types.ts +1 -0
- package/src/prompts/policyRegistry.js +31 -17
- package/src/prompts/policyRegistry.ts +31 -16
- package/src/prompts/promptAssembly.js +3 -9
- package/src/prompts/promptAssembly.ts +3 -9
- 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/finalAnswer.js +7 -0
- package/src/runtime/finalAnswer.ts +9 -0
- package/src/runtime/lifecycleTrace.js +16 -0
- package/src/runtime/lifecycleTrace.ts +22 -0
- package/src/session/runTurn.js +15 -1
- package/src/session/runTurn.ts +13 -1
- 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 +7 -1
- package/src/tools/agent/agent.ts +7 -1
- 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/types.ts +15 -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/loop.js
CHANGED
|
@@ -27,6 +27,9 @@
|
|
|
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 { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
30
33
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
31
34
|
import { dirname, resolve } from 'node:path';
|
|
32
35
|
import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
|
|
@@ -59,6 +62,7 @@ import { validateFinalClaims } from './harness/claimValidation.js';
|
|
|
59
62
|
import { AdvisorCoordinator, } from './advisor/advisorCoordinator.js';
|
|
60
63
|
import { toolEffectFor } from './tools/capabilities.js';
|
|
61
64
|
import { isReadOnlyShellCommand, isVerificationShellCommand } from './tools/bash/semantics.js';
|
|
65
|
+
import { collectReviewContext } from './advisor/reviewContext.js';
|
|
62
66
|
import { createAgentRuntimeContext, } from './runtime/agentRuntimeContext.js';
|
|
63
67
|
import { stripThinkBlocks } from './utils/stripThink.js';
|
|
64
68
|
import { orderedPool } from './runtime/orderedPool.js';
|
|
@@ -203,10 +207,12 @@ export async function* runQuery(userInput, options) {
|
|
|
203
207
|
delete sessionState.computerAutomation;
|
|
204
208
|
// DI fallback:测试可传 stub,生产走真实模块单例
|
|
205
209
|
const chatFn = options.chat ?? defaultChat;
|
|
210
|
+
const trace = createLifecycleTrace('loop');
|
|
206
211
|
const executeToolFn = options.executeTool ?? defaultExecuteTool;
|
|
207
212
|
const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
|
|
208
213
|
const currentUserMessageId = options.userMessageId ?? crypto.randomUUID();
|
|
209
214
|
let currentTransientBundle = options.transientBundle;
|
|
215
|
+
let advisorContextBundle = currentTransientBundle;
|
|
210
216
|
let modelEnvironmentCwd = runtimeContext.cwd;
|
|
211
217
|
const observedRuleScopes = new Map();
|
|
212
218
|
const pendingRuleScopes = new Set();
|
|
@@ -225,6 +231,7 @@ export async function* runQuery(userInput, options) {
|
|
|
225
231
|
const selected = availableForBundle >= bundle.diagnostics.usedTokens
|
|
226
232
|
? bundle
|
|
227
233
|
: resizeContextBundle(bundle, availableForBundle);
|
|
234
|
+
advisorContextBundle = selected;
|
|
228
235
|
return [selected.text, opaque].filter(Boolean).join('\n\n');
|
|
229
236
|
};
|
|
230
237
|
let resultPresented = false;
|
|
@@ -242,6 +249,16 @@ export async function* runQuery(userInput, options) {
|
|
|
242
249
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
243
250
|
history,
|
|
244
251
|
sessionState,
|
|
252
|
+
advisorReviewContext: () => ({
|
|
253
|
+
...collectReviewContext(advisorContextBundle),
|
|
254
|
+
constraints: [...collectReviewContext(advisorContextBundle).constraints,
|
|
255
|
+
{ id: 'execution.boundary', authority: 'host', content: JSON.stringify({ cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
256
|
+
conversationMode: toolCallContext.conversationMode, profile: runtimeContext.profile,
|
|
257
|
+
availableTools: [...currentEffectiveToolSet.names], invocation: invocationInstructions(options.invocationPolicy) }) }],
|
|
258
|
+
omittedIds: [...new Set([...(currentTransientBundle?.diagnostics.omittedIds ?? []), ...(advisorContextBundle?.diagnostics.omittedIds ?? [])]),
|
|
259
|
+
...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : [])],
|
|
260
|
+
clippedIds: [...new Set([...(currentTransientBundle?.diagnostics.clippedIds ?? []), ...(advisorContextBundle?.diagnostics.clippedIds ?? [])])],
|
|
261
|
+
}),
|
|
245
262
|
// Statistics only; consultations have no per-turn quota.
|
|
246
263
|
advisorCalls: { used: 0 },
|
|
247
264
|
advisorInvocation: 'agent',
|
|
@@ -684,7 +701,18 @@ export async function* runQuery(userInput, options) {
|
|
|
684
701
|
return;
|
|
685
702
|
}
|
|
686
703
|
// 3. 调 LLM 并组装 assistant 消息
|
|
704
|
+
trace('model_start');
|
|
687
705
|
let assistantText = '';
|
|
706
|
+
// Candidate text is private until the delivery gates allow publication.
|
|
707
|
+
// Keep the compatibility event as a phase notice, never as a text retraction.
|
|
708
|
+
function* retireDraft(reason) {
|
|
709
|
+
const message = { role: 'system', content: answerPreparationNotice(reason), id: crypto.randomUUID(),
|
|
710
|
+
timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
711
|
+
history.push(message);
|
|
712
|
+
yield { type: 'injected_message', message };
|
|
713
|
+
if (reason !== 'interrupted')
|
|
714
|
+
yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
|
|
715
|
+
}
|
|
688
716
|
/** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
|
|
689
717
|
const toolCallsByIndex = [];
|
|
690
718
|
/**
|
|
@@ -694,8 +722,6 @@ export async function* runQuery(userInput, options) {
|
|
|
694
722
|
let reasoningContent = '';
|
|
695
723
|
let reasoningString = '';
|
|
696
724
|
const reasoningDetails = [];
|
|
697
|
-
/** 首个 text_delta 时切到 streaming,再来不重复 yield */
|
|
698
|
-
let stageStreamingYielded = false;
|
|
699
725
|
/**
|
|
700
726
|
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
701
727
|
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
@@ -725,12 +751,7 @@ export async function* runQuery(userInput, options) {
|
|
|
725
751
|
: {}),
|
|
726
752
|
})) {
|
|
727
753
|
if (ev.type === 'text_delta') {
|
|
728
|
-
if (!stageStreamingYielded) {
|
|
729
|
-
stageStreamingYielded = true;
|
|
730
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
731
|
-
}
|
|
732
754
|
assistantText += ev.delta;
|
|
733
|
-
yield { type: 'text', delta: ev.delta };
|
|
734
755
|
}
|
|
735
756
|
else if (ev.type === 'tool_call_delta') {
|
|
736
757
|
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
@@ -783,7 +804,9 @@ export async function* runQuery(userInput, options) {
|
|
|
783
804
|
}
|
|
784
805
|
}
|
|
785
806
|
catch (e) {
|
|
807
|
+
trace('model_error');
|
|
786
808
|
if (e.name === 'AbortError') {
|
|
809
|
+
yield* retireDraft('interrupted');
|
|
787
810
|
yield interruption();
|
|
788
811
|
return;
|
|
789
812
|
}
|
|
@@ -799,6 +822,7 @@ export async function* runQuery(userInput, options) {
|
|
|
799
822
|
result = await reactiveCompactIfApplicable(history, provider, e, sessionState?.reactive, signal, { chat: chatFn }, options.promptCache);
|
|
800
823
|
}
|
|
801
824
|
catch (error) {
|
|
825
|
+
yield* retireDraft('interrupted');
|
|
802
826
|
if (signal?.aborted || error.name === 'AbortError') {
|
|
803
827
|
yield interruption();
|
|
804
828
|
return;
|
|
@@ -829,6 +853,7 @@ export async function* runQuery(userInput, options) {
|
|
|
829
853
|
// reactive 没压动或未恢复 → 退回正常错误路径
|
|
830
854
|
}
|
|
831
855
|
// code='llm_error':终结性错误,stop_reason='error'
|
|
856
|
+
yield* retireDraft('interrupted');
|
|
832
857
|
yield {
|
|
833
858
|
type: 'error',
|
|
834
859
|
error: `LLM 调用失败:${e.message}`,
|
|
@@ -836,7 +861,9 @@ export async function* runQuery(userInput, options) {
|
|
|
836
861
|
};
|
|
837
862
|
return;
|
|
838
863
|
}
|
|
864
|
+
trace('model_end');
|
|
839
865
|
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
866
|
+
yield* retireDraft('interrupted');
|
|
840
867
|
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
841
868
|
pushQueuedInputMarker(history);
|
|
842
869
|
yield { type: 'interrupted', reason: 'queued_input' };
|
|
@@ -856,6 +883,19 @@ export async function* runQuery(userInput, options) {
|
|
|
856
883
|
toolCallsByIndex.push(...dense);
|
|
857
884
|
}
|
|
858
885
|
}
|
|
886
|
+
// A pending review must run before another solving/publishing attempt.
|
|
887
|
+
// Read-only diagnosis and human questions remain available while stalled.
|
|
888
|
+
const pendingReview = options.advisorCheckpoints === false ? undefined : advisorCoordinator.pendingReview();
|
|
889
|
+
if (pendingReview && turn < maxTurns && toolCallsByIndex.length > 0
|
|
890
|
+
&& !toolCallsByIndex.some(tc => ['Advisor', 'AskUserQuestion'].includes(tc.function.name))
|
|
891
|
+
&& toolCallsByIndex.some(tc => tc.function.name === 'PresentResult'
|
|
892
|
+
|| advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find(t => t.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace
|
|
893
|
+
|| stallGuard.shouldReject(tc))) {
|
|
894
|
+
yield* retireDraft('advisor_checkpoint');
|
|
895
|
+
toolCallsByIndex.splice(0, toolCallsByIndex.length, { id: crypto.randomUUID(), type: 'function',
|
|
896
|
+
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
897
|
+
assistantText = '';
|
|
898
|
+
}
|
|
859
899
|
const assistantMsg = {
|
|
860
900
|
role: 'assistant',
|
|
861
901
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -868,12 +908,12 @@ export async function* runQuery(userInput, options) {
|
|
|
868
908
|
...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
|
|
869
909
|
};
|
|
870
910
|
// 4. 没有工具调用 → 整轮交互结束
|
|
871
|
-
if (toolCallsByIndex.length === 0) {
|
|
911
|
+
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
872
912
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
873
913
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
874
914
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
875
915
|
askCorrectionAttempted = true;
|
|
876
|
-
yield
|
|
916
|
+
yield* retireDraft('interactive_question');
|
|
877
917
|
continue;
|
|
878
918
|
}
|
|
879
919
|
const emptyResponse = stripThinkBlocks(assistantText).length === 0;
|
|
@@ -882,13 +922,11 @@ export async function* runQuery(userInput, options) {
|
|
|
882
922
|
if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
883
923
|
delegationCorrectionAttempted = true;
|
|
884
924
|
coordinationText = coordination.text;
|
|
885
|
-
yield
|
|
925
|
+
yield* retireDraft('formal_delivery');
|
|
886
926
|
continue;
|
|
887
927
|
}
|
|
888
928
|
const pendingMessages = [...(options.takePendingMessages?.() ?? []), ...await takeAgentNotifications(toolCallContext)];
|
|
889
929
|
if (pendingMessages.length) {
|
|
890
|
-
if (!emptyResponse)
|
|
891
|
-
history.push(assistantMsg);
|
|
892
930
|
history.push(...pendingMessages);
|
|
893
931
|
continue;
|
|
894
932
|
}
|
|
@@ -900,22 +938,27 @@ export async function* runQuery(userInput, options) {
|
|
|
900
938
|
&& !deliveryCorrectionAttempted
|
|
901
939
|
&& turn < maxTurns) {
|
|
902
940
|
deliveryCorrectionAttempted = true;
|
|
903
|
-
yield
|
|
904
|
-
type: 'assistant_draft_discarded',
|
|
905
|
-
reason: 'formal_delivery',
|
|
906
|
-
textLength: assistantText.length,
|
|
907
|
-
};
|
|
941
|
+
yield* retireDraft('formal_delivery');
|
|
908
942
|
continue;
|
|
909
943
|
}
|
|
910
|
-
const deliveryCheckpoint = emptyResponse
|
|
944
|
+
const deliveryCheckpoint = emptyResponse || options.advisorCheckpoints === false
|
|
911
945
|
? { allow: true }
|
|
912
946
|
: advisorCoordinator.beforeDelivery(assistantText);
|
|
947
|
+
const fallbackReview = !emptyResponse && options.advisorCheckpoints !== false
|
|
948
|
+
? advisorCoordinator.pendingReview(assistantText) : undefined;
|
|
949
|
+
if (fallbackReview && turn < maxTurns) {
|
|
950
|
+
yield* retireDraft('advisor_checkpoint');
|
|
951
|
+
// Reuse the effective tool boundary, permissions, cancellation, events and
|
|
952
|
+
// persistence. Never call a provider behind the host's tool pipeline.
|
|
953
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
954
|
+
name: 'Advisor', arguments: JSON.stringify(fallbackReview),
|
|
955
|
+
} });
|
|
956
|
+
assistantMsg.content = null;
|
|
957
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
958
|
+
break finalResponse;
|
|
959
|
+
}
|
|
913
960
|
if (!deliveryCheckpoint.allow && turn < maxTurns) {
|
|
914
|
-
yield
|
|
915
|
-
type: 'assistant_draft_discarded',
|
|
916
|
-
reason: 'advisor_checkpoint',
|
|
917
|
-
textLength: assistantText.length,
|
|
918
|
-
};
|
|
961
|
+
yield* retireDraft('advisor_checkpoint');
|
|
919
962
|
continue;
|
|
920
963
|
}
|
|
921
964
|
if (!emptyResponse) {
|
|
@@ -927,21 +970,127 @@ export async function* runQuery(userInput, options) {
|
|
|
927
970
|
if (emptyResponse) {
|
|
928
971
|
// A no-tool response is already the model's terminal signal. Do not turn it
|
|
929
972
|
// 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
|
-
};
|
|
973
|
+
yield* retireDraft('empty_response');
|
|
935
974
|
}
|
|
936
975
|
else {
|
|
976
|
+
// Publication is a separate tool-free request. Nothing from preparation
|
|
977
|
+
// has been streamed, and no gate may retract text after this boundary.
|
|
978
|
+
let publicationError;
|
|
979
|
+
let publicationInterrupted = false;
|
|
980
|
+
let streamed = false;
|
|
981
|
+
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
982
|
+
&& (runtimeContext.surface === 'desktop' || runtimeContext.surface === 'tui')) {
|
|
983
|
+
const candidate = assistantText;
|
|
984
|
+
const candidateFinishReason = lastFinishReason;
|
|
985
|
+
let publishedText = '';
|
|
986
|
+
let publicationDone = false;
|
|
987
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
988
|
+
try {
|
|
989
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText);
|
|
990
|
+
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
991
|
+
if (publicationPlan.overBudget)
|
|
992
|
+
throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
993
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
994
|
+
publicationInterrupted = true;
|
|
995
|
+
throw new Error('最终发布已取消。');
|
|
996
|
+
}
|
|
997
|
+
turnUsage.totalCalls += 1;
|
|
998
|
+
for await (const event of chatFn({
|
|
999
|
+
provider,
|
|
1000
|
+
messages: publicationMessages,
|
|
1001
|
+
tools: [], signal,
|
|
1002
|
+
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1003
|
+
promptCache: options.promptCache,
|
|
1004
|
+
})) {
|
|
1005
|
+
if (event.type === 'done' && !publicationDone) {
|
|
1006
|
+
publicationDone = true;
|
|
1007
|
+
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
1008
|
+
turnUsage.reportedCalls += 1;
|
|
1009
|
+
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
1010
|
+
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1014
|
+
publicationInterrupted = true;
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
if (event.type === 'text_delta') {
|
|
1018
|
+
// Never expose a rewrite of the reviewed candidate. Previously
|
|
1019
|
+
// published chunks are necessarily an exact prefix of it.
|
|
1020
|
+
if (!candidate.startsWith(publishedText + event.delta)) {
|
|
1021
|
+
throw new Error('发布内容偏离定稿,使用原定稿。');
|
|
1022
|
+
}
|
|
1023
|
+
publishedText += event.delta;
|
|
1024
|
+
streamed = true;
|
|
1025
|
+
yield { type: 'text', delta: event.delta };
|
|
1026
|
+
}
|
|
1027
|
+
else if (event.type === 'tool_call_delta') {
|
|
1028
|
+
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
1029
|
+
}
|
|
1030
|
+
else if (event.type === 'done') {
|
|
1031
|
+
publicationDone = true;
|
|
1032
|
+
lastFinishReason = event.stopReason;
|
|
1033
|
+
if (event.stopReason === 'aborted')
|
|
1034
|
+
publicationInterrupted = true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (!publicationDone && !publicationInterrupted)
|
|
1038
|
+
throw new Error('最终答案流未正常结束。');
|
|
1039
|
+
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length')
|
|
1040
|
+
throw new Error('最终答案流缺少有效的结束状态。');
|
|
1041
|
+
if (!publishedText.trim() && !publicationInterrupted)
|
|
1042
|
+
throw new Error('最终表达未返回正文。');
|
|
1043
|
+
if (!publicationInterrupted && publishedText !== candidate)
|
|
1044
|
+
throw new Error('最终表达未完整传递定稿。');
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || error?.name === 'AbortError';
|
|
1048
|
+
publicationError = error;
|
|
1049
|
+
}
|
|
1050
|
+
if (publicationError && !publicationInterrupted) {
|
|
1051
|
+
yield { type: 'warning', message: '流式发布未完成,已使用原定稿补齐;未重新生成或改写答案。' };
|
|
1052
|
+
const remaining = candidate.slice(publishedText.length);
|
|
1053
|
+
if (remaining) {
|
|
1054
|
+
yield { type: 'text', delta: remaining };
|
|
1055
|
+
streamed = true;
|
|
1056
|
+
}
|
|
1057
|
+
publishedText = candidate;
|
|
1058
|
+
publicationError = undefined;
|
|
1059
|
+
}
|
|
1060
|
+
assistantText = publishedText;
|
|
1061
|
+
if (!publicationInterrupted)
|
|
1062
|
+
lastFinishReason = candidateFinishReason;
|
|
1063
|
+
assistantMsg.content = publishedText || null;
|
|
1064
|
+
if (!publishedText) {
|
|
1065
|
+
if (publicationInterrupted)
|
|
1066
|
+
yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1067
|
+
else
|
|
1068
|
+
yield { type: 'error', code: 'llm_error', error: `最终答案生成失败:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
assistantMsg.presentation = 'final';
|
|
1073
|
+
trace('final_published');
|
|
937
1074
|
if (assistantMsg.role === 'assistant')
|
|
938
1075
|
assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
|
|
939
1076
|
assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
|
|
940
1077
|
history.push(assistantMsg);
|
|
1078
|
+
// Non-interactive consumers retain atomic output. Interactive hosts
|
|
1079
|
+
// already received real deltas; commit the identical text only once.
|
|
1080
|
+
if (!streamed)
|
|
1081
|
+
yield { type: 'text', delta: assistantText };
|
|
941
1082
|
yield {
|
|
942
1083
|
type: 'assistant_message',
|
|
943
1084
|
message: assistantMsg,
|
|
944
1085
|
};
|
|
1086
|
+
if (publicationInterrupted || publicationError) {
|
|
1087
|
+
yield { type: 'warning', message: '最终答案未完整生成,已保留已输出内容;未撤回或重新生成。' };
|
|
1088
|
+
if (publicationInterrupted)
|
|
1089
|
+
yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1090
|
+
else
|
|
1091
|
+
yield { type: 'error', code: 'llm_error', error: `最终答案流中断:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
945
1094
|
}
|
|
946
1095
|
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
947
1096
|
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
@@ -990,7 +1139,12 @@ export async function* runQuery(userInput, options) {
|
|
|
990
1139
|
...turnUsage,
|
|
991
1140
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
992
1141
|
};
|
|
993
|
-
if (
|
|
1142
|
+
if (emptyResponse && !resultPresented) {
|
|
1143
|
+
const warning = '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1144
|
+
yield { type: 'warning', message: warning };
|
|
1145
|
+
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1146
|
+
}
|
|
1147
|
+
else if (pendingDelegations.length) {
|
|
994
1148
|
const warning = `必要子任务尚未收口:${pendingDelegations.map((r) => r.id).join(', ')}。本轮不能视为全部完成。`;
|
|
995
1149
|
yield { type: 'warning', message: warning };
|
|
996
1150
|
yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning, usage };
|
|
@@ -1014,10 +1168,13 @@ export async function* runQuery(userInput, options) {
|
|
|
1014
1168
|
};
|
|
1015
1169
|
}
|
|
1016
1170
|
else {
|
|
1017
|
-
yield { type: 'turn_done', completionStatus: 'completed', usage
|
|
1171
|
+
yield { type: 'turn_done', completionStatus: 'completed', usage,
|
|
1172
|
+
...(emptyResponse ? { warning: '成果已提交,未生成文字总结;请查看已提交的成果。' } : {}) };
|
|
1018
1173
|
}
|
|
1019
1174
|
return;
|
|
1020
1175
|
}
|
|
1176
|
+
assistantMsg.presentation = 'progress';
|
|
1177
|
+
trace('progress_published');
|
|
1021
1178
|
history.push(assistantMsg);
|
|
1022
1179
|
yield {
|
|
1023
1180
|
type: 'assistant_message',
|
|
@@ -1231,7 +1388,22 @@ export async function* runQuery(userInput, options) {
|
|
|
1231
1388
|
}
|
|
1232
1389
|
};
|
|
1233
1390
|
const runOne = async (tc, reportProgress) => {
|
|
1391
|
+
// Also settle denied/invalid/unavailable calls that never reached the
|
|
1392
|
+
// provider adapter. This records a real attempt, not an ignored reminder.
|
|
1393
|
+
let consultation;
|
|
1394
|
+
if (tc.function.name === 'Advisor') {
|
|
1395
|
+
let reason;
|
|
1396
|
+
try {
|
|
1397
|
+
reason = JSON.parse(tc.function.arguments).reason;
|
|
1398
|
+
}
|
|
1399
|
+
catch { /* validation handles malformed input */ }
|
|
1400
|
+
consultation = advisorCoordinator.beginConsultation(reason);
|
|
1401
|
+
}
|
|
1234
1402
|
const outcome = await runOneImpl(tc, reportProgress);
|
|
1403
|
+
if (consultation && outcome.result.metadata?.kind !== 'advisor_consultation'
|
|
1404
|
+
&& !(!outcome.result.ok && outcome.result.error === 'aborted')) {
|
|
1405
|
+
outcome.result = { ...outcome.result, metadata: advisorCoordinator.finishConsultation(consultation, outcome.result.ok) };
|
|
1406
|
+
}
|
|
1235
1407
|
const metadata = outcome.result.metadata;
|
|
1236
1408
|
if (metadata?.kind === 'advisor_decision' && outcome.result.ok
|
|
1237
1409
|
|| metadata?.kind === 'advisor_consultation') {
|
|
@@ -1521,6 +1693,7 @@ export const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
|
|
|
1521
1693
|
* serialized request a strict prefix extension of the previous one.
|
|
1522
1694
|
*/
|
|
1523
1695
|
export function buildModelMessages(history, transientContext, currentUserMessageId) {
|
|
1696
|
+
history = history.filter(message => !(message.role === 'system' && message._ephemeral));
|
|
1524
1697
|
const transient = transientContext?.trim();
|
|
1525
1698
|
if (!transient)
|
|
1526
1699
|
return history;
|