@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.
- package/CHANGELOG.md +93 -0
- package/README.md +35 -19
- package/README.zh-CN.md +28 -16
- package/dist/agent/context-budget.d.ts +69 -0
- package/dist/agent/context-budget.d.ts.map +1 -0
- package/dist/agent/context-budget.js +169 -0
- package/dist/agent/loop.d.ts +19 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +32 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +7 -1
- package/dist/agent/tools/run-command.d.ts +28 -0
- package/dist/agent/tools/run-command.d.ts.map +1 -1
- package/dist/agent/tools/run-command.js +34 -13
- package/dist/api/http.d.ts +15 -1
- package/dist/api/http.d.ts.map +1 -1
- package/dist/api/http.js +26 -5
- package/dist/api/sse-parser.d.ts +26 -1
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +62 -21
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +76 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +136 -19
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +0 -2
- package/dist/commands/runtime.d.ts +6 -0
- package/dist/commands/runtime.d.ts.map +1 -1
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +129 -18
- package/dist/config/config.d.ts +0 -12
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +31 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/output/format.d.ts.map +1 -1
- package/dist/output/format.js +10 -5
- package/dist/output/terminal-safe.d.ts +12 -0
- package/dist/output/terminal-safe.d.ts.map +1 -0
- package/dist/output/terminal-safe.js +38 -0
- package/dist/sessions/open.d.ts.map +1 -1
- package/dist/sessions/open.js +26 -2
- package/dist/sessions/recorder.d.ts.map +1 -1
- package/dist/sessions/recorder.js +6 -1
- package/dist/sessions/session-id.d.ts +16 -0
- package/dist/sessions/session-id.d.ts.map +1 -0
- package/dist/sessions/session-id.js +93 -0
- package/dist/sessions/store.d.ts +41 -2
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +48 -24
- package/dist/sessions/usage.d.ts +6 -0
- package/dist/sessions/usage.d.ts.map +1 -1
- package/dist/sessions/usage.js +12 -1
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +13 -2
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +43 -10
- package/dist/tui/start-agent.d.ts +31 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +147 -45
- 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.
|
|
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.
|
|
29
|
-
npm install -g ./mafdet-cli-0.4.
|
|
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
|
|
81
|
-
|
|
|
82
|
-
| `mafdet doctor`
|
|
83
|
-
| `mafdet models list [--surface chat\|embeddings\|speech] [--agent]`
|
|
84
|
-
| `mafdet models show <id>`
|
|
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>"]`
|
|
87
|
-
| `mafdet agent` (no goal, needs a terminal)
|
|
88
|
-
| `mafdet embed [text\|<file>] --model <id> [--output <file>]`
|
|
89
|
-
| `mafdet speech [text\|<file>] --model <id> --output <file> --voice <name>`
|
|
90
|
-
| `mafdet account balance\|usage\|subscription`
|
|
91
|
-
| `mafdet sessions list\|show\|usage\|delete`
|
|
92
|
-
| `mafdet sessions usage <id> --refresh`
|
|
93
|
-
| `mafdet config list`
|
|
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
|
|
135
|
-
|
|
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.
|
|
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`
|
|
66
|
-
| `mafdet models list [--surface chat\|embeddings\|speech] [--agent]`
|
|
67
|
-
| `mafdet models show <id>`
|
|
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 "<前缀>"]`
|
|
72
|
-
| `mafdet agent`(不带目标,需终端)
|
|
73
|
-
| `mafdet account balance\|usage\|subscription`
|
|
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
|
|
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
|
+
}
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -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"
|
|
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";
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|