@mafdet/cli 0.4.1 → 0.4.3

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 (80) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +79 -12
  3. package/README.zh-CN.md +69 -12
  4. package/dist/agent/context-budget.d.ts +37 -0
  5. package/dist/agent/context-budget.d.ts.map +1 -0
  6. package/dist/agent/context-budget.js +105 -0
  7. package/dist/agent/loop.d.ts +25 -0
  8. package/dist/agent/loop.d.ts.map +1 -1
  9. package/dist/agent/loop.js +36 -3
  10. package/dist/agent/session.d.ts +5 -0
  11. package/dist/agent/session.d.ts.map +1 -1
  12. package/dist/agent/session.js +1 -1
  13. package/dist/agent/tools/read-only.d.ts.map +1 -1
  14. package/dist/agent/tools/read-only.js +6 -0
  15. package/dist/agent/tools/run-command.d.ts +28 -0
  16. package/dist/agent/tools/run-command.d.ts.map +1 -1
  17. package/dist/agent/tools/run-command.js +34 -13
  18. package/dist/api/account.d.ts +43 -0
  19. package/dist/api/account.d.ts.map +1 -1
  20. package/dist/api/account.js +40 -0
  21. package/dist/commands/chat.d.ts.map +1 -1
  22. package/dist/commands/chat.js +118 -18
  23. package/dist/commands/sessions.d.ts +5 -0
  24. package/dist/commands/sessions.d.ts.map +1 -0
  25. package/dist/commands/sessions.js +379 -0
  26. package/dist/config/paths.d.ts +7 -0
  27. package/dist/config/paths.d.ts.map +1 -1
  28. package/dist/config/paths.js +24 -0
  29. package/dist/filesystem/protected-dirs.d.ts +26 -0
  30. package/dist/filesystem/protected-dirs.d.ts.map +1 -0
  31. package/dist/filesystem/protected-dirs.js +78 -0
  32. package/dist/filesystem/workspace.d.ts +12 -1
  33. package/dist/filesystem/workspace.d.ts.map +1 -1
  34. package/dist/filesystem/workspace.js +24 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -0
  37. package/dist/security/redact.d.ts +26 -3
  38. package/dist/security/redact.d.ts.map +1 -1
  39. package/dist/security/redact.js +48 -3
  40. package/dist/sessions/index-file.d.ts +38 -0
  41. package/dist/sessions/index-file.d.ts.map +1 -0
  42. package/dist/sessions/index-file.js +174 -0
  43. package/dist/sessions/lock.d.ts +39 -0
  44. package/dist/sessions/lock.d.ts.map +1 -0
  45. package/dist/sessions/lock.js +134 -0
  46. package/dist/sessions/open.d.ts +45 -0
  47. package/dist/sessions/open.d.ts.map +1 -0
  48. package/dist/sessions/open.js +96 -0
  49. package/dist/sessions/policy.d.ts +32 -0
  50. package/dist/sessions/policy.d.ts.map +1 -0
  51. package/dist/sessions/policy.js +57 -0
  52. package/dist/sessions/reconcile.d.ts +49 -0
  53. package/dist/sessions/reconcile.d.ts.map +1 -0
  54. package/dist/sessions/reconcile.js +115 -0
  55. package/dist/sessions/recorder.d.ts +42 -0
  56. package/dist/sessions/recorder.d.ts.map +1 -0
  57. package/dist/sessions/recorder.js +147 -0
  58. package/dist/sessions/resume.d.ts +67 -0
  59. package/dist/sessions/resume.d.ts.map +1 -0
  60. package/dist/sessions/resume.js +100 -0
  61. package/dist/sessions/store.d.ts +141 -0
  62. package/dist/sessions/store.d.ts.map +1 -0
  63. package/dist/sessions/store.js +272 -0
  64. package/dist/sessions/usage.d.ts +35 -0
  65. package/dist/sessions/usage.d.ts.map +1 -0
  66. package/dist/sessions/usage.js +157 -0
  67. package/dist/tui/agent-surface.d.ts +3 -0
  68. package/dist/tui/agent-surface.d.ts.map +1 -1
  69. package/dist/tui/agent-surface.js +23 -1
  70. package/dist/tui/app.d.ts.map +1 -1
  71. package/dist/tui/app.js +18 -1
  72. package/dist/tui/route.d.ts.map +1 -1
  73. package/dist/tui/route.js +19 -2
  74. package/dist/tui/session-centre.d.ts +15 -0
  75. package/dist/tui/session-centre.d.ts.map +1 -0
  76. package/dist/tui/session-centre.js +59 -0
  77. package/dist/tui/start-agent.d.ts +9 -0
  78. package/dist/tui/start-agent.d.ts.map +1 -1
  79. package/dist/tui/start-agent.js +122 -0
  80. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,69 @@ The version is the contract for the JSON output shapes and the exit codes.
