minimal-agent 0.18.6 → 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/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/prompts/policyRegistry.js +17 -9
- package/src/prompts/policyRegistry.ts +17 -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.ts
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import crypto from 'node:crypto';
|
|
31
|
+
import { answerPreparationNotice } from './runtime/answerPreparation.js';
|
|
32
|
+
import { finalAnswerMessages } from './runtime/finalAnswer.js';
|
|
33
|
+
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
31
34
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
32
35
|
import { dirname, resolve } from 'node:path';
|
|
33
36
|
import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
|
|
@@ -100,6 +103,7 @@ import {
|
|
|
100
103
|
} from './advisor/advisorCoordinator.js';
|
|
101
104
|
import { toolEffectFor } from './tools/capabilities.js';
|
|
102
105
|
import { isReadOnlyShellCommand, isVerificationShellCommand } from './tools/bash/semantics.js';
|
|
106
|
+
import { collectReviewContext } from './advisor/reviewContext.js';
|
|
103
107
|
import {
|
|
104
108
|
createAgentRuntimeContext,
|
|
105
109
|
type AgentRuntimeContext,
|
|
@@ -410,11 +414,13 @@ export async function* runQuery(
|
|
|
410
414
|
if (!computerIntent.needed && sessionState) delete sessionState.computerAutomation;
|
|
411
415
|
// DI fallback:测试可传 stub,生产走真实模块单例
|
|
412
416
|
const chatFn = options.chat ?? defaultChat;
|
|
417
|
+
const trace = createLifecycleTrace('loop');
|
|
413
418
|
const executeToolFn = options.executeTool ?? defaultExecuteTool;
|
|
414
419
|
const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
|
|
415
420
|
const currentUserMessageId = options.userMessageId ?? crypto.randomUUID();
|
|
416
421
|
|
|
417
422
|
let currentTransientBundle = options.transientBundle;
|
|
423
|
+
let advisorContextBundle = currentTransientBundle;
|
|
418
424
|
let modelEnvironmentCwd = runtimeContext.cwd;
|
|
419
425
|
const observedRuleScopes = new Map<string, string>();
|
|
420
426
|
const pendingRuleScopes = new Set<string>();
|
|
@@ -435,6 +441,7 @@ export async function* runQuery(
|
|
|
435
441
|
const selected = availableForBundle >= bundle.diagnostics.usedTokens
|
|
436
442
|
? bundle
|
|
437
443
|
: resizeContextBundle(bundle, availableForBundle);
|
|
444
|
+
advisorContextBundle = selected;
|
|
438
445
|
return [selected.text, opaque].filter(Boolean).join('\n\n');
|
|
439
446
|
};
|
|
440
447
|
let resultPresented = false;
|
|
@@ -452,6 +459,16 @@ export async function* runQuery(
|
|
|
452
459
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
453
460
|
history,
|
|
454
461
|
sessionState,
|
|
462
|
+
advisorReviewContext: () => ({
|
|
463
|
+
...collectReviewContext(advisorContextBundle),
|
|
464
|
+
constraints: [...collectReviewContext(advisorContextBundle).constraints,
|
|
465
|
+
{ id: 'execution.boundary', authority: 'host', content: JSON.stringify({ cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
466
|
+
conversationMode: toolCallContext.conversationMode, profile: runtimeContext.profile,
|
|
467
|
+
availableTools: [...currentEffectiveToolSet.names], invocation: invocationInstructions(options.invocationPolicy) }) }],
|
|
468
|
+
omittedIds: [...new Set([...(currentTransientBundle?.diagnostics.omittedIds ?? []), ...(advisorContextBundle?.diagnostics.omittedIds ?? [])]),
|
|
469
|
+
...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : [])],
|
|
470
|
+
clippedIds: [...new Set([...(currentTransientBundle?.diagnostics.clippedIds ?? []), ...(advisorContextBundle?.diagnostics.clippedIds ?? [])])],
|
|
471
|
+
}),
|
|
455
472
|
// Statistics only; consultations have no per-turn quota.
|
|
456
473
|
advisorCalls: { used: 0 },
|
|
457
474
|
advisorInvocation: 'agent',
|
|
@@ -910,7 +927,17 @@ export async function* runQuery(
|
|
|
910
927
|
}
|
|
911
928
|
|
|
912
929
|
// 3. 调 LLM 并组装 assistant 消息
|
|
930
|
+
trace('model_start');
|
|
913
931
|
let assistantText = '';
|
|
932
|
+
// Candidate text is private until the delivery gates allow publication.
|
|
933
|
+
// Keep the compatibility event as a phase notice, never as a text retraction.
|
|
934
|
+
function* retireDraft(reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted'): Generator<LoopEvent> {
|
|
935
|
+
const message: Message = { role: 'system', content: answerPreparationNotice(reason), id: crypto.randomUUID(),
|
|
936
|
+
timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
937
|
+
history.push(message);
|
|
938
|
+
yield { type: 'injected_message', message };
|
|
939
|
+
if (reason !== 'interrupted') yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
|
|
940
|
+
}
|
|
914
941
|
/** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
|
|
915
942
|
const toolCallsByIndex: ToolCall[] = [];
|
|
916
943
|
/**
|
|
@@ -921,9 +948,6 @@ export async function* runQuery(
|
|
|
921
948
|
let reasoningString = '';
|
|
922
949
|
const reasoningDetails: unknown[] = [];
|
|
923
950
|
|
|
924
|
-
/** 首个 text_delta 时切到 streaming,再来不重复 yield */
|
|
925
|
-
let stageStreamingYielded = false;
|
|
926
|
-
|
|
927
951
|
/**
|
|
928
952
|
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
929
953
|
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
@@ -956,12 +980,7 @@ export async function* runQuery(
|
|
|
956
980
|
: {}),
|
|
957
981
|
})) {
|
|
958
982
|
if (ev.type === 'text_delta') {
|
|
959
|
-
if (!stageStreamingYielded) {
|
|
960
|
-
stageStreamingYielded = true;
|
|
961
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
962
|
-
}
|
|
963
983
|
assistantText += ev.delta;
|
|
964
|
-
yield { type: 'text', delta: ev.delta };
|
|
965
984
|
} else if (ev.type === 'tool_call_delta') {
|
|
966
985
|
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
967
986
|
} else if (ev.type === 'reasoning_delta') {
|
|
@@ -1002,7 +1021,9 @@ export async function* runQuery(
|
|
|
1002
1021
|
}
|
|
1003
1022
|
}
|
|
1004
1023
|
} catch (e) {
|
|
1024
|
+
trace('model_error');
|
|
1005
1025
|
if ((e as Error).name === 'AbortError') {
|
|
1026
|
+
yield* retireDraft('interrupted');
|
|
1006
1027
|
yield interruption();
|
|
1007
1028
|
return;
|
|
1008
1029
|
}
|
|
@@ -1026,6 +1047,7 @@ export async function* runQuery(
|
|
|
1026
1047
|
options.promptCache,
|
|
1027
1048
|
);
|
|
1028
1049
|
} catch (error) {
|
|
1050
|
+
yield* retireDraft('interrupted');
|
|
1029
1051
|
if (signal?.aborted || (error as Error).name === 'AbortError') {
|
|
1030
1052
|
yield interruption();
|
|
1031
1053
|
return;
|
|
@@ -1057,6 +1079,7 @@ export async function* runQuery(
|
|
|
1057
1079
|
}
|
|
1058
1080
|
|
|
1059
1081
|
// code='llm_error':终结性错误,stop_reason='error'
|
|
1082
|
+
yield* retireDraft('interrupted');
|
|
1060
1083
|
yield {
|
|
1061
1084
|
type: 'error',
|
|
1062
1085
|
error: `LLM 调用失败:${(e as Error).message}`,
|
|
@@ -1065,7 +1088,9 @@ export async function* runQuery(
|
|
|
1065
1088
|
return;
|
|
1066
1089
|
}
|
|
1067
1090
|
|
|
1091
|
+
trace('model_end');
|
|
1068
1092
|
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
1093
|
+
yield* retireDraft('interrupted');
|
|
1069
1094
|
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
1070
1095
|
pushQueuedInputMarker(history);
|
|
1071
1096
|
yield { type: 'interrupted', reason: 'queued_input' };
|
|
@@ -1086,6 +1111,19 @@ export async function* runQuery(
|
|
|
1086
1111
|
}
|
|
1087
1112
|
}
|
|
1088
1113
|
|
|
1114
|
+
// A pending review must run before another solving/publishing attempt.
|
|
1115
|
+
// Read-only diagnosis and human questions remain available while stalled.
|
|
1116
|
+
const pendingReview = options.advisorCheckpoints === false ? undefined : advisorCoordinator.pendingReview();
|
|
1117
|
+
if (pendingReview && turn < maxTurns && toolCallsByIndex.length > 0
|
|
1118
|
+
&& !toolCallsByIndex.some(tc => ['Advisor', 'AskUserQuestion'].includes(tc.function.name))
|
|
1119
|
+
&& toolCallsByIndex.some(tc => tc.function.name === 'PresentResult'
|
|
1120
|
+
|| advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find(t => t.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace
|
|
1121
|
+
|| stallGuard.shouldReject(tc))) {
|
|
1122
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1123
|
+
toolCallsByIndex.splice(0, toolCallsByIndex.length, { id: crypto.randomUUID(), type: 'function',
|
|
1124
|
+
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
1125
|
+
assistantText = '';
|
|
1126
|
+
}
|
|
1089
1127
|
const assistantMsg: Message = {
|
|
1090
1128
|
role: 'assistant',
|
|
1091
1129
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -1098,12 +1136,12 @@ export async function* runQuery(
|
|
|
1098
1136
|
...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
|
|
1099
1137
|
};
|
|
1100
1138
|
// 4. 没有工具调用 → 整轮交互结束
|
|
1101
|
-
if (toolCallsByIndex.length === 0) {
|
|
1139
|
+
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
1102
1140
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
1103
1141
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
1104
1142
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
1105
1143
|
askCorrectionAttempted = true;
|
|
1106
|
-
yield
|
|
1144
|
+
yield* retireDraft('interactive_question');
|
|
1107
1145
|
continue;
|
|
1108
1146
|
}
|
|
1109
1147
|
const emptyResponse = stripThinkBlocks(assistantText).length === 0;
|
|
@@ -1112,12 +1150,11 @@ export async function* runQuery(
|
|
|
1112
1150
|
if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
1113
1151
|
delegationCorrectionAttempted = true;
|
|
1114
1152
|
coordinationText = coordination!.text;
|
|
1115
|
-
yield
|
|
1153
|
+
yield* retireDraft('formal_delivery');
|
|
1116
1154
|
continue;
|
|
1117
1155
|
}
|
|
1118
1156
|
const pendingMessages = [...(options.takePendingMessages?.() ?? []), ...await takeAgentNotifications(toolCallContext)];
|
|
1119
1157
|
if (pendingMessages.length) {
|
|
1120
|
-
if (!emptyResponse) history.push(assistantMsg);
|
|
1121
1158
|
history.push(...pendingMessages);
|
|
1122
1159
|
continue;
|
|
1123
1160
|
}
|
|
@@ -1131,22 +1168,27 @@ export async function* runQuery(
|
|
|
1131
1168
|
&& turn < maxTurns
|
|
1132
1169
|
) {
|
|
1133
1170
|
deliveryCorrectionAttempted = true;
|
|
1134
|
-
yield
|
|
1135
|
-
type: 'assistant_draft_discarded',
|
|
1136
|
-
reason: 'formal_delivery',
|
|
1137
|
-
textLength: assistantText.length,
|
|
1138
|
-
};
|
|
1171
|
+
yield* retireDraft('formal_delivery');
|
|
1139
1172
|
continue;
|
|
1140
1173
|
}
|
|
1141
|
-
const deliveryCheckpoint = emptyResponse
|
|
1174
|
+
const deliveryCheckpoint = emptyResponse || options.advisorCheckpoints === false
|
|
1142
1175
|
? { allow: true as const }
|
|
1143
1176
|
: advisorCoordinator.beforeDelivery(assistantText);
|
|
1177
|
+
const fallbackReview = !emptyResponse && options.advisorCheckpoints !== false
|
|
1178
|
+
? advisorCoordinator.pendingReview(assistantText) : undefined;
|
|
1179
|
+
if (fallbackReview && turn < maxTurns) {
|
|
1180
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1181
|
+
// Reuse the effective tool boundary, permissions, cancellation, events and
|
|
1182
|
+
// persistence. Never call a provider behind the host's tool pipeline.
|
|
1183
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
1184
|
+
name: 'Advisor', arguments: JSON.stringify(fallbackReview),
|
|
1185
|
+
} });
|
|
1186
|
+
assistantMsg.content = null;
|
|
1187
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
1188
|
+
break finalResponse;
|
|
1189
|
+
}
|
|
1144
1190
|
if (!deliveryCheckpoint.allow && turn < maxTurns) {
|
|
1145
|
-
yield
|
|
1146
|
-
type: 'assistant_draft_discarded',
|
|
1147
|
-
reason: 'advisor_checkpoint',
|
|
1148
|
-
textLength: assistantText.length,
|
|
1149
|
-
};
|
|
1191
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1150
1192
|
continue;
|
|
1151
1193
|
}
|
|
1152
1194
|
if (!emptyResponse) {
|
|
@@ -1156,19 +1198,110 @@ export async function* runQuery(
|
|
|
1156
1198
|
if (emptyResponse) {
|
|
1157
1199
|
// A no-tool response is already the model's terminal signal. Do not turn it
|
|
1158
1200
|
// into another model round, and do not persist/broadcast an empty bubble.
|
|
1159
|
-
yield
|
|
1160
|
-
type: 'assistant_draft_discarded',
|
|
1161
|
-
reason: 'empty_response',
|
|
1162
|
-
textLength: assistantText.length,
|
|
1163
|
-
};
|
|
1201
|
+
yield* retireDraft('empty_response');
|
|
1164
1202
|
} else {
|
|
1203
|
+
// Publication is a separate tool-free request. Nothing from preparation
|
|
1204
|
+
// has been streamed, and no gate may retract text after this boundary.
|
|
1205
|
+
let publicationError: unknown;
|
|
1206
|
+
let publicationInterrupted = false;
|
|
1207
|
+
let streamed = false;
|
|
1208
|
+
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
1209
|
+
&& (runtimeContext.surface === 'desktop' || runtimeContext.surface === 'tui')) {
|
|
1210
|
+
const candidate = assistantText;
|
|
1211
|
+
const candidateFinishReason = lastFinishReason;
|
|
1212
|
+
let publishedText = '';
|
|
1213
|
+
let publicationDone = false;
|
|
1214
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
1215
|
+
try {
|
|
1216
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText);
|
|
1217
|
+
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
1218
|
+
if (publicationPlan.overBudget) throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
1219
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1220
|
+
publicationInterrupted = true;
|
|
1221
|
+
throw new Error('最终发布已取消。');
|
|
1222
|
+
}
|
|
1223
|
+
turnUsage.totalCalls += 1;
|
|
1224
|
+
for await (const event of chatFn({
|
|
1225
|
+
provider,
|
|
1226
|
+
messages: publicationMessages,
|
|
1227
|
+
tools: [], signal,
|
|
1228
|
+
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1229
|
+
promptCache: options.promptCache,
|
|
1230
|
+
})) {
|
|
1231
|
+
if (event.type === 'done' && !publicationDone) {
|
|
1232
|
+
publicationDone = true;
|
|
1233
|
+
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
1234
|
+
turnUsage.reportedCalls += 1;
|
|
1235
|
+
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
1236
|
+
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1240
|
+
publicationInterrupted = true;
|
|
1241
|
+
break;
|
|
1242
|
+
}
|
|
1243
|
+
if (event.type === 'text_delta') {
|
|
1244
|
+
// Never expose a rewrite of the reviewed candidate. Previously
|
|
1245
|
+
// published chunks are necessarily an exact prefix of it.
|
|
1246
|
+
if (!candidate.startsWith(publishedText + event.delta)) {
|
|
1247
|
+
throw new Error('发布内容偏离定稿,使用原定稿。');
|
|
1248
|
+
}
|
|
1249
|
+
publishedText += event.delta;
|
|
1250
|
+
streamed = true;
|
|
1251
|
+
yield { type: 'text', delta: event.delta };
|
|
1252
|
+
} else if (event.type === 'tool_call_delta') {
|
|
1253
|
+
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
1254
|
+
} else if (event.type === 'done') {
|
|
1255
|
+
publicationDone = true;
|
|
1256
|
+
lastFinishReason = event.stopReason;
|
|
1257
|
+
if (event.stopReason === 'aborted') publicationInterrupted = true;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (!publicationDone && !publicationInterrupted) throw new Error('最终答案流未正常结束。');
|
|
1261
|
+
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length') throw new Error('最终答案流缺少有效的结束状态。');
|
|
1262
|
+
if (!publishedText.trim() && !publicationInterrupted) throw new Error('最终表达未返回正文。');
|
|
1263
|
+
if (!publicationInterrupted && publishedText !== candidate) throw new Error('最终表达未完整传递定稿。');
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || (error as Error)?.name === 'AbortError';
|
|
1266
|
+
publicationError = error;
|
|
1267
|
+
}
|
|
1268
|
+
if (publicationError && !publicationInterrupted) {
|
|
1269
|
+
yield { type: 'warning', message: '流式发布未完成,已使用原定稿补齐;未重新生成或改写答案。' };
|
|
1270
|
+
const remaining = candidate.slice(publishedText.length);
|
|
1271
|
+
if (remaining) {
|
|
1272
|
+
yield { type: 'text', delta: remaining };
|
|
1273
|
+
streamed = true;
|
|
1274
|
+
}
|
|
1275
|
+
publishedText = candidate;
|
|
1276
|
+
publicationError = undefined;
|
|
1277
|
+
}
|
|
1278
|
+
assistantText = publishedText;
|
|
1279
|
+
if (!publicationInterrupted) lastFinishReason = candidateFinishReason;
|
|
1280
|
+
assistantMsg.content = publishedText || null;
|
|
1281
|
+
if (!publishedText) {
|
|
1282
|
+
if (publicationInterrupted) yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1283
|
+
else yield { type: 'error', code: 'llm_error', error: `最终答案生成失败:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
assistantMsg.presentation = 'final';
|
|
1288
|
+
trace('final_published');
|
|
1165
1289
|
if (assistantMsg.role === 'assistant') assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
|
|
1166
1290
|
assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
|
|
1167
1291
|
history.push(assistantMsg);
|
|
1292
|
+
// Non-interactive consumers retain atomic output. Interactive hosts
|
|
1293
|
+
// already received real deltas; commit the identical text only once.
|
|
1294
|
+
if (!streamed) yield { type: 'text', delta: assistantText };
|
|
1168
1295
|
yield {
|
|
1169
1296
|
type: 'assistant_message',
|
|
1170
1297
|
message: assistantMsg as Extract<Message, { role: 'assistant' }>,
|
|
1171
1298
|
};
|
|
1299
|
+
if (publicationInterrupted || publicationError) {
|
|
1300
|
+
yield { type: 'warning', message: '最终答案未完整生成,已保留已输出内容;未撤回或重新生成。' };
|
|
1301
|
+
if (publicationInterrupted) yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1302
|
+
else yield { type: 'error', code: 'llm_error', error: `最终答案流中断:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1172
1305
|
}
|
|
1173
1306
|
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
1174
1307
|
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
@@ -1220,7 +1353,11 @@ export async function* runQuery(
|
|
|
1220
1353
|
...turnUsage,
|
|
1221
1354
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
1222
1355
|
};
|
|
1223
|
-
if (
|
|
1356
|
+
if (emptyResponse && !resultPresented) {
|
|
1357
|
+
const warning = '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1358
|
+
yield { type: 'warning', message: warning };
|
|
1359
|
+
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1360
|
+
} else if (pendingDelegations.length) {
|
|
1224
1361
|
const warning = `必要子任务尚未收口:${pendingDelegations.map((r) => r.id).join(', ')}。本轮不能视为全部完成。`;
|
|
1225
1362
|
yield { type: 'warning', message: warning };
|
|
1226
1363
|
yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning, usage };
|
|
@@ -1240,11 +1377,14 @@ export async function* runQuery(
|
|
|
1240
1377
|
usage,
|
|
1241
1378
|
};
|
|
1242
1379
|
} else {
|
|
1243
|
-
yield { type: 'turn_done', completionStatus: 'completed', usage
|
|
1380
|
+
yield { type: 'turn_done', completionStatus: 'completed', usage,
|
|
1381
|
+
...(emptyResponse ? { warning: '成果已提交,未生成文字总结;请查看已提交的成果。' } : {}) };
|
|
1244
1382
|
}
|
|
1245
1383
|
return;
|
|
1246
1384
|
}
|
|
1247
1385
|
|
|
1386
|
+
assistantMsg.presentation = 'progress';
|
|
1387
|
+
trace('progress_published');
|
|
1248
1388
|
history.push(assistantMsg);
|
|
1249
1389
|
yield {
|
|
1250
1390
|
type: 'assistant_message',
|
|
@@ -1471,7 +1611,19 @@ export async function* runQuery(
|
|
|
1471
1611
|
};
|
|
1472
1612
|
|
|
1473
1613
|
const runOne: typeof runOneImpl = async (tc, reportProgress) => {
|
|
1614
|
+
// Also settle denied/invalid/unavailable calls that never reached the
|
|
1615
|
+
// provider adapter. This records a real attempt, not an ignored reminder.
|
|
1616
|
+
let consultation;
|
|
1617
|
+
if (tc.function.name === 'Advisor') {
|
|
1618
|
+
let reason: import('./types.js').AdvisorReason | undefined;
|
|
1619
|
+
try { reason = JSON.parse(tc.function.arguments).reason; } catch { /* validation handles malformed input */ }
|
|
1620
|
+
consultation = advisorCoordinator.beginConsultation(reason);
|
|
1621
|
+
}
|
|
1474
1622
|
const outcome = await runOneImpl(tc, reportProgress);
|
|
1623
|
+
if (consultation && outcome.result.metadata?.kind !== 'advisor_consultation'
|
|
1624
|
+
&& !( !outcome.result.ok && outcome.result.error === 'aborted')) {
|
|
1625
|
+
outcome.result = { ...outcome.result, metadata: advisorCoordinator.finishConsultation(consultation, outcome.result.ok) };
|
|
1626
|
+
}
|
|
1475
1627
|
const metadata = outcome.result.metadata;
|
|
1476
1628
|
if (metadata?.kind === 'advisor_decision' && outcome.result.ok
|
|
1477
1629
|
|| metadata?.kind === 'advisor_consultation') {
|
|
@@ -1788,6 +1940,7 @@ export function buildModelMessages(
|
|
|
1788
1940
|
transientContext?: string,
|
|
1789
1941
|
currentUserMessageId?: string,
|
|
1790
1942
|
): Message[] {
|
|
1943
|
+
history = history.filter(message => !(message.role === 'system' && message._ephemeral));
|
|
1791
1944
|
const transient = transientContext?.trim();
|
|
1792
1945
|
if (!transient) return history;
|
|
1793
1946
|
let index = currentUserMessageId
|
|
@@ -193,10 +193,18 @@ const CAPABILITY_POLICIES = [
|
|
|
193
193
|
render: ({ toolNames }) => `- 需求匹配已安装 Skill 或用户点名时,${skillInvokeInstruction(toolNames.has('Skill'))};前端开发与交互修复也要主动匹配设计技能,不等用户说“设计”。引用资源按需读取,多阶段按需组合,不预读整个目录或加载无关技能。`,
|
|
194
194
|
},
|
|
195
195
|
{
|
|
196
|
-
id: 'execution.desktop-diagrams', layer: 'execution', profiles:
|
|
197
|
-
render: ({ surface, toolNames }) =>
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
197
|
+
render: ({ surface, toolNames }) => [
|
|
198
|
+
'- 按需配图,优先 Mermaid/SVG;用户指定 ASCII、代码/目录树除外。',
|
|
199
|
+
surface === 'desktop'
|
|
200
|
+
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
201
|
+
: surface === 'cli' || surface === 'tui'
|
|
202
|
+
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
203
|
+
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
204
|
+
toolNames.has('Skill') || toolNames.has('Read')
|
|
205
|
+
? '- 加载可用的 dynamic-ui。'
|
|
206
|
+
: '',
|
|
207
|
+
].filter(Boolean).join('\n'),
|
|
200
208
|
},
|
|
201
209
|
{
|
|
202
210
|
id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
|
|
@@ -246,12 +254,12 @@ const CAPABILITY_POLICIES = [
|
|
|
246
254
|
requiresTools: ['Advisor'],
|
|
247
255
|
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
248
256
|
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
249
|
-
render: ({ toolNames }) => `- Advisor
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
|
|
258
|
+
咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
|
|
259
|
+
文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
|
|
260
|
+
同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
|
|
252
261
|
不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
|
|
253
|
-
|
|
254
|
-
提供关键疑点与必要现场,返回后先读结果再决定后续行动;同批暂缓调用不是执行证据,不因此重复咨询。`,
|
|
262
|
+
以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
|
|
255
263
|
},
|
|
256
264
|
{
|
|
257
265
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -240,10 +240,18 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
240
240
|
render: ({ toolNames }) => `- 需求匹配已安装 Skill 或用户点名时,${skillInvokeInstruction(toolNames.has('Skill'))};前端开发与交互修复也要主动匹配设计技能,不等用户说“设计”。引用资源按需读取,多阶段按需组合,不预读整个目录或加载无关技能。`,
|
|
241
241
|
},
|
|
242
242
|
{
|
|
243
|
-
id: 'execution.desktop-diagrams', layer: 'execution', profiles:
|
|
244
|
-
render: ({ surface, toolNames }) =>
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
244
|
+
render: ({ surface, toolNames }) => [
|
|
245
|
+
'- 按需配图,优先 Mermaid/SVG;用户指定 ASCII、代码/目录树除外。',
|
|
246
|
+
surface === 'desktop'
|
|
247
|
+
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
248
|
+
: surface === 'cli' || surface === 'tui'
|
|
249
|
+
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
250
|
+
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
251
|
+
toolNames.has('Skill') || toolNames.has('Read')
|
|
252
|
+
? '- 加载可用的 dynamic-ui。'
|
|
253
|
+
: '',
|
|
254
|
+
].filter(Boolean).join('\n'),
|
|
247
255
|
},
|
|
248
256
|
{
|
|
249
257
|
id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
|
|
@@ -293,12 +301,12 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
293
301
|
requiresTools: ['Advisor'],
|
|
294
302
|
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
295
303
|
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
296
|
-
render: ({ toolNames }) => `- Advisor
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
|
|
305
|
+
咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
|
|
306
|
+
文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
|
|
307
|
+
同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
|
|
299
308
|
不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
|
|
300
|
-
|
|
301
|
-
提供关键疑点与必要现场,返回后先读结果再决定后续行动;同批暂缓调用不是执行证据,不因此重复咨询。`,
|
|
309
|
+
以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
|
|
302
310
|
},
|
|
303
311
|
{
|
|
304
312
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function answerPreparationNotice(reason) {
|
|
2
|
+
switch (reason) {
|
|
3
|
+
case 'advisor_checkpoint': return '交付前复核:正在核查内部候选结论,完成后发布最终答案。';
|
|
4
|
+
case 'formal_delivery': return '整理交付:正在核对委派结果或补齐正式成果提交。';
|
|
5
|
+
case 'interactive_question': return '等待关键决策:接下来通过交互问题确认,请在输入区作答。';
|
|
6
|
+
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
7
|
+
case 'interrupted': return '生成未完成,未发布最终答案。';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoopEvent } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export function answerPreparationNotice(reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted'): string {
|
|
4
|
+
switch (reason) {
|
|
5
|
+
case 'advisor_checkpoint': return '交付前复核:正在核查内部候选结论,完成后发布最终答案。';
|
|
6
|
+
case 'formal_delivery': return '整理交付:正在核对委派结果或补齐正式成果提交。';
|
|
7
|
+
case 'interactive_question': return '等待关键决策:接下来通过交互问题确认,请在输入区作答。';
|
|
8
|
+
case 'empty_response': return '本轮未产生可显示的正文,未生成新的最终答复。';
|
|
9
|
+
case 'interrupted': return '生成未完成,未发布最终答案。';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -4,6 +4,8 @@ export function isRecoverableLoopError(event) {
|
|
|
4
4
|
}
|
|
5
5
|
export function completionWarning(event) {
|
|
6
6
|
switch (event.completionStatus) {
|
|
7
|
+
case 'no_answer':
|
|
8
|
+
return event.warning || '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
7
9
|
case 'delivery_incomplete':
|
|
8
10
|
return event.warning || '交付未完成,请检查保留的结果。';
|
|
9
11
|
case 'delivery_unverified':
|
|
@@ -11,6 +11,8 @@ export function completionWarning(
|
|
|
11
11
|
event: Extract<LoopEvent, { type: 'turn_done' }>,
|
|
12
12
|
): string | undefined {
|
|
13
13
|
switch (event.completionStatus) {
|
|
14
|
+
case 'no_answer':
|
|
15
|
+
return event.warning || '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
14
16
|
case 'delivery_incomplete':
|
|
15
17
|
return event.warning || '交付未完成,请检查保留的结果。';
|
|
16
18
|
case 'delivery_unverified':
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Ephemeral publication request; never append this private candidate to history. */
|
|
2
|
+
export function finalAnswerMessages(messages, candidate) {
|
|
3
|
+
return [...messages,
|
|
4
|
+
{ role: 'assistant', content: candidate },
|
|
5
|
+
{ role: 'user', content: '内部核查与工具阶段已结束。上一条 assistant 消息是待发布的定稿。请逐字输出该消息,保留全部文字、空白、代码、链接与格式;不要改写、补充、删减,不加开场白或外层代码围栏,不调用工具或顾问。不要输出本条指令。' },
|
|
6
|
+
];
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Message } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Ephemeral publication request; never append this private candidate to history. */
|
|
4
|
+
export function finalAnswerMessages(messages: Message[], candidate: string): Message[] {
|
|
5
|
+
return [...messages,
|
|
6
|
+
{ role: 'assistant', content: candidate },
|
|
7
|
+
{ role: 'user', content: '内部核查与工具阶段已结束。上一条 assistant 消息是待发布的定稿。请逐字输出该消息,保留全部文字、空白、代码、链接与格式;不要改写、补充、删减,不加开场白或外层代码围栏,不调用工具或顾问。不要输出本条指令。' },
|
|
8
|
+
];
|
|
9
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
/** Opt-in diagnostics: no prompts, paths, session IDs, tool arguments or model output. */
|
|
3
|
+
export function createLifecycleTrace(scope, enabled = process.env.MINIMAL_AGENT_TRACE_LIFECYCLE === '1', write = line => { process.stderr.write(line); }) {
|
|
4
|
+
if (!enabled)
|
|
5
|
+
return () => { };
|
|
6
|
+
const traceId = randomUUID();
|
|
7
|
+
const start = performance.now();
|
|
8
|
+
let sequence = 0;
|
|
9
|
+
return phase => {
|
|
10
|
+
try {
|
|
11
|
+
write(`${JSON.stringify({ kind: 'agent_lifecycle', scope, traceId, sequence: ++sequence,
|
|
12
|
+
phase, timestamp: Date.now(), elapsedMs: Math.round(performance.now() - start) })}\n`);
|
|
13
|
+
}
|
|
14
|
+
catch { /* Diagnostics must never interrupt execution. */ }
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
type Phase = 'model_start' | 'model_end' | 'model_error' | 'progress_published' | 'final_published'
|
|
4
|
+
| 'save_start' | 'save_end' | 'save_error' | 'turn_done' | 'memory_start' | 'memory_end';
|
|
5
|
+
|
|
6
|
+
/** Opt-in diagnostics: no prompts, paths, session IDs, tool arguments or model output. */
|
|
7
|
+
export function createLifecycleTrace(
|
|
8
|
+
scope: 'loop' | 'host',
|
|
9
|
+
enabled = process.env.MINIMAL_AGENT_TRACE_LIFECYCLE === '1',
|
|
10
|
+
write: (line: string) => void = line => { process.stderr.write(line); },
|
|
11
|
+
): (phase: Phase) => void {
|
|
12
|
+
if (!enabled) return () => {};
|
|
13
|
+
const traceId = randomUUID();
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
let sequence = 0;
|
|
16
|
+
return phase => {
|
|
17
|
+
try {
|
|
18
|
+
write(`${JSON.stringify({ kind: 'agent_lifecycle', scope, traceId, sequence: ++sequence,
|
|
19
|
+
phase, timestamp: Date.now(), elapsedMs: Math.round(performance.now() - start) })}\n`);
|
|
20
|
+
} catch { /* Diagnostics must never interrupt execution. */ }
|
|
21
|
+
};
|
|
22
|
+
}
|
package/src/session/runTurn.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* ============================================================
|
|
20
20
|
*/
|
|
21
21
|
import { acquireContextLock } from '../context/contextLock.js';
|
|
22
|
+
import { createLifecycleTrace } from '../runtime/lifecycleTrace.js';
|
|
22
23
|
import { loadContext, saveContext } from '../context/persistContext.js';
|
|
23
24
|
import { startTurnJournal } from '../memory/turnJournal.js';
|
|
24
25
|
import { createLearningModel } from '../memory/learningModel.js';
|
|
@@ -69,6 +70,7 @@ export async function* runTurn(opts) {
|
|
|
69
70
|
return null;
|
|
70
71
|
});
|
|
71
72
|
let completedNormally = false;
|
|
73
|
+
const trace = createLifecycleTrace('host');
|
|
72
74
|
let hostFailed = false;
|
|
73
75
|
try {
|
|
74
76
|
for await (const event of runWithPlugins(opts.userInput, {
|
|
@@ -107,8 +109,18 @@ export async function* runTurn(opts) {
|
|
|
107
109
|
// at stage boundaries so a Runtime restart does not lose the whole workflow.
|
|
108
110
|
if (persist && (['user_message_committed', 'assistant_message', 'compact_start', 'turn_done'].includes(event.type)
|
|
109
111
|
|| (event.type === 'injected_message' && event.message && typeof event.message === 'object' && 'role' in event.message && event.message.role === 'system' && '_uiProgress' in event.message && event.message._uiProgress === true))) {
|
|
110
|
-
|
|
112
|
+
trace('save_start');
|
|
113
|
+
try {
|
|
114
|
+
await saveContext(history, opts.sessionFile);
|
|
115
|
+
trace('save_end');
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
trace('save_error');
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
111
121
|
}
|
|
122
|
+
if (event.type === 'turn_done')
|
|
123
|
+
trace('turn_done');
|
|
112
124
|
yield event;
|
|
113
125
|
}
|
|
114
126
|
completedNormally = true;
|
|
@@ -124,9 +136,11 @@ export async function* runTurn(opts) {
|
|
|
124
136
|
try {
|
|
125
137
|
if (!completedNormally && !hostFailed)
|
|
126
138
|
journal?.observe({ type: 'interrupted' });
|
|
139
|
+
trace('memory_start');
|
|
127
140
|
await journal?.finish(history, historyStart).catch(error => {
|
|
128
141
|
process.stderr.write(`[minimal-agent memory] 本轮记忆收尾失败,不影响会话落盘:${error.message}\n`);
|
|
129
142
|
});
|
|
143
|
+
trace('memory_end');
|
|
130
144
|
if (persist)
|
|
131
145
|
await saveContext(history, opts.sessionFile);
|
|
132
146
|
}
|