@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/dist/agent/loop.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ApiError } from "../api/errors.js";
|
|
2
|
+
import { compactForBudget, shouldCompact } from "./context-budget.js";
|
|
2
3
|
import { toolToJsonSchema, } from "./tools/types.js";
|
|
3
4
|
/** Plan §7.4. */
|
|
4
5
|
export const DEFAULT_LIMITS = {
|
|
@@ -44,6 +45,8 @@ export async function runAgent(options) {
|
|
|
44
45
|
completionTokens: 0,
|
|
45
46
|
costMicroUsd: 0n,
|
|
46
47
|
};
|
|
48
|
+
/** What the provider counted for the previous request; 0 until one lands. */
|
|
49
|
+
let lastPromptTokens = 0;
|
|
47
50
|
for (let turn = 1; turn <= limits.maxTurns; turn += 1) {
|
|
48
51
|
if (options.signal?.aborted)
|
|
49
52
|
return stop(report, "cancelled");
|
|
@@ -55,6 +58,27 @@ export async function runAgent(options) {
|
|
|
55
58
|
}
|
|
56
59
|
report.turns = turn;
|
|
57
60
|
options.onEvent?.({ type: "turn", turn });
|
|
61
|
+
// §7.4 · Shorten before asking, using what the provider counted last time.
|
|
62
|
+
if (shouldCompact(options.pricing?.contextLength, lastPromptTokens)) {
|
|
63
|
+
const compacted = compactForBudget(messages);
|
|
64
|
+
if (compacted.exhausted) {
|
|
65
|
+
// F-008 · Nothing left that can be dropped without breaking the
|
|
66
|
+
// conversation. Sending anyway means a request over the window, which
|
|
67
|
+
// fails upstream and is billed for the attempt - stopping and saying
|
|
68
|
+
// so is the only honest option.
|
|
69
|
+
return stop(report, "context_limit", `the conversation no longer fits in ${options.model}'s context window ` +
|
|
70
|
+
`(${lastPromptTokens} input tokens last turn) and cannot be shortened further`);
|
|
71
|
+
}
|
|
72
|
+
options.onEvent?.({
|
|
73
|
+
type: "compacted",
|
|
74
|
+
exchanges: compacted.exchanges,
|
|
75
|
+
messages: compacted.removed,
|
|
76
|
+
promptTokens: lastPromptTokens,
|
|
77
|
+
});
|
|
78
|
+
// Reset the measurement: the next response measures the shorter
|
|
79
|
+
// history, and reusing the old figure would compact again every turn.
|
|
80
|
+
lastPromptTokens = 0;
|
|
81
|
+
}
|
|
58
82
|
let response;
|
|
59
83
|
try {
|
|
60
84
|
response = await requestWithRateLimitHandling(options, limits, {
|
|
@@ -74,6 +98,7 @@ export async function runAgent(options) {
|
|
|
74
98
|
return stop(report, "error", error instanceof Error ? error.message : String(error));
|
|
75
99
|
}
|
|
76
100
|
const usage = response.usage;
|
|
101
|
+
lastPromptTokens = usage?.prompt_tokens ?? lastPromptTokens;
|
|
77
102
|
report.promptTokens += usage?.prompt_tokens ?? 0;
|
|
78
103
|
report.completionTokens += usage?.completion_tokens ?? 0;
|
|
79
104
|
// Cost comes from returned usage, never from the reservation: CLI-000B
|
|
@@ -89,6 +114,7 @@ export async function runAgent(options) {
|
|
|
89
114
|
promptTokens: usage?.prompt_tokens ?? 0,
|
|
90
115
|
completionTokens: usage?.completion_tokens ?? 0,
|
|
91
116
|
costMicroUsd: turnCost,
|
|
117
|
+
priced: canPrice(options.pricing, usage),
|
|
92
118
|
});
|
|
93
119
|
const choice = response.choices?.[0];
|
|
94
120
|
const message = choice?.message;
|
|
@@ -247,6 +273,12 @@ export function backoffMs(retryAfterSeconds, attempt, random = Math.random) {
|
|
|
247
273
|
// Full jitter: a fleet of CLIs must not retry in lockstep.
|
|
248
274
|
return Math.round(base * (0.5 + random() * 0.5));
|
|
249
275
|
}
|
|
276
|
+
/** F-012 · Whether `priceTurn` had everything it needed to give an answer. */
|
|
277
|
+
function canPrice(model, usage) {
|
|
278
|
+
return (usage !== undefined &&
|
|
279
|
+
model?.inputPricePer1mMicroUsd !== undefined &&
|
|
280
|
+
model?.outputPricePer1mMicroUsd !== undefined);
|
|
281
|
+
}
|
|
250
282
|
function priceTurn(model, usage) {
|
|
251
283
|
if (!model?.inputPricePer1mMicroUsd ||
|
|
252
284
|
!model.outputPricePer1mMicroUsd ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/agent/session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAY,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAapE,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,oEAAoE;IACpE,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,cAAc,CAAC,CA8FzB;AAgHD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,MAAM,CA2B7D"}
|
package/dist/agent/session.js
CHANGED
|
@@ -80,7 +80,13 @@ export async function runSession(options) {
|
|
|
80
80
|
history,
|
|
81
81
|
approver: options.approver,
|
|
82
82
|
signal: options.signal,
|
|
83
|
-
|
|
83
|
+
// Both, not either: `onEvent` was replacing the progress display, so
|
|
84
|
+
// attaching a recorder would have silently blanked the terminal. An
|
|
85
|
+
// observer is an addition to the rendering, never a substitute for it.
|
|
86
|
+
onEvent: (event) => {
|
|
87
|
+
reportEvent(event, options);
|
|
88
|
+
options.onEvent?.(event);
|
|
89
|
+
},
|
|
84
90
|
});
|
|
85
91
|
summary.turns += report.turns;
|
|
86
92
|
summary.modelRequests += report.modelRequests;
|
|
@@ -15,6 +15,34 @@ export interface CommandResult {
|
|
|
15
15
|
* failure mode where a "timed out" test run keeps holding a port.
|
|
16
16
|
*/
|
|
17
17
|
export declare function executeCommand(argv: readonly string[], cwd: string, timeoutMs?: number): Promise<CommandResult>;
|
|
18
|
+
/**
|
|
19
|
+
* What "kill the whole tree" means on this platform.
|
|
20
|
+
*
|
|
21
|
+
* Pure, and separated from doing it, because the Windows branch cannot be
|
|
22
|
+
* executed here: there is no Windows machine in this project, and a kill path
|
|
23
|
+
* that has never run is exactly the kind of code that is wrong in a way only
|
|
24
|
+
* production finds. Keeping the decision pure means the *shape* of the command
|
|
25
|
+
* is under test on both platforms even though only one of them can be run.
|
|
26
|
+
*
|
|
27
|
+
* POSIX: a negative pid signals the process group, which is why the child is
|
|
28
|
+
* spawned detached. Killing the parent alone leaves grandchildren running -
|
|
29
|
+
* the familiar case where a "timed out" test runner keeps holding a port.
|
|
30
|
+
*
|
|
31
|
+
* Windows has no process groups in that sense. `taskkill /T` walks the child
|
|
32
|
+
* tree instead, and `/F` is the forceful pass: without it taskkill asks
|
|
33
|
+
* politely, which a console process is free to ignore - the same distinction
|
|
34
|
+
* as SIGTERM and SIGKILL.
|
|
35
|
+
*/
|
|
36
|
+
export type KillPlan = {
|
|
37
|
+
readonly kind: "signal";
|
|
38
|
+
readonly target: number;
|
|
39
|
+
readonly signal: NodeJS.Signals;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "spawn";
|
|
42
|
+
readonly command: string;
|
|
43
|
+
readonly args: readonly string[];
|
|
44
|
+
};
|
|
45
|
+
export declare function killPlan(pid: number, force: boolean, platform?: string): KillPlan;
|
|
18
46
|
export declare const runCommandTool: ToolDefinition;
|
|
19
47
|
export declare const COMMAND_TOOLS: ToolDefinition[];
|
|
20
48
|
//# sourceMappingURL=run-command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/run-command.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9E,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAG1C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,aAAa,CAAC,CAkDxB;
|
|
1
|
+
{"version":3,"file":"run-command.d.ts","sourceRoot":"","sources":["../../../src/agent/tools/run-command.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAA+B,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9E,0DAA0D;AAC1D,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAG1C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,SAAS,GAAE,MAA2B,GACrC,OAAO,CAAC,aAAa,CAAC,CAkDxB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,QAAQ,GAChB;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;CACjC,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC,CAAC;AAEN,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAyB,GAClC,QAAQ,CAaV;AA8BD,eAAO,MAAM,cAAc,EAAE,cAmC5B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,cAAc,EAAqB,CAAC"}
|
|
@@ -48,8 +48,8 @@ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS)
|
|
|
48
48
|
const timer = setTimeout(() => {
|
|
49
49
|
timedOut = true;
|
|
50
50
|
killTree(child.pid);
|
|
51
|
-
// A process ignoring
|
|
52
|
-
setTimeout(() => killTree(child.pid,
|
|
51
|
+
// A process ignoring the polite request still has to go.
|
|
52
|
+
setTimeout(() => killTree(child.pid, true), 2000).unref();
|
|
53
53
|
}, timeoutMs);
|
|
54
54
|
const finish = (code, signal) => {
|
|
55
55
|
clearTimeout(timer);
|
|
@@ -59,23 +59,44 @@ export async function executeCommand(argv, cwd, timeoutMs = COMMAND_TIMEOUT_MS)
|
|
|
59
59
|
child.on("close", (code, signal) => finish(code, signal));
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
|
+
export function killPlan(pid, force, platform = process.platform) {
|
|
63
|
+
if (platform === "win32") {
|
|
64
|
+
return {
|
|
65
|
+
kind: "spawn",
|
|
66
|
+
command: "taskkill",
|
|
67
|
+
args: ["/PID", String(pid), "/T", ...(force ? ["/F"] : [])],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: "signal",
|
|
72
|
+
target: -pid,
|
|
73
|
+
signal: force ? "SIGKILL" : "SIGTERM",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
62
76
|
/**
|
|
63
|
-
* Kill the whole
|
|
77
|
+
* Kill the whole tree.
|
|
64
78
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* child is killed and the limitation is stated rather than papered over
|
|
69
|
-
* (no Windows machine here to verify a taskkill /T path).
|
|
79
|
+
* ⚠️ The Windows branch is UNVERIFIED on a real machine. Its shape is tested;
|
|
80
|
+
* whether `taskkill /T` actually reaches every grandchild of a Node-spawned
|
|
81
|
+
* process is not something this project has been able to check.
|
|
70
82
|
*/
|
|
71
|
-
function killTree(pid,
|
|
83
|
+
function killTree(pid, force = false) {
|
|
72
84
|
if (pid === undefined)
|
|
73
85
|
return;
|
|
86
|
+
const plan = killPlan(pid, force);
|
|
74
87
|
try {
|
|
75
|
-
if (
|
|
76
|
-
process.kill(
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
if (plan.kind === "signal") {
|
|
89
|
+
process.kill(plan.target, plan.signal);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Detached and ignored: this is fire-and-forget cleanup, and a taskkill
|
|
93
|
+
// that fails must not become an unhandled error on the way out.
|
|
94
|
+
const killer = spawn(plan.command, [...plan.args], {
|
|
95
|
+
stdio: "ignore",
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
});
|
|
98
|
+
killer.on("error", () => { });
|
|
99
|
+
killer.unref();
|
|
79
100
|
}
|
|
80
101
|
catch {
|
|
81
102
|
// Already exited, or never became a group leader.
|
package/dist/api/http.d.ts
CHANGED
|
@@ -44,10 +44,24 @@ export declare class HttpClient {
|
|
|
44
44
|
postJson<T>(path: string, body: unknown, options?: RequestOptions): Promise<HttpResponse<T>>;
|
|
45
45
|
/** For `/v1/audio/speech`, which answers with audio bytes. */
|
|
46
46
|
postBinary(path: string, body: unknown, options?: RequestOptions): Promise<HttpResponse<Uint8Array>>;
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Raw streaming response, for the SSE reader to consume.
|
|
49
|
+
*
|
|
50
|
+
* F-009 · Returns `abort` and the limits the reader should apply. The
|
|
51
|
+
* request timeout in `send` is cleared once the headers arrive - which for a
|
|
52
|
+
* stream is the start - so from here on the deadline belongs to whoever is
|
|
53
|
+
* reading the body, and it needs a way to close the connection rather than
|
|
54
|
+
* merely stop reading. Generation is what gets billed; dropping our end of
|
|
55
|
+
* a socket we are no longer reading does not stop it.
|
|
56
|
+
*/
|
|
48
57
|
postStream(path: string, body: unknown, options?: RequestOptions): Promise<{
|
|
49
58
|
response: Response;
|
|
50
59
|
requestId?: string;
|
|
60
|
+
abort: () => void;
|
|
61
|
+
limits: {
|
|
62
|
+
idleTimeoutMs: number;
|
|
63
|
+
maxBytes: number;
|
|
64
|
+
};
|
|
51
65
|
}>;
|
|
52
66
|
private send;
|
|
53
67
|
private readJson;
|
package/dist/api/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,QAAmB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,iBAAiB;IAShC,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAKrB,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAK3B,8DAA8D;IACxD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAiBpC
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/api/http.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,kBAAkB,SAAU,CAAC;AAC1C,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,QAAmB,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC;IACR,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAe;gBAE7B,OAAO,EAAE,iBAAiB;IAShC,OAAO,CAAC,CAAC,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAKrB,QAAQ,CAAC,CAAC,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAK3B,8DAA8D;IACxD,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;IAiBpC;;;;;;;;;OASG;IACG,UAAU,CACd,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC;QACT,QAAQ,EAAE,QAAQ,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,IAAI,CAAC;QAClB,MAAM,EAAE;YAAE,aAAa,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC;KACrD,CAAC;YA2BY,IAAI;YA4DJ,QAAQ;YAuCR,SAAS;CAexB;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CAkCrB"}
|
package/dist/api/http.js
CHANGED
|
@@ -49,15 +49,36 @@ export class HttpClient {
|
|
|
49
49
|
requestId,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Raw streaming response, for the SSE reader to consume.
|
|
54
|
+
*
|
|
55
|
+
* F-009 · Returns `abort` and the limits the reader should apply. The
|
|
56
|
+
* request timeout in `send` is cleared once the headers arrive - which for a
|
|
57
|
+
* stream is the start - so from here on the deadline belongs to whoever is
|
|
58
|
+
* reading the body, and it needs a way to close the connection rather than
|
|
59
|
+
* merely stop reading. Generation is what gets billed; dropping our end of
|
|
60
|
+
* a socket we are no longer reading does not stop it.
|
|
61
|
+
*/
|
|
53
62
|
async postStream(path, body, options = {}) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const streamController = new AbortController();
|
|
64
|
+
const response = await this.send("POST", path, body, {
|
|
65
|
+
...options,
|
|
66
|
+
signal: options.signal
|
|
67
|
+
? AbortSignal.any([options.signal, streamController.signal])
|
|
68
|
+
: streamController.signal,
|
|
69
|
+
}, { accept: "text/event-stream" });
|
|
57
70
|
const requestId = requestIdOf(response.headers);
|
|
58
71
|
if (!response.ok)
|
|
59
72
|
throw await this.errorFrom(response, requestId, options);
|
|
60
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
response,
|
|
75
|
+
requestId,
|
|
76
|
+
abort: () => streamController.abort(),
|
|
77
|
+
limits: {
|
|
78
|
+
idleTimeoutMs: options.timeoutMs ?? this.timeoutMs,
|
|
79
|
+
maxBytes: options.maxBytes ?? this.maxBytes,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
61
82
|
}
|
|
62
83
|
async send(method, path, body, options, extraHeaders = {}) {
|
|
63
84
|
const url = `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
package/dist/api/sse-parser.d.ts
CHANGED
|
@@ -62,11 +62,36 @@ export declare class ChatStreamAssembler {
|
|
|
62
62
|
push(payload: string): boolean;
|
|
63
63
|
result(): StreamResult;
|
|
64
64
|
}
|
|
65
|
+
/** F-009 · Limits that apply for the whole life of the stream. */
|
|
66
|
+
export interface StreamLimits {
|
|
67
|
+
/**
|
|
68
|
+
* How long to wait for the *next* chunk, not for the whole stream. A total
|
|
69
|
+
* deadline would kill a legitimately long generation; what "hung" actually
|
|
70
|
+
* means is that nothing is arriving.
|
|
71
|
+
*/
|
|
72
|
+
readonly idleTimeoutMs?: number;
|
|
73
|
+
/** Total bytes accepted before the stream is treated as hostile. */
|
|
74
|
+
readonly maxBytes?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Closes the underlying connection. Cancelling the reader alone stops us
|
|
77
|
+
* reading; it does not reliably stop the gateway generating, and generation
|
|
78
|
+
* is what gets billed.
|
|
79
|
+
*/
|
|
80
|
+
readonly abort?: () => void;
|
|
81
|
+
}
|
|
82
|
+
export declare const DEFAULT_STREAM_IDLE_MS = 120000;
|
|
83
|
+
export declare const DEFAULT_STREAM_MAX_BYTES: number;
|
|
65
84
|
/**
|
|
66
85
|
* Read a streaming response to completion.
|
|
67
86
|
*
|
|
68
87
|
* `signal` is the user's Ctrl+C: aborting stops reading AND closes the
|
|
69
88
|
* connection, which is what actually tells the gateway to stop generating.
|
|
89
|
+
*
|
|
90
|
+
* F-009 · The request timeout in `HttpClient` is cleared the moment the
|
|
91
|
+
* response headers arrive, which for a stream is the beginning rather than the
|
|
92
|
+
* end. Without the two limits below, a gateway that sent headers and then
|
|
93
|
+
* stopped would hang the CLI forever, and one that never stopped sending would
|
|
94
|
+
* grow the buffer until the process died.
|
|
70
95
|
*/
|
|
71
|
-
export declare function readChatStream(response: Response, events?: StreamEvents, signal?: AbortSignal): Promise<StreamResult>;
|
|
96
|
+
export declare function readChatStream(response: Response, events?: StreamEvents, signal?: AbortSignal, limits?: StreamLimits): Promise<StreamResult>;
|
|
72
97
|
//# sourceMappingURL=sse-parser.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sse-parser.d.ts","sourceRoot":"","sources":["../../src/api/sse-parser.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAWD,+EAA+E;AAC/E,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IAMjC,kDAAkD;IAClD,GAAG,IAAI,MAAM,EAAE;IASf,OAAO,CAAC,KAAK;CAYd;AAED,8DAA8D;AAC9D,qBAAa,mBAAmB;IAOlB,OAAO,CAAC,QAAQ,CAAC,MAAM;IANnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuC;IAC7D,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,KAAK,CAAC,CAAc;IAC5B,OAAO,CAAC,IAAI,CAAS;gBAEQ,MAAM,GAAE,YAAiB;IAEtD,4DAA4D;IAC5D,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAgD9B,MAAM,IAAI,YAAY;CASvB;AAED,kEAAkE;AAClE,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAC9C,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD;;;;;;;;;;;GAWG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,QAAQ,EAClB,MAAM,GAAE,YAAiB,EACzB,MAAM,CAAC,EAAE,WAAW,EACpB,MAAM,GAAE,YAAiB,GACxB,OAAO,CAAC,YAAY,CAAC,CA8DvB"}
|
package/dist/api/sse-parser.js
CHANGED
|
@@ -1,22 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Every rule here is a fact measured in Phase 0 against real transcripts from
|
|
5
|
-
* DeepSeek, Kimi, Qwen and Vertex Gemini (tests/fixtures/cli-000*), not an
|
|
6
|
-
* assumption about how streaming "usually" works:
|
|
7
|
-
*
|
|
8
|
-
* - `id` and `name` arrive exactly once, so tool calls merge by `index`.
|
|
9
|
-
* - Qwen sends `"id": ""` on continuation frames; `??` would overwrite the
|
|
10
|
-
* real id with an empty string and the tool result would be rejected.
|
|
11
|
-
* - Gemini packs a thought signature into the id (700+ chars) and delivers a
|
|
12
|
-
* whole call in one frame, where Kimi takes nine — fragment counts mean
|
|
13
|
-
* nothing.
|
|
14
|
-
* - `finish_reason` arrives in its own frame with an empty delta; `[DONE]`
|
|
15
|
-
* ends the stream.
|
|
16
|
-
* - With `stream_options.include_usage`, usage arrives AFTER finish_reason
|
|
17
|
-
* in a frame whose `choices` is non-empty but whose delta is empty. A
|
|
18
|
-
* parser that only reads usage when `choices` is empty reports zero cost.
|
|
19
|
-
*/
|
|
1
|
+
import { ApiError } from "./errors.js";
|
|
2
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
20
3
|
/** Split a byte stream into SSE `data:` payloads across arbitrary chunking. */
|
|
21
4
|
export class SseFramer {
|
|
22
5
|
buffer = "";
|
|
@@ -117,18 +100,29 @@ export class ChatStreamAssembler {
|
|
|
117
100
|
};
|
|
118
101
|
}
|
|
119
102
|
}
|
|
103
|
+
export const DEFAULT_STREAM_IDLE_MS = 120_000;
|
|
104
|
+
export const DEFAULT_STREAM_MAX_BYTES = 32 * 1024 * 1024;
|
|
120
105
|
/**
|
|
121
106
|
* Read a streaming response to completion.
|
|
122
107
|
*
|
|
123
108
|
* `signal` is the user's Ctrl+C: aborting stops reading AND closes the
|
|
124
109
|
* connection, which is what actually tells the gateway to stop generating.
|
|
110
|
+
*
|
|
111
|
+
* F-009 · The request timeout in `HttpClient` is cleared the moment the
|
|
112
|
+
* response headers arrive, which for a stream is the beginning rather than the
|
|
113
|
+
* end. Without the two limits below, a gateway that sent headers and then
|
|
114
|
+
* stopped would hang the CLI forever, and one that never stopped sending would
|
|
115
|
+
* grow the buffer until the process died.
|
|
125
116
|
*/
|
|
126
|
-
export async function readChatStream(response, events = {}, signal) {
|
|
117
|
+
export async function readChatStream(response, events = {}, signal, limits = {}) {
|
|
127
118
|
const framer = new SseFramer();
|
|
128
119
|
const assembler = new ChatStreamAssembler(events);
|
|
129
120
|
const body = response.body;
|
|
130
121
|
if (!body)
|
|
131
122
|
return assembler.result();
|
|
123
|
+
const idleMs = limits.idleTimeoutMs ?? DEFAULT_STREAM_IDLE_MS;
|
|
124
|
+
const maxBytes = limits.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
125
|
+
let received = 0;
|
|
132
126
|
const reader = body.getReader();
|
|
133
127
|
const onAbort = () => void reader.cancel();
|
|
134
128
|
// Attaching a listener is not enough: a signal that aborted between the
|
|
@@ -144,9 +138,29 @@ export async function readChatStream(response, events = {}, signal) {
|
|
|
144
138
|
for (;;) {
|
|
145
139
|
if (signal?.aborted)
|
|
146
140
|
break;
|
|
147
|
-
const
|
|
141
|
+
const chunk = await withIdleDeadline(reader.read(), idleMs);
|
|
142
|
+
if (chunk === IDLE) {
|
|
143
|
+
limits.abort?.();
|
|
144
|
+
await reader.cancel().catch(() => { });
|
|
145
|
+
throw new ApiError({
|
|
146
|
+
kind: "timeout",
|
|
147
|
+
exitCode: ExitCode.Unavailable,
|
|
148
|
+
message: `the stream sent nothing for ${idleMs}ms`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const { done, value } = chunk;
|
|
148
152
|
if (done)
|
|
149
153
|
break;
|
|
154
|
+
received += value.byteLength;
|
|
155
|
+
if (received > maxBytes) {
|
|
156
|
+
limits.abort?.();
|
|
157
|
+
await reader.cancel().catch(() => { });
|
|
158
|
+
throw new ApiError({
|
|
159
|
+
kind: "too_large",
|
|
160
|
+
exitCode: ExitCode.Unavailable,
|
|
161
|
+
message: `the stream exceeded the ${maxBytes} byte limit`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
150
164
|
for (const payload of framer.push(value)) {
|
|
151
165
|
if (assembler.push(payload))
|
|
152
166
|
return assembler.result();
|
|
@@ -161,3 +175,30 @@ export async function readChatStream(response, events = {}, signal) {
|
|
|
161
175
|
}
|
|
162
176
|
return assembler.result();
|
|
163
177
|
}
|
|
178
|
+
/** Sentinel for "the deadline won", distinguishable from a real chunk. */
|
|
179
|
+
const IDLE = Symbol("idle");
|
|
180
|
+
/**
|
|
181
|
+
* Race a read against the idle deadline.
|
|
182
|
+
*
|
|
183
|
+
* The timer is unref'd and always cleared: a pending timer would keep the
|
|
184
|
+
* process alive after a stream that finished normally.
|
|
185
|
+
*/
|
|
186
|
+
async function withIdleDeadline(read, idleMs) {
|
|
187
|
+
let timer;
|
|
188
|
+
try {
|
|
189
|
+
return await Promise.race([
|
|
190
|
+
read,
|
|
191
|
+
new Promise((resolve) => {
|
|
192
|
+
// Deliberately not unref'd: if a stalled stream were the only thing
|
|
193
|
+
// left, an unref'd timer would let the process exit silently instead
|
|
194
|
+
// of reporting the timeout. The finally below always clears it, so
|
|
195
|
+
// nothing is held open after a stream that ended normally.
|
|
196
|
+
timer = setTimeout(() => resolve(IDLE), idleMs);
|
|
197
|
+
}),
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
if (timer !== undefined)
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAKA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/commands/agent.ts"],"names":[],"mappings":"AAKA,OAAO,EAAe,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAYvE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAGnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,eAAO,MAAM,gBAAgB,0XAK+C,CAAC;AAE7E,eAAO,MAAM,YAAY,mgBAOmB,CAAC;AAE7C,MAAM,WAAW,OAAQ,SAAQ,SAAS;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,uBAAuB,EAAE,QAAQ,CAAC;CACrD;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,QAAQ,CAAC,CAsHnB;AAyJD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,kBAAkB,EAAE,OAAO,0JAI3E"}
|
package/dist/commands/agent.js
CHANGED
|
@@ -2,6 +2,11 @@ import { fetchAccessibleModels, fetchCatalog, intersectModels, } from "../api/mo
|
|
|
2
2
|
import { approverFor } from "../agent/approvals.js";
|
|
3
3
|
import { runAgent } from "../agent/loop.js";
|
|
4
4
|
import { runSession } from "../agent/session.js";
|
|
5
|
+
import { sessionsDir } from "../config/paths.js";
|
|
6
|
+
import { keyDigest } from "../security/redact.js";
|
|
7
|
+
import { decideRecording } from "../sessions/policy.js";
|
|
8
|
+
import { SessionRecorder } from "../sessions/recorder.js";
|
|
9
|
+
import { SessionWriter } from "../sessions/store.js";
|
|
5
10
|
import { READ_ONLY_TOOLS } from "../agent/tools/read-only.js";
|
|
6
11
|
import { COMMAND_TOOLS } from "../agent/tools/run-command.js";
|
|
7
12
|
import { WRITE_TOOLS } from "../agent/tools/write.js";
|
|
@@ -74,8 +79,14 @@ export async function agentCommand(argv, io) {
|
|
|
74
79
|
io.stderr(`mafdet agent: no goal\n${USAGE}`);
|
|
75
80
|
return ExitCode.Usage;
|
|
76
81
|
}
|
|
82
|
+
// F-007 · `decideRecording` already called this surface "interactive",
|
|
83
|
+
// so the policy claimed these runs were saved while nothing saved them.
|
|
84
|
+
const recorder = openRecorder(parsed, io, model, workspace.root, mode);
|
|
77
85
|
const summary = await runSession({
|
|
78
86
|
client: runtime.dataPlane,
|
|
87
|
+
...(recorder === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { onEvent: (event) => recorder.observe(event) }),
|
|
79
90
|
model,
|
|
80
91
|
pricing: resolved,
|
|
81
92
|
catalog,
|
|
@@ -89,6 +100,7 @@ export async function agentCommand(argv, io) {
|
|
|
89
100
|
stderr: io.stderr,
|
|
90
101
|
signal: io.signal,
|
|
91
102
|
});
|
|
103
|
+
recorder?.close();
|
|
92
104
|
io.stderr("\n" +
|
|
93
105
|
renderRecord([
|
|
94
106
|
["goals", `${summary.goals}`],
|
|
@@ -130,6 +142,45 @@ export async function agentCommand(argv, io) {
|
|
|
130
142
|
return reportError(error, io);
|
|
131
143
|
}
|
|
132
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* F-007 · A recorder for an interactive `mafdet agent`, or nothing.
|
|
147
|
+
*
|
|
148
|
+
* The same store, policy and failure behaviour as the TUI: `--no-history`
|
|
149
|
+
* writes nothing at all, and a store that cannot be opened costs the
|
|
150
|
+
* transcript rather than the session.
|
|
151
|
+
*/
|
|
152
|
+
function openRecorder(parsed, io, modelId, workspaceRoot, mode) {
|
|
153
|
+
const decision = decideRecording({
|
|
154
|
+
surface: "interactive",
|
|
155
|
+
noHistory: parsed.noHistory,
|
|
156
|
+
save: false,
|
|
157
|
+
});
|
|
158
|
+
if (!decision.record)
|
|
159
|
+
return undefined;
|
|
160
|
+
const env = io.env ?? process.env;
|
|
161
|
+
const root = sessionsDir(env);
|
|
162
|
+
try {
|
|
163
|
+
const writer = SessionWriter.create({
|
|
164
|
+
root,
|
|
165
|
+
title: parsed.title ??
|
|
166
|
+
workspaceRoot.split(/[\\/]/).filter(Boolean).at(-1) ??
|
|
167
|
+
"Conversation",
|
|
168
|
+
modelId,
|
|
169
|
+
workspace: workspaceRoot,
|
|
170
|
+
mode,
|
|
171
|
+
});
|
|
172
|
+
return new SessionRecorder({
|
|
173
|
+
writer,
|
|
174
|
+
root,
|
|
175
|
+
apiKeyRef: keyDigest(env.MAFDET_API_KEY ?? ""),
|
|
176
|
+
onProblem: (message) => io.stderr(`${message}\n`),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
io.stderr(`not recording this conversation: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
133
184
|
function parseArgs(argv, io) {
|
|
134
185
|
const positional = [];
|
|
135
186
|
let model;
|
|
@@ -137,6 +188,8 @@ function parseArgs(argv, io) {
|
|
|
137
188
|
let maxCostMicroUsd;
|
|
138
189
|
let json = false;
|
|
139
190
|
let allowWrite = false;
|
|
191
|
+
let noHistory = false;
|
|
192
|
+
let title;
|
|
140
193
|
const allowedCommands = [];
|
|
141
194
|
for (let i = 0; i < argv.length; i += 1) {
|
|
142
195
|
const arg = argv[i];
|
|
@@ -157,6 +210,25 @@ function parseArgs(argv, io) {
|
|
|
157
210
|
}
|
|
158
211
|
else if (arg === "--read-only")
|
|
159
212
|
allowWrite = false;
|
|
213
|
+
else if (arg === "--no-history")
|
|
214
|
+
noHistory = true;
|
|
215
|
+
else if (arg === "--title") {
|
|
216
|
+
const entry = next();
|
|
217
|
+
if (!entry) {
|
|
218
|
+
io.stderr("mafdet agent: --title needs a value\n");
|
|
219
|
+
return ExitCode.Usage;
|
|
220
|
+
}
|
|
221
|
+
title = entry;
|
|
222
|
+
}
|
|
223
|
+
else if (arg === "--resume") {
|
|
224
|
+
// F-007 · Deliberately not implemented here, and said rather than
|
|
225
|
+
// silently ignored. Resuming has to reconcile the session's model and
|
|
226
|
+
// workspace against the current key, which the TUI has a place to do
|
|
227
|
+
// and this surface does not - and a second, weaker resume path is
|
|
228
|
+
// exactly how the two drift apart. F-003 was that drift.
|
|
229
|
+
io.stderr("mafdet agent: --resume is not available here; run 'mafdet --resume <id>'\n");
|
|
230
|
+
return ExitCode.Usage;
|
|
231
|
+
}
|
|
160
232
|
else if (arg === "--max-turns") {
|
|
161
233
|
const raw = next();
|
|
162
234
|
maxTurns = Number(raw);
|
|
@@ -198,6 +270,8 @@ function parseArgs(argv, io) {
|
|
|
198
270
|
maxTurns,
|
|
199
271
|
maxCostMicroUsd,
|
|
200
272
|
json,
|
|
273
|
+
noHistory,
|
|
274
|
+
...(title === undefined ? {} : { title }),
|
|
201
275
|
};
|
|
202
276
|
}
|
|
203
277
|
/**
|
|
@@ -258,6 +332,8 @@ function finish(report, json, io) {
|
|
|
258
332
|
case "max_turns":
|
|
259
333
|
case "timeout":
|
|
260
334
|
case "max_cost":
|
|
335
|
+
// F-008 · A ceiling too: the window ran out, and the report says which.
|
|
336
|
+
case "context_limit":
|
|
261
337
|
// Stopped short on purpose; the report says so rather than pretending.
|
|
262
338
|
return ExitCode.LimitReached;
|
|
263
339
|
case "rate_limited":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/commands/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWxE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAatB,MAAM,WAAW,MAAO,SAAQ,SAAS;IACvC,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,gDAAgD;IAChD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAcD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AA8SD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,WAAW,GACjB,MAAM,GAAG,SAAS,CASpB"}
|