mocode-ai 0.1.7 → 0.1.8

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 CHANGED
@@ -2,21 +2,30 @@
2
2
 
3
3
  # mocode
4
4
 
5
- 一个终端编码 agent:**LLM + tool-call 循环 + 流式输出 + 思考可见**。
5
+ 一个终端编码 agent:你给一个目标,它**自主完成**——不需要你逐步指挥。
6
6
 
7
- 接任意 OpenAI 兼容接口(GLM、DeepSeek、Qwen、本地 Ollama / vLLM 等),全屏 TUI 交互式 REPL。agent 自主探索代码、读写文件、执行命令、联网搜索,以「思考 → 调用工具 → 观察结果 → 再思考」的循环完成编程任务。
7
+ mocode 自己探索代码、读写改文件、执行命令、联网查资料,以「思考 → 调用工具 → 观察结果 → 再思考」的循环一步步把任务推进到完成。接任意 OpenAI 兼容接口(GLM、DeepSeek、Qwen、本地 Ollama / vLLM 等),全屏 TUI 交互,流式输出、思考过程可见。
8
+
9
+ ## 为什么用 mocode
10
+
11
+ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
12
+
13
+ - **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
14
+ - **只读工具并行执行** — 一轮里连续的只读操作(读文件、grep、glob、codegraph、联网搜索/抓取)自动并发跑,总耗时 ≈ 最慢一个,而不是逐个排队。写文件 / 改文件这类有副作用的操作仍串行,保快照顺序与数据安全。
15
+ - **子 agent 分而治之** — 复杂任务可派生独立子 agent:各自有自己的对话历史(不污染主线),可限定只读工具集和步数上限,并行探查多片代码 / 多个方向,最后只把摘要回灌主线。主线据此决定下一步。
16
+ - **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
17
+ - **上下文自动压缩** — 接近窗口上限时三层压缩(单条结果裁剪 → 旧工具结果原地微压缩 → 旧对话摘要),长会话也不爆窗口;`/context` 实时显示 token 用量,`/compact` 可手动压缩(能带焦点指令聚焦保留)。
18
+ - **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
19
+ - **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
20
+ - **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
8
21
 
9
22
  ## 特性
10
23
 
11
- - **流式输出**回复边生成边显示,模型支持 reasoning 时思考过程实时可见,思考段自动折叠(不占屏),`/think N` 按需展开
24
+ - **流式输出 + 思考可见** 回复边生成边显示;模型支持 reasoning 时思考过程实时可见,思考段自动折叠(不占屏),`/think N` 按需展开
12
25
  - **全屏 TUI** — 备用屏(alt screen)+ 固定底栏状态行 + 滚动回看(PgUp/PgDn),运行中可打字(typeahead),下一轮自动预填
13
- - **16 个内置工具** — 读 / 写 / 改文件、执行命令、glob 找路径、grep 搜内容、codegraph 代码索引、联网搜索、抓取网页、加载 skill、询问用户、5 个跨会话记忆工具(存/搜/列/改/遗忘)
14
26
  - **会话持久化** — 每轮自动落盘,`--resume` / `/resume` 续接历史会话
15
- - **轮次回滚**`/rollback` 菜单选轮次,删该轮及之后 + 逐个文件「保留/撤销」(快照恢复,不依赖 git)
16
- - **上下文工程** — 接近窗口上限时自动三层压缩,`/context` 实时显示 token 用量条,`/compact` 手动压缩(可带焦点)
17
- - **Skills 系统** — 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令
27
+ - **Skills 系统** 自动扫描 `~/.mocode/skills/` 等目录,description 注入系统提示,模型按需调 `use_skill` 加载完整指令(渐进式披露:先看简介,任务相关才加载正文)
18
28
  - **斜杠命令** — `/exit` `/clear` `/context` `/skills` `/compact` `/resume` `/think` `/rollback`,输入时下拉过滤
19
- - **Ctrl+C 中断** — 运行中随时中断当前 agent 轮次(不退进程)
20
29
 
