mocode-ai 0.7.3 → 1.0.2
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 +58 -4
- package/README.zh-CN.md +58 -4
- package/dist/agent/core.js +86 -34
- package/dist/agent/index.js +57 -6
- package/dist/agent/spawn.js +74 -30
- package/dist/agents/coordinator.js +60 -0
- package/dist/changeset/index.js +289 -0
- package/dist/changeset/types.js +1 -0
- package/dist/config/index.js +25 -13
- package/dist/context/artifacts.js +254 -0
- package/dist/context/classifier.js +1 -1
- package/dist/context/index.js +1 -0
- package/dist/i18n/index.js +16 -4
- package/dist/llm/index.js +2 -2
- package/dist/repl/index.js +31 -5
- package/dist/rollback/index.js +43 -8
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +2 -2
- package/dist/sandbox/root.js +9 -3
- package/dist/session/compact.js +9 -3
- package/dist/session/scheduler.js +4 -0
- package/dist/session/state.js +3 -19
- package/dist/tools/builtins/apply-patch.js +174 -0
- package/dist/tools/builtins/edit-file.js +59 -46
- package/dist/tools/builtins/index.js +10 -8
- package/dist/tools/builtins/read-file.js +4 -2
- package/dist/tools/builtins/task.js +47 -16
- package/dist/tools/builtins/write-file.js +52 -17
- package/dist/tools/constants.js +3 -3
- package/dist/tools/registry.js +5 -2
- package/dist/ui/batch.js +56 -24
- package/dist/ui/layout.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,67 @@ A terminal coding agent: give it a goal, and it **completes it autonomously**
|
|
|
8
8
|
|
|
9
9
|
MoCode explores your code, reads/writes/edits files, runs shell commands, and searches the web on its own, driving the task forward through a loop of "think → call a tool → observe the result → think again." It works with any OpenAI-compatible endpoint (GLM, DeepSeek, Qwen, local Ollama / vLLM, etc.), runs as a full-screen TUI with streaming output and visible reasoning.
|
|
10
10
|
|
|
11
|
+
## Architecture
|
|
12
|
+
|
|
13
|
+
MoCode is organized as a layered runtime: the terminal experience drives an autonomous core, the core reaches capabilities through a guarded execution plane, and a persistent intelligence layer keeps long-running work coherent.
|
|
14
|
+
|
|
15
|
+
<p align="center"><img src="./assets/architecture/system-overview.svg" alt="MoCode layered system architecture" width="100%"></p>
|
|
16
|
+
|
|
17
|
+
### Autonomous execution loop
|
|
18
|
+
|
|
19
|
+
Each model response is one step in a closed loop. Tool calls are classified by declared capabilities, safe reads can run in parallel, writes acquire canonical resource locks, observations are encoded before returning to context, and code changes pass through automatic validation.
|
|
20
|
+
|
|
21
|
+
<p align="center"><img src="./assets/architecture/agent-loop.svg" alt="MoCode autonomous agent execution loop" width="100%"></p>
|
|
22
|
+
|
|
23
|
+
### Context that ages instead of exploding
|
|
24
|
+
|
|
25
|
+
Tool output does not accumulate as an undifferentiated transcript. Typed encoders, relevance pruning, an observation lifecycle, age-aware compression, and a five-zone budget scheduler continuously reshape the active working set while sessions, snapshots, skills, notes, and memory retain durable knowledge.
|
|
26
|
+
|
|
27
|
+
<p align="center"><img src="./assets/architecture/context-engine.svg" alt="MoCode context engineering and durable memory architecture" width="100%"></p>
|
|
28
|
+
|
|
29
|
+
### Multi-agent work without unsafe shared writes
|
|
30
|
+
|
|
31
|
+
Read-only sub-agents fan out concurrently. Writer agents work inside private filesystem overlays and return structured ChangeSets; the coordinator checks expected hashes, acquires canonical locks, performs conflict-safe merges, and runs one unified verification gate in the main workspace.
|
|
32
|
+
|
|
33
|
+
<p align="center"><img src="./assets/architecture/multi-agent.svg" alt="MoCode multi-agent overlay and ChangeSet coordination" width="100%"></p>
|
|
34
|
+
|
|
35
|
+
### Controlled execution: permission gates and capability scheduling
|
|
36
|
+
|
|
37
|
+
Every mutating tool calls into a permission layer before it runs. Tools are classified `safe` / `confirm` / `dangerous`, scopes can be `once` / `session` / `project` / global-tool, fingerprints are stable hashes (command, path, or args), and the persistent record lives in `~/.mocode/permissions.json` (v3 schema, with v2 resource grants still loaded). Piped or CI environments default to deny until you opt in.
|
|
38
|
+
|
|
39
|
+
<p align="center"><img src="./assets/architecture/permission-model.svg" alt="MoCode permission model: tool classes, four-tier grants, fingerprinting, durable storage" width="100%"></p>
|
|
40
|
+
|
|
41
|
+
### Verification cascade: cheap checks first, expensive checks only on demand
|
|
42
|
+
|
|
43
|
+
Code changes go through V0 (file post-conditions) → V1 (scoped tsc/eslint markers) → V2 (targeted unit tests) → V3 (affected package scripts). The first actionable failure stops the cascade and is fed back to the agent as a fresh observation; a SHA-256 content cache skips repeated work on unchanged files.
|
|
44
|
+
|
|
45
|
+
<p align="center"><img src="./assets/architecture/verification-cascade.svg" alt="MoCode verification cascade V0 to V3 with content fingerprint cache" width="100%"></p>
|
|
46
|
+
|
|
47
|
+
### Rollback timeline: per-mutation snapshots, restore by turn
|
|
48
|
+
|
|
49
|
+
A clean undo point is saved before every mutating tool. `/rollback <turnId>` restores file buffers in reverse-chronological order under canonical resource locks, then reruns V0+V1 to confirm a clean state — never re-runs the model. Read tools, network effects, and binary changes are explicitly out of scope, kept honest in the contract.
|
|
50
|
+
|
|
51
|
+
<p align="center"><img src="./assets/architecture/rollback-flow.svg" alt="MoCode rollback timeline and per-turn snapshot flow" width="100%"></p>
|
|
52
|
+
|
|
53
|
+
### Context controls: five independent dials, not one big toggle
|
|
54
|
+
|
|
55
|
+
`autoCompact` / `contextOptimize` / `contextRelprune` / `contextLifecycle` / `contextBudget` each gate a different knob (push-time compression, encoders, superseded-read pruning, observation lifecycle, five-zone scheduler). Each is independently killable via a `MOCODE_*=false` env var; the observation lifecycle runs even with all toggles off, so context still ages instead of exploding. An EWMA self-calibrates the token estimator against real provider usage.
|
|
56
|
+
|
|
57
|
+
<p align="center"><img src="./assets/architecture/context-controls.svg" alt="MoCode context controls: five independent toggles, observation lifecycle, token self-calibration" width="100%"></p>
|
|
58
|
+
|
|
59
|
+
### Desktop pet: a passive mirror over WebSocket
|
|
60
|
+
|
|
61
|
+
The optional Electron sub-package (`packages/pet-app`) shows a stateful floating character that mirrors agent activity via a one-way WebSocket stream. Quit with `/pet quit`. The renderer owns no business logic; the agent loop is unchanged regardless of whether the pet is running.
|
|
62
|
+
|
|
63
|
+
<p align="center"><img src="./assets/architecture/pet-bridge.svg" alt="MoCode desktop pet bridge: hooks, frames, Electron client" width="100%"></p>
|
|
64
|
+
|
|
11
65
|
## Why MoCode
|
|
12
66
|
|
|
13
67
|
MoCode isn't a chat box with a coat of paint — it's an agent that actually gets things done:
|
|
14
68
|
|
|
15
69
|
- **Autonomous multi-step execution** — In a single conversation, the agent chains multiple steps on its own: read code, edit code, run tests, fix based on errors, and so on. It decides the next step without you nagging it. When it hits a decision point, it calls `ask_human` to pop up a panel and ask you (blocking until you respond).
|
|
16
70
|
- **Parallel read-only tools** — Consecutive read-only operations in a turn (reading files, grep, glob, codegraph, web search/fetch) run concurrently, so total time is roughly the slowest single call instead of the sum of all of them. Operations with side effects (writing/editing files) stay sequential to preserve snapshot ordering and data safety.
|
|
17
|
-
- **Sub-agents divide and conquer** — Complex tasks can spawn independent sub-agents
|
|
71
|
+
- **Sub-agents divide and conquer** — Complex tasks can spawn independent sub-agents with isolated histories and scoped toolsets. Read-only workers can fan out concurrently; writer workers run in private filesystem overlays and return ChangeSets that are merged under expected-hash checks and canonical resource locks. Only structured findings return to the main thread.
|
|
18
72
|
- **Plan / Auto dual mode** — In `plan` mode the agent is read-only (reads code, queries indexes, searches — never writes to disk, runs commands, or spawns sub-agents) and produces a plan; `auto` mode unlocks the full toolset. The agent can switch between the two on its own — scope out an unfamiliar codebase first, then start making changes.
|
|
19
73
|
- **Automatic context compression** — As the context window fills up, a three-tier compression kicks in (trim individual results → compact older tool results in place → summarize older turns), so long sessions never overflow. `/context` shows live token usage; `/compact` triggers manual compression (optionally with a focus hint to preserve what matters).
|
|
20
74
|
- **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. A background process periodically reflects on conversations to mine things worth remembering. Memories can be created, searched, updated, and forgotten, with recall-based decay.
|
|
@@ -112,8 +166,8 @@ Common backend `base_url` values:
|
|
|
112
166
|
| `ANYSEARCH_BASE_URL` | Search API endpoint | `https://api.anysearch.com` |
|
|
113
167
|
| `SKILLS_DIRS` | Override the default skill scan directories (platform path separator) | three default directories |
|
|
114
168
|
| `MOCODE_CONTEXT_OPTIMIZE` | Typed encoding of tool results before they reach the LLM (tree/search/log…); disable for raw passthrough (length trimming only) | `true` |
|
|
115
|
-
| `MAX_STEPS` | Max agent loop steps per turn (
|
|
116
|
-
| `SUB_AGENT_MAX_STEPS` |
|
|
169
|
+
| `MAX_STEPS` | Max agent loop steps per turn (infinite-loop safety only) | `1000` |
|
|
170
|
+
| `SUB_AGENT_MAX_STEPS` | Sub-agent loop safety ceiling; defaults to the main-agent value | `1000` |
|
|
117
171
|
| `SANDBOX_ROOT` | Sandbox root directory (file operation boundary; falls back to cwd if unset) | none |
|
|
118
172
|
| `MOCODE_THEME` | Color theme (default/dark/light…; shell env takes precedence over file) | `default` |
|
|
119
173
|
|
|
@@ -149,7 +203,7 @@ The agent operates in **the working directory it was launched from** — to have
|
|
|
149
203
|
| `ask_human` | Pop up a Q&A panel at decision points; user picks a preset or types freely (blocks until answered) |
|
|
150
204
|
| `switch_mode` | Switch between `plan` (read-only planning) and `auto` (full execution); the agent can call this itself to explore before acting |
|
|
151
205
|
| `drop_context` | Replace irrelevant old tool results in history with stubs to free up context (preserves tool_call_id pairing, leaves system prompt and current turn untouched, idempotent) |
|
|
152
|
-
| `
|
|
206
|
+
| `sub-agent` | Spawn a capable isolated worker; read tasks can run concurrently and writes use overlay + ChangeSet safe merge |
|
|
153
207
|
|
|
154
208
|
| `memory_save` | Save a piece of cross-session long-term memory (title indexed, body fetched on demand) |
|
|
155
209
|
| `memory_search` | Search memory bodies by keyword; hits boost the recall count (affects forgetting decay) |
|
package/README.zh-CN.md
CHANGED
|
@@ -8,13 +8,67 @@
|
|
|
8
8
|
|
|
9
9
|
mocode 自己探索代码、读写改文件、执行命令、联网查资料,以「思考 → 调用工具 → 观察结果 → 再思考」的循环一步步把任务推进到完成。接任意 OpenAI 兼容接口(GLM、DeepSeek、Qwen、本地 Ollama / vLLM 等),全屏 TUI 交互,流式输出、思考过程可见。
|
|
10
10
|
|
|
11
|
+
## 架构
|
|
12
|
+
|
|
13
|
+
MoCode 是一个分层的自治运行时:终端交互层驱动 Agent 内核,内核通过受控能力平面执行真实操作,持久化认知层则让长任务和跨会话工作保持连贯。
|
|
14
|
+
|
|
15
|
+
<p align="center"><img src="./assets/architecture/system-overview-zh-CN.svg" alt="MoCode 分层系统架构" width="100%"></p>
|
|
16
|
+
|
|
17
|
+
### 自治执行循环
|
|
18
|
+
|
|
19
|
+
每次模型响应都是闭环中的一步。工具调用按能力声明分类,安全读取可以并行,写操作获取规范化资源锁,观察结果编码后才回到上下文,代码改动最终经过自动验证门。
|
|
20
|
+
|
|
21
|
+
<p align="center"><img src="./assets/architecture/agent-loop-zh-CN.svg" alt="MoCode 自治 Agent 执行循环" width="100%"></p>
|
|
22
|
+
|
|
23
|
+
### 会衰减、不会膨胀的上下文
|
|
24
|
+
|
|
25
|
+
工具输出不会作为无差别日志无限堆积。类型化编码、相关性裁剪、观察生命周期、年龄感知压缩和五区预算调度持续重塑活跃工作集;会话、Snapshot、Skill、notes.md 与长期记忆负责保留耐久知识。
|
|
26
|
+
|
|
27
|
+
<p align="center"><img src="./assets/architecture/context-engine-zh-CN.svg" alt="MoCode 上下文工程与持久化记忆架构" width="100%"></p>
|
|
28
|
+
|
|
29
|
+
### 多 Agent 并行,但不冒险共享写入
|
|
30
|
+
|
|
31
|
+
只读子 Agent 可以并行扇出;写任务在私有文件系统 overlay 中完成并返回结构化 ChangeSet。协调器校验 expected hash、获取规范化资源锁、安全合并冲突,最后由主工作区统一执行验证。
|
|
32
|
+
|
|
33
|
+
<p align="center"><img src="./assets/architecture/multi-agent-zh-CN.svg" alt="MoCode 多 Agent overlay 与 ChangeSet 协调" width="100%"></p>
|
|
34
|
+
|
|
35
|
+
### 受控执行:权限门 + 能力调度
|
|
36
|
+
|
|
37
|
+
每个写入工具在执行前都会经过权限层。工具分为 `safe` / `confirm` / `dangerous` 三档,授权粒度支持 `once` / `session` / `project` / 全局工具四档,指纹使用稳定哈希(命令、路径或参数),持久化记录落在 `~/.mocode/permissions.json`(v3 格式,会自动加载 v2 资源授权)。管道/CI 环境默认拒绝所有需确认的操作,除非显式开启。
|
|
38
|
+
|
|
39
|
+
<p align="center"><img src="./assets/architecture/permission-model-zh-CN.svg" alt="MoCode 权限模型:工具分级、四档授权、指纹、持久化" width="100%"></p>
|
|
40
|
+
|
|
41
|
+
### 验证瀑布:便宜检查先做,贵检查按需上场
|
|
42
|
+
|
|
43
|
+
代码改动按 V0(文件级后置条件)→ V1(限范围的 tsc/eslint)→ V2(定向单元测试)→ V3(受影响 package 的脚本)由低到高执行。首个可操作失败立即停止,作为新的观察反馈给 Agent;SHA-256 文件指纹缓存避免对未改动文件重复劳动。
|
|
44
|
+
|
|
45
|
+
<p align="center"><img src="./assets/architecture/verification-cascade-zh-CN.svg" alt="MoCode 自动验证瀑布 V0 到 V3,带文件指纹缓存" width="100%"></p>
|
|
46
|
+
|
|
47
|
+
### 回滚时间线:每次写入都留干净撤销点
|
|
48
|
+
|
|
49
|
+
每次写入工具执行前先存一份 undo 快照。`/rollback <turnId>` 按时间逆序在 canonical 资源锁下恢复文件缓冲,然后重跑 V0+V1 验证状态干净——完全不重跑模型。读取类工具、网络副作用、二进制改动明确不在截图范围,契约里写死。
|
|
50
|
+
|
|
51
|
+
<p align="center"><img src="./assets/architecture/rollback-flow-zh-CN.svg" alt="MoCode 回滚时间线和每轮快照流" width="100%"></p>
|
|
52
|
+
|
|
53
|
+
### 上下文控制:五个独立开关,不是一锅端
|
|
54
|
+
|
|
55
|
+
`autoCompact` / `contextOptimize` / `contextRelprune` / `contextLifecycle` / `contextBudget` 各自把控一个旋钮(push 压缩、编码器、被取代读取的剪裁、观察生命周期、五区调度器)。每个都能用 `MOCODE_*=false` 单独关;即使五个全关,观察结果仍按生命周期老化。token 估算带 EWMA 自动校准真实 provider 用量。
|
|
56
|
+
|
|
57
|
+
<p align="center"><img src="./assets/architecture/context-controls-zh-CN.svg" alt="MoCode 上下文控制:五个独立开关、观察生命周期、token 自校准" width="100%"></p>
|
|
58
|
+
|
|
59
|
+
### 桌宠:WebSocket 上的被动镜像
|
|
60
|
+
|
|
61
|
+
可选 Electron 子包(`packages/pet-app`)用一个悬浮小角色镜像 agent 状态:单向 WebSocket 推送事件帧,`/pet quit` 完全关闭。渲染层零业务逻辑,无论桌宠是否在跑,主 agent 循环一字不改。
|
|
62
|
+
|
|
63
|
+
<p align="center"><img src="./assets/architecture/pet-bridge-zh-CN.svg" alt="MoCode 桌宠桥:hooks、事件帧、Electron 客户端" width="100%"></p>
|
|
64
|
+
|
|
11
65
|
## 为什么用 mocode
|
|
12
66
|
|
|
13
67
|
mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
14
68
|
|
|
15
69
|
- **自主多步推进** — 一次对话里连续多步:读代码、改代码、跑测试、根据报错再改……agent 自己决定下一步,中途不用你反复催。遇到卡点会调 `ask_human` 弹面板问你(阻塞到回应)。
|
|
16
70
|
- **只读工具并行执行** — 一轮里连续的只读操作(读文件、grep、glob、codegraph、联网搜索/抓取)自动并发跑,总耗时 ≈ 最慢一个,而不是逐个排队。写文件 / 改文件这类有副作用的操作仍串行,保快照顺序与数据安全。
|
|
17
|
-
- **子 agent 分而治之** —
|
|
71
|
+
- **子 agent 分而治之** — 复杂任务可派生拥有独立历史与受限工具集的子 agent。只读 worker 可并行扇出;写 worker 在私有文件系统 overlay 中运行,返回的 ChangeSet 经过 expected hash 校验与规范化资源锁后才合并。主线只接收结构化发现,不接收过程噪声。
|
|
18
72
|
- **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
|
|
19
73
|
- **上下文自动压缩** — 接近窗口上限时三层压缩(单条结果裁剪 → 旧工具结果原地微压缩 → 旧对话摘要),长会话也不爆窗口;`/context` 实时显示 token 用量,`/compact` 可手动压缩(能带焦点指令聚焦保留)。
|
|
20
74
|
- **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
|
|
@@ -110,8 +164,8 @@ LLM_MODEL=glm-4.6 # 换成你的模型名
|
|
|
110
164
|
| `ANYSEARCH_BASE_URL` | 搜索 API 端点 | `https://api.anysearch.com` |
|
|
111
165
|
| `SKILLS_DIRS` | 覆盖默认 skill 扫描目录(平台分隔符) | 三目录自动扫描 |
|
|
112
166
|
| `MOCODE_CONTEXT_OPTIMIZE` | 工具结果进 LLM 前的类型化编码(树/搜索/日志…),关掉则原样进(仅长度裁剪) | `true` |
|
|
113
|
-
| `MAX_STEPS` | 每轮
|
|
114
|
-
| `SUB_AGENT_MAX_STEPS` | 子
|
|
167
|
+
| `MAX_STEPS` | 每轮 Agent 循环最大步数(仅防无限循环) | `1000` |
|
|
168
|
+
| `SUB_AGENT_MAX_STEPS` | 子 Agent 循环安全上限,默认与主 Agent 一致 | `1000` |
|
|
115
169
|
| `SANDBOX_ROOT` | 沙箱根目录(文件操作边界;未配则用 cwd 兜底) | 无 |
|
|
116
170
|
| `MOCODE_THEME` | 颜色主题(default/dark/light…;shell 设置优先于文件) | `default` |
|
|
117
171
|
|
|
@@ -147,7 +201,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
147
201
|
| `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
|
|
148
202
|
| `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
|
|
149
203
|
| `drop_context` | 把历史里无关的旧工具结果替换为存根释放上下文(保 tool_call_id 配对,不动 system 与当前轮;幂等) |
|
|
150
|
-
| `
|
|
204
|
+
| `sub-agent` | 派生具备完整能力的隔离子 Agent;只读任务可并发,写任务通过 overlay + ChangeSet 安全合并 |
|
|
151
205
|
|
|
152
206
|
| `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
|
|
153
207
|
| `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
|
package/dist/agent/core.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getPlanDisabledTools, getRuntimeDisabledTools } from '../tools/constant
|
|
|
13
13
|
import { getAgentMode, setAgentMode } from './mode.js';
|
|
14
14
|
import { maybeCompact, contextState, dropContextFromHistory, createTraceEvent, summarizeToolArguments, safeProviderId, } from '../session/index.js';
|
|
15
15
|
import { createBudgetScheduler } from '../session/scheduler.js';
|
|
16
|
-
import { optimizeToolResult, HOT_TURN_WINDOW, userTurnBoundary } from '../context/index.js';
|
|
16
|
+
import { optimizeToolResult, HOT_TURN_WINDOW, userTurnBoundary, recordArtifact, invalidateArtifacts, rehydrateArtifacts, } from '../context/index.js';
|
|
17
17
|
import { createAgeAwareEncodingState, } from '../context/age-aware.js';
|
|
18
18
|
import { createRelevancePruner } from '../context/relevance.js';
|
|
19
19
|
import { isToolResultSuccess } from '../context/utils.js';
|
|
@@ -51,7 +51,7 @@ function parseArgs(raw) {
|
|
|
51
51
|
* (直接拼,不哈希——避免热路径开销;args 长度本身有限,内存压力可忽略)。
|
|
52
52
|
* null 表示未触发,不污染输出。
|
|
53
53
|
*/
|
|
54
|
-
const THRASH_THRESHOLD =
|
|
54
|
+
const THRASH_THRESHOLD = 2;
|
|
55
55
|
function thrashHint(name, args, count) {
|
|
56
56
|
if (count < THRASH_THRESHOLD)
|
|
57
57
|
return null;
|
|
@@ -59,9 +59,34 @@ function thrashHint(name, args, count) {
|
|
|
59
59
|
'either failing or returning the same content. STOP retrying and switch strategy:\n' +
|
|
60
60
|
'- read_file / glob → path likely wrong; call `glob` to discover paths, or `ask_human`\n' +
|
|
61
61
|
'- run_command → Windows path-escaping issue; use `read_file` / `glob` with absolute paths instead\n' +
|
|
62
|
-
'- edit_file →
|
|
62
|
+
'- write_file / edit_file CHANGE_CONFLICT → do not resend; read_file the same path and use its latest hash (use null only when read_file says the path is missing)\n' +
|
|
63
|
+
'- edit_file old_string mismatch → re-read the exact region and copy it verbatim\n' +
|
|
63
64
|
'- otherwise → re-read the tool description; the argument shape may be wrong');
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Detect only a strict streak of identical failures. Successful calls, or a
|
|
68
|
+
* different tool/argument pair, end the streak. This keeps intentional phased
|
|
69
|
+
* read_file/glob calls from being mislabeled after the underlying files change.
|
|
70
|
+
*/
|
|
71
|
+
export function createThrashTracker() {
|
|
72
|
+
let lastFailedFingerprint = null;
|
|
73
|
+
let consecutiveFailures = 0;
|
|
74
|
+
return (name, args, succeeded) => {
|
|
75
|
+
if (succeeded) {
|
|
76
|
+
lastFailedFingerprint = null;
|
|
77
|
+
consecutiveFailures = 0;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const fingerprint = `${name}\x00${args}`;
|
|
81
|
+
if (fingerprint === lastFailedFingerprint)
|
|
82
|
+
consecutiveFailures += 1;
|
|
83
|
+
else {
|
|
84
|
+
lastFailedFingerprint = fingerprint;
|
|
85
|
+
consecutiveFailures = 1;
|
|
86
|
+
}
|
|
87
|
+
return thrashHint(name, args, consecutiveFailures);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
65
90
|
/** 只有显式声明 parallel 且无需权限确认的工具才进入普通并发组。 */
|
|
66
91
|
function isParallelTool(name) {
|
|
67
92
|
const tool = tools.find((candidate) => candidate.name === name);
|
|
@@ -73,6 +98,15 @@ function isResourceLockedTool(name) {
|
|
|
73
98
|
const tool = tools.find((candidate) => candidate.name === name);
|
|
74
99
|
return !!tool && getToolCapabilities(tool).concurrency === 'resource-locked';
|
|
75
100
|
}
|
|
101
|
+
function isResourceLockedCall(call) {
|
|
102
|
+
if (!isResourceLockedTool(call.name))
|
|
103
|
+
return false;
|
|
104
|
+
if (call.name !== 'sub-agent')
|
|
105
|
+
return true;
|
|
106
|
+
const args = parseArgs(call.arguments);
|
|
107
|
+
// Unknown write sets stay on the serial path. Read tasks and known disjoint write sets may batch.
|
|
108
|
+
return args?.mode !== 'write' || (Array.isArray(args.writeSet) && args.writeSet.length > 0);
|
|
109
|
+
}
|
|
76
110
|
/** 文件 mutation 由 capability metadata 判定,供 diff、回滚与上下文失效共用。 */
|
|
77
111
|
const isMutationTool = (name) => isFileMutationTool(name);
|
|
78
112
|
function deniedOutcome(name) {
|
|
@@ -142,11 +176,13 @@ function pushToolResult(history, tc, output, pruner, lifecycle, _scheduler, runt
|
|
|
142
176
|
content: optimizeToolResult(tc.name, output, tc.arguments, encodingContext),
|
|
143
177
|
};
|
|
144
178
|
history.push(msg);
|
|
179
|
+
const messageIndex = history.length - 1;
|
|
180
|
+
recordArtifact(runtimeContextState, history, messageIndex, output, succeeded);
|
|
145
181
|
// 失败 read 不得淘汰旧 read;失败 consumer 也不能改变 lifecycle 上游状态。
|
|
146
182
|
if (pruner)
|
|
147
183
|
pruner.observePush(history, msg, succeeded);
|
|
148
184
|
if (lifecycle)
|
|
149
|
-
lifecycle.pushTool(history,
|
|
185
|
+
lifecycle.pushTool(history, messageIndex, succeeded);
|
|
150
186
|
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
151
187
|
}
|
|
152
188
|
/**
|
|
@@ -219,15 +255,10 @@ export async function runAgentCore(opts) {
|
|
|
219
255
|
}
|
|
220
256
|
: u;
|
|
221
257
|
};
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const recordAndHint = (
|
|
226
|
-
const fp = `${name}\x00${args}`;
|
|
227
|
-
const c = (recentToolCalls.get(fp) ?? 0) + 1;
|
|
228
|
-
recentToolCalls.set(fp, c);
|
|
229
|
-
return thrashHint(name, args, c);
|
|
230
|
-
};
|
|
258
|
+
const addToolUsage = (outcome) => addUsage(outcome.usage);
|
|
259
|
+
// Only consecutive identical failures are thrashing. Any success (including
|
|
260
|
+
// a mutation between two reads) or different call resets the streak.
|
|
261
|
+
const recordAndHint = createThrashTracker();
|
|
231
262
|
history.push({ role: 'user', content: userInput });
|
|
232
263
|
// 中断回滚快照:push 用户消息后整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
233
264
|
// 这样中断时至少保留用户消息(及之前的历史);每步工具全部执行完毕后刷新快照,
|
|
@@ -250,6 +281,7 @@ export async function runAgentCore(opts) {
|
|
|
250
281
|
? createLifecycleEngine(history)
|
|
251
282
|
: null;
|
|
252
283
|
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
284
|
+
rehydrateArtifacts(runtimeContextState, history);
|
|
253
285
|
// 预算调度器:每个 runAgentCore 实例一个,在 age-aware sweep 后评估并执行 warn / compact。
|
|
254
286
|
// contextBudget 开关关闭时为 null。
|
|
255
287
|
const scheduler = config.contextBudget !== false
|
|
@@ -310,6 +342,7 @@ export async function runAgentCore(opts) {
|
|
|
310
342
|
completed: false,
|
|
311
343
|
terminationReason: 'aborted',
|
|
312
344
|
finalText: null,
|
|
345
|
+
usage: turnUsage,
|
|
313
346
|
validation: latestValidation,
|
|
314
347
|
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
315
348
|
};
|
|
@@ -362,6 +395,7 @@ export async function runAgentCore(opts) {
|
|
|
362
395
|
runtimeContextState.lifecycleStats = lifecycle.stats();
|
|
363
396
|
}
|
|
364
397
|
ageAware?.rehydrate(history);
|
|
398
|
+
rehydrateArtifacts(runtimeContextState, history);
|
|
365
399
|
}
|
|
366
400
|
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
367
401
|
mode = 'idle';
|
|
@@ -410,6 +444,7 @@ export async function runAgentCore(opts) {
|
|
|
410
444
|
completed: false,
|
|
411
445
|
terminationReason: 'aborted',
|
|
412
446
|
finalText: null,
|
|
447
|
+
usage: turnUsage,
|
|
413
448
|
validation: latestValidation,
|
|
414
449
|
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
415
450
|
};
|
|
@@ -507,6 +542,9 @@ export async function runAgentCore(opts) {
|
|
|
507
542
|
retry: Math.max(0, (outcome.attempts ?? 1) - 1),
|
|
508
543
|
retryDelayMs: outcome.retryDelayMs ?? 0,
|
|
509
544
|
changedFiles: outcome.changedFiles ?? [],
|
|
545
|
+
staleFiles: outcome.staleFiles ?? [],
|
|
546
|
+
...(outcome.changeSet ? { changeSet: outcome.changeSet } : {}),
|
|
547
|
+
...(outcome.usage ? { nestedUsage: outcome.usage } : {}),
|
|
510
548
|
}, {
|
|
511
549
|
toolCallId: traceCall.toolCallId,
|
|
512
550
|
...(tc.id ? { providerToolCallId: tc.id } : {}),
|
|
@@ -527,7 +565,7 @@ export async function runAgentCore(opts) {
|
|
|
527
565
|
durationMs: 0,
|
|
528
566
|
};
|
|
529
567
|
hooks.onToolResult?.(currentCall, error, null, null, 1);
|
|
530
|
-
const hint = recordAndHint(currentCall.name, currentCall.arguments);
|
|
568
|
+
const hint = recordAndHint(currentCall.name, currentCall.arguments, false);
|
|
531
569
|
pushToolResult(history, currentCall, hint ? `${error}${hint}` : error, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
532
570
|
traceToolEnd(currentCall, i, outcome);
|
|
533
571
|
i++;
|
|
@@ -553,23 +591,25 @@ export async function runAgentCore(opts) {
|
|
|
553
591
|
for (let k = 0; k < batch.length; k++) {
|
|
554
592
|
const tc = batch[k];
|
|
555
593
|
const outcome = await started[k];
|
|
594
|
+
addToolUsage(outcome);
|
|
595
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
556
596
|
traceToolEnd(tc, i + k, outcome);
|
|
557
597
|
const output = outcome.output;
|
|
558
598
|
hooks.onToolResult?.(tc, output, null, null, 1); // 并行工具无 diff
|
|
559
599
|
// Thrashing:history 里附 hint(UI 已用干净 output 渲染,避免屏幕噪声)
|
|
560
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
600
|
+
const hint = recordAndHint(tc.name, tc.arguments, outcome.status === 'success');
|
|
561
601
|
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
562
602
|
}
|
|
563
603
|
hooks.onToolDone?.();
|
|
564
604
|
i = j;
|
|
565
605
|
}
|
|
566
|
-
else if (
|
|
606
|
+
else if (isResourceLockedCall(currentCall) &&
|
|
567
607
|
!(getAgentMode() === 'plan' && getPlanDisabledTools().has(currentCall.name))) {
|
|
568
608
|
// 连续文件 mutation:权限确认仍严格按原序进行;全部 preflight 完成后再启动。
|
|
569
609
|
// 每个执行在 registry 内按 canonical path 获取锁,不同文件可并发,同文件别名会排队。
|
|
570
610
|
let j = i;
|
|
571
611
|
while (j < calls.length &&
|
|
572
|
-
|
|
612
|
+
isResourceLockedCall(calls[j]) &&
|
|
573
613
|
!getRuntimeDisabledTools().has(calls[j].name) &&
|
|
574
614
|
!(getAgentMode() === 'plan' && getPlanDisabledTools().has(calls[j].name)))
|
|
575
615
|
j++;
|
|
@@ -621,17 +661,23 @@ export async function runAgentCore(opts) {
|
|
|
621
661
|
for (let k = 0; k < entries.length; k++) {
|
|
622
662
|
const entry = entries[k];
|
|
623
663
|
const outcome = await started[k];
|
|
664
|
+
addToolUsage(outcome);
|
|
665
|
+
opts.onToolOutcome?.(entry.tc.name, entry.parsed ?? {}, outcome);
|
|
624
666
|
traceToolEnd(entry.tc, i + k, outcome);
|
|
625
667
|
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
626
|
-
const hint = recordAndHint(entry.tc.name, entry.tc.arguments);
|
|
668
|
+
const hint = recordAndHint(entry.tc.name, entry.tc.arguments, outcome.status === 'success');
|
|
627
669
|
pushToolResult(history, entry.tc, hint ? `${outcome.output}${hint}` : outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
670
|
+
const invalidatedFiles = [...new Set([
|
|
671
|
+
...(outcome.changedFiles ?? []),
|
|
672
|
+
...(outcome.staleFiles ?? []),
|
|
673
|
+
])];
|
|
674
|
+
if (invalidatedFiles.length > 0) {
|
|
675
|
+
for (const changedFile of invalidatedFiles) {
|
|
676
|
+
relprune?.observeMutation(history, changedFile);
|
|
677
|
+
lifecycle?.pushMutation(history, history.length - 1, changedFile);
|
|
634
678
|
}
|
|
679
|
+
invalidateArtifacts(runtimeContextState, history, invalidatedFiles);
|
|
680
|
+
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
635
681
|
}
|
|
636
682
|
}
|
|
637
683
|
if (firstAllowed)
|
|
@@ -656,7 +702,7 @@ export async function runAgentCore(opts) {
|
|
|
656
702
|
};
|
|
657
703
|
hooks.onToolResult?.(tc, err, null, null, 1);
|
|
658
704
|
// Thrashing:同上
|
|
659
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
705
|
+
const hint = recordAndHint(tc.name, tc.arguments, false);
|
|
660
706
|
pushToolResult(history, tc, hint ? `${err}${hint}` : err, relprune, lifecycle, scheduler);
|
|
661
707
|
traceToolEnd(tc, i, outcome);
|
|
662
708
|
i++;
|
|
@@ -684,7 +730,7 @@ export async function runAgentCore(opts) {
|
|
|
684
730
|
hooks.onToolHeader?.(tc);
|
|
685
731
|
const outcome = deniedOutcome(tc.name);
|
|
686
732
|
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
687
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
733
|
+
const hint = recordAndHint(tc.name, tc.arguments, false);
|
|
688
734
|
pushToolResult(history, tc, hint ? `${outcome.output}${hint}` : outcome.output, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
689
735
|
traceToolEnd(tc, i, outcome);
|
|
690
736
|
i++;
|
|
@@ -705,21 +751,26 @@ export async function runAgentCore(opts) {
|
|
|
705
751
|
},
|
|
706
752
|
onRetry: (retry) => traceToolRetry(tc, i, retry),
|
|
707
753
|
});
|
|
754
|
+
addToolUsage(outcome);
|
|
755
|
+
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
708
756
|
traceToolEnd(tc, i, outcome);
|
|
709
757
|
const output = outcome.output;
|
|
710
758
|
hooks.onToolDone?.();
|
|
711
759
|
hooks.onToolResult?.(tc, output, mutationParsed, diff.preWriteOld, diff.editStartLine);
|
|
712
760
|
// Thrashing:同上(history 附 hint,UI 干净)
|
|
713
|
-
const hint = recordAndHint(tc.name, tc.arguments);
|
|
761
|
+
const hint = recordAndHint(tc.name, tc.arguments, outcome.status === 'success');
|
|
714
762
|
pushToolResult(history, tc, hint ? `${output}${hint}` : output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
763
|
+
const invalidatedFiles = [...new Set([
|
|
764
|
+
...(outcome.changedFiles ?? []),
|
|
765
|
+
...(outcome.staleFiles ?? []),
|
|
766
|
+
])];
|
|
767
|
+
if (invalidatedFiles.length > 0) {
|
|
768
|
+
for (const changedFile of invalidatedFiles) {
|
|
769
|
+
relprune?.observeMutation(history, changedFile);
|
|
770
|
+
lifecycle?.pushMutation(history, history.length - 1, changedFile);
|
|
722
771
|
}
|
|
772
|
+
invalidateArtifacts(runtimeContextState, history, invalidatedFiles);
|
|
773
|
+
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
723
774
|
}
|
|
724
775
|
i++;
|
|
725
776
|
}
|
|
@@ -829,6 +880,7 @@ export async function runAgentCore(opts) {
|
|
|
829
880
|
completed: false,
|
|
830
881
|
terminationReason: 'aborted',
|
|
831
882
|
finalText: null,
|
|
883
|
+
usage: turnUsage,
|
|
832
884
|
validation: latestValidation,
|
|
833
885
|
changedFiles: mutation.changedFiles.map((item) => item.path),
|
|
834
886
|
};
|
package/dist/agent/index.js
CHANGED
|
@@ -17,6 +17,44 @@ import { isToolErrorOutput } from '../tools/result.js';
|
|
|
17
17
|
import { appendCurrentSessionTraceEvent } from '../session/index.js';
|
|
18
18
|
/** 当前 turn 的 batch id(runAgent 内闭包变量;一条 turn 一轮 tool batch 结束即清空)。 */
|
|
19
19
|
let currentBatchId = null;
|
|
20
|
+
let turnFileChanges = [];
|
|
21
|
+
function lineDelta(oldText, newText) {
|
|
22
|
+
const before = oldText ? oldText.split('\n') : [];
|
|
23
|
+
const after = newText ? newText.split('\n') : [];
|
|
24
|
+
let head = 0;
|
|
25
|
+
while (head < before.length && head < after.length && before[head] === after[head])
|
|
26
|
+
head++;
|
|
27
|
+
let tail = 0;
|
|
28
|
+
while (tail < before.length - head
|
|
29
|
+
&& tail < after.length - head
|
|
30
|
+
&& before[before.length - 1 - tail] === after[after.length - 1 - tail])
|
|
31
|
+
tail++;
|
|
32
|
+
return { added: after.length - head - tail, removed: before.length - head - tail };
|
|
33
|
+
}
|
|
34
|
+
function writeChangeOverview() {
|
|
35
|
+
if (turnFileChanges.length === 0)
|
|
36
|
+
return;
|
|
37
|
+
const merged = new Map();
|
|
38
|
+
for (const change of turnFileChanges) {
|
|
39
|
+
const current = merged.get(change.path);
|
|
40
|
+
if (current) {
|
|
41
|
+
current.added += change.added;
|
|
42
|
+
current.removed += change.removed;
|
|
43
|
+
if (change.kind === 'A')
|
|
44
|
+
current.kind = 'A';
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
merged.set(change.path, { ...change });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const changes = [...merged.values()];
|
|
51
|
+
const added = changes.reduce((n, c) => n + c.added, 0);
|
|
52
|
+
const removed = changes.reduce((n, c) => n + c.removed, 0);
|
|
53
|
+
layout.contentWrite(` ${ui.dim}├─${ui.reset} ${ui.bold}${ui.green}◆${ui.reset} ${t('agent.changes')} ${t('agent.files', { count: changes.length })} ${ui.green}+${added}${ui.reset} ${ui.red}−${removed}${ui.reset}\n`);
|
|
54
|
+
for (const change of changes) {
|
|
55
|
+
layout.contentWrite(` ${ui.dim}│ ${change.kind}${ui.reset} ${change.path} ${ui.green}+${change.added}${ui.reset} ${ui.red}−${change.removed}${ui.reset}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
20
58
|
/** 取 userInput 的首行:字符串直接 split;多模态 parts 找首个 text part 再 split。 */
|
|
21
59
|
function firstLineOf(ui) {
|
|
22
60
|
if (typeof ui === 'string')
|
|
@@ -43,7 +81,9 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
|
43
81
|
if (!currentBatchId)
|
|
44
82
|
return;
|
|
45
83
|
let diff = null;
|
|
46
|
-
if (
|
|
84
|
+
if ((tc.name === 'edit_file' || tc.name === 'write_file') && parsed && !isToolErrorOutput(output)) {
|
|
85
|
+
const oldText = tc.name === 'edit_file' ? String(parsed.old_string ?? '') : preWriteOld;
|
|
86
|
+
const newText = String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ?? '');
|
|
47
87
|
diff = renderFileChange({
|
|
48
88
|
path: String(parsed.path ?? ''),
|
|
49
89
|
kind: tc.name === 'edit_file' ? 'edit' : 'write',
|
|
@@ -53,9 +93,15 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
|
|
|
53
93
|
newStr: String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ?? ''),
|
|
54
94
|
startLine: tc.name === 'edit_file' ? editStartLine : 1,
|
|
55
95
|
});
|
|
96
|
+
turnFileChanges.push({
|
|
97
|
+
path: String(parsed.path ?? ''),
|
|
98
|
+
kind: tc.name === 'write_file' && preWriteOld == null ? 'A' : 'M',
|
|
99
|
+
...lineDelta(oldText, newText),
|
|
100
|
+
});
|
|
56
101
|
}
|
|
57
102
|
const preview = diff ? '' : summarizeToolResult(tc.name, output);
|
|
58
|
-
batch.recordResult(currentBatchId, tc.name, preview, diff, output);
|
|
103
|
+
batch.recordResult(currentBatchId, tc.name, preview, diff, output, isToolErrorOutput(output));
|
|
104
|
+
batch.showLiveBatch(currentBatchId, layout);
|
|
59
105
|
// mutation 结果(成功 diff 或错误输出)立即可见,并阻止后续普通工具并入这一批。
|
|
60
106
|
if (isMutationTool(tc.name))
|
|
61
107
|
flushToolBatch(true);
|
|
@@ -96,6 +142,7 @@ onContextUpdate) {
|
|
|
96
142
|
beginTurn(truncateDisplay(firstLineOf(userInput), 40));
|
|
97
143
|
layout.contentMode(); // 防御性:运行态光标归输入框光标位供 IME 锚定(enterRunningMode 已置,这里兜底)
|
|
98
144
|
currentBatchId = null; // 新 turn 清旧 batch id(防上 turn 残留)
|
|
145
|
+
turnFileChanges = [];
|
|
99
146
|
// spinner:状态行最前面转圈(思考中 / 生成 / 执行 工具时,状态栏 lead 位显帧 + 文字)。
|
|
100
147
|
// 经 setStatus 注入状态行(spinnerFrame + statusText),composeStatus 把帧 + 文字放 lead 位;
|
|
101
148
|
// 不画内容区续写位——内容区在等待期间保持干净,首 token 到达即从续写位开始写正文。
|
|
@@ -121,6 +168,8 @@ onContextUpdate) {
|
|
|
121
168
|
// batch 收尾已经统一留了一条空白行。部分后端会把下一段正文以 \n / \n\n
|
|
122
169
|
// 开头发来;去掉这些“边界换行”,避免与 UI 分隔叠成两条空白行。
|
|
123
170
|
const visible = followsToolBatch ? s.replace(/^(?:[ \t]*\r?\n)+/, '') : s;
|
|
171
|
+
// 正文是工具批次边界:只有“连续且中间没有正文”的工具调用才合并。
|
|
172
|
+
// 一旦模型开始解释阶段结果,立即收尾当前摘要;后续工具重新建立批次。
|
|
124
173
|
if (s)
|
|
125
174
|
flushToolBatch();
|
|
126
175
|
spinner.stop(); // 任何正文 token 都停 spinner(首 token 停「思考中」;onToolCall 重启后若又来文本则停「生成中」)。未旋转时 stop 为 no-op。
|
|
@@ -203,12 +252,14 @@ onContextUpdate) {
|
|
|
203
252
|
const detail = validation.status === 'skipped' && validation.skipReason
|
|
204
253
|
? `${validation.status}: ${validation.skipReason}`
|
|
205
254
|
: validation.status;
|
|
206
|
-
|
|
255
|
+
const symbol = validation.status === 'passed' ? '◆' : validation.status === 'failed' ? '×' : '!';
|
|
256
|
+
layout.contentWrite(` ${ui.dim}├─${ui.reset} ${color}${symbol}${ui.reset} ${t('agent.validationResult', { command, status: detail })}\n\n`);
|
|
207
257
|
},
|
|
208
258
|
onDone: (elapsedMs, usage) => {
|
|
209
259
|
flushToolBatch();
|
|
260
|
+
writeChangeOverview();
|
|
210
261
|
const tok = formatTurnTokens(usage);
|
|
211
|
-
layout.contentWrite(` ${ui.
|
|
262
|
+
layout.contentWrite(` ${ui.bold}${ui.green}◆${ui.reset} ${t('agent.complete')} ${fmtElapsed(elapsedMs)}${tok}\n`);
|
|
212
263
|
// 内容区触底时,DECSTBM 增量滚屏可能只推进物理终端,未把 Worked 前已在
|
|
213
264
|
// buffer 中的空行完整画出来;用户滚动/点击触发 repaint 后才“突然”出现。
|
|
214
265
|
// 轮次收尾立即按 buffer 原子重画,使未满屏与触底滚屏的布局一致。
|
|
@@ -279,9 +330,9 @@ function formatTurnTokens(usage) {
|
|
|
279
330
|
const billablePrompt = usage.promptTokens - cached;
|
|
280
331
|
const extras = [];
|
|
281
332
|
if (cached > 0)
|
|
282
|
-
extras.push(
|
|
333
|
+
extras.push(`${Math.round((cached / Math.max(1, usage.promptTokens)) * 100)}% cached`);
|
|
283
334
|
if (reasoning > 0)
|
|
284
335
|
extras.push(`reasoning ${fmt(reasoning)}`);
|
|
285
336
|
const extrasStr = extras.length > 0 ? ` · ${extras.join(' · ')}` : '';
|
|
286
|
-
return `
|
|
337
|
+
return ` ${fmt(total)} tokens${extrasStr} ${ui.dim}(↑ ${fmt(billablePrompt)} ↓ ${fmt(usage.completionTokens)})${ui.reset}`;
|
|
287
338
|
}
|