6
6
  Being 0.x, semver still permits a break in a minor bump; the intent is not to
7
7
  use that latitude, and the promise becomes binding at 1.0.0.
8
8
 
9
+ ## 0.4.3 - 2026-08-11
10
+
11
+ A long agent run no longer walks into the model's context window, a one-shot
12
+ chat can be saved, and the Windows timeout kill now walks the process tree.
13
+
14
+ **Automatic compaction.** When a run gets close to the model's context window,
15
+ the oldest tool exchanges are dropped from what is sent and you are told it
16
+ happened. The size is measured rather than estimated: every response reports
17
+ how many input tokens it actually cost, which is the provider's own count of
18
+ exactly the conversation we sent. What gets dropped is always a whole exchange
19
+
20
+ - an assistant tool call and the results answering it are inseparable - and
21
+ the system prompt and your original goal are never touched. A model whose
22
+ context length the catalogue does not publish is left alone, because guessing
23
+ a window would either truncate a conversation that fit or fail to truncate one
24
+ that did not. Your saved transcript is unaffected; only what is sent gets
25
+ shorter.
26
+
27
+ **`mafdet chat "..." --save`** records a one-shot chat, through exactly the
28
+ same store as an interactive session - the same redaction, the same private
29
+ files, the same `mafdet sessions delete`. Chats are still not saved by
30
+ default, `--no-history` still wins over `--save`, and a chat you cancel is not
31
+ saved at all.
32
+
33
+ **Windows command timeouts** now use `taskkill /T`, then `taskkill /T /F`,
34
+ where POSIX uses SIGTERM then SIGKILL on the process group. Note the standing
35
+ caveat: this path has not been run on a real Windows machine, so a surviving
36
+ grandchild process there remains a known limitation rather than a fixed bug.
37
+
38
+ ## 0.4.2 - 2026-08-11
39
+
40
+ Conversations are now saved on this computer, and a session's cost can be
41
+ reconciled against the billing ledger.
42
+
43
+ `mafdet sessions list|show|usage|delete` browses what is stored locally. None
44
+ of it calls a model, so reading your own history is never charged. Recording
45
+ is on by default for interactive sessions and off wherever a machine is
46
+ reading the output; `--no-history` writes nothing at all.
47
+
48
+ The transcript is plain text and stays on this machine - it is not uploaded,
49
+ and Mafdet holds no copy. That is a statement about the archive, not about the
50
+ model: the messages you send are still sent to the API to be answered and
51
+ still appear in your usage records. The README says where the files live on
52
+ each platform, and that deleting one is an ordinary file delete rather than a
53
+ secure erase.
54
+
55
+ `mafdet sessions usage <id> --refresh` asks the billing ledger what the
56
+ requests actually cost and replaces the local estimate with the settled
57
+ figure. It sends the request IDs already in the transcript and nothing else -
58
+ no messages, no title, no workspace, no session ID. Requests paid for by a
59
+ different API key are left alone and reported rather than guessed at, and a
60
+ request the ledger has not settled stays `pending` rather than being shown as
61
+ zero.
62
+
63
+ The agent cannot read the CLI's own directories, even when you run `mafdet`
64
+ from your home directory and the session store is technically inside the
65
+ workspace.
66
+
67
+ Fixed on the way: the value recorded to attribute usage to a key was the
68
+ display fingerprint, which renders every live key identically - so a session
69
+ that used two keys reported them as one. It is now a digest that actually
70
+ differs per key, and still cannot be turned back into a key.
71
+
9
72
  ## 0.4.1 - 2026-08-07
10
73
 
11
74
  Fixes Ctrl+C in the interactive session, which did nothing while a model
package/README.md CHANGED
@@ -77,18 +77,20 @@ prompt as an argument, or use `--input <file>`.
77
77
 
78
78
  ## Commands
79
79
 
