mocode-ai 0.5.4 → 0.5.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.
@@ -26,6 +26,7 @@
26
26
  // - mutation 分支额外调 pushMutation 通知(也走 observeMutation 同语义)。
27
27
  //
28
28
  // 开关:`config.contextLifecycle`(默认 true;MOCODE_LIFECYCLE=false 回退)。
29
+ import { extractPath, lastUserIndex, toText, toolNameOf } from './utils.js';
29
30
  /** 观察类工具(永远只到 REFERENCED,不参与自动 STUB)。 */
30
31
  const OBSERVER_TOOLS = new Set([
31
32
  'grep',
@@ -45,61 +46,6 @@ const STUB_PREFIX_NO_CONSUMER = '⌦[无消费者:观察结果已无引用价值
45
46
  * ≥ 这个值且仍为 LIVE 且非观察类 → 视为 OBSOLETE → STUB。
46
47
  * 默认 2:等价于「跨过两个消费者 push 仍无人引用」= 跨过整轮最末尾的工具调用。 */
47
48
  const DEFAULT_AGE_THRESHOLD = 2;
48
- /** 复用 drop.ts 的取 path 思路,但本层要支持更多字段名(read_file/edit_file/write_file 都有 path;
49
- * edit_file 还可能有 file_path,但这里只看 path,保持单一)。 */
50
- function extractPath(argsRaw) {
51
- if (!argsRaw)
52
- return null;
53
- let parsed;
54
- try {
55
- parsed = JSON.parse(argsRaw);
56
- }
57
- catch {
58
- return null;
59
- }
60
- if (!parsed || typeof parsed !== 'object')
61
- return null;
62
- const p = parsed.path;
63
- return typeof p === 'string' && p ? p : null;
64
- }
65
- /** 从 tool 消息往前找匹配的 assistant.tool_calls 拿 tool 名。找不到返 null(保守跳过)。 */
66
- function toolNameOf(history, idx) {
67
- const tcId = history[idx].tool_call_id;
68
- if (!tcId)
69
- return null;
70
- for (let j = idx - 1; j >= 1; j--) {
71
- const m = history[j];
72
- if (m.role !== 'assistant')
73
- continue;
74
- const tcs = m.tool_calls;
75
- if (!tcs)
76
- continue;
77
- const hit = tcs.find((tc) => tc?.id === tcId);
78
- if (hit)
79
- return hit.function?.name ?? null;
80
- }
81
- return null;
82
- }
83
- /** 找最后一个 user 消息索引;无 user 返 -1。 */
84
- function lastUserIndex(history) {
85
- for (let i = history.length - 1; i >= 1; i--) {
86
- if (history[i].role === 'user')
87
- return i;
88
- }
89
- return -1;
90
- }
91
- function toText(content) {
92
- if (content == null)
93
- return '';
94
- if (typeof content === 'string')
95
- return content;
96
- try {
97
- return JSON.stringify(content);
98
- }
99
- catch {
100
- return String(content);
101
- }
102
- }
103
49
  /** 从 tool 结果的 content 中提取「生产者命中过的 path 列表」。
104
50
  * - read_file:没有 path 列表(本身就是单 path 消费者,无需再追生产者)。
105
51
  * - grep:content 是 `file:line: ...` 行,提取每行的 file 段(只保留绝对路径形态或与 pattern 匹配的)。
@@ -22,71 +22,10 @@
22
22
  // - TUI 渲染(hooks.onToolResult)用原始 output,与本层解耦——屏上看全量,LLM 看裁剪后版。
23
23
  //
24
24
  // 零行为变化兜底:开关 `config.contextRelprune` 关闭时,pipeline 路径完全不调本模块。
25
+ import { extractPath, lastUserIndex, toText, toolNameOf } from './utils.js';
25
26
  /** stub 标记前缀(供幂等判定)。drop_context 用的是「⌦[已剔除:与当前任务无关]」,
26
27
  * 本层用「⌦[已过时:同 path 已有新 read / 已被 mutation 覆写]」,区分两类剔除来源。 */
27
28
  const STUB_PREFIX = '⌦[已过时:同 path 已有新 read / 已被 mutation 覆写]';
28
- /** 解析工具 arguments(只关心 path);非法返 null。 */
29
- function extractPath(argsRaw) {
30
- if (!argsRaw)
31
- return null;
32
- let parsed;
33
- try {
34
- parsed = JSON.parse(argsRaw);
35
- }
36
- catch {
37
- return null;
38
- }
39
- if (!parsed || typeof parsed !== 'object')
40
- return null;
41
- const p = parsed.path;
42
- if (typeof p !== 'string' || !p)
43
- return null;
44
- return p;
45
- }
46
- /** 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
47
- * 复用 drop.ts 的思路:user 及其之后的 tool 结果视为当前轮保护区。
48
- * 在本层里,read_file tool 消息若落在 user 之后,本轮还在用,不能 stub。
49
- * 注:history[0] 是 system,user 不会落在 0;若 user 就在末尾(即 0 user 之后),
50
- * protectedFrom=0 时整段历史都不可 stub(实际不会发生:pushToolResult 必在 user 之后)。 */
51
- function lastUserIndex(history) {
52
- for (let i = history.length - 1; i >= 1; i--) {
53
- if (history[i].role === 'user')
54
- return i;
55
- }
56
- return -1;
57
- }
58
- /** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。
59
- * 返回 null 表示找不到(孤儿 tool,极少见),本层保守不动。 */
60
- function toolNameOf(history, idx) {
61
- const tcId = history[idx].tool_call_id;
62
- if (!tcId)
63
- return null;
64
- for (let j = idx - 1; j >= 1; j--) {
65
- const m = history[j];
66
- if (m.role !== 'assistant')
67
- continue;
68
- const tcs = m.tool_calls;
69
- if (!tcs)
70
- continue;
71
- const hit = tcs.find((tc) => tc?.id === tcId);
72
- if (hit)
73
- return hit.function?.name ?? null;
74
- }
75
- return null;
76
- }
77
- /** 把 content 拍平成字符串(对齐 drop.ts)。 */
78
- function toText(content) {
79
- if (content == null)
80
- return '';
81
- if (typeof content === 'string')
82
- return content;
83
- try {
84
- return JSON.stringify(content);
85
- }
86
- catch {
87
- return String(content);
88
- }
89
- }
90
29
  /**
91
30
  * 维护「path → 该 path 所有 read_file tool 消息的 history index」映射。
92
31
  * - observePush:把刚 push 的 read_file tool 消息登记,并把同 path 的"更早" read 全部 stub。
@@ -0,0 +1,68 @@
1
+ // 跨上下文模块共享的小工具函数。
2
+ //
3
+ // 目的:消除 `relevance.ts` / `lifecycle.ts` / `budget.ts` / `compact.ts` / `session/drop.ts`
4
+ // 之间的 copy-paste。这些函数逻辑完全一致,统一维护于此,避免行为漂移。
5
+ //
6
+ // 不变量:
7
+ // - 永不抛错(对齐「调度器永不抛错」契约)。
8
+ // - 仅依赖 `ChatMessage` 的最小形状,不反向 import agent / session / tools。
9
+ /** 把消息 content 拍平成字符串(OpenAI 可能 string / null / 多模态数组)。
10
+ * 用于估算 token、内容匹配、stub 拼接等场景。 */
11
+ export function toText(content) {
12
+ if (content == null)
13
+ return '';
14
+ if (typeof content === 'string')
15
+ return content;
16
+ try {
17
+ return JSON.stringify(content);
18
+ }
19
+ catch {
20
+ return String(content);
21
+ }
22
+ }
23
+ /** 从 history 末尾向前找最后一个 user 消息的索引;无 user 返 -1。
24
+ * 用于划定「当前轮保护区」——该 user 及其之后的消息(agent 本轮还在用)不动。
25
+ * 注:history[0] 是 system,故下界为 1。 */
26
+ export function lastUserIndex(history) {
27
+ for (let i = history.length - 1; i >= 1; i--) {
28
+ if (history[i].role === 'user')
29
+ return i;
30
+ }
31
+ return -1;
32
+ }
33
+ /** 取 tool 消息对应的工具名(从紧邻的前导 assistant.tool_calls 按 tool_call_id 配对找)。
34
+ * 返回 null 表示找不到(孤儿 tool 消息,极少见),调用方保守跳过。 */
35
+ export function toolNameOf(history, idx) {
36
+ const tcId = history[idx].tool_call_id;
37
+ if (!tcId)
38
+ return null;
39
+ for (let j = idx - 1; j >= 1; j--) {
40
+ const m = history[j];
41
+ if (m.role !== 'assistant')
42
+ continue;
43
+ const tcs = m.tool_calls;
44
+ if (!tcs)
45
+ continue;
46
+ const hit = tcs.find((tc) => tc?.id === tcId);
47
+ if (hit)
48
+ return hit.function?.name ?? null;
49
+ }
50
+ return null;
51
+ }
52
+ /** 解析工具 arguments JSON,只取 `path` 字段;非法 / 无 path 返 null。
53
+ * 用于 read_file / edit_file / write_file 等以 path 为关键字的工具,供跨消息关联使用。 */
54
+ export function extractPath(argsRaw) {
55
+ if (!argsRaw)
56
+ return null;
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(argsRaw);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ if (!parsed || typeof parsed !== 'object')
65
+ return null;
66
+ const p = parsed.path;
67
+ return typeof p === 'string' && p ? p : null;
68
+ }
@@ -5,63 +5,63 @@ import { spawnAgent } from '../agent/spawn.js';
5
5
  /**
6
6
  * 子 agent 的系统提示后缀:角色与输出格式约束
7
7
  */
8
- const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise "Project Skill" document.
9
-
10
- ## ⚠️ Complementary to Snapshot — CRITICAL
11
- The following information is ALREADY provided by Project Snapshot — **do NOT repeat**:
12
- - ✗ Project one-liner description (快照已有)
13
- - ✗ Module names and file locations (快照 srcTree 已有)
14
- - ✗ Tech stack names and versions (快照 techStack 已有)
15
- - ✗ Build/test commands (快照 keyCommands 已有)
16
- - ✗ File lists and directory structure (快照 srcTree 已有)
17
-
18
- **Your job is to capture INSIGHTS only:**
19
- - **WHY** — design decisions, trade-offs, reasons for choosing X over Y
20
- - **HOW** — module behaviors, data flows, call chains, non-obvious interactions
21
- - **GOTCHAS** — pitfalls, edge cases, non-intuitive behaviors
22
- - **CONVENTIONS** — naming patterns, code style, unwritten rules
23
-
24
- ## Exploration Strategy
25
- 1. Use \`codegraph\` to trace call chains and understand module interactions
26
- 2. Read core modules to understand **behaviors** (not just names)
27
- 3. Look for complex logic, error handling patterns, unusual configs
28
- 4. Identify naming conventions, common abstractions
29
-
30
- ## Output Format
31
- Output the skill document between these exact delimiters:
32
-
33
- \`\`\`skill-start
34
- ## 架构要点
35
- ### 核心模块及职责
36
- - **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
37
- ...
38
-
39
- ### 关键设计决策
40
- - 决策:原因
41
- ...
42
-
43
- ## 项目约定
44
- - 约定(具体的,有例子的)
45
- ...
46
-
47
- ## 开发流程
48
- - 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
49
- ...
50
-
51
- ## 常见坑点
52
- - 坑:解法
53
- ...
54
- \`\`\`skill-end
55
-
56
- ## Rules — MUST FOLLOW
57
- 1. 禁止列出文件名清单或目录结构
58
- 2. 禁止列出依赖名和版本
59
- 3. 禁止写项目概述/一句话描述
60
- 4. 禁止只写命令本身(快照已有),只写注意事项
61
- 5. 每个模块描述必须包含行为(做什么 + 怎么协作),不只是名称
62
- 6. **总长度目标 3000-4000 字符,硬上限 4000**
63
- 7. 写可操作的内容,用具体路径和例子
64
- 8. 如果某个 section 没有实质内容,省略它
8
+ const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise "Project Skill" document.
9
+
10
+ ## ⚠️ Complementary to Snapshot — CRITICAL
11
+ The following information is ALREADY provided by Project Snapshot — **do NOT repeat**:
12
+ - ✗ Project one-liner description (快照已有)
13
+ - ✗ Module names and file locations (快照 srcTree 已有)
14
+ - ✗ Tech stack names and versions (快照 techStack 已有)
15
+ - ✗ Build/test commands (快照 keyCommands 已有)
16
+ - ✗ File lists and directory structure (快照 srcTree 已有)
17
+
18
+ **Your job is to capture INSIGHTS only:**
19
+ - **WHY** — design decisions, trade-offs, reasons for choosing X over Y
20
+ - **HOW** — module behaviors, data flows, call chains, non-obvious interactions
21
+ - **GOTCHAS** — pitfalls, edge cases, non-intuitive behaviors
22
+ - **CONVENTIONS** — naming patterns, code style, unwritten rules
23
+
24
+ ## Exploration Strategy
25
+ 1. Use \`codegraph\` to trace call chains and understand module interactions
26
+ 2. Read core modules to understand **behaviors** (not just names)
27
+ 3. Look for complex logic, error handling patterns, unusual configs
28
+ 4. Identify naming conventions, common abstractions
29
+
30
+ ## Output Format
31
+ Output the skill document between these exact delimiters:
32
+
33
+ \`\`\`skill-start
34
+ ## 架构要点
35
+ ### 核心模块及职责
36
+ - **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
37
+ ...
38
+
39
+ ### 关键设计决策
40
+ - 决策:原因
41
+ ...
42
+
43
+ ## 项目约定
44
+ - 约定(具体的,有例子的)
45
+ ...
46
+
47
+ ## 开发流程
48
+ - 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
49
+ ...
50
+
51
+ ## 常见坑点
52
+ - 坑:解法
53
+ ...
54
+ \`\`\`skill-end
55
+
56
+ ## Rules — MUST FOLLOW
57
+ 1. 禁止列出文件名清单或目录结构
58
+ 2. 禁止列出依赖名和版本
59
+ 3. 禁止写项目概述/一句话描述
60
+ 4. 禁止只写命令本身(快照已有),只写注意事项
61
+ 5. 每个模块描述必须包含行为(做什么 + 怎么协作),不只是名称
62
+ 6. **总长度目标 3000-4000 字符,硬上限 4000**
63
+ 7. 写可操作的内容,用具体路径和例子
64
+ 8. 如果某个 section 没有实质内容,省略它
65
65
  `;
66
66
  /**
67
67
  * 生成初始项目 Skill(使用子 agent 深度探索)
@@ -72,35 +72,35 @@ export async function generateInitialSkill(existingSkill, signal) {
72
72
  const hasExisting = !!existingSkill;
73
73
  let prompt;
74
74
  if (hasExisting) {
75
- prompt = `以下是当前的项目 Skill 内容,请深度探索项目并优化/完善它:
76
-
77
- \`\`\`markdown
78
- ${existingSkill}
79
- \`\`\`
80
-
81
- ## 优化方向
82
- 1. 补充缺失的信息(架构要点、设计决策、调用链等)
83
- 2. 修正过时或不准确的内容
84
- 3. 让描述更具体(用实际路径和例子)
85
- 4. 删除冗余或空泛的描述
86
- 5. 如果发现新的坑点或约定,添加进去
87
-
75
+ prompt = `以下是当前的项目 Skill 内容,请深度探索项目并优化/完善它:
76
+
77
+ \`\`\`markdown
78
+ ${existingSkill}
79
+ \`\`\`
80
+
81
+ ## 优化方向
82
+ 1. 补充缺失的信息(架构要点、设计决策、调用链等)
83
+ 2. 修正过时或不准确的内容
84
+ 3. 让描述更具体(用实际路径和例子)
85
+ 4. 删除冗余或空泛的描述
86
+ 5. 如果发现新的坑点或约定,添加进去
87
+
88
88
  记住:保持精简,总长度控制在 4000-5000 字符。输出完整的优化后版本。`;
89
89
  }
90
90
  else {
91
- prompt = `请深度探索当前项目,生成一份项目专属 Skill 文档。
92
-
93
- ## 探索步骤
94
- 1. 先用 glob 扫描项目结构(**/*.ts, **/*.json 等)
95
- 2. 读取 package.json、README.md、tsconfig.json 等关键文件
96
- 3. 用 codegraph 或 grep 找到主入口、核心模块、关键接口
97
- 4. 理解架构模式、数据流向、调用关系
98
- 5. 识别命名约定、代码风格、测试策略
99
- 6. 找出可能的坑点(复杂配置、特殊依赖、构建陷阱)
100
-
101
- ## 输出
102
- 探索完成后,将完整的 skill 文档输出在 \`\`\`skill-start 和 \`\`\`skill-end 之间。
103
-
91
+ prompt = `请深度探索当前项目,生成一份项目专属 Skill 文档。
92
+
93
+ ## 探索步骤
94
+ 1. 先用 glob 扫描项目结构(**/*.ts, **/*.json 等)
95
+ 2. 读取 package.json、README.md、tsconfig.json 等关键文件
96
+ 3. 用 codegraph 或 grep 找到主入口、核心模块、关键接口
97
+ 4. 理解架构模式、数据流向、调用关系
98
+ 5. 识别命名约定、代码风格、测试策略
99
+ 6. 找出可能的坑点(复杂配置、特殊依赖、构建陷阱)
100
+
101
+ ## 输出
102
+ 探索完成后,将完整的 skill 文档输出在 \`\`\`skill-start 和 \`\`\`skill-end 之间。
103
+
104
104
  记住:内容要精简、可操作、有具体路径和例子。总长度控制在 4000-5000 字符。`;
105
105
  }
106
106
  try {
@@ -2,64 +2,64 @@ import { spawnAgent } from '../agent/spawn.js';
2
2
  /**
3
3
  * 子 agent 系统提示:让 agent 自主探索项目并生成 markdown 快照
4
4
  */
5
- const SNAPSHOT_LLM_SUFFIX = `You are a project analysis agent. Your task is to explore this project and generate a concise markdown snapshot for an AI coding assistant.
6
-
7
- ## Your Mission
8
- Explore the project using available tools (read_file, glob, grep) and generate a structured markdown snapshot that captures:
9
- - WHAT exists (project description, tech stack, commands, modules, directory structure)
10
- - WHERE it is (paths, locations)
11
-
12
- Do NOT explain WHY or HOW (that's Project Skill's job).
13
-
14
- ## Exploration Strategy
15
- 1. Read package.json, README.md, tsconfig.json (or equivalent for other languages)
16
- 2. List top-level directories to identify modules
17
- 3. Scan src/ directory structure (depth ≤3, directories only)
18
- 4. Extract key commands from package.json scripts
19
- 5. Identify tech stack from dependencies
20
-
21
- ## Output Format
22
- Output a markdown document between these exact delimiters:
23
-
24
- \`\`\`snapshot-md
25
- # Project Snapshot
26
-
27
- ## Description
28
- (一句话描述项目,≤80字,中文)
29
-
30
- ## Tech Stack
31
- (关键技术栈,逗号分隔,含版本,≤15项)
32
-
33
- ## Commands
34
- | Command | Description |
35
- |---------|-------------|
36
- | \`npm run build\` | 构建项目 |
37
- | \`npm test\` | 运行测试 |
38
- | ... | ... |
39
-
40
- ## Modules
41
- - **module-name** — 一句话职责描述(≤20字)
42
- - **module-name** — 一句话职责描述
43
- - ...
44
-
45
- ## Source Tree
46
- \`\`\`
47
- src/
48
- agent/
49
- tools/
50
- ui/
51
- \`\`\`
52
- \`\`\`snapshot-md
53
-
54
- ## Rules
55
- 1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
56
- 2. **Tech Stack**: ≤15项,逗号分隔,含版本。格式如 "TypeScript 5.x, Node.js ≥18, openai@^4"。
57
- 3. **Commands**: 5-8个最重要的命令,从 package.json scripts 提取。Description ≤15字。用表格格式。
58
- 4. **Modules**: 每个顶层模块目录一条。职责≤20字,只写 WHAT(做什么)。
59
- 5. **Source Tree**: 只列目录(不列文件),深度≤3,用缩进表示层级,放在代码块中。
60
- 6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
61
- 7. 总 markdown ≤ 2500 字符。
62
- 8. 使用中文。
5
+ const SNAPSHOT_LLM_SUFFIX = `You are a project analysis agent. Your task is to explore this project and generate a concise markdown snapshot for an AI coding assistant.
6
+
7
+ ## Your Mission
8
+ Explore the project using available tools (read_file, glob, grep) and generate a structured markdown snapshot that captures:
9
+ - WHAT exists (project description, tech stack, commands, modules, directory structure)
10
+ - WHERE it is (paths, locations)
11
+
12
+ Do NOT explain WHY or HOW (that's Project Skill's job).
13
+
14
+ ## Exploration Strategy
15
+ 1. Read package.json, README.md, tsconfig.json (or equivalent for other languages)
16
+ 2. List top-level directories to identify modules
17
+ 3. Scan src/ directory structure (depth ≤3, directories only)
18
+ 4. Extract key commands from package.json scripts
19
+ 5. Identify tech stack from dependencies
20
+
21
+ ## Output Format
22
+ Output a markdown document between these exact delimiters:
23
+
24
+ \`\`\`snapshot-md
25
+ # Project Snapshot
26
+
27
+ ## Description
28
+ (一句话描述项目,≤80字,中文)
29
+
30
+ ## Tech Stack
31
+ (关键技术栈,逗号分隔,含版本,≤15项)
32
+
33
+ ## Commands
34
+ | Command | Description |
35
+ |---------|-------------|
36
+ | \`npm run build\` | 构建项目 |
37
+ | \`npm test\` | 运行测试 |
38
+ | ... | ... |
39
+
40
+ ## Modules
41
+ - **module-name** — 一句话职责描述(≤20字)
42
+ - **module-name** — 一句话职责描述
43
+ - ...
44
+
45
+ ## Source Tree
46
+ \`\`\`
47
+ src/
48
+ agent/
49
+ tools/
50
+ ui/
51
+ \`\`\`
52
+ \`\`\`snapshot-md
53
+
54
+ ## Rules
55
+ 1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
56
+ 2. **Tech Stack**: ≤15项,逗号分隔,含版本。格式如 "TypeScript 5.x, Node.js ≥18, openai@^4"。
57
+ 3. **Commands**: 5-8个最重要的命令,从 package.json scripts 提取。Description ≤15字。用表格格式。
58
+ 4. **Modules**: 每个顶层模块目录一条。职责≤20字,只写 WHAT(做什么)。
59
+ 5. **Source Tree**: 只列目录(不列文件),深度≤3,用缩进表示层级,放在代码块中。
60
+ 6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
61
+ 7. 总 markdown ≤ 2500 字符。
62
+ 8. 使用中文。
63
63
  `;
64
64
  /**
65
65
  * 生成 LLM 快照(markdown 格式)
@@ -67,17 +67,17 @@ src/
67
67
  * @param signal AbortSignal
68
68
  */
69
69
  export async function generateLLMSnapshot(root, signal) {
70
- const prompt = `请探索项目 ${root},生成项目快照(markdown 格式)。
71
-
72
- ## 探索步骤
73
- 1. 读取 package.json(或等效配置文件)
74
- 2. 读取 README.md
75
- 3. 列出顶层目录,识别模块
76
- 4. 扫描 src/ 目录结构(深度≤3,只列目录)
77
- 5. 从 package.json scripts 提取关键命令
78
- 6. 从 dependencies 识别技术栈
79
-
80
- ## 输出
70
+ const prompt = `请探索项目 ${root},生成项目快照(markdown 格式)。
71
+
72
+ ## 探索步骤
73
+ 1. 读取 package.json(或等效配置文件)
74
+ 2. 读取 README.md
75
+ 3. 列出顶层目录,识别模块
76
+ 4. 扫描 src/ 目录结构(深度≤3,只列目录)
77
+ 5. 从 package.json scripts 提取关键命令
78
+ 6. 从 dependencies 识别技术栈
79
+
80
+ ## 输出
81
81
  严格按照系统提示的 markdown 格式输出,不要添加额外解释。`;
82
82
  try {
83
83
  const result = await spawnAgent({
@@ -169,7 +169,7 @@ function refreshStatusBase(history, lastTurnUsage) {
169
169
  model: config.model,
170
170
  contextBar: renderContextBarInline(history),
171
171
  cwd: process.cwd(),
172
- modeTag: getAgentMode() === 'plan' ? 'plan' : 'auto',
172
+ modeTag: getAgentMode() === 'plan' ? 'Plan' : 'Auto',
173
173
  planSummary: hasActivePlan() ? getActivePlanSummary(process.stdout.columns ?? 80) : '',
174
174
  lastTurnUsage,
175
175
  });
@@ -455,14 +455,15 @@ function textOf(c) {
455
455
  * 回放默认全折叠;用户可鼠标点击摘要行展开(由 BatchRenderer 接管,见 ui/batch.ts)。
456
456
  */
457
457
  export function renderHistory(history) {
458
- const idToName = new Map();
459
- // 当前累积的 batch(assistant.tool_calls + 后续 tool 消息);一旦遇到非 tool 消息即收尾出摘要
460
- let pendingBatch = [];
458
+ const idToEntry = new Map();
459
+ // 普通工具可跨 assistant 步聚合;mutation 各自占一个 group,并切断前后普通工具。
460
+ let pendingBatches = [];
461
+ let normalBatch = null;
461
462
  const flushBatch = () => {
462
- if (pendingBatch.length === 0)
463
- return;
464
- batch.writeSummaryOnly(pendingBatch, layout);
465
- pendingBatch = [];
463
+ for (const entries of pendingBatches)
464
+ batch.writeSummaryOnly(entries, layout);
465
+ pendingBatches = [];
466
+ normalBatch = null;
466
467
  };
467
468
  for (let idx = 0; idx < history.length; idx++) {
468
469
  const m = history[idx];
@@ -505,14 +506,25 @@ export function renderHistory(history) {
505
506
  for (const tc of tcs) {
506
507
  const name = tc?.function?.name ?? '';
507
508
  const args = tc?.function?.arguments ?? '';
508
- if (tc?.id && name)
509
- idToName.set(tc.id, name);
510
- pendingBatch.push({
509
+ const entry = {
511
510
  name,
512
511
  callSummary: summarizeToolCall(name, args),
513
512
  resultSummary: '',
514
513
  diffBlock: null,
515
- });
514
+ };
515
+ if (batch.isMutationToolName(name)) {
516
+ normalBatch = null;
517
+ pendingBatches.push([entry]);
518
+ }
519
+ else {
520
+ if (!normalBatch) {
521
+ normalBatch = [];
522
+ pendingBatches.push(normalBatch);
523
+ }
524
+ normalBatch.push(entry);
525
+ }
526
+ if (tc?.id)
527
+ idToEntry.set(tc.id, entry);
516
528
  }
517
529
  continue; // 跳过后续 tool 消息处理循环(由下一分支填 result)
518
530
  }
@@ -522,18 +534,10 @@ export function renderHistory(history) {
522
534
  }
523
535
  if (m.role === 'tool') {
524
536
  const id = m.tool_call_id ?? '';
525
- const name = idToName.get(id) ?? '';
537
+ const target = idToEntry.get(id);
538
+ const name = target?.name ?? '';
526
539
  const output = textOf(m.content);
527
540
  const preview = summarizeToolResult(name, output);
528
- // 匹配 pendingBatch 中尚未填 result 的同名 entry;同名前缀 tool 较罕见(并行工具同 id 不同名)
529
- let target;
530
- for (let i = pendingBatch.length - 1; i >= 0; i--) {
531
- const e = pendingBatch[i];
532
- if (e.name === name && !e.resultSummary) {
533
- target = e;
534
- break;
535
- }
536
- }
537
541
  if (target) {
538
542
  target.resultSummary = preview;
539
543
  target.fullOutput = output;
@@ -853,6 +857,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
853
857
  if (!loadSnapshots(loaded.id))
854
858
  rebuildFromHistory(history);
855
859
  contextState.lastUsage = undefined;
860
+ contextState.correction = 1;
856
861
  lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
857
862
  layout.clearContent();
858
863
  renderHistory(history);
@@ -985,6 +990,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
985
990
  currentSessionId = undefined; // 下轮起新会话文件
986
991
  turnCount = 0; // 反思 cadence 重新计数
987
992
  contextState.lastUsage = undefined;
993
+ contextState.correction = 1;
988
994
  lastTurnUsage = undefined; // 清空旧轮的 token 累计
989
995
  pendingAttachments = []; // 一并清空待发图片
990
996
  layout.clearContent();
@@ -2,24 +2,12 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from
2
2
  import path from 'node:path';
3
3
  import { config } from '../config/index.js';
4
4
  import { truncateDisplay } from '../ui/render.js';
5
+ import { toText } from '../context/utils.js';
5
6
  let turnIdCounter = 0;
6
7
  let currentTurnId = 0;
7
8
  let turns = [];
8
9
  let snapshots = [];
9
10
  const MUTATION_TOOLS = new Set(['write_file', 'edit_file']);
10
- /** 把任意 content 拍平成字符串(OpenAI 可能是 string / 多模态数组)。 */
11
- function toText(content) {
12
- if (content == null)
13
- return '';
14
- if (typeof content === 'string')
15
- return content;
16
- try {
17
- return JSON.stringify(content);
18
- }
19
- catch {
20
- return String(content);
21
- }
22
- }
23
11
  /** 规整成 cwd 相对路径(快照存相对,跨 /resume 同项目可识别;resolve 已归一 ./ 和 ..)。 */
24
12
  function toRel(p) {
25
13
  try {