@mafdet/cli 0.4.2 → 0.4.4

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 (61) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +35 -19
  3. package/README.zh-CN.md +28 -16
  4. package/dist/agent/context-budget.d.ts +69 -0
  5. package/dist/agent/context-budget.d.ts.map +1 -0
  6. package/dist/agent/context-budget.js +169 -0
  7. package/dist/agent/loop.d.ts +19 -1
  8. package/dist/agent/loop.d.ts.map +1 -1
  9. package/dist/agent/loop.js +32 -0
  10. package/dist/agent/session.d.ts.map +1 -1
  11. package/dist/agent/session.js +7 -1
  12. package/dist/agent/tools/run-command.d.ts +28 -0
  13. package/dist/agent/tools/run-command.d.ts.map +1 -1
  14. package/dist/agent/tools/run-command.js +34 -13
  15. package/dist/api/http.d.ts +15 -1
  16. package/dist/api/http.d.ts.map +1 -1
  17. package/dist/api/http.js +26 -5
  18. package/dist/api/sse-parser.d.ts +26 -1
  19. package/dist/api/sse-parser.d.ts.map +1 -1
  20. package/dist/api/sse-parser.js +62 -21
  21. package/dist/commands/agent.d.ts.map +1 -1
  22. package/dist/commands/agent.js +76 -0
  23. package/dist/commands/chat.d.ts.map +1 -1
  24. package/dist/commands/chat.js +136 -19
  25. package/dist/commands/config.d.ts.map +1 -1
  26. package/dist/commands/config.js +0 -2
  27. package/dist/commands/runtime.d.ts +6 -0
  28. package/dist/commands/runtime.d.ts.map +1 -1
  29. package/dist/commands/sessions.d.ts.map +1 -1
  30. package/dist/commands/sessions.js +129 -18
  31. package/dist/config/config.d.ts +0 -12
  32. package/dist/config/config.d.ts.map +1 -1
  33. package/dist/config/config.js +31 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +17 -1
  36. package/dist/output/format.d.ts.map +1 -1
  37. package/dist/output/format.js +10 -5
  38. package/dist/output/terminal-safe.d.ts +12 -0
  39. package/dist/output/terminal-safe.d.ts.map +1 -0
  40. package/dist/output/terminal-safe.js +38 -0
  41. package/dist/sessions/open.d.ts.map +1 -1
  42. package/dist/sessions/open.js +26 -2
  43. package/dist/sessions/recorder.d.ts.map +1 -1
  44. package/dist/sessions/recorder.js +6 -1
  45. package/dist/sessions/session-id.d.ts +16 -0
  46. package/dist/sessions/session-id.d.ts.map +1 -0
  47. package/dist/sessions/session-id.js +93 -0
  48. package/dist/sessions/store.d.ts +41 -2
  49. package/dist/sessions/store.d.ts.map +1 -1
  50. package/dist/sessions/store.js +48 -24
  51. package/dist/sessions/usage.d.ts +6 -0
  52. package/dist/sessions/usage.d.ts.map +1 -1
  53. package/dist/sessions/usage.js +12 -1
  54. package/dist/tui/agent-surface.d.ts.map +1 -1
  55. package/dist/tui/agent-surface.js +13 -2
  56. package/dist/tui/app.d.ts.map +1 -1
  57. package/dist/tui/app.js +43 -10
  58. package/dist/tui/start-agent.d.ts +31 -0
  59. package/dist/tui/start-agent.d.ts.map +1 -1
  60. package/dist/tui/start-agent.js +147 -45
  61. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,99 @@ 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.4 - 2026-08-11