80
- | Command | What it does |
81
- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
82
- | `mafdet doctor` | One pass over key, endpoints, model, tool-calling, balance, workspace, git |
83
- | `mafdet models list [--surface chat\|embeddings\|speech] [--agent]` | Models this key can call, joined with catalogue metadata |
84
- | `mafdet models show <id>` | One model in detail, including whether the agent may drive it |
85
- | `mafdet chat [prompt] [--model] [--system <file>] [--input <file>] [--no-stream] [--quiet]` | Send a prompt; streams by default |
86
- | `mafdet agent "<goal>" [--allow-write] [--allow-command "<prefix>"]` | Local agent; read-only unless told otherwise |
87
- | `mafdet agent` (no goal, needs a terminal) | Interactive session |
88
- | `mafdet embed [text\|<file>] --model <id> [--output <file>]` | Text to vector |
89
- | `mafdet speech [text\|<file>] --model <id> --output <file> --voice <name>` | Text to an audio file |
90
- | `mafdet account balance\|usage\|subscription` | Wallet, spend and plan (needs the `account:read` scope) |
91
- | `mafdet config list` | The resolved settings and where each came from |
80
+ | Command | What it does |
81
+ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
82
+ | `mafdet doctor` | One pass over key, endpoints, model, tool-calling, balance, workspace, git |
83
+ | `mafdet models list [--surface chat\|embeddings\|speech] [--agent]` | Models this key can call, joined with catalogue metadata |
84
+ | `mafdet models show <id>` | One model in detail, including whether the agent may drive it |
85
+ | `mafdet chat [prompt] [--model] [--system <file>] [--input <file>] [--no-stream] [--quiet] [--save]` | Send a prompt; streams by default |
86
+ | `mafdet agent "<goal>" [--allow-write] [--allow-command "<prefix>"]` | Local agent; read-only unless told otherwise |
87
+ | `mafdet agent` (no goal, needs a terminal) | Interactive session |
88
+ | `mafdet embed [text\|<file>] --model <id> [--output <file>]` | Text to vector |
89
+ | `mafdet speech [text\|<file>] --model <id> --output <file> --voice <name>` | Text to an audio file |
90
+ | `mafdet account balance\|usage\|subscription` | Wallet, spend and plan (needs the `account:read` scope) |
91
+ | `mafdet sessions list\|show\|usage\|delete` | Conversations saved on this computer (never calls a model) |
92
+ | `mafdet sessions usage <id> --refresh` | Replace the local estimate with the billing ledger's figure |
93
+ | `mafdet config list` | The resolved settings and where each came from |
92
94
 
93
95
  `--json` on any query command emits one complete JSON document; `--plain`
94
96
  emits tab-separated rows with no header, for `cut` and `awk`.
@@ -181,6 +183,71 @@ flag: it would put the key into shell history and the process list. Keys are
181
183
  never written to a config file and never printed in full — only as a
182
184
  fingerprint like `sk-mafdet-live-<redacted>`.
183
185
 
186
+ ## Saved conversations
187
+
188
+ Interactive sessions are saved on this computer, and only on this computer.
189
+ They are not uploaded, and Mafdet holds no copy of them.
190
+
191
+ That is a statement about the transcript, not about the model. The messages you
192
+ send are still sent to the API to be answered and still appear in your usage
193
+ records; what stays local is the saved copy.
194
+
195
+ Where they live:
196
+
197
+ | Platform | Path |
198
+ | ----------- | --------------------------------------------------- |
199
+ | Linux / WSL | `${XDG_STATE_HOME:-~/.local/state}/mafdet/sessions` |
200
+ | macOS | `~/Library/Application Support/mafdet/sessions` |
201
+ | Windows | `%LOCALAPPDATA%\mafdet\sessions` |
202
+
203
+ The files are **plain text**. A transcript contains your messages, the model's
204
+ replies, and any source the agent read while working — so treat a session
205
+ directory the way you would treat the project it came from. API keys are the
206
+ one exception: everything is passed through redaction on the way to disk, so a
207
+ key you paste into a prompt is stored as `<redacted>`. Files are created
208
+ `0600` and directories `0700` on Linux and macOS; Windows has no equivalent
209
+ mode bits and inherits the ACL of `%LOCALAPPDATA%`.
210
+
211
+ The agent cannot read these directories, even when you run `mafdet` from your
212
+ home directory and the store is technically inside the workspace. Otherwise an
213
+ agent working on one project could read another project's transcript in full.
214
+
215
+ Turning it off and cleaning up:
216
+
217
+ ```bash
218
+ mafdet --no-history # this run is not recorded at all
219
+ mafdet chat "..." --save # a one-shot chat is saved only when asked
220
+ mafdet sessions list # what is saved, and the path when nothing is
221
+ mafdet sessions delete <id> --yes
222
+ ```
223
+
224
+ A single `mafdet chat` is not a conversation anyone resumes, so it is not
225
+ recorded unless `--save` is given. `--no-history` still wins over `--save`, and
226
+ a chat cancelled with Ctrl+C is not saved at all — half a reply is a worse
227
+ record than none.
228
+
229
+ `--no-history` writes nothing — not an empty directory, not an index entry.
230
+
231
+ Deleting removes the local messages and this session's usage mapping. It does
232
+ **not** remove API usage, billing records or charges: those belong to the API
233
+ key, not to the transcript. And it is an ordinary file delete, not a secure
234
+ erase — backups, snapshots and the filesystem itself may still hold copies.
235
+
236
+ ## Reconciling what a session cost
237
+
238
+ `mafdet sessions usage <id>` adds up what a conversation used from the local
239
+ record. Those charges are estimates, computed with the gateway's own
240
+ arithmetic but computed here.
241
+
242
+ `--refresh` asks the billing ledger for the real figure. It sends the request
243
+ IDs already in the transcript and nothing else — no messages, no title, no
244
+ workspace, no session ID. Requests paid for by a different API key are left
245
+ alone and reported, so you know to run it again with that key.
246
+
247
+ A request that the ledger has not caught up with stays `pending`, and a request
248
+ that never returned an ID stays `unknown`. Neither is ever shown as `0`, which
249
+ would read as "it was free".
250
+
184
251
  ## Cost and cancellation
