minimal-agent 0.18.7 → 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/src/context/persistContext.js +5 -1
- package/src/context/persistContext.ts +5 -2
- package/src/loop.js +42 -20
- package/src/loop.ts +39 -18
- package/src/memory/fileStore.js +2 -34
- package/src/memory/fileStore.ts +2 -26
- package/src/prompts/policyRegistry.js +4 -8
- package/src/prompts/policyRegistry.ts +6 -8
- 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/fileLock.js +75 -0
- package/src/runtime/fileLock.ts +73 -0
- package/src/runtime/finalAnswer.js +4 -2
- package/src/runtime/finalAnswer.ts +6 -2
- 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/runtimeStore.js +11 -28
- package/src/session/runtimeStore.ts +9 -23
- package/src/tools/agent/agent.js +7 -5
- package/src/tools/agent/agent.ts +7 -4
- 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 +4 -0
- package/src/web/protocol.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.8",
|
|
4
4
|
"description": "Bun-first 的本地 Agent 内核,支持工具、插件、工作流、上下文压缩、长期记忆与 OpenAI 兼容接口;Tauri 桌面宿主和 Node.js npm 发布可复用同一内核",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Bill Wang <leiwang0359@gmail.com>",
|
|
@@ -91,7 +91,7 @@ export function filterEphemeralMessages(messages) {
|
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
93
|
* 保存当前 messages 为"上下文文件",覆盖之前内容。
|
|
94
|
-
*
|
|
94
|
+
* 自动保存默认尽力写入;显式用户操作使用 strict,在写失败时抛错。
|
|
95
95
|
*
|
|
96
96
|
* v2.x 新增:写盘前过滤掉 _ephemeral=true 的 system 消息(UI 临时提示,
|
|
97
97
|
* 不应进入持久化历史)。
|
|
@@ -116,6 +116,10 @@ export async function saveContext(messages, file, options) {
|
|
|
116
116
|
await rename(temp, target);
|
|
117
117
|
}
|
|
118
118
|
catch (e) {
|
|
119
|
+
// Explicit user mutations must not report success when persistence failed.
|
|
120
|
+
// Ordinary turn autosave retains its existing best-effort behavior.
|
|
121
|
+
if (options?.strict)
|
|
122
|
+
throw e;
|
|
119
123
|
// 不抛(持久化失败不该影响对话本身),但要出声——磁盘满/权限问题时
|
|
120
124
|
// 用户整段会话会静默丢失;与 clearContext 的 archive 失败处理风格对齐。
|
|
121
125
|
try {
|
|
@@ -101,12 +101,12 @@ export function filterEphemeralMessages(messages: Message[]): Message[] {
|
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* 保存当前 messages 为"上下文文件",覆盖之前内容。
|
|
104
|
-
*
|
|
104
|
+
* 自动保存默认尽力写入;显式用户操作使用 strict,在写失败时抛错。
|
|
105
105
|
*
|
|
106
106
|
* v2.x 新增:写盘前过滤掉 _ephemeral=true 的 system 消息(UI 临时提示,
|
|
107
107
|
* 不应进入持久化历史)。
|
|
108
108
|
*/
|
|
109
|
-
export async function saveContext(messages: Message[], file?: string, options?: { replaceDisplayHistory?: boolean }): Promise<void> {
|
|
109
|
+
export async function saveContext(messages: Message[], file?: string, options?: { replaceDisplayHistory?: boolean; strict?: boolean }): Promise<void> {
|
|
110
110
|
const target = file ?? getContextPath();
|
|
111
111
|
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
112
112
|
try {
|
|
@@ -122,6 +122,9 @@ export async function saveContext(messages: Message[], file?: string, options?:
|
|
|
122
122
|
await writeFile(temp, JSON.stringify(data), { encoding: 'utf8', mode: 0o600 });
|
|
123
123
|
await rename(temp, target);
|
|
124
124
|
} catch (e) {
|
|
125
|
+
// Explicit user mutations must not report success when persistence failed.
|
|
126
|
+
// Ordinary turn autosave retains its existing best-effort behavior.
|
|
127
|
+
if (options?.strict) throw e;
|
|
125
128
|
// 不抛(持久化失败不该影响对话本身),但要出声——磁盘满/权限问题时
|
|
126
129
|
// 用户整段会话会静默丢失;与 clearContext 的 archive 失败处理风格对齐。
|
|
127
130
|
try {
|
package/src/loop.js
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
import crypto from 'node:crypto';
|
|
30
30
|
import { answerPreparationNotice } from './runtime/answerPreparation.js';
|
|
31
31
|
import { finalAnswerMessages } from './runtime/finalAnswer.js';
|
|
32
|
+
import { resolveOutputContract, toolsForResponsePhase } from './runtime/outputContract.js';
|
|
33
|
+
import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
|
|
32
34
|
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
33
35
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
34
36
|
import { dirname, resolve } from 'node:path';
|
|
@@ -238,6 +240,8 @@ export async function* runQuery(userInput, options) {
|
|
|
238
240
|
let presentedResultUnverified = false;
|
|
239
241
|
let deliveryCorrectionAttempted = false;
|
|
240
242
|
let askCorrectionAttempted = false;
|
|
243
|
+
let diagramPreparationAttempted = false;
|
|
244
|
+
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
241
245
|
const subagents = await configService.subagents();
|
|
242
246
|
await hydrateSessionWorktree({
|
|
243
247
|
sessionState,
|
|
@@ -246,6 +250,8 @@ export async function* runQuery(userInput, options) {
|
|
|
246
250
|
cwd: runtimeContext.cwd,
|
|
247
251
|
});
|
|
248
252
|
const toolCallContext = {
|
|
253
|
+
output,
|
|
254
|
+
availableToolNames: () => currentEffectiveToolSet.names,
|
|
249
255
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
250
256
|
history,
|
|
251
257
|
sessionState,
|
|
@@ -320,6 +326,7 @@ export async function* runQuery(userInput, options) {
|
|
|
320
326
|
const parts = [
|
|
321
327
|
transientFor(activeTools),
|
|
322
328
|
coordinationText,
|
|
329
|
+
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
323
330
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
324
331
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
325
332
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
@@ -737,7 +744,7 @@ export async function* runQuery(userInput, options) {
|
|
|
737
744
|
for await (const ev of chatFn({
|
|
738
745
|
provider,
|
|
739
746
|
messages: modelMessages,
|
|
740
|
-
tools: currentEffectiveToolSet.tools,
|
|
747
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'preparation'),
|
|
741
748
|
signal,
|
|
742
749
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
743
750
|
promptCache: options.promptCache,
|
|
@@ -896,6 +903,16 @@ export async function* runQuery(userInput, options) {
|
|
|
896
903
|
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
897
904
|
assistantText = '';
|
|
898
905
|
}
|
|
906
|
+
// Count the calls that survived review scheduling, including failed loads.
|
|
907
|
+
for (const call of toolCallsByIndex) {
|
|
908
|
+
if (call.function.name !== 'Skill')
|
|
909
|
+
continue;
|
|
910
|
+
try {
|
|
911
|
+
if (JSON.parse(call.function.arguments).name === 'dynamic-ui')
|
|
912
|
+
diagramPreparationAttempted = true;
|
|
913
|
+
}
|
|
914
|
+
catch { /* Ordinary tool validation reports malformed arguments. */ }
|
|
915
|
+
}
|
|
899
916
|
const assistantMsg = {
|
|
900
917
|
role: 'assistant',
|
|
901
918
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -909,6 +926,20 @@ export async function* runQuery(userInput, options) {
|
|
|
909
926
|
};
|
|
910
927
|
// 4. 没有工具调用 → 整轮交互结束
|
|
911
928
|
finalResponse: if (toolCallsByIndex.length === 0) {
|
|
929
|
+
// Repair only private preparation, through the ordinary tool pipeline.
|
|
930
|
+
// One attempt per turn, including missing/failed skills; never rewind a reply.
|
|
931
|
+
if (!diagramPreparationAttempted && !options.responseFormat
|
|
932
|
+
&& runtimeContext.profile === 'main' && output.diagrams.length > 0
|
|
933
|
+
&& currentEffectiveToolSet.names.has('Skill') && turn < maxTurns
|
|
934
|
+
&& needsDiagramPreparation(assistantText, userInput)) {
|
|
935
|
+
diagramPreparationAttempted = true;
|
|
936
|
+
toolCallsByIndex.push({ id: crypto.randomUUID(), type: 'function', function: {
|
|
937
|
+
name: 'Skill', arguments: JSON.stringify({ name: 'dynamic-ui' }),
|
|
938
|
+
} });
|
|
939
|
+
assistantMsg.content = null;
|
|
940
|
+
assistantMsg.tool_calls = toolCallsByIndex;
|
|
941
|
+
break finalResponse;
|
|
942
|
+
}
|
|
912
943
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
913
944
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
914
945
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
@@ -979,14 +1010,14 @@ export async function* runQuery(userInput, options) {
|
|
|
979
1010
|
let publicationInterrupted = false;
|
|
980
1011
|
let streamed = false;
|
|
981
1012
|
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
982
|
-
&&
|
|
1013
|
+
&& output.finalDelivery === 'stream') {
|
|
983
1014
|
const candidate = assistantText;
|
|
984
1015
|
const candidateFinishReason = lastFinishReason;
|
|
985
1016
|
let publishedText = '';
|
|
986
1017
|
let publicationDone = false;
|
|
987
1018
|
yield { type: 'stage_change', stage: 'streaming' };
|
|
988
1019
|
try {
|
|
989
|
-
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText);
|
|
1020
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText, output);
|
|
990
1021
|
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
991
1022
|
if (publicationPlan.overBudget)
|
|
992
1023
|
throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
@@ -998,7 +1029,7 @@ export async function* runQuery(userInput, options) {
|
|
|
998
1029
|
for await (const event of chatFn({
|
|
999
1030
|
provider,
|
|
1000
1031
|
messages: publicationMessages,
|
|
1001
|
-
tools:
|
|
1032
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'publication'), signal,
|
|
1002
1033
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1003
1034
|
promptCache: options.promptCache,
|
|
1004
1035
|
})) {
|
|
@@ -1015,11 +1046,8 @@ export async function* runQuery(userInput, options) {
|
|
|
1015
1046
|
break;
|
|
1016
1047
|
}
|
|
1017
1048
|
if (event.type === 'text_delta') {
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
if (!candidate.startsWith(publishedText + event.delta)) {
|
|
1021
|
-
throw new Error('发布内容偏离定稿,使用原定稿。');
|
|
1022
|
-
}
|
|
1049
|
+
// Final expression is live generation, not an exact-copy replay.
|
|
1050
|
+
// Once visible, text is never replaced or joined with a different draft.
|
|
1023
1051
|
publishedText += event.delta;
|
|
1024
1052
|
streamed = true;
|
|
1025
1053
|
yield { type: 'text', delta: event.delta };
|
|
@@ -1040,26 +1068,20 @@ export async function* runQuery(userInput, options) {
|
|
|
1040
1068
|
throw new Error('最终答案流缺少有效的结束状态。');
|
|
1041
1069
|
if (!publishedText.trim() && !publicationInterrupted)
|
|
1042
1070
|
throw new Error('最终表达未返回正文。');
|
|
1043
|
-
if (!publicationInterrupted && publishedText !== candidate)
|
|
1044
|
-
throw new Error('最终表达未完整传递定稿。');
|
|
1045
1071
|
}
|
|
1046
1072
|
catch (error) {
|
|
1047
1073
|
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || error?.name === 'AbortError';
|
|
1048
1074
|
publicationError = error;
|
|
1049
1075
|
}
|
|
1050
|
-
if (publicationError && !publicationInterrupted) {
|
|
1051
|
-
yield { type: 'warning', message: '
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
yield { type: 'text', delta: remaining };
|
|
1055
|
-
streamed = true;
|
|
1056
|
-
}
|
|
1076
|
+
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
1077
|
+
yield { type: 'warning', message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。' };
|
|
1078
|
+
yield { type: 'text', delta: candidate };
|
|
1079
|
+
streamed = true;
|
|
1057
1080
|
publishedText = candidate;
|
|
1081
|
+
lastFinishReason = candidateFinishReason;
|
|
1058
1082
|
publicationError = undefined;
|
|
1059
1083
|
}
|
|
1060
1084
|
assistantText = publishedText;
|
|
1061
|
-
if (!publicationInterrupted)
|
|
1062
|
-
lastFinishReason = candidateFinishReason;
|
|
1063
1085
|
assistantMsg.content = publishedText || null;
|
|
1064
1086
|
if (!publishedText) {
|
|
1065
1087
|
if (publicationInterrupted)
|
package/src/loop.ts
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
import crypto from 'node:crypto';
|
|
31
31
|
import { answerPreparationNotice } from './runtime/answerPreparation.js';
|
|
32
32
|
import { finalAnswerMessages } from './runtime/finalAnswer.js';
|
|
33
|
+
import { resolveOutputContract, toolsForResponsePhase } from './runtime/outputContract.js';
|
|
34
|
+
import { needsDiagramPreparation, visualPresentation } from './prompts/visualPresentation.js';
|
|
33
35
|
import { createLifecycleTrace } from './runtime/lifecycleTrace.js';
|
|
34
36
|
import { conciseToolPath } from './runtime/toolDisplay.js';
|
|
35
37
|
import { dirname, resolve } from 'node:path';
|
|
@@ -448,6 +450,8 @@ export async function* runQuery(
|
|
|
448
450
|
let presentedResultUnverified = false;
|
|
449
451
|
let deliveryCorrectionAttempted = false;
|
|
450
452
|
let askCorrectionAttempted = false;
|
|
453
|
+
let diagramPreparationAttempted = false;
|
|
454
|
+
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
451
455
|
const subagents = await configService.subagents();
|
|
452
456
|
await hydrateSessionWorktree({
|
|
453
457
|
sessionState,
|
|
@@ -456,6 +460,8 @@ export async function* runQuery(
|
|
|
456
460
|
cwd: runtimeContext.cwd,
|
|
457
461
|
});
|
|
458
462
|
const toolCallContext: ToolCallContext = {
|
|
463
|
+
output,
|
|
464
|
+
availableToolNames: () => currentEffectiveToolSet.names,
|
|
459
465
|
executionServices: { ...options.executionServices, executeTool: executeToolFn, chat: chatFn, invocationPolicy: options.invocationPolicy },
|
|
460
466
|
history,
|
|
461
467
|
sessionState,
|
|
@@ -533,6 +539,7 @@ export async function* runQuery(
|
|
|
533
539
|
const parts = [
|
|
534
540
|
transientFor(activeTools),
|
|
535
541
|
coordinationText,
|
|
542
|
+
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
536
543
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
537
544
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
538
545
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
@@ -966,7 +973,7 @@ export async function* runQuery(
|
|
|
966
973
|
for await (const ev of chatFn({
|
|
967
974
|
provider,
|
|
968
975
|
messages: modelMessages,
|
|
969
|
-
tools: currentEffectiveToolSet.tools,
|
|
976
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'preparation'),
|
|
970
977
|
signal,
|
|
971
978
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
972
979
|
promptCache: options.promptCache,
|
|
@@ -1124,6 +1131,13 @@ export async function* runQuery(
|
|
|
1124
1131
|
function: { name: 'Advisor', arguments: JSON.stringify(pendingReview) } });
|
|
1125
1132
|
assistantText = '';
|
|
1126
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
|
+
}
|
|
1127
1141
|
const assistantMsg: Message = {
|
|
1128
1142
|
role: 'assistant',
|
|
1129
1143
|
content: assistantText.length > 0 ? assistantText : null,
|
|
@@ -1137,6 +1151,20 @@ export async function* runQuery(
|
|
|
1137
1151
|
};
|
|
1138
1152
|
// 4. 没有工具调用 → 整轮交互结束
|
|
1139
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
|
+
}
|
|
1140
1168
|
if (!askCorrectionAttempted && runtimeContext.interactiveAsk
|
|
1141
1169
|
&& currentEffectiveToolSet.names.has('AskUserQuestion')
|
|
1142
1170
|
&& needsInteractiveQuestion(stripThinkBlocks(assistantText)) && turn < maxTurns) {
|
|
@@ -1206,14 +1234,14 @@ export async function* runQuery(
|
|
|
1206
1234
|
let publicationInterrupted = false;
|
|
1207
1235
|
let streamed = false;
|
|
1208
1236
|
if (!options.responseFormat && runtimeContext.profile === 'main'
|
|
1209
|
-
&&
|
|
1237
|
+
&& output.finalDelivery === 'stream') {
|
|
1210
1238
|
const candidate = assistantText;
|
|
1211
1239
|
const candidateFinishReason = lastFinishReason;
|
|
1212
1240
|
let publishedText = '';
|
|
1213
1241
|
let publicationDone = false;
|
|
1214
1242
|
yield { type: 'stage_change', stage: 'streaming' };
|
|
1215
1243
|
try {
|
|
1216
|
-
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText);
|
|
1244
|
+
const publicationMessages = finalAnswerMessages(buildModelMessages(history, requestTransientContext, userMessage.id), assistantText, output);
|
|
1217
1245
|
const publicationPlan = buildContextBudgetPlan({ provider, history: publicationMessages, tools: [] });
|
|
1218
1246
|
if (publicationPlan.overBudget) throw new Error('最终表达上下文超过模型输入上限,未发送请求。');
|
|
1219
1247
|
if (signal?.aborted || options.hasQueuedInput?.()) {
|
|
@@ -1224,7 +1252,7 @@ export async function* runQuery(
|
|
|
1224
1252
|
for await (const event of chatFn({
|
|
1225
1253
|
provider,
|
|
1226
1254
|
messages: publicationMessages,
|
|
1227
|
-
tools:
|
|
1255
|
+
tools: toolsForResponsePhase(currentEffectiveToolSet.tools, 'publication'), signal,
|
|
1228
1256
|
conversationMode: runtimeContext.conversationMode ?? 'private',
|
|
1229
1257
|
promptCache: options.promptCache,
|
|
1230
1258
|
})) {
|
|
@@ -1241,11 +1269,8 @@ export async function* runQuery(
|
|
|
1241
1269
|
break;
|
|
1242
1270
|
}
|
|
1243
1271
|
if (event.type === 'text_delta') {
|
|
1244
|
-
//
|
|
1245
|
-
//
|
|
1246
|
-
if (!candidate.startsWith(publishedText + event.delta)) {
|
|
1247
|
-
throw new Error('发布内容偏离定稿,使用原定稿。');
|
|
1248
|
-
}
|
|
1272
|
+
// Final expression is live generation, not an exact-copy replay.
|
|
1273
|
+
// Once visible, text is never replaced or joined with a different draft.
|
|
1249
1274
|
publishedText += event.delta;
|
|
1250
1275
|
streamed = true;
|
|
1251
1276
|
yield { type: 'text', delta: event.delta };
|
|
@@ -1260,23 +1285,19 @@ export async function* runQuery(
|
|
|
1260
1285
|
if (!publicationDone && !publicationInterrupted) throw new Error('最终答案流未正常结束。');
|
|
1261
1286
|
if (!publicationInterrupted && lastFinishReason !== 'stop' && lastFinishReason !== 'length') throw new Error('最终答案流缺少有效的结束状态。');
|
|
1262
1287
|
if (!publishedText.trim() && !publicationInterrupted) throw new Error('最终表达未返回正文。');
|
|
1263
|
-
if (!publicationInterrupted && publishedText !== candidate) throw new Error('最终表达未完整传递定稿。');
|
|
1264
1288
|
} catch (error) {
|
|
1265
1289
|
publicationInterrupted ||= signal?.aborted === true || options.hasQueuedInput?.() === true || (error as Error)?.name === 'AbortError';
|
|
1266
1290
|
publicationError = error;
|
|
1267
1291
|
}
|
|
1268
|
-
if (publicationError && !publicationInterrupted) {
|
|
1269
|
-
yield { type: 'warning', message: '
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
yield { type: 'text', delta: remaining };
|
|
1273
|
-
streamed = true;
|
|
1274
|
-
}
|
|
1292
|
+
if (publicationError && !publicationInterrupted && !publishedText) {
|
|
1293
|
+
yield { type: 'warning', message: '最终回答流未能开始,已一次性显示内部准备的答案;未进行新的事实核验。' };
|
|
1294
|
+
yield { type: 'text', delta: candidate };
|
|
1295
|
+
streamed = true;
|
|
1275
1296
|
publishedText = candidate;
|
|
1297
|
+
lastFinishReason = candidateFinishReason;
|
|
1276
1298
|
publicationError = undefined;
|
|
1277
1299
|
}
|
|
1278
1300
|
assistantText = publishedText;
|
|
1279
|
-
if (!publicationInterrupted) lastFinishReason = candidateFinishReason;
|
|
1280
1301
|
assistantMsg.content = publishedText || null;
|
|
1281
1302
|
if (!publishedText) {
|
|
1282
1303
|
if (publicationInterrupted) yield { ...interruption(), usage: { ...turnUsage, trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls } };
|
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) {
|
|
@@ -194,15 +195,10 @@ const CAPABILITY_POLICIES = [
|
|
|
194
195
|
},
|
|
195
196
|
{
|
|
196
197
|
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
197
|
-
render: ({ surface, toolNames }) => [
|
|
198
|
-
|
|
199
|
-
surface === 'desktop'
|
|
200
|
-
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
201
|
-
: surface === 'cli' || surface === 'tui'
|
|
202
|
-
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
203
|
-
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
198
|
+
render: ({ surface, output, toolNames }) => [
|
|
199
|
+
visualPresentation(output ?? surface),
|
|
204
200
|
toolNames.has('Skill') || toolNames.has('Read')
|
|
205
|
-
? '-
|
|
201
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
206
202
|
: '',
|
|
207
203
|
].filter(Boolean).join('\n'),
|
|
208
204
|
},
|
|
@@ -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>;
|
|
@@ -241,15 +244,10 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
241
244
|
},
|
|
242
245
|
{
|
|
243
246
|
id: 'execution.desktop-diagrams', layer: 'execution', profiles: ALL_PROFILES, source: 'native',
|
|
244
|
-
render: ({ surface, toolNames }) => [
|
|
245
|
-
|
|
246
|
-
surface === 'desktop'
|
|
247
|
-
? '- Desktop/Web 聊天支持 mermaid 和 svg 围栏,不用裸 HTML。'
|
|
248
|
-
: surface === 'cli' || surface === 'tui'
|
|
249
|
-
? '- CLI/TUI 直接回复用文字/表格/紧凑 ASCII,生成文件仍优先 Mermaid/SVG。'
|
|
250
|
-
: '- 跟随最终阅读载体,不等同于终端:Web 用 mermaid/svg 围栏;保持协议契约,未知不假定可渲染。',
|
|
247
|
+
render: ({ surface, output, toolNames }) => [
|
|
248
|
+
visualPresentation(output ?? surface),
|
|
251
249
|
toolNames.has('Skill') || toolNames.has('Read')
|
|
252
|
-
? '-
|
|
250
|
+
? '- 配图前加载可用的 dynamic-ui。'
|
|
253
251
|
: '',
|
|
254
252
|
].filter(Boolean).join('\n'),
|
|
255
253
|
},
|
|
@@ -235,17 +235,24 @@ async function buildAdvisorTransient(profile, tools) {
|
|
|
235
235
|
return '';
|
|
236
236
|
return buildAdvisorStatusLine();
|
|
237
237
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
/** One default contract for stable, transient and combined prompt entry points.
|
|
239
|
+
* Preserve explicit legacy fields at this compatibility boundary; omission never
|
|
240
|
+
* grants memory, browser or vision capabilities.
|
|
241
|
+
*/
|
|
242
|
+
function resolvePromptRuntime(options) {
|
|
243
|
+
return {
|
|
244
|
+
surface: (options.profile ?? 'main') === 'main' ? 'cli' : 'subagent',
|
|
242
245
|
conversationMode: 'private',
|
|
243
|
-
cwd,
|
|
246
|
+
cwd: options.cwd,
|
|
244
247
|
interactiveAsk: true,
|
|
245
248
|
supervisedBrowser: false,
|
|
246
249
|
ownerLocalSession: false,
|
|
247
250
|
...options.runtime,
|
|
248
251
|
};
|
|
252
|
+
}
|
|
253
|
+
export async function assemblePromptEnvelope(options) {
|
|
254
|
+
const { cwd, tools, profile = 'main', includeProjectInstructions = true, includePlugins = profile === 'main', } = options;
|
|
255
|
+
const runtime = resolvePromptRuntime(options);
|
|
249
256
|
const { base, capabilities, stableContext } = await buildStableBase(cwd, tools, profile, runtime);
|
|
250
257
|
const contextBundle = await buildTransientBundle(runtime, profile, tools, {
|
|
251
258
|
includeProjectInstructions,
|
|
@@ -273,6 +280,7 @@ async function buildStableBase(cwd, tools, profile, runtime) {
|
|
|
273
280
|
const stableContext = {
|
|
274
281
|
profile,
|
|
275
282
|
surface: runtime.surface,
|
|
283
|
+
output: runtime.output,
|
|
276
284
|
cwd,
|
|
277
285
|
capabilities,
|
|
278
286
|
toolNames: new Set(tools.map((tool) => tool.name)),
|
|
@@ -289,19 +297,12 @@ async function buildStableBase(cwd, tools, profile, runtime) {
|
|
|
289
297
|
/** Build only the cache-stable layer; performs no memory or project scans. */
|
|
290
298
|
export async function assembleStablePrompt(options) {
|
|
291
299
|
const profile = options.profile ?? 'main';
|
|
292
|
-
const runtime =
|
|
293
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
294
|
-
conversationMode: 'private',
|
|
295
|
-
cwd: options.cwd,
|
|
296
|
-
interactiveAsk: true,
|
|
297
|
-
supervisedBrowser: false,
|
|
298
|
-
ownerLocalSession: false,
|
|
299
|
-
...options.runtime,
|
|
300
|
-
};
|
|
300
|
+
const runtime = resolvePromptRuntime(options);
|
|
301
301
|
const key = [
|
|
302
302
|
options.cwd,
|
|
303
303
|
profile,
|
|
304
304
|
runtime.surface,
|
|
305
|
+
JSON.stringify(runtime.output ?? null),
|
|
305
306
|
runtime.conversationMode,
|
|
306
307
|
createHash('sha256')
|
|
307
308
|
.update(JSON.stringify(options.tools.map((tool) => ({
|
|
@@ -544,15 +545,7 @@ export async function buildTransientContext(options) {
|
|
|
544
545
|
/** Structured counterpart used by the Harness for request-time budget reduction. */
|
|
545
546
|
export async function buildTransientPromptBundle(options) {
|
|
546
547
|
const profile = options.profile ?? 'main';
|
|
547
|
-
const runtime =
|
|
548
|
-
surface: profile === 'main' ? 'cli' : 'subagent',
|
|
549
|
-
conversationMode: 'private',
|
|
550
|
-
cwd: options.cwd,
|
|
551
|
-
interactiveAsk: true,
|
|
552
|
-
supervisedBrowser: false,
|
|
553
|
-
ownerLocalSession: false,
|
|
554
|
-
...options.runtime,
|
|
555
|
-
};
|
|
548
|
+
const runtime = resolvePromptRuntime(options);
|
|
556
549
|
return buildTransientBundle(runtime, profile, options.tools, {
|
|
557
550
|
includeProjectInstructions: options.includeProjectInstructions ?? true,
|
|
558
551
|
includePlugins: options.includePlugins ?? profile === 'main',
|