mocode-ai 0.1.4 → 0.1.6
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 +196 -188
- package/dist/agent/core.js +309 -0
- package/dist/agent/index.js +62 -249
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +81 -36
- package/dist/context/classifier.js +83 -0
- package/dist/context/encoders/index.js +10 -0
- package/dist/context/encoders/passthrough.js +23 -0
- package/dist/context/index.js +10 -0
- package/dist/context/pipeline.js +84 -0
- package/dist/context/registry.js +30 -0
- package/dist/context/types.js +12 -0
- package/dist/index.js +12 -2
- package/dist/llm/index.js +11 -9
- package/dist/memory/index.js +2 -2
- package/dist/memory/reflect.js +11 -11
- package/dist/memory/store.js +4 -4
- package/dist/repl/index.js +269 -109
- package/dist/sandbox/command.js +41 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/jail.js +75 -0
- package/dist/sandbox/policy.js +58 -0
- package/dist/sandbox/root.js +19 -0
- package/dist/session/compact.js +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +7 -7
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +10 -5
- package/dist/tools/builtins/grep.js +14 -9
- package/dist/tools/builtins/index.js +7 -1
- package/dist/tools/builtins/memory-forget.js +2 -2
- package/dist/tools/builtins/memory-list.js +2 -2
- package/dist/tools/builtins/memory-save.js +7 -7
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/memory-update.js +6 -6
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +47 -8
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +16 -0
- package/dist/tools/registry.js +18 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +24 -3
- package/dist/ui/layout.js +295 -81
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +259 -5
- package/dist/ui/render.js +1 -1
- package/dist/ui/theme.js +146 -13
- package/package.json +2 -2
package/dist/config/index.js
CHANGED
|
@@ -28,6 +28,8 @@ function loadEnvFiles() {
|
|
|
28
28
|
process.env[k] = v;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// 在 loadEnvFiles 回填前捕获:MOCODE_THEME 是否由 shell 设置(决定 /theme 写文件是否下次启动生效)。
|
|
32
|
+
const themeFromShell = process.env.MOCODE_THEME !== undefined;
|
|
31
33
|
loadEnvFiles();
|
|
32
34
|
function requireEnv(key) {
|
|
33
35
|
const v = process.env[key];
|
|
@@ -37,42 +39,79 @@ function requireEnv(key) {
|
|
|
37
39
|
}
|
|
38
40
|
return v;
|
|
39
41
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
##
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
-
|
|
42
|
+
const PLATFORM_NOTE = (() => {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
return `## Environment (Windows)
|
|
45
|
+
- You are on Windows; run_command runs commands via cmd.exe (/c). Unix shell builtins are NOT available here.
|
|
46
|
+
- Windows equivalents: which→where, cat→type, ls→dir, rm→del/rd, cp→copy, mv→move. cmd.exe uses %VAR% (not $VAR); pipes (|) and redirects (>, >>) work, but no $(...) command substitution or backticks.
|
|
47
|
+
- head/tail/find/grep/sed have no cmd.exe equivalent — use the dedicated tools (read_file for head/tail, glob for find, grep for grep), or invoke PowerShell via run_command if you need more.
|
|
48
|
+
- Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they're cross-platform and already wired in.`;
|
|
49
|
+
}
|
|
50
|
+
if (process.platform === 'darwin') {
|
|
51
|
+
return `## Environment (macOS)
|
|
52
|
+
- You are on macOS; run_command runs via bash -c (user default shell may be zsh). BSD coreutils, not GNU.
|
|
53
|
+
- Pitfalls: sed -i needs an empty backup-ext arg (sed -i '' 's/x/y/' file); grep -P unavailable (use grep -E or the grep tool); find/readlink/date are BSD variants; readlink -f unsupported (use realpath, or greadlink -f if GNU coreutils installed via brew).
|
|
54
|
+
- Prefer the dedicated tools (read_file/glob/grep) over shell equivalents — they sidestep BSD/GNU differences.`;
|
|
55
|
+
}
|
|
56
|
+
return `## Environment (Linux/Unix)
|
|
57
|
+
- You are on ${process.platform}; run_command runs via bash -c. GNU coreutils — standard POSIX/GNU shell syntax is safe.
|
|
58
|
+
- Still prefer the dedicated tools (read_file/glob/grep) over hand-rolled shell where they fit — they avoid quoting pitfalls and are already wired in.`;
|
|
59
|
+
})();
|
|
60
|
+
const SYSTEM_PROMPT = `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
|
|
61
|
+
|
|
62
|
+
${PLATFORM_NOTE}
|
|
63
|
+
|
|
64
|
+
## Workflow
|
|
65
|
+
- Understand before acting: when unsure about requirements or code state, explore first; don't assume.
|
|
66
|
+
- Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
|
|
67
|
+
- Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
|
|
68
|
+
|
|
69
|
+
## Tool Guidelines
|
|
70
|
+
- See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
|
|
71
|
+
- **Prefer codegraph for code exploration**: when understanding/locating code, tracing call chains, or assessing impact of changes, if a .codegraph/ index exists, use the codegraph tool first (explore to query by question, node to look up a single symbol) — it returns relevant source + call paths in one shot, more accurate and economical than piecing together via read_file/grep. Fall back to read_file / grep / glob only when codegraph is unavailable (no index), misses, you need to see just-changed content, or you're editing a single known small file. Build the index first with \`codegraph init\` if none exists.
|
|
72
|
+
- Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory.
|
|
73
|
+
- For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
|
|
74
|
+
- Use glob to find file paths, grep to search content; don't use run_command to pipe cat / sed / find / grep.
|
|
75
|
+
- run_command runs per platform (cmd on Windows, bash elsewhere); state intent before running commands with side effects (deleting files, installing packages, git push, resets, etc.).
|
|
76
|
+
- 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
|
+
- 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
|
+
- 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.
|
|
79
|
+
|
|
80
|
+
## Failure Handling
|
|
81
|
+
- 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.
|
|
82
|
+
- When a command errors, read the actual output before judging; don't skip it.
|
|
83
|
+
|
|
84
|
+
## Safety & Boundaries
|
|
85
|
+
- Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
|
|
86
|
+
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
87
|
+
|
|
88
|
+
## Memory (cross-session long-term facts)
|
|
89
|
+
- A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
|
|
90
|
+
- Store non-obvious, cross-session-useful facts/decisions/pitfalls (architecture conventions, gotchas, user preferences, decisions made) with memory_save — only long-term stable items, not current bugs / temp files / undecided TODOs.
|
|
91
|
+
- If an existing memory is outdated or contradicts new facts, correct it in-place with memory_update(id, …) (don't create a duplicate); archive clearly-stale ones with memory_forget(id).
|
|
92
|
+
- Before saving, memory_search to check for an existing similar entry to avoid duplicates. Better to store less than to store trivially correct information.
|
|
93
|
+
- A background reflection pass periodically mines and organizes memories from the session (no manual action needed), but key facts you proactively save are more reliable.
|
|
94
|
+
|
|
95
|
+
## Plan vs Auto modes
|
|
96
|
+
- Default is AUTO mode: you research and execute with all tools (read/edit/run_command/memory/web/skills).
|
|
97
|
+
- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.
|
|
98
|
+
|
|
99
|
+
## Termination & Reporting
|
|
100
|
+
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
101
|
+
- Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.`;
|
|
102
|
+
/**
|
|
103
|
+
* plan 模式追加到系统提示末尾的指令(切到 plan 模式时由 repl 拼进 history[0])。
|
|
104
|
+
* 与 SYSTEM_PROMPT 同语种(英文),指示:只读探查、产出步骤化计划、不执行、审批后回 auto。
|
|
105
|
+
*/
|
|
106
|
+
export const PLAN_MODE_SUFFIX = `
|
|
107
|
+
|
|
108
|
+
## ⛯ PLAN MODE (active now)
|
|
109
|
+
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
110
|
+
- Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
|
|
111
|
+
- 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
|
+
- 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
|
+
- 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; the user will approve via a prompt and execution happens in a follow-up turn.`;
|
|
76
115
|
export const config = {
|
|
77
116
|
baseURL: requireEnv('LLM_BASE_URL'),
|
|
78
117
|
apiKey: requireEnv('LLM_API_KEY'),
|
|
@@ -83,9 +122,15 @@ export const config = {
|
|
|
83
122
|
compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
|
|
84
123
|
includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
|
|
85
124
|
autoCompact: process.env.AUTO_COMPACT !== 'false',
|
|
125
|
+
contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
|
|
86
126
|
autoReflect: process.env.AUTO_REFLECT !== 'false',
|
|
87
127
|
reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
|
|
128
|
+
maxSteps: Number(process.env.MAX_STEPS) || 200,
|
|
129
|
+
subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || 50,
|
|
88
130
|
sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
|
|
89
131
|
searchApiKey: process.env.ANYSEARCH_API_KEY,
|
|
132
|
+
sandboxRoot: process.env.SANDBOX_ROOT || undefined,
|
|
90
133
|
searchBaseUrl: process.env.ANYSEARCH_BASE_URL || 'https://api.anysearch.com',
|
|
134
|
+
theme: process.env.MOCODE_THEME || 'default',
|
|
135
|
+
themeFromShell,
|
|
91
136
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Context Classifier:据工具名(强先验)+ 输出形状(启发)+ 兜底,选 ContextKind。
|
|
2
|
+
//
|
|
3
|
+
// 三级信号:
|
|
4
|
+
// 1) 名字强先验(BY_NAME 表,覆盖全部 17 内置工具,确定性强)。
|
|
5
|
+
// 2) 形状启发(为 MCP 工具 / 未来工具 / 未登记工具兜底识别)。
|
|
6
|
+
// 3) 兜底 'passthrough'(不认识 = 不动,零行为变化)。
|
|
7
|
+
//
|
|
8
|
+
// 单一事实源风格(仿 tools/constants.ts 的 READ_TOOL_NAMES / PLAN_DISABLED_TOOLS)。
|
|
9
|
+
// 加新工具:在 BY_NAME 加一行;或靠形状启发自动识别。
|
|
10
|
+
/** 工具名 → ContextKind 的强先验表(覆盖全部 17 内置工具)。 */
|
|
11
|
+
const BY_NAME = {
|
|
12
|
+
// tree:路径列表 → 缩进树
|
|
13
|
+
glob: 'tree',
|
|
14
|
+
// search:file:line 分组
|
|
15
|
+
grep: 'search',
|
|
16
|
+
web_search: 'search',
|
|
17
|
+
// graph:CLI dump → 精炼图
|
|
18
|
+
codegraph: 'graph',
|
|
19
|
+
// log:分级 / 折叠 / 尾偏置
|
|
20
|
+
run_command: 'log',
|
|
21
|
+
// code:保行号(edit_file 依赖,最敏感)
|
|
22
|
+
read_file: 'code',
|
|
23
|
+
// table:列对齐
|
|
24
|
+
memory_list: 'table',
|
|
25
|
+
// memory:紧凑卡片
|
|
26
|
+
memory_search: 'memory',
|
|
27
|
+
// doc:去噪音保正文
|
|
28
|
+
web_fetch: 'doc',
|
|
29
|
+
use_skill: 'doc',
|
|
30
|
+
// status:一行状态(identity,不动)
|
|
31
|
+
edit_file: 'status',
|
|
32
|
+
write_file: 'status',
|
|
33
|
+
ask_human: 'status',
|
|
34
|
+
switch_mode: 'status',
|
|
35
|
+
memory_save: 'status',
|
|
36
|
+
memory_update: 'status',
|
|
37
|
+
memory_forget: 'status',
|
|
38
|
+
// summary:子 agent 摘要(轻量)
|
|
39
|
+
task: 'summary',
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 形状启发:对未在 BY_NAME 登记的工具输出做模式识别(为 MCP / 未来工具兜底)。
|
|
43
|
+
* 故意保守:识别不准时回落 passthrough(不动),宁可不少省也不可错改。
|
|
44
|
+
*/
|
|
45
|
+
function classifyByShape(output) {
|
|
46
|
+
// file:line: content 形(grep 风格)
|
|
47
|
+
if (/^[^\n:]+:\d+:[^\n]*$/m.test(output))
|
|
48
|
+
return 'search';
|
|
49
|
+
// [退出码 N] 前缀(run_command / codegraph 风格)
|
|
50
|
+
if (/^\[退出码 \d+\]/m.test(output))
|
|
51
|
+
return 'log';
|
|
52
|
+
// 路径列表:多行都是含分隔符的相对路径(glob 风格)
|
|
53
|
+
const lines = output.split('\n').filter((l) => l.trim().length > 0);
|
|
54
|
+
if (lines.length >= 3 &&
|
|
55
|
+
lines.every((l) => /^[\w.\-\\/ ]+$/.test(l.trim()) && /[\\/]/.test(l))) {
|
|
56
|
+
return 'tree';
|
|
57
|
+
}
|
|
58
|
+
// JSON 结构化(web_fetch 的 JSON 响应等)→ doc 渲染
|
|
59
|
+
const trimmed = output.trimStart();
|
|
60
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('['))
|
|
61
|
+
return 'doc';
|
|
62
|
+
return 'passthrough';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 判定 ContextKind。
|
|
66
|
+
* - 有 BY_NAME 强先验 → 用之(内置工具确定性强)。
|
|
67
|
+
* - 否则形状启发(MCP / 未来工具)。
|
|
68
|
+
* - 都不中 → passthrough(不动)。
|
|
69
|
+
*
|
|
70
|
+
* @param toolName 工具名
|
|
71
|
+
* @param output 工具原始输出(形状启发用;有 BY_NAME 时不读)
|
|
72
|
+
* @param _args 已解析参数(预留:未来 read_file 的 offset/limit 可影响 code 编码策略;Phase 1 不用)
|
|
73
|
+
*/
|
|
74
|
+
export function classify(toolName, output, _args) {
|
|
75
|
+
const byName = BY_NAME[toolName];
|
|
76
|
+
if (byName)
|
|
77
|
+
return byName;
|
|
78
|
+
return classifyByShape(output);
|
|
79
|
+
}
|
|
80
|
+
/** 暴露 BY_NAME 副本供调试 / 未来 /context 展示(只读视图)。 */
|
|
81
|
+
export function knownToolKinds() {
|
|
82
|
+
return { ...BY_NAME };
|
|
83
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// 内置 encoder 清单。启动期 pipeline 首次调用时经 registerAll 注册到 registry。
|
|
2
|
+
//
|
|
3
|
+
// Phase 1:仅 passthrough(identity)→ 全链路零行为变化(所有 kind 都回落到它)。
|
|
4
|
+
// Phase 2 起逐步加入:tree / search / log / code / table / memory(见各 encoder 文件)。
|
|
5
|
+
//
|
|
6
|
+
// 加 encoder:新建 encoders/xxx.ts 导出 ContextEncoder,在此数组加一行。无需动 agent / llm / core。
|
|
7
|
+
import { passthroughEncoder } from './passthrough.js';
|
|
8
|
+
export const builtinEncoders = [
|
|
9
|
+
passthroughEncoder,
|
|
10
|
+
];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 兜底 encoder:identity,原样返回。
|
|
3
|
+
* - classifier 未命中任何 kind(返回 'passthrough')时用。
|
|
4
|
+
* - pipeline 总开关关闭(MOCODE_CONTEXT_OPTIMIZE=false)时,所有 kind 都走它 → 行为与改造前逐字节一致。
|
|
5
|
+
* - Phase 1 阶段 registry 只注册它 → 全链路零行为变化。
|
|
6
|
+
* - 任何 encoder 报错时,pipeline catch 后回落到它(传原 output)。
|
|
7
|
+
*
|
|
8
|
+
* 永不抛错:output 可能是任意字符串(含 ANSI / 多行 / 非法 UTF-8 片段),identity 直接返回,无解析风险。
|
|
9
|
+
*/
|
|
10
|
+
export const passthroughEncoder = {
|
|
11
|
+
kind: 'passthrough',
|
|
12
|
+
encode({ output }) {
|
|
13
|
+
return {
|
|
14
|
+
text: output,
|
|
15
|
+
meta: {
|
|
16
|
+
kind: 'passthrough',
|
|
17
|
+
originalLen: output.length,
|
|
18
|
+
encodedLen: output.length,
|
|
19
|
+
note: 'identity (no encoder registered)',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// context/ barrel:Context Optimization Pipeline。
|
|
2
|
+
//
|
|
3
|
+
// 单一入口 optimizeToolResult(agent/core.ts pushToolResult 调)接管"工具结果进 LLM 前"的表示。
|
|
4
|
+
// 不调 LLM、不碰 Tool Calling schema / executeTool / tool_call_id 配对 / TUI 渲染
|
|
5
|
+
// (叶子级:仅 stdlib + tools/constants + session/compact 的 capToolResultForHistory 兜底 + config 开关)。
|
|
6
|
+
//
|
|
7
|
+
// 见 CLAUDE.md「Context Optimization Pipeline」节。
|
|
8
|
+
export { optimizeToolResult } from './pipeline.js';
|
|
9
|
+
export { classify, knownToolKinds } from './classifier.js';
|
|
10
|
+
export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Context Optimization Pipeline 单一入口。
|
|
2
|
+
//
|
|
3
|
+
// 接管"工具结果进 LLM 前"的表示优化(C1 收口,agent/core.ts pushToolResult 调)。
|
|
4
|
+
// 流程:
|
|
5
|
+
// 1) 解析 argsRaw(失败返 null,encoder 据此降级)。
|
|
6
|
+
// 2) classify(name, output, args) → ContextKind。
|
|
7
|
+
// 3) getEncoder(kind) ?? passthrough → encode(保不变量压缩,纯函数)。
|
|
8
|
+
// 4) capToolResultForHistory(name, text) 作末尾长度裁剪兜底(保 head+标记+tail,与改造前一致)。
|
|
9
|
+
//
|
|
10
|
+
// 不抛错:encoder 报错 → catch 回落原 output + capToolResultForHistory(对齐「调度器永不抛错」)。
|
|
11
|
+
// 兜底零行为变化:未注册 encoder / pipeline 关闭 → passthrough identity → 末尾 cap 与改造前逐字节一致。
|
|
12
|
+
//
|
|
13
|
+
// 兼容:不改 Tool Calling JSON schema、不改 executeTool、不改 tool_call_id 配对、不改 TUI 渲染
|
|
14
|
+
// (hooks.onToolResult 用原始 output,本函数只管进 history 的 content)。
|
|
15
|
+
//
|
|
16
|
+
// 依赖方向:context → {tools/constants, session/compact 的 cap, config};叶子,不反向依赖 llm/agent/tools。
|
|
17
|
+
import { classify } from './classifier.js';
|
|
18
|
+
import { getEncoder, registerAll } from './registry.js';
|
|
19
|
+
import { builtinEncoders } from './encoders/index.js';
|
|
20
|
+
import { passthroughEncoder } from './encoders/passthrough.js';
|
|
21
|
+
import { capToolResultForHistory } from '../session/compact.js';
|
|
22
|
+
import { config } from '../config/index.js';
|
|
23
|
+
import { MAX_HISTORY_RESULT, MAX_SKILL_RESULT, MAX_MEMORY_RESULT, } from '../tools/constants.js';
|
|
24
|
+
let booted = false;
|
|
25
|
+
/** 懒注册内置 encoder(首次调用 optimizeToolResult 时触发,避免模块加载期循环 import)。 */
|
|
26
|
+
function boot() {
|
|
27
|
+
if (booted)
|
|
28
|
+
return;
|
|
29
|
+
registerAll(builtinEncoders);
|
|
30
|
+
booted = true;
|
|
31
|
+
}
|
|
32
|
+
/** 解析工具 arguments JSON;非法或空返 null(同 agent/core.ts parseArgs 语义,独立实现避免循环依赖)。 */
|
|
33
|
+
function tryParseArgs(raw) {
|
|
34
|
+
try {
|
|
35
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 按工具名取软目标 budget(字符)。对齐 capToolResultForHistory 的放宽规则:
|
|
43
|
+
* use_skill / memory_search 走放宽上限(指令 / 记忆正文须完整);其余走 MAX_HISTORY_RESULT。
|
|
44
|
+
* 仅作 encoder 软目标;最终裁剪仍由末尾 capToolResultForHistory 兜底,故两处常量偶有漂移不致命。
|
|
45
|
+
*/
|
|
46
|
+
function budgetFor(name) {
|
|
47
|
+
if (name === 'use_skill')
|
|
48
|
+
return MAX_SKILL_RESULT;
|
|
49
|
+
if (name === 'memory_search')
|
|
50
|
+
return MAX_MEMORY_RESULT;
|
|
51
|
+
return MAX_HISTORY_RESULT;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 优化工具结果为进 LLM 的 tool 消息 content。
|
|
55
|
+
*
|
|
56
|
+
* @param name 工具名
|
|
57
|
+
* @param output executeTool 的原始返回字符串
|
|
58
|
+
* @param argsRaw 工具 arguments 原始 JSON 字符串(tc.arguments,可空;未传则 args=null)
|
|
59
|
+
* @returns 进 history 的 content 字符串(永不抛错)
|
|
60
|
+
*/
|
|
61
|
+
export function optimizeToolResult(name, output, argsRaw) {
|
|
62
|
+
boot();
|
|
63
|
+
// 总开关关闭:完全走老路径,零行为变化(Phase 1 默认 true,但保留紧急回退开关)。
|
|
64
|
+
if (!config.contextOptimize) {
|
|
65
|
+
return capToolResultForHistory(name, output);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const args = argsRaw != null ? tryParseArgs(argsRaw) : null;
|
|
69
|
+
const kind = classify(name, output, args);
|
|
70
|
+
const enc = getEncoder(kind) ?? passthroughEncoder;
|
|
71
|
+
const { text } = enc.encode({
|
|
72
|
+
toolName: name,
|
|
73
|
+
output,
|
|
74
|
+
args,
|
|
75
|
+
budget: budgetFor(name),
|
|
76
|
+
});
|
|
77
|
+
// 末尾长度裁剪兜底(同改造前):encoder 已更短则 no-op;use_skill/memory_search 的放宽 cap 由此保留。
|
|
78
|
+
return capToolResultForHistory(name, text);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// encoder 报错(不应发生,纯函数):回落原 output + cap(永不抛错契约)。
|
|
82
|
+
return capToolResultForHistory(name, output);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// kind → encoder 注册表。单一事实源(仿 tools/registry.ts 的 tools[] 风格)。
|
|
2
|
+
//
|
|
3
|
+
// 内置 encoder 在 encoders/index.ts 的 builtinEncoders 数组声明,启动期 pipeline 首次调用时
|
|
4
|
+
// 经 registerAll 注册(懒注册,避免循环 import 在模块加载期触发)。
|
|
5
|
+
// MCP 工具(未来)可在并入 tools/registry.ts 时调 registerEncoder 注册私有 encoder(后注册覆盖默认)。
|
|
6
|
+
//
|
|
7
|
+
// 未注册的 kind → getEncoder 返 undefined → pipeline 回落 passthrough(identity),零行为变化。
|
|
8
|
+
const encoders = new Map();
|
|
9
|
+
let registered = false;
|
|
10
|
+
/** 注册一个 encoder(后注册覆盖先注册,允许 MCP 覆盖默认)。返回 encoder 自身供链式。 */
|
|
11
|
+
export function registerEncoder(enc) {
|
|
12
|
+
encoders.set(enc.kind, enc);
|
|
13
|
+
return enc;
|
|
14
|
+
}
|
|
15
|
+
/** 批量注册(启动期 pipeline 调一次)。幂等:重复调忽略。 */
|
|
16
|
+
export function registerAll(list) {
|
|
17
|
+
if (registered)
|
|
18
|
+
return;
|
|
19
|
+
for (const e of list)
|
|
20
|
+
registerEncoder(e);
|
|
21
|
+
registered = true;
|
|
22
|
+
}
|
|
23
|
+
/** 取某 kind 的 encoder;未注册返 undefined(pipeline 回落 passthrough)。 */
|
|
24
|
+
export function getEncoder(kind) {
|
|
25
|
+
return encoders.get(kind);
|
|
26
|
+
}
|
|
27
|
+
/** 调试:列出已注册 kind。 */
|
|
28
|
+
export function registeredKinds() {
|
|
29
|
+
return [...encoders.keys()];
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Context Optimization Pipeline 的类型契约。
|
|
2
|
+
//
|
|
3
|
+
// 设计原则(见 CLAUDE.md context/ 子系统):
|
|
4
|
+
// - Tool Calling 的 JSON schema 与 executeTool 不动;本层只接管"工具结果进 LLM 前"的表示。
|
|
5
|
+
// - 不设计统一 DSL,针对不同数据类型各做最优 encoder。
|
|
6
|
+
// - 所有 encoder 是纯函数(无 LLM 调用 / 无 IO / 无副作用),永不抛错(pipeline 层 try/catch,
|
|
7
|
+
// 失败回落原 output + capToolResultForHistory,对齐 tools/registry.ts「调度器永不抛错」契约)。
|
|
8
|
+
// - 兜底 encoder = passthrough(identity):未注册 encoder 时行为与改造前逐字节一致。
|
|
9
|
+
//
|
|
10
|
+
// 依赖方向:context 是叶子(仅 stdlib + tools/constants + session/compact 的 cap + config 开关),
|
|
11
|
+
// 不反向依赖 llm / agent / tools 业务,无环。
|
|
12
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,16 @@ process.on('unhandledRejection', (e) => {
|
|
|
36
36
|
*/
|
|
37
37
|
async function main() {
|
|
38
38
|
const args = process.argv.slice(2);
|
|
39
|
+
// --sandbox-root <path>:覆盖沙箱根(文件操作边界)。缺值或以 -- 开头报错退出。
|
|
40
|
+
const sr = args.indexOf('--sandbox-root');
|
|
41
|
+
let sandboxRootOverride;
|
|
42
|
+
if (sr !== -1) {
|
|
43
|
+
sandboxRootOverride = args[sr + 1];
|
|
44
|
+
if (!sandboxRootOverride || sandboxRootOverride.startsWith('--')) {
|
|
45
|
+
console.error('[cli] --sandbox-root 需要一个路径参数');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
39
49
|
// 首跑配置向导:写 ~/.mocode/config。独立模块,不触发 config 校验,故零配置也能跑。
|
|
40
50
|
if (args[0] === 'config') {
|
|
41
51
|
const { runConfigWizard } = await import('./commands/config.js');
|
|
@@ -66,12 +76,12 @@ async function main() {
|
|
|
66
76
|
}
|
|
67
77
|
const updateNotice = checkAndMaybeUpdate();
|
|
68
78
|
const { startRepl } = await import('./repl/index.js');
|
|
69
|
-
await startRepl(loaded.history, loaded.id, updateNotice);
|
|
79
|
+
await startRepl(loaded.history, loaded.id, updateNotice, sandboxRootOverride);
|
|
70
80
|
}
|
|
71
81
|
else {
|
|
72
82
|
const updateNotice = checkAndMaybeUpdate();
|
|
73
83
|
const { startRepl } = await import('./repl/index.js');
|
|
74
|
-
await startRepl(undefined, undefined, updateNotice);
|
|
84
|
+
await startRepl(undefined, undefined, updateNotice, sandboxRootOverride);
|
|
75
85
|
}
|
|
76
86
|
process.exit(0);
|
|
77
87
|
}
|
package/dist/llm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
2
|
import { config } from '../config/index.js';
|
|
3
3
|
import { tools } from '../tools/registry.js';
|
|
4
|
+
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
4
5
|
const client = new OpenAI({
|
|
5
6
|
baseURL: config.baseURL,
|
|
6
7
|
apiKey: config.apiKey,
|
|
@@ -16,18 +17,23 @@ export const chatTools = tools.map((t) => ({
|
|
|
16
17
|
},
|
|
17
18
|
}));
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
20
|
+
* plan 模式用的受限工具 schema:剔除写盘 / 命令 / 记忆写入类(PLAN_DISABLED_TOOLS),
|
|
21
|
+
* 模型在 plan 模式下只看得到只读工具 → 调不到会改文件的工具。runAgent 在 plan 模式传给 chat()。
|
|
22
|
+
*/
|
|
23
|
+
export const planChatTools = chatTools.filter((t) => !PLAN_DISABLED_TOOLS.has(t.function.name));
|
|
24
|
+
/**
|
|
25
|
+
* 流式调一次 LLM:增量回调文本,内部累加 tool_calls 片段。
|
|
22
26
|
* tool_calls 跨 chunk 按 index 累加(id / name / arguments 拼接)。
|
|
23
27
|
* include_usage 时末尾 chunk 携带 usage,先读再 continue(末尾 chunk 无 delta)。
|
|
24
28
|
*/
|
|
25
|
-
export async function chat(messages, handlers = {}, signal
|
|
29
|
+
export async function chat(messages, handlers = {}, signal,
|
|
30
|
+
/** 覆盖默认工具 schema;plan 模式传 planChatTools(只读子集),缺省=全量 chatTools。 */
|
|
31
|
+
toolsOverride) {
|
|
26
32
|
// signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
|
|
27
33
|
const stream = await client.chat.completions.create({
|
|
28
34
|
model: config.model,
|
|
29
35
|
messages,
|
|
30
|
-
tools: chatTools,
|
|
36
|
+
tools: toolsOverride ?? chatTools,
|
|
31
37
|
stream: true,
|
|
32
38
|
...(config.maxTokens ? { max_tokens: config.maxTokens } : {}),
|
|
33
39
|
...(config.includeUsage ? { stream_options: { include_usage: true } } : {}),
|
|
@@ -48,10 +54,6 @@ export async function chat(messages, handlers = {}, signal) {
|
|
|
48
54
|
const delta = chunk.choices[0]?.delta;
|
|
49
55
|
if (!delta)
|
|
50
56
|
continue; // 末尾 usage-only chunk 等无 delta
|
|
51
|
-
// 思考内容(非标准字段,SDK 类型无)
|
|
52
|
-
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
53
|
-
if (reasoning)
|
|
54
|
-
handlers.onThinking?.(reasoning);
|
|
55
57
|
if (delta.content) {
|
|
56
58
|
content += delta.content;
|
|
57
59
|
hasContent = true;
|
package/dist/memory/index.js
CHANGED
|
@@ -38,8 +38,8 @@ export function buildMemorySection() {
|
|
|
38
38
|
return [
|
|
39
39
|
'',
|
|
40
40
|
'',
|
|
41
|
-
'##
|
|
42
|
-
'
|
|
41
|
+
'## Project Memory (MOCODE.md)',
|
|
42
|
+
'The following is project memory (architecture / conventions / commands and other cross-session long-term facts). Act accordingly:',
|
|
43
43
|
mem,
|
|
44
44
|
].join('\n');
|
|
45
45
|
}
|
package/dist/memory/reflect.js
CHANGED
|
@@ -92,17 +92,17 @@ function buildMemorySample() {
|
|
|
92
92
|
.join('\n---\n');
|
|
93
93
|
}
|
|
94
94
|
const TYPES = 'decision | fact | pitfall | reference | feedback';
|
|
95
|
-
const REFLECT_SYS =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
①
|
|
100
|
-
②
|
|
101
|
-
③ updates/forgets
|
|
102
|
-
④ saves
|
|
103
|
-
⑤
|
|
104
|
-
⑥ forgets
|
|
105
|
-
const REFLECT_USER = (transcript, sample) => `##
|
|
95
|
+
const REFLECT_SYS = `You are mocode's memory reflector. Review the recent session and existing memories, producing **only** updates worth remembering long-term.
|
|
96
|
+
Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}]}
|
|
97
|
+
Empty arrays are valid (if nothing is worth saving, all three arrays are empty).
|
|
98
|
+
Rules:
|
|
99
|
+
① Only store non-obvious, cross-session-useful facts/decisions/pitfalls; do not store current bugs, temp files, undecided TODOs, or volatile items;
|
|
100
|
+
② Better to store less than to store trivially correct info (e.g. "keep it concise");
|
|
101
|
+
③ ids in updates/forgets must come from the "existing memories" list below; do not fabricate ids not listed there;
|
|
102
|
+
④ names in saves must be concise and not collide with existing ones; type ∈ {${TYPES}};
|
|
103
|
+
⑤ If an existing memory contradicts new facts or is outdated, update the old entry (modify summary/body) rather than creating a duplicate;
|
|
104
|
+
⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete).`;
|
|
105
|
+
const REFLECT_USER = (transcript, sample) => `## Recent session\n${transcript}\n\n## Existing memories\n${sample}\n\nProduce JSON:`;
|
|
106
106
|
function parsePlan(content) {
|
|
107
107
|
if (!content)
|
|
108
108
|
return null;
|
package/dist/memory/store.js
CHANGED
|
@@ -325,14 +325,14 @@ export function buildMemoryIndexSection() {
|
|
|
325
325
|
const shown = active.slice(0, MAX_INDEX_ENTRIES);
|
|
326
326
|
const lines = shown.map((e) => `- ${e.id}: ${e.name} — ${e.summary} (${e.type})`);
|
|
327
327
|
const tail = active.length > MAX_INDEX_ENTRIES
|
|
328
|
-
? `\n\n…(
|
|
328
|
+
? `\n\n…(${active.length} total, showing first ${MAX_INDEX_ENTRIES}; use memory_search <id or keyword> for more)`
|
|
329
329
|
: '';
|
|
330
330
|
return [
|
|
331
331
|
'',
|
|
332
332
|
'',
|
|
333
|
-
'##
|
|
334
|
-
'
|
|
335
|
-
+ 'memory_update
|
|
333
|
+
'## Memory Index (retrieve full body via memory_search)',
|
|
334
|
+
'The following are saved memory entries (title/summary only). Retrieve full body via memory_search (pass id or keyword); use memory_list to see all,'
|
|
335
|
+
+ ' memory_update to modify, memory_forget to archive. This list is a startup snapshot; entries added during the session are not listed here — use memory_list/memory_search to find them.',
|
|
336
336
|
...lines,
|
|
337
337
|
tail,
|
|
338
338
|
].join('\n');
|