185
252
 
186
253
  `chat` prints tokens and an estimated charge to stderr, computed with the same
package/README.zh-CN.md CHANGED
@@ -60,18 +60,20 @@ mafdet doctor
60
60
 
61
61
  ## 命令
62
62
 
63
- | 命令 | 作用 |
64
- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
65
- | `mafdet doctor` | 一次检查 Node、两个端点、Key、工作区、git、模型与工具调用能力、余额 |
66
- | `mafdet models list [--surface chat\|embeddings\|speech] [--agent]` | 这把 Key 能调用的模型,并附上目录元数据 |
67
- | `mafdet models show <id>` | 单个模型详情,含 Agent 是否可以驱动它 |
68
- | `mafdet chat [提示词] [--model] [--system <文件>] [--input <文件>] [--no-stream] [--quiet]` | 发一次对话,默认流式 |
69
- | `mafdet embed [文本\|<文件>] --model <id> [--output <文件>]` | 文本转向量 |
70
- | `mafdet speech [文本\|<文件>] --model <id> --output <文件> --voice <音色>` | 文本转音频 |
71
- | `mafdet agent "<目标>" [--allow-write] [--allow-command "<前缀>"]` | 本地 Agent,默认只读 |
72
- | `mafdet agent`(不带目标,需终端) | 交互会话 |
73
- | `mafdet account balance\|usage\|subscription` | 钱包、用量、套餐(需 `account:read` scope) |
74
- | `mafdet config list` | 当前生效的配置及其来源 |
63
+ | 命令 | 作用 |
64
+ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
65
+ | `mafdet doctor` | 一次检查 Node、两个端点、Key、工作区、git、模型与工具调用能力、余额 |
66
+ | `mafdet models list [--surface chat\|embeddings\|speech] [--agent]` | 这把 Key 能调用的模型,并附上目录元数据 |
67
+ | `mafdet models show <id>` | 单个模型详情,含 Agent 是否可以驱动它 |
68
+ | `mafdet chat [提示词] [--model] [--system <文件>] [--input <文件>] [--no-stream] [--quiet] [--save]` | 发一次对话,默认流式 |
69
+ | `mafdet embed [文本\|<文件>] --model <id> [--output <文件>]` | 文本转向量 |
70
+ | `mafdet speech [文本\|<文件>] --model <id> --output <文件> --voice <音色>` | 文本转音频 |
71
+ | `mafdet agent "<目标>" [--allow-write] [--allow-command "<前缀>"]` | 本地 Agent,默认只读 |
72
+ | `mafdet agent`(不带目标,需终端) | 交互会话 |
73
+ | `mafdet account balance\|usage\|subscription` | 钱包、用量、套餐(需 `account:read` scope) |
74
+ | `mafdet sessions list\|show\|usage\|delete` | 保存在这台电脑上的会话(不会调用模型) |
75
+ | `mafdet sessions usage <id> --refresh` | 用账单账本的权威金额替换本地估算 |
76
+ | `mafdet config list` | 当前生效的配置及其来源 |
75
77
 
76
78
  查询类命令都支持 `--json`(输出一份完整 JSON 文档)与 `--plain`(无表头的 TSV,方便
77
79
  `cut`、`awk` 处理)。
@@ -156,6 +158,61 @@ Key 只从 `MAFDET_API_KEY` 读取。**刻意不提供 `--api-key` 参数**—
156
158
  shell 历史和进程列表。Key 不会被写入任何配置文件,也不会完整出现在任何输出里,
157
159
  只以 `sk-mafdet-live-<redacted>` 这样的指纹形式展示。
158
160
 
