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.ts
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
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 { resolveOutputContract, toolsForResponsePhase } from './runtime/outputContract.js';
|
|
34
|
+
import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
|
|
35
|
+
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
31
36
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
32
37
|
import { dirname, resolve } from 'node:path';
|
|
33
38
|
import { loadProjectPromptInstructions } from './prompts/projectInstructions.js';
|
|
@@ -100,6 +105,7 @@ import {
|
|
|
100
105
|
} from './advisor/advisorCoordinator.js';
|
|
101
106
|
import { toolEffectFor } from './tools/capabilities.js';
|
|
102
107
|
import { isReadOnlyShellCommand, isVerificationShellCommand } from './tools/bash/semantics.js';
|
|
108
|
+
import { collectReviewContext } from './advisor/reviewContext.js';
|
|
103
109
|
import {
|
|
104
110
|
createAgentRuntimeContext,
|
|
105
111
|
type AgentRuntimeContext,
|
|
@@ -410,11 +416,13 @@ export async function* runQuery(
|
|
|
410
416
|
if (!computerIntent.needed && sessionState) delete sessionState.computerAutomation;
|
|
411
417
|
// DI fallback:测试可传 stub,生产走真实模块单例
|
|
412
418
|
const chatFn = options.chat ?? defaultChat;
|
|
419
|
+
const trace = createLifecycleTrace('loop');
|
|
413
420
|
const executeToolFn = options.executeTool ?? defaultExecuteTool;
|
|
414
421
|
const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
|
|
415
422
|
const currentUserMessageId = options.userMessageId ?? crypto.randomUUID();
|
|
416
423
|
|
|
417
424
|
let currentTransientBundle = options.transientBundle;
|
|
425
|
+
let advisorContextBundle = currentTransientBundle;
|
|
418
426
|
let modelEnvironmentCwd = runtimeContext.cwd;
|
|
419
427
|
const observedRuleScopes = new Map<string, string>();
|
|
420
428
|
const pendingRuleScopes = new Set<string>();
|
|
@@ -435,12 +443,15 @@ export async function* runQuery(
|
|
|
435
443
|
const selected = availableForBundle >= bundle.diagnostics.usedTokens
|
|
436
444
|
? bundle
|
|
437
445
|
: resizeContextBundle(bundle, availableForBundle);
|
|
446
|
+
advisorContextBundle = selected;
|
|
438
447
|
return [selected.text, opaque].filter(Boolean).join('\n\n');
|
|
439
448
|
};
|
|
440
449
|
let resultPresented = false;
|
|
441
450
|
let presentedResultUnverified = false;
|
|
442
451
|
let deliveryCorrectionAttempted = false;
|
|
443
452
|
let askCorrectionAttempted = false;
|
|
453
|
+
let diagramPreparationAttempted = false;
|
|
454
|
+
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
444
455
|
const subagents = await configService.subagents();
|
|
445
456
|
await hydrateSessionWorktree({
|
|
446
457
|
sessionState,
|
|
@@ -449,9 +460,21 @@ export async function* runQuery(
|
|
|
449
460
|
cwd: runtimeContext.cwd,
|
|
450
461
|
});
|
|
451
462
|
const toolCallContext: ToolCallContext = {
|
|
463
|
+
output,
|
|
464
|
+
availableToolNames: () => currentEffectiveToolSet.names,
|
|
452
465
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
453
466
|
history,
|
|
454
467
|
sessionState,
|
|
468
|
+
advisorReviewContext: () => ({
|
|
469
|
+
...collectReviewContext(advisorContextBundle),
|
|
470
|
+
constraints: [...collectReviewContext(advisorContextBundle).constraints,
|
|
471
|
+
{ id: 'execution.boundary', authority: 'host', content: JSON.stringify({ cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd,
|
|
472
|
+
conversationMode: toolCallContext.conversationMode, profile: runtimeContext.profile,
|
|
473
|
+
availableTools: [...currentEffectiveToolSet.names], invocation: invocationInstructions(options.invocationPolicy) }) }],
|
|
474
|
+
omittedIds: [...new Set([...(currentTransientBundle?.diagnostics.omittedIds ?? []), ...(advisorContextBundle?.diagnostics.omittedIds ?? [])]),
|
|
475
|
+
...(options.transientContext ? ['legacy.transient-context:not-forwarded'] : [])],
|
|
476
|
+
clippedIds: [...new Set([...(currentTransientBundle?.diagnostics.clippedIds ?? []), ...(advisorContextBundle?.diagnostics.clippedIds ?? [])])],
|
|
477
|
+
}),
|
|
455
478
|
// Statistics only; consultations have no per-turn quota.
|
|
456
479
|
advisorCalls: { used: 0 },
|
|
457
480
|
advisorInvocation: 'agent',
|
|
@@ -516,6 +539,7 @@ export async function* runQuery(
|
|
|
516
539
|
const parts = [
|
|
517
540
|
transientFor(activeTools),
|
|
518
541
|
coordinationText,
|
|
542
|
+
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
519
543
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
520
544
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
521
545
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
@@ -910,7 +934,17 @@ export async function* runQuery(
|
|
|
910
934
|
}
|
|
911
935
|
|
|
912
936
|
// 3. 调 LLM 并组装 assistant 消息
|
|
937
|
+
trace('model_start');
|
|
913
938
|
let assistantText = '';
|
|
939
|
+
// Candidate text is private until the delivery gates allow publication.
|
|
940
|
+
// Keep the compatibility event as a phase notice, never as a text retraction.
|
|
941
|
+
function* retireDraft(reason: Extract<LoopEvent, { type: 'assistant_draft_discarded' }>['reason'] | 'interrupted'): Generator<LoopEvent> {
|
|
942
|
+
const message: Message = { role: 'system', content: answerPreparationNotice(reason), id: crypto.randomUUID(),
|
|
943
|
+
timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
944
|
+
history.push(message);
|
|
945
|
+
yield { type: 'injected_message', message };
|
|
946
|
+
if (reason !== 'interrupted') yield { type: 'assistant_draft_discarded', reason, textLength: 0 };
|
|
947
|
+
}
|
|
914
948
|
/** 按 index 累积 tool_calls(OpenAI 协议下 tool_calls 是分片返回的) */
|
|
915
949
|
const toolCallsByIndex: ToolCall[] = [];
|
|
916
950
|
/**
|
|
@@ -921,9 +955,6 @@ export async function* runQuery(
|
|
|
921
955
|
let reasoningString = '';
|
|
922
956
|
const reasoningDetails: unknown[] = [];
|
|
923
957
|
|
|
924
|
-
/** 首个 text_delta 时切到 streaming,再来不重复 yield */
|
|
925
|
-
let stageStreamingYielded = false;
|
|
926
|
-
|
|
927
958
|
/**
|
|
928
959
|
* R2:本轮 LLM 流的终止原因(来自 client 的 done.stopReason)。
|
|
929
960
|
* 'length' = 被 provider 因 max_tokens 截断 —— 收尾时据此给 warning,
|
|
@@ -942,7 +973,7 @@ export async function* runQuery(
|
|
|
942
973
|
for await (const ev of chatFn({
|
|
943
974
|
provider,
|
|
944
975
|
messages: modelMessages,
|
|
945
|
-
tools: currentEffectiveToolSet.tools,
|
|
976
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'preparation'),
|
|
946
977
|
signal,
|
|
947
978
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
948
979
|
promptCache: options.promptCache,
|
|
@@ -956,12 +987,7 @@ export async function* runQuery(
|
|
|
956
987
|
: {}),
|
|
957
988
|
})) {
|
|
958
989
|
if (ev.type === 'text_delta') {
|
|
959
|
-
if (!stageStreamingYielded) {
|
|
960
|
-
stageStreamingYielded = true;
|
|
961
|
-
yield { type: 'stage_change', stage: 'streaming' };
|
|
962
|
-
}
|
|
963
990
|
assistantText += ev.delta;
|
|
964
|
-
yield { type: 'text', delta: ev.delta };
|
|
965
991
|
} else if (ev.type === 'tool_call_delta') {
|
|
966
992
|
mergeToolCallDelta(toolCallsByIndex, ev);
|
|
967
993
|
} else if (ev.type === 'reasoning_delta') {
|
|
@@ -1002,7 +1028,9 @@ export async function* runQuery(
|
|
|
1002
1028
|
}
|
|
1003
1029
|
}
|
|
1004
1030
|
} catch (e) {
|
|
1031
|
+
trace('model_error');
|
|
1005
1032
|
if ((e as Error).name === 'AbortError') {
|
|
1033
|
+
yield* retireDraft('interrupted');
|
|
1006
1034
|
yield interruption();
|
|
1007
1035
|
return;
|
|
1008
1036
|
}
|
|
@@ -1026,6 +1054,7 @@ export async function* runQuery(
|
|
|
1026
1054
|
options.promptCache,
|
|
1027
1055
|
);
|
|
1028
1056
|
} catch (error) {
|
|
1057
|
+
yield* retireDraft('interrupted');
|
|
1029
1058
|
if (signal?.aborted || (error as Error).name === 'AbortError') {
|
|
1030
1059
|
yield interruption();
|
|
1031
1060
|
return;
|
|
@@ -1057,6 +1086,7 @@ export async function* runQuery(
|
|
|
1057
1086
|
}
|
|
1058
1087
|
|
|
1059
1088
|
// code='llm_error':终结性错误,stop_reason='error'
|
|
1089
|
+
yield* retireDraft('interrupted');
|
|
1060
1090
|
yield {
|
|
1061
1091
|
type: 'error',
|
|
1062
1092
|
error: `LLM 调用失败:${(e as Error).message}`,
|
|
@@ -1065,7 +1095,9 @@ export async function* runQuery(
|
|
|
1065
1095
|
return;
|
|
1066
1096
|
}
|
|
1067
1097
|
|
|
1098
|
+
trace('model_end');
|
|
1068
1099
|
if (lastFinishReason === 'aborted' || signal?.aborted || options.hasQueuedInput?.()) {
|
|
1100
|
+
yield* retireDraft('interrupted');
|
|
1069
1101
|
if (options.hasQueuedInput?.() && !signal?.aborted) {
|
|
1070
1102
|
pushQueuedInputMarker(history);
|
|
1071
1103
|
yield { type: 'interrupted', reason: 'queued_input' };
|
|
@@ -1086,6 +1118,26 @@ export async function* runQuery(
|
|
|
1086
1118
|
}
|
|
1087
1119
|
}
|
|
1088
1120
|
|
|
1121
|
+
// A pending review must run before another solving/publishing attempt.
|
|
1122
|
+
// Read-only diagnosis and human questions remain available while stalled.
|
|
1123
|
+
const pendingReview = options.advisorCheckpoints === false ? undefined : advisorCoordinator.pendingReview();
|
|
1124
|
+
if (pendingReview && turn < maxTurns && toolCallsByIndex.length > 0
|
|
1125
|
+
&& !toolCallsByIndex.some(tc => ['Advisor', 'AskUserQuestion'].includes(tc.function.name))
|
|
1126
|
+
&& toolCallsByIndex.some(tc => tc.function.name === 'PresentResult'
|
|
1127
|
+
|| advisorSemanticsFor(tc, toolCallContext, currentEffectiveToolSet.tools.find(t => t.name === tc.function.name), { treatVerificationAsReadOnly: true }).mutatesWorkspace
|
|
1128
|
+
|| stallGuard.shouldReject(tc))) {
|
|
1129
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1130
|
+
toolCallsByIndex.splice(0, toolCallsByIndex.length, { id: crypto.randomUUID(), type: 'function',
|
|
1131
|
+
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
1132
|
+
assistantText = '';
|
|
1133
|
+
}
|
|
1134
|
+
// Count the calls that survived review scheduling, including failed loads.
|
|
1135
|
+
for (const call of toolCallsByIndex) {
|
|
1136
|
+
if (call.function.name !== 'Skill') continue;
|
|
1137
|
+
try {
|
|
1138
|
+
if (JSON.parse(call.function.arguments).name === 'dynamic-ui') diagramPreparationAttempted = true;
|
|
1139
|
+
} catch { /* Ordinary tool validation reports malformed arguments. */ }
|
|
1140
|
+
}
|
|
1089
1141
|
const assistantMsg: Message = {
|
|
1090
1142
|
role: 'assistant',
|
|
1091
1143
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -1098,12 +1150,26 @@ export async function* runQuery(
|
|
|
1098
1150
|
...(options.messageProvenance ? { provenance: options.messageProvenance } : {}),
|
|
1099
1151
|
};
|
|
1100
1152
|
// 4. 没有工具调用 → 整轮交互结束
|
|
1101
|
-
if (toolCallsByIndex.length === 0) {
|
|
1153
|
+
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
1154
|
+
// Repair only private preparation, through the ordinary tool pipeline.
|
|
1155
|
+
// One attempt per turn, including missing/failed skills; never rewind a reply.
|
|
1156
|
+
if (!diagramPreparationAttempted && !options.responseFormat
|
|
1157
|
+
&& runtimeContext.profile === 'main' && output.diagrams.length > 0
|
|
1158
|
+
&& currentEffectiveToolSet.names.has('Skill') && turn < maxTurns
|
|
1159
|
+
&& needsDiagramPreparation(assistantText, userInput)) {
|
|
1160
|
+
diagramPreparationAttempted = true;
|
|
1161
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
1162
|
+
name: 'Skill', arguments: JSON.stringify({ name: 'dynamic-ui' }),
|
|
1163
|
+
} });
|
|
1164
|
+
assistantMsg.content = null;
|
|
1165
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
1166
|
+
break finalResponse;
|
|
1167
|
+
}
|
|
1102
1168
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
1103
1169
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
1104
1170
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
1105
1171
|
askCorrectionAttempted = true;
|
|
1106
|
-
yield
|
|
1172
|
+
yield* retireDraft('interactive_question');
|
|
1107
1173
|
continue;
|
|
1108
1174
|
}
|
|
1109
1175
|
const emptyResponse = stripThinkBlocks(assistantText).length === 0;
|
|
@@ -1112,12 +1178,11 @@ export async function* runQuery(
|
|
|
1112
1178
|
if (pendingDelegations.length && !delegationCorrectionAttempted && turn < maxTurns) {
|
|
1113
1179
|
delegationCorrectionAttempted = true;
|
|
1114
1180
|
coordinationText = coordination!.text;
|
|
1115
|
-
yield
|
|
1181
|
+
yield* retireDraft('formal_delivery');
|
|
1116
1182
|
continue;
|
|
1117
1183
|
}
|
|
1118
1184
|
const pendingMessages = [...(options.takePendingMessages?.() ?? []), ...await takeAgentNotifications(toolCallContext)];
|
|
1119
1185
|
if (pendingMessages.length) {
|
|
1120
|
-
if (!emptyResponse) history.push(assistantMsg);
|
|
1121
1186
|
history.push(...pendingMessages);
|
|
1122
1187
|
continue;
|
|
1123
1188
|
}
|
|
@@ -1131,22 +1196,27 @@ export async function* runQuery(
|
|
|
1131
1196
|
&& turn < maxTurns
|
|
1132
1197
|
) {
|
|
1133
1198
|
deliveryCorrectionAttempted = true;
|
|
1134
|
-
yield
|
|
1135
|
-
type: 'assistant_draft_discarded',
|
|
1136
|
-
reason: 'formal_delivery',
|
|
1137
|
-
textLength: assistantText.length,
|
|
1138
|
-
};
|
|
1199
|
+
yield* retireDraft('formal_delivery');
|
|
1139
1200
|
continue;
|
|
1140
1201
|
}
|
|
1141
|
-
const deliveryCheckpoint = emptyResponse
|
|
1202
|
+
const deliveryCheckpoint = emptyResponse || options.advisorCheckpoints === false
|
|
1142
1203
|
? { allow: true as const }
|
|
1143
1204
|
: advisorCoordinator.beforeDelivery(assistantText);
|
|
1205
|
+
const fallbackReview = !emptyResponse && options.advisorCheckpoints !== false
|
|
1206
|
+
? advisorCoordinator.pendingReview(assistantText) : undefined;
|
|
1207
|
+
if (fallbackReview && turn < maxTurns) {
|
|
1208
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1209
|
+
// Reuse the effective tool boundary, permissions, cancellation, events and
|
|
1210
|
+
// persistence. Never call a provider behind the host's tool pipeline.
|
|
1211
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
1212
|
+
name: 'Advisor', arguments: JSON.stringify(fallbackReview),
|
|
1213
|
+
} });
|
|
1214
|
+
assistantMsg.content = null;
|
|
1215
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
1216
|
+
break finalResponse;
|
|
1217
|
+
}
|
|
1144
1218
|
if (!deliveryCheckpoint.allow && turn < maxTurns) {
|
|
1145
|
-
yield
|
|
1146
|
-
type: 'assistant_draft_discarded',
|
|
1147
|
-
reason: 'advisor_checkpoint',
|
|
1148
|
-
textLength: assistantText.length,
|
|
1149
|
-
};
|
|
1219
|
+
yield* retireDraft('advisor_checkpoint');
|
|
1150
1220
|
continue;
|
|
1151
1221
|
}
|
|
1152
1222
|
if (!emptyResponse) {
|
|
@@ -1156,19 +1226,103 @@ export async function* runQuery(
|
|
|
1156
1226
|
if (emptyResponse) {
|
|
1157
1227
|
// A no-tool response is already the model's terminal signal. Do not turn it
|
|
1158
1228
|
// 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
|
-
};
|
|
1229
|
+
yield* retireDraft('empty_response');
|
|
1164
1230
|
} else {
|
|
1231
|
+
// Publication is a separate tool-free request. Nothing from preparation
|
|
1232
|
+
// has been streamed, and no gate may retract text after this boundary.
|
|
1233
|
+
let publicationError: unknown;
|
|
1234
|
+
let publicationInterrupted = false;
|
|
1235
|
+
let streamed = false;
|
|
1236
|
+
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
1237
|
+
&& output.finalDelivery === 'stream') {
|
|
1238
|
+
const candidate = assistantText;
|
|
1239
|
+
const candidateFinishReason = lastFinishReason;
|
|
1240
|
+
let publishedText = '';
|
|
1241
|
+
let publicationDone = false;
|
|
1242
|
+
yield { type: 'stage_change', stage: 'streaming' };
|
|
1243
|
+
try {
|
|
1244
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText, output);
|
|
1245
|
+
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
1246
|
+
if (publicationPlan.overBudget) throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
1247
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1248
|
+
publicationInterrupted = true;
|
|
1249
|
+
throw new Error('最终发布已取消。');
|
|
1250
|
+
}
|
|
1251
|
+
turnUsage.totalCalls += 1;
|
|
1252
|
+
for await (const event of chatFn({
|
|
1253
|
+
provider,
|
|
1254
|
+
messages: publicationMessages,
|
|
1255
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'publication'), signal,
|
|
1256
|
+
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1257
|
+
promptCache: options.promptCache,
|
|
1258
|
+
})) {
|
|
1259
|
+
if (event.type === 'done' && !publicationDone) {
|
|
1260
|
+
publicationDone = true;
|
|
1261
|
+
if (typeof event.promptTokens === 'number' || typeof event.outputTokens === 'number') {
|
|
1262
|
+
turnUsage.reportedCalls += 1;
|
|
1263
|
+
turnUsage.inputTokens += event.promptTokens ?? 0;
|
|
1264
|
+
turnUsage.outputTokens += event.outputTokens ?? 0;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
1268
|
+
publicationInterrupted = true;
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
if (event.type === 'text_delta') {
|
|
1272
|
+
// Final expression is live generation, not an exact-copy replay.
|
|
1273
|
+
// Once visible, text is never replaced or joined with a different draft.
|
|
1274
|
+
publishedText += event.delta;
|
|
1275
|
+
streamed = true;
|
|
1276
|
+
yield { type: 'text', delta: event.delta };
|
|
1277
|
+
} else if (event.type === 'tool_call_delta') {
|
|
1278
|
+
throw new Error('最终表达阶段返回了非预期工具调用,未执行该工具。');
|
|
1279
|
+
} else if (event.type === 'done') {
|
|
1280
|
+
publicationDone = true;
|
|
1281
|
+
lastFinishReason = event.stopReason;
|
|
1282
|
+
if (event.stopReason === 'aborted') publicationInterrupted = true;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (!publicationDone && !publicationInterrupted) throw new Error('最终答案流未正常结束。');
|
|
1286
|
+
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length') throw new Error('最终答案流缺少有效的结束状态。');
|
|
1287
|
+
if (!publishedText.trim() && !publicationInterrupted) throw new Error('最终表达未返回正文。');
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || (error as Error)?.name === 'AbortError';
|
|
1290
|
+
publicationError = error;
|
|
1291
|
+
}
|
|
1292
|
+
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
1293
|
+
yield { type: 'warning', message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。' };
|
|
1294
|
+
yield { type: 'text', delta: candidate };
|
|
1295
|
+
streamed = true;
|
|
1296
|
+
publishedText = candidate;
|
|
1297
|
+
lastFinishReason = candidateFinishReason;
|
|
1298
|
+
publicationError = undefined;
|
|
1299
|
+
}
|
|
1300
|
+
assistantText = publishedText;
|
|
1301
|
+
assistantMsg.content = publishedText || null;
|
|
1302
|
+
if (!publishedText) {
|
|
1303
|
+
if (publicationInterrupted) yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1304
|
+
else yield { type: 'error', code: 'llm_error', error: `最终答案生成失败:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
assistantMsg.presentation = 'final';
|
|
1309
|
+
trace('final_published');
|
|
1165
1310
|
if (assistantMsg.role === 'assistant') assistantMsg.advisorTaskVersion = advisorCoordinator.versions.taskVersion;
|
|
1166
1311
|
assistantMsg.advisorCheckpoints = advisorCoordinator.drainDegradations();
|
|
1167
1312
|
history.push(assistantMsg);
|
|
1313
|
+
// Non-interactive consumers retain atomic output. Interactive hosts
|
|
1314
|
+
// already received real deltas; commit the identical text only once.
|
|
1315
|
+
if (!streamed) yield { type: 'text', delta: assistantText };
|
|
1168
1316
|
yield {
|
|
1169
1317
|
type: 'assistant_message',
|
|
1170
1318
|
message: assistantMsg as Extract<Message, { role: 'assistant' }>,
|
|
1171
1319
|
};
|
|
1320
|
+
if (publicationInterrupted || publicationError) {
|
|
1321
|
+
yield { type: 'warning', message: '最终答案未完整生成,已保留已输出内容;未撤回或重新生成。' };
|
|
1322
|
+
if (publicationInterrupted) yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1323
|
+
else yield { type: 'error', code: 'llm_error', error: `最终答案流中断:${String(publicationError)}`, usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1172
1326
|
}
|
|
1173
1327
|
// R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
|
|
1174
1328
|
// 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
|
|
@@ -1220,7 +1374,11 @@ export async function* runQuery(
|
|
|
1220
1374
|
...turnUsage,
|
|
1221
1375
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
1222
1376
|
};
|
|
1223
|
-
if (
|
|
1377
|
+
if (emptyResponse && !resultPresented) {
|
|
1378
|
+
const warning = '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1379
|
+
yield { type: 'warning', message: warning };
|
|
1380
|
+
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1381
|
+
} else if (pendingDelegations.length) {
|
|
1224
1382
|
const warning = `必要子任务尚未收口:${pendingDelegations.map((r) => r.id).join(', ')}。本轮不能视为全部完成。`;
|
|
1225
1383
|
yield { type: 'warning', message: warning };
|
|
1226
1384
|
yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning, usage };
|
|
@@ -1240,11 +1398,14 @@ export async function* runQuery(
|
|
|
1240
1398
|
usage,
|
|
1241
1399
|
};
|
|
1242
1400
|
} else {
|
|
1243
|
-
yield { type: 'turn_done', completionStatus: 'completed', usage
|
|
1401
|
+
yield { type: 'turn_done', completionStatus: 'completed', usage,
|
|
1402
|
+
...(emptyResponse ? { warning: '成果已提交,未生成文字总结;请查看已提交的成果。' } : {}) };
|
|
1244
1403
|
}
|
|
1245
1404
|
return;
|
|
1246
1405
|
}
|
|
1247
1406
|
|
|
1407
|
+
assistantMsg.presentation = 'progress';
|
|
1408
|
+
trace('progress_published');
|
|
1248
1409
|
history.push(assistantMsg);
|
|
1249
1410
|
yield {
|
|
1250
1411
|
type: 'assistant_message',
|
|
@@ -1471,7 +1632,19 @@ export async function* runQuery(
|
|
|
1471
1632
|
};
|
|
1472
1633
|
|
|
1473
1634
|
const runOne: typeof runOneImpl = async (tc, reportProgress) => {
|
|
1635
|
+
// Also settle denied/invalid/unavailable calls that never reached the
|
|
1636
|
+
// provider adapter. This records a real attempt, not an ignored reminder.
|
|
1637
|
+
let consultation;
|
|
1638
|
+
if (tc.function.name === 'Advisor') {
|
|
1639
|
+
let reason: import('./types.js').AdvisorReason | undefined;
|
|
1640
|
+
try { reason = JSON.parse(tc.function.arguments).reason; } catch { /* validation handles malformed input */ }
|
|
1641
|
+
consultation = advisorCoordinator.beginConsultation(reason);
|
|
1642
|
+
}
|
|
1474
1643
|
const outcome = await runOneImpl(tc, reportProgress);
|
|
1644
|
+
if (consultation && outcome.result.metadata?.kind !== 'advisor_consultation'
|
|
1645
|
+
&& !( !outcome.result.ok && outcome.result.error === 'aborted')) {
|
|
1646
|
+
outcome.result = { ...outcome.result, metadata: advisorCoordinator.finishConsultation(consultation, outcome.result.ok) };
|
|
1647
|
+
}
|
|
1475
1648
|
const metadata = outcome.result.metadata;
|
|
1476
1649
|
if (metadata?.kind === 'advisor_decision' && outcome.result.ok
|
|
1477
1650
|
|| metadata?.kind === 'advisor_consultation') {
|
|
@@ -1788,6 +1961,7 @@ export function buildModelMessages(
|
|
|
1788
1961
|
transientContext?: string,
|
|
1789
1962
|
currentUserMessageId?: string,
|
|
1790
1963
|
): Message[] {
|
|
1964
|
+
history = history.filter(message => !(message.role === 'system' && message._ephemeral));
|
|
1791
1965
|
const transient = transientContext?.trim();
|
|
1792
1966
|
if (!transient) return history;
|
|
1793
1967
|
let index = currentUserMessageId
|
package/src/memory/fileStore.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import lockfile from 'proper-lockfile';
|
|
3
2
|
import { chmod, mkdir, open, readFile, rename, rm, stat, } from 'node:fs/promises';
|
|
4
3
|
import { dirname, join } from 'node:path';
|
|
5
|
-
import {
|
|
4
|
+
import { acquireStateFileLock } from '../runtime/fileLock.js';
|
|
6
5
|
import { agentStateDir } from '../context/agentHome.js';
|
|
7
6
|
import { CATEGORY_DIR, MEMORY_ID_PATTERN } from './constants.js';
|
|
8
|
-
const LOCK_STALE_MS = 120_000;
|
|
9
|
-
const LOCK_WAIT_MS = 15_000;
|
|
10
7
|
let mutationQueue = Promise.resolve();
|
|
11
8
|
export function memoryRootDir() {
|
|
12
9
|
return join(agentStateDir(), 'memory');
|
|
@@ -54,36 +51,7 @@ export async function pathExists(path) {
|
|
|
54
51
|
export async function acquireFileLock(path, signal) {
|
|
55
52
|
signal?.throwIfAborted();
|
|
56
53
|
await ensurePrivateDir(dirname(path));
|
|
57
|
-
|
|
58
|
-
while (true) {
|
|
59
|
-
signal?.throwIfAborted();
|
|
60
|
-
try {
|
|
61
|
-
// Do not steal old-version file locks. Restart old hosts before upgrading;
|
|
62
|
-
// their locks must be reviewed rather than aged out under a live writer.
|
|
63
|
-
const legacy = await stat(`${path}.lock`).catch(error => {
|
|
64
|
-
if (error.code === 'ENOENT')
|
|
65
|
-
return null;
|
|
66
|
-
throw error;
|
|
67
|
-
});
|
|
68
|
-
if (legacy?.isFile())
|
|
69
|
-
throw new Error('MEMORY_LEGACY_LOCK:请停止旧版本宿主并检查遗留记忆锁');
|
|
70
|
-
const release = await lockfile.lock(path, {
|
|
71
|
-
realpath: false, retries: 0, stale: LOCK_STALE_MS, update: 10_000,
|
|
72
|
-
});
|
|
73
|
-
if (signal?.aborted) {
|
|
74
|
-
await release();
|
|
75
|
-
signal.throwIfAborted();
|
|
76
|
-
}
|
|
77
|
-
return release;
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
if (error.code !== 'ELOCKED')
|
|
81
|
-
throw error;
|
|
82
|
-
if (Date.now() >= deadline)
|
|
83
|
-
throw new Error('等待记忆文件锁超时');
|
|
84
|
-
await delay(40, undefined, { signal });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
54
|
+
return acquireStateFileLock(path, { signal, label: 'MEMORY' });
|
|
87
55
|
}
|
|
88
56
|
export async function atomicWriteFile(path, content) {
|
|
89
57
|
await ensurePrivateDir(dirname(path));
|
package/src/memory/fileStore.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import lockfile from 'proper-lockfile';
|
|
3
2
|
import {
|
|
4
3
|
chmod,
|
|
5
4
|
mkdir,
|
|
@@ -10,14 +9,12 @@ import {
|
|
|
10
9
|
stat,
|
|
11
10
|
} from 'node:fs/promises';
|
|
12
11
|
import { dirname, join } from 'node:path';
|
|
13
|
-
import {
|
|
12
|
+
import { acquireStateFileLock } from '../runtime/fileLock.js';
|
|
14
13
|
|
|
15
14
|
import { agentStateDir } from '../context/agentHome.js';
|
|
16
15
|
import { CATEGORY_DIR, MEMORY_ID_PATTERN } from './constants.js';
|
|
17
16
|
import type { MemoryRecord, MemoryScope } from './types.js';
|
|
18
17
|
|
|
19
|
-
const LOCK_STALE_MS = 120_000;
|
|
20
|
-
const LOCK_WAIT_MS = 15_000;
|
|
21
18
|
let mutationQueue: Promise<void> = Promise.resolve();
|
|
22
19
|
|
|
23
20
|
export function memoryRootDir(): string {
|
|
@@ -84,28 +81,7 @@ export async function acquireFileLock(
|
|
|
84
81
|
): Promise<() => Promise<void>> {
|
|
85
82
|
signal?.throwIfAborted();
|
|
86
83
|
await ensurePrivateDir(dirname(path));
|
|
87
|
-
|
|
88
|
-
while (true) {
|
|
89
|
-
signal?.throwIfAborted();
|
|
90
|
-
try {
|
|
91
|
-
// Do not steal old-version file locks. Restart old hosts before upgrading;
|
|
92
|
-
// their locks must be reviewed rather than aged out under a live writer.
|
|
93
|
-
const legacy = await stat(`${path}.lock`).catch(error => {
|
|
94
|
-
if (error.code === 'ENOENT') return null;
|
|
95
|
-
throw error;
|
|
96
|
-
});
|
|
97
|
-
if (legacy?.isFile()) throw new Error('MEMORY_LEGACY_LOCK:请停止旧版本宿主并检查遗留记忆锁');
|
|
98
|
-
const release = await lockfile.lock(path, {
|
|
99
|
-
realpath: false, retries: 0, stale: LOCK_STALE_MS, update: 10_000,
|
|
100
|
-
});
|
|
101
|
-
if (signal?.aborted) { await release(); signal.throwIfAborted(); }
|
|
102
|
-
return release;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if ((error as NodeJS.ErrnoException).code !== 'ELOCKED') throw error;
|
|
105
|
-
if (Date.now() >= deadline) throw new Error('等待记忆文件锁超时');
|
|
106
|
-
await delay(40, undefined, { signal });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
84
|
+
return acquireStateFileLock(path, { signal, label: 'MEMORY' });
|
|
109
85
|
}
|
|
110
86
|
|
|
111
87
|
export async function atomicWriteFile(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CODE_REVIEW_RUBRIC } from './codeReview.js';
|
|
2
2
|
import { capabilityForToolName, } from '../tools/capabilities.js';
|
|
3
3
|
import { skillInvokeInstruction } from './skillList.js';
|
|
4
|
+
import { visualPresentation } from './visualPresentation.js';
|
|
4
5
|
export function capabilitiesForTools(tools) {
|
|
5
6
|
const result = new Set();
|
|
6
7
|
for (const tool of tools) {
|
|
@@ -193,10 +194,13 @@ const CAPABILITY_POLICIES = [
|
|
|
193
194
|
render: ({ toolNames }) => `- 需求匹配已安装 Skill 或用户点名时,${skillInvokeInstruction(toolNames.has('Skill'))};前端开发与交互修复也要主动匹配设计技能,不等用户说“设计”。引用资源按需读取,多阶段按需组合,不预读整个目录或加载无关技能。`,
|
|
194
195
|
},
|
|
195
196
|
{
|
|
196
|
-
id: 'execution.desktop-diagrams', layer: 'execution', profiles:
|
|
197
|
-
render: ({ surface, toolNames }) =>
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
198
|
+
render: ({ surface, output, toolNames }) => [
|
|
199
|
+
visualPresentation(output ?? surface),
|
|
200
|
+
toolNames.has('Skill') || toolNames.has('Read')
|
|
201
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
202
|
+
: '',
|
|
203
|
+
].filter(Boolean).join('\n'),
|
|
200
204
|
},
|
|
201
205
|
{
|
|
202
206
|
id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
|
|
@@ -246,12 +250,12 @@ const CAPABILITY_POLICIES = [
|
|
|
246
250
|
requiresTools: ['Advisor'],
|
|
247
251
|
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
248
252
|
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
249
|
-
render: ({ toolNames }) => `- Advisor
|
|
250
|
-
|
|
251
|
-
|
|
253
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
|
|
254
|
+
咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
|
|
255
|
+
文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
|
|
256
|
+
同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
|
|
252
257
|
不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
|
|
253
|
-
|
|
254
|
-
提供关键疑点与必要现场,返回后先读结果再决定后续行动;同批暂缓调用不是执行证据,不因此重复咨询。`,
|
|
258
|
+
以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
|
|
255
259
|
},
|
|
256
260
|
{
|
|
257
261
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type CapabilityId,
|
|
7
7
|
} from '../tools/capabilities.js';
|
|
8
8
|
import { skillInvokeInstruction } from './skillList.js';
|
|
9
|
+
import { visualPresentation } from './visualPresentation.js';
|
|
10
|
+
import type { OutputContract } from '../runtime/outputContract.js';
|
|
9
11
|
|
|
10
12
|
export type PromptPolicyLayer =
|
|
11
13
|
| 'authority'
|
|
@@ -31,6 +33,7 @@ export type { CapabilityId } from '../tools/capabilities.js';
|
|
|
31
33
|
export interface StablePromptContext {
|
|
32
34
|
profile: AgentProfile;
|
|
33
35
|
surface: AgentSurface;
|
|
36
|
+
output?: OutputContract;
|
|
34
37
|
cwd: string;
|
|
35
38
|
capabilities: ReadonlySet<CapabilityId>;
|
|
36
39
|
toolNames: ReadonlySet<string>;
|
|
@@ -240,10 +243,13 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
240
243
|
render: ({ toolNames }) => `- 需求匹配已安装 Skill 或用户点名时,${skillInvokeInstruction(toolNames.has('Skill'))};前端开发与交互修复也要主动匹配设计技能,不等用户说“设计”。引用资源按需读取,多阶段按需组合,不预读整个目录或加载无关技能。`,
|
|
241
244
|
},
|
|
242
245
|
{
|
|
243
|
-
id: 'execution.desktop-diagrams', layer: 'execution', profiles:
|
|
244
|
-
render: ({ surface, toolNames }) =>
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
247
|
+
render: ({ surface, output, toolNames }) => [
|
|
248
|
+
visualPresentation(output ?? surface),
|
|
249
|
+
toolNames.has('Skill') || toolNames.has('Read')
|
|
250
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
251
|
+
: '',
|
|
252
|
+
].filter(Boolean).join('\n'),
|
|
247
253
|
},
|
|
248
254
|
{
|
|
249
255
|
id: 'capability.session-worktree', layer: 'capability', profiles: MAIN_PROFILE, source: '06',
|
|
@@ -293,12 +299,12 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
293
299
|
requiresTools: ['Advisor'],
|
|
294
300
|
// 注意:文本内不得硬编码其他工具名(如 PresentResult),否则该工具缺席时这里
|
|
295
301
|
// 成为唯一残留的「幽灵引用」——capability 规则交叉引用必须在 toolNames 条件下拼接。
|
|
296
|
-
render: ({ toolNames }) => `- Advisor
|
|
297
|
-
|
|
298
|
-
|
|
302
|
+
render: ({ toolNames }) => `- Advisor(顾问模型)使用规则:提供第二意见,由主 Agent 核实取舍;不授予权限、不替用户验收。
|
|
303
|
+
咨询时机:实质计划执行前(plan_review)、实质交付前(delivery_review)、同一问题两次或以上处理失败后(stalled)、用户明显不满意(user_dissatisfaction)、非常不确定或无法解决(uncertainty)、关键证据冲突无法消解(evidence_conflict)。换工具不清零失败次数,无关失败不合并。
|
|
304
|
+
文字调研、计算、比较和推荐也是交付;自信、没文件不豁免。自动咨询不再问用户;寒暄、进度、拼写修正无需咨询。
|
|
305
|
+
同阶段不重复问;成果、假设实质变化或新冲突才重开。questions/note 指明疑点与证据 ID,历史由宿主附带。缺材料先补查,不算验证通过。读完意见再操作;暂缓操作未执行,提醒不算咨询。
|
|
299
306
|
不设咨询配额,每次咨询只尝试一次;失败、空答或超时自行降级继续,不转成人工审批,也不宣称获得验证。
|
|
300
|
-
|
|
301
|
-
提供关键疑点与必要现场,返回后先读结果再决定后续行动;同批暂缓调用不是执行证据,不因此重复咨询。`,
|
|
307
|
+
以代码、测试和用户要求核实意见,${toolNames.has('AdvisorDecision') ? '可用 AdvisorDecision 记录 accept/partial/reject;登记失败不阻止执行。' : '直接说明采纳判断。'}顾问不是事实来源,冲突以实测为准。`,
|
|
302
308
|
},
|
|
303
309
|
{
|
|
304
310
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|