minimal-agent 0.11.0 → 0.12.0

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 (128) hide show
  1. package/package.json +4 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
package/src/loop.js CHANGED
@@ -40,11 +40,26 @@ import { ALL_TOOLS, executeTool as defaultExecuteTool, getToolByName, getToolGat
40
40
  import { configService } from './config/configService.js';
41
41
  import { capabilityForToolName } from './tools/capabilities.js';
42
42
  import { createEffectiveToolSet } from './tools/effectiveToolSet.js';
43
+ import { hydrateSessionWorktree } from './tools/worktree/sessionWorktree.js';
43
44
  import { capabilitiesForTools, enabledPolicyIds, } from './prompts/policyRegistry.js';
44
45
  import { EvidenceLedger } from './harness/evidenceLedger.js';
45
46
  import { StallGuard } from './harness/stallGuard.js';
46
47
  import { validateFinalClaims } from './harness/claimValidation.js';
47
48
  import { createAgentRuntimeContext, } from './runtime/agentRuntimeContext.js';
49
+ const DELIVERY_ARTIFACT = '(?:文件|合同|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
50
+ const DELIVERY_ACTION = '(?:发|发送|交付|提交|给|传|制作|生成|创建|导出|输出|修改|更新|完成)';
51
+ const ENGLISH_DELIVERY_ARTIFACT = '(?:file|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
52
+ const ENGLISH_DELIVERY_ACTION = '(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce)';
53
+ /** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
54
+ export function expectsFormalDelivery(input) {
55
+ const compact = input.replace(/\s+/g, '');
56
+ const chinese = new RegExp(`${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?`, 'i');
57
+ const english = new RegExp(`\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`, 'i');
58
+ return chinese.test(compact) || english.test(input);
59
+ }
60
+ const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
61
+ Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
62
+ Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
48
63
  /**
49
64
  * 每轮 LLM 请求都会带上工具 schema,但 countMessagesTokens 只数历史消息、
50
65
  * 漏了这部分 → 系统性低估。autoCompact 判定阈值时加上它才准。
@@ -123,7 +138,7 @@ export async function* runQuery(userInput, options) {
123
138
  // DI fallback:测试可传 stub,生产走真实模块单例
124
139
  const chatFn = options.chat ?? defaultChat;
125
140
  const executeToolFn = options.executeTool ?? defaultExecuteTool;
126
- const explicitFileDeliveryRequested = /(?:发|发送|交付|提交|给|传)(?:给)?我.{0,24}(?:文件|合同|文档|表格|报告|图片|压缩包)|(?:文件|合同|文档|表格|报告|图片|压缩包).{0,24}(?:发|发送|交付|提交|给|传)(?:给)?我/.test(userInput.replace(/\s+/g, ''));
141
+ const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
127
142
  const transientFor = (tools) => {
128
143
  const opaque = options.transientContext?.trim() ?? '';
129
144
  const bundle = options.transientBundle;
@@ -143,7 +158,14 @@ export async function* runQuery(userInput, options) {
143
158
  };
144
159
  let resultPresented = false;
145
160
  let presentedResultUnverified = false;
161
+ let deliveryCorrectionAttempted = false;
146
162
  const subagents = await configService.subagents();
163
+ await hydrateSessionWorktree({
164
+ sessionState,
165
+ workspaceId: runtimeContext.workspaceId,
166
+ sessionId: runtimeContext.sessionId,
167
+ cwd: runtimeContext.cwd,
168
+ });
147
169
  const toolCallContext = {
148
170
  history,
149
171
  sessionState,
@@ -183,6 +205,10 @@ export async function* runQuery(userInput, options) {
183
205
  requiredNames: options.requiredTools,
184
206
  });
185
207
  const activeTools = effectiveToolSet.tools;
208
+ const formalDeliveryRequired = explicitFileDeliveryRequested
209
+ && runtimeContext.profile === 'main'
210
+ && runtimeContext.surface !== 'workflow'
211
+ && effectiveToolSet.names.has('PresentResult');
186
212
  let currentEffectiveToolSet = effectiveToolSet;
187
213
  let lastRequestTransientContext = [options.transientBundle?.text, options.transientContext]
188
214
  .filter(Boolean).join('\n\n');
@@ -298,7 +324,10 @@ export async function* runQuery(userInput, options) {
298
324
  }
299
325
  // 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
300
326
  // 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
301
- let requestTransientContext = transientFor(activeTools);
327
+ let requestTransientContext = [
328
+ transientFor(activeTools),
329
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
330
+ ].filter(Boolean).join('\n\n');
302
331
  lastRequestTransientContext = requestTransientContext;
303
332
  const preCompactBudget = buildContextBudgetPlan({
304
333
  provider,
@@ -374,9 +403,11 @@ export async function* runQuery(userInput, options) {
374
403
  /* snip 也失败 → 真的只能硬上 */
375
404
  }
