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.
- package/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
875
|
-
|
|
876
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
1069
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
249
|
+
try {
|
|
250
|
+
await waitUntilExit();
|
|
251
|
+
} finally {
|
|
252
|
+
uninstallTerminalCapture();
|
|
253
|
+
}
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
/**
|
package/src/memory/constants.js
CHANGED
|
@@ -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
|
};
|
package/src/memory/constants.ts
CHANGED
|
@@ -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
|
};
|