minimal-agent 0.18.8 → 0.18.9
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/loop.js +33 -5
- package/src/loop.ts +33 -5
- package/src/memory/factLearning.js +3 -3
- package/src/memory/factLearning.ts +3 -7
- package/src/memory/jobPolicy.js +21 -13
- package/src/memory/jobPolicy.ts +22 -11
- package/src/memory/learning.js +3 -3
- package/src/memory/learning.ts +3 -3
- package/src/memory/learningModel.js +4 -2
- package/src/memory/learningModel.ts +4 -2
- package/src/prompts/delegation.js +15 -0
- package/src/prompts/delegation.ts +20 -0
- package/src/prompts/policyRegistry.js +3 -2
- package/src/prompts/policyRegistry.ts +3 -2
- package/src/tools/agent/agent.js +2 -1
- package/src/tools/agent/agent.ts +2 -1
- package/src/tools/memory/maintenance.js +2 -0
- package/src/tools/memory/maintenance.ts +2 -0
- package/src/tools/memory/memory.js +58 -14
- package/src/tools/memory/memory.ts +57 -13
- package/src/tools/memory/policy.js +16 -1
- package/src/tools/memory/policy.ts +16 -1
- package/src/tools/toolPromptGuides.js +4 -3
- package/src/tools/toolPromptGuides.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minimal-agent",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.9",
|
|
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>",
|
package/src/loop.js
CHANGED
|
@@ -69,6 +69,7 @@ import { createAgentRuntimeContext, } from './runtime/agentRuntimeContext.js';
|
|
|
69
69
|
import { stripThinkBlocks } from './utils/stripThink.js';
|
|
70
70
|
import { orderedPool } from './runtime/orderedPool.js';
|
|
71
71
|
import { takeAgentNotifications, agentCoordinationContext } from './tools/agent/agentTaskService.js';
|
|
72
|
+
import { delegationRuntimeGuidance } from './prompts/delegation.js';
|
|
72
73
|
import { executionSegments } from './tools/executionSegments.js';
|
|
73
74
|
import { acquireSubagentLease, normalizeWriteScopes } from './tools/agent/coordinator.js';
|
|
74
75
|
const DELIVERY_ARTIFACT = '(?:文件|合同|协议|方案|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
|
|
@@ -239,6 +240,8 @@ export async function* runQuery(userInput, options) {
|
|
|
239
240
|
let resultPresented = false;
|
|
240
241
|
let presentedResultUnverified = false;
|
|
241
242
|
let deliveryCorrectionAttempted = false;
|
|
243
|
+
let emptyResponseRecoveryAttempted = false;
|
|
244
|
+
let emptyResponseRecoveryReason = '';
|
|
242
245
|
let askCorrectionAttempted = false;
|
|
243
246
|
let diagramPreparationAttempted = false;
|
|
244
247
|
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
@@ -326,9 +329,13 @@ export async function* runQuery(userInput, options) {
|
|
|
326
329
|
const parts = [
|
|
327
330
|
transientFor(activeTools),
|
|
328
331
|
coordinationText,
|
|
332
|
+
delegationRuntimeGuidance({ available: runtimeContext.profile === 'main' && currentEffectiveToolSet.names.has('Agent'),
|
|
333
|
+
maxConcurrent: subagents.maxConcurrent, callsUsed: toolCallContext.agentCalls?.used ?? 0,
|
|
334
|
+
maxCalls: toolCallContext.agentCalls?.max ?? 0 }),
|
|
329
335
|
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
330
336
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
331
337
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
338
|
+
emptyResponseRecoveryAttempted ? `上一次模型响应没有可交付正文(${emptyResponseRecoveryReason})。请基于已有历史和工具结果继续完成任务:信息充分则给出实质结论,不充分则继续必要操作或明确缺口。不要重复已经完成的调查或有副作用的操作;不要把未完成的任务说成完成。已有顾问意见继续有效,无需仅因恢复而再次咨询。` : '',
|
|
332
339
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
333
340
|
advisorAvailable ? advisorCoordinator.transientReminder() : '',
|
|
334
341
|
advisorAvailable
|
|
@@ -692,16 +699,24 @@ export async function* runQuery(userInput, options) {
|
|
|
692
699
|
requiredNames: [...effectiveToolSet.requiredNames],
|
|
693
700
|
});
|
|
694
701
|
advisorCoordinator.setAvailable(currentEffectiveToolSet.names.has('Advisor'));
|
|
702
|
+
// Tool availability may change during emergency fitting. Rebuild dynamic
|
|
703
|
+
// guidance against the actual request boundary, then account for its size.
|
|
704
|
+
requestTransientContext = buildRequestTransientContext();
|
|
705
|
+
lastRequestTransientContext = requestTransientContext;
|
|
706
|
+
const finalRequestBudget = buildContextBudgetPlan({
|
|
707
|
+
provider, history, tools: currentEffectiveToolSet.tools,
|
|
708
|
+
transientContext: requestTransientContext,
|
|
709
|
+
});
|
|
695
710
|
if (emergencyFit.removedNames.length > 0) {
|
|
696
711
|
yield {
|
|
697
712
|
type: 'warning',
|
|
698
713
|
message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
|
|
699
714
|
};
|
|
700
715
|
}
|
|
701
|
-
if (
|
|
716
|
+
if (finalRequestBudget.overBudget) {
|
|
702
717
|
yield {
|
|
703
718
|
type: 'error',
|
|
704
|
-
error: `当前请求即使经过上下文降级仍需要约 ${
|
|
719
|
+
error: `当前请求即使经过上下文降级仍需要约 ${finalRequestBudget.totalInputTokens} tokens,超过模型输入上限 ${finalRequestBudget.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
|
|
705
720
|
code: 'context_overflow',
|
|
706
721
|
};
|
|
707
722
|
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('error') };
|
|
@@ -999,8 +1014,19 @@ export async function* runQuery(userInput, options) {
|
|
|
999
1014
|
yield { type: 'warning', message: '已达到本轮执行上限,顾问交付检查尚未完成;不能把此次输出视为已复核。' };
|
|
1000
1015
|
}
|
|
1001
1016
|
if (emptyResponse) {
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
1017
|
+
// Recover once within the existing turn budget, retaining tool receipts and
|
|
1018
|
+
// advisor state. Never replay tools or expose an empty candidate as an answer.
|
|
1019
|
+
if (!resultPresented && !emptyResponseRecoveryAttempted && turn < maxTurns) {
|
|
1020
|
+
emptyResponseRecoveryAttempted = true;
|
|
1021
|
+
emptyResponseRecoveryReason = lastFinishReason === 'length'
|
|
1022
|
+
? '输出额度耗尽' : lastFinishReason === 'unknown'
|
|
1023
|
+
? '响应流缺少正常结束状态' : '响应为空或仅含思考内容';
|
|
1024
|
+
const message = { role: 'system', content: '正在恢复回答,已保留调查结果与工作进展。',
|
|
1025
|
+
id: crypto.randomUUID(), timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
1026
|
+
history.push(message);
|
|
1027
|
+
yield { type: 'injected_message', message };
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1004
1030
|
yield* retireDraft('empty_response');
|
|
1005
1031
|
}
|
|
1006
1032
|
else {
|
|
@@ -1162,7 +1188,9 @@ export async function* runQuery(userInput, options) {
|
|
|
1162
1188
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
1163
1189
|
};
|
|
1164
1190
|
if (emptyResponse && !resultPresented) {
|
|
1165
|
-
const warning =
|
|
1191
|
+
const warning = emptyResponseRecoveryAttempted
|
|
1192
|
+
? '自动恢复后仍未生成最终答复,已有进展已保留。可以继续本轮或检查模型服务。'
|
|
1193
|
+
: '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1166
1194
|
yield { type: 'warning', message: warning };
|
|
1167
1195
|
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1168
1196
|
}
|
package/src/loop.ts
CHANGED
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
import { stripThinkBlocks } from './utils/stripThink.js';
|
|
114
114
|
import { orderedPool } from './runtime/orderedPool.js';
|
|
115
115
|
import { takeAgentNotifications, agentCoordinationContext } from './tools/agent/agentTaskService.js';
|
|
116
|
+
import { delegationRuntimeGuidance } from './prompts/delegation.js';
|
|
116
117
|
import { executionSegments } from './tools/executionSegments.js';
|
|
117
118
|
import { acquireSubagentLease, normalizeWriteScopes } from './tools/agent/coordinator.js';
|
|
118
119
|
|
|
@@ -449,6 +450,8 @@ export async function* runQuery(
|
|
|
449
450
|
let resultPresented = false;
|
|
450
451
|
let presentedResultUnverified = false;
|
|
451
452
|
let deliveryCorrectionAttempted = false;
|
|
453
|
+
let emptyResponseRecoveryAttempted = false;
|
|
454
|
+
let emptyResponseRecoveryReason = '';
|
|
452
455
|
let askCorrectionAttempted = false;
|
|
453
456
|
let diagramPreparationAttempted = false;
|
|
454
457
|
const output = resolveOutputContract(runtimeContext.surface, runtimeContext.output);
|
|
@@ -539,9 +542,13 @@ export async function* runQuery(
|
|
|
539
542
|
const parts = [
|
|
540
543
|
transientFor(activeTools),
|
|
541
544
|
coordinationText,
|
|
545
|
+
delegationRuntimeGuidance({ available: runtimeContext.profile === 'main' && currentEffectiveToolSet.names.has('Agent'),
|
|
546
|
+
maxConcurrent: subagents.maxConcurrent, callsUsed: toolCallContext.agentCalls?.used ?? 0,
|
|
547
|
+
maxCalls: toolCallContext.agentCalls?.max ?? 0 }),
|
|
542
548
|
diagramPreparationAttempted ? visualPresentation(output) : '',
|
|
543
549
|
delegationCorrectionAttempted ? '本轮必要子任务尚未验收或接管完成。先读取结果、等待运行任务或完成接力,使用 AgentResolve 记录判断;若确实无法完成,明确报告缺口,不声称全部完成。' : '',
|
|
544
550
|
askCorrectionAttempted ? '当前交互宿主支持 AskUserQuestion。若仍需用户选择或补充,请调用该工具一次只问一个问题,等待答案后继续;不要仅在正文列问后停止。若没有待决事项则正常回答,不必提问。' : '',
|
|
551
|
+
emptyResponseRecoveryAttempted ? `上一次模型响应没有可交付正文(${emptyResponseRecoveryReason})。请基于已有历史和工具结果继续完成任务:信息充分则给出实质结论,不充分则继续必要操作或明确缺口。不要重复已经完成的调查或有副作用的操作;不要把未完成的任务说成完成。已有顾问意见继续有效,无需仅因恢复而再次咨询。` : '',
|
|
545
552
|
deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
|
|
546
553
|
advisorAvailable ? advisorCoordinator.transientReminder() : '',
|
|
547
554
|
advisorAvailable
|
|
@@ -917,16 +924,24 @@ export async function* runQuery(
|
|
|
917
924
|
advisorCoordinator.setAvailable(
|
|
918
925
|
currentEffectiveToolSet.names.has('Advisor'),
|
|
919
926
|
);
|
|
927
|
+
// Tool availability may change during emergency fitting. Rebuild dynamic
|
|
928
|
+
// guidance against the actual request boundary, then account for its size.
|
|
929
|
+
requestTransientContext = buildRequestTransientContext();
|
|
930
|
+
lastRequestTransientContext = requestTransientContext;
|
|
931
|
+
const finalRequestBudget = buildContextBudgetPlan({
|
|
932
|
+
provider, history, tools: currentEffectiveToolSet.tools,
|
|
933
|
+
transientContext: requestTransientContext,
|
|
934
|
+
});
|
|
920
935
|
if (emergencyFit.removedNames.length > 0) {
|
|
921
936
|
yield {
|
|
922
937
|
type: 'warning',
|
|
923
938
|
message: `当前模型窗口压力过高,本次请求临时省略工具 Schema:${emergencyFit.removedNames.join(', ')};下一轮会重新评估并恢复。`,
|
|
924
939
|
};
|
|
925
940
|
}
|
|
926
|
-
if (
|
|
941
|
+
if (finalRequestBudget.overBudget) {
|
|
927
942
|
yield {
|
|
928
943
|
type: 'error',
|
|
929
|
-
error: `当前请求即使经过上下文降级仍需要约 ${
|
|
944
|
+
error: `当前请求即使经过上下文降级仍需要约 ${finalRequestBudget.totalInputTokens} tokens,超过模型输入上限 ${finalRequestBudget.inputLimit}。已停止发送,避免 Provider 接收必然失败的超限请求。`,
|
|
930
945
|
code: 'context_overflow',
|
|
931
946
|
};
|
|
932
947
|
yield { type: 'harness_diagnostics', diagnostics: harnessDiagnostics('error') };
|
|
@@ -1224,8 +1239,19 @@ export async function* runQuery(
|
|
|
1224
1239
|
if (!deliveryCheckpoint.allow) yield { type: 'warning', message: '已达到本轮执行上限,顾问交付检查尚未完成;不能把此次输出视为已复核。' };
|
|
1225
1240
|
}
|
|
1226
1241
|
if (emptyResponse) {
|
|
1227
|
-
//
|
|
1228
|
-
//
|
|
1242
|
+
// Recover once within the existing turn budget, retaining tool receipts and
|
|
1243
|
+
// advisor state. Never replay tools or expose an empty candidate as an answer.
|
|
1244
|
+
if (!resultPresented && !emptyResponseRecoveryAttempted && turn < maxTurns) {
|
|
1245
|
+
emptyResponseRecoveryAttempted = true;
|
|
1246
|
+
emptyResponseRecoveryReason = lastFinishReason === 'length'
|
|
1247
|
+
? '输出额度耗尽' : lastFinishReason === 'unknown'
|
|
1248
|
+
? '响应流缺少正常结束状态' : '响应为空或仅含思考内容';
|
|
1249
|
+
const message: Message = { role: 'system', content: '正在恢复回答,已保留调查结果与工作进展。',
|
|
1250
|
+
id: crypto.randomUUID(), timestamp: Date.now(), _ephemeral: true, _uiProgress: true };
|
|
1251
|
+
history.push(message);
|
|
1252
|
+
yield { type: 'injected_message', message };
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1229
1255
|
yield* retireDraft('empty_response');
|
|
1230
1256
|
} else {
|
|
1231
1257
|
// Publication is a separate tool-free request. Nothing from preparation
|
|
@@ -1375,7 +1401,9 @@ export async function* runQuery(
|
|
|
1375
1401
|
trustworthy: turnUsage.totalCalls > 0 && turnUsage.reportedCalls === turnUsage.totalCalls,
|
|
1376
1402
|
};
|
|
1377
1403
|
if (emptyResponse && !resultPresented) {
|
|
1378
|
-
const warning =
|
|
1404
|
+
const warning = emptyResponseRecoveryAttempted
|
|
1405
|
+
? '自动恢复后仍未生成最终答复,已有进展已保留。可以继续本轮或检查模型服务。'
|
|
1406
|
+
: '本轮未生成最终答复,已有进展已保留。可以重试上一回合。';
|
|
1379
1407
|
yield { type: 'warning', message: warning };
|
|
1380
1408
|
yield { type: 'turn_done', completionStatus: 'no_answer', warning, usage };
|
|
1381
1409
|
} else if (pendingDelegations.length) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* its source material. A durable receipt makes retries idempotent. */
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { parseMemoryProposal } from './proposal.js';
|
|
5
|
-
import { failureSchema, jobEligible, recordJobFailure,
|
|
5
|
+
import { failureSchema, jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import lockfile from 'proper-lockfile';
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -437,7 +437,7 @@ export function scheduleFactLearning(options) {
|
|
|
437
437
|
void runFactLearning(options)
|
|
438
438
|
.catch((error) => {
|
|
439
439
|
if (!options.signal?.aborted)
|
|
440
|
-
|
|
440
|
+
recordBackgroundLearningFailure(options.workspaceId, 'facts', 'learning');
|
|
441
441
|
})
|
|
442
442
|
.finally(async () => {
|
|
443
443
|
const settings = await memoryService.settings();
|
|
@@ -453,7 +453,7 @@ export function scheduleFactLearning(options) {
|
|
|
453
453
|
if (pending.length)
|
|
454
454
|
wakeAt(directory(options.workspaceId), Math.max(Date.now() + 1000, Math.min(...pending.map((turn) => turn.nextAttemptAt ?? Date.now()))), () => scheduleFactLearning({ ...options, force: false }), options.signal);
|
|
455
455
|
})
|
|
456
|
-
.catch((
|
|
456
|
+
.catch(() => recordBackgroundLearningFailure(options.workspaceId, 'facts', 'scheduling'));
|
|
457
457
|
}
|
|
458
458
|
export async function drainFactLearning() {
|
|
459
459
|
await Promise.allSettled([...running.values()].map((task) => task.promise));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* its source material. A durable receipt makes retries idempotent. */
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { parseMemoryProposal } from './proposal.js';
|
|
5
|
-
import { failureSchema, jobEligible, recordJobFailure,
|
|
5
|
+
import { failureSchema, jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import lockfile from 'proper-lockfile';
|
|
8
8
|
import { z } from 'zod';
|
|
@@ -574,7 +574,7 @@ export function scheduleFactLearning(
|
|
|
574
574
|
void runFactLearning(options)
|
|
575
575
|
.catch((error) => {
|
|
576
576
|
if (!options.signal?.aborted)
|
|
577
|
-
|
|
577
|
+
recordBackgroundLearningFailure(options.workspaceId, 'facts', 'learning');
|
|
578
578
|
})
|
|
579
579
|
.finally(async () => {
|
|
580
580
|
const settings = await memoryService.settings();
|
|
@@ -604,11 +604,7 @@ export function scheduleFactLearning(
|
|
|
604
604
|
options.signal,
|
|
605
605
|
);
|
|
606
606
|
})
|
|
607
|
-
.catch((
|
|
608
|
-
process.stderr.write(
|
|
609
|
-
`[minimal-agent memory] 恢复调度失败:${error.message}\n`,
|
|
610
|
-
),
|
|
611
|
-
);
|
|
607
|
+
.catch(() => recordBackgroundLearningFailure(options.workspaceId, 'facts', 'scheduling'));
|
|
612
608
|
}
|
|
613
609
|
export async function drainFactLearning() {
|
|
614
610
|
await Promise.allSettled([...running.values()].map((task) => task.promise));
|
package/src/memory/jobPolicy.js
CHANGED
|
@@ -13,23 +13,31 @@ export function recordJobFailure(job, error) {
|
|
|
13
13
|
: /VERSION_CONFLICT|SOURCE_CHANGED/.test(message) ? 'conflict'
|
|
14
14
|
: /timeout|超时|429|503|fetch|network|ECONN|连接/i.test(message) ? 'transient'
|
|
15
15
|
: 'unknown';
|
|
16
|
-
|
|
16
|
+
// Changing failure categories is not recovery. Otherwise alternating provider
|
|
17
|
+
// and proposal failures can keep a source retrying forever.
|
|
18
|
+
const consecutive = (job.failure?.consecutive ?? 0) + 1;
|
|
17
19
|
job.attempts = (job.attempts ?? 0) + 1;
|
|
18
|
-
job.failure = { kind, consecutive, paused: consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
20
|
+
job.failure = { kind, consecutive, paused: Boolean(job.failure?.paused) || consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
19
21
|
job.nextAttemptAt = retryAt(consecutive);
|
|
20
22
|
}
|
|
21
23
|
export function jobEligible(job, force = false, now = Date.now()) {
|
|
22
24
|
return force || (!job.failure?.paused && (job.nextAttemptAt ?? 0) <= now);
|
|
23
25
|
}
|
|
24
|
-
const
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
const health = new Map();
|
|
27
|
+
/** Background services never write over a host's TUI. Durable queues retain
|
|
28
|
+
* source failures; this bounded process snapshot also exposes scheduler/IO errors
|
|
29
|
+
* that could not be persisted. No source text, paths or provider secrets. */
|
|
30
|
+
export function recordBackgroundLearningFailure(workspaceId, partition, phase) {
|
|
31
|
+
const key = `${workspaceId}:${partition}`;
|
|
32
|
+
const previous = health.get(key);
|
|
33
|
+
health.delete(key);
|
|
34
|
+
health.set(key, { partition, phase, failures: (previous?.failures ?? 0) + 1, at: Date.now() });
|
|
35
|
+
if (health.size > 128)
|
|
36
|
+
health.delete(health.keys().next().value);
|
|
37
|
+
}
|
|
38
|
+
export function backgroundLearningHealth(workspaceId) {
|
|
39
|
+
return ['facts', 'experience'].flatMap(partition => {
|
|
40
|
+
const value = health.get(`${workspaceId}:${partition}`);
|
|
41
|
+
return value ? [{ ...value }] : [];
|
|
42
|
+
});
|
|
35
43
|
}
|
package/src/memory/jobPolicy.ts
CHANGED
|
@@ -16,9 +16,11 @@ export function recordJobFailure(job: Job, error: unknown): void {
|
|
|
16
16
|
: /VERSION_CONFLICT|SOURCE_CHANGED/.test(message) ? 'conflict'
|
|
17
17
|
: /timeout|超时|429|503|fetch|network|ECONN|连接/i.test(message) ? 'transient'
|
|
18
18
|
: 'unknown';
|
|
19
|
-
|
|
19
|
+
// Changing failure categories is not recovery. Otherwise alternating provider
|
|
20
|
+
// and proposal failures can keep a source retrying forever.
|
|
21
|
+
const consecutive = (job.failure?.consecutive ?? 0) + 1;
|
|
20
22
|
job.attempts = (job.attempts ?? 0) + 1;
|
|
21
|
-
job.failure = { kind, consecutive, paused: consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
23
|
+
job.failure = { kind, consecutive, paused: Boolean(job.failure?.paused) || consecutive >= (kind === 'proposal' ? 3 : 6) };
|
|
22
24
|
job.nextAttemptAt = retryAt(consecutive);
|
|
23
25
|
}
|
|
24
26
|
|
|
@@ -26,13 +28,22 @@ export function jobEligible(job: Job, force = false, now = Date.now()): boolean
|
|
|
26
28
|
return force || (!job.failure?.paused && (job.nextAttemptAt ?? 0) <= now);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
type Partition = 'facts' | 'experience';
|
|
32
|
+
type Health = { partition: Partition; phase: 'learning' | 'scheduling'; failures: number; at: number };
|
|
33
|
+
const health = new Map<string, Health>();
|
|
34
|
+
/** Background services never write over a host's TUI. Durable queues retain
|
|
35
|
+
* source failures; this bounded process snapshot also exposes scheduler/IO errors
|
|
36
|
+
* that could not be persisted. No source text, paths or provider secrets. */
|
|
37
|
+
export function recordBackgroundLearningFailure(workspaceId: string, partition: Partition, phase: Health['phase']): void {
|
|
38
|
+
const key = `${workspaceId}:${partition}`;
|
|
39
|
+
const previous = health.get(key);
|
|
40
|
+
health.delete(key);
|
|
41
|
+
health.set(key, { partition, phase, failures: (previous?.failures ?? 0) + 1, at: Date.now() });
|
|
42
|
+
if (health.size > 128) health.delete(health.keys().next().value!);
|
|
43
|
+
}
|
|
44
|
+
export function backgroundLearningHealth(workspaceId: string): Health[] {
|
|
45
|
+
return (['facts', 'experience'] as const).flatMap(partition => {
|
|
46
|
+
const value = health.get(`${workspaceId}:${partition}`);
|
|
47
|
+
return value ? [{ ...value }] : [];
|
|
48
|
+
});
|
|
38
49
|
}
|
package/src/memory/learning.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure,
|
|
8
|
+
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -343,7 +343,7 @@ export function scheduleMemoryMaintenance(options) {
|
|
|
343
343
|
void runMemoryMaintenance(options).catch((error) => {
|
|
344
344
|
if (options.signal?.aborted)
|
|
345
345
|
return;
|
|
346
|
-
|
|
346
|
+
recordBackgroundLearningFailure(options.workspaceId, 'experience', 'learning');
|
|
347
347
|
}).finally(async () => {
|
|
348
348
|
const settings = await memoryService.settings();
|
|
349
349
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled)
|
|
@@ -358,7 +358,7 @@ export function scheduleMemoryMaintenance(options) {
|
|
|
358
358
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || active.has(experienceDirectory(options.workspaceId)))
|
|
359
359
|
return;
|
|
360
360
|
wakeAt(experienceDirectory(options.workspaceId), Math.max(next, Date.now() + 1000), () => scheduleMemoryMaintenance({ ...options, force: false }), options.signal);
|
|
361
|
-
}).catch(
|
|
361
|
+
}).catch(() => recordBackgroundLearningFailure(options.workspaceId, 'experience', 'scheduling'));
|
|
362
362
|
}
|
|
363
363
|
/** Tests and orderly hosts may drain without polling or busy waiting. */
|
|
364
364
|
export async function drainMemoryMaintenance() {
|
package/src/memory/learning.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { parseMemoryProposal } from './proposal.js';
|
|
7
7
|
import { acquireModelSlot, MAX_MODEL_DURATION_MS } from './modelBudget.js';
|
|
8
|
-
import { jobEligible, recordJobFailure,
|
|
8
|
+
import { jobEligible, recordJobFailure, recordBackgroundLearningFailure } from './jobPolicy.js';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import lockfile from 'proper-lockfile';
|
|
11
11
|
import { ensurePrivateDir } from './fileStore.js';
|
|
@@ -423,7 +423,7 @@ export function scheduleMemoryMaintenance(
|
|
|
423
423
|
if (active.has(experienceDirectory(options.workspaceId))) return;
|
|
424
424
|
void runMemoryMaintenance(options).catch((error) => {
|
|
425
425
|
if (options.signal?.aborted) return;
|
|
426
|
-
|
|
426
|
+
recordBackgroundLearningFailure(options.workspaceId, 'experience', 'learning');
|
|
427
427
|
}).finally(async () => {
|
|
428
428
|
const settings = await memoryService.settings();
|
|
429
429
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || options.signal?.aborted || !settings.autoRemember || !settings.learning?.enabled) return;
|
|
@@ -436,7 +436,7 @@ export function scheduleMemoryMaintenance(
|
|
|
436
436
|
// Cancellation can happen during the preceding disk reads.
|
|
437
437
|
if (cancelled.has(experienceDirectory(options.workspaceId)) || active.has(experienceDirectory(options.workspaceId))) return;
|
|
438
438
|
wakeAt(experienceDirectory(options.workspaceId), Math.max(next, Date.now() + 1000), () => scheduleMemoryMaintenance({ ...options, force: false }), options.signal);
|
|
439
|
-
}).catch(
|
|
439
|
+
}).catch(() => recordBackgroundLearningFailure(options.workspaceId, 'experience', 'scheduling'));
|
|
440
440
|
}
|
|
441
441
|
|
|
442
442
|
/** Tests and orderly hosts may drain without polling or busy waiting. */
|
|
@@ -43,8 +43,10 @@ export function createLearningModel(provider, execute = chat) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
signal.throwIfAborted();
|
|
46
|
-
if (!finished
|
|
47
|
-
throw new
|
|
46
|
+
if (!finished)
|
|
47
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
48
|
+
if (!text.trim())
|
|
49
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(空正文),保留待处理资料');
|
|
48
50
|
return { text, usage };
|
|
49
51
|
};
|
|
50
52
|
}
|
|
@@ -49,8 +49,10 @@ export function createLearningModel(
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
signal.throwIfAborted();
|
|
52
|
-
if (!finished
|
|
53
|
-
throw new
|
|
52
|
+
if (!finished)
|
|
53
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(截断或缺少正常结束标记),保留待处理资料');
|
|
54
|
+
if (!text.trim())
|
|
55
|
+
throw new MemoryProposalError('format', '记忆整理模型未正常完成(空正文),保留待处理资料');
|
|
54
56
|
return { text, usage };
|
|
55
57
|
};
|
|
56
58
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** One delegation contract shared by the main prompt, tool discovery and runtime. */
|
|
2
|
+
export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
|
|
3
|
+
- 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
|
|
4
|
+
- 少量已知文件同轮批量读;强依赖调用链自行跟进。只读委派无需写入铺路或 worktree,不为获得 shell 升级 general。
|
|
5
|
+
- 自包含任务书优先用 context.researchQuestions 写清问题、范围、停止条件,附基线、已知证据与排除项,不复制主历史。
|
|
6
|
+
- 有独立工作时后台派发,否则同轮前台派发;不立即轮询或重复调查。整合证据、冲突与缺口;摘要不代表全文覆盖,并发不保证降低总 Token。`;
|
|
7
|
+
export const AGENT_TOOL_DESCRIPTION = '启动隔离子任务(fork)并返回 agent_id。大型多模块批量阅读先定位入口,再优先同轮派发独立 explore 调查;少量已知文件直接批量读。explore 默认只读,general 使用 write_scopes 或 worktree。任务书自包含,context.researchQuestions 定义问题、范围、停止条件;AgentReport.findings 返回证据结论。run_in_background 立即返回供主 Agent 推进独立工作;resume_agent_id 续接子任务私有历史。reportMaxChars 限制预览,完整交接由 TaskOutput 获取。';
|
|
8
|
+
export function delegationRuntimeGuidance(input) {
|
|
9
|
+
if (!input.available)
|
|
10
|
+
return '';
|
|
11
|
+
const remaining = Math.max(0, input.maxCalls - input.callsUsed);
|
|
12
|
+
if (!remaining)
|
|
13
|
+
return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
|
|
14
|
+
return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One delegation contract shared by the main prompt, tool discovery and runtime. */
|
|
2
|
+
export const RESEARCH_DELEGATION_POLICY = `# 大范围阅读与并行调研
|
|
3
|
+
- 先读项目说明、目录、入口与基线,建立问题地图,不先串行读完整仓库。批量阅读涉及多个独立模块、各需多轮探索时,优先同轮派发多个 Agent(agent_type="explore");数量取决于独立工作量和宿主额度,不按文件数量机械切块。
|
|
4
|
+
- 少量已知文件同轮批量读;强依赖调用链自行跟进。只读委派无需写入铺路或 worktree,不为获得 shell 升级 general。
|
|
5
|
+
- 自包含任务书优先用 context.researchQuestions 写清问题、范围、停止条件,附基线、已知证据与排除项,不复制主历史。
|
|
6
|
+
- 有独立工作时后台派发,否则同轮前台派发;不立即轮询或重复调查。整合证据、冲突与缺口;摘要不代表全文覆盖,并发不保证降低总 Token。`;
|
|
7
|
+
|
|
8
|
+
export const AGENT_TOOL_DESCRIPTION = '启动隔离子任务(fork)并返回 agent_id。大型多模块批量阅读先定位入口,再优先同轮派发独立 explore 调查;少量已知文件直接批量读。explore 默认只读,general 使用 write_scopes 或 worktree。任务书自包含,context.researchQuestions 定义问题、范围、停止条件;AgentReport.findings 返回证据结论。run_in_background 立即返回供主 Agent 推进独立工作;resume_agent_id 续接子任务私有历史。reportMaxChars 限制预览,完整交接由 TaskOutput 获取。';
|
|
9
|
+
|
|
10
|
+
export function delegationRuntimeGuidance(input: {
|
|
11
|
+
available: boolean;
|
|
12
|
+
maxConcurrent: number;
|
|
13
|
+
callsUsed: number;
|
|
14
|
+
maxCalls: number;
|
|
15
|
+
}): string {
|
|
16
|
+
if (!input.available) return '';
|
|
17
|
+
const remaining = Math.max(0, input.maxCalls - input.callsUsed);
|
|
18
|
+
if (!remaining) return '本轮子任务派发次数已用完:收口已有子任务,其余调查由主 Agent 使用当前工具完成,不重复尝试派发。';
|
|
19
|
+
return `本轮可使用 Agent 进行隔离委派,剩余派发次数 ${remaining},配置并发上限 ${Math.max(1, Math.min(8, input.maxConcurrent))}(实际槽位由调度器协调)。大型批量阅读先建立问题地图,再优先并行派发独立 explore 调查;不要把所有模块留在主上下文串行阅读。少量已知文件仍直接批量读。`;
|
|
20
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CODE_REVIEW_RUBRIC } from './codeReview.js';
|
|
2
|
+
import { RESEARCH_DELEGATION_POLICY } from './delegation.js';
|
|
2
3
|
import { capabilityForToolName, } from '../tools/capabilities.js';
|
|
3
4
|
import { skillInvokeInstruction } from './skillList.js';
|
|
4
5
|
import { visualPresentation } from './visualPresentation.js';
|
|
@@ -260,7 +261,7 @@ const CAPABILITY_POLICIES = [
|
|
|
260
261
|
{
|
|
261
262
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
262
263
|
requiresTools: ['Agent'],
|
|
263
|
-
render: () =>
|
|
264
|
+
render: () => RESEARCH_DELEGATION_POLICY,
|
|
264
265
|
},
|
|
265
266
|
{
|
|
266
267
|
id: 'capability.agent-handoff', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -295,7 +296,7 @@ const CAPABILITY_POLICIES = [
|
|
|
295
296
|
if (toolNames.has('RecallMemory'))
|
|
296
297
|
rules.push('自包含任务无需检索记忆。依赖跨会话用户信息、偏好、决定或项目背景时,先检查已注入证据;缺少相关信息再用 RecallMemory,避免重复检索或直接反问用户。查询无关命中时停止扩大检索;旧结论与现场冲突时重新查证,未在本轮核验的历史结论标明来源及可能过时。');
|
|
297
298
|
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory'))
|
|
298
|
-
rules.push('
|
|
299
|
+
rules.push('RecallMemory 读版本 → UpdateMemory:update 原话/review 待复核/confirm 确认;冲突重读,拒绝不重试。');
|
|
299
300
|
if (toolNames.has('ForgetMemory'))
|
|
300
301
|
rules.push('ForgetMemory 仅响应当前明确遗忘要求。');
|
|
301
302
|
if (toolNames.has('ConfirmMemory'))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Tool } from '../types.js';
|
|
2
2
|
import { CODE_REVIEW_RUBRIC } from './codeReview.js';
|
|
3
|
+
import { RESEARCH_DELEGATION_POLICY } from './delegation.js';
|
|
3
4
|
import type { AgentProfile, AgentSurface } from '../runtime/agentRuntimeContext.js';
|
|
4
5
|
import {
|
|
5
6
|
capabilityForToolName,
|
|
@@ -309,7 +310,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
309
310
|
{
|
|
310
311
|
id: 'capability.subagent', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
311
312
|
requiresTools: ['Agent'],
|
|
312
|
-
render: () =>
|
|
313
|
+
render: () => RESEARCH_DELEGATION_POLICY,
|
|
313
314
|
},
|
|
314
315
|
{
|
|
315
316
|
id: 'capability.agent-handoff', layer: 'capability', profiles: MAIN_PROFILE, source: 'native',
|
|
@@ -341,7 +342,7 @@ const CAPABILITY_POLICIES: PromptPolicySpec[] = [
|
|
|
341
342
|
];
|
|
342
343
|
if (toolNames.has('RememberMemory')) rules.push('RememberMemory 保存明确要求记住的新事实;后台提炼启用时普通陈述交后台处理,不重复写入。自动记忆关闭时仅响应明确保存要求;失败如实说明,不阻断任务。');
|
|
343
344
|
if (toolNames.has('RecallMemory')) rules.push('自包含任务无需检索记忆。依赖跨会话用户信息、偏好、决定或项目背景时,先检查已注入证据;缺少相关信息再用 RecallMemory,避免重复检索或直接反问用户。查询无关命中时停止扩大检索;旧结论与现场冲突时重新查证,未在本轮核验的历史结论标明来源及可能过时。');
|
|
344
|
-
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory')) rules.push('
|
|
345
|
+
if (toolNames.has('RecallMemory') && toolNames.has('UpdateMemory')) rules.push('RecallMemory 读版本 → UpdateMemory:update 原话/review 待复核/confirm 确认;冲突重读,拒绝不重试。');
|
|
345
346
|
if (toolNames.has('ForgetMemory')) rules.push('ForgetMemory 仅响应当前明确遗忘要求。');
|
|
346
347
|
if (toolNames.has('ConfirmMemory')) rules.push('用户重新确认相同事实时使用 ConfirmMemory 更新确认时间,不创建副本。');
|
|
347
348
|
if (toolNames.has('ReviewMemories')) rules.push('已过复核周期的记录只降低优先级并提示复核,不自动删除。');
|
package/src/tools/agent/agent.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* ============================================================
|
|
26
26
|
*/
|
|
27
27
|
import { randomUUID } from 'node:crypto';
|
|
28
|
+
import { AGENT_TOOL_DESCRIPTION } from '../../prompts/delegation.js';
|
|
28
29
|
import { researchQuestionSchema, researchPreview } from './research.js';
|
|
29
30
|
import { EventEmitter } from 'node:events';
|
|
30
31
|
import { access } from 'node:fs/promises';
|
|
@@ -106,7 +107,7 @@ const inputSchema = z.object({
|
|
|
106
107
|
const parameters = toToolParameters(inputSchema);
|
|
107
108
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
108
109
|
// 借鉴官方 AgentTool prompt 文案(任务书自包含 / 同轮并行 / 何时不用)。
|
|
109
|
-
const description =
|
|
110
|
+
const description = AGENT_TOOL_DESCRIPTION;
|
|
110
111
|
// ---------------- 3. 子 agent 系统提示词(memoize,提示词缓存友好) ----------------
|
|
111
112
|
/** 任务回传契约。能力与权限已经由 assembleBasePrompt profile 精确注入。 */
|
|
112
113
|
const SUBAGENT_COMMON_APPENDIX = `# 你是子 agent(fork 分身)
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { randomUUID } from 'node:crypto';
|
|
29
|
+
import { AGENT_TOOL_DESCRIPTION } from '../../prompts/delegation.js';
|
|
29
30
|
import { researchQuestionSchema, researchPreview } from './research.js';
|
|
30
31
|
import { EventEmitter } from 'node:events';
|
|
31
32
|
import { access } from 'node:fs/promises';
|
|
@@ -152,7 +153,7 @@ const parameters = toToolParameters(inputSchema);
|
|
|
152
153
|
// ---------------- 2. Description(给 LLM 看的工具说明) ----------------
|
|
153
154
|
// 借鉴官方 AgentTool prompt 文案(任务书自包含 / 同轮并行 / 何时不用)。
|
|
154
155
|
|
|
155
|
-
const description =
|
|
156
|
+
const description = AGENT_TOOL_DESCRIPTION;
|
|
156
157
|
|
|
157
158
|
// ---------------- 3. 子 agent 系统提示词(memoize,提示词缓存友好) ----------------
|
|
158
159
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { backgroundLearningHealth } from '../../memory/jobPolicy.js';
|
|
2
3
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
3
4
|
import { sha256 } from '../../memory/codec.js';
|
|
4
5
|
import { captureDependencies } from '../../memory/dependencies.js';
|
|
@@ -289,6 +290,7 @@ export const memoryMaintenanceTool = {
|
|
|
289
290
|
ok: true,
|
|
290
291
|
content: JSON.stringify({
|
|
291
292
|
...(await persistedLearningStatus(ctx.workspaceId)),
|
|
293
|
+
recentBackgroundFailures: backgroundLearningHealth(ctx.workspaceId),
|
|
292
294
|
revision: store.revision,
|
|
293
295
|
taskSummaries: {
|
|
294
296
|
count: store.taskSummaries?.length ?? 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { backgroundLearningHealth } from '../../memory/jobPolicy.js';
|
|
2
3
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
3
4
|
import { sha256 } from '../../memory/codec.js';
|
|
4
5
|
import { captureDependencies } from '../../memory/dependencies.js';
|
|
@@ -388,6 +389,7 @@ export const memoryMaintenanceTool: Tool<z.infer<typeof schema>> = {
|
|
|
388
389
|
content: JSON.stringify(
|
|
389
390
|
{
|
|
390
391
|
...(await persistedLearningStatus(ctx.workspaceId)),
|
|
392
|
+
recentBackgroundFailures: backgroundLearningHealth(ctx.workspaceId),
|
|
391
393
|
revision: store.revision,
|
|
392
394
|
taskSummaries: {
|
|
393
395
|
count: store.taskSummaries?.length ?? 0,
|
|
@@ -6,7 +6,7 @@ import { memoryRecordPath } from '../../memory/fileStore.js';
|
|
|
6
6
|
import { memoryService, } from '../../memory/memoryService.js';
|
|
7
7
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
8
8
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
9
|
-
import { EXPLICIT_CROSS_WORKSPACE_INTENT, EXPLICIT_FORGET_INTENT, EXPLICIT_MEMORY_INTENT, EXPLICIT_PERSONA_INTENT, confirmSensitiveMemory, denyUnlessLocalOwner, requireCurrentEvidence, } from './policy.js';
|
|
9
|
+
import { EXPLICIT_CROSS_WORKSPACE_INTENT, EXPLICIT_FORGET_INTENT, EXPLICIT_MEMORY_INTENT, EXPLICIT_PERSONA_INTENT, confirmSensitiveMemory, confirmMemoryCorrection, denyUnlessLocalOwner, requireCurrentEvidence, } from './policy.js';
|
|
10
10
|
const scopeSchema = z.enum(['personal', 'workspace']);
|
|
11
11
|
const categorySchema = z.enum([
|
|
12
12
|
'profile',
|
|
@@ -21,7 +21,7 @@ function formatRecord(record, version) {
|
|
|
21
21
|
? ` workspace=${record.workspaceId}`
|
|
22
22
|
: '';
|
|
23
23
|
const lifecycle = ` status=${record.status ?? 'active'} revision=${record.revision ?? 1}`;
|
|
24
|
-
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()}
|
|
24
|
+
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()})${record.status === 'needs_review' && record.confirmationContext ? `\n复核线索(非事实):${record.confirmationContext}` : ''}`;
|
|
25
25
|
}
|
|
26
26
|
function diagnosticSuffix(diagnostics) {
|
|
27
27
|
if (!diagnostics.length)
|
|
@@ -159,15 +159,18 @@ export const rememberMemoryTool = {
|
|
|
159
159
|
},
|
|
160
160
|
};
|
|
161
161
|
const updateSchema = z.object({
|
|
162
|
+
action: z.enum(['update', 'review', 'confirm']).optional().describe('默认 update:当前用户原话纠正;review:仅标记待复核,不改事实;confirm:通过宿主向用户确认拟更新内容后保存'),
|
|
163
|
+
reason: z.string().min(1).max(1000).optional().describe('review/confirm 必填:冲突或纠正依据,注明来源;不得伪造用户陈述'),
|
|
162
164
|
id: z.string().uuid(),
|
|
163
165
|
scope: scopeSchema,
|
|
164
166
|
category: categorySchema.optional(),
|
|
165
167
|
subject: z.string().min(1).max(120).optional(),
|
|
166
|
-
statement: z.string().min(1).max(1000),
|
|
168
|
+
statement: z.string().min(1).max(1000).optional(),
|
|
167
169
|
evidenceQuote: z
|
|
168
170
|
.string()
|
|
169
171
|
.min(1)
|
|
170
172
|
.max(1000)
|
|
173
|
+
.optional()
|
|
171
174
|
.describe('当前用户消息中的逐字证据片段'),
|
|
172
175
|
sensitivity: z.enum(['normal', 'sensitive']).optional(),
|
|
173
176
|
aliases: z.array(z.string().min(1).max(120)).max(20).optional(),
|
|
@@ -184,7 +187,7 @@ const updateSchema = z.object({
|
|
|
184
187
|
});
|
|
185
188
|
export const updateMemoryTool = {
|
|
186
189
|
name: 'UpdateMemory',
|
|
187
|
-
description: '
|
|
190
|
+
description: '纠正用户事实记忆:先 RecallMemory 读取 ID/version。默认 update 需要 statement 与当前用户原话 evidenceQuote;review 需要 reason,仅标记待复核、保留原事实与证据;confirm 需要 statement/reason,通过宿主展示新旧内容并取得用户确认再更新。拒绝或无法交互不写入。项目知识和经验走 MemoryMaintenance,不伪造用户证据。',
|
|
188
191
|
inputSchema: updateSchema,
|
|
189
192
|
parameters: toToolParameters(updateSchema),
|
|
190
193
|
isReadOnly: false,
|
|
@@ -196,9 +199,23 @@ export const updateMemoryTool = {
|
|
|
196
199
|
if (denied)
|
|
197
200
|
return denied;
|
|
198
201
|
const current = ctx.currentUserInput ?? '';
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
return
|
|
202
|
+
const action = input.action ?? 'update';
|
|
203
|
+
if (action === 'review' && (input.statement !== undefined || input.evidenceQuote !== undefined)) {
|
|
204
|
+
return { ok: false, error: 'review 仅提交 reason,不提交 statement/evidenceQuote;标记疑点不代表确认新事实' };
|
|
205
|
+
}
|
|
206
|
+
if (action !== 'update' && (input.category !== undefined || input.subject !== undefined || input.aliases !== undefined || input.supersedesIds !== undefined || input.sensitivity !== undefined)) {
|
|
207
|
+
return { ok: false, error: 'review/confirm 只处理当前记录的复核状态或正文,不同时修改分类、敏感级别、别名或替代其他记录' };
|
|
208
|
+
}
|
|
209
|
+
if (action === 'update') {
|
|
210
|
+
if (!input.statement || !input.evidenceQuote)
|
|
211
|
+
return { ok: false, error: 'update 需要 statement 和当前用户原话 evidenceQuote' };
|
|
212
|
+
const invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
|
|
213
|
+
if (invalidEvidence)
|
|
214
|
+
return invalidEvidence;
|
|
215
|
+
}
|
|
216
|
+
else if (!input.reason || (action === 'confirm' && !input.statement)) {
|
|
217
|
+
return { ok: false, error: 'review 需要 reason;confirm 需要 reason 和 statement' };
|
|
218
|
+
}
|
|
202
219
|
if (input.scope === 'workspace' && !ctx.workspaceId) {
|
|
203
220
|
return {
|
|
204
221
|
ok: false,
|
|
@@ -206,7 +223,7 @@ export const updateMemoryTool = {
|
|
|
206
223
|
};
|
|
207
224
|
}
|
|
208
225
|
if (!(await memoryService.settings()).autoRemember &&
|
|
209
|
-
!EXPLICIT_MEMORY_INTENT.test(current)) {
|
|
226
|
+
!EXPLICIT_MEMORY_INTENT.test(current) && action !== 'confirm') {
|
|
210
227
|
return {
|
|
211
228
|
ok: false,
|
|
212
229
|
error: '用户已关闭自动记忆;当前消息没有明确要求更新记忆',
|
|
@@ -215,18 +232,43 @@ export const updateMemoryTool = {
|
|
|
215
232
|
const currentRecord = await memoryService.readWithVersion(input.id, input.scope, input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
216
233
|
if (!currentRecord)
|
|
217
234
|
return { ok: false, error: '未找到该记忆,未做修改' };
|
|
218
|
-
const
|
|
235
|
+
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
236
|
+
if (expectedVersion !== currentRecord.version) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: '记忆已被手工或并发修改。未应用本次更新;保留以下当前记录的并发修改后可直接重试,不存在时重新定位。\n'
|
|
240
|
+
+ JSON.stringify({ current: currentRecord }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (action === 'review') {
|
|
244
|
+
signal?.throwIfAborted();
|
|
245
|
+
try {
|
|
246
|
+
const reviewed = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion, { status: 'needs_review', confirmationContext: `待复核原因(非确认事实):${input.reason}` }, input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
247
|
+
return reviewed ? { ok: true, content: `已标记待复核,原事实和证据未改写。id=${input.id}\nversion=${reviewed.version}` }
|
|
248
|
+
: { ok: false, error: '记录已不存在,未做修改' };
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error.message === 'MEMORY_VERSION_CONFLICT')
|
|
252
|
+
return { ok: false, error: '记忆并发变更,请重新读取;未标记待复核' };
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const statement = input.statement;
|
|
257
|
+
const confirmed = action === 'confirm' && await confirmMemoryCorrection(currentRecord.record.subject, currentRecord.record.statement, statement, input.reason, signal);
|
|
258
|
+
if (action === 'confirm' && !confirmed)
|
|
259
|
+
return { ok: false, error: '未获得用户确认(拒绝或宿主无法交互),原记忆未改写;可标记待复核,不要重复请求确认' };
|
|
260
|
+
const evidenceQuote = confirmed ? `用户通过纠正记忆确认:${statement}` : input.evidenceQuote;
|
|
261
|
+
const sensitivity = currentRecord.record.sensitivity === 'sensitive' ? 'sensitive' : input.sensitivity ?? 'normal';
|
|
219
262
|
if (sensitivity === 'sensitive' &&
|
|
220
|
-
|
|
263
|
+
!confirmed && statement.trim() === evidenceQuote.trim()) {
|
|
221
264
|
return {
|
|
222
265
|
ok: false,
|
|
223
266
|
error: '敏感内容不得逐字更新;请先概括、去除不必要细节后再请求确认',
|
|
224
267
|
};
|
|
225
268
|
}
|
|
226
|
-
const evidence = await confirmSensitiveMemory(
|
|
269
|
+
const evidence = confirmed ? 'user_confirmed' : await confirmSensitiveMemory(statement, sensitivity, signal);
|
|
227
270
|
if (!evidence)
|
|
228
271
|
return { ok: false, error: '敏感记忆更新未获确认' };
|
|
229
|
-
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
230
272
|
let updated;
|
|
231
273
|
try {
|
|
232
274
|
updated = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion, {
|
|
@@ -234,9 +276,11 @@ export const updateMemoryTool = {
|
|
|
234
276
|
? { category: input.category }
|
|
235
277
|
: {}),
|
|
236
278
|
...(input.subject ? { subject: input.subject } : {}),
|
|
237
|
-
statement
|
|
279
|
+
statement,
|
|
280
|
+
status: 'active',
|
|
238
281
|
evidence,
|
|
239
|
-
evidenceQuote
|
|
282
|
+
evidenceQuote,
|
|
283
|
+
confirmationContext: confirmed ? `宿主确认纠正:${input.reason}` : '',
|
|
240
284
|
sourceSessionId: ctx.sessionId ?? 'unknown',
|
|
241
285
|
sourceMessageId: ctx.userMessageId ?? 'unknown',
|
|
242
286
|
sensitivity,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
EXPLICIT_MEMORY_INTENT,
|
|
19
19
|
EXPLICIT_PERSONA_INTENT,
|
|
20
20
|
confirmSensitiveMemory,
|
|
21
|
+
confirmMemoryCorrection,
|
|
21
22
|
denyUnlessLocalOwner,
|
|
22
23
|
requireCurrentEvidence,
|
|
23
24
|
} from './policy.js';
|
|
@@ -39,7 +40,7 @@ function formatRecord(
|
|
|
39
40
|
? ` workspace=${record.workspaceId}`
|
|
40
41
|
: '';
|
|
41
42
|
const lifecycle = ` status=${record.status ?? 'active'} revision=${record.revision ?? 1}`;
|
|
42
|
-
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()}
|
|
43
|
+
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()})${record.status === 'needs_review' && record.confirmationContext ? `\n复核线索(非事实):${record.confirmationContext}` : ''}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function diagnosticSuffix(
|
|
@@ -188,15 +189,18 @@ export const rememberMemoryTool: Tool<z.infer<typeof rememberSchema>> = {
|
|
|
188
189
|
};
|
|
189
190
|
|
|
190
191
|
const updateSchema = z.object({
|
|
192
|
+
action: z.enum(['update', 'review', 'confirm']).optional().describe('默认 update:当前用户原话纠正;review:仅标记待复核,不改事实;confirm:通过宿主向用户确认拟更新内容后保存'),
|
|
193
|
+
reason: z.string().min(1).max(1000).optional().describe('review/confirm 必填:冲突或纠正依据,注明来源;不得伪造用户陈述'),
|
|
191
194
|
id: z.string().uuid(),
|
|
192
195
|
scope: scopeSchema,
|
|
193
196
|
category: categorySchema.optional(),
|
|
194
197
|
subject: z.string().min(1).max(120).optional(),
|
|
195
|
-
statement: z.string().min(1).max(1000),
|
|
198
|
+
statement: z.string().min(1).max(1000).optional(),
|
|
196
199
|
evidenceQuote: z
|
|
197
200
|
.string()
|
|
198
201
|
.min(1)
|
|
199
202
|
.max(1000)
|
|
203
|
+
.optional()
|
|
200
204
|
.describe('当前用户消息中的逐字证据片段'),
|
|
201
205
|
sensitivity: z.enum(['normal', 'sensitive']).optional(),
|
|
202
206
|
aliases: z.array(z.string().min(1).max(120)).max(20).optional(),
|
|
@@ -217,7 +221,7 @@ const updateSchema = z.object({
|
|
|
217
221
|
export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
218
222
|
name: 'UpdateMemory',
|
|
219
223
|
description:
|
|
220
|
-
'
|
|
224
|
+
'纠正用户事实记忆:先 RecallMemory 读取 ID/version。默认 update 需要 statement 与当前用户原话 evidenceQuote;review 需要 reason,仅标记待复核、保留原事实与证据;confirm 需要 statement/reason,通过宿主展示新旧内容并取得用户确认再更新。拒绝或无法交互不写入。项目知识和经验走 MemoryMaintenance,不伪造用户证据。',
|
|
221
225
|
inputSchema: updateSchema,
|
|
222
226
|
parameters: toToolParameters(updateSchema),
|
|
223
227
|
isReadOnly: false,
|
|
@@ -228,8 +232,20 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
228
232
|
const denied = denyUnlessLocalOwner(ctx);
|
|
229
233
|
if (denied) return denied;
|
|
230
234
|
const current = ctx.currentUserInput ?? '';
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
235
|
+
const action = input.action ?? 'update';
|
|
236
|
+
if (action === 'review' && (input.statement !== undefined || input.evidenceQuote !== undefined)) {
|
|
237
|
+
return { ok: false, error: 'review 仅提交 reason,不提交 statement/evidenceQuote;标记疑点不代表确认新事实' };
|
|
238
|
+
}
|
|
239
|
+
if (action !== 'update' && (input.category !== undefined || input.subject !== undefined || input.aliases !== undefined || input.supersedesIds !== undefined || input.sensitivity !== undefined)) {
|
|
240
|
+
return { ok: false, error: 'review/confirm 只处理当前记录的复核状态或正文,不同时修改分类、敏感级别、别名或替代其他记录' };
|
|
241
|
+
}
|
|
242
|
+
if (action === 'update') {
|
|
243
|
+
if (!input.statement || !input.evidenceQuote) return { ok: false, error: 'update 需要 statement 和当前用户原话 evidenceQuote' };
|
|
244
|
+
const invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
|
|
245
|
+
if (invalidEvidence) return invalidEvidence;
|
|
246
|
+
} else if (!input.reason || (action === 'confirm' && !input.statement)) {
|
|
247
|
+
return { ok: false, error: 'review 需要 reason;confirm 需要 reason 和 statement' };
|
|
248
|
+
}
|
|
233
249
|
if (input.scope === 'workspace' && !ctx.workspaceId) {
|
|
234
250
|
return {
|
|
235
251
|
ok: false,
|
|
@@ -238,7 +254,7 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
238
254
|
}
|
|
239
255
|
if (
|
|
240
256
|
!(await memoryService.settings()).autoRemember &&
|
|
241
|
-
!EXPLICIT_MEMORY_INTENT.test(current)
|
|
257
|
+
!EXPLICIT_MEMORY_INTENT.test(current) && action !== 'confirm'
|
|
242
258
|
) {
|
|
243
259
|
return {
|
|
244
260
|
ok: false,
|
|
@@ -251,23 +267,49 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
251
267
|
input.scope === 'workspace' ? ctx.workspaceId : undefined,
|
|
252
268
|
);
|
|
253
269
|
if (!currentRecord) return { ok: false, error: '未找到该记忆,未做修改' };
|
|
254
|
-
const
|
|
270
|
+
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
271
|
+
if (expectedVersion !== currentRecord.version) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error:
|
|
275
|
+
'记忆已被手工或并发修改。未应用本次更新;保留以下当前记录的并发修改后可直接重试,不存在时重新定位。\n'
|
|
276
|
+
+ JSON.stringify({ current: currentRecord }),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (action === 'review') {
|
|
280
|
+
signal?.throwIfAborted();
|
|
281
|
+
try {
|
|
282
|
+
const reviewed = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion,
|
|
283
|
+
{ status: 'needs_review', confirmationContext: `待复核原因(非确认事实):${input.reason}` },
|
|
284
|
+
input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
285
|
+
return reviewed ? { ok: true, content: `已标记待复核,原事实和证据未改写。id=${input.id}\nversion=${reviewed.version}` }
|
|
286
|
+
: { ok: false, error: '记录已不存在,未做修改' };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if ((error as Error).message === 'MEMORY_VERSION_CONFLICT') return { ok: false, error: '记忆并发变更,请重新读取;未标记待复核' };
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const statement = input.statement!;
|
|
293
|
+
const confirmed = action === 'confirm' && await confirmMemoryCorrection(currentRecord.record.subject,
|
|
294
|
+
currentRecord.record.statement, statement, input.reason!, signal);
|
|
295
|
+
if (action === 'confirm' && !confirmed) return { ok: false, error: '未获得用户确认(拒绝或宿主无法交互),原记忆未改写;可标记待复核,不要重复请求确认' };
|
|
296
|
+
const evidenceQuote = confirmed ? `用户通过纠正记忆确认:${statement}` : input.evidenceQuote!;
|
|
297
|
+
const sensitivity = currentRecord.record.sensitivity === 'sensitive' ? 'sensitive' : input.sensitivity ?? 'normal';
|
|
255
298
|
if (
|
|
256
299
|
sensitivity === 'sensitive' &&
|
|
257
|
-
|
|
300
|
+
!confirmed && statement.trim() === evidenceQuote.trim()
|
|
258
301
|
) {
|
|
259
302
|
return {
|
|
260
303
|
ok: false,
|
|
261
304
|
error: '敏感内容不得逐字更新;请先概括、去除不必要细节后再请求确认',
|
|
262
305
|
};
|
|
263
306
|
}
|
|
264
|
-
const evidence = await confirmSensitiveMemory(
|
|
265
|
-
|
|
307
|
+
const evidence = confirmed ? 'user_confirmed' : await confirmSensitiveMemory(
|
|
308
|
+
statement,
|
|
266
309
|
sensitivity,
|
|
267
310
|
signal,
|
|
268
311
|
);
|
|
269
312
|
if (!evidence) return { ok: false, error: '敏感记忆更新未获确认' };
|
|
270
|
-
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
271
313
|
let updated: Awaited<ReturnType<typeof memoryService.updateWithVersion>>;
|
|
272
314
|
try {
|
|
273
315
|
updated = await memoryService.updateWithVersion(
|
|
@@ -279,9 +321,11 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
279
321
|
? { category: input.category as MemoryCategory }
|
|
280
322
|
: {}),
|
|
281
323
|
...(input.subject ? { subject: input.subject } : {}),
|
|
282
|
-
statement
|
|
324
|
+
statement,
|
|
325
|
+
status: 'active',
|
|
283
326
|
evidence,
|
|
284
|
-
evidenceQuote
|
|
327
|
+
evidenceQuote,
|
|
328
|
+
confirmationContext: confirmed ? `宿主确认纠正:${input.reason}` : '',
|
|
285
329
|
sourceSessionId: ctx.sessionId ?? 'unknown',
|
|
286
330
|
sourceMessageId: ctx.userMessageId ?? 'unknown',
|
|
287
331
|
sensitivity,
|
|
@@ -25,9 +25,24 @@ export function requireCurrentEvidence(ctx, evidenceQuote) {
|
|
|
25
25
|
? null
|
|
26
26
|
: {
|
|
27
27
|
ok: false,
|
|
28
|
-
error: 'evidenceQuote
|
|
28
|
+
error: 'evidenceQuote 不是当前用户消息的逐字片段,未写入。不要改写或伪造引用重试:用户事实可用 UpdateMemory(action=confirm) 请求确认,或 action=review 标记待复核;项目知识/经验使用 MemoryMaintenance,不把工具证据当用户原话。',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
export async function confirmMemoryCorrection(subject, previous, proposed, reason, signal) {
|
|
32
|
+
if (!hasAskHandler())
|
|
33
|
+
return false;
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
const [answer] = await requestUserChoice([{
|
|
36
|
+
header: '纠正记忆',
|
|
37
|
+
question: `是否更新“${subject}”?\n原记录:${previous}\n拟更新为:${proposed}\n纠正依据:${reason}`,
|
|
38
|
+
options: [
|
|
39
|
+
{ label: '暂不更新', description: '保留原记录,不把建议视为事实。' },
|
|
40
|
+
{ label: '确认更新', description: '确认拟更新内容准确,并保存为本机长期记忆。' },
|
|
41
|
+
],
|
|
42
|
+
}], signal);
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
return answer?.selected.length === 1 && answer.selected[0] === '确认更新';
|
|
45
|
+
}
|
|
31
46
|
export async function confirmSensitiveMemory(statement, sensitivity, signal) {
|
|
32
47
|
if (sensitivity === 'normal')
|
|
33
48
|
return 'user_stated';
|
|
@@ -38,10 +38,25 @@ export function requireCurrentEvidence(
|
|
|
38
38
|
? null
|
|
39
39
|
: {
|
|
40
40
|
ok: false,
|
|
41
|
-
error: 'evidenceQuote
|
|
41
|
+
error: 'evidenceQuote 不是当前用户消息的逐字片段,未写入。不要改写或伪造引用重试:用户事实可用 UpdateMemory(action=confirm) 请求确认,或 action=review 标记待复核;项目知识/经验使用 MemoryMaintenance,不把工具证据当用户原话。',
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export async function confirmMemoryCorrection(subject: string, previous: string, proposed: string, reason: string, signal?: AbortSignal): Promise<boolean> {
|
|
46
|
+
if (!hasAskHandler()) return false;
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
const [answer] = await requestUserChoice([{
|
|
49
|
+
header: '纠正记忆',
|
|
50
|
+
question: `是否更新“${subject}”?\n原记录:${previous}\n拟更新为:${proposed}\n纠正依据:${reason}`,
|
|
51
|
+
options: [
|
|
52
|
+
{ label: '暂不更新', description: '保留原记录,不把建议视为事实。' },
|
|
53
|
+
{ label: '确认更新', description: '确认拟更新内容准确,并保存为本机长期记忆。' },
|
|
54
|
+
],
|
|
55
|
+
}], signal);
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
|
+
return answer?.selected.length === 1 && answer.selected[0] === '确认更新';
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
export async function confirmSensitiveMemory(
|
|
46
61
|
statement: string,
|
|
47
62
|
sensitivity: 'normal' | 'sensitive',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_TOOL_DESCRIPTION } from '../prompts/delegation.js';
|
|
1
2
|
import { bashShellNotice } from './bash/bash.js';
|
|
2
3
|
import { COMPUTER_USE_TOOL_ACTIONS } from './computeruse/protocol.js';
|
|
3
4
|
export const TOOL_CATEGORY_LABELS = {
|
|
@@ -42,7 +43,7 @@ export const BUILTIN_TOOL_PROMPT_GUIDES = {
|
|
|
42
43
|
BrowserCDP: g('web', '需要现有浏览器工具未覆盖的原始 CDP 能力', '发送精确方法并按事件序列读取结果', '常规网页自动化优先 WebBrowser/BrowserBatch', 'last-resort'),
|
|
43
44
|
Download: g('web', '把已知 URL 的原始资源保存到本机', '给出目标路径并校验下载结果', '只阅读正文时用 WebFetch'),
|
|
44
45
|
ComputerUse: g('desktop', '用户明确要求观察或操作原生应用、系统窗口或桌面界面', '观察当前界面,执行一个动作,再验证结果', '文件、静态图片、Shell 和网页任务优先专用工具', 'last-resort'),
|
|
45
|
-
Agent: g('agents', '
|
|
46
|
+
Agent: g('agents', '大型多模块调查优先委派独立只读问题', '先定位入口,再同轮派发 explore,核验整合证据', '少量已知文件或强顺序工作直接处理'),
|
|
46
47
|
AgentMessage: g('agents', '向运行中的子 Agent 补充非触发消息', '按稳定 agent id 发送必要上下文', '需要追加新工作使用 AgentFollowup'),
|
|
47
48
|
AgentList: g('agents', '查看子 Agent 状态和稳定 ID', '按任务路径列出后选择目标', '已知目标 ID 时无需调用'),
|
|
48
49
|
AgentWait: g('agents', '等待指定子 Agent 进展或结果', '优先长等待,收到更新后再整合', '忙轮询'),
|
|
@@ -58,7 +59,7 @@ export const BUILTIN_TOOL_PROMPT_GUIDES = {
|
|
|
58
59
|
RecallMemory: g('memory', '当前回答依赖跨会话稳定偏好、决定或背景', '按主题召回并核对证据', '当前消息已足够时不要装饰性召回'),
|
|
59
60
|
MemoryMaintenance: g('memory', '处理学习队列、项目事实冲突及经验反馈', 'status 定位;summaries 按 query 找近期任务线索、按 turnId 查证据;resolve/resolveKnowledge 结清候选;明确跨项目适用条件后 promoteExperience/promoteKnowledge;adopt 后 feedback', '召回不算使用;失败摘要不是已确认事实;不每轮调用'),
|
|
60
61
|
RememberMemory: g('memory', '用户明确要求记住长期有效事实、偏好或决定', '保存原话证据和适用范围', '普通陈述交由已启用的后台提炼,不重复保存'),
|
|
61
|
-
UpdateMemory: g('memory', '
|
|
62
|
+
UpdateMemory: g('memory', '纠正用户事实', '读版本后 update/review/confirm', '项目知识/经验走 MemoryMaintenance'),
|
|
62
63
|
ListMemories: g('memory', '用户要查看已保存记忆或需要管理总览', '按范围列出后再决定操作', '正常任务无需全量列出'),
|
|
63
64
|
ForgetMemory: g('memory', '用户明确要求忘记某条记忆', '定位准确 ID 后删除', '不得主动清理'),
|
|
64
65
|
SetPersona: g('memory', '用户明确要求改变长期角色或协作风格', '写入清晰、稳定的人格要求', '一次性语气请求不要持久化'),
|
|
@@ -117,7 +118,7 @@ export const CONTRACT_DESCRIPTION_OVERRIDES = {
|
|
|
117
118
|
WebSearch: 'Tavily 搜索返回标题、URL、片段及可选综合答案,不是完整原文或浏览器。重要结论读取来源正文;按子问题查询,重试改变关键词或来源。recency 控制时效,历史范围用 start_date/end_date,none 关闭自动时效;advanced 更慢,news 偏新闻。需配置 Tavily。',
|
|
118
119
|
PowerShell: '运行宿主 PowerShell 独立进程并检查结果;长任务用 TaskOutput/TaskStop。永久删除用 Trash;不可逆操作先确认,非交互时取消。',
|
|
119
120
|
Download: '下载 HTTP(S) 原始字节到指定路径;相对路径基于工作目录,会覆盖已有文件;失败或中断不保留部分下载。',
|
|
120
|
-
Agent:
|
|
121
|
+
Agent: AGENT_TOOL_DESCRIPTION,
|
|
121
122
|
EnterWorktree: '从已提交 HEAD 创建并进入隔离工作树,不自动复制原工作区未提交修改;后续文件工具使用隔离目录,项目记忆仍属于原工作区。',
|
|
122
123
|
ExitWorktree: '退出隔离工作树:apply 检查并应用补丁到原工作区但不自动提交;keep 保留;discard 有改动则归档保留。实际应用成功后的清理告警不表示应重新应用。',
|
|
123
124
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Tool, ToolCategory, ToolPromptGuide } from '../types.js';
|
|
2
|
+
import { AGENT_TOOL_DESCRIPTION } from '../prompts/delegation.js';
|
|
2
3
|
import { bashShellNotice } from './bash/bash.js';
|
|
3
4
|
import { COMPUTER_USE_TOOL_ACTIONS } from './computeruse/protocol.js';
|
|
4
5
|
|
|
@@ -56,7 +57,7 @@ export const BUILTIN_TOOL_PROMPT_GUIDES: Readonly<Record<string, ToolPromptGuide
|
|
|
56
57
|
|
|
57
58
|
ComputerUse: g('desktop', '用户明确要求观察或操作原生应用、系统窗口或桌面界面', '观察当前界面,执行一个动作,再验证结果', '文件、静态图片、Shell 和网页任务优先专用工具', 'last-resort'),
|
|
58
59
|
|
|
59
|
-
Agent: g('agents', '
|
|
60
|
+
Agent: g('agents', '大型多模块调查优先委派独立只读问题', '先定位入口,再同轮派发 explore,核验整合证据', '少量已知文件或强顺序工作直接处理'),
|
|
60
61
|
AgentMessage: g('agents', '向运行中的子 Agent 补充非触发消息', '按稳定 agent id 发送必要上下文', '需要追加新工作使用 AgentFollowup'),
|
|
61
62
|
AgentList: g('agents', '查看子 Agent 状态和稳定 ID', '按任务路径列出后选择目标', '已知目标 ID 时无需调用'),
|
|
62
63
|
AgentWait: g('agents', '等待指定子 Agent 进展或结果', '优先长等待,收到更新后再整合', '忙轮询'),
|
|
@@ -73,7 +74,7 @@ export const BUILTIN_TOOL_PROMPT_GUIDES: Readonly<Record<string, ToolPromptGuide
|
|
|
73
74
|
RecallMemory: g('memory', '当前回答依赖跨会话稳定偏好、决定或背景', '按主题召回并核对证据', '当前消息已足够时不要装饰性召回'),
|
|
74
75
|
MemoryMaintenance: g('memory', '处理学习队列、项目事实冲突及经验反馈', 'status 定位;summaries 按 query 找近期任务线索、按 turnId 查证据;resolve/resolveKnowledge 结清候选;明确跨项目适用条件后 promoteExperience/promoteKnowledge;adopt 后 feedback', '召回不算使用;失败摘要不是已确认事实;不每轮调用'),
|
|
75
76
|
RememberMemory: g('memory', '用户明确要求记住长期有效事实、偏好或决定', '保存原话证据和适用范围', '普通陈述交由已启用的后台提炼,不重复保存'),
|
|
76
|
-
UpdateMemory: g('memory', '
|
|
77
|
+
UpdateMemory: g('memory', '纠正用户事实', '读版本后 update/review/confirm', '项目知识/经验走 MemoryMaintenance'),
|
|
77
78
|
ListMemories: g('memory', '用户要查看已保存记忆或需要管理总览', '按范围列出后再决定操作', '正常任务无需全量列出'),
|
|
78
79
|
ForgetMemory: g('memory', '用户明确要求忘记某条记忆', '定位准确 ID 后删除', '不得主动清理'),
|
|
79
80
|
SetPersona: g('memory', '用户明确要求改变长期角色或协作风格', '写入清晰、稳定的人格要求', '一次性语气请求不要持久化'),
|
|
@@ -135,7 +136,7 @@ export const CONTRACT_DESCRIPTION_OVERRIDES: Readonly<Record<string, string | ((
|
|
|
135
136
|
WebSearch: 'Tavily 搜索返回标题、URL、片段及可选综合答案,不是完整原文或浏览器。重要结论读取来源正文;按子问题查询,重试改变关键词或来源。recency 控制时效,历史范围用 start_date/end_date,none 关闭自动时效;advanced 更慢,news 偏新闻。需配置 Tavily。',
|
|
136
137
|
PowerShell: '运行宿主 PowerShell 独立进程并检查结果;长任务用 TaskOutput/TaskStop。永久删除用 Trash;不可逆操作先确认,非交互时取消。',
|
|
137
138
|
Download: '下载 HTTP(S) 原始字节到指定路径;相对路径基于工作目录,会覆盖已有文件;失败或中断不保留部分下载。',
|
|
138
|
-
Agent:
|
|
139
|
+
Agent: AGENT_TOOL_DESCRIPTION,
|
|
139
140
|
EnterWorktree: '从已提交 HEAD 创建并进入隔离工作树,不自动复制原工作区未提交修改;后续文件工具使用隔离目录,项目记忆仍属于原工作区。',
|
|
140
141
|
ExitWorktree: '退出隔离工作树:apply 检查并应用补丁到原工作区但不自动提交;keep 保留;discard 有改动则归档保留。实际应用成功后的清理告警不表示应重新应用。',
|
|
141
142
|
};
|