161
+ ## 保存下来的会话
162
+
163
+ 交互式会话会保存在这台电脑上,**也只在这台电脑上**。它们不会上传,Mafdet 没有副本。
164
+
165
+ 这是关于**存档**的说法,不是关于模型的。你发送的消息仍然会送到 API 去作答,
166
+ 仍然出现在你的用量记录里;留在本地的是那份保存下来的副本。
167
+
168
+ 存放位置:
169
+
170
+ | 平台 | 路径 |
171
+ | ----------- | --------------------------------------------------- |
172
+ | Linux / WSL | `${XDG_STATE_HOME:-~/.local/state}/mafdet/sessions` |
173
+ | macOS | `~/Library/Application Support/mafdet/sessions` |
174
+ | Windows | `%LOCALAPPDATA%\mafdet\sessions` |
175
+
176
+ 这些文件是**明文**。一份存档包含你的消息、模型的回复,以及 agent 工作时读过的任何源码——
177
+ 所以请把会话目录当作它所对应的那个项目本身来对待。API Key 是唯一的例外:
178
+ 所有内容在写盘前都会过一遍脱敏,你粘进提示词里的 Key 会以 `<redacted>` 的形式存下来。
179
+ Linux 与 macOS 上文件权限 `0600`、目录 `0700`;**Windows 没有等价的权限位**,
180
+ 继承 `%LOCALAPPDATA%` 的 ACL。
181
+
182
+ **Agent 读不到这些目录**——即使你在家目录里运行 `mafdet`、存档实际落在工作区内也一样。
183
+ 否则在 A 项目里工作的 agent 就能读到 B 项目会话的完整明文。
184
+
185
+ 关闭与清理:
186
+
187
+ ```bash
188
+ mafdet --no-history # 这一次运行完全不记录
189
+ mafdet chat "..." --save # 一次性对话只有明确要求时才保存
190
+ mafdet sessions list # 已保存的会话;没有时会告诉你路径
191
+ mafdet sessions delete <id> --yes
192
+ ```
193
+
194
+ 一次 `mafdet chat` 不是有人会去恢复的对话,所以**不带 `--save` 就不保存**。
195
+ `--no-history` 仍然压过 `--save`;被 Ctrl+C 取消的对话**完全不保存**——
196
+ 半个回复是比没有更糟的记录。
197
+
198
+ `--no-history` 什么都不留——不留空目录,也不留索引条目。
199
+
200
+ 删除会移除本地消息和这个会话的用量映射。它**不会**移除 API 用量、账单记录或扣费:
201
+ 那些属于 API Key,不属于存档。而且它只是一次普通的文件删除,**不是安全擦除**——
202
+ 备份、快照和文件系统本身仍可能保有副本。
203
+
204
+ ## 对账:这次会话到底花了多少
205
+
206
+ `mafdet sessions usage <id>` 从本地记录累加一次会话的用量。那些金额是**估算**,
207
+ 算法与网关一致,但是在本地算的。
208
+
209
+ `--refresh` 会去问账单账本要真实金额。它只发送存档里已有的 request ID,**别的什么都不发**——
210
+ 不发消息、不发标题、不发工作区路径、不发 session ID。由另一把 API Key 支付的请求会被跳过
211
+ 并如实告知,好让你知道该换那把 Key 再跑一次。
212
+
213
+ 账本还没跟上的请求保持 `pending`,从未拿到 ID 的请求保持 `unknown`。
214
+ **两者都绝不显示为 `0`**——0 读起来像「免费」。
215
+
159
216
  ## 费用与取消
160
217
 
161
218
  `chat` 会在 stderr 打印 token 数与预估费用,算法与网关计费一致,但**以钱包为准**。
