mocode-ai 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/agent/core.js +8 -4
- package/dist/commands/config.js +1 -1
- package/dist/config/file.js +1 -1
- package/dist/config/index.js +47 -7
- package/dist/context/classifier.js +2 -1
- package/dist/index.js +2 -1
- package/dist/llm/index.js +13 -1
- package/dist/repl/index.js +200 -3
- package/dist/session/drop.js +135 -0
- package/dist/session/index.js +1 -0
- package/dist/tools/builtins/ask-human.js +4 -4
- package/dist/tools/builtins/codegraph.js +3 -3
- package/dist/tools/builtins/drop-context.js +68 -0
- package/dist/tools/builtins/edit-file.js +1 -1
- package/dist/tools/builtins/glob.js +2 -2
- package/dist/tools/builtins/grep.js +2 -2
- package/dist/tools/builtins/index.js +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-list.js +1 -1
- package/dist/tools/builtins/memory-save.js +1 -1
- package/dist/tools/builtins/memory-search.js +1 -1
- package/dist/tools/builtins/memory-update.js +1 -1
- package/dist/tools/builtins/read-file.js +2 -2
- package/dist/tools/builtins/run-command.js +1 -1
- package/dist/tools/builtins/switch-mode.js +3 -5
- package/dist/tools/builtins/task.js +4 -5
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/web-fetch.js +1 -1
- package/dist/tools/builtins/web-search.js +1 -1
- package/dist/tools/builtins/write-file.js +1 -1
- package/dist/tools/registry.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,8 @@ npm start
|
|
|
58
58
|
mocode config
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
也可直接 `mocode` 进入 REPL 后用 `/model` 命令配置(交互选后端预设 + 逐项填写,即时生效 + 持久化)。未配置时 REPL 仍能打开,会提示你跑 `/model`。
|
|
62
|
+
|
|
61
63
|
也可手写配置文件。mocode 按以下优先级加载(后者覆盖前者,仅回填未设置的环境变量;shell 里 `export` 的永远最优先):
|
|
62
64
|
|
|
63
65
|
1. `~/.mocode/config` — 全局(`mocode config` 写此文件)
|
|
@@ -149,6 +151,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
149
151
|
| `/resume` | 续接已保存的会话 |
|
|
150
152
|
| `/think` | 展开折叠思考段(`/think N`) |
|
|
151
153
|
| `/rollback` | 菜单选轮次回滚(↑↓ · Enter) |
|
|
154
|
+
| `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
|
|
152
155
|
|
|
153
156
|
输入 `/` 触发下拉菜单,继续打字过滤;Esc 取消。
|
|
154
157
|
|
package/dist/agent/core.js
CHANGED
|
@@ -9,7 +9,7 @@ import { chat, planChatTools, } from '../llm/index.js';
|
|
|
9
9
|
import { executeTool } from '../tools/registry.js';
|
|
10
10
|
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
11
11
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
12
|
-
import { maybeCompact, contextState } from '../session/index.js';
|
|
12
|
+
import { maybeCompact, contextState, dropContextFromHistory } from '../session/index.js';
|
|
13
13
|
import { optimizeToolResult } from '../context/index.js';
|
|
14
14
|
import { config } from '../config/index.js';
|
|
15
15
|
import { jailResolve } from '../sandbox/index.js';
|
|
@@ -107,6 +107,10 @@ export async function runAgentCore(opts) {
|
|
|
107
107
|
const t0 = Date.now();
|
|
108
108
|
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
109
109
|
history.push({ role: 'user', content: userInput });
|
|
110
|
+
// drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
|
|
111
|
+
// 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
|
|
112
|
+
// 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
|
|
113
|
+
const dropContext = (filter) => dropContextFromHistory(history, filter);
|
|
110
114
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
111
115
|
let mode = 'idle';
|
|
112
116
|
let gotText = false;
|
|
@@ -202,7 +206,7 @@ export async function runAgentCore(opts) {
|
|
|
202
206
|
while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
|
|
203
207
|
j++;
|
|
204
208
|
const batch = calls.slice(i, j);
|
|
205
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
209
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
206
210
|
for (let k = 0; k < batch.length; k++) {
|
|
207
211
|
const tc = batch[k];
|
|
208
212
|
hooks.onToolHeader?.(tc);
|
|
@@ -238,7 +242,7 @@ export async function runAgentCore(opts) {
|
|
|
238
242
|
while (j < calls.length && calls[j].name === 'task')
|
|
239
243
|
j++;
|
|
240
244
|
const batch = calls.slice(i, j);
|
|
241
|
-
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
245
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext }));
|
|
242
246
|
// 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
|
|
243
247
|
for (const tc of batch) {
|
|
244
248
|
hooks.onToolHeader?.(tc);
|
|
@@ -273,7 +277,7 @@ export async function runAgentCore(opts) {
|
|
|
273
277
|
: null;
|
|
274
278
|
const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
|
|
275
279
|
hooks.onToolStart?.(tc.name);
|
|
276
|
-
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback });
|
|
280
|
+
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback, dropContext });
|
|
277
281
|
hooks.onToolDone?.();
|
|
278
282
|
hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
|
|
279
283
|
pushToolResult(history, tc, output);
|
package/dist/commands/config.js
CHANGED
|
@@ -9,7 +9,7 @@ function ask(rl, q) {
|
|
|
9
9
|
* 首跑配置向导:交互填 LLM_BASE_URL / LLM_API_KEY / LLM_MODEL,写 ~/.mocode/config。
|
|
10
10
|
* 只交互三键、保留文件里其它键(MAX_TOKENS / CONTEXT_WINDOW_TOKENS / MOCODE_THEME 等)。
|
|
11
11
|
* prompt 全纯文本(readline 光标按字符算,不能含 ANSI)。
|
|
12
|
-
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(
|
|
12
|
+
* 由 index.ts 在 `mocode config` 时动态加载,故不 import config/index.ts(避免触发 config 单例初始化 / loadEnvFiles,
|
|
13
13
|
* 直接退出);只 import config/file.ts(纯 I/O 叶子,无 env 校验 / process.exit)。
|
|
14
14
|
*/
|
|
15
15
|
export async function runConfigWizard() {
|
package/dist/config/file.js
CHANGED
|
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
/**
|
|
6
6
|
* ~/.mocode/config 全局 dotenv 配置的读写叶子(纯 node:fs / node:os / node:path + dotenv)。
|
|
7
7
|
*
|
|
8
|
-
* 独立于 `config/index.ts`——**不**触发
|
|
8
|
+
* 独立于 `config/index.ts`——**不**触发 config 单例初始化 / loadEnvFiles,故 `commands/config.ts`
|
|
9
9
|
* (首跑向导,刻意不 import config/index.ts)与 `repl/index.ts`(`/theme` 持久化)都能安全共享。
|
|
10
10
|
* 主题等 UI 偏好走 `updateConfigKey` 单键写;向导多键一次性写走 `writeConfigKeys`。
|
|
11
11
|
*/
|
package/dist/config/index.js
CHANGED
|
@@ -30,14 +30,26 @@ function loadEnvFiles() {
|
|
|
30
30
|
}
|
|
31
31
|
// 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
|
|
32
32
|
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
33
|
+
// 在 loadEnvFiles 回填前捕获:哪些 LLM 键由 shell 设置(决定 /model 写文件是否下次启动生效)。
|
|
34
|
+
// 仿 themeFromShell 模式:shell export 的环境变量在 loadEnvFiles 中不被回填(优先级最高),
|
|
35
|
+
// 故 /model 写入 ~/.mocode/config 的同名键下次启动会被 shell 值覆盖——据此给 dim 警告。
|
|
36
|
+
const LLM_ENV_KEYS = ['LLM_BASE_URL', 'LLM_API_KEY', 'LLM_MODEL', 'CONTEXT_WINDOW_TOKENS'];
|
|
37
|
+
const llmKeysFromShell = LLM_ENV_KEYS.filter((k) => process.env[k] !== undefined);
|
|
33
38
|
loadEnvFiles();
|
|
39
|
+
/**
|
|
40
|
+
* 取环境变量;缺则返回空字符串(不退出)。
|
|
41
|
+
* 历史上缺 LLM_BASE_URL/LLM_API_KEY 会 process.exit(1),但 /model 命令已能在 REPL 内配置模型,
|
|
42
|
+
* 故首次未配置也应让 REPL 起来,由开场提示引导用户跑 /model。发消息时 chat() 会抛错被 runTurn catch,不崩。
|
|
43
|
+
*/
|
|
34
44
|
function requireEnv(key) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
return process.env[key] || '';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 模型是否已配置(baseURL + apiKey 非空)。REPL 开场据此决定是否提示 /model。
|
|
49
|
+
* 未配置时 config.model 仍回退 'gpt-4o-mini',但发消息会因 baseURL/apiKey 空而失败——由 runTurn catch 友好提示。
|
|
50
|
+
*/
|
|
51
|
+
export function isModelConfigured() {
|
|
52
|
+
return !!config.baseURL && !!config.apiKey;
|
|
41
53
|
}
|
|
42
54
|
const PLATFORM_NOTE = (() => {
|
|
43
55
|
if (process.platform === 'win32') {
|
|
@@ -76,6 +88,7 @@ ${PLATFORM_NOTE}
|
|
|
76
88
|
- Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
|
|
77
89
|
- Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
|
|
78
90
|
- Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
|
|
91
|
+
- **Drop irrelevant context** (use sparingly): call drop_context to stub-replace tool results in history that are BOTH (a) irrelevant to the current task AND (b) large (the freed tokens must clearly exceed the ~300 tokens the call itself costs — roughly only worth it when targeting ≥2 bulky results, e.g. wide grep/read sweeps that returned mostly-irrelevant hits). The call itself adds a tool-call round-trip, so don't call it for one small result or when you're near done. It preserves tool_call_id pairing (only content changes); the system prompt and current turn are never dropped. Use filters (toolNames / contains) to target precisely.
|
|
79
92
|
|
|
80
93
|
## Failure Handling
|
|
81
94
|
- Tools return errors as strings (edit_file no match or non-unique, run_command non-zero exit, etc.). Analyze the root cause, adjust, then retry — don't resend the same call verbatim.
|
|
@@ -111,7 +124,7 @@ You are in PLAN mode: investigate and design only — do NOT execute or change a
|
|
|
111
124
|
- Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. Prefer codegraph when a .codegraph/ index exists.
|
|
112
125
|
- Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
|
|
113
126
|
- Present the plan as your final reply and STOP, unless the user explicitly asked you to "plan first then execute" / "先 plan 再 auto" / autonomous execution: in that case, after presenting the plan, call the switch_mode tool with mode="auto" to switch back to auto mode WITHIN THE SAME TURN and continue implementing the plan yourself (your write/edit/command/memory-write tools become available again immediately). The user will see no approval prompt because you self-switched.
|
|
114
|
-
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP
|
|
127
|
+
- If the user entered plan mode manually (via /plan or Shift+Tab) for a safety review and did NOT ask for autonomous execution, do NOT call switch_mode — present the plan and STOP. Do NOT ask the user for confirmation or approval in your text reply (e.g. "is this plan OK?", "shall I proceed?", "需要你确认") — the REPL automatically shows an approval prompt after you STOP, so asking in text is redundant and forces the user to answer twice. Just present the plan and end your reply.`;
|
|
115
128
|
export const config = {
|
|
116
129
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
117
130
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
@@ -133,4 +146,31 @@ export const config = {
|
|
|
133
146
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
134
147
|
theme: process.env.MOCODE_THEME || 'default',
|
|
135
148
|
themeFromShell,
|
|
149
|
+
llmKeysFromShell,
|
|
136
150
|
};
|
|
151
|
+
/**
|
|
152
|
+
* 运行时更新模型相关配置(/model 命令调)。
|
|
153
|
+
* - 更新 config 对象字段(即时生效:chat() 读 config.model,reconfigureClient 读 config.baseURL/apiKey)。
|
|
154
|
+
* - 同步 process.env(保持内存一致:其他读 process.env 的路径也拿到新值;且使新值在下次启动的
|
|
155
|
+
* loadEnvFiles 中被视为"已设",不被文件回填覆盖——即"优先拿这里的")。
|
|
156
|
+
* 持久化(写 ~/.mocode/config)由调用方走 writeConfigKeys,此处只管内存 + env。
|
|
157
|
+
* 重建 OpenAI 客户端(baseURL/apiKey 是构造时固化的实例字段)由调用方走 reconfigureClient。
|
|
158
|
+
*/
|
|
159
|
+
export function updateModelConfig(opts) {
|
|
160
|
+
if (opts.model !== undefined) {
|
|
161
|
+
config.model = opts.model;
|
|
162
|
+
process.env.LLM_MODEL = opts.model;
|
|
163
|
+
}
|
|
164
|
+
if (opts.baseURL !== undefined) {
|
|
165
|
+
config.baseURL = opts.baseURL;
|
|
166
|
+
process.env.LLM_BASE_URL = opts.baseURL;
|
|
167
|
+
}
|
|
168
|
+
if (opts.apiKey !== undefined) {
|
|
169
|
+
config.apiKey = opts.apiKey;
|
|
170
|
+
process.env.LLM_API_KEY = opts.apiKey;
|
|
171
|
+
}
|
|
172
|
+
if (opts.contextWindowTokens !== undefined) {
|
|
173
|
+
config.contextWindowTokens = opts.contextWindowTokens;
|
|
174
|
+
process.env.CONTEXT_WINDOW_TOKENS = String(opts.contextWindowTokens);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
//
|
|
8
8
|
// 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
|
|
9
9
|
// 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
|
|
10
|
-
/** 工具名 → ContextKind 的强先验表(覆盖全部
|
|
10
|
+
/** 工具名 → ContextKind 的强先验表(覆盖全部 18 内置工具)。 */
|
|
11
11
|
const BY_NAME = {
|
|
12
12
|
// tree:路径列表 → 缩进树
|
|
13
13
|
glob: 'tree',
|
|
@@ -32,6 +32,7 @@ const BY_NAME = {
|
|
|
32
32
|
write_file: 'status',
|
|
33
33
|
ask_human: 'status',
|
|
34
34
|
switch_mode: 'status',
|
|
35
|
+
drop_context: 'status',
|
|
35
36
|
memory_save: 'status',
|
|
36
37
|
memory_update: 'status',
|
|
37
38
|
memory_forget: 'status',
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { exitAltScreen } from './ui/layout.js';
|
|
|
2
2
|
import { checkAndMaybeUpdate } from './updater/index.js';
|
|
3
3
|
// 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
|
|
4
4
|
// exitAltScreen 幂等(未激活时空操作),故全局注册安全——进 alt screen 前的路径(如 --resume 列表、缺环境变量、`mocode config`)调用它无副作用。
|
|
5
|
-
// 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(
|
|
5
|
+
// 仅 layout 是叶子(不依赖 config),故静态导入安全;repl / session 依赖 config(模块加载触发 loadEnvFiles + config 单例初始化),
|
|
6
|
+
// 改动态按需加载——`mocode config` 向导只需读写文件(走 config/file.ts 叶子),不经 config 单例初始化,零配置也能跑。
|
|
6
7
|
process.on('exit', () => exitAltScreen());
|
|
7
8
|
process.on('SIGINT', () => {
|
|
8
9
|
exitAltScreen();
|
package/dist/llm/index.js
CHANGED
|
@@ -2,10 +2,22 @@ import OpenAI from 'openai';
|
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
4
|
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
5
|
-
|
|
5
|
+
let client = new OpenAI({
|
|
6
6
|
baseURL: config.baseURL,
|
|
7
7
|
apiKey: config.apiKey,
|
|
8
8
|
});
|
|
9
|
+
/**
|
|
10
|
+
* 运行时重建 OpenAI 客户端(/model 切换 baseURL/apiKey 后调)。
|
|
11
|
+
* config.model 已在 chat() 每次读取(热切),但 client 的 baseURL/apiKey 是构造时固化的实例字段,
|
|
12
|
+
* 改 config 后必须重建 client 才能让新 baseURL/apiKey 对后续请求生效。
|
|
13
|
+
* 子 agent 复用本模块 chat(),故只此一处重建即全链路生效。
|
|
14
|
+
*/
|
|
15
|
+
export function reconfigureClient() {
|
|
16
|
+
client = new OpenAI({
|
|
17
|
+
baseURL: config.baseURL,
|
|
18
|
+
apiKey: config.apiKey,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
9
21
|
/** 把内部工具定义转成 OpenAI 的 tool 格式 */
|
|
10
22
|
export const chatTools = tools.map((t) => ({
|
|
11
23
|
type: 'function',
|
package/dist/repl/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, PLAN_MODE_SUFFIX } from '../config/index.js';
|
|
5
|
-
import { updateConfigKey } from '../config/file.js';
|
|
4
|
+
import { config, PLAN_MODE_SUFFIX, updateModelConfig, isModelConfigured } from '../config/index.js';
|
|
5
|
+
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
6
6
|
import { runAgent } from '../agent/index.js';
|
|
7
7
|
import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
|
|
8
8
|
import { setSandboxRoot } from '../sandbox/root.js';
|
|
@@ -13,7 +13,7 @@ import * as mouse from '../ui/mouse.js';
|
|
|
13
13
|
import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
|
|
14
14
|
import { promptIntervention } from '../ui/intervention.js';
|
|
15
15
|
import { tools } from '../tools/registry.js';
|
|
16
|
-
import { estimateMessagesTokens, } from '../llm/index.js';
|
|
16
|
+
import { estimateMessagesTokens, reconfigureClient, } from '../llm/index.js';
|
|
17
17
|
import { compactHistory, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
|
|
18
18
|
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
|
|
19
19
|
import { listSkills, effectiveSystemPrompt } from '../skills/index.js';
|
|
@@ -36,6 +36,7 @@ const SLASH_COMMANDS = [
|
|
|
36
36
|
{ name: '/reflect', desc: '手动触发后台记忆反思 pass' },
|
|
37
37
|
{ name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆' },
|
|
38
38
|
{ name: '/theme', desc: '切换颜色主题(↑↓·Enter)' },
|
|
39
|
+
{ name: '/model', desc: '配置大模型(baseURL/key/model/窗口)' },
|
|
39
40
|
{ name: '/plan', desc: '切到 plan 模式(只读探查+产出计划)' },
|
|
40
41
|
{ name: '/auto', desc: '切回 auto 模式(全工具执行)' },
|
|
41
42
|
];
|
|
@@ -47,6 +48,23 @@ const THEME_DESCRIPTIONS = {
|
|
|
47
48
|
gruvbox: 'Gruvbox 暖色',
|
|
48
49
|
nord: 'Nord 冷色',
|
|
49
50
|
};
|
|
51
|
+
/** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
|
|
52
|
+
const MODEL_PRESETS = [
|
|
53
|
+
{ label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: 128000 },
|
|
54
|
+
{ label: 'DeepSeek', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window: 64000 },
|
|
55
|
+
{ label: 'Qwen(阿里)', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window: 128000 },
|
|
56
|
+
{ label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: 32768 },
|
|
57
|
+
{ label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window: 32768 },
|
|
58
|
+
{ label: '自定义 base_url', baseURL: '', model: '', window: 128000 },
|
|
59
|
+
];
|
|
60
|
+
/** apiKey 脱敏:只露末 4 位,前面打星号(显示用,绝不把明文 key 写进内容区)。 */
|
|
61
|
+
function maskKey(k) {
|
|
62
|
+
if (!k)
|
|
63
|
+
return '(未设置)';
|
|
64
|
+
if (k.length <= 8)
|
|
65
|
+
return '****';
|
|
66
|
+
return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
|
|
67
|
+
}
|
|
50
68
|
/**
|
|
51
69
|
* /init 指令:发给 agent 扫描项目并生成 MOCODE.md(对标 Claude Code /init 生成 CLAUDE.md,
|
|
52
70
|
* 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
@@ -144,6 +162,8 @@ function runningStateFor(cmd) {
|
|
|
144
162
|
return { status: '清空', placeholder: '…' };
|
|
145
163
|
case '/theme':
|
|
146
164
|
return { status: '切主题', placeholder: '选择主题…' };
|
|
165
|
+
case '/model':
|
|
166
|
+
return { status: '配模型', placeholder: '配置中…' };
|
|
147
167
|
default:
|
|
148
168
|
// 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
|
|
149
169
|
// 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
|
|
@@ -396,6 +416,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
396
416
|
// 自更新提示:开场静态段(进 INPUT 态前),dim 一行,不与流式 / 输入争用。
|
|
397
417
|
layout.contentWrite(` ${ui.gray}↳ ${updateNotice}${ui.reset}\n`);
|
|
398
418
|
}
|
|
419
|
+
if (!isModelConfigured()) {
|
|
420
|
+
// 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
|
|
421
|
+
layout.contentWrite(`${ui.yellow} ⚠ 未配置大模型。输入 ${ui.cyan}/model${ui.yellow} 配置 baseURL / apiKey / model(即时生效),或退出后运行 ${ui.cyan}mocode config${ui.yellow} 走向导。${ui.reset}\n`);
|
|
422
|
+
}
|
|
399
423
|
layout.contentWrite(`${ui.dim} /plan · /auto · Shift+Tab 切换模式(plan:只读探查 + 产出计划,审批后切 auto 执行)${ui.reset}\n`);
|
|
400
424
|
/**
|
|
401
425
|
* 切换 agent 模式(Shift+Tab 触发,经 prompt.ts 的 onCycleMode 回调)。
|
|
@@ -772,6 +796,179 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
772
796
|
}
|
|
773
797
|
continue;
|
|
774
798
|
}
|
|
799
|
+
if (line === '/model' || line.startsWith('/model ')) {
|
|
800
|
+
// /model:运行时配置大模型(baseURL/apiKey/model/contextWindowTokens)。
|
|
801
|
+
// 即时生效(updateModelConfig 改内存 + env,reconfigureClient 重建 OpenAI 实例)+ 持久化(writeConfigKeys 写 ~/.mocode/config)。
|
|
802
|
+
// 仿 /theme:promptIntervention 弹菜单/输入 → 改 config → refreshStatusBase 刷底栏 → clearContent+banner 重显横幅 → dim 警告(shell env 覆盖)。
|
|
803
|
+
const arg = line.startsWith('/model ') ? line.slice('/model '.length).trim() : '';
|
|
804
|
+
// /model list:显示当前四项配置(apiKey 脱敏)。
|
|
805
|
+
if (arg === 'list' || arg === 'show') {
|
|
806
|
+
layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
|
|
807
|
+
layout.contentWrite(` ${ui.cyan}baseURL${ui.reset} ${config.baseURL}\n`);
|
|
808
|
+
layout.contentWrite(` ${ui.cyan}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
|
|
809
|
+
layout.contentWrite(` ${ui.cyan}model ${ui.reset} ${config.model}\n`);
|
|
810
|
+
layout.contentWrite(` ${ui.cyan}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
|
|
811
|
+
layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
// 1) 选 provider 预设(预填 baseURL,后续仍可逐项改)。
|
|
815
|
+
let preset;
|
|
816
|
+
try {
|
|
817
|
+
const res = await promptIntervention({
|
|
818
|
+
type: 'choice',
|
|
819
|
+
title: '选择后端预设(预填 baseURL,后续可改)',
|
|
820
|
+
detail: '选一个会预填 baseURL/model/窗口,之后逐项确认。选「自定义」全部手填。',
|
|
821
|
+
options: MODEL_PRESETS.map((p) => p.label),
|
|
822
|
+
});
|
|
823
|
+
if (res.action === 'cancelled') {
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
const idx = MODEL_PRESETS.findIndex((p) => p.label === res.value);
|
|
827
|
+
if (idx === -1) {
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
preset = MODEL_PRESETS[idx];
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
continue; // Ctrl+C
|
|
834
|
+
}
|
|
835
|
+
// 1.5) 一键应用确认:非「自定义」预设(带预填值)给直接应用入口,免连按 4 次回车。
|
|
836
|
+
// 直接应用 = 用预设 model/baseURL/window + 保留当前 apiKey(等价于下方逐项链连按回车)。
|
|
837
|
+
// 逐项修改 / 自定义输入(promptIntervention choice 自动追加的「其他」项 submitted)→ 回落 4 步链。
|
|
838
|
+
// 「自定义」预设字段空,跳过确认直接进链。
|
|
839
|
+
let quickApply = false;
|
|
840
|
+
if (preset.model || preset.baseURL) {
|
|
841
|
+
try {
|
|
842
|
+
const res = await promptIntervention({
|
|
843
|
+
type: 'choice',
|
|
844
|
+
title: `应用 ${preset.label}?`,
|
|
845
|
+
detail: `model ${preset.model}\nbaseURL ${preset.baseURL}\napiKey ${maskKey(config.apiKey)}(直接应用=保留当前)\n窗口 ${preset.window}`,
|
|
846
|
+
options: ['直接应用', '逐项修改'],
|
|
847
|
+
});
|
|
848
|
+
if (res.action === 'cancelled') {
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (res.action === 'selected' && res.value === '直接应用') {
|
|
852
|
+
quickApply = true;
|
|
853
|
+
}
|
|
854
|
+
// 其余(逐项修改 / 自定义输入 submitted)→ quickApply 保持 false,走下方逐项链
|
|
855
|
+
}
|
|
856
|
+
catch {
|
|
857
|
+
continue; // Ctrl+C
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// 2) 收集 baseURL / apiKey / model / contextWindowTokens。
|
|
861
|
+
// quickApply:直接取预设值 + 当前 apiKey;否则逐项 input(预填 preset 值,回车=采纳;apiKey 不预填明文,回车=保留旧值)。
|
|
862
|
+
let baseURL;
|
|
863
|
+
let apiKey;
|
|
864
|
+
let model;
|
|
865
|
+
let window;
|
|
866
|
+
if (quickApply) {
|
|
867
|
+
baseURL = preset.baseURL;
|
|
868
|
+
apiKey = config.apiKey;
|
|
869
|
+
model = preset.model;
|
|
870
|
+
window = preset.window;
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
// baseURL
|
|
874
|
+
{
|
|
875
|
+
const res = await promptIntervention({
|
|
876
|
+
type: 'input',
|
|
877
|
+
title: 'LLM_BASE_URL',
|
|
878
|
+
detail: 'OpenAI 兼容 API 端点。回车采纳预填值。',
|
|
879
|
+
seed: preset.baseURL || config.baseURL,
|
|
880
|
+
});
|
|
881
|
+
if (res.action === 'cancelled') {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
baseURL = (res.value ?? '').trim() || preset.baseURL || config.baseURL;
|
|
885
|
+
}
|
|
886
|
+
if (!baseURL) {
|
|
887
|
+
layout.contentWrite(`${ui.yellow}baseURL 不能为空,已取消。${ui.reset}\n`);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
// apiKey(不预填明文:回车=保留旧值,输入新值=覆盖)
|
|
891
|
+
{
|
|
892
|
+
const res = await promptIntervention({
|
|
893
|
+
type: 'input',
|
|
894
|
+
title: 'LLM_API_KEY',
|
|
895
|
+
detail: `回车保留当前 ${maskKey(config.apiKey)};输入新值则覆盖。`,
|
|
896
|
+
seed: '',
|
|
897
|
+
});
|
|
898
|
+
if (res.action === 'cancelled') {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const v = (res.value ?? '').trim();
|
|
902
|
+
apiKey = v || config.apiKey;
|
|
903
|
+
}
|
|
904
|
+
if (!apiKey) {
|
|
905
|
+
layout.contentWrite(`${ui.yellow}apiKey 不能为空,已取消。${ui.reset}\n`);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
// model
|
|
909
|
+
{
|
|
910
|
+
const res = await promptIntervention({
|
|
911
|
+
type: 'input',
|
|
912
|
+
title: 'LLM_MODEL',
|
|
913
|
+
detail: '模型名(须支持 function calling)。回车采纳预填值。',
|
|
914
|
+
seed: preset.model || config.model,
|
|
915
|
+
});
|
|
916
|
+
if (res.action === 'cancelled') {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
model = (res.value ?? '').trim() || preset.model || config.model;
|
|
920
|
+
}
|
|
921
|
+
if (!model) {
|
|
922
|
+
layout.contentWrite(`${ui.yellow}model 不能为空,已取消。${ui.reset}\n`);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
// contextWindowTokens
|
|
926
|
+
{
|
|
927
|
+
const res = await promptIntervention({
|
|
928
|
+
type: 'input',
|
|
929
|
+
title: 'CONTEXT_WINDOW_TOKENS',
|
|
930
|
+
detail: '模型上下文窗口(须对齐真实模型;GLM≈128k,DeepSeek-V3≈64k)。回车采纳预填值。',
|
|
931
|
+
seed: String(preset.window || config.contextWindowTokens),
|
|
932
|
+
});
|
|
933
|
+
if (res.action === 'cancelled') {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const v = (res.value ?? '').trim();
|
|
937
|
+
const n = Number(v);
|
|
938
|
+
if (!v || !Number.isFinite(n) || n <= 0) {
|
|
939
|
+
// 非法输入:保留旧值,不阻断(用 preset.window 或当前值兜底)
|
|
940
|
+
window = preset.window || config.contextWindowTokens;
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
window = Math.floor(n);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
// 3) 应用:内存 config + env(updateModelConfig)→ 持久化(writeConfigKeys)→ 重建 client(reconfigureClient)。
|
|
948
|
+
updateModelConfig({ model, baseURL, apiKey, contextWindowTokens: window });
|
|
949
|
+
writeConfigKeys({
|
|
950
|
+
LLM_BASE_URL: baseURL,
|
|
951
|
+
LLM_API_KEY: apiKey,
|
|
952
|
+
LLM_MODEL: model,
|
|
953
|
+
CONTEXT_WINDOW_TOKENS: String(window),
|
|
954
|
+
});
|
|
955
|
+
reconfigureClient();
|
|
956
|
+
// 4) 刷新 UI:底栏模型名 + 重显横幅(banner() 闭包实时读 config,自动反映新值)。
|
|
957
|
+
refreshStatusBase(history);
|
|
958
|
+
layout.clearContent();
|
|
959
|
+
if (history.some((m) => m.role === 'user')) {
|
|
960
|
+
renderHistory(history);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
layout.contentWrite(bannerString(banner()));
|
|
964
|
+
}
|
|
965
|
+
layout.contentWrite(`${ui.dim}(已切换模型 → ${model} @ ${baseURL})${ui.reset}\n`);
|
|
966
|
+
// 5) dim 警告:shell export 的 LLM 键下次启动会覆盖文件值。
|
|
967
|
+
if (config.llmKeysFromShell.length > 0) {
|
|
968
|
+
layout.contentWrite(`${ui.dim}(shell 环境变量已设 ${config.llmKeysFromShell.join(' / ')},文件写入下次启动被其覆盖;取消该 shell 设置后生效)${ui.reset}\n`);
|
|
969
|
+
}
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
775
972
|
if (line === '/rollback' || line.startsWith('/rollback ')) {
|
|
776
973
|
// /rollback:打开轮次菜单(↑/↓ 选,Enter 回滚到该轮并预填其输入,再 Enter 重新跑)。
|
|
777
974
|
// 忽略任何数字参数(原「输数字选回滚」已删,统一走菜单)。无快照的旧轮次(/resume 重建)文件改动不可撤销。
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// 运行中上下文剔除(drop_context 工具的核心):把历史里无关的 tool 结果替换为存根。
|
|
2
|
+
//
|
|
3
|
+
// 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要),drop_context 是 agent 主动、
|
|
4
|
+
// 精准剔除"已判定无关"的具体 tool 结果——agent 检索到大量无关信息后主动调用,释放上下文。
|
|
5
|
+
//
|
|
6
|
+
// 不变量(对齐 compact.ts):
|
|
7
|
+
// - 永不动 history[0](system prompt)。
|
|
8
|
+
// - 永不动当前轮:从末尾向前找到最后一个 user 消息,该 user 及其之后的 tool 结果一律保留
|
|
9
|
+
// (agent 本轮还在用,踢了会丢失正在进行的上下文)。
|
|
10
|
+
// - tool_call_id 配对:只改 tool 消息的 content,不删消息、不动 tool_calls 数组结构、不改 id。
|
|
11
|
+
// - 原地修改 history(同 compact:length=0;push 重建,repl 持有同一引用)。
|
|
12
|
+
// - 永不抛错(对齐「调度器永不抛错」契约);无匹配 / 无可剔除 → 返 dropped=0。
|
|
13
|
+
import { messageTokens, estimateTokens, } from '../llm/index.js';
|
|
14
|
+
/** 把消息 content 拍平成字符串(OpenAI 可能 string / null / 多模态数组)。 */
|
|
15
|
+
function toText(content) {
|
|
16
|
+
if (content == null)
|
|
17
|
+
return '';
|
|
18
|
+
if (typeof content === 'string')
|
|
19
|
+
return content;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.stringify(content);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return String(content);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
|
|
29
|
+
* 用于划定"当前轮保护区"——该 user 及其之后的消息一律不剔除。
|
|
30
|
+
*/
|
|
31
|
+
function lastUserIndex(history) {
|
|
32
|
+
for (let i = history.length - 1; i >= 1; i--) {
|
|
33
|
+
if (history[i].role === 'user')
|
|
34
|
+
return i;
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
/** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。 */
|
|
39
|
+
function toolNameOf(history, idx) {
|
|
40
|
+
const tcId = history[idx].tool_call_id;
|
|
41
|
+
if (!tcId)
|
|
42
|
+
return null;
|
|
43
|
+
for (let j = idx - 1; j >= 1; j--) {
|
|
44
|
+
const m = history[j];
|
|
45
|
+
if (m.role !== 'assistant')
|
|
46
|
+
continue;
|
|
47
|
+
const tcs = m
|
|
48
|
+
.tool_calls;
|
|
49
|
+
if (!tcs)
|
|
50
|
+
continue;
|
|
51
|
+
const hit = tcs.find((tc) => tc?.id === tcId);
|
|
52
|
+
if (hit)
|
|
53
|
+
return hit.function?.name ?? null;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 剔除历史里命中的旧 tool 结果(原地修改 history)。
|
|
59
|
+
*
|
|
60
|
+
* 筛选(各维度 AND 组合):
|
|
61
|
+
* - toolNames:只剔除这些工具名的结果(空 = 不限)
|
|
62
|
+
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果(空 = 不限)
|
|
63
|
+
*
|
|
64
|
+
* 保护:history[0](system)+ 当前轮(最后一个 user 及其之后)永不剔除。
|
|
65
|
+
* 已是存根的 tool 消息(含「已剔除」标记)不重复剔除(幂等)。
|
|
66
|
+
*
|
|
67
|
+
* 永不抛错;无匹配返 dropped=0。
|
|
68
|
+
*/
|
|
69
|
+
export function dropContextFromHistory(history, filter) {
|
|
70
|
+
const toolNames = filter.toolNames && filter.toolNames.length > 0
|
|
71
|
+
? new Set(filter.toolNames)
|
|
72
|
+
: null;
|
|
73
|
+
const contains = filter.contains && filter.contains.length > 0
|
|
74
|
+
? filter.contains.map((s) => s.toLowerCase())
|
|
75
|
+
: null;
|
|
76
|
+
// 当前轮保护区:最后一个 user 及其之后一律保留(agent 还在用)。
|
|
77
|
+
const guard = lastUserIndex(history);
|
|
78
|
+
// guard <= 0 表示无 user 或 user 就是 history[0](不会):整段历史都可剔除(除 history[0])。
|
|
79
|
+
const protectedFrom = guard > 0 ? guard : 0; // < protectedFrom 的才可剔除(即 [1, protectedFrom)
|
|
80
|
+
const items = [];
|
|
81
|
+
let freedTokens = 0;
|
|
82
|
+
const STUB_PREFIX = '⌦[已剔除:与当前任务无关]';
|
|
83
|
+
for (let i = 1; i < protectedFrom; i++) {
|
|
84
|
+
const m = history[i];
|
|
85
|
+
if (m.role !== 'tool')
|
|
86
|
+
continue;
|
|
87
|
+
const content = toText(m.content);
|
|
88
|
+
// 幂等:已是存根(含标记)不重复剔除。
|
|
89
|
+
if (content.startsWith(STUB_PREFIX))
|
|
90
|
+
continue;
|
|
91
|
+
// 维度 1:工具名
|
|
92
|
+
const tname = toolNameOf(history, i);
|
|
93
|
+
if (toolNames && (!tname || !toolNames.has(tname)))
|
|
94
|
+
continue;
|
|
95
|
+
// 维度 2:内容关键词(AND)
|
|
96
|
+
if (contains) {
|
|
97
|
+
const lower = content.toLowerCase();
|
|
98
|
+
if (!contains.every((kw) => lower.includes(kw)))
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// 命中 → 替换为存根(保 tool_call_id 不动,只改 content)
|
|
102
|
+
const before = messageTokens(m);
|
|
103
|
+
const id = m.tool_call_id ?? '';
|
|
104
|
+
const stub = `${STUB_PREFIX} 原 ${tname ?? 'tool'} 结果(${content.length} 字符,约 ${before} tokens)${id ? ` · id …${id.slice(-6)}` : ''}⌫`;
|
|
105
|
+
m.content = stub;
|
|
106
|
+
const after = messageTokens(m);
|
|
107
|
+
freedTokens += Math.max(0, before - after);
|
|
108
|
+
items.push({
|
|
109
|
+
toolName: tname ?? 'tool',
|
|
110
|
+
toolCallId: id.slice(-6),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
dropped: items.length,
|
|
115
|
+
freedTokens,
|
|
116
|
+
items,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** 给 drop_context 工具结果格式化人类可读摘要(回灌给 agent)。 */
|
|
120
|
+
export function formatDropResult(r) {
|
|
121
|
+
if (r.dropped === 0) {
|
|
122
|
+
return '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。';
|
|
123
|
+
}
|
|
124
|
+
const lines = [
|
|
125
|
+
`已剔除 ${r.dropped} 条无关工具结果,释放约 ${r.freedTokens} tokens。`,
|
|
126
|
+
'被剔除项(已替换为存根,tool_call_id 配对不变):',
|
|
127
|
+
];
|
|
128
|
+
for (const it of r.items) {
|
|
129
|
+
lines.push(` - ${it.toolName} (id …${it.toolCallId})`);
|
|
130
|
+
}
|
|
131
|
+
lines.push('这些结果在后续上下文中仅保留存根标记,不再占用篇幅。');
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
// 供 drop_context 工具估算用(避免直接 import llm 的公开 API 造成耦合,这里重导出)。
|
|
135
|
+
export { estimateTokens };
|
package/dist/session/index.js
CHANGED
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
* 依赖方向:session → {llm(摘要复用 chat), config, ui};llm 不反向依赖 session。
|
|
6
6
|
*/
|
|
7
7
|
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, } from './compact.js';
|
|
8
|
+
export { dropContextFromHistory, formatDropResult, } from './drop.js';
|
|
8
9
|
export { newSessionId, saveSession, loadSession, listSessions, sessionDir, } from './persist.js';
|
|
@@ -3,10 +3,10 @@ import { promptIntervention } from '../../ui/intervention.js';
|
|
|
3
3
|
export const askHumanTool = {
|
|
4
4
|
name: 'ask_human',
|
|
5
5
|
description: [
|
|
6
|
-
'
|
|
7
|
-
'Use when: multiple
|
|
8
|
-
'
|
|
9
|
-
'
|
|
6
|
+
'Ask the user for input at a decision point (blocks until they respond).',
|
|
7
|
+
' Use when: multiple approaches need a user decision, intent is unclear, or extra info is needed.',
|
|
8
|
+
' Don\'t call when the task is clear and you can decide — it interrupts the user.',
|
|
9
|
+
' Options (2-6) let the user pick; omit/empty for free-text input. Their answer is returned as the result.',
|
|
10
10
|
].join(''),
|
|
11
11
|
parameters: {
|
|
12
12
|
type: 'object',
|
|
@@ -50,9 +50,9 @@ function runCodegraph(args) {
|
|
|
50
50
|
}
|
|
51
51
|
export const codegraphTool = {
|
|
52
52
|
name: 'codegraph',
|
|
53
|
-
description: 'Preferred
|
|
54
|
-
' Returns
|
|
55
|
-
'
|
|
53
|
+
description: 'Preferred for understanding/locating code, tracing call chains, assessing impact of changes in repos with a code index (.codegraph/).' +
|
|
54
|
+
' Returns symbol source + call paths in one shot — more accurate/economical than read_file/grep.' +
|
|
55
|
+
' Hints to run `codegraph init` when .codegraph/ is absent.',
|
|
56
56
|
parameters: {
|
|
57
57
|
type: 'object',
|
|
58
58
|
properties: {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ---------- drop_context ----------
|
|
2
|
+
/**
|
|
3
|
+
* 运行中上下文剔除工具:agent 检索到大量无关信息后,主动把历史里无关的 tool 结果替换为存根,
|
|
4
|
+
* 释放上下文空间(保 tool_call_id 配对不变量,只改 content)。
|
|
5
|
+
*
|
|
6
|
+
* 与 compact 的区别:compact 是阈值触发的整体压缩(微截 + 摘要);drop_context 是 agent 主动、
|
|
7
|
+
* 精准剔除"已判定无关"的具体 tool 结果。
|
|
8
|
+
*
|
|
9
|
+
* 保护:history[0](system)与当前轮(最后一个 user 及其之后)永不剔除——agent 还在用。
|
|
10
|
+
* 已是存根的不重复剔除(幂等)。永不抛错。
|
|
11
|
+
*
|
|
12
|
+
* 筛选(各维度 AND 组合,全部可选;不传 = 剔除所有可剔除的旧 tool 结果):
|
|
13
|
+
* - toolNames:只剔除这些工具名的结果(如 ["grep","read_file"])
|
|
14
|
+
* - contains:只剔除内容包含所有这些词(AND、大小写不敏感)的结果
|
|
15
|
+
*
|
|
16
|
+
* plan 模式不禁用:纯上下文管理,无文件 / 命令副作用。
|
|
17
|
+
*/
|
|
18
|
+
export const dropContextTool = {
|
|
19
|
+
name: 'drop_context',
|
|
20
|
+
description: [
|
|
21
|
+
'Drop (stub-replace) irrelevant OLDER tool results from history to free context.',
|
|
22
|
+
'COST-AWARE: ~300-token round-trip; only call if freed tokens clearly exceed it — i.e. MULTIPLE bulky results (e.g. a wide grep/read sweep of mostly-irrelevant hits), not a single small one or near done.',
|
|
23
|
+
'Never dropped: system prompt and the CURRENT turn (last user message onward). Idempotent. Filters AND-combine; omit both = drop all droppable. Returns dropped count, freed tokens, tool names.',
|
|
24
|
+
].join(' '),
|
|
25
|
+
parameters: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
toolNames: {
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: { type: 'string' },
|
|
31
|
+
description: 'Only drop results from these tool names (e.g. ["grep","read_file"]). Empty/omitted = no tool-name filter.',
|
|
32
|
+
},
|
|
33
|
+
contains: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: { type: 'string' },
|
|
36
|
+
description: 'Only drop results whose content contains ALL of these keywords (AND, case-insensitive). Empty/omitted = no content filter.',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: [],
|
|
40
|
+
},
|
|
41
|
+
async execute(args, ctx) {
|
|
42
|
+
const dropContext = ctx?.dropContext;
|
|
43
|
+
if (!dropContext) {
|
|
44
|
+
// 无注入(理论上不会:runAgentCore 总注入)。降级:不改 history,告知 agent。
|
|
45
|
+
return '错误:上下文剔除回调不可用(未由 agent 循环注入),无法剔除。';
|
|
46
|
+
}
|
|
47
|
+
const filter = {};
|
|
48
|
+
if (Array.isArray(args.toolNames)) {
|
|
49
|
+
filter.toolNames = args.toolNames
|
|
50
|
+
.filter((v) => typeof v === 'string' && v.length > 0)
|
|
51
|
+
.map((v) => String(v));
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(args.contains)) {
|
|
54
|
+
filter.contains = args.contains
|
|
55
|
+
.filter((v) => typeof v === 'string' && v.length > 0)
|
|
56
|
+
.map((v) => String(v));
|
|
57
|
+
}
|
|
58
|
+
const result = dropContext(filter);
|
|
59
|
+
return result.dropped === 0
|
|
60
|
+
? '未剔除任何工具结果(无匹配的旧 tool 消息,或均在当前轮保护区内不可剔除)。'
|
|
61
|
+
: [
|
|
62
|
+
`已剔除 ${result.dropped} 条无关工具结果,释放约 ${result.freedTokens} tokens。`,
|
|
63
|
+
'被剔除项(已替换为存根,tool_call_id 配对不变):',
|
|
64
|
+
...result.items.map((it) => ` - ${it.toolName} (id …${it.toolCallId})`),
|
|
65
|
+
'这些结果在后续上下文中仅保留存根标记,不再占用篇幅。',
|
|
66
|
+
].join('\n');
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
// ---------- edit_file ----------
|
|
4
4
|
export const editFileTool = {
|
|
5
5
|
name: 'edit_file',
|
|
6
|
-
description: '
|
|
6
|
+
description: 'Replace a string in a file. old_string must occur exactly once and match exactly (including indentation/newlines). Use write_file for new files.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -4,8 +4,8 @@ import { getSandboxRoot, isInsideRoot } from '../../sandbox/index.js';
|
|
|
4
4
|
// ---------- glob ----------
|
|
5
5
|
export const globTool = {
|
|
6
6
|
name: 'glob',
|
|
7
|
-
description: 'Find
|
|
8
|
-
'
|
|
7
|
+
description: 'Find files matching a glob pattern (e.g. **/*.ts). Auto-excludes node_modules/.git.' +
|
|
8
|
+
' For architecture/call chains, prefer codegraph.',
|
|
9
9
|
parameters: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
@@ -5,8 +5,8 @@ import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.j
|
|
|
5
5
|
// ---------- grep ----------
|
|
6
6
|
export const grepTool = {
|
|
7
7
|
name: 'grep',
|
|
8
|
-
description: 'Search file contents by regex, returning file:line: matched lines. Recursively searches
|
|
9
|
-
'
|
|
8
|
+
description: 'Search file contents by regex, returning file:line: matched lines. Recursively searches cwd excluding node_modules/.git. Optional glob restricts file types.' +
|
|
9
|
+
' For architecture/call chains, prefer codegraph.',
|
|
10
10
|
parameters: {
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {
|
|
@@ -10,6 +10,7 @@ import { useSkillTool } from './use-skill.js';
|
|
|
10
10
|
import { askHumanTool } from './ask-human.js';
|
|
11
11
|
import { codegraphTool } from './codegraph.js';
|
|
12
12
|
import { switchModeTool } from './switch-mode.js';
|
|
13
|
+
import { dropContextTool } from './drop-context.js';
|
|
13
14
|
import { memorySaveTool } from './memory-save.js';
|
|
14
15
|
import { memorySearchTool } from './memory-search.js';
|
|
15
16
|
import { memoryListTool } from './memory-list.js';
|
|
@@ -33,6 +34,7 @@ export const builtinTools = [
|
|
|
33
34
|
useSkillTool,
|
|
34
35
|
askHumanTool,
|
|
35
36
|
switchModeTool, // plan↔auto 自切(两模式都可见,不进 PLAN_DISABLED_TOOLS;副作用控制工具→串行分支)
|
|
37
|
+
dropContextTool, // 运行中剔除无关 tool 结果(上下文管理,无副作用;两模式都可见,串行分支)
|
|
36
38
|
memorySaveTool,
|
|
37
39
|
memorySearchTool,
|
|
38
40
|
memoryListTool,
|
|
@@ -3,7 +3,7 @@ import { forgetEntry } from '../../memory/store.js';
|
|
|
3
3
|
// 遗忘:默认归档(archived,从索引/默认搜索隐藏,可复活);mode=delete 硬删。pinned 拒删。
|
|
4
4
|
export const memoryForgetTool = {
|
|
5
5
|
name: 'memory_forget',
|
|
6
|
-
description: 'Forget a memory entry:
|
|
6
|
+
description: 'Forget a memory entry: default archive (hidden, revivable via memory_update); mode=delete hard-deletes. Pinned entries can\'t be deleted (unpin first).',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -3,7 +3,7 @@ import { listEntries } from '../../memory/store.js';
|
|
|
3
3
|
// 列索引(id/name/summary,无正文、不 bump recall)。用于浏览有哪些、拿 id 再 memory_search 取正文。
|
|
4
4
|
export const memoryListTool = {
|
|
5
5
|
name: 'memory_list',
|
|
6
|
-
description: 'List the memory index (id/name/summary, no body).
|
|
6
|
+
description: 'List the memory index (id/name/summary, no body). Get an id, then use memory_search for the full body.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -4,7 +4,7 @@ import { saveEntry } from '../../memory/store.js';
|
|
|
4
4
|
// 撞库(name→id 已存在)拒绝,引导用 memory_update。
|
|
5
5
|
export const memorySaveTool = {
|
|
6
6
|
name: 'memory_save',
|
|
7
|
-
description: 'Save a long-term memory entry
|
|
7
|
+
description: 'Save a cross-session long-term memory entry. Store only non-obvious, useful facts/decisions/pitfalls. Title enters the startup index; retrieve body via memory_search.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
@@ -4,7 +4,7 @@ import { searchEntries } from '../../memory/store.js';
|
|
|
4
4
|
// 结果走 capToolResultForHistory 的放宽上限(同 use_skill,保正文完整)。
|
|
5
5
|
export const memorySearchTool = {
|
|
6
6
|
name: 'memory_search',
|
|
7
|
-
description: 'Search memory entries by keyword (
|
|
7
|
+
description: 'Search memory entries by keyword (substring match), returning full body.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
@@ -3,7 +3,7 @@ import { updateEntry } from '../../memory/store.js';
|
|
|
3
3
|
// 原地改一条记忆(id 不变)。反思的弱意义:干活时发现事实变了/过时即纠正。
|
|
4
4
|
export const memoryUpdateTool = {
|
|
5
5
|
name: 'memory_update',
|
|
6
|
-
description: 'Update a memory entry in place (id unchanged). Use when facts changed / correcting outdated info /
|
|
6
|
+
description: 'Update a memory entry in place (id unchanged). Use when facts changed / correcting outdated info / toggling pinned. Get id from memory_list or memory_search.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
@@ -4,8 +4,8 @@ import { MAX_FILE_LINES } from '../constants.js';
|
|
|
4
4
|
// ---------- read_file ----------
|
|
5
5
|
export const readFileTool = {
|
|
6
6
|
name: 'read_file',
|
|
7
|
-
description: 'Read file content
|
|
8
|
-
'
|
|
7
|
+
description: 'Read file content with line numbers. Read before editing. offset (1-based, default 1), limit (default 2000).' +
|
|
8
|
+
' For architecture/call chains, prefer codegraph over reading files one at a time.',
|
|
9
9
|
parameters: {
|
|
10
10
|
type: 'object',
|
|
11
11
|
properties: {
|
|
@@ -4,7 +4,7 @@ import { getSandboxRoot, filterEnv, isCommandDenied } from '../../sandbox/index.
|
|
|
4
4
|
// ---------- run_command ----------
|
|
5
5
|
export const runCommandTool = {
|
|
6
6
|
name: 'run_command',
|
|
7
|
-
description: '
|
|
7
|
+
description: 'Run a shell command, merging stdout+stderr. Default timeout 120s. For tests, builds, git, etc.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
@@ -3,11 +3,9 @@ import { setAgentMode, getAgentMode } from '../../agent/mode.js';
|
|
|
3
3
|
export const switchModeTool = {
|
|
4
4
|
name: 'switch_mode',
|
|
5
5
|
description: [
|
|
6
|
-
'Switch
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'Do NOT switch to auto if the user entered plan mode manually (via /plan or Shift+Tab) for a safety review — in that case present the plan and STOP; the user will approve via a prompt and execution happens in a follow-up turn.',
|
|
10
|
-
'Switching to plan from auto is rarely needed; do it only if you realize you should investigate before changing anything.',
|
|
6
|
+
'Switch agent mode between "plan" (read-only investigation) and "auto" (full tool execution).',
|
|
7
|
+
'Use to transition from planning to execution WITHIN THE SAME TURN: in plan, after presenting a plan, call switch_mode("auto") and continue — ONLY when the user asked for autonomous execution ("先 plan 再 auto" etc.).',
|
|
8
|
+
'If the user entered plan mode manually (/plan or Shift+Tab) for review, do NOT switch to auto — present the plan and STOP for approval.',
|
|
11
9
|
].join(' '),
|
|
12
10
|
parameters: {
|
|
13
11
|
type: 'object',
|
|
@@ -9,10 +9,9 @@ import { MAX_OUTPUT } from '../constants.js';
|
|
|
9
9
|
export const taskTool = {
|
|
10
10
|
name: 'task',
|
|
11
11
|
description: [
|
|
12
|
-
'Spawn a sub-agent
|
|
13
|
-
'Use when
|
|
14
|
-
'
|
|
15
|
-
'Optionally restrict the sub-agent to a subset of tools (e.g. read-only tools for pure investigation) via the "tools" parameter.',
|
|
12
|
+
'Spawn a sub-agent for an isolated sub-task (independent history; only its final summary returns to you).',
|
|
13
|
+
'Use when a task splits into independent parts or its many tool calls would bloat your context.',
|
|
14
|
+
'Cannot recursively spawn sub-agents.',
|
|
16
15
|
].join(''),
|
|
17
16
|
parameters: {
|
|
18
17
|
type: 'object',
|
|
@@ -24,7 +23,7 @@ export const taskTool = {
|
|
|
24
23
|
tools: {
|
|
25
24
|
type: 'array',
|
|
26
25
|
items: { type: 'string' },
|
|
27
|
-
description: 'Optional whitelist of tool names the sub-agent is allowed to use (e.g. ["read_file","glob","grep","codegraph"] for read-only investigation). Omit to allow all tools.',
|
|
26
|
+
description: 'Optional whitelist of tool names the sub-agent is allowed to use (e.g. ["read_file","glob","grep","codegraph"] for read-only investigation). Omit to allow all tools. If the sub-task needs verification/build/test (running scripts, typecheck, etc.), remember to include "run_command".',
|
|
28
27
|
},
|
|
29
28
|
maxSteps: {
|
|
30
29
|
type: 'number',
|
|
@@ -4,7 +4,7 @@ import { getSkillBody } from '../../skills/index.js';
|
|
|
4
4
|
// 系统提示里已列出可用 skill 的 name + description(何时用),模型据此决定调用。
|
|
5
5
|
export const useSkillTool = {
|
|
6
6
|
name: 'use_skill',
|
|
7
|
-
description: 'Load
|
|
7
|
+
description: 'Load the full SKILL.md instructions for a given skill. See the skill list in the system prompt for when to use each.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
@@ -4,7 +4,7 @@ const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
|
|
|
4
4
|
// ---------- web_fetch ----------
|
|
5
5
|
export const webFetchTool = {
|
|
6
6
|
name: 'web_fetch',
|
|
7
|
-
description: 'Fetch
|
|
7
|
+
description: 'Fetch a URL and clean HTML to body text. Use to read a link from search results or a URL given by the user.',
|
|
8
8
|
parameters: {
|
|
9
9
|
type: 'object',
|
|
10
10
|
properties: {
|
|
@@ -6,7 +6,7 @@ const MAX_CONTENT_CHARS = 800;
|
|
|
6
6
|
// ---------- web_search ----------
|
|
7
7
|
export const webSearchTool = {
|
|
8
8
|
name: 'web_search',
|
|
9
|
-
description: 'Search the web (AnySearch). Returns title/url/snippet/body
|
|
9
|
+
description: 'Search the web (AnySearch). Returns title/url/snippet/body per result. Optional tag for sub-domain capability.',
|
|
10
10
|
parameters: {
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {
|
|
@@ -3,7 +3,7 @@ import { resolve, dirname } from 'node:path';
|
|
|
3
3
|
// ---------- write_file ----------
|
|
4
4
|
export const writeFileTool = {
|
|
5
5
|
name: 'write_file',
|
|
6
|
-
description: 'Create or overwrite a file
|
|
6
|
+
description: 'Create or overwrite a file; parent dirs created.',
|
|
7
7
|
parameters: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
package/dist/tools/registry.js
CHANGED
|
@@ -11,6 +11,7 @@ export const tools = builtinTools;
|
|
|
11
11
|
* signal 透传给 tool.execute(经 ctx):长任务工具(run_command/web_fetch)abort 即时取消,
|
|
12
12
|
* 让用户 Ctrl+C 能跟手中断工具执行(而非等命令跑完 / 超时)。
|
|
13
13
|
* opts.skipRollback:子 agent 逻辑隔离用——跳过 recordMutation,子 agent 改动不进主回滚快照链。
|
|
14
|
+
* opts.dropContext:上下文剔除回调(drop_context 工具用),透传给 tool.execute 经 ctx。
|
|
14
15
|
*/
|
|
15
16
|
export async function executeTool(name, argsRaw, signal, opts) {
|
|
16
17
|
const tool = tools.find((t) => t.name === name);
|
|
@@ -38,7 +39,11 @@ export async function executeTool(name, argsRaw, signal, opts) {
|
|
|
38
39
|
args.path) {
|
|
39
40
|
recordMutation(args.path);
|
|
40
41
|
}
|
|
41
|
-
return await tool.execute(args, {
|
|
42
|
+
return await tool.execute(args, {
|
|
43
|
+
signal,
|
|
44
|
+
skipRollback: opts?.skipRollback,
|
|
45
|
+
dropContext: opts?.dropContext,
|
|
46
|
+
});
|
|
42
47
|
}
|
|
43
48
|
catch (e) {
|
|
44
49
|
return `错误:工具 ${name} 执行失败: ${e instanceof Error ? e.message : String(e)}`;
|