mocode-ai 1.3.9 → 1.4.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/README.md +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
package/dist/agent/core.js
CHANGED
|
@@ -4,154 +4,32 @@
|
|
|
4
4
|
//
|
|
5
5
|
// 与 index.ts 的关系:index.ts 的 runAgent = runAgentCore + TUI hooks 薄封装(行为不变)。
|
|
6
6
|
// spawn.ts 的 spawnAgent = runAgentCore + 静默 hooks(子 agent)。
|
|
7
|
-
import { readFileSync } from 'node:fs';
|
|
8
|
-
import { getNotesMtime } from '../session/notes.js';
|
|
9
7
|
import { chat, estimatePromptTokens, estimateTokens, isContextLengthError, planChatTools, chatTools, } from '../llm/index.js';
|
|
10
|
-
import { executeToolOutcome, findTool,
|
|
8
|
+
import { executeToolOutcome, findTool, isFileMutationTool } from '../tools/registry.js';
|
|
11
9
|
import { checkPermission } from '../permissions/index.js';
|
|
12
10
|
import { validateToolArguments } from '../tools/validation.js';
|
|
13
|
-
import { getPlanDisabledTools, getRuntimeDisabledTools } from '../tools/constants.js';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
11
|
+
import { getPlanDisabledTools, getRuntimeDisabledTools, getSkillRuntimeDisabledTools } from '../tools/constants.js';
|
|
12
|
+
import { ADD_TOOL_GROUPS_TOOL_NAME } from '../config/profiles.js';
|
|
13
|
+
import { defaultAgentRuntimeContext } from './runtime-context.js';
|
|
14
|
+
import { parseArgs, argumentErrorHint, isToolResultsNoise, isParallelTool, isResourceLockedCall, deniedOutcome, readDiffContext, pushToolResult, } from './tool-helpers.js';
|
|
15
|
+
import { maybeCompact, contextState, summarizeToolArguments } from '../session/index.js';
|
|
16
|
+
import { TurnTraceState } from './trace-state.js';
|
|
17
17
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
18
|
-
import {
|
|
18
|
+
import { invalidateArtifacts, rehydrateArtifacts } from '../context/index.js';
|
|
19
19
|
import { createRelevancePruner } from '../context/relevance.js';
|
|
20
|
-
import { isToolResultSuccess } from '../context/utils.js';
|
|
21
|
-
import { config, getActiveModel, extractActivePlanSection, buildSessionStateReminder } from '../config/index.js';
|
|
22
20
|
import { t } from '../i18n/index.js';
|
|
23
|
-
import { jailResolve } from '../sandbox/index.js';
|
|
24
21
|
import { createLifecycleEngine } from '../context/lifecycle.js';
|
|
25
|
-
import { getTokenCalibration, updateTokenCalibration, } from '../context/token-calibration.js';
|
|
26
|
-
import { getCurrentTurnId, getCurrentTurnMutationState } from '../rollback/index.js';
|
|
27
|
-
import { getCurrentSessionId } from '../session/state.js';
|
|
28
22
|
/** nag 提醒阈值:连续 N 个"执行了工具但没更新 notes.md"的步后提醒一次(对齐 Claude Code TodoWrite 的 3 轮)。 */
|
|
29
23
|
const PLAN_NAG_THRESHOLD = 3;
|
|
30
24
|
/** nag 提醒文本:注入到当前步第一条 tool_result 内容前(与最新工具输出同批被模型看到,而非单独一条易被冲淡)。 */
|
|
31
25
|
const PLAN_NAG_TEXT = '[mocode] Reminder: you have an active plan in notes.md but have not updated it recently. ' +
|
|
32
26
|
'If you finished a step, call plan_update to check it off (keep at most one in_progress); ' +
|
|
33
27
|
'if the whole plan is done, let plan_update settle it to ## Done:. If the plan changed scope, update it to match reality.';
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return raw.trim() ? JSON.parse(raw) : {};
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/** 需要「已知编辑目标」恢复提示的文件编辑工具;其余工具的参数报错不注入该提示。 */
|
|
44
|
-
const EDIT_HINT_TOOLS = new Set(['edit_file', 'write_file']);
|
|
45
|
-
/** 文件编辑工具参数校验失败时的恢复提示:把系统已知仍新鲜的「最近 read_file 的
|
|
46
|
-
* path + hash」直接递给模型照抄,替代其在长上下文里凭记忆复述。
|
|
47
|
-
* 只展示事实、不替模型填值;没有候选(从未 read_file / 全部已失效)返 undefined。 */
|
|
48
|
-
function argumentErrorHint(name, state) {
|
|
49
|
-
if (!EDIT_HINT_TOOLS.has(name))
|
|
50
|
-
return undefined;
|
|
51
|
-
const targets = knownEditTargets(state);
|
|
52
|
-
if (targets.length === 0)
|
|
53
|
-
return undefined;
|
|
54
|
-
const lines = targets.map((target) => ` path=${target.path} expected_hash=${target.hash}`).join('\n');
|
|
55
|
-
return ('系统已知最近 read_file 且尚未被修改的文件(直接复制下面的 path / expected_hash,勿凭记忆复述):\n' +
|
|
56
|
-
`${lines}\n如目标文件不在其中,先 read_file 该文件再发起编辑。`);
|
|
57
|
-
}
|
|
58
|
-
/** 判定 assistant content 是否只是 "Tool results:" 这类工具结果前缀噪声。
|
|
59
|
-
* 部分模型(如 Claude)会在 tool_calls 前输出此种无意义过渡文本,写入 history
|
|
60
|
-
* 会污染后续轮次上下文并在 TUI 上泄露为孤立行。 */
|
|
61
|
-
function isToolResultsNoise(content) {
|
|
62
|
-
return /^(?:\s*Tool results:\s*)+$/i.test(content.trim());
|
|
63
|
-
}
|
|
64
|
-
/** 只有显式声明 parallel 且无需权限确认的工具才进入普通并发组。 */
|
|
65
|
-
function isParallelTool(name) {
|
|
66
|
-
const tool = findTool(name);
|
|
67
|
-
return !!tool && (tool.risk ?? 'safe') === 'safe' &&
|
|
68
|
-
getToolCapabilities(tool).concurrency === 'parallel';
|
|
69
|
-
}
|
|
70
|
-
/** resource-locked 工具先顺序完成权限预检,再依赖 canonical resource lock 并发执行。 */
|
|
71
|
-
function isResourceLockedTool(name) {
|
|
72
|
-
const tool = findTool(name);
|
|
73
|
-
return !!tool && getToolCapabilities(tool).concurrency === 'resource-locked';
|
|
74
|
-
}
|
|
75
|
-
function isResourceLockedCall(call) {
|
|
76
|
-
if (!isResourceLockedTool(call.name))
|
|
77
|
-
return false;
|
|
78
|
-
if (call.name !== 'sub-agent')
|
|
79
|
-
return true;
|
|
80
|
-
const args = parseArgs(call.arguments);
|
|
81
|
-
// Unknown write sets stay on the serial path. Read tasks and known disjoint write sets may batch.
|
|
82
|
-
return args?.mode !== 'write' || (Array.isArray(args.writeSet) && args.writeSet.length > 0);
|
|
83
|
-
}
|
|
28
|
+
// 工具辅助纯函数(parseArgs / argumentErrorHint / isToolResultsNoise / isParallelTool /
|
|
29
|
+
// isResourceLockedTool / isResourceLockedCall / deniedOutcome / readDiffContext / pushToolResult)
|
|
30
|
+
// 已提取至 ./tool-helpers.ts——它们不依赖本循环的局部状态,只接受显式参数,故可安全模块化。
|
|
84
31
|
/** 文件 mutation 由 capability metadata 判定,供 diff、回滚与上下文失效共用。 */
|
|
85
32
|
const isMutationTool = (name) => isFileMutationTool(name);
|
|
86
|
-
function deniedOutcome(name) {
|
|
87
|
-
return {
|
|
88
|
-
status: 'denied',
|
|
89
|
-
code: 'PERMISSION_DENIED',
|
|
90
|
-
retryable: false,
|
|
91
|
-
output: `错误:用户拒绝了工具 ${name} 的执行。`,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
/** mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
95
|
-
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
96
|
-
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。 */
|
|
97
|
-
function readDiffContext(tc, parsed) {
|
|
98
|
-
if (!parsed)
|
|
99
|
-
return { preWriteOld: null, editStartLine: 1 };
|
|
100
|
-
const p = String(parsed.path ?? '');
|
|
101
|
-
if (!p)
|
|
102
|
-
return { preWriteOld: null, editStartLine: 1 };
|
|
103
|
-
if (tc.name === 'write_file') {
|
|
104
|
-
try {
|
|
105
|
-
// jailResolve:沙箱越界(../../、绝对外圈、软链出圈)抛错 → catch 兜底返 null,不泄露牢外内容(TOCTOU)
|
|
106
|
-
return { preWriteOld: readFileSync(jailResolve(p), 'utf8'), editStartLine: 1 };
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return { preWriteOld: null, editStartLine: 1 }; // 文件不存在(新建)、不可读 或 沙箱越界(不泄露)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (tc.name === 'edit_file') {
|
|
113
|
-
// 行尾归一化:LLM 生成的 old_string 用 LF(\n),但 Windows 文件可能是 CRLF(\r\n),
|
|
114
|
-
// 不统一则 indexOf 必败、editStartLine 恒为 1。与 edit-file.ts 保持一致归一化为 LF。
|
|
115
|
-
const oldStr = String(parsed.old_string ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
116
|
-
try {
|
|
117
|
-
// jailResolve:同上,沙箱越界抛错 → catch 兜底,不泄露牢外内容
|
|
118
|
-
const raw = readFileSync(jailResolve(p), 'utf8');
|
|
119
|
-
const data = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
120
|
-
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
121
|
-
return {
|
|
122
|
-
preWriteOld: null,
|
|
123
|
-
editStartLine: idx >= 0 ? data.slice(0, idx).split('\n').length : 1,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return { preWriteOld: null, editStartLine: 1 }; // 读不到:diff 退化为相对行号(含沙箱越界)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return { preWriteOld: null, editStartLine: 1 };
|
|
131
|
-
}
|
|
132
|
-
/** 回灌 tool 结果到 history。
|
|
133
|
-
* 正常路径只做单条 hard cap;原始 output 同时供 TUI 展示,因此用户与模型
|
|
134
|
-
* 看到同一事实。Artifact/Relevance/Lifecycle 仅登记 metadata/provenance,
|
|
135
|
-
* 不在这里改写旧正文;所有自动清理与压缩统一由 80% pressure scheduler 决定。 */
|
|
136
|
-
function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runtimeContextState = contextState, succeededOverride) {
|
|
137
|
-
const succeeded = succeededOverride ?? isToolResultSuccess(output);
|
|
138
|
-
const msg = {
|
|
139
|
-
role: 'tool',
|
|
140
|
-
tool_call_id: tc.id,
|
|
141
|
-
// Preserve evidence verbatim in normal operation; the hard per-result cap
|
|
142
|
-
// remains solely as a request-size safety rail.
|
|
143
|
-
content: capToolResultForHistory(tc.name, output),
|
|
144
|
-
};
|
|
145
|
-
history.push(msg);
|
|
146
|
-
const messageIndex = history.length - 1;
|
|
147
|
-
recordArtifact(runtimeContextState, history, messageIndex, output, succeeded);
|
|
148
|
-
// 失败 read 不得淘汰旧 read;失败 consumer 也不能改变 lifecycle 上游状态。
|
|
149
|
-
if (pruner)
|
|
150
|
-
pruner.observePush(history, msg, succeeded);
|
|
151
|
-
if (lifecycle)
|
|
152
|
-
lifecycle.pushTool(history, messageIndex, succeeded);
|
|
153
|
-
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
154
|
-
}
|
|
155
33
|
/**
|
|
156
34
|
* agent 核心循环(纯逻辑):
|
|
157
35
|
* 流式调 LLM(经 hooks.onText 实时渲染)→ 有 tool_calls 就分组执行并回灌
|
|
@@ -169,67 +47,41 @@ function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runt
|
|
|
169
47
|
*/
|
|
170
48
|
export async function runAgentCore(opts) {
|
|
171
49
|
const { history, userInput, signal, onContextUpdate, hooks } = opts;
|
|
50
|
+
const ctx = opts.runtimeContext ?? defaultAgentRuntimeContext;
|
|
172
51
|
const runtimeContextState = opts.contextState ?? contextState;
|
|
173
52
|
/** 本轮 ask_human 成功调用次数,仅用于 trace 观测,不影响工具执行或模型上下文。 */
|
|
174
53
|
let askHumanCountThisTurn = 0;
|
|
175
|
-
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
54
|
+
const maxSteps = opts.maxSteps ?? ctx.config.maxSteps;
|
|
176
55
|
// 中断还原:repl 的 /plan / /auto / Shift+Tab 等用户面触发 setAgentMode 中途切了模式,
|
|
177
56
|
// abort 时连同模式一起还原回轮首。模型不再持有 switch_mode 工具,无法自切。
|
|
178
|
-
const savedMode = getAgentMode();
|
|
57
|
+
const savedMode = ctx.getAgentMode();
|
|
179
58
|
// 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
|
|
180
59
|
const t0 = Date.now();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}),
|
|
195
|
-
...ids,
|
|
196
|
-
data,
|
|
197
|
-
}));
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// Trace is best-effort and must never alter execution.
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
emitTrace('turn_start', { mode: getAgentMode() });
|
|
60
|
+
// trace / token-usage 状态聚合(2.0 步骤2 深拆第一刀):emit/addUsage/turnUsage 收敛进
|
|
61
|
+
// TurnTraceState,事件 payload 与 hooks 序列保持字节级不变。
|
|
62
|
+
const traceState = new TurnTraceState({
|
|
63
|
+
sessionId: opts.traceContext?.sessionId ?? ctx.getCurrentSessionId() ?? `ephemeral-${process.pid}`,
|
|
64
|
+
turnId: opts.traceContext?.turnId ?? ctx.getCurrentTurnId(),
|
|
65
|
+
onTraceEvent: opts.onTraceEvent,
|
|
66
|
+
});
|
|
67
|
+
const traceSessionId = traceState.sessionId;
|
|
68
|
+
const traceTurnId = traceState.turnId;
|
|
69
|
+
const emitTrace = (type, data = {}, ids = {}) => traceState.emit(type, data, ids);
|
|
70
|
+
emitTrace('turn_start', { mode: ctx.getAgentMode() });
|
|
71
|
+
if (opts.initialToolRoute)
|
|
72
|
+
emitTrace('tool_route', opts.initialToolRoute);
|
|
204
73
|
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
205
74
|
let traceStatus = 'error';
|
|
206
|
-
let toolCallCount = 0;
|
|
207
75
|
// A+B(plan 可靠性):跨步计数"执行了工具但没改动 notes.md"的连续步数。
|
|
208
76
|
// 本步写了 notes.md(plan_update 或直接 write/edit)→ 清零并重同步 history[0];
|
|
209
77
|
// 否则累计,达阈值则在当前步 tool_result 前注入 nag 提醒。
|
|
210
78
|
let stepsSincePlanTouch = 0;
|
|
211
|
-
// 本轮 token
|
|
79
|
+
// 本轮 token 累计在 traceState.turnUsage(每步 chat() 返回后 addUsage),供 onDone 摘要行 + AgentRunResult.usage
|
|
212
80
|
// 透传给 repl(显示在底栏模式 chip 右边)。未开启 include_usage 或全失败时为 undefined。
|
|
213
|
-
let turnUsage;
|
|
214
81
|
// 实时 chip ↻ 估算用:上一步 chat 实测 prompt(前缀缓存下当前步命中 ≈ 它)+ 后端是否报过 cache 命中
|
|
215
82
|
// (从不报 cache 的后端不估算,避免虚显 ↻)。
|
|
216
83
|
let lastStepPromptTokens = 0;
|
|
217
84
|
let providerCacheSeen = false;
|
|
218
|
-
const addUsage = (u) => {
|
|
219
|
-
if (!u)
|
|
220
|
-
return;
|
|
221
|
-
turnUsage = turnUsage
|
|
222
|
-
? {
|
|
223
|
-
promptTokens: turnUsage.promptTokens + u.promptTokens,
|
|
224
|
-
completionTokens: turnUsage.completionTokens + u.completionTokens,
|
|
225
|
-
totalTokens: turnUsage.totalTokens + u.totalTokens,
|
|
226
|
-
cachedTokens: turnUsage.cachedTokens + u.cachedTokens,
|
|
227
|
-
cacheCreationTokens: (turnUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0),
|
|
228
|
-
reasoningTokens: turnUsage.reasoningTokens + u.reasoningTokens,
|
|
229
|
-
}
|
|
230
|
-
: u;
|
|
231
|
-
};
|
|
232
|
-
const addToolUsage = (outcome) => addUsage(outcome.usage);
|
|
233
85
|
history.push({ role: 'user', content: userInput });
|
|
234
86
|
// 中断回滚快照:push 用户消息后整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
235
87
|
// 这样中断时至少保留用户消息(及之前的历史);每步工具全部执行完毕后刷新快照,
|
|
@@ -238,18 +90,14 @@ export async function runAgentCore(opts) {
|
|
|
238
90
|
let savedHistory = history.slice();
|
|
239
91
|
// Relevance and lifecycle collect provenance during normal work. Neither path
|
|
240
92
|
// rewrites history; exact supersession is applied only by the pressure scheduler.
|
|
241
|
-
const relprune = config.contextRelprune ? createRelevancePruner() : null;
|
|
242
|
-
let lifecycle = config.contextLifecycle
|
|
243
|
-
? createLifecycleEngine(history)
|
|
244
|
-
: null;
|
|
93
|
+
const relprune = ctx.config.contextRelprune ? createRelevancePruner() : null;
|
|
94
|
+
let lifecycle = ctx.config.contextLifecycle ? createLifecycleEngine(history) : null;
|
|
245
95
|
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
246
96
|
rehydrateArtifacts(runtimeContextState, history);
|
|
247
97
|
// The scheduler is the sole automatic history-rewrite entry point. It runs
|
|
248
98
|
// superseded → stale artifact → old logs/search → compact at real pressure.
|
|
249
99
|
// contextBudget=false keeps only the infrastructure compact fallback.
|
|
250
|
-
const scheduler = config.contextBudget !== false
|
|
251
|
-
? createBudgetScheduler(runtimeContextState)
|
|
252
|
-
: null;
|
|
100
|
+
const scheduler = ctx.config.contextBudget !== false ? createBudgetScheduler(runtimeContextState) : null;
|
|
253
101
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
254
102
|
let mode = 'idle';
|
|
255
103
|
let gotText = false;
|
|
@@ -272,19 +120,11 @@ export async function runAgentCore(opts) {
|
|
|
272
120
|
};
|
|
273
121
|
// 中断还原:停 spinner + 补换行 + (已中断)提示 + history 还原到本 turn 前 + 模式还原。
|
|
274
122
|
// 两处共用:① await chat() 抛 AbortError 的 catch;② 工具被 abort 杀后循环顶检查。
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
emitTrace('abort', { phase: 'observed', reason: 'signal' });
|
|
278
|
-
abortTraced = true;
|
|
279
|
-
}
|
|
280
|
-
hooks.onAbort?.();
|
|
281
|
-
history.length = 0;
|
|
282
|
-
history.push(...savedHistory);
|
|
283
|
-
setAgentMode(savedMode);
|
|
284
|
-
};
|
|
123
|
+
// 实现已收敛进 TurnTraceState.abortRestore;savedHistory/savedMode 是函数级 let,此处闭包现读。
|
|
124
|
+
const abortRestore = () => traceState.abortRestore({ hooks, history, savedHistory, ctx, savedMode });
|
|
285
125
|
try {
|
|
286
126
|
for (let step = 0; step < maxSteps; step++) {
|
|
287
|
-
currentTraceStep = step;
|
|
127
|
+
traceState.currentTraceStep = step;
|
|
288
128
|
const stepStartedAt = Date.now();
|
|
289
129
|
emitTrace('step_start', { ordinal: step });
|
|
290
130
|
try {
|
|
@@ -292,21 +132,45 @@ export async function runAgentCore(opts) {
|
|
|
292
132
|
if (signal?.aborted) {
|
|
293
133
|
abortRestore();
|
|
294
134
|
traceStatus = 'aborted';
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
completed: false,
|
|
298
|
-
terminationReason: 'aborted',
|
|
299
|
-
finalText: null,
|
|
300
|
-
usage: turnUsage,
|
|
301
|
-
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
302
|
-
};
|
|
135
|
+
return traceState.buildAbortedResult(ctx.getCurrentTurnMutationState());
|
|
303
136
|
}
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
const
|
|
309
|
-
const
|
|
137
|
+
// 本步只捕获一次不可变 policy snapshot。即便 add_tool_groups 在执行阶段扩容,
|
|
138
|
+
// 本次模型响应仍必须按旧 snapshot 校验;新工具只在下一 step 的 schema 中出现。
|
|
139
|
+
const planMode = ctx.getAgentMode() === 'plan';
|
|
140
|
+
const policySnapshot = opts.toolPolicy?.snapshot(planMode);
|
|
141
|
+
const configuredTools = opts.toolsOverride ?? policySnapshot?.tools ?? (planMode ? planChatTools : chatTools);
|
|
142
|
+
const policyAllowedTools = opts.runtimeAllowedToolNames
|
|
143
|
+
? configuredTools.filter((tool) => opts.runtimeAllowedToolNames?.has(tool.function.name))
|
|
144
|
+
: configuredTools;
|
|
145
|
+
const skillDisabledTools = getSkillRuntimeDisabledTools();
|
|
146
|
+
const legacyDisabledTools = opts.toolPolicy || opts.runtimeAllowedToolNames ? new Set() : getRuntimeDisabledTools();
|
|
147
|
+
// schema、runtime backstop 与后代权限都从同一 effective allow-list 派生。
|
|
148
|
+
// policy snapshot 是本 step 的不可扩张上限;skill deny 可在同批 use_skill 后继续动态收窄。
|
|
149
|
+
const activeTools = policyAllowedTools.filter((tool) => !skillDisabledTools.has(tool.function.name) && !legacyDisabledTools.has(tool.function.name));
|
|
150
|
+
const stepAllowedNames = new Set(activeTools.map((tool) => tool.function.name));
|
|
151
|
+
const currentAllowedToolNames = () => {
|
|
152
|
+
const currentSkillDisabledTools = getSkillRuntimeDisabledTools();
|
|
153
|
+
return [...stepAllowedNames].filter((name) => !currentSkillDisabledTools.has(name));
|
|
154
|
+
};
|
|
155
|
+
const isToolDeniedForStep = (name) => !stepAllowedNames.has(name) || getSkillRuntimeDisabledTools().has(name);
|
|
156
|
+
// 委派给编排工具(sub-agent/run_skill)的父前缀快照:去掉历史末尾「产生本次调用的
|
|
157
|
+
// assistant tool_call 消息」(协议上它必须紧跟 tool_result,不能出现在子 history),
|
|
158
|
+
// 只保留其前的主前缀。子 agent 以它为前缀、尾部追加委派消息 → 与主 agent 已发送
|
|
159
|
+
// 前缀逐字节一致,命中前缀缓存。tools 直接用本步 activeTools:子 agent 与主 agent
|
|
160
|
+
// 同权同 schema,不做任何裁剪,也没有额外的执行层禁用集合。
|
|
161
|
+
const delegationForOrchestrator = () => {
|
|
162
|
+
let k = history.length - 1;
|
|
163
|
+
while (k > 0) {
|
|
164
|
+
const m = history[k];
|
|
165
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls))
|
|
166
|
+
break;
|
|
167
|
+
k--;
|
|
168
|
+
}
|
|
169
|
+
return { history: history.slice(0, k > 0 ? k : history.length), tools: activeTools };
|
|
170
|
+
};
|
|
171
|
+
const requestBaseURL = ctx.config.baseURL;
|
|
172
|
+
const requestModel = ctx.getActiveModel();
|
|
173
|
+
const storedCalibration = ctx.getTokenCalibration(requestBaseURL, requestModel, activeTools);
|
|
310
174
|
runtimeContextState.correction = storedCalibration.correction;
|
|
311
175
|
runtimeContextState.calibrationSamples = storedCalibration.samples;
|
|
312
176
|
// 会话状态(活跃 plan + 笔记正文)在调度器**之前**取一次:
|
|
@@ -314,7 +178,7 @@ export async function runAgentCore(opts) {
|
|
|
314
178
|
// 必须计入压力线——它不在 history 里,调度器只能由此入参看见(否则最多 5k
|
|
315
179
|
// 的笔记 + plan 段对 80% 触发线完全不可见,小窗口模型会压不住);
|
|
316
180
|
// ② 压缩步在压缩成功后重取(P2 固结的 Compaction Snapshot 当步即可见)。
|
|
317
|
-
let sessionStateText = opts.suppressSessionState ? '' : buildSessionStateReminder();
|
|
181
|
+
let sessionStateText = opts.suppressSessionState ? '' : ctx.buildSessionStateReminder();
|
|
318
182
|
// The scheduler is the only automatic path that may compress old evidence.
|
|
319
183
|
// Normal tool pushes and lifecycle tracking remain metadata-only.
|
|
320
184
|
// 压缩**之前**先刷一次状态栏:bar 要显示「本步真实 prompt 撞线」那一刻。
|
|
@@ -368,7 +232,7 @@ export async function runAgentCore(opts) {
|
|
|
368
232
|
// 末尾就带上最新 Compaction Snapshot,不必等下一步。bar 口径对应的
|
|
369
233
|
// ephemeralText 仍用触发时旧值(见 buildRequestHistory 注释),仅差这一段。
|
|
370
234
|
if (!opts.suppressSessionState)
|
|
371
|
-
sessionStateText = buildSessionStateReminder();
|
|
235
|
+
sessionStateText = ctx.buildSessionStateReminder();
|
|
372
236
|
// 会话状态(活跃 plan + 笔记段)不再回写 history[0]:每步都会在 requestHistory
|
|
373
237
|
// 末尾注入最新副本(见下方 ephemeralReminder),compact 后自然恢复。
|
|
374
238
|
}
|
|
@@ -378,7 +242,7 @@ export async function runAgentCore(opts) {
|
|
|
378
242
|
lastChar = '';
|
|
379
243
|
let result;
|
|
380
244
|
const modelStartedAt = Date.now();
|
|
381
|
-
const provider = safeProviderId(requestBaseURL);
|
|
245
|
+
const provider = ctx.safeProviderId(requestBaseURL);
|
|
382
246
|
emitTrace('model_start', { model: requestModel, provider });
|
|
383
247
|
// 动态注入(prompt 缓存关键):所有随步/随文件变化的提示统一拼成**历史末尾**一条
|
|
384
248
|
// ephemeral system 消息,不再改写 history[0]。这样系统提示 + 已有对话逐字节稳定,
|
|
@@ -398,7 +262,8 @@ export async function runAgentCore(opts) {
|
|
|
398
262
|
// 不计,让 bar 用会让两条线再错开几百 token。
|
|
399
263
|
const buildRequestHistory = () => {
|
|
400
264
|
const ephemeralReminder = [
|
|
401
|
-
|
|
265
|
+
opts.toolPolicy?.reminder(planMode) ?? '',
|
|
266
|
+
!opts.suppressOpeningAnalysis && step === 0
|
|
402
267
|
? '## Opening analysis\nBegin your FIRST response of this turn with a brief analysis of the request and your planned approach (1-3 sentences, no filler), THEN start tool calls. This opening is the only place where pre-tool prose is expected; after it, work quietly with no narration between tool calls.'
|
|
403
268
|
: '',
|
|
404
269
|
historyRebuilt
|
|
@@ -413,7 +278,9 @@ export async function runAgentCore(opts) {
|
|
|
413
278
|
'4. Before re-running a search/read you think you already did, check the summary and notes first: only repeat it if the result is genuinely missing or the target has changed.'
|
|
414
279
|
: '',
|
|
415
280
|
sessionStateText, // 调度器之前已取(并计入压力线),此处复用同一份,不重复读文件
|
|
416
|
-
]
|
|
281
|
+
]
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
.join('\n\n');
|
|
417
284
|
return ephemeralReminder
|
|
418
285
|
? [...history, { role: 'system', content: ephemeralReminder }]
|
|
419
286
|
: history;
|
|
@@ -424,25 +291,9 @@ export async function runAgentCore(opts) {
|
|
|
424
291
|
// 那一次才是解释「为什么要压」的。
|
|
425
292
|
onContextUpdate?.();
|
|
426
293
|
// 实时用量:当前步 prompt 估算(含校准系数)+ 流式累计 completion 估算,
|
|
427
|
-
// 叠上已完成步的实测 turnUsage,经 onLiveUsage 推给底栏实时 chip。
|
|
428
|
-
// turnUsage 在闭包里被 addUsage 原地更新,reportLive 每次调用读最新值。
|
|
294
|
+
// 叠上已完成步的实测 turnUsage(traceState 内累加,每次调用读最新值),经 onLiveUsage 推给底栏实时 chip。
|
|
429
295
|
let stepPromptEst = estimatePromptTokens(requestHistory, activeTools, runtimeContextState.correction);
|
|
430
|
-
const reportLive = (p) =>
|
|
431
|
-
// 当前步 prompt:末尾 usage chunk 到达后用实测,流式期间用估算(含校准)。
|
|
432
|
-
// 当前步 cache 命中同理:上报即用实测;流式期间按前缀缓存估算 ≈ 上一步实测 prompt
|
|
433
|
-
// (当前 prompt 总含其为前缀),不超过当前步 prompt;后端从不报 cache 时不估算。
|
|
434
|
-
// 口径与轮末摘要一致:chip ↑ 显计费 prompt(裸 - cached),↓/↻ 同。
|
|
435
|
-
const curPrompt = p.promptTokens ?? stepPromptEst;
|
|
436
|
-
const curCached = p.cachedTokens ?? (providerCacheSeen
|
|
437
|
-
? Math.min(lastStepPromptTokens, curPrompt)
|
|
438
|
-
: 0);
|
|
439
|
-
hooks.onLiveUsage?.({
|
|
440
|
-
promptTokens: (turnUsage?.promptTokens ?? 0) + curPrompt,
|
|
441
|
-
completionTokens: (turnUsage?.completionTokens ?? 0) + p.completionTokens,
|
|
442
|
-
totalTokens: (turnUsage?.totalTokens ?? 0) + curPrompt + p.completionTokens,
|
|
443
|
-
cachedTokens: (turnUsage?.cachedTokens ?? 0) + curCached,
|
|
444
|
-
});
|
|
445
|
-
};
|
|
296
|
+
const reportLive = (p) => traceState.reportLive(hooks, stepPromptEst, lastStepPromptTokens, providerCacheSeen, p);
|
|
446
297
|
reportLive({ completionTokens: 0 }); // 思考阶段先显 ↑ prompt 估算,首 token 到达后 ↓ 开始涨
|
|
447
298
|
// 单一 chat 入口:错误侧的 model_end 埋点只写一处(重试也会记,不丢失败轨迹)。
|
|
448
299
|
const chatHandlers = {
|
|
@@ -463,16 +314,14 @@ export async function runAgentCore(opts) {
|
|
|
463
314
|
return await chat(requestHistory, chatHandlers, signal, activeTools);
|
|
464
315
|
}
|
|
465
316
|
catch (err) {
|
|
466
|
-
const errorValue = err && typeof err === 'object'
|
|
467
|
-
? err
|
|
468
|
-
: undefined;
|
|
317
|
+
const errorValue = err && typeof err === 'object' ? err : undefined;
|
|
469
318
|
emitTrace('model_end', {
|
|
470
319
|
model: requestModel,
|
|
471
320
|
provider,
|
|
472
321
|
status: signal?.aborted ? 'aborted' : 'error',
|
|
473
322
|
code: typeof errorValue?.status === 'number'
|
|
474
323
|
? `HTTP_${errorValue.status}`
|
|
475
|
-
: errorValue?.code ?? errorValue?.name ?? 'MODEL_ERROR',
|
|
324
|
+
: (errorValue?.code ?? errorValue?.name ?? 'MODEL_ERROR'),
|
|
476
325
|
durationMs: Date.now() - modelStartedAt,
|
|
477
326
|
});
|
|
478
327
|
throw err;
|
|
@@ -484,19 +333,10 @@ export async function runAgentCore(opts) {
|
|
|
484
333
|
catch (e) {
|
|
485
334
|
// 中断(用户运行中 Ctrl+C):chat() 抛 AbortError(signal.aborted)→ 还原 history + 模式 + return(不抛)。
|
|
486
335
|
// 工具执行现已串 signal:run_command/web_fetch 被 abort 即时杀,循环顶检查兜底(不会留未配对 tool_call_id)。
|
|
487
|
-
if (signal?.aborted ||
|
|
488
|
-
(e instanceof Error &&
|
|
489
|
-
(e.name === 'AbortError' || e.name === 'APIUserAbortError'))) {
|
|
336
|
+
if (signal?.aborted || (e instanceof Error && (e.name === 'AbortError' || e.name === 'APIUserAbortError'))) {
|
|
490
337
|
abortRestore();
|
|
491
338
|
traceStatus = 'aborted';
|
|
492
|
-
|
|
493
|
-
return {
|
|
494
|
-
completed: false,
|
|
495
|
-
terminationReason: 'aborted',
|
|
496
|
-
finalText: null,
|
|
497
|
-
usage: turnUsage,
|
|
498
|
-
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
499
|
-
};
|
|
339
|
+
return traceState.buildAbortedResult(ctx.getCurrentTurnMutationState());
|
|
500
340
|
}
|
|
501
341
|
// 后端实测拒绝了 prompt(上下文超长):本地估算对该 provider 系统性偏低时,
|
|
502
342
|
// 压力线压不住,这是唯一可信的触发。强压一轮后重试一次;仍失败才抛(限一次,防循环)。
|
|
@@ -508,7 +348,7 @@ export async function runAgentCore(opts) {
|
|
|
508
348
|
// 偶发误判会被后续真实 usage 样本拉回。
|
|
509
349
|
const rawEstimate = estimatePromptTokens(requestHistory, activeTools);
|
|
510
350
|
if (rawEstimate > 1_000) {
|
|
511
|
-
const cal = updateTokenCalibration(requestBaseURL, requestModel, activeTools, rawEstimate, config.contextWindowTokens);
|
|
351
|
+
const cal = ctx.updateTokenCalibration(requestBaseURL, requestModel, activeTools, rawEstimate, ctx.config.contextWindowTokens);
|
|
512
352
|
runtimeContextState.correction = cal.correction;
|
|
513
353
|
runtimeContextState.calibrationSamples = cal.samples;
|
|
514
354
|
}
|
|
@@ -551,7 +391,7 @@ export async function runAgentCore(opts) {
|
|
|
551
391
|
reasoningTokens: result.usage?.reasoningTokens,
|
|
552
392
|
});
|
|
553
393
|
runtimeContextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
554
|
-
addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
|
|
394
|
+
traceState.addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
|
|
555
395
|
if (result.usage) {
|
|
556
396
|
lastStepPromptTokens = result.usage.promptTokens; // 下一步流式期 ↻ 估算的前缀基准
|
|
557
397
|
if (result.usage.cachedTokens > 0)
|
|
@@ -561,7 +401,7 @@ export async function runAgentCore(opts) {
|
|
|
561
401
|
// 只持久化比例与样本数;无 usage 或短 prompt 时保持既有值。
|
|
562
402
|
if (result.usage?.promptTokens && result.usage.promptTokens > 100) {
|
|
563
403
|
const estimated = estimatePromptTokens(requestHistory, activeTools);
|
|
564
|
-
const updated = updateTokenCalibration(requestBaseURL, requestModel, activeTools, estimated, result.usage.promptTokens);
|
|
404
|
+
const updated = ctx.updateTokenCalibration(requestBaseURL, requestModel, activeTools, estimated, result.usage.promptTokens);
|
|
565
405
|
runtimeContextState.correction = updated.correction;
|
|
566
406
|
runtimeContextState.calibrationSamples = updated.samples;
|
|
567
407
|
}
|
|
@@ -569,7 +409,7 @@ export async function runAgentCore(opts) {
|
|
|
569
409
|
// lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
|
|
570
410
|
onContextUpdate?.();
|
|
571
411
|
if (result.toolCalls.length > 0) {
|
|
572
|
-
toolCallCount += result.toolCalls.length;
|
|
412
|
+
traceState.toolCallCount += result.toolCalls.length;
|
|
573
413
|
// 若 content 只是 Claude 式 "Tool results:" 噪声,清空它:不补换行、不写入 history,
|
|
574
414
|
// 避免污染后续轮次上下文并在 TUI 泄露为孤立行。
|
|
575
415
|
if (result.content && isToolResultsNoise(result.content)) {
|
|
@@ -594,7 +434,7 @@ export async function runAgentCore(opts) {
|
|
|
594
434
|
// A+B:记录本步第一条 tool_result 的下标 + 执行前 notes.md 的 mtime,
|
|
595
435
|
// 工具全部执行完后据此判断"本步是否改动了 notes.md"(重同步 / nag)。
|
|
596
436
|
const toolResultStartIdx = history.length;
|
|
597
|
-
const notesMtimeBefore = getNotesMtime();
|
|
437
|
+
const notesMtimeBefore = ctx.getNotesMtime();
|
|
598
438
|
// Record interstitial narration for observability only. It never changes tool output
|
|
599
439
|
// or injects instructions back into the model context.
|
|
600
440
|
const narration = result.content?.trim() ?? '';
|
|
@@ -649,10 +489,104 @@ export async function runAgentCore(opts) {
|
|
|
649
489
|
...(tc.id ? { providerToolCallId: tc.id } : {}),
|
|
650
490
|
});
|
|
651
491
|
};
|
|
652
|
-
|
|
492
|
+
const hasToolRouteBarrier = calls.some((tc) => tc.name === ADD_TOOL_GROUPS_TOOL_NAME);
|
|
493
|
+
if (hasToolRouteBarrier) {
|
|
494
|
+
const mixedCall = calls.length !== 1;
|
|
495
|
+
for (let index = 0; index < calls.length; index++) {
|
|
496
|
+
const tc = calls[index];
|
|
497
|
+
hooks.onToolHeader?.(tc);
|
|
498
|
+
const parsed = parseArgs(tc.arguments);
|
|
499
|
+
let outcome;
|
|
500
|
+
if (mixedCall) {
|
|
501
|
+
const isControl = tc.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
502
|
+
outcome = {
|
|
503
|
+
status: 'denied',
|
|
504
|
+
code: isControl ? 'INVALID_ARGUMENTS' : 'TOOL_DISABLED',
|
|
505
|
+
retryable: false,
|
|
506
|
+
output: isControl
|
|
507
|
+
? '错误:add_tool_groups 必须在一个独立的 model step 中单独调用;本次没有扩容。'
|
|
508
|
+
: `错误:同一响应包含 add_tool_groups,工具 ${tc.name} 未执行。请等待扩容结果后在下一 step 重试。`,
|
|
509
|
+
changedFiles: [],
|
|
510
|
+
durationMs: 0,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
else if (isToolDeniedForStep(tc.name)) {
|
|
514
|
+
outcome = {
|
|
515
|
+
status: 'denied',
|
|
516
|
+
code: 'TOOL_DISABLED',
|
|
517
|
+
retryable: false,
|
|
518
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
519
|
+
changedFiles: [],
|
|
520
|
+
durationMs: 0,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
else if (!opts.toolPolicy) {
|
|
524
|
+
outcome = {
|
|
525
|
+
status: 'denied',
|
|
526
|
+
code: 'TOOL_DISABLED',
|
|
527
|
+
retryable: false,
|
|
528
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
529
|
+
changedFiles: [],
|
|
530
|
+
durationMs: 0,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
else if (!parsed ||
|
|
534
|
+
!Array.isArray(parsed.groups) ||
|
|
535
|
+
parsed.groups.length === 0 ||
|
|
536
|
+
typeof parsed.reason !== 'string' ||
|
|
537
|
+
!parsed.reason.trim()) {
|
|
538
|
+
outcome = {
|
|
539
|
+
status: 'error',
|
|
540
|
+
code: 'INVALID_ARGUMENTS',
|
|
541
|
+
retryable: false,
|
|
542
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
543
|
+
changedFiles: [],
|
|
544
|
+
durationMs: 0,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
549
|
+
const succeeded = expansion.added.length > 0;
|
|
550
|
+
const details = [
|
|
551
|
+
succeeded
|
|
552
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
553
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
554
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
555
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
556
|
+
]
|
|
557
|
+
.filter(Boolean)
|
|
558
|
+
.join('\n');
|
|
559
|
+
outcome = {
|
|
560
|
+
status: succeeded ? 'success' : 'error',
|
|
561
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
562
|
+
retryable: false,
|
|
563
|
+
output: details,
|
|
564
|
+
changedFiles: [],
|
|
565
|
+
durationMs: 0,
|
|
566
|
+
};
|
|
567
|
+
emitTrace('tool_route_expand', {
|
|
568
|
+
policyId: expansion.snapshot.id,
|
|
569
|
+
fromVersion: policySnapshot?.version,
|
|
570
|
+
toVersion: expansion.snapshot.version,
|
|
571
|
+
requestedGroups: parsed.groups.map(String),
|
|
572
|
+
addedGroups: expansion.added,
|
|
573
|
+
rejected: expansion.rejected,
|
|
574
|
+
reason: parsed.reason,
|
|
575
|
+
status: outcome.status,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
579
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
580
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
581
|
+
traceToolEnd(tc, index, outcome);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// add_tool_groups 是 step 屏障:只要本响应出现该控制调用,本批所有普通工具都不执行。
|
|
585
|
+
// 但上面仍为每个 provider tool_call 写入了配对 tool_result,保持 OpenAI 协议完整。
|
|
586
|
+
let i = hasToolRouteBarrier ? calls.length : 0;
|
|
653
587
|
while (i < calls.length) {
|
|
654
588
|
const currentCall = calls[i];
|
|
655
|
-
if (
|
|
589
|
+
if (isToolDeniedForStep(currentCall.name)) {
|
|
656
590
|
hooks.onToolHeader?.(currentCall);
|
|
657
591
|
const error = t('task.disabled');
|
|
658
592
|
const outcome = {
|
|
@@ -676,17 +610,21 @@ export async function runAgentCore(opts) {
|
|
|
676
610
|
// 已经完成;若先 started.map,用户只能在工具完成后才看到摘要与其前面的换行。
|
|
677
611
|
// 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
|
|
678
612
|
let j = i;
|
|
679
|
-
while (j < calls.length && isParallelTool(calls[j].name))
|
|
613
|
+
while (j < calls.length && isParallelTool(calls[j].name) && !isToolDeniedForStep(calls[j].name))
|
|
680
614
|
j++;
|
|
681
615
|
const batch = calls.slice(i, j);
|
|
682
616
|
for (const tc of batch)
|
|
683
617
|
hooks.onToolHeader?.(tc);
|
|
684
618
|
hooks.onToolStart?.(batch[0].name);
|
|
685
|
-
const started = batch.map((tc) => executeToolOutcome(tc.name, tc.arguments, signal, {
|
|
619
|
+
const started = batch.map((tc) => executeToolOutcome(tc.name, tc.arguments, signal, {
|
|
620
|
+
callId: tc.id,
|
|
621
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
622
|
+
delegation: delegationForOrchestrator(),
|
|
623
|
+
}));
|
|
686
624
|
for (let k = 0; k < batch.length; k++) {
|
|
687
625
|
const tc = batch[k];
|
|
688
626
|
const outcome = await started[k];
|
|
689
|
-
|
|
627
|
+
traceState.addUsage(outcome.usage);
|
|
690
628
|
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
691
629
|
traceToolEnd(tc, i + k, outcome);
|
|
692
630
|
const output = outcome.output;
|
|
@@ -705,14 +643,14 @@ export async function runAgentCore(opts) {
|
|
|
705
643
|
i = j;
|
|
706
644
|
}
|
|
707
645
|
else if (isResourceLockedCall(currentCall) &&
|
|
708
|
-
!(getAgentMode() === 'plan' && getPlanDisabledTools().has(currentCall.name))) {
|
|
646
|
+
!(ctx.getAgentMode() === 'plan' && getPlanDisabledTools().has(currentCall.name))) {
|
|
709
647
|
// 连续文件 mutation:权限确认仍严格按原序进行;全部 preflight 完成后再启动。
|
|
710
648
|
// 每个执行在 registry 内按 canonical path 获取锁,不同文件可并发,同文件别名会排队。
|
|
711
649
|
let j = i;
|
|
712
650
|
while (j < calls.length &&
|
|
713
651
|
isResourceLockedCall(calls[j]) &&
|
|
714
|
-
!
|
|
715
|
-
!(getAgentMode() === 'plan' && getPlanDisabledTools().has(calls[j].name)))
|
|
652
|
+
!isToolDeniedForStep(calls[j].name) &&
|
|
653
|
+
!(ctx.getAgentMode() === 'plan' && getPlanDisabledTools().has(calls[j].name)))
|
|
716
654
|
j++;
|
|
717
655
|
const batch = calls.slice(i, j);
|
|
718
656
|
const entries = [];
|
|
@@ -720,9 +658,7 @@ export async function runAgentCore(opts) {
|
|
|
720
658
|
const tc = batch[k];
|
|
721
659
|
const parsed = parseArgs(tc.arguments);
|
|
722
660
|
const tool = findTool(tc.name);
|
|
723
|
-
const argumentsValid = tool && parsed !== null
|
|
724
|
-
? validateToolArguments(tool, parsed).valid
|
|
725
|
-
: false;
|
|
661
|
+
const argumentsValid = tool && parsed !== null ? validateToolArguments(tool, parsed).valid : false;
|
|
726
662
|
let denied;
|
|
727
663
|
if (tool && argumentsValid) {
|
|
728
664
|
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
@@ -758,6 +694,8 @@ export async function runAgentCore(opts) {
|
|
|
758
694
|
const hint = argumentErrorHint(entry.tc.name, runtimeContextState);
|
|
759
695
|
return executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
|
|
760
696
|
callId: entry.tc.id,
|
|
697
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
698
|
+
delegation: delegationForOrchestrator(),
|
|
761
699
|
...(hint ? { argumentErrorHint: hint } : {}),
|
|
762
700
|
onLockAcquired: (lockedArgs) => {
|
|
763
701
|
entry.diff = readDiffContext(entry.tc, lockedArgs);
|
|
@@ -767,15 +705,12 @@ export async function runAgentCore(opts) {
|
|
|
767
705
|
for (let k = 0; k < entries.length; k++) {
|
|
768
706
|
const entry = entries[k];
|
|
769
707
|
const outcome = await started[k];
|
|
770
|
-
|
|
708
|
+
traceState.addUsage(outcome.usage);
|
|
771
709
|
opts.onToolOutcome?.(entry.tc.name, entry.parsed ?? {}, outcome);
|
|
772
710
|
traceToolEnd(entry.tc, i + k, outcome);
|
|
773
711
|
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
774
712
|
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
775
|
-
const invalidatedFiles = [...new Set([
|
|
776
|
-
...(outcome.changedFiles ?? []),
|
|
777
|
-
...(outcome.staleFiles ?? []),
|
|
778
|
-
])];
|
|
713
|
+
const invalidatedFiles = [...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])])];
|
|
779
714
|
if (invalidatedFiles.length > 0) {
|
|
780
715
|
for (const changedFile of invalidatedFiles) {
|
|
781
716
|
relprune?.observeMutation(history, changedFile);
|
|
@@ -794,7 +729,7 @@ export async function runAgentCore(opts) {
|
|
|
794
729
|
const tc = calls[i];
|
|
795
730
|
// plan 模式防御 backstop:schema 已剔除这些工具,正常不会进这里;防后端幻觉调用——
|
|
796
731
|
// 不执行,直接返错回灌(让模型看到「plan 模式禁用」并停止),绝不写盘 / 跑命令。
|
|
797
|
-
if (getAgentMode() === 'plan' && getPlanDisabledTools().has(tc.name)) {
|
|
732
|
+
if (ctx.getAgentMode() === 'plan' && getPlanDisabledTools().has(tc.name)) {
|
|
798
733
|
hooks.onToolHeader?.(tc);
|
|
799
734
|
const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
|
|
800
735
|
const outcome = {
|
|
@@ -815,9 +750,7 @@ export async function runAgentCore(opts) {
|
|
|
815
750
|
// 拒绝时只渲染拒绝结果,不渲染执行头;放行则继续走 header → start → executeTool 流程。
|
|
816
751
|
const parsed = parseArgs(tc.arguments);
|
|
817
752
|
const tool = findTool(tc.name);
|
|
818
|
-
const argumentsValid = tool && parsed !== null
|
|
819
|
-
? validateToolArguments(tool, parsed).valid
|
|
820
|
-
: false;
|
|
753
|
+
const argumentsValid = tool && parsed !== null ? validateToolArguments(tool, parsed).valid : false;
|
|
821
754
|
if (tool && argumentsValid) {
|
|
822
755
|
const perm = await checkPermission(tool, parsed ?? {}, signal, {
|
|
823
756
|
prompt: opts.permissionPrompt,
|
|
@@ -842,21 +775,21 @@ export async function runAgentCore(opts) {
|
|
|
842
775
|
}
|
|
843
776
|
}
|
|
844
777
|
hooks.onToolHeader?.(tc);
|
|
845
|
-
const mutationParsed = isMutationTool(tc.name)
|
|
846
|
-
? parsed
|
|
847
|
-
: null;
|
|
778
|
+
const mutationParsed = isMutationTool(tc.name) ? parsed : null;
|
|
848
779
|
let diff = readDiffContext(tc, mutationParsed);
|
|
849
780
|
hooks.onToolStart?.(tc.name);
|
|
850
781
|
const serialHint = argumentErrorHint(tc.name, runtimeContextState);
|
|
851
782
|
const outcome = await executeToolOutcome(tc.name, tc.arguments, signal, {
|
|
852
783
|
callId: tc.id,
|
|
784
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
785
|
+
delegation: delegationForOrchestrator(),
|
|
853
786
|
...(serialHint ? { argumentErrorHint: serialHint } : {}),
|
|
854
787
|
onLockAcquired: (lockedArgs) => {
|
|
855
788
|
if (mutationParsed)
|
|
856
789
|
diff = readDiffContext(tc, lockedArgs);
|
|
857
790
|
},
|
|
858
791
|
});
|
|
859
|
-
|
|
792
|
+
traceState.addUsage(outcome.usage);
|
|
860
793
|
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
861
794
|
traceToolEnd(tc, i, outcome);
|
|
862
795
|
const output = outcome.output;
|
|
@@ -871,10 +804,7 @@ export async function runAgentCore(opts) {
|
|
|
871
804
|
}, tc.id ? { providerToolCallId: tc.id } : {});
|
|
872
805
|
}
|
|
873
806
|
pushToolResult(history, tc, output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
874
|
-
const invalidatedFiles = [...new Set([
|
|
875
|
-
...(outcome.changedFiles ?? []),
|
|
876
|
-
...(outcome.staleFiles ?? []),
|
|
877
|
-
])];
|
|
807
|
+
const invalidatedFiles = [...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])])];
|
|
878
808
|
if (invalidatedFiles.length > 0) {
|
|
879
809
|
for (const changedFile of invalidatedFiles) {
|
|
880
810
|
relprune?.observeMutation(history, changedFile);
|
|
@@ -890,16 +820,19 @@ export async function runAgentCore(opts) {
|
|
|
890
820
|
// 每步都会由 buildSessionStateReminder() 在 requestHistory 末尾重建最新副本(见上方注入点),
|
|
891
821
|
// 所以模型下一步看到的必然是当前勾选态。只保留计数,避免多余的 history 改写(prompt 缓存)。
|
|
892
822
|
// B(nag 提醒):连续 N 步有工具活动但没更新 plan,在当前步第一条 tool_result 前注入提醒。
|
|
893
|
-
const notesMtimeAfter = getNotesMtime();
|
|
823
|
+
const notesMtimeAfter = ctx.getNotesMtime();
|
|
894
824
|
if (notesMtimeAfter !== notesMtimeBefore) {
|
|
895
825
|
stepsSincePlanTouch = 0;
|
|
896
826
|
}
|
|
897
827
|
else {
|
|
898
828
|
stepsSincePlanTouch += 1;
|
|
899
829
|
if (stepsSincePlanTouch >= PLAN_NAG_THRESHOLD) {
|
|
900
|
-
const activePlan = extractActivePlanSection();
|
|
830
|
+
const activePlan = ctx.extractActivePlanSection();
|
|
901
831
|
const firstToolMsg = history[toolResultStartIdx];
|
|
902
|
-
if (activePlan &&
|
|
832
|
+
if (activePlan &&
|
|
833
|
+
firstToolMsg &&
|
|
834
|
+
firstToolMsg.role === 'tool' &&
|
|
835
|
+
typeof firstToolMsg.content === 'string') {
|
|
903
836
|
firstToolMsg.content = `${PLAN_NAG_TEXT}\n\n${firstToolMsg.content}`;
|
|
904
837
|
}
|
|
905
838
|
stepsSincePlanTouch = 0;
|
|
@@ -943,14 +876,14 @@ export async function runAgentCore(opts) {
|
|
|
943
876
|
if (!gotText)
|
|
944
877
|
hooks.onNoReply?.();
|
|
945
878
|
history.push({ role: 'assistant', content: result.content });
|
|
946
|
-
const finalMutation = getCurrentTurnMutationState();
|
|
879
|
+
const finalMutation = ctx.getCurrentTurnMutationState();
|
|
947
880
|
done = true;
|
|
948
881
|
traceStatus = 'completed';
|
|
949
882
|
return {
|
|
950
883
|
completed: true,
|
|
951
884
|
terminationReason: 'completed',
|
|
952
885
|
finalText: result.content,
|
|
953
|
-
usage: turnUsage,
|
|
886
|
+
usage: traceState.turnUsage,
|
|
954
887
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
955
888
|
};
|
|
956
889
|
}
|
|
@@ -964,24 +897,24 @@ export async function runAgentCore(opts) {
|
|
|
964
897
|
hooks.onMaxSteps?.();
|
|
965
898
|
done = true;
|
|
966
899
|
traceStatus = 'max_steps';
|
|
967
|
-
const finalMutation = getCurrentTurnMutationState();
|
|
900
|
+
const finalMutation = ctx.getCurrentTurnMutationState();
|
|
968
901
|
return {
|
|
969
902
|
completed: false,
|
|
970
903
|
terminationReason: 'max_steps',
|
|
971
904
|
finalText: null,
|
|
972
|
-
usage: turnUsage,
|
|
905
|
+
usage: traceState.turnUsage,
|
|
973
906
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
974
907
|
};
|
|
975
908
|
}
|
|
976
909
|
finally {
|
|
977
|
-
const finalMutation = getCurrentTurnMutationState();
|
|
978
|
-
currentTraceStep = undefined;
|
|
910
|
+
const finalMutation = ctx.getCurrentTurnMutationState();
|
|
911
|
+
traceState.currentTraceStep = undefined;
|
|
979
912
|
emitTrace('turn_end', {
|
|
980
913
|
status: traceStatus,
|
|
981
914
|
durationMs: Date.now() - t0,
|
|
982
|
-
toolCalls: toolCallCount,
|
|
915
|
+
toolCalls: traceState.toolCallCount,
|
|
983
916
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
984
|
-
totalTokens: turnUsage?.totalTokens,
|
|
917
|
+
totalTokens: traceState.turnUsage?.totalTokens,
|
|
985
918
|
});
|
|
986
919
|
try {
|
|
987
920
|
opts.onTrace?.({
|
|
@@ -990,9 +923,9 @@ export async function runAgentCore(opts) {
|
|
|
990
923
|
turnId: traceTurnId,
|
|
991
924
|
status: traceStatus,
|
|
992
925
|
durationMs: Date.now() - t0,
|
|
993
|
-
toolCalls: toolCallCount,
|
|
926
|
+
toolCalls: traceState.toolCallCount,
|
|
994
927
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
995
|
-
usage: turnUsage,
|
|
928
|
+
usage: traceState.turnUsage,
|
|
996
929
|
});
|
|
997
930
|
}
|
|
998
931
|
catch {
|
|
@@ -1000,7 +933,7 @@ export async function runAgentCore(opts) {
|
|
|
1000
933
|
}
|
|
1001
934
|
// 跑完(正常 / 达上限)在回复末尾打耗时摘要行;中断 done=false 不打。
|
|
1002
935
|
if (done) {
|
|
1003
|
-
hooks.onDone?.(Date.now() - t0, turnUsage);
|
|
936
|
+
hooks.onDone?.(Date.now() - t0, traceState.turnUsage);
|
|
1004
937
|
}
|
|
1005
938
|
}
|
|
1006
939
|
}
|