10
+
11
+ A security fix and twenty-one other corrections, all from an external audit of
12
+ the 0.4.3 release.
13
+
14
+ **Session ids are now checked before they become paths.** `mafdet sessions
15
+ show`, `usage`, `delete` and `--resume` each joined whatever you typed onto
16
+ the sessions directory, and `join` resolves `..` - so an id like
17
+ `../../../something` reached outside the store. Reading it leaked another
18
+ directory's contents, and `sessions delete ../../../something --yes` removed
19
+ that directory's files. This was present from 0.3.0. It requires you (or a
20
+ script running as you) to pass such a path - the agent has no way to invoke
21
+ these commands - but it was a real destructive defect and it is fixed.
22
+
23
+ **Resuming a conversation now honours what it says.** Blockers were computed
24
+ and printed and then ignored, so a session whose workspace had been deleted,
25
+ or whose model your key can no longer call, continued anyway - and it
26
+ continued in your _current_ directory rather than the recorded one, which with
27
+ `--allow-write` meant carrying one project's conversation into another
28
+ project's files. A resumed session now runs where it was recorded, on the
29
+ model it was recorded with, and refuses when it cannot.
30
+
31
+ **Deleting reports what happened.** Every failure used to be swallowed and
32
+ "deleted" printed regardless. It now takes the session lock first and only
33
+ claims success when the directory is gone.
34
+
35
+ **`--read-only` works in the interactive session.** It existed only in `mafdet
36
+ agent`, so `mafdet --allow-write --read-only` was read-only there and
37
+ write-enabled here.
38
+
39
+ **Streams have limits.** The request timeout was cleared when the response
40
+ headers arrived, which for a stream is the beginning: a gateway that then went
41
+ quiet hung the CLI indefinitely. There is now a per-chunk idle deadline and a
42
+ byte cap, and both close the connection.
43
+
44
+ **Terminal control sequences.** A model's reply and a session's title, model
45
+ and workspace are stripped of escape sequences when a terminal is reading
46
+ them. Redirected output still receives exact bytes.
47
+
48
+ **Context compaction covers conversations without tools**, and when nothing
49
+ can safely be dropped the run stops and says so instead of sending a request
50
+ that is over the window.
51
+
52
+ **An unpriced request is no longer recorded as costing zero** - unknown and
53
+ free are different claims.
54
+
55
+ **`approvalMode` and `testCommands` are removed from the config.** They were
56
+ accepted, displayed by `config list`, and read by nothing, so setting
57
+ `auto-edit` looked like it turned approval off. They are removed rather than
58
+ implemented because in a project file both would let a cloned repository raise
59
+ its own permission level. Setting one is now an error explaining why.
60
+
61
+ **A damaged line in the middle of a transcript is reported** rather than
62
+ skipped in silence, and resuming from one is refused.
63
+
64
+ Also: `mafdet agent` gains `--title` and `--no-history` (`--resume` stays in
65
+ the interactive session and now says so); the model picker highlights the
66
+ default you saved; `sessions` refuses unknown flags and stray arguments
67
+ instead of ignoring them; `--json --plain` prints a usage error rather than a
68
+ stack trace; the resume lock is released on exit; `mafdet help` describes what
69
+ the agent can actually do; and the README states plainly that approving a
70
+ command means allowing that program to run as you - the workspace bounds the
71
+ agent's own tools, not the program, and is not a sandbox.
72
+
73
+ ## 0.4.3 - 2026-08-11
74
+
75
+ A long agent run no longer walks into the model's context window, a one-shot
76
+ chat can be saved, and the Windows timeout kill now walks the process tree.
77
+
78
+ **Automatic compaction.** When a run gets close to the model's context window,
79
+ the oldest tool exchanges are dropped from what is sent and you are told it
80
+ happened. The size is measured rather than estimated: every response reports
81
+ how many input tokens it actually cost, which is the provider's own count of
82
+ exactly the conversation we sent. What gets dropped is always a whole exchange
83
+
84
+ - an assistant tool call and the results answering it are inseparable - and
85
+ the system prompt and your original goal are never touched. A model whose
86
+ context length the catalogue does not publish is left alone, because guessing
87
+ a window would either truncate a conversation that fit or fail to truncate one
88
+ that did not. Your saved transcript is unaffected; only what is sent gets
89
+ shorter.
90
+
91
+ **`mafdet chat "..." --save`** records a one-shot chat, through exactly the
92
+ same store as an interactive session - the same redaction, the same private
93
+ files, the same `mafdet sessions delete`. Chats are still not saved by
94
+ default, `--no-history` still wins over `--save`, and a chat you cancel is not
95
+ saved at all.
96
+
97
+ **Windows command timeouts** now use `taskkill /T`, then `taskkill /T /F`,
98
+ where POSIX uses SIGTERM then SIGKILL on the process group. Note the standing
99
+ caveat: this path has not been run on a real Windows machine, so a surviving
100
+ grandchild process there remains a known limitation rather than a fixed bug.
101
+
9
102
  ## 0.4.2 - 2026-08-11
10
103
 
11
104
  Conversations are now saved on this computer, and a session's cost can be
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The official Mafdet AI command line: model calls, account queries, and a
4
4
  permissioned local agent.
5
5
 
6
- > **0.4.1.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
6
+ > **0.4.4.** 中文版见 [README.zh-CN.md](./README.zh-CN.md). The CLI never
7
7
  > advertises a command it cannot serve, so `mafdet help` always lists exactly