@@ -0,0 +1,37 @@
1
+ import type { ChatMessage } from "./loop.js";
2
+ /**
3
+ * Fraction of the usable window at which we start shortening.
4
+ *
5
+ * Not 1.0, and the gap is not caution for its own sake: the request that
6
+ * measured `prompt_tokens` is smaller than the next one will be, because the
7
+ * assistant reply, its tool results and the next user message all land in
8
+ * between and none of them are counted yet.
9
+ */
10
+ export declare const COMPACT_AT = 0.75;
11
+ /** Room left for the reply, which shares the window with the input. */
12
+ export declare function replyReserve(contextLength: number): number;
13
+ /** Tool exchanges kept whatever happens, so the recent work stays intact. */
14
+ export declare const KEEP_RECENT_EXCHANGES = 2;
15
+ /**
16
+ * Is the conversation close enough to the window to shorten it?
17
+ *
18
+ * `lastPromptTokens` is what the provider counted for the previous request in
19
+ * this run. Zero means nothing has been measured yet, and an unknown window
20
+ * means we have nothing to compare against.
21
+ */
22
+ export declare function shouldCompact(contextLength: number | undefined, lastPromptTokens: number): boolean;
23
+ export interface PruneResult {
24
+ /** Messages removed from the array. */
25
+ readonly removed: number;
26
+ /** Complete tool exchanges dropped. */
27
+ readonly exchanges: number;
28
+ }
29
+ /**
30
+ * Drop the oldest complete tool exchanges, in place.
31
+ *
32
+ * The system prompt and the opening goal are never touched - without the goal
33
+ * the model no longer knows what it is doing, which is a worse failure than
34
+ * running out of context.
35
+ */
36
+ export declare function pruneToolExchanges(history: ChatMessage[], keepRecent?: number): PruneResult;
37
+ //# sourceMappingURL=context-budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-budget.d.ts","sourceRoot":"","sources":["../../src/agent/context-budget.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,6EAA6E;AAC7E,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,wBAAgB,aAAa,CAC3B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAKT;AAED,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAsCD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,WAAW,CAkBb"}
@@ -0,0 +1,105 @@
1
+ // §7.4 · Keeping a long run inside the model's context window.
2
+ //
3
+ // The plan asks for "总上下文按模型窗口设置预算,接近上限时压缩历史". Two decisions
4
+ // shape what that can honestly be:
5
+ //
6
+ // The size of the history is *measured*, not estimated. Every response carries
7
+ // `prompt_tokens` for the request that produced it, which is the provider's own
8
+ // count of exactly the conversation we sent. Estimating instead would mean
9
+ // shipping a tokenizer per provider - CLI-000B already found the gateway's own
10
+ // reservation under-counts badly because it cannot see the tool schema, and a
11
+ // second wrong estimate here would be no better.
12
+ //
13
+ // And what gets dropped is whole tool exchanges, never individual messages.
14
+ // CLI-014 established the rule the hard way: an assistant message carrying
15
+ // `tool_calls` and the `tool` results answering them are inseparable, because
16
+ // the API rejects a tool result with no matching call and a call with no
17
+ // result. So the unit of removal is the pair, or nothing.
18
+ //
19
+ // When the model's context length is unknown, nothing happens at all. A guess
20
+ // at the window would either truncate a conversation that fit, or fail to
21
+ // truncate one that did not.
22
+ /**
23
+ * Fraction of the usable window at which we start shortening.
24
+ *
25
+ * Not 1.0, and the gap is not caution for its own sake: the request that
26
+ * measured `prompt_tokens` is smaller than the next one will be, because the
27
+ * assistant reply, its tool results and the next user message all land in
28
+ * between and none of them are counted yet.
29
+ */
30
+ export const COMPACT_AT = 0.75;
31
+ /** Room left for the reply, which shares the window with the input. */
32
+ export function replyReserve(contextLength) {
33
+ return Math.min(4096, Math.floor(contextLength / 8));
34
+ }
35
+ /** Tool exchanges kept whatever happens, so the recent work stays intact. */
36
+ export const KEEP_RECENT_EXCHANGES = 2;
37
+ /**
38
+ * Is the conversation close enough to the window to shorten it?
39
+ *
40
+ * `lastPromptTokens` is what the provider counted for the previous request in
41
+ * this run. Zero means nothing has been measured yet, and an unknown window
42
+ * means we have nothing to compare against.
43
+ */
44
+ export function shouldCompact(contextLength, lastPromptTokens) {
45
+ if (contextLength === undefined || contextLength <= 0)
46
+ return false;
47
+ if (lastPromptTokens <= 0)
48
+ return false;
49
+ const usable = contextLength - replyReserve(contextLength);
50
+ return usable > 0 && lastPromptTokens >= usable * COMPACT_AT;
51
+ }
52
+ /**
53
+ * The assistant-with-tool_calls messages and the tool results that answer them.
54
+ *
55
+ * An exchange is only counted when every one of its calls has a result: a
56
+ * partial one is the tail of a run still in progress, and removing half of it
57
+ * is precisely the shape the API refuses.
58
+ */
59
+ function exchangesIn(history) {
60
+ const found = [];
61
+ for (let i = 0; i < history.length; i += 1) {
62
+ const message = history[i];
63
+ if (message === undefined)
64
+ continue;
65
+ const calls = message.role === "assistant" ? message.tool_calls : undefined;
66
+ if (calls === undefined || calls.length === 0)
67
+ continue;
68
+ const answered = new Set();
69
+ let end = i + 1;
70
+ while (end < history.length && history[end]?.role === "tool") {
71
+ const id = history[end]?.tool_call_id;
72
+ if (typeof id === "string")
73
+ answered.add(id);
74
+ end += 1;
75
+ }
76
+ if (calls.every((call) => answered.has(call.id))) {
77
+ found.push({ start: i, end });
78
+ }
79
+ i = end - 1;
80
+ }
81
+ return found;
82
+ }
83
+ /**
84
+ * Drop the oldest complete tool exchanges, in place.
85
+ *
86
+ * The system prompt and the opening goal are never touched - without the goal
87
+ * the model no longer knows what it is doing, which is a worse failure than
88
+ * running out of context.
89
+ */
90
+ export function pruneToolExchanges(history, keepRecent = KEEP_RECENT_EXCHANGES) {
91
+ const exchanges = exchangesIn(history);
92
+ const droppable = exchanges.slice(0, Math.max(0, exchanges.length - keepRecent));
93
+ if (droppable.length === 0)
94
+ return { removed: 0, exchanges: 0 };
95
+ const drop = new Set();
96
+ for (const exchange of droppable) {
97
+ for (let i = exchange.start; i < exchange.end; i += 1)
98
+ drop.add(i);
99
+ }
100
+ const kept = history.filter((_, index) => !drop.has(index));
101
+ const removed = history.length - kept.length;
102
+ history.length = 0;
103
+ history.push(...kept);
104
+ return { removed, exchanges: droppable.length };
105
+ }
@@ -88,10 +88,35 @@ export type AgentEvent = {
88
88
  } | {
89
89
  type: "text";
90
90
  text: string;
91
+ }
92
+ /**
93
+ * One model request's accounting, as it happens. The summary already
94
+ * aggregates this; the event exists so a recorder can attribute each request
95
+ * to the ledger row it became (CLI-024/025) - `requestId` is the gateway's
96
+ * x-litellm-call-id, which is unique per row in request_ledger.
97
+ */
98
+ | {
99
+ type: "usage";
100
+ model: string;
101
+ requestId?: string;
102
+ promptTokens: number;
103
+ completionTokens: number;
104
+ costMicroUsd: bigint;
91
105
  } | {
92
106
  type: "rate_limited";
93
107
  waitMs: number;
94
108
  attempt: number;
109
+ }
110
+ /**
111
+ * §7.4 · The history was shortened to stay inside the model's window.
112
+ * Surfaced rather than done quietly: the user is entitled to know that
113
+ * earlier tool output is no longer being sent.
114
+ */
115
+ | {
116
+ type: "compacted";
117
+ exchanges: number;
118
+ messages: number;
119
+ promptTokens: number;
95
120
  };
