@deepwhale/coding-agent 1.0.11 → 1.0.12

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.
Files changed (150) hide show
  1. package/package.json +1 -1
  2. package/dist/agent/agent-compaction.d.ts +0 -74
  3. package/dist/agent/agent-compaction.d.ts.map +0 -1
  4. package/dist/agent/agent-compaction.js +0 -145
  5. package/dist/agent/agent-compaction.js.map +0 -1
  6. package/dist/agent/index.d.ts +0 -16
  7. package/dist/agent/index.d.ts.map +0 -1
  8. package/dist/agent/index.js +0 -17
  9. package/dist/agent/index.js.map +0 -1
  10. package/dist/agent/session-adapter.d.ts +0 -177
  11. package/dist/agent/session-adapter.d.ts.map +0 -1
  12. package/dist/agent/session-adapter.js +0 -365
  13. package/dist/agent/session-adapter.js.map +0 -1
  14. package/dist/agent/tool-loop.d.ts +0 -123
  15. package/dist/agent/tool-loop.d.ts.map +0 -1
  16. package/dist/agent/tool-loop.js +0 -436
  17. package/dist/agent/tool-loop.js.map +0 -1
  18. package/dist/env/load-project-env.d.ts +0 -40
  19. package/dist/env/load-project-env.d.ts.map +0 -1
  20. package/dist/env/load-project-env.js +0 -80
  21. package/dist/env/load-project-env.js.map +0 -1
  22. package/dist/index.d.ts +0 -32
  23. package/dist/index.d.ts.map +0 -1
  24. package/dist/index.js +0 -33
  25. package/dist/index.js.map +0 -1
  26. package/dist/llm-factory.d.ts +0 -50
  27. package/dist/llm-factory.d.ts.map +0 -1
  28. package/dist/llm-factory.js +0 -110
  29. package/dist/llm-factory.js.map +0 -1
  30. package/dist/modes/index.d.ts +0 -14
  31. package/dist/modes/index.d.ts.map +0 -1
  32. package/dist/modes/index.js +0 -14
  33. package/dist/modes/index.js.map +0 -1
  34. package/dist/modes/print.d.ts +0 -50
  35. package/dist/modes/print.d.ts.map +0 -1
  36. package/dist/modes/print.js +0 -236
  37. package/dist/modes/print.js.map +0 -1
  38. package/dist/modes/rpc.d.ts +0 -52
  39. package/dist/modes/rpc.d.ts.map +0 -1
  40. package/dist/modes/rpc.js +0 -316
  41. package/dist/modes/rpc.js.map +0 -1
  42. package/dist/modes/tui.d.ts +0 -112
  43. package/dist/modes/tui.d.ts.map +0 -1
  44. package/dist/modes/tui.js +0 -733
  45. package/dist/modes/tui.js.map +0 -1
  46. package/dist/policy/args-digest.d.ts +0 -13
  47. package/dist/policy/args-digest.d.ts.map +0 -1
  48. package/dist/policy/args-digest.js +0 -29
  49. package/dist/policy/args-digest.js.map +0 -1
  50. package/dist/policy/chain.d.ts +0 -19
  51. package/dist/policy/chain.d.ts.map +0 -1
  52. package/dist/policy/chain.js +0 -24
  53. package/dist/policy/chain.js.map +0 -1
  54. package/dist/policy/index.d.ts +0 -17
  55. package/dist/policy/index.d.ts.map +0 -1
  56. package/dist/policy/index.js +0 -16
  57. package/dist/policy/index.js.map +0 -1
  58. package/dist/policy/sanitize-reason.d.ts +0 -11
  59. package/dist/policy/sanitize-reason.d.ts.map +0 -1
  60. package/dist/policy/sanitize-reason.js +0 -24
  61. package/dist/policy/sanitize-reason.js.map +0 -1
  62. package/dist/policy/static-rules.d.ts +0 -32
  63. package/dist/policy/static-rules.d.ts.map +0 -1
  64. package/dist/policy/static-rules.js +0 -106
  65. package/dist/policy/static-rules.js.map +0 -1
  66. package/dist/policy/types.d.ts +0 -56
  67. package/dist/policy/types.d.ts.map +0 -1
  68. package/dist/policy/types.js +0 -13
  69. package/dist/policy/types.js.map +0 -1
  70. package/dist/repl/repl-confirm.d.ts +0 -49
  71. package/dist/repl/repl-confirm.d.ts.map +0 -1
  72. package/dist/repl/repl-confirm.js +0 -88
  73. package/dist/repl/repl-confirm.js.map +0 -1
  74. package/dist/repl.d.ts +0 -154
  75. package/dist/repl.d.ts.map +0 -1
  76. package/dist/repl.js +0 -780
  77. package/dist/repl.js.map +0 -1
  78. package/dist/sandbox/docker-runner.d.ts +0 -147
  79. package/dist/sandbox/docker-runner.d.ts.map +0 -1
  80. package/dist/sandbox/docker-runner.js +0 -426
  81. package/dist/sandbox/docker-runner.js.map +0 -1
  82. package/dist/sandbox/env-gate.d.ts +0 -28
  83. package/dist/sandbox/env-gate.d.ts.map +0 -1
  84. package/dist/sandbox/env-gate.js +0 -65
  85. package/dist/sandbox/env-gate.js.map +0 -1
  86. package/dist/sandbox/local-runner.d.ts +0 -29
  87. package/dist/sandbox/local-runner.d.ts.map +0 -1
  88. package/dist/sandbox/local-runner.js +0 -79
  89. package/dist/sandbox/local-runner.js.map +0 -1
  90. package/dist/sandbox/types.d.ts +0 -80
  91. package/dist/sandbox/types.d.ts.map +0 -1
  92. package/dist/sandbox/types.js +0 -25
  93. package/dist/sandbox/types.js.map +0 -1
  94. package/dist/tools/bash.d.ts +0 -35
  95. package/dist/tools/bash.d.ts.map +0 -1
  96. package/dist/tools/bash.js +0 -233
  97. package/dist/tools/bash.js.map +0 -1
  98. package/dist/tools/edit-file.d.ts +0 -22
  99. package/dist/tools/edit-file.d.ts.map +0 -1
  100. package/dist/tools/edit-file.js +0 -79
  101. package/dist/tools/edit-file.js.map +0 -1
  102. package/dist/tools/find.d.ts +0 -21
  103. package/dist/tools/find.d.ts.map +0 -1
  104. package/dist/tools/find.js +0 -168
  105. package/dist/tools/find.js.map +0 -1
  106. package/dist/tools/grep.d.ts +0 -19
  107. package/dist/tools/grep.d.ts.map +0 -1
  108. package/dist/tools/grep.js +0 -170
  109. package/dist/tools/grep.js.map +0 -1
  110. package/dist/tools/index.d.ts +0 -10
  111. package/dist/tools/index.d.ts.map +0 -1
  112. package/dist/tools/index.js +0 -10
  113. package/dist/tools/index.js.map +0 -1
  114. package/dist/tools/read-file.d.ts +0 -18
  115. package/dist/tools/read-file.d.ts.map +0 -1
  116. package/dist/tools/read-file.js +0 -52
  117. package/dist/tools/read-file.js.map +0 -1
  118. package/dist/tools/registry.d.ts +0 -39
  119. package/dist/tools/registry.d.ts.map +0 -1
  120. package/dist/tools/registry.js +0 -67
  121. package/dist/tools/registry.js.map +0 -1
  122. package/dist/tools/write-file.d.ts +0 -18
  123. package/dist/tools/write-file.d.ts.map +0 -1
  124. package/dist/tools/write-file.js +0 -47
  125. package/dist/tools/write-file.js.map +0 -1
  126. package/dist/tui-ink-bundle.js +0 -39104
  127. package/dist/types.d.ts +0 -89
  128. package/dist/types.d.ts.map +0 -1
  129. package/dist/types.js +0 -5
  130. package/dist/types.js.map +0 -1
  131. package/dist/util/index.d.ts +0 -16
  132. package/dist/util/index.d.ts.map +0 -1
  133. package/dist/util/index.js +0 -16
  134. package/dist/util/index.js.map +0 -1
  135. package/dist/util/tui-history.d.ts +0 -37
  136. package/dist/util/tui-history.d.ts.map +0 -1
  137. package/dist/util/tui-history.js +0 -93
  138. package/dist/util/tui-history.js.map +0 -1
  139. package/dist/verify/format-report.d.ts +0 -57
  140. package/dist/verify/format-report.d.ts.map +0 -1
  141. package/dist/verify/format-report.js +0 -128
  142. package/dist/verify/format-report.js.map +0 -1
  143. package/dist/verify/index.d.ts +0 -8
  144. package/dist/verify/index.d.ts.map +0 -1
  145. package/dist/verify/index.js +0 -8
  146. package/dist/verify/index.js.map +0 -1
  147. package/dist/verify/verify-runner.d.ts +0 -186
  148. package/dist/verify/verify-runner.d.ts.map +0 -1
  149. package/dist/verify/verify-runner.js +0 -707
  150. package/dist/verify/verify-runner.js.map +0 -1