8
8
  > what works.
9
9
  >
@@ -25,8 +25,8 @@ Or build it from this repository:
25
25
 
26
26
  ```bash
27
27
  pnpm --filter @mafdet/cli build
28
- cd packages/cli && npm pack # -> mafdet-cli-0.4.1.tgz
29
- npm install -g ./mafdet-cli-0.4.1.tgz
28
+ cd packages/cli && npm pack # -> mafdet-cli-0.4.4.tgz
29
+ npm install -g ./mafdet-cli-0.4.4.tgz
30
30
  ```
31
31
 
32
32
  ## The interactive session
@@ -77,20 +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 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 |
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 |
94
94
 
95
95
  `--json` on any query command emits one complete JSON document; `--plain`
96
96
  emits tab-separated rows with no header, for `cut` and `awk`.
@@ -113,6 +113,16 @@ prefixes: `pnpm test` permits `pnpm test --filter web` but never `pnpm publish`.
113
113
  `;`, `&&`, backticks and `$( )` are refused with an explanation rather than
114
114
  sanitised.
115
115
 
116
+ **What approving a command means.** It means allowing that program to run on
117
+ your machine, as you. The workspace bounds which files the agent's own tools
118
+ may touch; it is not a sandbox, and it does not bound the program. `pnpm test`
119
+ can read your home directory, reach the network, or start something that
120
+ outlives the run - because that is what running a program is. A short list of
121
+ obviously destructive names is refused (`sudo`, `su`, `doas`, `chown`,
122
+ `shutdown`, `reboot`, and invoking a program by path), but treat that as a
123
+ guard against slips rather than as containment. Approve prefixes the way you
124
+ would run the command yourself.
125
+
116
126
  In a session: `/model`, `/permissions`, `/diff`, `/cost`, `/compact`, `/exit`.
117
127
 
118
128
  ## Configuration
@@ -131,8 +141,8 @@ User config lives at `~/.config/mafdet/config.json` (Linux),
131
141
  `~/Library/Application Support/mafdet/config.json` (macOS) or
132
142
  `%APPDATA%\mafdet\config.json` (Windows).
133
143
 
134
- A project may carry `.mafdet.json` with `model`, `approvalMode` and
135
- `testCommands` — and nothing else. Credentials and endpoints in a project file
144
+ A project may carry `.mafdet.json` with `model` — and nothing else.
145
+ Credentials and endpoints in a project file
136
146
  are rejected outright: cloning a repository must never change where your key
137
147
  is sent.
138
148
 
@@ -216,10 +226,16 @@ Turning it off and cleaning up:
216
226
 
217
227
  ```bash
218
228
  mafdet --no-history # this run is not recorded at all
229
+ mafdet chat "..." --save # a one-shot chat is saved only when asked
219
230
  mafdet sessions list # what is saved, and the path when nothing is
220
231
  mafdet sessions delete <id> --yes
221
232
  ```
222
233
 
234
+ A single `mafdet chat` is not a conversation anyone resumes, so it is not
235
+ recorded unless `--save` is given. `--no-history` still wins over `--save`, and
236
+ a chat cancelled with Ctrl+C is not saved at all — half a reply is a worse
237
+ record than none.
238
+
223
239
  `--no-history` writes nothing — not an empty directory, not an index entry.
224
240
 
225
241
  Deleting removes the local messages and this session's usage mapping. It does
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Mafdet AI 官方命令行:模型调用、账户查询,以及一个受权限约束的本地 Agent。
4
4
 
5
- > **0.4.1。** 英文版见 [README.md](./README.md)。
5
+ > **0.4.4。** 英文版见 [README.md](./README.md)。
6
6
 
7
7
  ## 环境要求
8
8
 
