minimal-agent 0.18.7 → 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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/context/persistContext.js +5 -1
  3. package/src/context/persistContext.ts +5 -2
  4. package/src/loop.js +75 -25
  5. package/src/loop.ts +72 -23
  6. package/src/memory/factLearning.js +3 -3
  7. package/src/memory/factLearning.ts +3 -7
  8. package/src/memory/fileStore.js +2 -34
  9. package/src/memory/fileStore.ts +2 -26
  10. package/src/memory/jobPolicy.js +21 -13
  11. package/src/memory/jobPolicy.ts +22 -11
  12. package/src/memory/learning.js +3 -3
  13. package/src/memory/learning.ts +3 -3
  14. package/src/memory/learningModel.js +4 -2
  15. package/src/memory/learningModel.ts +4 -2
  16. package/src/prompts/delegation.js +15 -0
  17. package/src/prompts/delegation.ts +20 -0
  18. package/src/prompts/policyRegistry.js +7 -10
  19. package/src/prompts/policyRegistry.ts +9 -10
  20. package/src/prompts/promptAssembly.js +16 -23
  21. package/src/prompts/promptAssembly.ts +22 -27
  22. package/src/prompts/visualPresentation.js +28 -0
  23. package/src/prompts/visualPresentation.ts +26 -0
  24. package/src/runtime/agentRuntimeContext.js +3 -0
  25. package/src/runtime/agentRuntimeContext.ts +4 -0
  26. package/src/runtime/fileLock.js +75 -0
  27. package/src/runtime/fileLock.ts +73 -0
  28. package/src/runtime/finalAnswer.js +4 -2
  29. package/src/runtime/finalAnswer.ts +6 -2
  30. package/src/runtime/outputContract.js +17 -0
  31. package/src/runtime/outputContract.ts +34 -0
  32. package/src/session/runAgentTurn.js +7 -2
  33. package/src/session/runAgentTurn.ts +7 -2
  34. package/src/session/runtimeStore.js +11 -28
  35. package/src/session/runtimeStore.ts +9 -23
  36. package/src/tools/agent/agent.js +9 -6
  37. package/src/tools/agent/agent.ts +9 -5
  38. package/src/tools/memory/maintenance.js +2 -0
  39. package/src/tools/memory/maintenance.ts +2 -0
  40. package/src/tools/memory/memory.js +58 -14
  41. package/src/tools/memory/memory.ts +57 -13
  42. package/src/tools/memory/policy.js +16 -1
  43. package/src/tools/memory/policy.ts +16 -1
  44. package/src/tools/skill/searchSkills.js +2 -2
  45. package/src/tools/skill/searchSkills.ts +2 -2
  46. package/src/tools/skill/skill.js +4 -0
  47. package/src/tools/skill/skill.ts +3 -0
  48. package/src/tools/toolPromptGuides.js +4 -3
  49. package/src/tools/toolPromptGuides.ts +4 -3
  50. package/src/types.ts +4 -0
  51. package/src/web/protocol.ts +3 -1
@@ -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: '在用户当前陈述纠正或替换旧事实时,按 RecallMemory 返回的 ID 整条更新。必须先读取旧记录,evidenceQuote 必须逐字来自当前用户消息;不得用推断补写历史。',
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 invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
200
- if (invalidEvidence)
201
- return invalidEvidence;
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 sensitivity = input.sensitivity ?? currentRecord.record.sensitivity ?? 'normal';
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
- input.statement.trim() === input.evidenceQuote.trim()) {
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(input.statement, sensitivity, signal);
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: input.statement,
279
+ statement,
280
+ status: 'active',
238
281
  evidence,
239
- evidenceQuote: input.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
- '在用户当前陈述纠正或替换旧事实时,按 RecallMemory 返回的 ID 整条更新。必须先读取旧记录,evidenceQuote 必须逐字来自当前用户消息;不得用推断补写历史。',
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 invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
232
- if (invalidEvidence) return invalidEvidence;
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 sensitivity = input.sensitivity ?? currentRecord.record.sensitivity ?? 'normal';
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
- input.statement.trim() === input.evidenceQuote.trim()
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
- input.statement,
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: input.statement,
324
+ statement,
325
+ status: 'active',
283
326
  evidence,
284
- evidenceQuote: input.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',
@@ -24,7 +24,7 @@ export const searchSkillsTool = {
24
24
  }),
