@mafdet/cli 0.4.4 → 0.4.6
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 +81 -0
- package/README.md +4 -6
- package/README.zh-CN.md +4 -3
- package/dist/agent/command-policy.d.ts.map +1 -1
- package/dist/agent/command-policy.js +19 -6
- package/dist/agent/context-budget.d.ts +19 -1
- package/dist/agent/context-budget.d.ts.map +1 -1
- package/dist/agent/context-budget.js +87 -12
- package/dist/agent/loop.d.ts +16 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +34 -2
- package/dist/agent/session.d.ts +14 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +15 -4
- package/dist/api/models.d.ts.map +1 -1
- package/dist/api/models.js +28 -2
- package/dist/api/sse-parser.d.ts +15 -0
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +39 -3
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +88 -30
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +45 -10
- package/dist/output/money.d.ts +9 -0
- package/dist/output/money.d.ts.map +1 -1
- package/dist/output/money.js +14 -0
- package/dist/sessions/index-file.d.ts +2 -0
- package/dist/sessions/index-file.d.ts.map +1 -1
- package/dist/sessions/index-file.js +3 -0
- package/dist/sessions/open.d.ts.map +1 -1
- package/dist/sessions/open.js +3 -1
- package/dist/sessions/resume.d.ts +1 -0
- package/dist/sessions/resume.d.ts.map +1 -1
- package/dist/sessions/resume.js +5 -3
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +18 -12
- package/dist/tui/app.d.ts +14 -0
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +45 -11
- package/dist/tui/picker.d.ts.map +1 -1
- package/dist/tui/picker.js +2 -1
- package/dist/tui/session-centre.d.ts +5 -0
- package/dist/tui/session-centre.d.ts.map +1 -1
- package/dist/tui/session-centre.js +14 -4
- package/dist/tui/start-agent.d.ts +28 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +39 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,87 @@ 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.6 - 2026-08-12
|
|
10
|
+
|
|
11
|
+
A third audit round. Two of its findings were things the previous round's
|
|
12
|
+
notes recorded as fixed, and the pattern is worth naming: a partial fix logged
|
|
13
|
+
as complete is not looked at again.
|
|
14
|
+
|
|
15
|
+
**An interactive `mafdet agent` now records your questions.** It saved the
|
|
16
|
+
model's replies, the tool runs and the usage, and none of the prompts - so
|
|
17
|
+
resuming such a session could not continue a conversation that was only half
|
|
18
|
+
there. The TUI wrapped its own reader to record them and the command surface
|
|
19
|
+
did not; recording now happens in the session loop, where neither can forget.
|
|
20
|
+
|
|
21
|
+
**`--max-cost-usd` refuses before spending, not after.** When the catalogue
|
|
22
|
+
publishes no price for a model, the run used to make one billable request and
|
|
23
|
+
discover the problem from the response. Whether a price exists is known before
|
|
24
|
+
anything is sent.
|
|
25
|
+
|
|
26
|
+
**A run nobody could price no longer reports `$0.00`.** The command summary,
|
|
27
|
+
the TUI summary, `/cost` and the JSON report all showed zero for a run whose
|
|
28
|
+
model had no published price; they now say how many requests the figure does
|
|
29
|
+
not cover, as `sessions usage` already did.
|
|
30
|
+
|
|
31
|
+
**Context compaction accounts for what cannot shrink.** The estimate assumed
|
|
32
|
+
tokens fall in proportion to messages, but the system prompt and tool schemas
|
|
33
|
+
are re-sent every turn - so it could decide a history had come inside the
|
|
34
|
+
window when only the removable part had.
|
|
35
|
+
|
|
36
|
+
**Model catalogue text is sanitised where it enters.** Terminal escape
|
|
37
|
+
sequences were being stripped sink by sink, and the model picker was still
|
|
38
|
+
missing after two rounds; cleaning the catalogue on arrival leaves nothing
|
|
39
|
+
downstream to forget. Fixed with it: stripping a sequence no longer eats the
|
|
40
|
+
text that follows it, which an 8-bit OSC terminated by BEL used to do.
|
|
41
|
+
|
|
42
|
+
Also: the stream's total deadline actually bounds the wait instead of allowing
|
|
43
|
+
one more idle period; compaction reports how many turns it dropped rather than
|
|
44
|
+
always one; the session picker and the per-model and per-key JSON carry the
|
|
45
|
+
unpriced count.
|
|
46
|
+
|
|
47
|
+
## 0.4.5 - 2026-08-12
|
|
48
|
+
|
|
49
|
+
A second audit round, and the security fix in it is the important one.
|
|
50
|
+
|
|
51
|
+
**With no tool-calling model, the interactive session no longer opens.** It
|
|
52
|
+
used to print a warning and then offer every chat model - so the one case with
|
|
53
|
+
nothing safe to offer was the case that offered everything, and the agent then
|
|
54
|
+
ran on a model whose tool calling had never been verified. `mafdet agent`
|
|
55
|
+
refused the same model outright, so the two surfaces disagreed about
|
|
56
|
+
admission. They no longer do.
|
|
57
|
+
|
|
58
|
+
**A cost ceiling that cannot be enforced stops the run.** When the catalogue
|
|
59
|
+
has no price for a model, each turn counted as zero towards `--max-cost-usd`,
|
|
60
|
+
so the ceiling quietly stopped being one while the run kept spending. Asking
|
|
61
|
+
for a limit we cannot apply now ends the run and says why.
|
|
62
|
+
|
|
63
|
+
**Context compaction keeps going until the history fits.** It used to drop a
|
|
64
|
+
single tool exchange and send regardless, which changes nothing when the
|
|
65
|
+
history is several times over the window - the request then failed upstream
|
|
66
|
+
and was billed for the attempt.
|
|
67
|
+
|
|
68
|
+
**A stream is closed when the answer ends.** `[DONE]` means the reply is
|
|
69
|
+
complete, not that the socket is; an upstream that kept the connection open
|
|
70
|
+
held ours too. There is also a total deadline now, because a trickle of bytes
|
|
71
|
+
defeats an idle one.
|
|
72
|
+
|
|
73
|
+
**More of what reaches the terminal is sanitised**: the answer from `mafdet
|
|
74
|
+
agent`, the interactive session's answer, the session picker, the resume
|
|
75
|
+
notice and the remaining `sessions` columns. The stripper also handles the
|
|
76
|
+
8-bit C1 forms, which do the same things without an escape character.
|
|
77
|
+
|
|
78
|
+
**An unpriced request is no longer displayed as `$0.00`** in `sessions list`,
|
|
79
|
+
`show` or `usage`, and the JSON carries `unpriced_requests` so a script can
|
|
80
|
+
tell the difference too.
|
|
81
|
+
|
|
82
|
+
Also: `sessions list` refuses stray arguments like the other subcommands;
|
|
83
|
+
`mafdet agent` documents `--title`, `--no-history` and `--read-only`, shows
|
|
84
|
+
the first-save notice, and closes its recorder even when the session throws;
|
|
85
|
+
resuming no longer asks you to pick a model it is going to ignore, and the
|
|
86
|
+
banner names the model actually in use; and the Windows command-timeout note
|
|
87
|
+
now says the `taskkill` path is written but unverified, rather than describing
|
|
88
|
+
the implementation it replaced.
|
|
89
|
+
|
|
9
90
|
## 0.4.4 - 2026-08-11
|
|
10
91
|
|
|
11
92
|
A security fix and twenty-one other corrections, all from an external audit of
|
package/README.md
CHANGED
|
@@ -3,13 +3,11 @@
|
|
|
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.6.** 中文版见 [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
|
>
|
|
10
|
-
> macOS and Windows are not yet verified. On Windows the command timeout
|
|
11
|
-
> relies on POSIX process groups, so only the direct child is terminated and a
|
|
12
|
-
> grandchild can survive.
|
|
10
|
+
> macOS and Windows are not yet verified. On Windows the command timeout uses `taskkill /T`, which is written but has never been run on a real Windows machine - so whether every grandchild process is reached there is unverified rather than fixed.
|
|
13
11
|
|
|
14
12
|
## Requirements
|
|
15
13
|
|
|
@@ -25,8 +23,8 @@ Or build it from this repository:
|
|
|
25
23
|
|
|
26
24
|
```bash
|
|
27
25
|
pnpm --filter @mafdet/cli build
|
|
28
|
-
cd packages/cli && npm pack # -> mafdet-cli-0.4.
|
|
29
|
-
npm install -g ./mafdet-cli-0.4.
|
|
26
|
+
cd packages/cli && npm pack # -> mafdet-cli-0.4.6.tgz
|
|
27
|
+
npm install -g ./mafdet-cli-0.4.6.tgz
|
|
30
28
|
```
|
|
31
29
|
|
|
32
30
|
## The interactive session
|
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.6。** 英文版见 [README.md](./README.md)。
|
|
6
6
|
|
|
7
7
|
## 环境要求
|
|
8
8
|
|
|
@@ -55,8 +55,9 @@ mafdet doctor
|
|
|
55
55
|
的默认状态,中文系统为 cp936)也不会变成乱码。管道传入 prompt 也可以,但 PowerShell
|
|
56
56
|
默认按 UTF-16 走管道——建议直接把 prompt 作为参数,或用 `--input <文件>`。
|
|
57
57
|
|
|
58
|
-
⚠️ **macOS 与 Windows 尚未验收**。Windows
|
|
59
|
-
|
|
58
|
+
⚠️ **macOS 与 Windows 尚未验收**。Windows 上的命令超时终止已改用 `taskkill /T`,
|
|
59
|
+
但**从未在真 Windows 上执行过**——它是否真的能杀到每一个孙子进程,属于**未验证**,
|
|
60
|
+
不是「已修复」。
|
|
60
61
|
|
|
61
62
|
## 命令
|
|
62
63
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"command-policy.d.ts","sourceRoot":"","sources":["../../src/agent/command-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,OAAO,CAAC;IACZ,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CA4BlD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,SAAS,MAAM,EAAO,GAChC,cAAc,CAwChB;AAmBD,+EAA+E;AAC/E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B9C"}
|
|
@@ -119,15 +119,28 @@ function matchesAllowlist(argv, allowlist) {
|
|
|
119
119
|
/** Terminal control sequences, stripped before any output is shown or sent. */
|
|
120
120
|
export function stripAnsi(text) {
|
|
121
121
|
return (text
|
|
122
|
-
//
|
|
122
|
+
// Whole sequences first, in both their 7-bit and 8-bit forms, and only
|
|
123
|
+
// then the leftover control characters.
|
|
124
|
+
//
|
|
125
|
+
// The order is load-bearing and was wrong once: removing C0 controls
|
|
126
|
+
// before parsing the 8-bit OSC took away the BEL that terminates it, so
|
|
127
|
+
// the pattern ran to the end of the string and swallowed the ordinary
|
|
128
|
+
// text after the sequence. Failing safe, but destroying output.
|
|
129
|
+
//
|
|
130
|
+
// CSI: colours, cursor moves, screen clears.
|
|
123
131
|
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
124
|
-
|
|
125
|
-
// the
|
|
126
|
-
|
|
127
|
-
//
|
|
132
|
+
.replace(/\u009b[0-?]*[ -/]*[@-~]/g, "")
|
|
133
|
+
// OSC: window titles and the like. The payload goes with the
|
|
134
|
+
// introducer - stripping only the introducer would leave "0;retitled"
|
|
135
|
+
// sitting in the output. Terminated by BEL or by ST in either form.
|
|
136
|
+
.replace(/\u001b\][^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "")
|
|
137
|
+
.replace(/\u009d[^\u0007\u001b\u009c]*(?:\u0007|\u001b\\|\u009c)?/g, "")
|
|
138
|
+
// Other single-character escapes.
|
|
128
139
|
.replace(/\u001b[@-Z\\-_]/g, "")
|
|
129
140
|
// Remaining C0 controls. Tab, newline and carriage return survive; the
|
|
130
141
|
// rest can retitle a window, move the cursor or hide what follows, and a
|
|
131
142
|
// model's output is not a trusted source of terminal instructions.
|
|
132
|
-
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
143
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
|
|
144
|
+
// Remaining C1, including NEL (U+0085) and any lone introducer.
|
|
145
|
+
.replace(/[\u0080-\u009f]/g, ""));
|
|
133
146
|
}
|
|
@@ -47,7 +47,10 @@ export declare function pruneToolExchanges(history: ChatMessage[], keepRecent?:
|
|
|
47
47
|
* unanswered: CLI-014's rule holds by construction. The system prompt and the
|
|
48
48
|
* first user message - the goal - always stay.
|
|
49
49
|
*/
|
|
50
|
-
export declare function dropOldestTurns(history: ChatMessage[], keepTurns: number):
|
|
50
|
+
export declare function dropOldestTurns(history: ChatMessage[], keepTurns: number): {
|
|
51
|
+
removed: number;
|
|
52
|
+
turns: number;
|
|
53
|
+
};
|
|
51
54
|
export interface CompactionResult extends PruneResult {
|
|
52
55
|
/** Whole turns dropped, when exchanges were not enough. */
|
|
53
56
|
readonly turns: number;
|
|
@@ -66,4 +69,19 @@ export interface CompactionResult extends PruneResult {
|
|
|
66
69
|
* upstream and be billed for the attempt.
|
|
67
70
|
*/
|
|
68
71
|
export declare function compactForBudget(history: ChatMessage[], keepRecent?: number): CompactionResult;
|
|
72
|
+
/**
|
|
73
|
+
* R-2 · Shorten until it fits, or until nothing more can go.
|
|
74
|
+
*
|
|
75
|
+
* `compactForBudget` used to return the moment it dropped a single tool
|
|
76
|
+
* exchange, and the caller then sent the request regardless. One exchange out
|
|
77
|
+
* of a history three times over the window changes nothing, so the request
|
|
78
|
+
* still failed upstream - and was billed for the attempt.
|
|
79
|
+
*
|
|
80
|
+
* `estimate` maps the current history back to a token count. There is no
|
|
81
|
+
* tokenizer here (see the note at the top of this file), so it is derived from
|
|
82
|
+
* the last measurement: tokens per message, applied to what is left. Rough, but
|
|
83
|
+
* it is the only honest figure available and it is monotone in the right
|
|
84
|
+
* direction, which is what a loop needs.
|
|
85
|
+
*/
|
|
86
|
+
export declare function compactUntilItFits(history: ChatMessage[], contextLength: number | undefined, lastPromptTokens: number, keepRecent?: number): CompactionResult;
|
|
69
87
|
//# sourceMappingURL=context-budget.d.ts.map
|
|
@@ -1 +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,
|
|
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;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA0BpC;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,CASlB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,WAAW,EAAE,EACtB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,gBAAgB,EAAE,MAAM,EACxB,UAAU,GAAE,MAA8B,GACzC,gBAAgB,CAoClB"}
|
|
@@ -125,10 +125,10 @@ export function dropOldestTurns(history, keepTurns) {
|
|
|
125
125
|
// The first user message is the goal and is not a droppable turn, so there
|
|
126
126
|
// have to be more than `keepTurns` turns after it before anything can go.
|
|
127
127
|
if (userAt.length <= keepTurns + 1)
|
|
128
|
-
return 0;
|
|
128
|
+
return { removed: 0, turns: 0 };
|
|
129
129
|
const cut = userAt[userAt.length - keepTurns];
|
|
130
130
|
if (cut === undefined)
|
|
131
|
-
return 0;
|
|
131
|
+
return { removed: 0, turns: 0 };
|
|
132
132
|
const kept = [];
|
|
133
133
|
if (history[0]?.role === "system")
|
|
134
134
|
kept.push(history[0]);
|
|
@@ -138,10 +138,13 @@ export function dropOldestTurns(history, keepTurns) {
|
|
|
138
138
|
kept.push(...history.slice(cut));
|
|
139
139
|
const removed = history.length - kept.length;
|
|
140
140
|
if (removed <= 0)
|
|
141
|
-
return 0;
|
|
141
|
+
return { removed: 0, turns: 0 };
|
|
142
142
|
history.length = 0;
|
|
143
143
|
history.push(...kept);
|
|
144
|
-
|
|
144
|
+
// T-7 · The count of *turns*, not of messages. Reporting a fixed 1 made the
|
|
145
|
+
// display say "dropped 1 earlier turn" after removing eight of them.
|
|
146
|
+
const turns = userAt.filter((at) => at < cut && at !== userAt[0]).length;
|
|
147
|
+
return { removed, turns };
|
|
145
148
|
}
|
|
146
149
|
/**
|
|
147
150
|
* Shorten the history as much as can be done safely.
|
|
@@ -156,14 +159,86 @@ export function dropOldestTurns(history, keepTurns) {
|
|
|
156
159
|
*/
|
|
157
160
|
export function compactForBudget(history, keepRecent = KEEP_RECENT_EXCHANGES) {
|
|
158
161
|
const pruned = pruneToolExchanges(history, keepRecent);
|
|
159
|
-
|
|
160
|
-
return { ...pruned, turns: 0, exhausted: false };
|
|
161
|
-
}
|
|
162
|
-
const turnsRemoved = dropOldestTurns(history, keepRecent);
|
|
162
|
+
const dropped = dropOldestTurns(history, keepRecent);
|
|
163
163
|
return {
|
|
164
|
-
removed:
|
|
165
|
-
exchanges:
|
|
166
|
-
turns:
|
|
167
|
-
exhausted:
|
|
164
|
+
removed: pruned.removed + dropped.removed,
|
|
165
|
+
exchanges: pruned.exchanges,
|
|
166
|
+
turns: dropped.turns,
|
|
167
|
+
exhausted: pruned.exchanges === 0 && dropped.removed === 0,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* R-2 · Shorten until it fits, or until nothing more can go.
|
|
172
|
+
*
|
|
173
|
+
* `compactForBudget` used to return the moment it dropped a single tool
|
|
174
|
+
* exchange, and the caller then sent the request regardless. One exchange out
|
|
175
|
+
* of a history three times over the window changes nothing, so the request
|
|
176
|
+
* still failed upstream - and was billed for the attempt.
|
|
177
|
+
*
|
|
178
|
+
* `estimate` maps the current history back to a token count. There is no
|
|
179
|
+
* tokenizer here (see the note at the top of this file), so it is derived from
|
|
180
|
+
* the last measurement: tokens per message, applied to what is left. Rough, but
|
|
181
|
+
* it is the only honest figure available and it is monotone in the right
|
|
182
|
+
* direction, which is what a loop needs.
|
|
183
|
+
*/
|
|
184
|
+
export function compactUntilItFits(history, contextLength, lastPromptTokens, keepRecent = KEEP_RECENT_EXCHANGES) {
|
|
185
|
+
const startedWith = history.length;
|
|
186
|
+
const floor = fixedOverhead(history, lastPromptTokens);
|
|
187
|
+
const perMessage = startedWith > 0 ? Math.max(0, lastPromptTokens - floor) / startedWith : 0;
|
|
188
|
+
let exchanges = 0;
|
|
189
|
+
let turns = 0;
|
|
190
|
+
for (;;) {
|
|
191
|
+
// T-2 · The floor does not shrink. The system prompt and the tool schemas
|
|
192
|
+
// are re-sent every turn whatever the history looks like, so treating the
|
|
193
|
+
// whole measurement as proportional to the message count said a
|
|
194
|
+
// conversation had come inside the window when only the removable part
|
|
195
|
+
// had. Everything below the floor is not ours to drop.
|
|
196
|
+
const estimate = floor + Math.round(perMessage * history.length);
|
|
197
|
+
if (!shouldCompact(contextLength, estimate)) {
|
|
198
|
+
return {
|
|
199
|
+
removed: startedWith - history.length,
|
|
200
|
+
exchanges,
|
|
201
|
+
turns,
|
|
202
|
+
exhausted: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const step = compactForBudget(history, keepRecent);
|
|
206
|
+
if (step.exhausted) {
|
|
207
|
+
return {
|
|
208
|
+
removed: startedWith - history.length,
|
|
209
|
+
exchanges,
|
|
210
|
+
turns,
|
|
211
|
+
// Still over budget with nothing left that is safe to drop.
|
|
212
|
+
exhausted: true,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
exchanges += step.exchanges;
|
|
216
|
+
turns += step.turns;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* T-2 · What the next request costs before any history is added.
|
|
221
|
+
*
|
|
222
|
+
* The system prompt is measurable; the tool schemas are not from here - the
|
|
223
|
+
* loop knows them but this module does not, and CLI-000B found the gateway's
|
|
224
|
+
* own reservation undercounts badly for exactly that reason. So it is
|
|
225
|
+
* approximated from the one message we can size, with a floor that keeps the
|
|
226
|
+
* estimate conservative: over-estimating the fixed part makes compaction try
|
|
227
|
+
* harder, which is the direction that fails safe.
|
|
228
|
+
*/
|
|
229
|
+
function fixedOverhead(history, lastPromptTokens) {
|
|
230
|
+
const system = history.find((m) => m.role === "system");
|
|
231
|
+
if (system === undefined || lastPromptTokens <= 0)
|
|
232
|
+
return 0;
|
|
233
|
+
const chars = (system.content ?? "").length;
|
|
234
|
+
// Roughly four characters per token, doubled to stand in for the tool
|
|
235
|
+
// schemas that are sent alongside it and cannot be measured here.
|
|
236
|
+
const approx = Math.round((chars / 4) * 2);
|
|
237
|
+
// Bounded by the measurement, because the fixed part cannot exceed the whole
|
|
238
|
+
// request - but NOT halved. Capping it lower was the unsafe direction: it
|
|
239
|
+
// let the loop conclude that dropping a few messages had brought a history
|
|
240
|
+
// inside a window the system prompt alone did not fit in. Over-estimating
|
|
241
|
+
// makes compaction try harder and, at worst, stop with an honest message;
|
|
242
|
+
// under-estimating sends a request that fails upstream and is billed.
|
|
243
|
+
return Math.min(approx, lastPromptTokens);
|
|
244
|
+
}
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -14,7 +14,13 @@ export declare const DEFAULT_LIMITS: {
|
|
|
14
14
|
export type AgentLimits = typeof DEFAULT_LIMITS;
|
|
15
15
|
export type StopReason = "completed" | "max_turns" | "timeout" | "max_cost" | "cancelled" | "rate_limited"
|
|
16
16
|
/** F-008 · Over the model's window with nothing left that is safe to drop. */
|
|
17
|
-
| "context_limit"
|
|
17
|
+
| "context_limit"
|
|
18
|
+
/**
|
|
19
|
+
* R-5 · A cost ceiling was asked for and cannot be honoured, because the
|
|
20
|
+
* catalogue has no price for this model. Distinct from `max_cost`, which
|
|
21
|
+
* means the ceiling was reached.
|
|
22
|
+
*/
|
|
23
|
+
| "cost_unknown" | "error";
|
|
18
24
|
export interface AgentReport {
|
|
19
25
|
stopReason: StopReason;
|
|
20
26
|
/** The model's last text answer, if it produced one. */
|
|
@@ -30,6 +36,13 @@ export interface AgentReport {
|
|
|
30
36
|
promptTokens: number;
|
|
31
37
|
completionTokens: number;
|
|
32
38
|
costMicroUsd: bigint;
|
|
39
|
+
/**
|
|
40
|
+
* T-4 · Requests the cost above does not include, because the catalogue had
|
|
41
|
+
* no price for them. Without it every live summary shows `$0.00` for a run
|
|
42
|
+
* that was not free - the session store learned to say "unknown" and the
|
|
43
|
+
* surfaces the user actually watches did not.
|
|
44
|
+
*/
|
|
45
|
+
unpricedRequests: number;
|
|
33
46
|
/** Set when the run ended because something failed. */
|
|
34
47
|
error?: string;
|
|
35
48
|
}
|
|
@@ -122,6 +135,8 @@ export type AgentEvent = {
|
|
|
122
135
|
| {
|
|
123
136
|
type: "compacted";
|
|
124
137
|
exchanges: number;
|
|
138
|
+
/** Whole conversation turns dropped, when exchanges were not enough. */
|
|
139
|
+
turns: number;
|
|
125
140
|
messages: number;
|
|
126
141
|
promptTokens: number;
|
|
127
142
|
};
|
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":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAE1B,iBAAiB;AACjB,eAAO,MAAM,cAAc;;;IAGzB,iFAAiF;;;qBAGnD,MAAM,GAAG,SAAS;CACjD,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,OAAO,cAAc,CAAC;AAEhD,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,WAAW,GACX,SAAS,GACT,UAAU,GACV,WAAW,GACX,cAAc;AAChB,8EAA8E;GAC5E,eAAe,
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAE1B,iBAAiB;AACjB,eAAO,MAAM,cAAc;;;IAGzB,iFAAiF;;;qBAGnD,MAAM,GAAG,SAAS;CACjD,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,OAAO,cAAc,CAAC;AAEhD,MAAM,MAAM,UAAU,GAClB,WAAW,GACX,WAAW,GACX,SAAS,GACT,UAAU,GACV,WAAW,GACX,cAAc;AAChB,8EAA8E;GAC5E,eAAe;AACjB;;;;GAIG;GACD,cAAc,GACd,OAAO,CAAC;AAEZ,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,UAAU,CAAC;IACvB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAC1C,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,uDAAuD;IACvD,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,wBAAwB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAChC;;;;;GAKG;GACD;IACE,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB,GACD;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAC3D;;;;GAIG;GACD;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEN,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;IAC/C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,UAAU,QAAQ;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C;AAYD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAkN1E;AAqID,+EAA+E;AAC/E,wBAAgB,SAAS,CACvB,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,MAAoB,GACjC,MAAM,CAOR"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiError } from "../api/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { compactUntilItFits, shouldCompact } from "./context-budget.js";
|
|
3
3
|
import { toolToJsonSchema, } from "./tools/types.js";
|
|
4
4
|
/** Plan §7.4. */
|
|
5
5
|
export const DEFAULT_LIMITS = {
|
|
@@ -44,9 +44,22 @@ export async function runAgent(options) {
|
|
|
44
44
|
promptTokens: 0,
|
|
45
45
|
completionTokens: 0,
|
|
46
46
|
costMicroUsd: 0n,
|
|
47
|
+
unpricedRequests: 0,
|
|
47
48
|
};
|
|
48
49
|
/** What the provider counted for the previous request; 0 until one lands. */
|
|
49
50
|
let lastPromptTokens = 0;
|
|
51
|
+
/** R-5 · Turns whose cost the catalogue could not tell us. */
|
|
52
|
+
let unpricedTurns = 0;
|
|
53
|
+
// T-3 · Known before anything is sent. Waiting for the first response to
|
|
54
|
+
// discover it meant a run with an explicit ceiling still made one billable
|
|
55
|
+
// request that could never be counted against it.
|
|
56
|
+
if (limits.maxCostMicroUsd !== undefined &&
|
|
57
|
+
(!options.pricing?.inputPricePer1mMicroUsd ||
|
|
58
|
+
!options.pricing.outputPricePer1mMicroUsd)) {
|
|
59
|
+
return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue publishes no price for ` +
|
|
60
|
+
`${options.model}, so nothing can be counted towards the ceiling. ` +
|
|
61
|
+
`Re-run without the limit to continue anyway.`);
|
|
62
|
+
}
|
|
50
63
|
for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
|
|
51
64
|
if (options.signal?.aborted)
|
|
52
65
|
return stop(report, "cancelled");
|
|
@@ -56,11 +69,23 @@ export async function runAgent(options) {
|
|
|
56
69
|
report.costMicroUsd >= limits.maxCostMicroUsd) {
|
|
57
70
|
return stop(report, "max_cost");
|
|
58
71
|
}
|
|
72
|
+
// R-5 · An unpriceable turn counted as 0 towards the ceiling, so
|
|
73
|
+
// `--max-cost-usd` silently stopped being a ceiling at all and the run
|
|
74
|
+
// continued spending real money. A limit that cannot be enforced has to
|
|
75
|
+
// stop the run, not be quietly abandoned.
|
|
76
|
+
if (limits.maxCostMicroUsd !== undefined && unpricedTurns > 0) {
|
|
77
|
+
return stop(report, "cost_unknown", `--max-cost-usd cannot be enforced: the catalogue has no price for ` +
|
|
78
|
+
`${options.model}, so ${unpricedTurns} request(s) could not be counted ` +
|
|
79
|
+
`towards the ceiling. Re-run without the limit to continue anyway.`);
|
|
80
|
+
}
|
|
59
81
|
report.turns = turn;
|
|
60
82
|
options.onEvent?.({ type: "turn", turn });
|
|
61
83
|
// §7.4 · Shorten before asking, using what the provider counted last time.
|
|
62
84
|
if (shouldCompact(options.pricing?.contextLength, lastPromptTokens)) {
|
|
63
|
-
|
|
85
|
+
// R-2 · Keep going until the estimate is inside the budget. Dropping one
|
|
86
|
+
// exchange and sending anyway left a history that was still over the
|
|
87
|
+
// window: the request failed upstream and was billed for the attempt.
|
|
88
|
+
const compacted = compactUntilItFits(messages, options.pricing?.contextLength, lastPromptTokens);
|
|
64
89
|
if (compacted.exhausted) {
|
|
65
90
|
// F-008 · Nothing left that can be dropped without breaking the
|
|
66
91
|
// conversation. Sending anyway means a request over the window, which
|
|
@@ -72,6 +97,9 @@ export async function runAgent(options) {
|
|
|
72
97
|
options.onEvent?.({
|
|
73
98
|
type: "compacted",
|
|
74
99
|
exchanges: compacted.exchanges,
|
|
100
|
+
// R-2 · Reported separately: a pure conversation drops turns and no
|
|
101
|
+
// exchanges, and the display said "dropped 0 earlier tool exchanges".
|
|
102
|
+
turns: compacted.turns,
|
|
75
103
|
messages: compacted.removed,
|
|
76
104
|
promptTokens: lastPromptTokens,
|
|
77
105
|
});
|
|
@@ -116,6 +144,10 @@ export async function runAgent(options) {
|
|
|
116
144
|
costMicroUsd: turnCost,
|
|
117
145
|
priced: canPrice(options.pricing, usage),
|
|
118
146
|
});
|
|
147
|
+
if (!canPrice(options.pricing, usage)) {
|
|
148
|
+
unpricedTurns += 1;
|
|
149
|
+
report.unpricedRequests += 1;
|
|
150
|
+
}
|
|
119
151
|
const choice = response.choices?.[0];
|
|
120
152
|
const message = choice?.message;
|
|
121
153
|
const calls = message?.tool_calls ?? [];
|
package/dist/agent/session.d.ts
CHANGED
|
@@ -32,6 +32,18 @@ export interface SessionOptions {
|
|
|
32
32
|
* the same events are drawn.
|
|
33
33
|
*/
|
|
34
34
|
onEvent?: (event: AgentEvent) => void;
|
|
35
|
+
/**
|
|
36
|
+
* T-1 · The user's own words, which never appear in the agent event stream.
|
|
37
|
+
*
|
|
38
|
+
* Called from the loop rather than from each surface's `readLine`, because
|
|
39
|
+
* that is exactly how it came to be missing: the TUI wrapped its reader and
|
|
40
|
+
* `mafdet agent` did not, so a command-mode session recorded the model's
|
|
41
|
+
* replies and none of the questions - and resuming it could not continue the
|
|
42
|
+
* conversation, because half of it was not there.
|
|
43
|
+
*/
|
|
44
|
+
onGoal?: (goal: string) => void;
|
|
45
|
+
/** R-4 · False when stdout is a pipe or a file, which get exact bytes. */
|
|
46
|
+
isTty?: boolean;
|
|
35
47
|
/**
|
|
36
48
|
* A conversation to continue, rebuilt from a saved session (CLI-024).
|
|
37
49
|
* Appended to in place, exactly as the fresh array would be.
|
|
@@ -46,6 +58,8 @@ export interface SessionSummary {
|
|
|
46
58
|
promptTokens: number;
|
|
47
59
|
completionTokens: number;
|
|
48
60
|
costMicroUsd: bigint;
|
|
61
|
+
/** T-4 · Requests the cost above does not include; see AgentReport. */
|
|
62
|
+
unpricedRequests: number;
|
|
49
63
|
}
|
|
50
64
|
/**
|
|
51
65
|
* The interactive session (plan §4.4).
|
|
@@ -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;
|
|
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;AAE/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;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;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;IACrB,uEAAuE;IACvE,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CAwGzB;AAsHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
|
package/dist/agent/session.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { isAgentEligible } from "../api/models.js";
|
|
3
|
-
import {
|
|
3
|
+
import { formatUsdWithUnpriced } from "../output/money.js";
|
|
4
4
|
import { renderRecord } from "../output/format.js";
|
|
5
|
+
import { safeForTerminal } from "../output/terminal-safe.js";
|
|
5
6
|
import { runAgent } from "./loop.js";
|
|
6
7
|
const HELP = `Session commands:
|
|
7
8
|
/model [id] Show or switch the model
|
|
@@ -31,6 +32,7 @@ export async function runSession(options) {
|
|
|
31
32
|
promptTokens: 0,
|
|
32
33
|
completionTokens: 0,
|
|
33
34
|
costMicroUsd: 0n,
|
|
35
|
+
unpricedRequests: 0,
|
|
34
36
|
};
|
|
35
37
|
let model = options.model;
|
|
36
38
|
let pricing = options.pricing;
|
|
@@ -68,6 +70,7 @@ export async function runSession(options) {
|
|
|
68
70
|
break;
|
|
69
71
|
continue;
|
|
70
72
|
}
|
|
73
|
+
options.onGoal?.(line);
|
|
71
74
|
summary.goals += 1;
|
|
72
75
|
const report = await runAgent({
|
|
73
76
|
client: options.client,
|
|
@@ -94,8 +97,13 @@ export async function runSession(options) {
|
|
|
94
97
|
summary.promptTokens += report.promptTokens;
|
|
95
98
|
summary.completionTokens += report.completionTokens;
|
|
96
99
|
summary.costMicroUsd += report.costMicroUsd;
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
summary.unpricedRequests += report.unpricedRequests;
|
|
101
|
+
// R-4 · The model's answer reaches a terminal here too. The TUI wraps
|
|
102
|
+
// this writer, but `mafdet agent` without a goal does not, so the
|
|
103
|
+
// sanitising belongs at the sink rather than at one of its callers.
|
|
104
|
+
if (report.finalText) {
|
|
105
|
+
options.stdout(`${safeForTerminal(report.finalText, options.isTty ?? true)}\n`);
|
|
106
|
+
}
|
|
99
107
|
if (report.stopReason !== "completed") {
|
|
100
108
|
options.stderr(`[${report.stopReason}${report.error ? `: ${report.error}` : ""}]\n`);
|
|
101
109
|
}
|
|
@@ -175,7 +183,10 @@ async function handleCommand(line, context) {
|
|
|
175
183
|
"tokens",
|
|
176
184
|
`${context.summary.promptTokens} in / ${context.summary.completionTokens} out`,
|
|
177
185
|
],
|
|
178
|
-
[
|
|
186
|
+
[
|
|
187
|
+
"cost",
|
|
188
|
+
`~${formatUsdWithUnpriced(context.summary.costMicroUsd, context.summary.unpricedRequests)}`,
|
|
189
|
+
],
|
|
179
190
|
]));
|
|
180
191
|
return false;
|
|
181
192
|
case "/compact": {
|
package/dist/api/models.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"models.d.ts","sourceRoot":"","sources":["../../src/api/models.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE5C,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,0CAA0C;AAC1C,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,mDAAmD;IACnD,SAAS,EAAE,aAAa,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,gDAAgD;IAChD,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,SAAS,GAAG,OAAO,CAQxE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,cAAc,EAAE,EACpC,OAAO,EAAE,SAAS,YAAY,EAAE,GAC/B,SAAS,CAuBX;AAED,8CAA8C;AAC9C,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ3B;AAED,2EAA2E;AAC3E,wBAAsB,YAAY,CAChC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,EAAE,CAAC,CAGzB;AA0BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,YAAY,GAAG,SAAS,CAY1B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAFhB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM;CAO5B"}
|
package/dist/api/models.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
1
2
|
/**
|
|
2
3
|
* Agent admission. Every condition is something the catalogue states outright;
|
|
3
4
|
* nothing is inferred from the model id, the provider or the display name —
|
|
@@ -51,12 +52,37 @@ export function intersectModels(dataPlane, catalog) {
|
|
|
51
52
|
/** GET /v1/models — the key's access list. */
|
|
52
53
|
export async function fetchAccessibleModels(client) {
|
|
53
54
|
const { data } = await client.getJson("/models");
|
|
54
|
-
|
|
55
|
+
// T-5 · Same reason as `cleanModel`: these ids are printed as the "unlisted"
|
|
56
|
+
// set, so they reach a terminal without ever passing through the catalogue.
|
|
57
|
+
return (data.data ?? []).map((model) => ({
|
|
58
|
+
...model,
|
|
59
|
+
id: typeof model.id === "string" ? stripAnsi(model.id) : model.id,
|
|
60
|
+
}));
|
|
55
61
|
}
|
|
56
62
|
/** GET /models on the business API — public catalogue, no key required. */
|
|
57
63
|
export async function fetchCatalog(client) {
|
|
58
64
|
const { data } = await client.getJson("/models");
|
|
59
|
-
return data.data ?? [];
|
|
65
|
+
return (data.data ?? []).map(cleanModel);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* T-5 · Strip terminal control sequences from catalogue text, once, here.
|
|
69
|
+
*
|
|
70
|
+
* The catalogue is remote data that ends up on a terminal in the picker, in
|
|
71
|
+
* `models list`, in `models show`, in `doctor` and in error messages. F-010
|
|
72
|
+
* was fixed sink by sink across two review rounds and the picker was still
|
|
73
|
+
* missing, which is the argument for doing it where the data enters instead:
|
|
74
|
+
* every renderer downstream, present and future, then has nothing to forget.
|
|
75
|
+
*
|
|
76
|
+
* The model id is cleaned too. It is sent back to the API, so in principle
|
|
77
|
+
* this changes what we would ask for - but an id carrying a control character
|
|
78
|
+
* is not a model id, and declining to echo one back is right either way.
|
|
79
|
+
*/
|
|
80
|
+
function cleanModel(model) {
|
|
81
|
+
const clean = (value) => typeof value === "string" ? stripAnsi(value) : value;
|
|
82
|
+
return Object.fromEntries(Object.entries(model).map(([key, value]) => [
|
|
83
|
+
key,
|
|
84
|
+
Array.isArray(value) ? value.map(clean) : clean(value),
|
|
85
|
+
]));
|
|
60
86
|
}
|
|
61
87
|
/**
|
|
62
88
|
* Check the model serves the endpoint the command needs, before spending a
|