96
121
  export interface ChatMessage {
97
122
  role: "system" | "user" | "assistant" | "tool";
@@ -1 +1 @@
1
- {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from "../api/errors.js";
2
+ import { pruneToolExchanges, shouldCompact } from "./context-budget.js";
2
3
  import { toolToJsonSchema, } from "./tools/types.js";
3
4
  /** Plan §7.4. */
4
5
  export const DEFAULT_LIMITS = {
@@ -44,6 +45,8 @@ export async function runAgent(options) {
44
45
  completionTokens: 0,
45
46
  costMicroUsd: 0n,
46
47
  };
48
+ /** What the provider counted for the previous request; 0 until one lands. */
49
+ let lastPromptTokens = 0;
47
50
  for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
48
51
  if (options.signal?.aborted)
49
52
  return stop(report, "cancelled");
@@ -55,6 +58,22 @@ export async function runAgent(options) {
55
58
  }
56
59
  report.turns = turn;
57
60
  options.onEvent?.({ type: "turn", turn });
61
+ // §7.4 · Shorten before asking, using what the provider counted last time.
62
+ // Only whole tool exchanges go, and only when the window is known.
63
+ if (shouldCompact(options.pricing?.contextLength, lastPromptTokens)) {
64
+ const pruned = pruneToolExchanges(messages);
65
+ if (pruned.exchanges > 0) {
66
+ options.onEvent?.({
67
+ type: "compacted",
68
+ exchanges: pruned.exchanges,
69
+ messages: pruned.removed,
70
+ promptTokens: lastPromptTokens,
71
+ });
72
+ // Reset the measurement: the next response measures the shorter
73
+ // history, and reusing the old figure would compact again every turn.
74
+ lastPromptTokens = 0;
75
+ }
76
+ }
58
77
  let response;
59
78
  try {
60
79
  response = await requestWithRateLimitHandling(options, limits, {
@@ -74,11 +93,23 @@ export async function runAgent(options) {
74
93
  return stop(report, "error", error instanceof Error ? error.message : String(error));
75
94
  }
76
95
  const usage = response.usage;
96
+ lastPromptTokens = usage?.prompt_tokens ?? lastPromptTokens;
77
97
  report.promptTokens += usage?.prompt_tokens ?? 0;
78
98
  report.completionTokens += usage?.completion_tokens ?? 0;
79
99
  // Cost comes from returned usage, never from the reservation: CLI-000B
80
100
  // showed the reserve cannot see the tool schema and under-counts badly.
81
- report.costMicroUsd += priceTurn(options.pricing, usage);
101
+ const turnCost = priceTurn(options.pricing, usage);
102
+ report.costMicroUsd += turnCost;
103
+ options.onEvent?.({
104
+ type: "usage",
105
+ model: options.model,
106
+ ...(response.requestId === undefined
107
+ ? {}
108
+ : { requestId: response.requestId }),
109
+ promptTokens: usage?.prompt_tokens ?? 0,
110
+ completionTokens: usage?.completion_tokens ?? 0,
111
+ costMicroUsd: turnCost,
112
+ });
82
113
  const choice = response.choices?.[0];
83
114
  const message = choice?.message;
84
115
  const calls = message?.tool_calls ?? [];
@@ -198,8 +229,10 @@ async function requestWithRateLimitHandling(options, limits, body, report, clock
198
229
  for (let attempt = 0;; attempt += 1) {
199
230
  try {
200
231
  report.modelRequests += 1;
201
- const { data } = await options.client.postJson("/chat/completions", body, { signal: options.signal });
202
- return data;
232
+ const { data, requestId } = await options.client.postJson("/chat/completions", body, { signal: options.signal });
233
+ // Carried on the response so the caller can tie this turn to its ledger
234
+ // row without the loop needing to know what a session is.
235
+ return { ...data, requestId };
203
236
  }
204
237
  catch (error) {
205
238
  // Everything except an explicit 429 propagates. A timeout or a 5xx may
@@ -32,6 +32,11 @@ export interface SessionOptions {
32
32
  * the same events are drawn.
33
33
  */
34
34
  onEvent?: (event: AgentEvent) => void;
35
+ /**
36
+ * A conversation to continue, rebuilt from a saved session (CLI-024).
37
+ * Appended to in place, exactly as the fresh array would be.
38
+ */
39
+ history?: ChatMessage[];
35
40
  }
36
41
  export interface SessionSummary {
37
42
  goals: number;
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAwFzB;AAgHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAwFzB;AAgHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
@@ -22,7 +22,7 @@ Anything else is sent to the model as the next goal.
22
22
  * rather than the last question.
23
23
  */
24
24
  export async function runSession(options) {
25
- const history = [];
25
+ const history = options.history ?? [];
26
26
  const summary = {
27
27
  goals: 0,
28
28
  turns: 0,
@@ -1 +1 @@
1
- {"version":3,"file":"read-only.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/read-only.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,YAAY,CAAC;AAmB9D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYjD;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuBjD;AAmLD,eAAO,MAAM,eAAe,EAAE,cAAc,EAM3C,CAAC"}
1
+ {"version":3,"file":"read-only.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/read-only.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,YAAY,CAAC;AAmB9D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAYjD;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAuBjD;AAwLD,eAAO,MAAM,eAAe,EAAE,cAAc,EAM3C,CAAC"}
@@ -87,6 +87,12 @@ function walk(workspace, onFile) {
87
87
  if (entry.isDirectory()) {
88
88
  if (SKIPPED_DIRECTORIES.has(entry.name))
89
89
  continue;
90
+ // The CLI's own directories, uniformly. Dot-directories are skipped
91
+ // anyway, which covers Linux and Windows by accident - but on macOS
92
+ // the state directory is `Library/Application Support/mafdet`, so
93
+ // without this the session ids would be listed there and nowhere else.
94
+ if (workspace.isOwnedByCli(full))
95
+ continue;
90
96
  stack.push(full);
91
97
  continue;
92
98
  }
@@ -15,6 +15,34 @@ export interface CommandResult {
15
15
  * failure mode where a "timed out" test run keeps holding a port.
16
16
  */
17
17
  export declare function executeCommand(argv: readonly string[], cwd: string, timeoutMs?: number): Promise<CommandResult>;
18
+ /**
19
+ * What "kill the whole tree" means on this platform.
20
+ *
21
+ * Pure, and separated from doing it, because the Windows branch cannot be
22
+ * executed here: there is no Windows machine in this project, and a kill path
23
+ * that has never run is exactly the kind of code that is wrong in a way only
24
+ * production finds. Keeping the decision pure means the *shape* of the command
25
+ * is under test on both platforms even though only one of them can be run.
26
+ *
27
+ * POSIX: a negative pid signals the process group, which is why the child is
28
+ * spawned detached. Killing the parent alone leaves grandchildren running -
29
+ * the familiar case where a "timed out" test runner keeps holding a port.
30
+ *
31
+ * Windows has no process groups in that sense. `taskkill /T` walks the child
32
+ * tree instead, and `/F` is the forceful pass: without it taskkill asks
33
+ * politely, which a console process is free to ignore - the same distinction
34
+ * as SIGTERM and SIGKILL.
35
+ */
36
+ export type KillPlan = {
37
+ readonly kind: "signal";
38
+ readonly target: number;
39
+ readonly signal: NodeJS.Signals;
40
+ } | {
41
+ readonly kind: "spawn";
42
+ readonly command: string;
43
+ readonly args: readonly string[];
44
+ };
45
+ export declare function killPlan(pid: number, force: boolean, platform?: string): KillPlan;
18
46
  export declare const runCommandTool: ToolDefinition;
19
47
  export declare const COMMAND_TOOLS: ToolDefinition[];
20
48
  //# sourceMappingURL=run-command.d.ts.map