@@ -60,20 +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 sessions list\|show\|usage\|delete` | 保存在这台电脑上的会话(不会调用模型) |
75
- | `mafdet sessions usage <id> --refresh` | 用账单账本的权威金额替换本地估算 |
76
- | `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` | 当前生效的配置及其来源 |
77
77
 
78
78
  查询类命令都支持 `--json`(输出一份完整 JSON 文档)与 `--plain`(无表头的 TSV,方便
79
79
  `cut`、`awk` 处理)。
@@ -94,6 +94,13 @@ mafdet doctor
94
94
  **没有 shell**:命令被切成 argv 直接执行,所以管道、重定向、`;`、`&&`、反引号、`$( )`
95
95
  一律被拒绝并说明原因,而不是尝试"消毒"。
96
96
 
97
+ **批准一条命令意味着什么。** 意味着允许那个程序在你的机器上、以你的身份运行。
98
+ 工作区限定的是 **Agent 自己的工具**能碰哪些文件,它**不是沙箱**,也约束不了那个程序。
99
+ `pnpm test` 可以读你的家目录、可以联网、可以起一个比这次运行活得更久的进程——
100
+ 因为那就是"运行一个程序"的含义。我们会拒绝一小份明显破坏性的名字
101
+ (`sudo`、`su`、`doas`、`chown`、`shutdown`、`reboot`,以及按路径调用),
102
+ 但请把它当作**防手滑的护栏,而不是隔离**。批准前缀时,请按"我自己会不会直接敲这条命令"来判断。
103
+
97
104
  会话内命令:`/model`、`/permissions`、`/diff`、`/cost`、`/compact`、`/exit`。
98
105
 
99
106
  ## 配置
@@ -111,7 +118,7 @@ mafdet doctor
111
118
  用户配置位置:Linux `~/.config/mafdet/config.json`、macOS
112
119
  `~/Library/Application Support/mafdet/config.json`、Windows `%APPDATA%\mafdet\config.json`。
113
120
 
114
- 项目可以放一个 `.mafdet.json`,但**只允许** `model`、`approvalMode`、`testCommands` 三项。
121
+ 项目可以放一个 `.mafdet.json`,但**只允许 `model` 一项**。
115
122
  里面出现凭据或端点会**直接报错**:克隆一个仓库绝不该改变你的 Key 发往哪里。
116
123
 
117
124
  ## 退出码
@@ -186,10 +193,15 @@ Linux 与 macOS 上文件权限 `0600`、目录 `0700`;**Windows 没有等价
186
193
 
187
194
  ```bash
188
195
  mafdet --no-history # 这一次运行完全不记录
196
+ mafdet chat "..." --save # 一次性对话只有明确要求时才保存
189
197
  mafdet sessions list # 已保存的会话;没有时会告诉你路径
190
198
  mafdet sessions delete <id> --yes