25
25
  callWithContext: async (input, signal, ctx) => {
26
26
  signal?.throwIfAborted();
27
- const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx));
27
+ const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx), ctx.availableToolNames?.());
28
28
  signal?.throwIfAborted();
29
29
  const terms = (input.query ?? '')
30
30
  .toLocaleLowerCase()
@@ -48,7 +48,7 @@ export const searchSkillsTool = {
48
48
  total: ranked.length,
49
49
  nextOffset: offset + page.length < ranked.length ? offset + page.length : null,
50
50
  skills: page.map(({ skill }) => skill),
51
- note: '这是已安装目录,不表示依赖工具已启用。同名项用 sourceId 精确加载;未找到可换关键词,仅加载当前阶段相关技能。',
51
+ note: '宿主提供有效工具集合时已按依赖筛选;目录不授予执行权限。同名项用 sourceId 精确加载,仅加载当前阶段相关技能。',
52
52
  }),
53
53
  };
54
54
  },
@@ -28,7 +28,7 @@ export const searchSkillsTool: Tool<z.infer<typeof schema>> = {
28
28
  }),
29
29
  callWithContext: async (input, signal, ctx) => {
30
30
  signal?.throwIfAborted();
31
- const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx));
31
+ const skills = visibleSkills(await discoverSkills(ctx.cwd, ctx), ctx.availableToolNames?.());
32
32
  signal?.throwIfAborted();
33
33
  const terms = (input.query ?? '')
34
34
  .toLocaleLowerCase()
@@ -58,7 +58,7 @@ export const searchSkillsTool: Tool<z.infer<typeof schema>> = {
58
58
  nextOffset:
59
59
  offset + page.length < ranked.length ? offset + page.length : null,
60
60
  skills: page.map(({ skill }) => skill),
61
- note: '这是已安装目录,不表示依赖工具已启用。同名项用 sourceId 精确加载;未找到可换关键词,仅加载当前阶段相关技能。',
61
+ note: '宿主提供有效工具集合时已按依赖筛选;目录不授予执行权限。同名项用 sourceId 精确加载,仅加载当前阶段相关技能。',
62
62
  }),
63
63
  };
64
64
  },
@@ -27,6 +27,10 @@ async function call(input, context) {
27
27
  ok: false,
28
28
  error: `Skill 名称有歧义:${input.name}。请选择 sourceId 后重试。\n${JSON.stringify(found.map(({ sourceId, sourceDir }) => ({ sourceId, sourceDir })))}`,
29
29
  };
30
+ const available = context.availableToolNames?.();
31
+ const missing = available && found[0].requiresTools?.filter(name => !available.has(name));
32
+ if (missing?.length)
33
+ return { ok: false, error: `Skill ${input.name} 当前缺少工具:${missing.join(', ')}。不能通过技能扩大执行权限。` };
30
34
  // Search roots are ordered: project skill intentionally overrides the bundled skill.
31
35
  const path = join(found[0].sourceDir, 'SKILL.md');
32
36
  const content = await readFile(path);
@@ -42,6 +42,9 @@ async function call(
42
42
  ok: false,
43
43
  error: `Skill 名称有歧义:${input.name}。请选择 sourceId 后重试。\n${JSON.stringify(found.map(({ sourceId, sourceDir }) => ({ sourceId, sourceDir })))}`,
44
44
  };
45
+ const available = context.availableToolNames?.();
46
+ const missing = available && found[0]!.requiresTools?.filter(name => !available.has(name));
47
+ if (missing?.length) return { ok: false, error: `Skill ${input.name} 当前缺少工具:${missing.join(', ')}。不能通过技能扩大执行权限。` };
45
48
  // Search roots are ordered: project skill intentionally overrides the bundled skill.
