mocode-ai 0.5.5 → 0.5.7
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/dist/agent/core.js +6 -5
- package/dist/agent/index.js +17 -4
- package/dist/agent/spawn.js +7 -2
- package/dist/context/index.js +0 -2
- package/dist/context/types.js +1 -1
- package/dist/memory/discover.js +1 -2
- package/dist/project-skill/initializer.js +83 -83
- package/dist/project-snapshot/llm-snapshot.js +69 -69
- package/dist/repl/index.js +8 -8
- package/dist/sandbox/policy.js +1 -1
- package/dist/session/compact.js +17 -16
- package/dist/session/index.js +1 -1
- package/dist/session/scheduler.js +6 -6
- package/dist/skills/discover.js +1 -1
- package/dist/ui/batch.js +2 -2
- package/dist/ui/content.js +16 -0
- package/dist/ui/layout.js +28 -8
- package/dist/ui/markdown.js +5 -4
- package/dist/ui/mouse.js +1 -1
- package/dist/ui/prompt.js +1 -1
- package/dist/ui/render.js +1 -1
- package/package.json +1 -1
package/dist/agent/core.js
CHANGED
|
@@ -138,6 +138,7 @@ function pushToolResult(history, tc, output, pruner, lifecycle, scheduler) {
|
|
|
138
138
|
*/
|
|
139
139
|
export async function runAgentCore(opts) {
|
|
140
140
|
const { history, userInput, signal, onContextUpdate, hooks, skipRollback } = opts;
|
|
141
|
+
const runtimeContextState = opts.contextState ?? contextState;
|
|
141
142
|
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
142
143
|
// 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
143
144
|
// 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
|
|
@@ -187,7 +188,7 @@ export async function runAgentCore(opts) {
|
|
|
187
188
|
// 预算调度器:每个 runAgentCore 实例一个,步前 evaluateBudget + scheduleActions。
|
|
188
189
|
// 决策按 ROI 分发(cold tools 优先 / history 摘要最后);contextBudget 开关关闭时为 null。
|
|
189
190
|
const scheduler = config.contextBudget !== false
|
|
190
|
-
? createBudgetScheduler() // 在 step 循环之外实例化一次,跨步持有 lastRunLog
|
|
191
|
+
? createBudgetScheduler(runtimeContextState) // 在 step 循环之外实例化一次,跨步持有 lastRunLog
|
|
191
192
|
: null;
|
|
192
193
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
193
194
|
let mode = 'idle';
|
|
@@ -235,7 +236,7 @@ export async function runAgentCore(opts) {
|
|
|
235
236
|
await scheduler.runStep(history, step);
|
|
236
237
|
}
|
|
237
238
|
else {
|
|
238
|
-
await maybeCompact(history);
|
|
239
|
+
await maybeCompact(history, undefined, undefined, runtimeContextState);
|
|
239
240
|
}
|
|
240
241
|
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
241
242
|
mode = 'idle';
|
|
@@ -260,7 +261,7 @@ export async function runAgentCore(opts) {
|
|
|
260
261
|
}
|
|
261
262
|
throw e;
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
runtimeContextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
264
265
|
addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
|
|
265
266
|
// 校正系数:API 实测 prompt_tokens / 估算 token。
|
|
266
267
|
// 每次 chat 响应后刷新,让下次 evaluateBudget 用更接近真实的 actual。
|
|
@@ -269,7 +270,7 @@ export async function runAgentCore(opts) {
|
|
|
269
270
|
const estimated = estimateMessagesTokens(history) + estimateToolSchemaTokens();
|
|
270
271
|
if (estimated > 100) {
|
|
271
272
|
const raw = result.usage.promptTokens / estimated;
|
|
272
|
-
|
|
273
|
+
runtimeContextState.correction = Math.max(0.5, Math.min(2.0, raw));
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
hooks.onChatDone?.(); // 主 agent:spinner.stop()
|
|
@@ -482,7 +483,7 @@ export async function runAgentCore(opts) {
|
|
|
482
483
|
return { completed: true, finalText: null, usage: turnUsage };
|
|
483
484
|
}
|
|
484
485
|
finally {
|
|
485
|
-
// 跑完(正常 / 达上限)
|
|
486
|
+
// 跑完(正常 / 达上限)在回复末尾打耗时摘要行;中断 done=false 不打。
|
|
486
487
|
if (done) {
|
|
487
488
|
hooks.onDone?.(Date.now() - t0, turnUsage);
|
|
488
489
|
}
|
package/dist/agent/index.js
CHANGED
|
@@ -34,7 +34,7 @@ function writeToolHeader(tc) {
|
|
|
34
34
|
// 第一条工具开始时立即落摘要;后续调用加入同一 batch,并原地刷新计数。
|
|
35
35
|
batch.showLiveBatch(currentBatchId, layout);
|
|
36
36
|
}
|
|
37
|
-
/** 渲染工具结果:mutation 成功走 diff 块(行号 +
|
|
37
|
+
/** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮);其余走一行 preview。
|
|
38
38
|
* 同 writeToolHeader,改为累积到 BatchRenderer(只缓存字符串,不写屏)。 */
|
|
39
39
|
function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
40
40
|
if (!currentBatchId)
|
|
@@ -66,8 +66,11 @@ function flushToolBatch(expandSingleEntry = false) {
|
|
|
66
66
|
batch.endBatch(id, layout);
|
|
67
67
|
if (expandSingleEntry)
|
|
68
68
|
batch.expandSingleEntryFully(id, layout);
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// 普通摘要只有一个“当前空行”,再 break 一次把它提交为分隔空行。
|
|
70
|
+
// mutation 自动展开时 content.insertAfter 已先把该当前空行提交到 rows;若这里仍补 \n,
|
|
71
|
+
// diff 后就会固定出现两条空白行。
|
|
72
|
+
if (!expandSingleEntry)
|
|
73
|
+
layout.contentWrite('\n');
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
76
|
* agent 核心循环(主 agent,TUI 渲染版):
|
|
@@ -104,6 +107,7 @@ onContextUpdate) {
|
|
|
104
107
|
// 不能按普通字符串的“两个换行才有一个空行”来计算。
|
|
105
108
|
let textBoundaryNewlines = 0;
|
|
106
109
|
let hasPendingTextBoundary = false;
|
|
110
|
+
let toolBatchFollowsText = false;
|
|
107
111
|
const hooks = {
|
|
108
112
|
onText: (s) => {
|
|
109
113
|
// 纯空白 chunk 在视觉上不是正文:既不切 batch,也不写入 markdown 缓冲。
|
|
@@ -130,6 +134,7 @@ onContextUpdate) {
|
|
|
130
134
|
// 文本/思考已流完,模型转而生成 tool_call 参数(可能很长,如 write_file 整篇内容):
|
|
131
135
|
// 补换行(让随后的 ● 行与 diff 不黏在正文末尾)+ 启「生成中」内联 spinner,内容区不再干等。
|
|
132
136
|
if (hasPendingTextBoundary) {
|
|
137
|
+
toolBatchFollowsText = true;
|
|
133
138
|
if (textBoundaryNewlines < 1) {
|
|
134
139
|
layout.contentWrite('\n');
|
|
135
140
|
}
|
|
@@ -150,7 +155,15 @@ onContextUpdate) {
|
|
|
150
155
|
textBoundaryNewlines = 1;
|
|
151
156
|
}
|
|
152
157
|
},
|
|
153
|
-
onToolHeader: (tc) =>
|
|
158
|
+
onToolHeader: (tc) => {
|
|
159
|
+
// mutation 的自动展开会把 current/committed 空行状态互相转换;在首摘要真正
|
|
160
|
+
// 落屏前按视觉行归一,避免同样的文本→edit 边界偶发 1 行或 2 行。
|
|
161
|
+
if (toolBatchFollowsText && isMutationTool(tc.name)) {
|
|
162
|
+
layout.normalizeMutationBoundary();
|
|
163
|
+
}
|
|
164
|
+
toolBatchFollowsText = false;
|
|
165
|
+
writeToolHeader(tc);
|
|
166
|
+
},
|
|
154
167
|
onToolStart: (name) => spinner.start(`执行 ${name}`),
|
|
155
168
|
onToolDone: () => spinner.stop(),
|
|
156
169
|
onToolResult: (tc, output, parsed, preWriteOld, editStartLine) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine),
|
package/dist/agent/spawn.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// - 独立 history:不共享主对话,避免子任务的工具噪声污染主上下文。
|
|
8
8
|
// - 系统提示复用主 agent 组装链(config.systemPrompt + memory 段 + skills 段)+ 子 agent 角色后缀。
|
|
9
9
|
// - 工具子集:按白名单从 chatTools 过滤;无白名单 = 全量(但 task 工具调用方通常会限定只读)。
|
|
10
|
-
// - 不调 beginTurn(不进主回滚链)
|
|
11
|
-
//
|
|
10
|
+
// - 不调 beginTurn(不进主回滚链);skipRollback=true 使文件 mutation 跳过 recordMutation,
|
|
11
|
+
// 子 agent 的改动不进主回滚快照链(靠 git 兜底,见下方 skipRollback 注释)。
|
|
12
12
|
// - 步数上限默认更低(config.subAgentMaxSteps ?? 50),防子任务失控耗尽配额。
|
|
13
13
|
// - 中断透传:opts.signal(主 agent 的 abort signal)透传给 runAgentCore → chat/executeTool,
|
|
14
14
|
// 主 Ctrl+C 树杀子 agent(chat 流式 abort + run_command/web_fetch 即时取消)。
|
|
@@ -21,6 +21,7 @@ import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js'
|
|
|
21
21
|
import { ui } from '../ui/theme.js';
|
|
22
22
|
import { runAgentCore } from './core.js';
|
|
23
23
|
import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
|
|
24
|
+
import { createContextState } from '../session/compact.js';
|
|
24
25
|
/** 子 agent 系统提示后缀:角色与约束。 */
|
|
25
26
|
const SUBAGENT_SUFFIX = `
|
|
26
27
|
|
|
@@ -115,6 +116,9 @@ export async function spawnAgent(opts) {
|
|
|
115
116
|
// onStepStart / onChatDone / onToolStart / onToolDone / onAbort:子 agent 静默,无需 spinner / 中断渲染。
|
|
116
117
|
// abort 还原(history 还原 + 模式还原)由 core 的 abortRestore 处理,hooks 只管展示。
|
|
117
118
|
};
|
|
119
|
+
// 每个子 agent 独享统计/预算状态。不能保存再恢复模块级单例:多个 task 并发时
|
|
120
|
+
// save/restore 会竞态,且 lastEstimate / schedulerLog 仍会污染主 agent。
|
|
121
|
+
const localContextState = createContextState();
|
|
118
122
|
const result = await runAgentCore({
|
|
119
123
|
history,
|
|
120
124
|
userInput: opts.prompt,
|
|
@@ -123,6 +127,7 @@ export async function spawnAgent(opts) {
|
|
|
123
127
|
maxSteps,
|
|
124
128
|
toolsOverride,
|
|
125
129
|
skipRollback: true, // 逻辑隔离:子 agent 文件改动不进主回滚快照链,主 /rollback 不撤销(靠 git 兜底)
|
|
130
|
+
contextState: localContextState,
|
|
126
131
|
});
|
|
127
132
|
return {
|
|
128
133
|
summary: result.finalText,
|
package/dist/context/index.js
CHANGED
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
// 单一入口 runScheduler(agent/core.ts 步前调)接管"何时调用哪一闸"的调度。
|
|
5
5
|
// 不调 LLM、不碰 Tool Calling schema / executeTool / tool_call_id 配对 / TUI 渲染
|
|
6
6
|
// (叶子级:仅 stdlib + tools/constants + session/compact 的 capToolResultForHistory 兜底 + config 开关)。
|
|
7
|
-
//
|
|
8
|
-
// 见 CLAUDE.md「Context Optimization Pipeline」节 +「Context Budget Scheduler」节。
|
|
9
7
|
export { optimizeToolResult } from './pipeline.js';
|
|
10
8
|
export { classify, knownToolKinds } from './classifier.js';
|
|
11
9
|
export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
|
package/dist/context/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Context Optimization Pipeline 的类型契约。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 设计原则:
|
|
4
4
|
// - Tool Calling 的 JSON schema 与 executeTool 不动;本层只接管"工具结果进 LLM 前"的表示。
|
|
5
5
|
// - 不设计统一 DSL,针对不同数据类型各做最优 encoder。
|
|
6
6
|
// - 所有 encoder 是纯函数(无 LLM 调用 / 无 IO / 无副作用),永不抛错(pipeline 层 try/catch,
|
package/dist/memory/discover.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// memory 发现子系统:加载项目记忆 MOCODE.md(对标 skills/discover.ts 的叶子模式)。
|
|
2
2
|
// 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools/skills,避免环。
|
|
3
3
|
//
|
|
4
|
-
// 约定:纯 MOCODE.md(
|
|
5
|
-
// 有自己的工具集与约定,叫 CLAUDE.md 名不副实且可能读到 Claude 专属内容)。
|
|
4
|
+
// 约定:纯 MOCODE.md(mocode 是独立工具,有自己的工具集与约定)。
|
|
6
5
|
// 项目级从 cwd 向上逐级找,全局 ~/.mocode/MOCODE.md。全量注入 systemPrompt(超长截断);
|
|
7
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
8
7
|
import os from 'node:os';
|
|
@@ -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({
|
package/dist/repl/index.js
CHANGED
|
@@ -95,8 +95,7 @@ function maskKey(k) {
|
|
|
95
95
|
return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
|
|
96
96
|
}
|
|
97
97
|
/**
|
|
98
|
-
* /init 指令:发给 agent 扫描项目并生成 MOCODE.md
|
|
99
|
-
* 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
98
|
+
* /init 指令:发给 agent 扫描项目并生成 MOCODE.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
|
|
100
99
|
*/
|
|
101
100
|
const INIT_PROMPT = `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
|
|
102
101
|
|
|
@@ -169,7 +168,7 @@ function refreshStatusBase(history, lastTurnUsage) {
|
|
|
169
168
|
model: config.model,
|
|
170
169
|
contextBar: renderContextBarInline(history),
|
|
171
170
|
cwd: process.cwd(),
|
|
172
|
-
modeTag: getAgentMode() === 'plan' ? '
|
|
171
|
+
modeTag: getAgentMode() === 'plan' ? 'Plan' : 'Auto',
|
|
173
172
|
planSummary: hasActivePlan() ? getActivePlanSummary(process.stdout.columns ?? 80) : '',
|
|
174
173
|
lastTurnUsage,
|
|
175
174
|
});
|
|
@@ -332,7 +331,7 @@ function stopRunningListener() {
|
|
|
332
331
|
* 把用户消息格式化为带满宽背景色的文本(上滑时易辨认用户消息)。
|
|
333
332
|
* 每行用 padEndDisplay 填充到终端宽度(含 ❯ / 缩进),背景色 SGR 包裹整行 + 行末 reset。
|
|
334
333
|
* 满宽 pad 使终端背景色覆盖整行(含行尾空单元格),上滑滚动时用户消息呈连续色块、与 assistant 正文区分。
|
|
335
|
-
* 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)
|
|
334
|
+
* 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)之间空一行。
|
|
336
335
|
*/
|
|
337
336
|
function formatUserMessage(lines) {
|
|
338
337
|
const cols = layout.getGeo().cols;
|
|
@@ -442,7 +441,7 @@ function textOf(c) {
|
|
|
442
441
|
return String(c);
|
|
443
442
|
}
|
|
444
443
|
/**
|
|
445
|
-
* 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume
|
|
444
|
+
* 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文):
|
|
446
445
|
* user→❯ 回显、assistant→正文(+ tool_calls 折叠成 ● 摘要行)、tool→↳ 结果预览;system 跳过。
|
|
447
446
|
* 思考段不持久(history 只存正文),故无思考折叠。渲染后续写位在末尾,紧接 enterInputMode 画输入框。
|
|
448
447
|
* 内容长于屏时 viewport 显尾(最近轮次),PgUp 可看更早——与流式态一致。
|
|
@@ -608,7 +607,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
608
607
|
memoryEnabled: isMemoryEnabled(),
|
|
609
608
|
});
|
|
610
609
|
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
611
|
-
// --resume
|
|
610
|
+
// --resume 有历史则渲染对话,否则横幅。
|
|
612
611
|
setTheme(config.theme);
|
|
613
612
|
layout.enterAltScreen();
|
|
614
613
|
refreshStatusBase(history);
|
|
@@ -679,7 +678,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
679
678
|
});
|
|
680
679
|
/**
|
|
681
680
|
* 回滚子流程(由 /rollback 触发):菜单(↑/↓)选轮次 → 选中第 X 轮 = 删第 X 轮及之后 + 预填第 X 轮 user 输入
|
|
682
|
-
* (
|
|
681
|
+
* (Enter 重新跑该轮);被删轮次的文件改动走二选一菜单(promptRevertChoice:
|
|
683
682
|
* 撤销文件 / 只撤销消息)。选轮 + 方式菜单均走 raw mode。预填经 pendingPrefill 注入下轮 INPUT。
|
|
684
683
|
*/
|
|
685
684
|
const rollbackFlow = async () => {
|
|
@@ -861,7 +860,8 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
861
860
|
lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
|
|
862
861
|
layout.clearContent();
|
|
863
862
|
renderHistory(history);
|
|
864
|
-
|
|
863
|
+
// 末尾 \n\n:与后续用户消息(❯ bubble)之间空一行。
|
|
864
|
+
layout.contentWrite(`${ui.dim}(已续接会话 ${loaded.id})${ui.reset}\n\n`);
|
|
865
865
|
}
|
|
866
866
|
while (true) {
|
|
867
867
|
// INPUT 态:画底栏输入框 + 状态行,光标入输入框
|
package/dist/sandbox/policy.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { jailResolve, jailGlobPattern } from './jail.js';
|
|
4
4
|
/**
|
|
5
5
|
* 豁免 cwd 牢笼的工具:
|
|
6
|
-
* - memory_*:操作 ~/.mocode 与 <cwd>/.mocode
|
|
6
|
+
* - memory_*:操作 ~/.mocode 与 <cwd>/.mocode,本就该在外圈
|
|
7
7
|
* - use_skill:读 ~/.claude/skills、~/.mocode/skills、<cwd>/.mocode/skills,部分在外圈
|
|
8
8
|
* - web_*:跨网络,非文件路径
|
|
9
9
|
* - ask_human / switch_mode:无文件路径
|
package/dist/session/compact.js
CHANGED
|
@@ -6,10 +6,9 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
7
7
|
import { pruneAfterCompaction } from '../rollback/index.js';
|
|
8
8
|
import { toText } from '../context/utils.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* 由 agent/core.ts 在每次 chat 响应后更新;compact/repl 在 usage 失效时同步重置。 */
|
|
9
|
+
export function createContextState() {
|
|
10
|
+
return { lastEstimate: 0, correction: 1 };
|
|
11
|
+
}
|
|
13
12
|
export const contextState = {
|
|
14
13
|
lastEstimate: 0,
|
|
15
14
|
correction: 1,
|
|
@@ -240,9 +239,10 @@ async function defaultSummarize(older, focus) {
|
|
|
240
239
|
* 不检查阈值——调用方(maybeCompact)决定是否调;/compact 直接调以强制压缩。
|
|
241
240
|
*/
|
|
242
241
|
export async function compactHistory(history, opts) {
|
|
242
|
+
const state = opts.contextState ?? contextState;
|
|
243
243
|
const schemaTokens = estimateToolSchemaTokens();
|
|
244
244
|
const estimateBefore = estimateMessagesTokens(history) + schemaTokens;
|
|
245
|
-
|
|
245
|
+
state.lastEstimate = estimateBefore;
|
|
246
246
|
const groups = groupFromEnd(history);
|
|
247
247
|
// 保近期:从尾向前累积直到预算花完(至少保 1 组),永不劈开 group。
|
|
248
248
|
const keepBudget = Math.floor(opts.window * 0.4);
|
|
@@ -313,9 +313,9 @@ export async function compactHistory(history, opts) {
|
|
|
313
313
|
history.push(...rebuilt);
|
|
314
314
|
pruneAfterCompaction(history);
|
|
315
315
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
state.lastEstimate = estimateAfter;
|
|
317
|
+
state.lastUsage = undefined;
|
|
318
|
+
state.correction = 1;
|
|
319
319
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
320
320
|
return {
|
|
321
321
|
compacted: true,
|
|
@@ -400,9 +400,9 @@ export async function compactHistory(history, opts) {
|
|
|
400
400
|
history.push(...rebuilt);
|
|
401
401
|
pruneAfterCompaction(history); // 摘要删了旧轮次 → 按存活轮次裁剪回滚日志
|
|
402
402
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
state.lastEstimate = estimateAfter;
|
|
404
|
+
state.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
|
|
405
|
+
state.correction = 1;
|
|
406
406
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
407
407
|
// 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
|
|
408
408
|
if (estimateAfter >= opts.threshold * opts.window) {
|
|
@@ -418,9 +418,9 @@ export async function compactHistory(history, opts) {
|
|
|
418
418
|
}
|
|
419
419
|
// 摘要失败:回退仅微压缩(tool content 已原地改),结构不动
|
|
420
420
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
state.lastEstimate = estimateAfter;
|
|
422
|
+
state.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
|
|
423
|
+
state.correction = 1;
|
|
424
424
|
if (microcompactDone) {
|
|
425
425
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
426
426
|
return {
|
|
@@ -458,10 +458,10 @@ export async function compactHistory(history, opts) {
|
|
|
458
458
|
* 强制走 compactHistory(manual/force 参数透传)。返 CompactResult 给 caller 文案展示。
|
|
459
459
|
* 默认 manual=false 自动路径完全不变。
|
|
460
460
|
*/
|
|
461
|
-
export async function maybeCompact(history, report, manualOpts) {
|
|
461
|
+
export async function maybeCompact(history, report, manualOpts, state = contextState) {
|
|
462
462
|
const schemaTokens = estimateToolSchemaTokens();
|
|
463
463
|
const est = estimateMessagesTokens(history) + schemaTokens;
|
|
464
|
-
|
|
464
|
+
state.lastEstimate = est;
|
|
465
465
|
const isManual = manualOpts?.manual === true;
|
|
466
466
|
// 手动路径:旁路 autoCompact / report / 总阈三重门
|
|
467
467
|
if (!isManual) {
|
|
@@ -485,6 +485,7 @@ export async function maybeCompact(history, report, manualOpts) {
|
|
|
485
485
|
focus: manualOpts?.focus,
|
|
486
486
|
manual: isManual,
|
|
487
487
|
force: manualOpts?.force,
|
|
488
|
+
contextState: state,
|
|
488
489
|
});
|
|
489
490
|
if (isManual)
|
|
490
491
|
return r;
|
package/dist/session/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - persist.ts:history 序列化到磁盘 + --resume / /resume
|
|
5
5
|
* 依赖方向:session → {llm(摘要复用 chat), config, ui};llm 不反向依赖 session。
|
|
6
6
|
*/
|
|
7
|
-
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, } from './compact.js';
|
|
7
|
+
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, createContextState, } from './compact.js';
|
|
8
8
|
// ── Context Budget Scheduler 接缝 ────────────────────────────────────────
|
|
9
9
|
// agent/core.ts 步前调 runScheduler(history, step):评估五区预算 → 按 ROI 调度
|
|
10
10
|
// shrink_cold_tools / cap_hot_tools / compact_history。开关关闭时退化为 maybeCompact。
|
|
@@ -38,14 +38,14 @@ import { ui } from '../ui/theme.js';
|
|
|
38
38
|
/** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。
|
|
39
39
|
* observePush 当前只是占位:真正 L1/L2/L3 已由 cap / pruner / lifecycle 在 push 时跑;
|
|
40
40
|
* 保留接口为后续「调度器注入 hotBoundary 给 lifecycle」演进留接缝。 */
|
|
41
|
-
export function createBudgetScheduler() {
|
|
41
|
+
export function createBudgetScheduler(state = contextState) {
|
|
42
42
|
const obs = {
|
|
43
43
|
lastRunLog: null,
|
|
44
44
|
observePush(_history, _idx) {
|
|
45
45
|
// 占位:push-time 三闸(cap / pruner / lifecycle)已自动跑;此接缝供将来演进。
|
|
46
46
|
},
|
|
47
47
|
async runStep(history, step) {
|
|
48
|
-
const report = evaluateBudget(history, config.contextWindowTokens, step,
|
|
48
|
+
const report = evaluateBudget(history, config.contextWindowTokens, step, state.correction);
|
|
49
49
|
const actions = scheduleActions(report);
|
|
50
50
|
let compactHistoryCalled = false;
|
|
51
51
|
for (const a of actions) {
|
|
@@ -55,7 +55,7 @@ export function createBudgetScheduler() {
|
|
|
55
55
|
}
|
|
56
56
|
else if (a.kind === 'compact_history') {
|
|
57
57
|
// 路由到 maybeCompact(history, report)——按 ROI 调度(只有 history 超 / totalOver 才真压)
|
|
58
|
-
await maybeCompact(history, report);
|
|
58
|
+
await maybeCompact(history, report, undefined, state);
|
|
59
59
|
compactHistoryCalled = true;
|
|
60
60
|
}
|
|
61
61
|
// shrink_cold_tools L1/L2/L3 与 cap_hot_tools:已由 push-time 闸在每次 push 自动跑
|
|
@@ -71,14 +71,14 @@ export function createBudgetScheduler() {
|
|
|
71
71
|
};
|
|
72
72
|
obs.lastRunLog = log;
|
|
73
73
|
// 暴露给 /context 共享读(repl / context 命令)
|
|
74
|
-
|
|
74
|
+
state.schedulerLog = log;
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
return obs;
|
|
78
78
|
}
|
|
79
79
|
/** 便捷:agent/core.ts 不需要每次 createBudgetScheduler,直接 runScheduler(history, step)。 */
|
|
80
|
-
export async function runScheduler(history, step) {
|
|
81
|
-
const s = createBudgetScheduler();
|
|
80
|
+
export async function runScheduler(history, step, state = contextState) {
|
|
81
|
+
const s = createBudgetScheduler(state);
|
|
82
82
|
await s.runStep(history, step);
|
|
83
83
|
}
|
|
84
84
|
/** 手动 /compact 入口(repl):与自动路径完全一致——五区 ROI 调度,但 history 摘要强制执行。
|
package/dist/skills/discover.js
CHANGED
package/dist/ui/batch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 工具调用批量折叠渲染:
|
|
3
3
|
* agent 一轮返回 N 个 tool_calls 时,不逐个打印 `● name ↳ result`,
|
|
4
4
|
* 改输出一行摘要 `● Ran N tools · read 3, grep 1, glob 1`;
|
|
5
5
|
* 鼠标点击该摘要行 → 展开完整明细(● 头 + ↳ preview / diff 块),再点折回。
|
|
@@ -70,7 +70,7 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
// ── 摘要行文本生成 ──
|
|
73
|
-
/** 把 entry
|
|
73
|
+
/** 把 entry 列表压缩成一行摘要。 */
|
|
74
74
|
function buildSummaryLine(entries) {
|
|
75
75
|
if (entries.length === 0) {
|
|
76
76
|
return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.dim}No tools${ui.reset}`;
|
package/dist/ui/content.js
CHANGED
|
@@ -136,6 +136,22 @@ export function totalRows() {
|
|
|
136
136
|
export function committedRows() {
|
|
137
137
|
return rows.length;
|
|
138
138
|
}
|
|
139
|
+
/** 把缓冲尾部的视觉空白行归一化为恰好 count 条,并结束在“无当前行”状态。
|
|
140
|
+
* 供 markdown→mutation 边界使用;ANSI reset/颜色码和空格均按视觉空白处理。 */
|
|
141
|
+
export function normalizeTrailingBlankRows(count) {
|
|
142
|
+
if (hasCurrent) {
|
|
143
|
+
rows.push(rowStartSgr + curRaw + '\x1B[0m');
|
|
144
|
+
}
|
|
145
|
+
const isBlank = (line) => line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
|
|
146
|
+
while (rows.length > 0 && isBlank(rows[rows.length - 1]))
|
|
147
|
+
rows.pop();
|
|
148
|
+
for (let i = 0; i < Math.max(0, count); i++)
|
|
149
|
+
rows.push('\x1B[0m');
|
|
150
|
+
curSgr = '';
|
|
151
|
+
rowStartSgr = '';
|
|
152
|
+
curRaw = '';
|
|
153
|
+
hasCurrent = false;
|
|
154
|
+
}
|
|
139
155
|
/** 取绝对行索引(0-based,含当前行)的原始自洽行;越界返 null。供鼠标选区文本提取。 */
|
|
140
156
|
export function lineAt(abs) {
|
|
141
157
|
const all = snapshot();
|
package/dist/ui/layout.js
CHANGED
|
@@ -3,6 +3,7 @@ import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncate
|
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import * as content from './content.js';
|
|
5
5
|
import * as mouse from './mouse.js';
|
|
6
|
+
import { shiftBatchesAfter } from './batch.js';
|
|
6
7
|
import { copyToClipboard, readClipboard } from './clipboard.js';
|
|
7
8
|
import { renderMarkdown } from './markdown.js';
|
|
8
9
|
// ── 内部状态 ──
|
|
@@ -64,7 +65,7 @@ let cursorChangeHandler = null;
|
|
|
64
65
|
const esc = {
|
|
65
66
|
altOn: '\x1B[?1049h',
|
|
66
67
|
altOff: '\x1B[?1049l',
|
|
67
|
-
//
|
|
68
|
+
// 完整鼠标追踪:1000=按键(按下/释放)+ 1002=拖动 motion + 1006=SGR 编码。
|
|
68
69
|
// 拿到按下/拖动/释放的坐标后,由 layout 在应用层维护选区(mouse.ts 重组报表 → handleMouseEvent):
|
|
69
70
|
// - 左键:内容区按下开选区、拖动扩展(触边自动翻页跨屏)、释放只留高亮,不自动复制;输入框不响应
|
|
70
71
|
// (不干扰正常打字/焦点)。
|
|
@@ -541,8 +542,9 @@ export function contentInsertAfter(after, lines) {
|
|
|
541
542
|
scrollOffset = Math.min(delta, Math.max(0, content.totalRows() - g.contentBottom));
|
|
542
543
|
}
|
|
543
544
|
}
|
|
544
|
-
// batch
|
|
545
|
-
|
|
545
|
+
// 必须同步平移 batch 索引。若延迟到 dynamic import.then,下一条 mutation 可能已经
|
|
546
|
+
// 按插入后的 buffer 创建;旧回调会再平移它一次,导致详情插到空行之后。
|
|
547
|
+
shiftBatchesAfter(after, delta);
|
|
546
548
|
repaintViewport();
|
|
547
549
|
}
|
|
548
550
|
/**
|
|
@@ -570,14 +572,32 @@ export function contentDeleteFrom(startIdx, n) {
|
|
|
570
572
|
if (scrolled) {
|
|
571
573
|
scrollOffset = Math.max(0, scrollOffset - delta);
|
|
572
574
|
}
|
|
573
|
-
// batch 摘要行索引平移(用 -delta 表示后段索引前移)
|
|
574
|
-
|
|
575
|
+
// batch 摘要行索引平移(用 -delta 表示后段索引前移),同插入路径必须同步。
|
|
576
|
+
shiftBatchesAfter(startIdx, -delta);
|
|
575
577
|
repaintViewport();
|
|
576
578
|
}
|
|
577
579
|
/** 当前内容物理行总数(含当前未提交行)。供 batch 渲染器在 endBatch 时定位摘要行索引。 */
|
|
578
580
|
export function totalRows() {
|
|
579
581
|
return content.totalRows();
|
|
580
582
|
}
|
|
583
|
+
/** 正文→mutation 首摘要前,把尾部间距强制归一为一条视觉空行。 */
|
|
584
|
+
export function normalizeMutationBoundary() {
|
|
585
|
+
if (!active || !ui.isTTY)
|
|
586
|
+
return;
|
|
587
|
+
if (mdActive)
|
|
588
|
+
commitMd();
|
|
589
|
+
const totalBefore = content.totalRows();
|
|
590
|
+
content.normalizeTrailingBlankRows(1);
|
|
591
|
+
const g = getGeo();
|
|
592
|
+
const delta = content.totalRows() - totalBefore;
|
|
593
|
+
if (scrollOffset > 0 && delta !== 0) {
|
|
594
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, Math.max(0, content.totalRows() - g.contentBottom)));
|
|
595
|
+
}
|
|
596
|
+
contentRow = Math.min(content.committedRows() + 1, g.contentBottom);
|
|
597
|
+
contentCol = 1;
|
|
598
|
+
if (scrollOffset === 0)
|
|
599
|
+
repaintViewport();
|
|
600
|
+
}
|
|
581
601
|
/** 原地刷新一条内容行(行数不变),用于运行中的工具 batch 更新计数。 */
|
|
582
602
|
export function contentReplaceLine(absIdx, line) {
|
|
583
603
|
if (!active)
|
|
@@ -1139,7 +1159,7 @@ function handleMouseEvent(e) {
|
|
|
1139
1159
|
if (!selection)
|
|
1140
1160
|
return;
|
|
1141
1161
|
if (!selection.dragged) {
|
|
1142
|
-
// 内容区纯点击(未拖动):若落在工具 batch 摘要行上 →
|
|
1162
|
+
// 内容区纯点击(未拖动):若落在工具 batch 摘要行上 → 切换展开/折叠;
|
|
1143
1163
|
// 否则原行为:清选区。batch 反查通过 content lineAt + dynamic import(避免 layout↔batch 循环依赖)。
|
|
1144
1164
|
const absClick = selection.anchorLine; // 起止同行同列,取任一;未拖动时 line = anchor = end
|
|
1145
1165
|
void (async () => {
|
|
@@ -1273,11 +1293,11 @@ function composeModelLine(status, cols) {
|
|
|
1273
1293
|
const ctxW = ansiDisplayWidth(ctx);
|
|
1274
1294
|
// 左段:模式标识 + 切换提示(灰)+ 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1275
1295
|
const modeTag = status.modeTag ?? '';
|
|
1276
|
-
const modeColor = modeTag === '
|
|
1296
|
+
const modeColor = modeTag === 'Plan' ? ui.yellow : ui.accent;
|
|
1277
1297
|
const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
|
|
1278
1298
|
const modeW = modeTag ? displayWidth(modeTag) : 0;
|
|
1279
1299
|
// 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
|
|
1280
|
-
const HINT = 'Shift+Tab
|
|
1300
|
+
const HINT = 'Shift+Tab 切换';
|
|
1281
1301
|
const hintW = modeTag ? displayWidth(HINT) : 0;
|
|
1282
1302
|
const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
|
|
1283
1303
|
const tokChip = formatTurnTokenChip(status.lastTurnUsage);
|
package/dist/ui/markdown.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// 整段(text 可能停在未闭合 ``` fence 中段,状态机扫到 EOF 仍 inFence 时把已累积代码行
|
|
6
6
|
// 当「进行中代码块」照常 emit,边生成边显)。
|
|
7
7
|
//
|
|
8
|
-
// 样式:代码块 Flat
|
|
8
|
+
// 样式:代码块 Flat——语言标签 dim 置顶 + 2 空格 gutter + cli-highlight
|
|
9
9
|
// 语法高亮 + 软折行,无边框。其余:标题/列表(嵌套)/行内代码/粗体/斜体/删除线/链接/引用块/分隔线。
|
|
10
10
|
//
|
|
11
11
|
// 硬约束:每行 ansiDisplayWidth ≤ cols(layout.repaintViewport 直出行,超宽会让终端 auto-wrap
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// clipAnsiLine 兜底,绝不溢出。颜色一律嵌入式 ANSI(ui.*)写进字符串。
|
|
14
14
|
import { highlight, supportsLanguage } from 'cli-highlight';
|
|
15
15
|
import { ui, getThemeVersion } from './theme.js';
|
|
16
|
-
import { charWidth, displayWidth, ansiDisplayWidth } from './render.js';
|
|
16
|
+
import { charWidth, displayWidth, ansiDisplayWidth, stripAnsi } from './render.js';
|
|
17
17
|
const RESET = '\x1b[0m';
|
|
18
18
|
// theme 无 italic/strike,自备(TTY 感知:!isTTY 退化为空串,同 ui.* 契约)。
|
|
19
19
|
const ITALIC = ui.isTTY ? '\x1b[3m' : '';
|
|
@@ -577,13 +577,14 @@ function renderMarkdownImpl(text, cols) {
|
|
|
577
577
|
// 独立 markdown 段的外部间距由 agent/batch 边界统一管理。流式后端可能把
|
|
578
578
|
// "\n\n" 与首段正文拆成不同 chunk;仅清洗首个正文 chunk 不够,因为 mdBuf
|
|
579
579
|
// 累积重渲染时这些换行仍会变成段首空行。这里最终兜底,正文段永不自带前导空行。
|
|
580
|
-
|
|
580
|
+
const isVisualBlank = (line) => stripAnsi(line).trim().length === 0;
|
|
581
|
+
while (out.length > 0 && isVisualBlank(out[0]))
|
|
581
582
|
out.shift();
|
|
582
583
|
// 末尾不留空行:agent onText 后接 onToolCall 的 contentWrite('\n') 会补 1 空行分隔正文与 ● 行;
|
|
583
584
|
// 若 md 末尾自带空行(段落/代码块后)则叠成 2 空行。裁掉末尾连续空行,让 onToolCall / 轮末
|
|
584
585
|
// contentWrite('\n') 恰好补 1 行(与改造前 raw 文本行为一致)。块间空行(flushPara/flushCode 中段
|
|
585
586
|
// push 的)不受影响——只裁末尾。
|
|
586
|
-
while (out.length > 0 && out[out.length - 1]
|
|
587
|
+
while (out.length > 0 && isVisualBlank(out[out.length - 1]))
|
|
587
588
|
out.pop();
|
|
588
589
|
return out;
|
|
589
590
|
}
|
package/dist/ui/mouse.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// 背景:layout 进 alt 屏时发 \x1B[?1000h + \x1B[?1002h + \x1B[?1006h,启用:
|
|
4
4
|
// 1000 = 按键事件追踪(按下 / 释放),1002 = 拖动追踪(按住键移动时上报 motion),
|
|
5
5
|
// 1006 = SGR 编码 → \x1B[<btn;col;rowM(按下 / 拖动)或 \x1B[<btn;col;rowm(释放)。
|
|
6
|
-
// 有了 1002 的拖动上报,才能实现"按住左键拖过多屏 → 应用层维护选区 → 松开复制"
|
|
6
|
+
// 有了 1002 的拖动上报,才能实现"按住左键拖过多屏 → 应用层维护选区 → 松开复制"。
|
|
7
7
|
//
|
|
8
8
|
// 但 Node readline 的 emitKeypressEvents 不认 `<` 为 CSI 参数字节,把一条报表拆成 `\x1b[<` +
|
|
9
9
|
// 逐字符共 ≥9 个 keypress(数字 / `;` / `M` 有可打印 name,会被当文本砸进输入框)。故本模块在
|
package/dist/ui/prompt.js
CHANGED
|
@@ -420,7 +420,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
420
420
|
computeFiltered();
|
|
421
421
|
redraw();
|
|
422
422
|
}
|
|
423
|
-
/** Ctrl+C:有内容(已打字 / 多行 / chip / 菜单草稿 / 粘贴缓冲 / 粘贴中)则清空,再按一次(空)才退出(仿 fish
|
|
423
|
+
/** Ctrl+C:有内容(已打字 / 多行 / chip / 菜单草稿 / 粘贴缓冲 / 粘贴中)则清空,再按一次(空)才退出(仿 fish)。 */
|
|
424
424
|
function onCtrlC() {
|
|
425
425
|
const hasContent = chip != null ||
|
|
426
426
|
lines.length > 1 ||
|
package/dist/ui/render.js
CHANGED