191
199
  ```
192
200
 
201
+ 一次 `mafdet chat` 不是有人会去恢复的对话,所以**不带 `--save` 就不保存**。
202
+ `--no-history` 仍然压过 `--save`;被 Ctrl+C 取消的对话**完全不保存**——
203
+ 半个回复是比没有更糟的记录。
204
+
193
205
  `--no-history` 什么都不留——不留空目录,也不留索引条目。
194
206
 
195
207
  删除会移除本地消息和这个会话的用量映射。它**不会**移除 API 用量、账单记录或扣费:
@@ -0,0 +1,69 @@
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
+ /**
38
+ * F-008 · Drop the oldest whole turns.
39
+ *
40
+ * The exchange-based prune above cannot help a conversation with no tools in
41
+ * it - a long back-and-forth of plain questions removed nothing and carried on
42
+ * requesting, so "a long run no longer walks into the context window" was only
43
+ * true for tool-using runs.
44
+ *
45
+ * A turn starts at a user message and runs to just before the next one, so
46
+ * cutting on that boundary can never orphan a tool result or leave a tool call
47
+ * unanswered: CLI-014's rule holds by construction. The system prompt and the
48
+ * first user message - the goal - always stay.
49
+ */
50
+ export declare function dropOldestTurns(history: ChatMessage[], keepTurns: number): number;
51
+ export interface CompactionResult extends PruneResult {
52
+ /** Whole turns dropped, when exchanges were not enough. */
53
+ readonly turns: number;
54
+ /** True when nothing could be dropped and the history is still too big. */
55
+ readonly exhausted: boolean;
56
+ }
57
+ /**
58
+ * Shorten the history as much as can be done safely.
59
+ *
60
+ * Tool exchanges go first because they are the bulk of a long agent run and
61
+ * the least useful to keep. Only when there are none left does it start
62
+ * dropping turns, which costs the model more context.
63
+ *
64
+ * `exhausted` is the honest outcome when neither helped: the caller must stop
65
+ * rather than send a request that is over the window, which would fail
66
+ * upstream and be billed for the attempt.
67
+ */
68
+ export declare function compactForBudget(history: ChatMessage[], keepRecent?: number): CompactionResult;
69
+ //# 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;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,MAAM,GAChB,MAAM,CAuBR;AAED,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EAAE,EACtB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CAYlB"}
@@ -0,0 +1,169 @@
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
+ }
106
+ /**
107
+ * F-008 · Drop the oldest whole turns.
108
+ *
109
+ * The exchange-based prune above cannot help a conversation with no tools in
110
+ * it - a long back-and-forth of plain questions removed nothing and carried on
111
+ * requesting, so "a long run no longer walks into the context window" was only
112
+ * true for tool-using runs.
113
+ *
114
+ * A turn starts at a user message and runs to just before the next one, so
115
+ * cutting on that boundary can never orphan a tool result or leave a tool call
116
+ * unanswered: CLI-014's rule holds by construction. The system prompt and the
117
+ * first user message - the goal - always stay.
118
+ */
119
+ export function dropOldestTurns(history, keepTurns) {
120
+ const userAt = [];
121
+ for (let i = 0; i < history.length; i += 1) {
122
+ if (history[i]?.role === "user")
123
+ userAt.push(i);
124
+ }
125
+ // The first user message is the goal and is not a droppable turn, so there
126
+ // have to be more than `keepTurns` turns after it before anything can go.
127
+ if (userAt.length <= keepTurns + 1)
128
+ return 0;
129
+ const cut = userAt[userAt.length - keepTurns];
130
+ if (cut === undefined)
131
+ return 0;
132
+ const kept = [];
133
+ if (history[0]?.role === "system")
134
+ kept.push(history[0]);
135
+ const goal = history[userAt[0]];
136
+ if (goal !== undefined && userAt[0] !== cut)
137
+ kept.push(goal);
138
+ kept.push(...history.slice(cut));
139
+ const removed = history.length - kept.length;
140
+ if (removed <= 0)
141
+ return 0;
142
+ history.length = 0;
143
+ history.push(...kept);
144
+ return removed;
145
+ }
146
+ /**
147
+ * Shorten the history as much as can be done safely.
148
+ *
149
+ * Tool exchanges go first because they are the bulk of a long agent run and
150
+ * the least useful to keep. Only when there are none left does it start
151
+ * dropping turns, which costs the model more context.
152
+ *
153
+ * `exhausted` is the honest outcome when neither helped: the caller must stop
154
+ * rather than send a request that is over the window, which would fail
155
+ * upstream and be billed for the attempt.
156
+ */
157
+ export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
158
+ const pruned = pruneToolExchanges(history, keepRecent);
159
+ if (pruned.exchanges > 0) {
160
+ return { ...pruned, turns: 0, exhausted: false };
161
+ }
162
+ const turnsRemoved = dropOldestTurns(history, keepRecent);
163
+ return {
164
+ removed: turnsRemoved,
165
+ exchanges: 0,
166
+ turns: turnsRemoved > 0 ? 1 : 0,
167
+ exhausted: turnsRemoved === 0,
168
+ };
169
+ }
@@ -12,7 +12,9 @@ export declare const DEFAULT_LIMITS: {
12
12
  maxCostMicroUsd: bigint | undefined;
13
13
  };
14
14
  export type AgentLimits = typeof DEFAULT_LIMITS;
15
- export type StopReason = "completed" | "max_turns" | "timeout" | "max_cost" | "cancelled" | "rate_limited" | "error";
15
+ export type StopReason = "completed" | "max_turns" | "timeout" | "max_cost" | "cancelled" | "rate_limited"
16
+ /** F-008 · Over the model's window with nothing left that is safe to drop. */
17
+ | "context_limit" | "error";
16
18
  export interface AgentReport {
17
19
  stopReason: StopReason;
18
20
  /** The model's last text answer, if it produced one. */
@@ -102,10 +104,26 @@ export type AgentEvent = {
102
104
  promptTokens: number;
103
105
  completionTokens: number;
104
106
  costMicroUsd: bigint;
107
+ /**
108
+ * F-012 · False when the catalogue had no price for this model, so a
109
+ * recorder can tell "we could not work it out" from "it was free".
110
+ */
111
+ priced: boolean;
105
112
  } | {
106
113
  type: "rate_limited";
107
114
  waitMs: number;
108
115
  attempt: number;
116
+ }
117
+ /**
118
+ * §7.4 · The history was shortened to stay inside the model's window.
119
+ * Surfaced rather than done quietly: the user is entitled to know that
120
+ * earlier tool output is no longer being sent.
121
+ */
122
+ | {
123
+ type: "compacted";
124
+ exchanges: number;
125
+ messages: number;
126
+ promptTokens: number;
109
127
  };
110
128
  export interface ChatMessage {
111
129
  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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"}