376
405
  if (!recovered) {
406
+ // 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
407
+ // 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
377
408
  yield {
378
409
  type: 'error',
379
- error: '自动压缩失败,规则式截断也未能释放空间;已继续尝试回答。',
410
+ error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${e.message}`,
380
411
  code: 'compact_failed',
381
412
  };
382
413
  }
@@ -384,7 +415,10 @@ export async function* runQuery(userInput, options) {
384
415
  }
385
416
  // Re-open transient capacity after compaction. If the compacted tail is still
386
417
  // too large, trim old history before sacrificing any model-visible ability.
387
- requestTransientContext = transientFor(activeTools);
418
+ requestTransientContext = [
419
+ transientFor(activeTools),
420
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
421
+ ].filter(Boolean).join('\n\n');
388
422
  lastRequestTransientContext = requestTransientContext;
389
423
  let postCompactBudget = buildContextBudgetPlan({
390
424
  provider,
@@ -409,7 +443,10 @@ export async function* runQuery(userInput, options) {
409
443
  messagesRemoved: snipped.messagesRemoved,
410
444
  tokensFreed: snipped.tokensFreed,
411
445
  };
412
- requestTransientContext = transientFor(activeTools);
446
+ requestTransientContext = [
447
+ transientFor(activeTools),
448
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
449
+ ].filter(Boolean).join('\n\n');
413
450
  lastRequestTransientContext = requestTransientContext;
414
451
  postCompactBudget = buildContextBudgetPlan({
415
452
  provider,
@@ -612,6 +649,16 @@ export async function* runQuery(userInput, options) {
612
649
  yield { type: 'assistant_message', message: assistantMsg };
613
650
  // 4. 没有工具调用 → 整轮交互结束
614
651
  if (toolCallsByIndex.length === 0) {
652
+ // A model occasionally writes a path/link in prose and stops without formally
653
+ // presenting it. Give it one model-only corrective pass; the reminder is
654
+ // transient and therefore never pollutes persisted conversation history.
655
+ if (formalDeliveryRequired
656
+ && !resultPresented
657
+ && !deliveryCorrectionAttempted
658
+ && turn < maxTurns) {
659
+ deliveryCorrectionAttempted = true;
660
+ continue;
661
+ }
615
662
  // R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
616
663
  // 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
617
664
  // 走通用 warning(text/json 都落 stderr,不污染 json 那一行 stdout 契约)。
@@ -651,7 +698,7 @@ export async function* runQuery(userInput, options) {
651
698
  : {}),
652
699
  promptCacheUsage: lastPromptCacheUsage,
653
700
  };
654
- if (explicitFileDeliveryRequested && !resultPresented) {
701
+ if (formalDeliveryRequired && !resultPresented) {
655
702
  const warning = '交付未完成:用户明确要求提供文件,但本轮没有成功提交可打开的文件结果。';
656
703
  yield { type: 'warning', message: warning };
657
704
  yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning };
@@ -687,6 +734,17 @@ export async function* runQuery(userInput, options) {
687
734
  // 执行单个 tool call → WorkerOutcome(内部吞掉 abort/异常,永不 reject)。
688
735
  const runOne = async (tc, reportProgress) => {
689
736
  try {
737
+ if (stallGuard.shouldReject(tc)) {
738
+ return {
739
+ tc,
740
+ result: {
741
+ ok: false,
742
+ error: '检测到同一工具和参数已连续两次返回相同失败;第三次调用已在执行前拒绝,请改变输入或改用其他路径。',
743
+ recovery: 'change-input',
744
+ sensitivity: 'public',
745
+ },
746
+ };
747
+ }
690
748
  // The model-visible catalog and the execution boundary are the same set.
691
749
  // A provider may still emit a stale or fabricated tool call; return a
692
750
  // recoverable result so the model can choose an available path.
@@ -711,7 +769,7 @@ export async function* runQuery(userInput, options) {
711
769
  if (effectiveToolGate) {
712
770
  let decision;
713
771
  try {
714
- const gateDetails = getToolGateDetails(tc.function.name, tc.function.arguments, { ...toolCallContext, toolCallId: tc.id, reportProgress });
772
+ const gateDetails = getToolGateDetails(tc.function.name, tc.function.arguments, { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress });
715
773
  decision = await effectiveToolGate({
716
774
  name: tc.function.name,
717
775
  argsJson: tc.function.arguments,
@@ -730,7 +788,27 @@ export async function* runQuery(userInput, options) {
730
788
  options.onToolStart?.(tc.function.name, tc.id);
731
789
  // 浅拷贝填入本次调用的 toolCallId:进度按调用实例路由(并行同名工具不串扰)。
732
790
  // advisorCalls/agentCalls 是共享对象引用,计数仍全轮共享,不受拷贝影响。
733
- const result = await executeToolFn(tc.function.name, tc.function.arguments, signal, { ...toolCallContext, toolCallId: tc.id, reportProgress });
791
+ let result = await executeToolFn(tc.function.name, tc.function.arguments, signal, { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress });
792
+ if (tc.function.name === 'PresentResult'
793
+ && result.ok
794
+ && result.metadata?.kind === 'result_manifest'
795
+ && options.commitFormalDelivery) {
796
+ try {
797
+ await options.commitFormalDelivery({
798
+ toolCallId: tc.id,
799
+ turnId: userMessage.id ?? options.userMessageId ?? 'turn:unknown',
800
+ manifest: result.metadata.manifest,
801
+ });
802
+ }
803
+ catch (error) {
804
+ result = {
805
+ ok: false,
806
+ error: `正式交付未能可靠保存:${error.message}`,
807
+ recovery: 'retry',
808
+ sensitivity: 'local',
809
+ };
810
+ }
811
+ }
734
812
  options.onToolEnd?.(tc.id, false);
735
813
  return { tc, result };
736
814
  }
@@ -871,9 +949,15 @@ export async function* runQuery(userInput, options) {
871
949
  // If the third identical observation is also the configured final turn, retain the
872
950
  // established max_turns termination contract. The stall guard is valuable only when
873
951
  // it can stop a failing path before the caller's explicit turn budget does.
874
- const stall = turn + 1 < maxTurns
875
- ? stallGuard.observe(settled.map((item) => item.tc), settled.map((item) => item.result), assistantText)
876
- : { stalled: false };
952
+ const preemptedRepeatedFailure = settled.some(({ result }) => !result.ok && result.error.includes('第三次调用已在执行前拒绝'));
953
+ const stall = preemptedRepeatedFailure
954
+ ? ({
955
+ stalled: true,
956
+ reason: 'repeated_failure',
957
+ })
958
+ : turn + 1 < maxTurns
959
+ ? stallGuard.observe(settled.map((item) => item.tc), settled.map((item) => item.result), assistantText)
960
+ : { stalled: false };
877
961
  if (stall.stalled) {
878
962
  const message = stall.reason === 'repeated_failure'
879
963
  ? '检测到同一工具以相同参数连续返回相同失败,已停止重复重试。'
package/src/loop.ts CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  } from './tools/index.js';
54
54
  import type { ToolGate } from './toolGate.js';
55
55
  import type {
56
+ DeliverableManifest,
56
57
  ImageDetail,
57
58
  ImageInputSpec,
58
59
  LoopEvent,
@@ -66,6 +67,7 @@ import type {
66
67
  import { configService } from './config/configService.js';
67
68
  import { capabilityForToolName } from './tools/capabilities.js';
68
69
  import { createEffectiveToolSet } from './tools/effectiveToolSet.js';
70
+ import { hydrateSessionWorktree } from './tools/worktree/sessionWorktree.js';
69
71
  import {
70
72
  capabilitiesForTools,
71
73
  enabledPolicyIds,
@@ -78,6 +80,29 @@ import {
78
80
  type AgentRuntimeContext,
79
81
  } from './runtime/agentRuntimeContext.js';
80
82
 
83
+ const DELIVERY_ARTIFACT = '(?:文件|合同|文档|表格|报告|图片|图像|海报|幻灯片|PPT|PDF|代码|脚本|压缩包|网页|页面|HTML|DOCX|XLSX|CSV|ZIP)';
84
+ const DELIVERY_ACTION = '(?:发|发送|交付|提交|给|传|制作|生成|创建|导出|输出|修改|更新|完成)';
85
+ const ENGLISH_DELIVERY_ARTIFACT = '(?:file|document|report|spreadsheet|workbook|slides?|presentation|pdf|image|code|script|archive|zip|html|webpage|website)';
86
+ const ENGLISH_DELIVERY_ACTION = '(?:send|deliver|submit|provide|create|generate|export|update|modify|finish|produce)';
87
+
88
+ /** Whether this turn explicitly asks for a concrete artifact that should be formally delivered. */
89
+ export function expectsFormalDelivery(input: string): boolean {
90
+ const compact = input.replace(/\s+/g, '');
91
+ const chinese = new RegExp(
92
+ `${DELIVERY_ACTION}(?:给)?我?.{0,24}${DELIVERY_ARTIFACT}|${DELIVERY_ARTIFACT}.{0,24}${DELIVERY_ACTION}(?:给)?我?`,
93
+ 'i',
94
+ );
95
+ const english = new RegExp(
96
+ `\\b${ENGLISH_DELIVERY_ACTION}\\b.{0,80}\\b${ENGLISH_DELIVERY_ARTIFACT}\\b|\\b${ENGLISH_DELIVERY_ARTIFACT}\\b.{0,80}\\b${ENGLISH_DELIVERY_ACTION}\\b`,
97
+ 'i',
98
+ );
99
+ return chinese.test(compact) || english.test(input);
100
+ }
101
+
102
+ const DELIVERY_CORRECTION_REMINDER = `The user requested a concrete final deliverable, but no successful PresentResult call has been made.
103
+ Do not repeat the answer or mention a path only. Call PresentResult now with the actual final files and/or final http(s) links.
104
+ Submit only final outputs, never temporary scripts, unpacked assets, caches, or reference sources. If no valid result exists, explain that honestly without inventing one.`;
105
+
81
106
  /**
82
107
  * 每轮 LLM 请求都会带上工具 schema,但 countMessagesTokens 只数历史消息、
83
108
  * 漏了这部分 → 系统性低估。autoCompact 判定阈值时加上它才准。
@@ -189,6 +214,17 @@ export interface RunQueryOptions {
189
214
  signal?: AbortSignal,
190
215
  ctx?: ToolCallContext,
191
216
  ) => Promise<ToolResult>;
217
+ /**
218
+ * Trusted host persistence boundary for formal results. It runs after
219
+ * PresentResult validates the files but before tool_end is published or the
220
+ * result is appended to history. Throwing turns the tool call into a failure,
221
+ * so a UI can never display an uncommitted delivery as durable.
222
+ */
223
+ commitFormalDelivery?: (input: {
224
+ toolCallId: string;
225
+ turnId: string;
226
+ manifest: DeliverableManifest;
227
+ }) => Promise<void>;
192
228
  /**
193
229
  * 工具执行闸门(通用机制,见 src/toolGate.ts):loop 在执行每个工具前
194
230
  * await 它拿放行/拒绝;拒绝时把 reason 当 tool_result 回填。内核零策略——审批语义
@@ -264,10 +300,7 @@ export async function* runQuery(
264
300
  // DI fallback:测试可传 stub,生产走真实模块单例
265
301
  const chatFn = options.chat ?? defaultChat;
266
302
  const executeToolFn = options.executeTool ?? defaultExecuteTool;
267
- const explicitFileDeliveryRequested =
268
- /(?:发|发送|交付|提交|给|传)(?:给)?我.{0,24}(?:文件|合同|文档|表格|报告|图片|压缩包)|(?:文件|合同|文档|表格|报告|图片|压缩包).{0,24}(?:发|发送|交付|提交|给|传)(?:给)?我/.test(
269
- userInput.replace(/\s+/g, ''),
270
- );
303
+ const explicitFileDeliveryRequested = expectsFormalDelivery(userInput);
271
304
 
272
305
  const transientFor = (tools: readonly Tool[]): string => {
273
306
  const opaque = options.transientContext?.trim() ?? '';
@@ -290,7 +323,14 @@ export async function* runQuery(
290
323
  };
291
324
  let resultPresented = false;
292
325
  let presentedResultUnverified = false;
326
+ let deliveryCorrectionAttempted = false;
293
327
  const subagents = await configService.subagents();
328
+ await hydrateSessionWorktree({
329
+ sessionState,
330
+ workspaceId: runtimeContext.workspaceId,
331
+ sessionId: runtimeContext.sessionId,
332
+ cwd: runtimeContext.cwd,
333
+ });
294
334
  const toolCallContext: ToolCallContext = {
295
335
  history,
296
336
  sessionState,
@@ -333,6 +373,10 @@ export async function* runQuery(
333
373
  requiredNames: options.requiredTools,
334
374
  });
335
375
  const activeTools = effectiveToolSet.tools;
376
+ const formalDeliveryRequired = explicitFileDeliveryRequested
377
+ && runtimeContext.profile === 'main'
378
+ && runtimeContext.surface !== 'workflow'
379
+ && effectiveToolSet.names.has('PresentResult');
336
380
  let currentEffectiveToolSet = effectiveToolSet;
337
381
  let lastRequestTransientContext = [options.transientBundle?.text, options.transientContext]
338
382
  .filter(Boolean).join('\n\n');
@@ -455,7 +499,10 @@ export async function* runQuery(
455
499
 
456
500
  // 2. 自动压缩(判据 = max(上一轮真实 usage, 历史估算 + 工具 schema),
457
501
  // 阈值 provider 无关、按 contextWindow × compactRatio 比例触发)
458
- let requestTransientContext = transientFor(activeTools);
502
+ let requestTransientContext = [
503
+ transientFor(activeTools),
504
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
505
+ ].filter(Boolean).join('\n\n');
459
506
  lastRequestTransientContext = requestTransientContext;
460
507
  const preCompactBudget = buildContextBudgetPlan({
461
508
  provider,
@@ -528,9 +575,11 @@ export async function* runQuery(
528
575
  /* snip 也失败 → 真的只能硬上 */
529
576
  }
530
577
  if (!recovered) {
578
+ // 事件是 provider 原始报错唯一该去的地方:只到 stderr / UI,不进 history、
579
+ // 不落盘、不会在后续轮次重新发给模型(compact.ts 里那条降级消息因此保持无错误体)。
531
580
  yield {
532
581
  type: 'error',
533
- error: '自动压缩失败,规则式截断也未能释放空间;已继续尝试回答。',
582
+ error: `自动压缩失败,规则式截断也未能释放空间;已继续尝试回答:${(e as Error).message}`,
534
583
  code: 'compact_failed',
535
584
  };
536
585
  }
@@ -539,7 +588,10 @@ export async function* runQuery(
539
588
 
540
589
  // Re-open transient capacity after compaction. If the compacted tail is still
541
590
  // too large, trim old history before sacrificing any model-visible ability.
542
- requestTransientContext = transientFor(activeTools);
591
+ requestTransientContext = [
592
+ transientFor(activeTools),
593
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
594
+ ].filter(Boolean).join('\n\n');
543
595
  lastRequestTransientContext = requestTransientContext;
544
596
  let postCompactBudget = buildContextBudgetPlan({
545
597
  provider,
@@ -564,7 +616,10 @@ export async function* runQuery(
564
616
  messagesRemoved: snipped.messagesRemoved,
565
617
  tokensFreed: snipped.tokensFreed,
566
618
  };
567
- requestTransientContext = transientFor(activeTools);
619
+ requestTransientContext = [
620
+ transientFor(activeTools),
621
+ deliveryCorrectionAttempted && !resultPresented ? DELIVERY_CORRECTION_REMINDER : '',
622
+ ].filter(Boolean).join('\n\n');
568
623
  lastRequestTransientContext = requestTransientContext;
569
624
  postCompactBudget = buildContextBudgetPlan({
570
625
  provider,
@@ -775,6 +830,18 @@ export async function* runQuery(
775
830
 
776
831
  // 4. 没有工具调用 → 整轮交互结束
777
832
  if (toolCallsByIndex.length === 0) {
833
+ // A model occasionally writes a path/link in prose and stops without formally
834
+ // presenting it. Give it one model-only corrective pass; the reminder is
835
+ // transient and therefore never pollutes persisted conversation history.
836
+ if (
837
+ formalDeliveryRequired
838
+ && !resultPresented
839
+ && !deliveryCorrectionAttempted
840
+ && turn < maxTurns
841
+ ) {
842
+ deliveryCorrectionAttempted = true;
843
+ continue;
844
+ }
778
845
  // R2:最终回答被 provider 因 max_tokens 截断(finish_reason='length')时,
779
846
  // 不能静默当正常 end_turn 收尾 —— 否则 -p 宿主会拿到半截答案却以为成功。
780
847
  // 走通用 warning(text/json 都落 stderr,不污染 json 那一行 stdout 契约)。
@@ -817,7 +884,7 @@ export async function* runQuery(
817
884
  : {}),
818
885
  promptCacheUsage: lastPromptCacheUsage,
819
886
  };
820
- if (explicitFileDeliveryRequested && !resultPresented) {
887
+ if (formalDeliveryRequired && !resultPresented) {
821
888
  const warning = '交付未完成:用户明确要求提供文件,但本轮没有成功提交可打开的文件结果。';
822
889
  yield { type: 'warning', message: warning };
823
890
  yield { type: 'turn_done', completionStatus: 'delivery_incomplete', warning };
@@ -860,6 +927,18 @@ export async function* runQuery(
860
927
  reportProgress?: (message: string, detail?: import('./types.js').ToolProgressDetail) => void,
861
928
  ): Promise<WorkerOutcome> => {
862
929
  try {
930
+ if (stallGuard.shouldReject(tc)) {
931
+ return {
932
+ tc,
933
+ result: {
934
+ ok: false as const,
935
+ error:
936
+ '检测到同一工具和参数已连续两次返回相同失败;第三次调用已在执行前拒绝,请改变输入或改用其他路径。',
937
+ recovery: 'change-input',
938
+ sensitivity: 'public',
939
+ },
940
+ };
941
+ }
863
942
  // The model-visible catalog and the execution boundary are the same set.
864
943
  // A provider may still emit a stale or fabricated tool call; return a
865
944
  // recoverable result so the model can choose an available path.
@@ -887,7 +966,7 @@ export async function* runQuery(
887
966
  const gateDetails = getToolGateDetails(
888
967
  tc.function.name,
889
968
  tc.function.arguments,
890
- { ...toolCallContext, toolCallId: tc.id, reportProgress },
969
+ { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress },
891
970
  );
892
971
  decision = await effectiveToolGate(
893
972
  {
@@ -909,12 +988,33 @@ export async function* runQuery(
909
988
  options.onToolStart?.(tc.function.name, tc.id);
910
989
  // 浅拷贝填入本次调用的 toolCallId:进度按调用实例路由(并行同名工具不串扰)。
911
990
  // advisorCalls/agentCalls 是共享对象引用,计数仍全轮共享,不受拷贝影响。
912
- const result = await executeToolFn(
991
+ let result = await executeToolFn(
913
992
  tc.function.name,
914
993
  tc.function.arguments,
915
994
  signal,
916
- { ...toolCallContext, toolCallId: tc.id, reportProgress },
995
+ { ...toolCallContext, cwd: sessionState?.activeWorktree?.path ?? toolCallContext.cwd, toolCallId: tc.id, reportProgress },
917
996
  );
997
+ if (
998
+ tc.function.name === 'PresentResult'
999
+ && result.ok
1000
+ && result.metadata?.kind === 'result_manifest'
1001
+ && options.commitFormalDelivery
1002
+ ) {
1003
+ try {
1004
+ await options.commitFormalDelivery({
1005
+ toolCallId: tc.id,
1006
+ turnId: userMessage.id ?? options.userMessageId ?? 'turn:unknown',
1007
+ manifest: result.metadata.manifest,
1008
+ });
1009
+ } catch (error) {
1010
+ result = {
1011
+ ok: false,
1012
+ error: `正式交付未能可靠保存:${(error as Error).message}`,
1013
+ recovery: 'retry',
1014
+ sensitivity: 'local',
1015
+ };
1016
+ }
1017
+ }
918
1018
  options.onToolEnd?.(tc.id, false);
919
1019
  return { tc, result };
920
1020
  } catch (e) {
@@ -1065,13 +1165,22 @@ export async function* runQuery(
1065
1165
  // If the third identical observation is also the configured final turn, retain the
1066
1166
  // established max_turns termination contract. The stall guard is valuable only when
1067
1167
  // it can stop a failing path before the caller's explicit turn budget does.
1068
- const stall = turn + 1 < maxTurns
1069
- ? stallGuard.observe(
1168
+ const preemptedRepeatedFailure = settled.some(
1169
+ ({ result }) =>
1170
+ !result.ok && result.error.includes('第三次调用已在执行前拒绝'),
1171
+ );
1172
+ const stall = preemptedRepeatedFailure
1173
+ ? ({
1174
+ stalled: true as const,
1175
+ reason: 'repeated_failure' as const,
1176
+ })
1177
+ : turn + 1 < maxTurns
1178
+ ? stallGuard.observe(
1070
1179
  settled.map((item) => item.tc),
1071
1180
  settled.map((item) => item.result),
1072
1181
  assistantText,
1073
- )
1074
- : { stalled: false as const };
1182
+ )
1183
+ : { stalled: false as const };
1075
1184
  if (stall.stalled) {
1076
1185
  const message = stall.reason === 'repeated_failure'
1077
1186
  ? '检测到同一工具以相同参数连续返回相同失败,已停止重复重试。'
package/src/main.js CHANGED
@@ -4,6 +4,7 @@ import { render } from 'ink';
4
4
  import { existsSync, mkdirSync } from 'node:fs';
5
5
  import { createRequire } from 'node:module';
6
6
  import { resolve } from 'node:path';
7
+ import { installTerminalCapture } from './tools/terminal/terminalCapture.js';
7
8
  const require = createRequire(import.meta.url);
8
9
  const pkg = (() => {
9
10
  if (typeof __GREEN_BUILD_VERSION__ === 'string') {
@@ -193,8 +194,14 @@ async function main() {
193
194
  // TUI 交互模式:优先读 JSON 配置(home → exe 同级),没有再用 env 兜底;
194
195
  // 都没有则 Root 渲染首次配置菜单。
195
196
  const initialProvider = await loadProviderLayered();
197
+ const uninstallTerminalCapture = installTerminalCapture();
196
198
  const { waitUntilExit } = render(_jsx(Root, { initialProvider: initialProvider }));
197
- await waitUntilExit();
199
+ try {
200
+ await waitUntilExit();
201
+ }
202
+ finally {
203
+ uninstallTerminalCapture();
204
+ }
198
205
  }
199
206
  /**
200
207
  * 启动时构造 history:用当前 cwd / 工具的最新 system prompt,
package/src/main.tsx CHANGED
@@ -27,6 +27,7 @@ import { render } from 'ink';
27
27
  import { existsSync, mkdirSync } from 'node:fs';
28
28
  import { createRequire } from 'node:module';
29
29
  import { resolve } from 'node:path';
30
+ import { installTerminalCapture } from './tools/terminal/terminalCapture.js';
30
31
 
31
32
  const require = createRequire(import.meta.url);
32
33
 
@@ -243,8 +244,13 @@ async function main(): Promise<void> {
243
244
  // TUI 交互模式:优先读 JSON 配置(home → exe 同级),没有再用 env 兜底;
244
245
  // 都没有则 Root 渲染首次配置菜单。
245
246
  const initialProvider = await loadProviderLayered();
247
+ const uninstallTerminalCapture = installTerminalCapture();
246
248
  const { waitUntilExit } = render(<Root initialProvider={initialProvider} />);
247
- await waitUntilExit();
249
+ try {
250
+ await waitUntilExit();
251
+ } finally {
252
+ uninstallTerminalCapture();
253
+ }
248
254
  }
249
255
 
250
256
  /**
@@ -18,7 +18,17 @@ export const DEFAULT_REVIEW_WINDOWS = {
18
18
  project: 90,
19
19
  decision: 180,
20
20
  };
21
+ /** 会话原文默认保留 90 天 / 单工作区 500MB,两者取先到者。 */
22
+ export const DEFAULT_RETENTION = {
23
+ episodeDays: 90,
24
+ episodeMaxMegabytes: 500,
25
+ };
26
+ export const MEMORY_RETENTION_DAYS_MIN = 1;
27
+ export const MEMORY_RETENTION_DAYS_MAX = 3_650;
28
+ export const MEMORY_RETENTION_MB_MIN = 16;
29
+ export const MEMORY_RETENTION_MB_MAX = 20_480;
21
30
  export const DEFAULT_SETTINGS = {
22
31
  autoRemember: true,
23
32
  reviewAfterDays: DEFAULT_REVIEW_WINDOWS,
33
+ retention: DEFAULT_RETENTION,
24
34
  };
@@ -1,5 +1,6 @@
1
1
  import type {
2
2
  MemoryCategory,
3
+ MemoryRetention,
3
4
  MemoryReviewWindows,
4
5
  MemorySettings,
5
6
  } from './types.js';
@@ -31,7 +32,19 @@ export const DEFAULT_REVIEW_WINDOWS: MemoryReviewWindows = {
31
32
  decision: 180,
32
33
  };
33
34
 
35
+ /** 会话原文默认保留 90 天 / 单工作区 500MB,两者取先到者。 */
36
+ export const DEFAULT_RETENTION: MemoryRetention = {
37
+ episodeDays: 90,
38
+ episodeMaxMegabytes: 500,
39
+ };
40
+
41
+ export const MEMORY_RETENTION_DAYS_MIN = 1;
42
+ export const MEMORY_RETENTION_DAYS_MAX = 3_650;
43
+ export const MEMORY_RETENTION_MB_MIN = 16;
44
+ export const MEMORY_RETENTION_MB_MAX = 20_480;
45
+
34
46
  export const DEFAULT_SETTINGS: MemorySettings = {
35
47
  autoRemember: true,
36
48
  reviewAfterDays: DEFAULT_REVIEW_WINDOWS,
49
+ retention: DEFAULT_RETENTION,
37
50
  };