package/dist/repl.js DELETED
@@ -1,780 +0,0 @@
1
- /**
2
- * deepwhale REPL — Sprint 1a 接入 tool loop + session
3
- *
4
- * Sprint 0.3 范围:单轮 chat + 内建命令。
5
- * Sprint 1a 扩展:
6
- * - 接 Session JSONL:启动时 load(可选路径),退出时 close(flush)
7
- * - 接 runToolLoop:每轮 user → tool loop → 持久化 steps
8
- * - 流式:onChunk 实时打印 final.content(assistant 增量)
9
- * - 命令:保留 /help / /exit / exit / quit
10
- *
11
- * Sprint 1a 简化:
12
- * - 不做 plan mode、recovery、自动压缩
13
- * - 不做 multi-session 切换
14
- * - 错误用 i18n + 不污染 messages
15
- *
16
- * 文件职责:
17
- * - runOneTurn: 仍保留为低层单轮 API(不持久化,无 tool loop)
18
- * - startRepl: 接 tool loop + session 的入口
19
- */
20
- import { createInterface } from 'node:readline';
21
- import { stdin, stdout, stderr } from 'node:process';
22
- import { t } from '@deepwhale/core';
23
- import { SessionReader, SessionWriter } from '@deepwhale/core';
24
- import { APIKeyMissingError, isLLMError, LLMAuthError, LLMNetworkError, LLMRateLimitError, LLMStreamError, LLMUnknownError, } from '@deepwhale/llm';
25
- import { isToolLoopError, loadSession, persistToolLoopSteps, runToolLoop, runToolLoopWithCompaction, appendVerificationEvent, ToolLoopLimitError, } from './agent/index.js';
26
- import { CompactionState } from '@deepwhale/core';
27
- import { createDefaultRegistry } from './tools/registry.js';
28
- import { createDefaultClient } from './llm-factory.js';
29
- import { buildSummaryAndNext, formatReport, runVerify } from './verify/index.js';
30
- import { resolveSandboxRunnerFromEnv } from './sandbox/env-gate.js';
31
- import { staticToolPolicy } from './policy/static-rules.js';
32
- import { createReplConfirm } from './repl/repl-confirm.js'; // D-15: REPL y/N confirm 工厂
33
- export { createReplConfirm } from './repl/repl-confirm.js'; // Sprint 1c-revive-2-D-24.2: re-export for tui-ink
34
- const VERSION = '0.1.0';
35
- /**
36
- * 单轮 chat 工具函数:把 user 输入 → LLM chat → 输出 assistant 文本。
37
- * 不修改 messages;不调工具;不持久化。
38
- *
39
- * Sprint 1a 保留作为低层 API。Sprint 1a 之后 REPL 入口推荐用 startRepl +
40
- * enableToolLoop=true 走完整 agent loop。
41
- */
42
- export async function runOneTurn(client, line, messages, options = {}) {
43
- const trimmed = line.trim();
44
- if (trimmed === '')
45
- return { kind: 'empty' };
46
- const userMessage = { role: 'user', content: trimmed };
47
- const allMessages = [...messages, userMessage];
48
- try {
49
- const result = await client.chat(allMessages, options.signal !== undefined ? { signal: options.signal } : {});
50
- return { kind: 'chat', assistant: result.content };
51
- }
52
- catch (e) {
53
- return { kind: 'error', error: formatError(e) };
54
- }
55
- }
56
- /**
57
- * 启动 REPL。返回 Promise,resolve 时为退出码。
58
- */
59
- export async function startRepl(options = {}) {
60
- const out = options.output ?? stdout;
61
- const err = options.errorOutput ?? stderr;
62
- // Sprint 1b.5 Step 2 (2.5 C3 拍板): provider 由 options.client / options.provider / env 推断
63
- // - client 显式给 → 走 client (单测路径)
64
- // - client 未给 + provider 显式给 → 走 createDefaultClient({provider})
65
- // - client 未给 + provider 未给 → 走 createDefaultClient() (env 推断 + 双设报错)
66
- // 任何抛 APIKeyMissingError 都被 catch 后写到 stderr (跟 1b 时代行为一致)
67
- //
68
- // Sprint 1c-revive-2-D-11-4 review P1 修复 (2026-06-04): **lazy** client 初始化.
69
- // 之前 146 行抢创 createDefaultClient() 在无 LLM key 时抛 APIKeyMissingError,
70
- // REPL 根本进不去 → 跟 README "/verify 不依赖 key" 承诺冲突. 修复:
71
- // 1. options.client 显式给 → 立即 bind (单测路径不变)
72
- // 2. options.client 未给 → 走 tryCreateClient, 失败存 clientError, 不抛
73
- // 3. /verify 路径完全跳过 client 引用 (跟 deepwhale --verify 同语义)
74
- // 4. chat 路径首次调 getClient() 时才真创, clientError 走 i18n 输出
75
- const clientFromOptions = options.client;
76
- let client = clientFromOptions ?? null;
77
- let clientError = clientFromOptions ? null : null;
78
- // Sprint 1c-revive-2-D-21.1 (2026-06-06, 修默认走 Anthropic 误判 bug):
79
- // tryCreateClient 之前 catch 静默存 clientError, stderr 啥都不说, 用户根本
80
- // 不知道. 现在 catch 时显式 stderr 写一行 [deepwhale] init error, 跟 chat
81
- // 路径 (L494 error.api_key_missing) 互补. 走 createDefaultClient → resolveProvider
82
- // "Both set" 错时, 显式 message 让用户立刻看到 "改用 --provider 决断".
83
- let initErrorReported = false;
84
- const tryCreateClient = () => {
85
- if (clientFromOptions)
86
- return { client: clientFromOptions, error: null };
87
- if (client !== null || clientError !== null) {
88
- return { client, error: clientError };
89
- }
90
- try {
91
- const c = createDefaultClient({
92
- ...(options.provider !== undefined ? { provider: options.provider } : {}),
93
- ...(options.model !== undefined ? { model: options.model } : {}),
94
- });
95
- client = c;
96
- clientError = null;
97
- return { client: c, error: null };
98
- }
99
- catch (e) {
100
- const err = e instanceof Error ? e : new Error(String(e));
101
- clientError = err;
102
- if (!initErrorReported) {
103
- initErrorReported = true;
104
- // 走 stderr, 跟 chat 路径保持一致, 避免用户看不到. 拍板: message 含
105
- // 原始 err.message (有 "Both set" 关键信息), 不强行套 i18n (i18n 太泛
106
- // 用户看不出是 Both set 还是 No key).
107
- stderr.write(`[deepwhale] init error: ${err.message}\n`);
108
- }
109
- return { client: null, error: err };
110
- }
111
- };
112
- // Sprint 1c-revive-2-D-6 (review P2 修复, 2026-06-04): 拿掉 anthropic × tool loop
113
- // 温柔降级. 拍板: D-4 (commit 80d3fd7/bbf1bf6) 已实装 AnthropicClient tool
114
- // schema 转换 (toAnthropicMessages 合并连续 tool 消息), --provider anthropic
115
- // 选了 anthropic 就该跑 tool loop. 旧 1b.5 Step 2.5 时代 'Sprint 1b.5 does not
116
- // support tool loop' 拍板已废 (Step 2.5 → 1c, Anthropic tool protocol 已 ship).
117
- // 兜底: requestedToolLoop 默认 true, caller 显式 false 才不跑.
118
- const enableToolLoop = options.enableToolLoop ?? true;
119
- const sessionPath = options.sessionPath;
120
- // Sprint 1c-revive-3-D-12 review P1 修复 (2026-06-05): 入口解析 sandbox env.
121
- // 未知值 throw (fail-closed), 由 CLI `main().catch` 写到 stderr + exit 1.
122
- const sandboxRunner = resolveSandboxRunnerFromEnv({ sandboxRoot: process.cwd() });
123
- // Sprint 1c-revive-3-D-13: REPL = 交互模式 (isInteractive=true), --yes 标志透传.
124
- const policyYes = options.yes ?? false;
125
- // Sprint 1c-revive-3-D-19 (2026-06-05): P1 修法 — 不再开第二个 readline 抢同一 input.
126
- // createReplConfirm 现在返回 controller (confirm + offerLine + hasPending + dismiss),
127
- // 主 rl.on('line') 是 stdin 唯一消费者, 确认期间用 offerLine() 串行化.
128
- // 拍板 (D-19): 单 readline 路径, 删 D-15 R-1 "子 rl 短窗口" 妥协.
129
- // 拍板红线: --yes 永远先于 confirm (D-13.5 P1 重排), replPolicy.confirm 只在 yes=false
130
- // 才被 tool-loop 调. runAgentTurn 加可选 policy 参数透传 (默认 staticToolPolicy 向后兼容).
131
- const confirmController = createReplConfirm({
132
- output: options.output ?? stdout,
133
- });
134
- const replPolicy = {
135
- ...staticToolPolicy,
136
- confirm: confirmController.confirm,
137
- };
138
- // greeting — Sprint 1c-revive-2-D-11-4 review P1 修复: 不依赖 client.model (lazy 化后
139
- // client 可能未创). 真创只在 chat 首次发生; 创失败 i18n 错误到 stderr. 这里 greeting
140
- // 只显示 ready + 版本号, 跟 1b.5 时代 'model 在 greeting 显示' 比, 牺牲一点 UX
141
- // (用户得 chat 一次才能看到 model 名) 换 REPL 可在无 key 状态启动.
142
- const initialClientState = tryCreateClient();
143
- out.write(`${t('cli.greeting', VERSION, initialClientState.client?.model ?? 'not-configured')}\n`);
144
- if (initialClientState.error) {
145
- // 无 key 提示沿用 1b.5 时代 163-166 行的 stderr 警告语义, 但挪到 lazy create 之后
146
- err.write(`${t('error.api_key_missing')}\n`);
147
- }
148
- out.write(`${t('cli.no_api_key_hint')}\n\n`);
149
- // session 加载
150
- let workingMessages = [];
151
- // Sprint 1c-revive-2-D-21.1 (2026-06-06, 修 cache 96%↔85% 跳变 footer 焦虑):
152
- // EMA 平滑闭包 state. appendUsageStatus 每 turn in-place 更新, formatUsageStatus
153
- // 读 ema 显示 (avg NN%). 跨 turn 累积, 闭包内 mutable, 不持久化 (session reload
154
- // 后 sampleCount 重置为 0, 避免误导 — user 看到 avg 段消失就知道 reload 过了).
155
- const emaState = { sampleCount: 0 };
156
- const writer = sessionPath ? new SessionWriter(sessionPath) : null;
157
- const reader = sessionPath ? new SessionReader(sessionPath) : null;
158
- if (writer && reader) {
159
- try {
160
- await writer.open();
161
- const loaded = await loadSession(reader);
162
- workingMessages = [...loaded.messages];
163
- if (workingMessages.length > 0) {
164
- out.write(`${t('cli.session_resumed', workingMessages.length, sessionPath)}\n\n`);
165
- }
166
- }
167
- catch (e) {
168
- err.write(`${t('cli.session_load_warning', String(e))}\n\n`);
169
- }
170
- }
171
- // Sprint 1c-revive-2-D-6 (review P1 修复, 2026-06-04): CompactionState 闭包持有,
172
- // 跨 turn 持续累计 failures (paused 状态跨 turn 生效, 跟 test 1c-revive-2-D-5-2 拍板).
173
- // - 传 options.compactionConfig + writer 存在 → 构造完整 AgentCompactionConfig 注入
174
- // - 不传 / writer 缺失 → 走 baseline 行为, compactionConfig = null
175
- let compactionConfig = null;
176
- if (options.compactionConfig && writer) {
177
- compactionConfig = {
178
- ...options.compactionConfig,
179
- writer,
180
- state: new CompactionState(options.compactionConfig.pauseAfterFailures ?? 2),
181
- };
182
- }
183
- else if (options.compactionConfig && !writer) {
184
- err.write('warning: compactionConfig requires sessionPath; falling back to baseline (no compaction).\n');
185
- }
186
- const rl = createInterface({
187
- input: options.input ?? stdin,
188
- terminal: false,
189
- output: options.output ?? stdout,
190
- });
191
- return new Promise((resolve) => {
192
- let exiting = false;
193
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): P2-SIGINT 修法 — finish 移除全局 listener ===
194
- // 拍板 (D-19.5, user review 2026-06-05 P2): repl.ts:307 每次 startRepl() 挂全局
195
- // process.on('SIGINT'), finish() 没 process.off, 嵌入式/测试多次启动 REPL → 累积
196
- // listener. 后 Ctrl+C 触发已退出 REPL 的闭包. 修法: finish() 入口先 .off 一次.
197
- // 顺序: .off 必须在 rl.close() 之前, 否则 close 派发 'close' event 期间 Ctrl+C 还能
198
- // 触达 onSigint 闭包. 红线: 跟 D-19 P2-Ctrl+C 拍板不冲突 — finish 才清理, SIGINT
199
- // 触发的 dismiss + abort 仍由 onSigint 兜底 (D-19 行为不变).
200
- const finish = async (code) => {
201
- if (exiting)
202
- return;
203
- exiting = true;
204
- // === Sprint 1c-revive-3-D-19.6 (2026-06-05): 清 exitTimer 防止 P1 兜底 timer 泄漏 ===
205
- if (exitTimer) {
206
- clearTimeout(exitTimer);
207
- exitTimer = null;
208
- }
209
- process.off('SIGINT', onSigint);
210
- rl.close();
211
- if (writer) {
212
- try {
213
- await writer.close();
214
- }
215
- catch {
216
- /* 关闭失败 best-effort,REPL 退出码仍按 caller 决定 */
217
- }
218
- }
219
- out.write(`${t('cli.goodbye')}\n`);
220
- resolve(code);
221
- };
222
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): P1 turn guard + 排队 + SIGINT/dismiss 链路 ===
223
- // 拍板 (D-19.5, user review 2026-06-05 P1): 旧 line handler 在 confirm settle 之后, 紧
224
- // 跟的下一行 (e.g. /exit\n) 立刻 leak 到 main chat/builtin 分支, /exit 提前 close
225
- // writer, 第二轮 chat 用旧 workingMessages 并发跑. 修法: turnInFlight 闭包标志 +
226
- // lineQueue 排队, 关键时序:
227
- // - 派发前检查 turnInFlight, true → 入队不入 chat
228
- // - turn 跑完在 finally 块: 检查 pendingExit (走 finish) → 否则 drain 下一条
229
- // - /exit fast-path: turn 在跑时只标 pendingExit, finally 兜底; turn 不在跑时直接
230
- // finish (exiting 守卫幂等)
231
- // - confirm 期间: 旧 D-19 offerLine 派发仍走, 但 line 不能 leak 到 chat 分支
232
- // - drain 用 setImmediate 避免同步递归爆栈 (P-verify-4 实测, 同步 emit 在大量排队
233
- // 时撞 V8 10000 帧限制)
234
- let turnInFlight = false;
235
- let pendingExit = false;
236
- // === Sprint 1c-revive-3-D-19.6 (2026-06-05): P1 close-during-turn 30s 兜底 timer ===
237
- // 拍板 (D-19.6, user review 2026-06-05 P1): close handler 走 pendingExit + finally
238
- // 兜底 finish() 后, 若 in-flight turn 永远不收束 (e.g. 网络卡死/无限 retry), REPL
239
- // 永远不退出. exitTimer 启动 30s 硬 timeout, 触发时 stderr warning (i18n) +
240
- // 强制 finish. 变量与 pendingExit 同 scope (Q2=方案 2): REPL 单次生命周期状态,
241
- // 模块级会让嵌入式/并行多 REPL 实例互相污染. 仅 turnInFlight=true 启动 (Q3=b):
242
- // 没有 in-flight turn 时直接 finish, 不需要兜底.
243
- let exitTimer = null;
244
- const lineQueue = [];
245
- // === Sprint 1c-revive-3-D-19 (2026-06-05): P2-Ctrl+C 修法 — turn AbortController ===
246
- // 拍板 (D-19): turnAbortController 闭包共享, 让 SIGINT handler 能 abort 它,
247
- // 透传到 runToolLoop → executeToolCall → policy.confirm 的 signal 参数.
248
- // 注意: plan R-1 实测 — terminal:false rl 不自动派发 SIGINT 事件, 必须挂 process.
249
- // 单测用 mock 的 process, 通过 rl.input (PassThrough) 触发不了 SIGINT; 测 Ctrl+C
250
- // 行为走 turnAbortController.abort() 直接调 (见 repl-shared-stdin / tool-loop-policy test).
251
- //
252
- // turn 生命周期: 每次 chat 入口 new 一个新 controller, 旧的引用还在闭包里
253
- // (供 SIGINT handler 用). 拍板 (D-19): AbortController 单次 abort 语义, 一次
254
- // turn SIGINT 之后, 下一个 turn 用新 controller + 重新挂 SIGINT (drain old handler).
255
- let turnAbortController = new AbortController();
256
- const onSigint = () => {
257
- // Ctrl+C: dismiss in-flight confirm first (落 user_denied), 然后 abort turn.
258
- // 拍板 (D-19): 进程不退出, 用户可继续. finish() 仍由 /exit 或 EOF 触发.
259
- if (confirmController.hasPending()) {
260
- confirmController.dismiss();
261
- }
262
- if (!turnAbortController.signal.aborted) {
263
- turnAbortController.abort();
264
- }
265
- };
266
- process.on('SIGINT', onSigint);
267
- rl.on('line', async (rawLine) => {
268
- const line = rawLine.trim();
269
- // === Sprint 1c-revive-3-D-19 (2026-06-05): P1 修法 — 串行化 confirm 期间 line 消费 ===
270
- // 拍板 (D-19): 主 rl 是 stdin 唯一消费者. 确认期间收到的 line 必须喂给 confirm
271
- // resolver, 不能入 chat. 修 D-15 P1 (同流双 readline 抢同一行 → y 被当新 chat turn).
272
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): 补 — confirm 期间 /exit 不入 chat, dismiss 兜底 ===
273
- // 拍板 (D-19.5, user review 2026-06-05 P1): 旧逻辑 confirm 期间 /exit 走到下面
274
- // /exit 分支直接 await finish(0), 但 confirm 还在 pending → finish 里 rl.close
275
- // 后 confirm Promise 永远悬空 (跟 P2-dismiss 同源). 修法: confirm 期间 /exit 先
276
- // dismiss confirm 再标记 pendingExit, finally 兜底 finish. 顺序: dismiss 先
277
- // (让 runToolLoop 走 user_denied 审计), 再标 pendingExit.
278
- if (confirmController.hasPending()) {
279
- if (line === 'exit' || line === 'quit' || line === '/exit' || line === '/quit') {
280
- confirmController.dismiss();
281
- pendingExit = true;
282
- return;
283
- }
284
- const consumed = confirmController.offerLine(line);
285
- if (consumed) {
286
- // confirm resolver 已 settle, 等待 promise 走完; 调 prompt() 让用户看见下一轮.
287
- // 注意: confirmController 内部在 offerLine 同步 settle, 但 await 仍在 tool-loop 端.
288
- // 拍板 (D-19): 不在这里 await confirm 本身, 避免阻塞 rl 内部 line queue.
289
- return;
290
- }
291
- }
292
- // === Sprint 1c-revive-3-D-19.6 (2026-06-05): P2 turn guard deny 非 /exit builtin ===
293
- // === Sprint 1c-revive-3-D-19.6.1 (2026-06-05): 加 line.startsWith('/') 限制, 不拦普通 chat line ===
294
- // 拍板 (D-19.6, user review 2026-06-05 P2): 老逻辑 L373 注释"内建命令全部
295
- // fast-path, 不走 turnInFlight"在 turn 正在跑时仍然跑 builtin, e.g. /verify
296
- // 调 runVerify + 写 verification event, /help 写 out + prompt, /unknown
297
- // 写 out + prompt, 都跟 in-flight chat turn 输出/session 交错, 违背
298
- // "turn running 时下一行不进入 builtin/chat" 的 review 语义.
299
- // === Sprint 1c-revive-3-D-19.6.1 (2026-06-05): Q2 修法 — 加 line.startsWith('/') 限制 ===
300
- // 拍板 (D-19.6.1, user review 2026-06-05 P1.2): D-19.6 守卫条件缺 slash
301
- // 限制, 普通 chat line 也会被 deny, 跟 D-19.5 lineQueue "只排 chat line"
302
- // 红线冲突, 永远到不了 lineQueue. 修法: 加 `line.startsWith('/')` 限制, 只
303
- // deny slash builtin. 普通 chat line 继续走 L408 lineQueue 排队 (D-19.5 拍板
304
- // 不变). 守卫名改成"slash builtin guard"以反映新语义.
305
- //
306
- // 修复: turnInFlight 时, 除 /exit /quit /exit /quit /'' 之外的 **slash builtin**
307
- // (/verify /help /unknown slash) 走 deny, 输出 i18n 提示
308
- // (cli.turn_in_flight_deny) + prompt + return, 不入 lineQueue.
309
- // 选择 deny 而非 defer, 因为 lineQueue 在 D-19.5 已有红线 (L407):
310
- // "lineQueue 只排 chat line", defer 会让 finally drain 还要判 builtin vs chat.
311
- //
312
- // 位置红线: 必须 confirm 守卫 (L341-358) 之后, 内建命令 dispatcher (L373 起始)
313
- // 之前. confirm 期间 /exit 走 dismiss+pendingExit (D-19.5 P1), 不应被本守卫拦.
314
- // /exit /quit /exit /quit 走 fast-path (L378), 也不应被拦. 空行 L374 调
315
- // prompt() 也不应被拦 (空行 ≠ 内建命令).
316
- if (turnInFlight &&
317
- line.startsWith('/') &&
318
- line !== '/exit' &&
319
- line !== '/quit') {
320
- out.write(`${t('cli.turn_in_flight_deny')}\n\n`);
321
- prompt();
322
- return;
323
- }
324
- // 内建命令 — 全部 fast-path, 不走 turnInFlight (内建不等 chat turn)
325
- if (line === '') {
326
- prompt();
327
- return;
328
- }
329
- if (line === 'exit' || line === 'quit' || line === '/exit' || line === '/quit') {
330
- // 拍板 (D-19.5): turn 不在跑直接 finish; 在跑标 pendingExit, finally 兜底.
331
- if (turnInFlight) {
332
- pendingExit = true;
333
- return;
334
- }
335
- await finish(0);
336
- return;
337
- }
338
- if (line === '/help') {
339
- out.write(`${t('cli.builtin_help')}\n`);
340
- prompt();
341
- return;
342
- }
343
- if (line === '/verify') {
344
- // Sprint 1c-revive-2-D-11-4 (2026-06-04): REPL `/verify` 内建命令.
345
- // 跟 CLI `deepwhale --verify` 走同一 runVerify() — 不走 LLM / tool loop.
346
- // 拍板 (D-11-4 review, 2026-06-04): REPL 里 /verify 走**异步** runVerify,
347
- // 跑完打 formatReport 到 out (跟其它内建命令风格一致), 然后**写 verification
348
- // event 到 session JSONL** (因为用户在 REPL 里跑了 verify, session 走 audit
349
- // 轨迹, 跟 CLI 不写 session 形成差异).
350
- // 退出: REPL 不退, 跑完回到 prompt 继续.
351
- try {
352
- const report = await runVerify(options.verifyChecks !== undefined ? { checks: options.verifyChecks } : {});
353
- const filled = buildSummaryAndNext(report);
354
- const text = formatReport({
355
- ...report,
356
- summary: filled.summary,
357
- nextSuggestedAction: filled.nextSuggestedAction,
358
- });
359
- out.write(`${text}\n`);
360
- if (writer) {
361
- // 写 verification event 到 session (跟 CLI 不同: REPL 用户有 session, 应该审计)
362
- const failedCount = report.checks.filter((c) => c.status !== 'passed').length;
363
- await appendVerificationEvent(writer, {
364
- status: report.overallStatus,
365
- durationMs: report.durationMs,
366
- commandCount: report.checks.length,
367
- failedCount,
368
- summary: filled.summary,
369
- });
370
- }
371
- }
372
- catch (e) {
373
- err.write(`error: verify failed to start: ${e instanceof Error ? e.message : String(e)}\n\n`);
374
- }
375
- prompt();
376
- return;
377
- }
378
- if (line.startsWith('/')) {
379
- out.write(`${t('cli.builtin_unknown', line)}\n`);
380
- prompt();
381
- return;
382
- }
383
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): P1 turn guard — 排队 turnInFlight 期间 line ===
384
- // 拍板 (D-19.5, user review 2026-06-05 P1): 旧逻辑紧跟 chat turn 的下一行 (紧贴
385
- // y\n 或 turn 还没跑完时 stdin 排队的行) 立刻进 chat 分支, 用旧 workingMessages
386
- // 并发跑第二轮, /exit 提前 close writer. 修法: 派发前检查 turnInFlight, true
387
- // → 入队不入 chat. finally 块跑完 turn, 检查 pendingExit (走 finish) → 否则
388
- // drain lineQueue 下一条 (setImmediate 避免爆栈). 红线: pendingExit 优先级高于
389
- // drain, 因为 /exit 应该是"不处理后续, 立刻走"语义, 不应该 drain 排队行.
390
- if (turnInFlight) {
391
- lineQueue.push(line);
392
- return;
393
- }
394
- turnInFlight = true;
395
- // chat — Sprint 1c-revive-2-D-11-4 review P1 修复: client lazy 化后, chat
396
- // 路径首次调 tryCreateClient() 真创. 创失败 (无 key) → i18n stderr 提示 + 跳
397
- // 过本次 turn (不退出 REPL, 用户可继续 /verify 或 /exit).
398
- // Sprint 1c-revive-3-D-19 (2026-06-05): 续命 turnAbortController. 上一个 turn 已被
399
- // SIGINT abort, 复用同一个 controller 第二次 abort 无效, new 一个新的. onSigint
400
- // 闭包持有的是变量名 (let), 新 controller 一被赋值, 下次 SIGINT 自动 abort 新的,
401
- // 不需要重建 handler. 红线: 不要 add 多份 SIGINT listener 重复触发.
402
- turnAbortController = new AbortController();
403
- const c = clientFromOptions ? { client: clientFromOptions, error: null } : tryCreateClient();
404
- if (c.client === null) {
405
- err.write(`${t('error.api_key_missing')}\n\n`);
406
- turnInFlight = false;
407
- prompt();
408
- return;
409
- }
410
- const liveClient = c.client;
411
- try {
412
- if (enableToolLoop) {
413
- await runAgentTurn(liveClient, line, workingMessages, writer, out, err, turnAbortController.signal, compactionConfig, sandboxRunner, policyYes, replPolicy, // D-15: 注入 y/N confirm; 默认 staticToolPolicy 向后兼容
414
- emaState);
415
- }
416
- else {
417
- const turn = await runOneTurn(liveClient, line, [], { signal: turnAbortController.signal });
418
- if (turn.kind === 'error') {
419
- err.write(`${turn.error}\n\n`);
420
- }
421
- else if (turn.kind === 'chat') {
422
- out.write(`${turn.assistant}\n\n`);
423
- }
424
- }
425
- }
426
- finally {
427
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): drain lineQueue / 走 pendingExit ===
428
- // 拍板 (D-19.5): turn 跑完 → 1) pendingExit=true 走 finish (丢弃排队);
429
- // 2) 否则 drain 下一条 line (setImmediate 避免同步递归爆栈);
430
- // 3) 都 false → prompt 继续. 顺序: pendingExit 优先, 不然用户 /exit 后还跑排队行.
431
- // 红线: finally 不能 return (no-unsafe-finally), 用 if/else if/else 链.
432
- turnInFlight = false;
433
- if (pendingExit) {
434
- pendingExit = false;
435
- void finish(0);
436
- }
437
- else if (lineQueue.length > 0 && !exiting) {
438
- const next = lineQueue.shift();
439
- setImmediate(() => rl.emit('line', next));
440
- }
441
- else {
442
- prompt();
443
- }
444
- }
445
- });
446
- rl.on('close', () => {
447
- // stdin EOF(管道/Ctrl-D)→ 优雅退出
448
- // === Sprint 1c-revive-3-D-19.5 (2026-06-05): P2-dismiss 修法 — close 期间清理 pending confirm + abort turn ===
449
- // 拍板 (D-19.5, user review 2026-06-05 P2): 旧逻辑只调 finish(0), 忽略两种悬空:
450
- // 1) confirm 还在 pending → policy.confirm() Promise 永远不 resolve, turn 不会
451
- // 走 finally, session 不会落 user_denied 审计.
452
- // 2) turn 还在跑 → LLM stream / tool exec 还在 await, 进程表面已关但内部链未断.
453
- // 修法: close 时先 dismiss pending confirm (resolve null → tool 走 user_denied 落审计),
454
- // 再 abort turnAbortController 让 runAgentTurn 走 finally 收束. 顺序: dismiss 先于
455
- // abort, 因为 confirm resolve 后 runToolLoop 才检查 signal, 调换会丢 audit 路径.
456
- // === Sprint 1c-revive-3-D-19.6 (2026-06-05): P1 close-during-turn 收束 — 不再直接 finish() ===
457
- // 拍板 (D-19.6, user review 2026-06-05 P1): dismiss + abort 之后, 老 finish() 立即
458
- // 关 writer, 后续 turn 内部 writer.append (user_denied / 其它 audit event) 撞
459
- // 'file closed' (stderr "Unexpected error: Error: file closed").
460
- // 修复: 设 pendingExit=true 让 finally 块 (L490 附近) 兜底调 finish. 如果 in-flight
461
- // turn 30s 内没 finally, exitTimer 触发 stderr warning (i18n) + 强制 finish.
462
- // 红线: dismiss 先于 abort (D-19.5 P2-dismiss); finally 块 if/else if/else 链
463
- // pendingExit 优先 (D-19.5 P1); exitTimer 仅 turnInFlight 时启动 (Q3=b).
464
- if (confirmController.hasPending()) {
465
- confirmController.dismiss();
466
- }
467
- if (turnInFlight && !turnAbortController.signal.aborted) {
468
- turnAbortController.abort();
469
- }
470
- pendingExit = true;
471
- if (turnInFlight) {
472
- if (exitTimer)
473
- clearTimeout(exitTimer);
474
- exitTimer = setTimeout(() => {
475
- // 30s 兜底: turn 卡死时强制 finish, stderr warning 走 i18n (Q1=A).
476
- // 注: t() 是位置参数, 模板用 {0}, 不是 {ms}.
477
- if (exiting)
478
- return;
479
- err.write(`${t('cli.repl_force_exit_timeout', 30000)}\n`);
480
- void finish(0);
481
- }, 30_000);
482
- // unref: 不让 timer 阻止进程退出 (finish 自己会调 process.exit / resolve).
483
- exitTimer.unref?.();
484
- }
485
- else {
486
- // turn 没在跑, 直接 finish (Q3=b 的 else 分支).
487
- void finish(0);
488
- }
489
- });
490
- const prompt = () => {
491
- if (exiting)
492
- return;
493
- out.write(t('cli.prompt'));
494
- };
495
- // 第一个 prompt
496
- prompt();
497
- });
498
- }
499
- /**
500
- * 跑一轮 agent turn:append user → runToolLoop → 持久化 → 打印 final content。
501
- *
502
- * workingMessages 由 caller 持有(startRepl 闭包),turn 跑完后 caller 用新 messages 覆盖。
503
- *
504
- * Sprint 1a 修 P1:user 必须进 LLM。Sprint 1a 修 P2-A:流式不再重复打印 final content。
505
- * Sprint 1c-revive-2-D-6 (review P1 修复, 2026-06-04): 可选 compactionConfig — 传
506
- * 时调 runToolLoopWithCompaction (入口测 token, 触发则 compact + 写 event),
507
- * 不传 = 走裸 runToolLoop (向后兼容, 单测 baseline 244 不变). summaryFn 内部
508
- * 用 client + 固定 prompt 模板生成, 跟 test 1c-revive-2-D-5 cluster 拍板一致.
509
- * 单测通过 export 暴露,直接注入 mock LLMClient + WritableStream 验证行为。
510
- */
511
- export async function runAgentTurn(client, userInput, workingMessages, writer, out, err, signal, compactionConfig = null,
512
- // Sprint 1c-revive-3-D-12 review P1 修复: startRepl 把 env 解析的 runner
513
- // 传进来, 工具注册表跟 env 状态对齐. 不传 = 用 LocalSandboxRunner (向后兼容).
514
- sandboxRunner,
515
- // Sprint 1c-revive-3-D-13: 透传 yes 进 turn.
516
- yes,
517
- // Sprint 1c-revive-3-D-15: 透传 policy 进 turn (REPL 注入 replPolicy; 单测传
518
- // staticToolPolicy 走 baseline). undefined = 默认 staticToolPolicy (向后兼容).
519
- policy,
520
- // Sprint 1c-revive-2-D-21.1 (2026-06-06, 修 cache 96%↔85% 跳变 footer 焦虑):
521
- // EMA state 透传 (闭包持有). 旧 caller 不传 = 默认 EMPTY_EMA, 行为兼容
522
- // (不显示 avg 段). REPL 路径必传, 跨 turn 累积.
523
- emaState) {
524
- // 1) 持久化 user 输入
525
- if (writer) {
526
- const userEvent = {
527
- kind: 'user',
528
- ts: Date.now(),
529
- content: userInput,
530
- };
531
- await writer.append(userEvent);
532
- }
533
- // 2) 构造 turn 消息:历史 + 本轮 user。Sprint 1a 修 P1 — user 必须进 LLM。
534
- const turnMessages = [...workingMessages, { role: 'user', content: userInput }];
535
- // 3) 调 tool loop. Sprint 1c-revive-2-D-6: 传 compactionConfig 时走
536
- // runToolLoopWithCompaction (带入口 compaction + 写 compaction event),
537
- // 不传 = 裸 runToolLoop (向后兼容, baseline 244 不变).
538
- const summaryFn = compactionConfig
539
- ? makeLlmSummarizeFn(client, compactionConfig.protocol)
540
- : null;
541
- // 拍板 (D-15, 2026-06-05): REPL 注入 replPolicy; 显式传 policy 也用, 默认 staticToolPolicy.
542
- // 拍板红线 (D-13.5 P1 重排): --yes 永远先于 confirm, replPolicy.confirm 只在 yes=false 才被调.
543
- const resolvedPolicy = policy ?? staticToolPolicy;
544
- let result;
545
- try {
546
- if (compactionConfig !== null && summaryFn !== null) {
547
- result = await runToolLoopWithCompaction(client, turnMessages, {
548
- registry: createDefaultRegistry({
549
- ...(sandboxRunner !== undefined ? { sandboxRunner } : {}),
550
- }),
551
- onChunk: (chunk) => {
552
- if (chunk.content)
553
- out.write(chunk.content);
554
- },
555
- signal,
556
- policy: resolvedPolicy,
557
- isInteractive: true, // REPL = 交互模式 (D-13 拍板)
558
- yes: yes ?? false,
559
- ...(writer ? { writer } : {}),
560
- }, compactionConfig, summaryFn);
561
- }
562
- else {
563
- result = await runToolLoop(client, turnMessages, {
564
- registry: createDefaultRegistry({
565
- ...(sandboxRunner !== undefined ? { sandboxRunner } : {}),
566
- }),
567
- onChunk: (chunk) => {
568
- if (chunk.content)
569
- out.write(chunk.content);
570
- },
571
- signal,
572
- policy: resolvedPolicy,
573
- isInteractive: true, // REPL = 交互模式 (D-13 拍板)
574
- yes: yes ?? false,
575
- ...(writer ? { writer } : {}),
576
- });
577
- }
578
- }
579
- catch (e) {
580
- // === Sprint 1c-revive-3-D-19.6.1 (2026-06-05): Q3 修法 — abort-aware 分支 ===
581
- // 拍板 (D-19.6.1, user review 2026-06-05 P2.1): D-19.6 P1 修法让 close 路径 abort
582
- // in-flight turn, runToolLoop 内部 throw "Tool loop aborted by caller", 老 catch
583
- // 走 cli.error.unknown ("Unexpected error: {0}") 污染 stderr 为 unexpected error.
584
- // 修法: 检测 signal.aborted 优先于 isToolLoopError/isLLMError, 走专门 i18n key
585
- // (cli.turn_aborted_shutdown). 文案 "no audit gap" 强调 user_denied 该落的都
586
- // 落了 (D-19.6 P1 dismiss+abort+pendingExit 链路已保审计). 不走 unexpected
587
- // 路径, stderr 不再被 intentional shutdown 污染.
588
- //
589
- // 顺序红线: signal.aborted 检查必须在 isToolLoopError 之前 — runToolLoop 内部
590
- // abort 时 throw Error('Tool loop aborted by caller'), 这 Error 满足 isToolLoopError
591
- // 的某些宽松判定 (e.g. 有 .message 但无 .steps) 是不稳的. signal.aborted 是
592
- // 最直接的真相, 优先.
593
- if (signal.aborted) {
594
- err.write(`${t('cli.turn_aborted_shutdown')}\n\n`);
595
- }
596
- else if (isToolLoopError(e)) {
597
- err.write(`${t('cli.tool_loop_limit', e.steps)}\n\n`);
598
- }
599
- else if (isLLMError(e)) {
600
- err.write(`${formatError(e)}\n\n`);
601
- }
602
- else {
603
- err.write(`${t('cli.error.unknown', String(e))}\n\n`);
604
- }
605
- return;
606
- }
607
- // 4) 流式已实时打印;非流式分支此处补打印 final content(给上层 caller 留 fallback)。
608
- // Sprint 1a REPL 总是传 onChunk 走流式,所以这里不再重复打印。
609
- // 4) 持久化 steps
610
- if (writer) {
611
- try {
612
- await persistToolLoopSteps(writer, result.steps);
613
- }
614
- catch (e) {
615
- err.write(`${t('cli.session_write_warning', String(e))}\n`);
616
- }
617
- }
618
- // 5) 更新 working messages(startRepl 闭包会保留新值)
619
- workingMessages.length = 0;
620
- workingMessages.push(...result.messages);
621
- // 6) Step summary(人类可读)
622
- for (const step of result.steps) {
623
- appendStepSummary(step, out, err);
624
- }
625
- // 7) Sprint 1b: Prefix-cache 可观测性 — 每 turn 打印一行 status 到 stderr
626
- // 风格: 分两行(跟 plan 拍板), 不污染 stdout 流式输出, 不打 prompt 前面
627
- // 字段: cache_hit_rate / cost_turn / prompt / completion, 多字段同值时去冗余(Hermes footer 教训)
628
- // Sprint 1c-revive-2-D-21.1 (2026-06-06, 修 cache 96%↔85% 跳变 footer 焦虑):
629
- // 加 EMA 滚动平均 5-turn 平滑, 显示 "cache: 90% (avg 85%)". per-turn 数字
630
- // 仍是真实值, avg 是过去 5 turn 平滑趋势. user 不会被单 turn 抖动骗.
631
- appendUsageStatus(result.final.usage, err, emaState ?? EMPTY_EMA);
632
- }
633
- function appendStepSummary(step, out, err) {
634
- if (step.kind === 'tool') {
635
- const status = step.result.success ? '✓' : '✗';
636
- out.write(` ${status} ${step.tool_call.name} (${step.duration_ms}ms)\n`);
637
- if (!step.result.success && step.result.error) {
638
- err.write(` ${step.result.error}\n`);
639
- }
640
- }
641
- // 'assistant' / 'limit' / 'error' 的 summary 留 Sprint 1b(不污染 Sprint 1a 验收面)
642
- }
643
- const EMPTY_EMA = { sampleCount: 0 };
644
- export function formatUsageStatus(usage, emaState = EMPTY_EMA) {
645
- if (usage === undefined)
646
- return null;
647
- const { prompt_tokens, completion_tokens } = usage;
648
- // 无 cached_tokens: 简版
649
- if (usage.cached_tokens === undefined) {
650
- return `usage: ${formatTokens(prompt_tokens)} prompt / ${formatTokens(completion_tokens)} completion`;
651
- }
652
- // 满 usage: 完整 status
653
- const hitRatePct = ((usage.cache_hit_rate ?? 0) * 100).toFixed(0);
654
- const uncached = formatTokens(usage.tokens_uncached ?? prompt_tokens);
655
- // Sprint 1c-revive-2-D-21.1: EMA 平滑尾部段. sampleCount >= 3 才显示 avg
656
- // (样本太少趋势不稳). 不更新 caller state, 只读 (state update 在
657
- // appendUsageStatus, 这是纯函数好测).
658
- const avgSegment = emaState.sampleCount >= 3 && emaState.hitRateEMA !== undefined
659
- ? ` (avg ${(emaState.hitRateEMA * 100).toFixed(0)}%)`
660
- : '';
661
- // Sprint 1b.5 Step 2.5 (F5 拍板, review 2026-06-03 找到): cost_turn/cost_currency 都 absent
662
- // (R7 中间路径 / F4 保守) → 安静少显示字段, **不**显示 'cost ?'. 跟 1b 拍板 "absent 安静"
663
- // 一致. user 视角看 'cost ?/turn' 是 'UI 不知道' 不是 '这次没算', 显示 '?' 反而误导.
664
- if (usage.cost_turn === undefined || usage.cost_currency === undefined) {
665
- return `cache: ${hitRatePct}%${avgSegment} | prompt ${formatTokens(prompt_tokens)} (${uncached} new)`;
666
- }
667
- // cost 字段齐: 读 cost_currency 决 symbol
668
- const symbol = formatCostSymbol(usage.cost_currency);
669
- const cost = usage.cost_turn; // narrowed by 上面 if guard (cost_turn !== undefined)
670
- const costStr = cost < 0.01 ? `${symbol}${cost.toFixed(4)}` : `${symbol}${cost.toFixed(3)}`;
671
- return `cache: ${hitRatePct}%${avgSegment} | ${costStr}/turn | prompt ${formatTokens(prompt_tokens)} (${uncached} new)`;
672
- }
673
- /** cost_currency → 显示 symbol. 不在 UI 层做汇率换算. */
674
- function formatCostSymbol(currency) {
675
- switch (currency) {
676
- case 'CNY':
677
- return '¥';
678
- case 'USD':
679
- return '$';
680
- case undefined:
681
- return '?';
682
- }
683
- }
684
- function formatTokens(n) {
685
- if (n >= 1000)
686
- return `${(n / 1000).toFixed(1)}k`;
687
- return String(n);
688
- }
689
- /**
690
- * Sprint 1c-revive-2-D-21.1 (2026-06-06): emaState 接受 mutable 引用 (闭包),
691
- * 在 sampleCount < 5 用 cold-start EMA (直接赋值), 之后 α=0.5 平滑:
692
- * newEMA = α * current + (1 - α) * oldEMA = 0.5 * current + 0.5 * oldEMA
693
- * α=0.5 是 "等权平滑" (5-turn 半衰期 ≈ 1 turn, 快速响应 + 适度平滑).
694
- * 数学: sampleCount=5 时, 5 turn 前的数据权重 = 0.5^5 = 3.1% (基本忘掉),
695
- * 跟 5-turn rolling window 趋势一致, 但 EMA 实现更轻.
696
- *
697
- * export 出来供单测 (test/unit/usage-ema.test.ts) 验证 state machine.
698
- * 之前是 local function, D-21.1 改成 export — 单测需要直接调它验 in-place update.
699
- */
700
- export function appendUsageStatus(usage, err, emaState) {
701
- // 同步更新 EMA state (in-place). 调 formatUsageStatus 之前先 update,
702
- // 防止 "刚 sample 1 个, display 当 turn 仍显示 sample 0 的 ema".
703
- if (usage !== undefined && usage.cached_tokens !== undefined) {
704
- const current = usage.cache_hit_rate ?? 0;
705
- if (emaState.hitRateEMA === undefined) {
706
- emaState.hitRateEMA = current;
707
- }
708
- else {
709
- emaState.hitRateEMA = 0.5 * current + 0.5 * emaState.hitRateEMA;
710
- }
711
- emaState.sampleCount += 1;
712
- }
713
- const line = formatUsageStatus(usage, emaState);
714
- if (line !== null) {
715
- err.write(` ${line}\n`);
716
- }
717
- }
718
- function formatError(e) {
719
- if (e instanceof APIKeyMissingError)
720
- return t('error.api_key_missing');
721
- if (e instanceof LLMAuthError) {
722
- // Sprint 1b.5 Step 2.5 修: tsc strict 看 LLMAuthError.status 在 .status 上
723
- return t('cli.error.auth', String(e.status));
724
- }
725
- if (e instanceof LLMRateLimitError)
726
- return t('cli.error.rate_limit');
727
- if (e instanceof LLMNetworkError) {
728
- const err = e;
729
- const msg = err.cause instanceof Error ? err.cause.message : err.message;
730
- return t('cli.error.network', msg);
731
- }
732
- if (e instanceof LLMStreamError) {
733
- return t('cli.error.stream', e.message);
734
- }
735
- if (e instanceof LLMUnknownError) {
736
- const err = e;
737
- const detail = err.status !== undefined ? `HTTP ${err.status}` : err.message;
738
- return t('cli.error.unknown', detail);
739
- }
740
- if (e instanceof ToolLoopLimitError) {
741
- return t('cli.tool_loop_limit', e.steps);
742
- }
743
- if (isLLMError(e))
744
- return t('cli.error.unknown', e.message);
745
- if (e instanceof Error)
746
- return t('cli.error.unknown', e.message);
747
- return t('cli.error.unknown', String(e));
748
- }
749
- /**
750
- * 生成 LLM summary callback (Sprint 1c-revive-2-D-6).
751
- *
752
- * 跟 1c-revive-2-D-5 cluster test (compaction-cross-protocol-2d5.test.ts:231)
753
- * 拍板一致: 走 client.chat 调 LLM 生成 1 short paragraph summary. 跨
754
- * openai/anthropic 同形态, 因为 client.chat 是 LLMClient 契约的统一入口.
755
- *
756
- * 不**在**这里拼 protocol-specific system prompt: Anthropic protocol
757
- * 走 client.chat 时已由 client 内部加 (跟 agent-compaction.ts protocol
758
- * 字段对齐). 1c-revive-2-D-6 拍板: protocol 字段保留供未来 system
759
- * prompt 模板差异化用, 当前 D-6 默认用同 system prompt 模板 (跨协议一致).
760
- */
761
- function makeLlmSummarizeFn(client, _protocol) {
762
- return async (toSummarize) => {
763
- const summaryMessages = [
764
- {
765
- role: 'system',
766
- content: 'You are a concise summarizer. Compress the following conversation into 1 short paragraph ' +
767
- '(max 200 words). Preserve key arithmetic results, tool calls, and final answers.',
768
- },
769
- {
770
- role: 'user',
771
- content: toSummarize
772
- .map((m, i) => `[${i}] ${m.role}: ${m.content ?? '(empty)'}`)
773
- .join('\n'),
774
- },
775
- ];
776
- const r = await client.chat(summaryMessages, {});
777
- return r.content;
778
- };
779
- }
780
- //# sourceMappingURL=repl.js.map