21
30
  ## 安装
22
31
 
@@ -120,6 +129,8 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
120
129
  | `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本 |
121
130
  | `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
122
131
  | `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
132
+ | `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
133
+ | `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限);连续多个自动并行,只回摘要 |
123
134
  | `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
124
135
  | `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
125
136
  | `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
@@ -165,26 +176,6 @@ mocode 自动扫描以下目录的 skill(每个 skill 是 `<name>/SKILL.md`,带
165
176
 
166
177
  skill 的 `description` 注入系统提示(渐进式披露第①层),模型只在任务相关时调 `use_skill` 加载完整正文(第②层)。用 `/skills` 查看已发现的 skill。
167
178
 
168
- ## 结构
169
-
170
- ```
171
- src/
172
- ├── index.ts # 入口:装配 + 启动 REPL + --resume 续接
173
- ├── repl/index.ts # 全屏 TUI、斜杠命令、运行态交互、Ctrl+C 中断
174
- ├── agent/index.ts # tool-call 循环(调 LLM→执行工具→回灌→再调,≤200 步)
175
- ├── llm/index.ts # OpenAI 兼容客户端 + 工具格式转换 + chat() 流式
176
- ├── tools/ # types / constants / registry + builtins/(一工具一文件)
177
- │ └── builtins/ # read-file write-file edit-file run-command glob grep
178
- │ # codegraph web-search web-fetch use-skill ask-human memory-*
179
- ├── config/index.ts # 读配置、校验必填项、系统提示词
180
- ├── skills/ # discover(扫描)+ index(缓存/拼系统提示)
181
- ├── session/ # 落盘/续接 + compact(三层压缩)+ token 估算
182
- ├── memory/ # MOCODE.md 加载 + 跨会话记忆存储 + 后台反思
183
- ├── rollback/ # 轮次快照 + /rollback 菜单 + 文件撤销
184
- ├── ui/ # theme(颜色)render(横幅/摘要)layout(全屏布局)spinner prompt
185
- └── agents/ commands/ mcp/ permissions/ # 未来子系统(agents/mcp/permissions 为空骨架)
186
- ```
187
-
188
179
  ## 类型检查
189
180
 
190
181
  ```bash
@@ -9,7 +9,8 @@ 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, capToolResultForHistory, contextState, } from '../session/index.js';
12
+ import { maybeCompact, contextState } from '../session/index.js';
13
+ import { optimizeToolResult } from '../context/index.js';
13
14
  import { config } from '../config/index.js';
14
15
  import { jailResolve } from '../sandbox/index.js';
15
16
  /** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
@@ -67,12 +68,16 @@ function readDiffContext(tc, parsed) {
67
68
  }
68
69
  return { preWriteOld: null, editStartLine: 1 };
69
70
  }
70
- /** 回灌 tool 结果到 history(裁到单条上限);tool_call_id assistant.tool_calls 按序配对。 */
71
+ /** 回灌 tool 结果到 history:经 Context Optimization Pipeline 编码(tree/search/log/...)后裁到单条上限。
72
+ * tool_call_id 与 assistant.tool_calls 按序配对。未注册 encoder 时回落 capToolResultForHistory(零行为变化)。
73
+ * TUI 渲染(hooks.onToolResult)用原始 output,与此解耦——屏上看全量,LLM 看编码后紧凑版。 */
71
74
  function pushToolResult(history, tc, output) {
72
75
  history.push({
73
76
  role: 'tool',
74
77
  tool_call_id: tc.id,
75
- content: capToolResultForHistory(tc.name, output),
78
+ // optimizeToolResult:classifier 选 encoder → encode(保不变量压缩)→ capToolResultForHistory 兜底。
79
+ // tc.arguments 透传给 encoder(上下文感知编码,如 read_file 的 offset/limit)。永不抛错。
80
+ content: optimizeToolResult(tc.name, output, tc.arguments),
76
81
  });
77
82
  }
78
83
  /**
@@ -80,7 +85,7 @@ function pushToolResult(history, tc, output) {
80
85
  * 流式调 LLM(经 hooks.onText 实时渲染)→ 有 tool_calls 就分组执行并回灌
81
86
  * → 否则流式正文即最终回复。history 在调用间持久,由调用方持有。
82
87
  * 步前经 session/maybeCompact 自动压缩(接近窗口上限时三层压缩);
83
- * 工具结果进 history 前经 capToolResultForHistory 裁到单条上限。
88
+ * 工具结果进 history 前经 Context Optimization Pipeline(optimizeToolResult:类型化编码 + 长度裁剪)。
84
89
  *
85
90
  * 中断语义:signal 经 executeTool(name, args, signal) 串进工具;run_command/web_fetch 等 abort 即时杀
86
91
  * (树杀子进程 / 取消 fetch),循环顶 if(signal.aborted) 兜底还原。不会留下未配对的 tool_call_id。
@@ -122,6 +122,7 @@ export const config = {
122
122
  compactThreshold: Number(process.env.COMPACT_THRESHOLD) || 0.85,
123
123
  includeUsage: process.env.LLM_STREAM_USAGE !== 'false',
124
124
  autoCompact: process.env.AUTO_COMPACT !== 'false',
125
+ contextOptimize: process.env.MOCODE_CONTEXT_OPTIMIZE !== 'false',
125
126
  autoReflect: process.env.AUTO_REFLECT !== 'false',
126
127
  reflectEveryN: Number(process.env.REFLECT_EVERY_N) || 5,
127
128
  maxSteps: Number(process.env.MAX_STEPS) || 200,
@@ -0,0 +1,40 @@
1
+ // encoder 共享小工具(非 encoder,纯函数)。供 graph/doc/summary 等复用。
2
+ // log/code 各有专属逻辑(log 用重复行折叠;code 用前缀感知空行检测),不在此。
3
+ /** 去 ANSI CSI 序列(颜色 / 光标 / 清屏等)。语义无损:颜色码不含信息。 */
4
+ export function stripAnsi(s) {
5
+ return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
6
+ }
7
+ /**
8
+ * 折叠连续空行(只含空白字符的行)≥ threshold → 单个空行。
9
+ * 用于 prose / source dump(web_fetch / use_skill / task / codegraph):HTML→文本与 markdown 常留多余空行,
10
+ * 多空行与单空行语义等价,折叠无损。默认 threshold=3(只动真正过量的空行,常见 ≤2 空行不动)。
11
+ *
12
+ * 注意:用 `.trim() === ''` 判空——故 read_file 的 ` 2\t`(行号前缀 + tab,trim 后剩 `2`)不会被
13
+ * 视作空行。read_file 的空行折叠见 code encoder(前缀感知)。
14
+ */
15
+ export function collapseBlankRuns(text, threshold = 3) {
16
+ const lines = text.split('\n');
17
+ const out = [];
18
+ let i = 0;
19
+ while (i < lines.length) {
20
+ if (lines[i].trim() === '') {
21
+ let j = i;
22
+ while (j < lines.length && lines[j].trim() === '')
23
+ j++;
24
+ const run = j - i;
25
+ if (run >= threshold) {
26
+ out.push(''); // 单个空行
27
+ }
28
+ else {
29
+ for (let k = 0; k < run; k++)
30
+ out.push(lines[i]);
31
+ }
32
+ i = j;
33
+ }
34
+ else {
35
+ out.push(lines[i]);
36
+ i++;
37
+ }
38
+ }
39
+ return out.join('\n');
40
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Code Encoder(read_file):折叠连续空行(≥3 → 1,保留首空行行号前缀)。
3
+ *
4
+ * 输入:read_file 返回的 ` N\t<content>` 行(6 宽右对齐行号 + tab + 内容),可能带尾部
5
+ * `... (N 行未显示,共 M 行)`。
6
+ * 输出:连续 ≥3 个空行(行号前缀 + 空 content)折叠为 1 个(保留首行前缀,丢后续空行);其余逐字保留。
7
+ *
8
+ * 不变量(离线脚本断言):
9
+ * - 所有非空 content 行逐字保留(前缀 + 内容不变)→ edit_file 的 old_string 按内容匹配仍可用。
10
+ * - content 行的行号前缀不变 → LLM 看到的行号与文件一致。
11
+ * - 仅空行(前缀 + tab + 空 content)被折叠,且仅 ≥3 连续时;≤2 空行原样(常见,不动)。
12
+ * - 尾部标记 `... (...)` 不匹配行号前缀,原样保留。
13
+ *
14
+ * ⚠️ 残余风险(可接受、可恢复):若 LLM 编辑一个含 ≥3 连续空行的区域,它看到的空行数比文件实际少,
15
+ * old_string 的空行数可能不匹配 → edit_file 返"未找到" → LLM 重读重试(系统提示已要求编辑前 read_file)。
16
+ * 真实代码极少 ≥3 连续空行(linter 通常强制 ≤2),故实际几乎不触发。出问题可设
17
+ * MOCODE_CONTEXT_OPTIMIZE=false 全局回退,或调高阈值。
18
+ *
19
+ * 不做长度裁剪(cap 兜底);不动行号前缀;不删 content 行;不改内容(含尾随空白)。
20
+ * 空行判定用前缀感知(前缀 + tab + 空 content),不能用 trim——read_file 空行 ` 2\t` trim 后剩 `2`。
21
+ */
22
+ const LINE_RE = /^(\s*\d+)\t(.*)$/;
23
+ function isBlankCodeLine(l) {
24
+ const m = LINE_RE.exec(l);
25
+ return m ? m[2] === '' : false;
26
+ }
27
+ export const codeEncoder = {
28
+ kind: 'code',
29
+ encode({ output }) {
30
+ const lines = output.split('\n');
31
+ const out = [];
32
+ let i = 0;
33
+ let collapsedRuns = 0;
34
+ while (i < lines.length) {
35
+ if (isBlankCodeLine(lines[i])) {
36
+ let j = i;
37
+ while (j < lines.length && isBlankCodeLine(lines[j]))
38
+ j++;
39
+ const run = j - i;
40
+ if (run >= 3) {
41
+ out.push(lines[i]); // 保留首空行(含其行号前缀)
42
+ collapsedRuns++;
43
+ }
44
+ else {
45
+ for (let k = 0; k < run; k++)
46
+ out.push(lines[i]);
47
+ }
48
+ i = j;
49
+ }
50
+ else {
51
+ out.push(lines[i]);
52
+ i++;
53
+ }
54
+ }
55
+ if (collapsedRuns === 0) {
56
+ return {
57
+ text: output,
58
+ meta: {
59
+ kind: 'code',
60
+ originalLen: output.length,
61
+ encodedLen: output.length,
62
+ note: 'no ≥3 blank runs → passthrough',
63
+ },
64
+ };
65
+ }
66
+ const text = out.join('\n');
67
+ return {
68
+ text,
69
+ meta: {
70
+ kind: 'code',
71
+ originalLen: output.length,
72
+ encodedLen: text.length,
73
+ note: `collapsed ${collapsedRuns} blank runs (≥3 → 1)`,
74
+ },
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,28 @@
1
+ import { collapseBlankRuns } from './_util.js';
2
+ /**
3
+ * Doc Encoder(web_fetch / use_skill):折叠连续空行(≥3 → 1)。
4
+ *
5
+ * 输入:
6
+ * - web_fetch 的 `URL (HTTP ...)\n\n<body>`(HTML→文本,常留多余空行)。
7
+ * - use_skill 的 `# Skill: name\n\n<body>`(SKILL.md 正文,frontmatter 已剥离)。
8
+ * 输出:空行折叠;正文 / 指令逐字保留。
9
+ *
10
+ * 不变量:URL 前缀行 / Skill 标题行保留;正文文本逐字保留(仅折叠空行——markdown / HTML 多空行与
11
+ * 单空行语义等价,无损)。use_skill 走放宽 cap(MAX_SKILL_RESULT)保指令完整——本 encoder 在 cap 前先
12
+ * 折叠空行,减少被 cap 截断的风险。
13
+ */
14
+ export const docEncoder = {
15
+ kind: 'doc',
16
+ encode({ output }) {
17
+ const text = collapseBlankRuns(output, 3);
18
+ return {
19
+ text,
20
+ meta: {
21
+ kind: 'doc',
22
+ originalLen: output.length,
23
+ encodedLen: text.length,
24
+ note: text !== output ? 'blank runs collapsed' : 'no change',
25
+ },
26
+ };
27
+ },
28
+ };
@@ -0,0 +1,31 @@
1
+ import { stripAnsi, collapseBlankRuns } from './_util.js';
2
+ /**
3
+ * Graph Encoder(codegraph):去 ANSI + 折叠连续空行(≥3 → 1)。
4
+ *
5
+ * 输入:codegraph CLI 返回的 `[退出码 N]\n<源码 + 调用路径>`(可能含 ANSI 颜色、多余空行)。
6
+ * 输出:去 ANSI + 空行折叠;结构(调用路径块、源码段)不动。
7
+ *
8
+ * 不变量:退出码行保留;源码与调用路径文本逐字保留(仅去颜色码 + 折叠空行);不删内容行。
9
+ * 保守:不重构调用路径 / 不去重源码(codegraph CLI 输出格式未稳定,需先采样真实输出定不变量,留后续)。
10
+ */
11
+ export const graphEncoder = {
12
+ kind: 'graph',
13
+ encode({ output }) {
14
+ const stripped = stripAnsi(output);
15
+ const text = collapseBlankRuns(stripped, 3);
16
+ const hadAnsi = /\x1b/.test(output);
17
+ const hadBlanks = text !== stripped;
18
+ const note = [hadAnsi ? 'ANSI stripped' : '', hadBlanks ? 'blank runs collapsed' : '']
19
+ .filter(Boolean)
20
+ .join(', ') || 'no change';
21
+ return {
22
+ text,
23
+ meta: {
24
+ kind: 'graph',
25
+ originalLen: output.length,
26
+ encodedLen: text.length,
27
+ note,
28
+ },
29
+ };
30
+ },
31
+ };
@@ -1,10 +1,32 @@
1
1
  // 内置 encoder 清单。启动期 pipeline 首次调用时经 registerAll 注册到 registry。
2
2
  //
3
- // Phase 1:仅 passthrough(identity)→ 全链路零行为变化(所有 kind 都回落到它)。
4
- // Phase 2 起逐步加入:tree / search / log / code / table / memory(见各 encoder 文件)。
3
+ // Phase 1:passthrough(identity 兜底)→ 全链路零行为变化。
4
+ // Phase 2:tree / search / log / table / memory(高价值低风险)。
5
+ // Phase 2.5:code / graph / doc / summary(覆盖剩余有 encoder 的 kind)。
6
+ // - code(read_file):仅折叠 ≥3 连续空行,行号保真(edit_file 依赖)。
7
+ // - graph(codegraph)/ doc(web_fetch, use_skill)/ summary(task):去 ANSI + 折叠空行,保守不重构结构。
8
+ // - status(edit/write/ask_human/switch_mode/mem 增删改)无 encoder:本就是一行,无需编码。
5
9
  //
6
10
  // 加 encoder:新建 encoders/xxx.ts 导出 ContextEncoder,在此数组加一行。无需动 agent / llm / core。
7
11
  import { passthroughEncoder } from './passthrough.js';
12
+ import { treeEncoder } from './tree.js';
13
+ import { searchEncoder } from './search.js';
14
+ import { logEncoder } from './log.js';
15
+ import { tableEncoder } from './table.js';
16
+ import { memoryEncoder } from './memory.js';
17
+ import { codeEncoder } from './code.js';
18
+ import { graphEncoder } from './graph.js';
19
+ import { docEncoder } from './doc.js';
20
+ import { summaryEncoder } from './summary.js';
8
21
  export const builtinEncoders = [
9
22
  passthroughEncoder,
23
+ treeEncoder,
24
+ searchEncoder,
25
+ logEncoder,
26
+ tableEncoder,
27
+ memoryEncoder,
28
+ codeEncoder,
29
+ graphEncoder,
30
+ docEncoder,
31
+ summaryEncoder,
10
32
  ];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Log Encoder(run_command):去 ANSI 颜色码 + 折叠连续重复行(≥3 → 单行 + [×N])。
3
+ *
4
+ * 输入:run_command 返回的 `[退出码 N]\n<合并 stdout+stderr>`,可能含 ANSI(tsc --pretty / 测试框架)、
5
+ * 重复行(构建 / 编译日志)、尾部 `...(输出已截断)`、`[已中断]` / `[超时,已终止]` 前缀。
6
+ * 输出:去 ANSI CSI 序列 + 连续重复行折叠;行顺序不变(退出码头恒在首、错误行与尾部原位保留)。
7
+ *
8
+ * 不变量(离线脚本断言):退出码行 `[退出码 N]` / `[已中断]` / `[超时,已终止]` 保留;
9
+ * 重复行以 `[×N]` 标注计数(语义不丢——LLM 仍知该行重复 N 次);ANSI 去除语义无损(颜色码不含信息)。
10
+ * 不做长度裁剪(由 pipeline 末尾 capToolResultForHistory 兜底 head+标记+tail,与改造前一致)。
11
+ * run ≤2 原样保留(常见输出不必标注,避免噪音);run ≥3 才折叠(真正的大规模重复才省)。
12
+ */
13
+ const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
14
+ export const logEncoder = {
15
+ kind: 'log',
16
+ encode({ output }) {
17
+ // 去 ANSI CSI 序列(颜色 / 光标 / 清屏等),保留所有可见文本。
18
+ const stripped = output.replace(ANSI_RE, '');
19
+ const lines = stripped.split('\n');
20
+ // 折叠连续重复行:run ≥3 → 单行 + [×N];run ≤2 原样。
21
+ const out = [];
22
+ let i = 0;
23
+ let collapsedRuns = 0;
24
+ while (i < lines.length) {
25
+ let j = i;
26
+ while (j < lines.length && lines[j] === lines[i])
27
+ j++;
28
+ const run = j - i;
29
+ if (run >= 3) {
30
+ out.push(`${lines[i]} [×${run}]`);
31
+ collapsedRuns++;
32
+ }
33
+ else {
34
+ for (let k = 0; k < run; k++)
35
+ out.push(lines[i]);
36
+ }
37
+ i = j;
38
+ }
39
+ const text = out.join('\n');
40
+ return {
41
+ text,
42
+ meta: {
43
+ kind: 'log',
44
+ originalLen: output.length,
45
+ encodedLen: text.length,
46
+ note: collapsedRuns > 0
47
+ ? `ANSI stripped, ${collapsedRuns} dup runs collapsed`
48
+ : 'ANSI stripped',
49
+ },
50
+ };
51
+ },
52
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Memory Encoder(memory_search):去掉 `recalled N` 召回计数噪音(遗忘系统内部用,非 LLM 推理所需)。
3
+ *
4
+ * 输入:memory_search 返回的条目卡,每张:`# [id] name (type, recalled N)\nsummary: ...\n\nbody`,
5
+ * 多张以 `\n\n---\n\n` 分隔。
6
+ * 输出:头部 `# [id] name (type)`(丢 `, recalled N`);summary/body 原样。
7
+ *
8
+ * 不变量(离线脚本断言):id/name/type/summary/body 全保留;仅 `recalled N` 被省略。
9
+ * 正则锚定整行 `# [id] ... (type, recalled N)`,group1 贪婪捕获到 `(\w+` 为止,name 含 ` (` 也不影响。
10
+ * 不匹配的行原样返回(防误伤 body 正文里的 `#` 标题等)。
11
+ */
12
+ const HEADER_RE = /^(# \[[^\]]+\] .+ \(\w+), recalled \d+\)$/;
13
+ export const memoryEncoder = {
14
+ kind: 'memory',
15
+ encode({ output }) {
16
+ if (output.startsWith('(无匹配记忆')) {
17
+ return {
18
+ text: output,
19
+ meta: {
20
+ kind: 'memory',
21
+ originalLen: output.length,
22
+ encodedLen: output.length,
23
+ note: 'no matches → passthrough',
24
+ },
25
+ };
26
+ }
27
+ const lines = output.split('\n');
28
+ const out = [];
29
+ let changed = 0;
30
+ for (const l of lines) {
31
+ const m = HEADER_RE.exec(l);
32
+ if (m) {
33
+ // m[1] = "# [id] name (type";补 ")" 收尾,丢 ", recalled N"
34
+ out.push(`${m[1]})`);
35
+ changed++;
36
+ }
37
+ else {
38
+ out.push(l);
39
+ }
40
+ }
41
+ if (changed === 0) {
42
+ return {
43
+ text: output,
44
+ meta: {
45
+ kind: 'memory',
46
+ originalLen: output.length,
47
+ encodedLen: output.length,
48
+ note: 'no recalled headers → passthrough',
49
+ },
50
+ };
51
+ }
52
+ const text = out.join('\n');
53
+ return {
54
+ text,
55
+ meta: {
56
+ kind: 'memory',
57
+ originalLen: output.length,
58
+ encodedLen: text.length,
59
+ note: `dropped recalled count from ${changed} entries`,
60
+ },
61
+ };
62
+ },
63
+ };
@@ -0,0 +1,69 @@
1
+ const GREP_RE = /^(.*?):(\d+):(.*)$/;
2
+ export const searchEncoder = {
3
+ kind: 'search',
4
+ encode({ output }) {
5
+ const lines = output.split('\n');
6
+ const matches = [];
7
+ const tail = [];
8
+ let inTail = false;
9
+ for (const l of lines) {
10
+ if (!l)
11
+ continue;
12
+ if (inTail) {
13
+ tail.push(l);
14
+ continue;
15
+ }
16
+ const m = GREP_RE.exec(l);
17
+ if (m) {
18
+ matches.push({ file: m[1], line: m[2], content: m[3] });
19
+ }
20
+ else {
21
+ // 首个非 grep 行起视作 tail(grep 上限标记 / 无匹配串 / web_search 非 grep 结构)
22
+ inTail = true;
23
+ tail.push(l);
24
+ }
25
+ }
26
+ if (matches.length === 0) {
27
+ // 非 grep 格式(web_search 等)→ 不动其已格式化结构
28
+ return {
29
+ text: output,
30
+ meta: {
31
+ kind: 'search',
32
+ originalLen: output.length,
33
+ encodedLen: output.length,
34
+ note: 'no file:line matches → passthrough',
35
+ },
36
+ };
37
+ }
38
+ const groups = new Map();
39
+ const order = [];
40
+ for (const m of matches) {
41
+ if (!groups.has(m.file)) {
42
+ groups.set(m.file, []);
43
+ order.push(m.file);
44
+ }
45
+ groups.get(m.file).push({ line: m.line, content: m.content });
46
+ }
47
+ const out = [
48
+ `# ${matches.length} matches · ${order.length} files · search-encoded`,
49
+ ];
50
+ for (const file of order) {
51
+ out.push(`${file}:`);
52
+ for (const { line, content } of groups.get(file)) {
53
+ out.push(` ${line}:${content}`);
54
+ }
55
+ }
56
+ if (tail.length)
57
+ out.push(...tail);
58
+ const text = out.join('\n');
59
+ return {
60
+ text,
61
+ meta: {
62
+ kind: 'search',
63
+ originalLen: output.length,
64
+ encodedLen: text.length,
65
+ note: `${matches.length} matches / ${order.length} files`,
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,26 @@
1
+ import { collapseBlankRuns } from './_util.js';
2
+ /**
3
+ * Summary Encoder(task):折叠连续空行(≥3 → 1)。
4
+ *
5
+ * 输入:task 工具返回的子 agent 最终摘要文本(已是摘要,可能含多余空行),可能带截断尾标
6
+ * `…(子 agent 摘要已截断 N 字符)`。
7
+ * 输出:空行折叠;摘要文本逐字保留。
8
+ *
9
+ * 不变量:摘要事实文本逐字保留(仅折叠空行);截断尾标保留。子 agent 摘要已是高密度文本,只做轻量去冗余,
10
+ * 不再做进一步压缩(避免丢事实)。
11
+ */
12
+ export const summaryEncoder = {
13
+ kind: 'summary',
14
+ encode({ output }) {
15
+ const text = collapseBlankRuns(output, 3);
16
+ return {
17
+ text,
18
+ meta: {
19
+ kind: 'summary',
20
+ originalLen: output.length,
21
+ encodedLen: text.length,
22
+ note: text !== output ? 'blank runs collapsed' : 'no change',
23
+ },
24
+ };
25
+ },
26
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Table Encoder(memory_list):去掉默认 `active` 状态标注(冗余信息)。
3
+ *
4
+ * 输入:memory_list 返回的 `- id: name — summary (type, status)` 行(每行一条)。
5
+ * 输出:status 为默认 `active` 时省略 `, active`(→ `(type)`);非默认(archived/superseded)保留。
6
+ * 顶部加 `# N entries · table-encoded` 计数头。
7
+ *
8
+ * 不变量(离线脚本断言):id/name/summary/type 全保留;仅 `active` 状态被省略(默认值,无信息损失)。
9
+ * 正则锚定整行 + 要求 `id: ... — ... (type, active)` 形,不匹配的行原样返回(防误伤 MOCODE.md 等正文)。
10
+ * group1 贪婪捕获到 `(\w+` 为止,故 name/summary 内含 ` — ` 或 ` (` 也不影响(仅丢尾部 `, active)`)。
11
+ */
12
+ const ACTIVE_LINE_RE = /^(- [^:]+: .+ — .+ \(\w+), active\)$/;
13
+ export const tableEncoder = {
14
+ kind: 'table',
15
+ encode({ output }) {
16
+ if (output === '(无记忆条目)') {
17
+ return {
18
+ text: output,
19
+ meta: {
20
+ kind: 'table',
21
+ originalLen: output.length,
22
+ encodedLen: output.length,
23
+ note: 'empty → passthrough',
24
+ },
25
+ };
26
+ }
27
+ const lines = output.split('\n');
28
+ const out = [];
29
+ let changed = 0;
30
+ for (const l of lines) {
31
+ const m = ACTIVE_LINE_RE.exec(l);
32
+ if (m) {
33
+ // m[1] = "- id: name — summary (type";补 ")" 收尾,丢 ", active"
34
+ out.push(`${m[1]})`);
35
+ changed++;
36
+ }
37
+ else {
38
+ out.push(l);
39
+ }
40
+ }
41
+ if (changed === 0) {
42
+ return {
43
+ text: output,
44
+ meta: {
45
+ kind: 'table',
46
+ originalLen: output.length,
47
+ encodedLen: output.length,
48
+ note: 'no active-status lines → passthrough',
49
+ },
50
+ };
51
+ }
52
+ const count = lines.filter((l) => l.startsWith('- ')).length;
53
+ const text = `# ${count} entries · table-encoded (default "active" status omitted)\n${out.join('\n')}`;
54
+ return {
55
+ text,
56
+ meta: {
57
+ kind: 'table',
58
+ originalLen: output.length,
59
+ encodedLen: text.length,
60
+ note: `omitted active from ${changed}/${count} entries`,
61
+ },
62
+ };
63
+ },
64
+ };