46
49
  const path = join(found[0]!.sourceDir, 'SKILL.md');
47
50
  const content = await readFile(path);
@@ -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', '当前用户陈述纠正一条已有记忆', '先 Recall 定位版本,再完整替换', '不要创建冲突副本'),
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: '启动隔离上下文子任务并返回 agent_id。explore 只读,general 按 write_scopes 或 worktree 执行。run_in_background 立即返回;resume_agent_id 续接私有历史。context.researchQuestions 定义问题契约,AgentReport.findings 按 ID 回传证据结论;reportMaxChars 限制返回预览,完整交接可由 TaskOutput 获取。',
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', '当前用户陈述纠正一条已有记忆', '先 Recall 定位版本,再完整替换', '不要创建冲突副本'),
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: '启动隔离上下文子任务并返回 agent_id。explore 只读,general 按 write_scopes 或 worktree 执行。run_in_background 立即返回;resume_agent_id 续接私有历史。context.researchQuestions 定义问题契约,AgentReport.findings 按 ID 回传证据结论;reportMaxChars 限制返回预览,完整交接可由 TaskOutput 获取。',
139
+ Agent: AGENT_TOOL_DESCRIPTION,
139
140
  EnterWorktree: '从已提交 HEAD 创建并进入隔离工作树,不自动复制原工作区未提交修改;后续文件工具使用隔离目录,项目记忆仍属于原工作区。',
140
141
  ExitWorktree: '退出隔离工作树:apply 检查并应用补丁到原工作区但不自动提交;keep 保留;discard 有改动则归档保留。实际应用成功后的清理告警不表示应重新应用。',
141
142
  };
package/src/types.ts CHANGED
@@ -570,6 +570,10 @@ export type ToolResult = { problem?: ToolProblemOutcome; observation?: ReadObser
570
570
  * 避免按工具名写特判分支。
571
571
  */
572
572
  export interface ToolCallContext {
573
+ /** Host presentation only; cannot grant tools to a delegated agent. */
574
+ output?: import('./runtime/outputContract.js').OutputContract;
575
+ /** Live effective catalog, including per-turn restrictions. */
576
+ availableToolNames?: () => ReadonlySet<string>;
573
577
  messageProvenance?: ConversationProvenance;
574
578
  agentHandoff?: {
575
579
  questionIds?: readonly string[];
@@ -292,6 +292,7 @@ export interface SkillInfo {
292
292
  name: string;
293
293
  description: string;
294
294
  source?: 'builtin' | 'workspace';
295
+ unavailableReason?: string;
295
296
  }
296
297
 
297
298
  export type ApiFormat = 'openai' | 'anthropic';
@@ -2195,6 +2196,7 @@ export type ClientFrame =
2195
2196
  | { t: 'answer'; requestId: string; answers: AskAnswer[] }
2196
2197
  | { t: 'cancelAsk'; requestId: string }
2197
2198
  | { t: 'abort' }
2199
+ | { t: 'compact/abort'; requestId: string }
2198
2200
  | { t: 'new'; dir: string; sessionId?: string }
2199
2201
  | { t: 'compact'; dir: string; sessionId?: string; requestId?: string }
2200
2202
  | { t: 'listDirs' }
@@ -3060,7 +3062,7 @@ export type ServerFrame =
3060
3062
  ok: boolean;
3061
3063
  message: string;
3062
3064
  }
3063
- | { t: 'skills'; skills: SkillInfo[] }
3065
+ | { t: 'skills'; skills: SkillInfo[]; dir?: string | null }
3064
3066
  | { t: 'server_error'; message: string }
3065
3067
  | { t: 'resource/crons/list'; tasks: CronTaskWithRuns[]; missedCount: number }
3066
3068
  | { t: 'resource/crons/draft'; draft: CronDraftResult }