@addai/node 0.25.0 → 0.27.0
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/dist/kimi-spawn.d.ts +62 -18
- package/dist/kimi-spawn.js +159 -66
- package/dist/session-runner.js +32 -1
- package/dist/subagent-usage.d.ts +19 -0
- package/dist/subagent-usage.js +173 -0
- package/dist/types.d.ts +8 -0
- package/package.json +1 -1
package/dist/kimi-spawn.d.ts
CHANGED
|
@@ -11,7 +11,9 @@ export interface KimiInput {
|
|
|
11
11
|
model?: string;
|
|
12
12
|
/** Resume an existing session (mapped to `--session <id>`). */
|
|
13
13
|
resumeSessionId?: string;
|
|
14
|
-
/**
|
|
14
|
+
/** Accepted for interface parity with the other adapters and deliberately
|
|
15
|
+
* ignored: prompt mode always runs in "auto" permission mode, and passing
|
|
16
|
+
* --yolo alongside --prompt is a hard error in the CLI. */
|
|
15
17
|
bypassPermissions?: boolean;
|
|
16
18
|
/** When set, kimi runs against a sandboxed mcp config that contains
|
|
17
19
|
* ONLY these MCP servers (host's ~/.kimi/mcp.json suppressed). */
|
|
@@ -20,12 +22,12 @@ export interface KimiInput {
|
|
|
20
22
|
* --system-prompt flag, so we prepend it to the user prompt with a
|
|
21
23
|
* separator the model can recognise. */
|
|
22
24
|
appendSystemPrompt?: string;
|
|
23
|
-
/** Per-run reasoning effort.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
/** Per-run reasoning effort. kimi has no effort FLAG, but it does read
|
|
26
|
+
* KIMI_MODEL_THINKING_EFFORT from the environment, which bypasses the
|
|
27
|
+
* model's declared support_efforts list. (The old code set
|
|
28
|
+
* KIMI_REASONING_EFFORT, a name that appears nowhere in the CLI, so effort
|
|
29
|
+
* was simply ignored.) 'max' is migrated to 'high' by kimi itself; we send
|
|
30
|
+
* 'high' rather than rely on that. */
|
|
29
31
|
effortLevel?: RuntimeEffortLevel;
|
|
30
32
|
}
|
|
31
33
|
export interface KimiHandle {
|
|
@@ -39,16 +41,58 @@ export interface KimiHandle {
|
|
|
39
41
|
onActivity(cb: () => void): void;
|
|
40
42
|
done: Promise<number>;
|
|
41
43
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
44
|
+
/**
|
|
45
|
+
* Write the entity's MCP servers where kimi will actually read them:
|
|
46
|
+
* `<workDir>/.kimi-code/mcp.json`, the last and highest-precedence of the
|
|
47
|
+
* three files it merges (user `~/.kimi-code/mcp.json`, project-root
|
|
48
|
+
* `.mcp.json`, then this one).
|
|
49
|
+
*
|
|
50
|
+
* The two alternatives are both worse. `--mcp-config-file` — what this used
|
|
51
|
+
* to pass — is not a flag kimi has, so it took the whole run down at argument
|
|
52
|
+
* parsing. Pointing KIMI_CODE_HOME at a throwaway dir WOULD isolate the user
|
|
53
|
+
* file, but that dir also holds kimi's credentials, config.toml and session
|
|
54
|
+
* index: it would log the daemon out on every run. Same trap as the HOME
|
|
55
|
+
* override this file already backed away from.
|
|
56
|
+
*
|
|
57
|
+
* The cost is that a server configured on the host machine still merges in.
|
|
58
|
+
* Ours always win on key collision, and the file is rewritten on every spawn
|
|
59
|
+
* so a detached MCP disappears on the next run rather than lingering.
|
|
60
|
+
*/
|
|
61
|
+
export declare function writeKimiProjectMcpConfig(workingDirectory: string, servers: KimiMcpServer[]): string;
|
|
62
|
+
/**
|
|
63
|
+
* Parse one stream-json line into 0+ RuntimeEvents.
|
|
64
|
+
*
|
|
65
|
+
* kimi's prompt-mode stream is OpenAI-CHAT shaped, not claude shaped: every
|
|
66
|
+
* content line is keyed by `role`, and only the three metadata lines carry a
|
|
67
|
+
* `type` at all (`system.version`, `session.resume_hint`,
|
|
68
|
+
* `turn.step.retrying`). The role branches therefore come first. Without them
|
|
69
|
+
* every real line fell through to the opaque passthrough at the bottom, which
|
|
70
|
+
* meant a kimi run that worked perfectly would still have delivered an empty
|
|
71
|
+
* reply — a second, independent bug hiding behind the argument-parsing one.
|
|
72
|
+
*
|
|
73
|
+
* Shapes, from PromptJsonWriter in the shipped bundle:
|
|
74
|
+
* {role:"assistant", content?:string, tool_calls?:[{id,function:{name,arguments}}]}
|
|
75
|
+
* {role:"tool", tool_call_id:string, content:string}
|
|
76
|
+
* {role:"meta", type:"...", ...}
|
|
77
|
+
*
|
|
78
|
+
* Note there is no terminal line and no usage line: the runner keys
|
|
79
|
+
* completion off process exit, and kimi runs report no token counts.
|
|
80
|
+
*
|
|
81
|
+
* The `type`-keyed branches below are kept for older builds, and anything
|
|
82
|
+
* unrecognised is still forwarded as `kimi:<type>` rather than dropped.
|
|
83
|
+
*/
|
|
53
84
|
export declare function lineToEvents(line: Record<string, unknown>): RuntimeEvent[];
|
|
85
|
+
/**
|
|
86
|
+
* The kimi command line for one prompt-mode run.
|
|
87
|
+
*
|
|
88
|
+
* Extracted so the flag set is testable without spawning anything — this is
|
|
89
|
+
* the surface where four invented flags shipped undetected, each one killing
|
|
90
|
+
* the run before a single token was generated. Every flag below is present in
|
|
91
|
+
* the CLI's own option table.
|
|
92
|
+
*
|
|
93
|
+
* Not here on purpose: --work-dir (kimi reads process.cwd(), which the spawn
|
|
94
|
+
* sets), --mcp-config-file (config comes from <cwd>/.kimi-code/mcp.json), and
|
|
95
|
+
* --yolo (prompt mode forces "auto" permissions and rejects the flag).
|
|
96
|
+
*/
|
|
97
|
+
export declare function buildKimiArgs(input: KimiInput): string[];
|
|
54
98
|
export declare function spawnKimi(input: KimiInput): KimiHandle;
|
package/dist/kimi-spawn.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// kimi --
|
|
2
|
+
// kimi --prompt --output-format stream-json adapter.
|
|
3
3
|
//
|
|
4
|
-
// Mirrors claude-print.ts: spawns `kimi` non-interactively with a
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Mirrors claude-print.ts: spawns `kimi` non-interactively with a JSONL
|
|
5
|
+
// output stream, parses each line into our normalised RuntimeEvent shape,
|
|
6
|
+
// and resolves on exit.
|
|
7
|
+
//
|
|
8
|
+
// Everything here is read out of the shipped bundle (@moonshot-ai/kimi-code
|
|
9
|
+
// 0.36.0, dist/main.mjs), not the docs, because the previous version of this
|
|
10
|
+
// file was written from the docs and got FOUR things wrong at once — every
|
|
11
|
+
// one of them fatal:
|
|
12
|
+
//
|
|
13
|
+
// --print does not exist. `-p, --prompt <prompt>` IS prompt
|
|
14
|
+
// mode and takes the prompt as its value. Every kimi
|
|
15
|
+
// run died at argument parsing, silently laddering to
|
|
16
|
+
// claude.
|
|
17
|
+
// --work-dir <dir> does not exist. kimi takes its work dir from
|
|
18
|
+
// process.cwd(), which spawn() already sets.
|
|
19
|
+
// --mcp-config-file does not exist. kimi merges three files; the
|
|
20
|
+
// highest-precedence one is <cwd>/.kimi-code/mcp.json.
|
|
21
|
+
// --yolo is REJECTED alongside --prompt ("Cannot combine
|
|
22
|
+
// --prompt with --yolo"). It is also unnecessary:
|
|
23
|
+
// prompt mode calls forceAuto() and runs the whole
|
|
24
|
+
// turn in "auto" permission mode by itself.
|
|
25
|
+
//
|
|
26
|
+
// The output format was wrong too — see lineToEvents.
|
|
9
27
|
//
|
|
10
28
|
// Reference: https://moonshotai.github.io/kimi-cli/en/reference/kimi-command.html
|
|
11
29
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -42,24 +60,46 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
60
|
};
|
|
43
61
|
})();
|
|
44
62
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.
|
|
63
|
+
exports.writeKimiProjectMcpConfig = writeKimiProjectMcpConfig;
|
|
46
64
|
exports.lineToEvents = lineToEvents;
|
|
65
|
+
exports.buildKimiArgs = buildKimiArgs;
|
|
47
66
|
exports.spawnKimi = spawnKimi;
|
|
48
67
|
const child_process_1 = require("child_process");
|
|
49
68
|
const fs = __importStar(require("fs"));
|
|
50
|
-
const os = __importStar(require("os"));
|
|
51
69
|
const path = __importStar(require("path"));
|
|
52
70
|
const kimi_binary_1 = require("./kimi-binary");
|
|
53
71
|
const win_1 = require("./win");
|
|
54
72
|
const events_1 = require("./events");
|
|
55
73
|
const think_split_1 = require("./think-split");
|
|
56
74
|
const mcp_headers_1 = require("./mcp-headers");
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
/** kimi forwards this string to the provider verbatim — the override exists
|
|
76
|
+
* precisely to bypass the model's declared support_efforts — so a level kimi
|
|
77
|
+
* has never heard of reaches the API as-is. It retired 'max' (it migrates a
|
|
78
|
+
* stored 'max' to 'high' on load) and never had our 'xhigh', so both collapse
|
|
79
|
+
* to the highest level it does know. */
|
|
80
|
+
function kimiEffort(level) {
|
|
81
|
+
return level === 'max' || level === 'xhigh' ? 'high' : level;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Write the entity's MCP servers where kimi will actually read them:
|
|
85
|
+
* `<workDir>/.kimi-code/mcp.json`, the last and highest-precedence of the
|
|
86
|
+
* three files it merges (user `~/.kimi-code/mcp.json`, project-root
|
|
87
|
+
* `.mcp.json`, then this one).
|
|
88
|
+
*
|
|
89
|
+
* The two alternatives are both worse. `--mcp-config-file` — what this used
|
|
90
|
+
* to pass — is not a flag kimi has, so it took the whole run down at argument
|
|
91
|
+
* parsing. Pointing KIMI_CODE_HOME at a throwaway dir WOULD isolate the user
|
|
92
|
+
* file, but that dir also holds kimi's credentials, config.toml and session
|
|
93
|
+
* index: it would log the daemon out on every run. Same trap as the HOME
|
|
94
|
+
* override this file already backed away from.
|
|
95
|
+
*
|
|
96
|
+
* The cost is that a server configured on the host machine still merges in.
|
|
97
|
+
* Ours always win on key collision, and the file is rewritten on every spawn
|
|
98
|
+
* so a detached MCP disappears on the next run rather than lingering.
|
|
99
|
+
*/
|
|
100
|
+
function writeKimiProjectMcpConfig(workingDirectory, servers) {
|
|
101
|
+
const configDir = path.join(workingDirectory, '.kimi-code');
|
|
102
|
+
fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
63
103
|
const mcpConfig = { mcpServers: {} };
|
|
64
104
|
for (const s of servers) {
|
|
65
105
|
if (!s.slug || !s.command)
|
|
@@ -90,15 +130,76 @@ function writeKimiSandboxHome(servers) {
|
|
|
90
130
|
...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
|
|
91
131
|
};
|
|
92
132
|
}
|
|
93
|
-
|
|
133
|
+
// 0600: stdio servers carry their credentials in `env` and remote ones in
|
|
134
|
+
// `headers`, and this file lives inside the entity's working directory.
|
|
135
|
+
const mcpConfigPath = path.join(configDir, 'mcp.json');
|
|
94
136
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), { mode: 0o600 });
|
|
95
|
-
return
|
|
137
|
+
return mcpConfigPath;
|
|
96
138
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
139
|
+
/**
|
|
140
|
+
* Parse one stream-json line into 0+ RuntimeEvents.
|
|
141
|
+
*
|
|
142
|
+
* kimi's prompt-mode stream is OpenAI-CHAT shaped, not claude shaped: every
|
|
143
|
+
* content line is keyed by `role`, and only the three metadata lines carry a
|
|
144
|
+
* `type` at all (`system.version`, `session.resume_hint`,
|
|
145
|
+
* `turn.step.retrying`). The role branches therefore come first. Without them
|
|
146
|
+
* every real line fell through to the opaque passthrough at the bottom, which
|
|
147
|
+
* meant a kimi run that worked perfectly would still have delivered an empty
|
|
148
|
+
* reply — a second, independent bug hiding behind the argument-parsing one.
|
|
149
|
+
*
|
|
150
|
+
* Shapes, from PromptJsonWriter in the shipped bundle:
|
|
151
|
+
* {role:"assistant", content?:string, tool_calls?:[{id,function:{name,arguments}}]}
|
|
152
|
+
* {role:"tool", tool_call_id:string, content:string}
|
|
153
|
+
* {role:"meta", type:"...", ...}
|
|
154
|
+
*
|
|
155
|
+
* Note there is no terminal line and no usage line: the runner keys
|
|
156
|
+
* completion off process exit, and kimi runs report no token counts.
|
|
157
|
+
*
|
|
158
|
+
* The `type`-keyed branches below are kept for older builds, and anything
|
|
159
|
+
* unrecognised is still forwarded as `kimi:<type>` rather than dropped.
|
|
160
|
+
*/
|
|
101
161
|
function lineToEvents(line) {
|
|
162
|
+
const role = typeof line.role === 'string' ? line.role : '';
|
|
163
|
+
if (role === 'assistant') {
|
|
164
|
+
const out = [];
|
|
165
|
+
if (typeof line.content === 'string' && line.content) {
|
|
166
|
+
out.push({ type: 'assistant_text', delta: line.content });
|
|
167
|
+
}
|
|
168
|
+
const calls = Array.isArray(line.tool_calls) ? line.tool_calls : [];
|
|
169
|
+
for (const c of calls) {
|
|
170
|
+
// Arguments arrive as a JSON string (streamed in parts, joined by the
|
|
171
|
+
// writer). Keep the raw string if it doesn't parse — a malformed
|
|
172
|
+
// argument list is worth showing, not worth losing the tool call over.
|
|
173
|
+
const rawArgs = c?.function?.arguments;
|
|
174
|
+
let input = rawArgs;
|
|
175
|
+
if (typeof rawArgs === 'string' && rawArgs.length > 0) {
|
|
176
|
+
try {
|
|
177
|
+
input = JSON.parse(rawArgs);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
input = rawArgs;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
out.push({
|
|
184
|
+
type: 'tool_use',
|
|
185
|
+
id: String(c?.id ?? ''),
|
|
186
|
+
name: String(c?.function?.name ?? ''),
|
|
187
|
+
input,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// An assistant line with neither content nor calls shouldn't happen (the
|
|
191
|
+
// writer skips empty flushes) — pass it through rather than swallow it.
|
|
192
|
+
return out.length > 0 ? out : [{ type: 'kimi:assistant', raw: line }];
|
|
193
|
+
}
|
|
194
|
+
if (role === 'tool') {
|
|
195
|
+
// kimi has no error channel on tool results; failures come back as text.
|
|
196
|
+
return [{
|
|
197
|
+
type: 'tool_result',
|
|
198
|
+
id: String(line.tool_call_id ?? ''),
|
|
199
|
+
content: line.content,
|
|
200
|
+
isError: false,
|
|
201
|
+
}];
|
|
202
|
+
}
|
|
102
203
|
const type = typeof line.type === 'string' ? line.type : '';
|
|
103
204
|
// Assistant text — kimi emits 'assistant_message' or 'assistant.delta'
|
|
104
205
|
// depending on version.
|
|
@@ -179,6 +280,36 @@ function lineToEvents(line) {
|
|
|
179
280
|
// Anything else — opaque passthrough
|
|
180
281
|
return [{ type: `kimi:${type || 'unknown'}`, raw: line }];
|
|
181
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* The kimi command line for one prompt-mode run.
|
|
285
|
+
*
|
|
286
|
+
* Extracted so the flag set is testable without spawning anything — this is
|
|
287
|
+
* the surface where four invented flags shipped undetected, each one killing
|
|
288
|
+
* the run before a single token was generated. Every flag below is present in
|
|
289
|
+
* the CLI's own option table.
|
|
290
|
+
*
|
|
291
|
+
* Not here on purpose: --work-dir (kimi reads process.cwd(), which the spawn
|
|
292
|
+
* sets), --mcp-config-file (config comes from <cwd>/.kimi-code/mcp.json), and
|
|
293
|
+
* --yolo (prompt mode forces "auto" permissions and rejects the flag).
|
|
294
|
+
*/
|
|
295
|
+
function buildKimiArgs(input) {
|
|
296
|
+
// --output-format is only accepted in prompt mode, which --prompt turns on.
|
|
297
|
+
const args = ['--output-format', 'stream-json'];
|
|
298
|
+
// kimi refuses a bare --session in prompt mode, so only pass it with an id.
|
|
299
|
+
if (input.resumeSessionId)
|
|
300
|
+
args.push('--session', input.resumeSessionId);
|
|
301
|
+
if (input.model)
|
|
302
|
+
args.push('--model', input.model);
|
|
303
|
+
// kimi has no --append-system-prompt; prepend with a separator the model can
|
|
304
|
+
// recognise as a system-like instruction block.
|
|
305
|
+
let promptText = input.prompt;
|
|
306
|
+
if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
|
|
307
|
+
promptText = `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`;
|
|
308
|
+
}
|
|
309
|
+
// Last, and always: --prompt takes the prompt as its VALUE.
|
|
310
|
+
args.push('--prompt', promptText);
|
|
311
|
+
return args;
|
|
312
|
+
}
|
|
182
313
|
function spawnKimi(input) {
|
|
183
314
|
const bin = (0, kimi_binary_1.findKimiBinary)();
|
|
184
315
|
if (!bin) {
|
|
@@ -201,49 +332,20 @@ function spawnKimi(input) {
|
|
|
201
332
|
done: Promise.resolve(127),
|
|
202
333
|
};
|
|
203
334
|
}
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
//
|
|
209
|
-
// The OLD approach (HOME=<tempdir>) was destructive: kimi shells out
|
|
210
|
-
// to git, pip, etc., and those tools read ~/.gitconfig, ~/.npmrc,
|
|
211
|
-
// ~/.netrc from HOME. Pointing HOME at an empty dir blew away every
|
|
212
|
-
// user-level tool config, causing silent tool-call failures
|
|
213
|
-
// (git push without creds, pip install without index URL).
|
|
214
|
-
const { homeDir: kimiSandboxDir, mcpConfigPath } = writeKimiSandboxHome(input.mcpServers ?? []);
|
|
215
|
-
const args = [
|
|
216
|
-
'--print',
|
|
217
|
-
'--output-format', 'stream-json',
|
|
218
|
-
'--work-dir', input.workingDirectory,
|
|
219
|
-
'--mcp-config-file', mcpConfigPath,
|
|
220
|
-
];
|
|
221
|
-
if (input.resumeSessionId) {
|
|
222
|
-
args.push('--session', input.resumeSessionId);
|
|
223
|
-
}
|
|
224
|
-
if (input.model) {
|
|
225
|
-
args.push('--model', input.model);
|
|
226
|
-
}
|
|
227
|
-
if (input.bypassPermissions) {
|
|
228
|
-
args.push('--yolo');
|
|
229
|
-
}
|
|
230
|
-
// Kimi doesn't have --append-system-prompt; prepend with a separator
|
|
231
|
-
// the model can recognise as a system-like instruction block.
|
|
232
|
-
let promptText = input.prompt;
|
|
233
|
-
if (input.appendSystemPrompt && input.appendSystemPrompt.trim().length > 0) {
|
|
234
|
-
promptText = `[system]\n${input.appendSystemPrompt.trim()}\n[/system]\n\n${input.prompt}`;
|
|
235
|
-
}
|
|
236
|
-
args.push('--prompt', promptText);
|
|
335
|
+
// MCP servers reach kimi through a file in the working directory, not a
|
|
336
|
+
// flag. Written before spawn and rewritten every run — see above.
|
|
337
|
+
writeKimiProjectMcpConfig(input.workingDirectory, input.mcpServers ?? []);
|
|
338
|
+
const args = buildKimiArgs(input);
|
|
237
339
|
// No HOME override: the real $HOME flows through so kimi's own auth +
|
|
238
340
|
// shelled-out tools (git/pip/node) can find their config files. The
|
|
239
341
|
// explicit --mcp-config-file already prevents host mcp.json leakage.
|
|
240
342
|
const childEnv = {
|
|
241
343
|
...process.env,
|
|
242
344
|
TERM: 'dumb',
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
...(input.effortLevel ? {
|
|
345
|
+
// kimi's own effort override. It bypasses the model's support_efforts
|
|
346
|
+
// list, and kimi migrates a stored 'max' to 'high' — so normalise here
|
|
347
|
+
// rather than send a level the provider may reject.
|
|
348
|
+
...(input.effortLevel ? { KIMI_MODEL_THINKING_EFFORT: kimiEffort(input.effortLevel) } : {}),
|
|
247
349
|
};
|
|
248
350
|
const listeners = [];
|
|
249
351
|
const emit = (e) => { for (const l of listeners)
|
|
@@ -273,10 +375,6 @@ function spawnKimi(input) {
|
|
|
273
375
|
}
|
|
274
376
|
catch (err) {
|
|
275
377
|
const msg = err.message || String(err);
|
|
276
|
-
try {
|
|
277
|
-
fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
|
|
278
|
-
}
|
|
279
|
-
catch { }
|
|
280
378
|
const events = (0, events_1.bufferedEvents)([
|
|
281
379
|
{ type: 'error', code: 'kimi_spawn_failed', message: msg },
|
|
282
380
|
{ type: 'turn_complete', stopReason: 'failed' },
|
|
@@ -328,11 +426,6 @@ function spawnKimi(input) {
|
|
|
328
426
|
// Release anything the splitter is still holding — a run that ends
|
|
329
427
|
// mid-tag must not lose the tail of its answer.
|
|
330
428
|
emitSplit.flush();
|
|
331
|
-
// Best-effort cleanup of sandbox tmp dir
|
|
332
|
-
try {
|
|
333
|
-
fs.rmSync(kimiSandboxDir, { recursive: true, force: true });
|
|
334
|
-
}
|
|
335
|
-
catch { }
|
|
336
429
|
resolve(code ?? -1);
|
|
337
430
|
});
|
|
338
431
|
});
|
package/dist/session-runner.js
CHANGED
|
@@ -59,6 +59,7 @@ const events_1 = require("./events");
|
|
|
59
59
|
const paths_1 = require("./paths");
|
|
60
60
|
const projects_1 = require("./projects");
|
|
61
61
|
const claude_print_1 = require("./claude-print");
|
|
62
|
+
const subagent_usage_1 = require("./subagent-usage");
|
|
62
63
|
const attachments_1 = require("./attachments");
|
|
63
64
|
const codex_spawn_1 = require("./codex-spawn");
|
|
64
65
|
const diskguard_1 = require("./diskguard");
|
|
@@ -808,6 +809,26 @@ function isOwnSessionDir(dir) {
|
|
|
808
809
|
const resolved = path.resolve(dir);
|
|
809
810
|
return resolved.startsWith(root + path.sep);
|
|
810
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Report what this run's subagents cost, before the run goes terminal.
|
|
814
|
+
*
|
|
815
|
+
* Awaited rather than fired-and-forgotten: the server recomputes a run's
|
|
816
|
+
* rollup from its events the moment the status flips, so an event that lands
|
|
817
|
+
* a beat later would be counted only if something else came along to trigger
|
|
818
|
+
* a recompute. Failure here must never change the run's outcome — a missing
|
|
819
|
+
* figure is a worse report, not a worse run.
|
|
820
|
+
*/
|
|
821
|
+
async function emitSubagentUsage(requestId, cwd, startedAtMs) {
|
|
822
|
+
try {
|
|
823
|
+
const sub = await (0, subagent_usage_1.readSubagentUsage)(cwd, startedAtMs);
|
|
824
|
+
if (!sub)
|
|
825
|
+
return;
|
|
826
|
+
await emit(requestId, 'subagent_usage', { agents: sub.agents, usage: sub.usage });
|
|
827
|
+
}
|
|
828
|
+
catch (err) {
|
|
829
|
+
console.error(`[session-runner] subagent usage [reqId=${requestId}] failed: ${err.message}`);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
811
832
|
function eventPayload(e) {
|
|
812
833
|
const { type: _type, ...rest } = e;
|
|
813
834
|
return rest;
|
|
@@ -1110,6 +1131,9 @@ async function runClaudeTui(req, cwd, installed) {
|
|
|
1110
1131
|
await new Promise(r => setTimeout(r, 250));
|
|
1111
1132
|
}
|
|
1112
1133
|
}
|
|
1134
|
+
// Floor for "which subagent transcripts belong to this attempt" — see
|
|
1135
|
+
// subagent-usage.ts.
|
|
1136
|
+
const startedAtMs = Date.now();
|
|
1113
1137
|
let spawn;
|
|
1114
1138
|
try {
|
|
1115
1139
|
spawn = (0, claude_spawn_1.spawnClaudeForRuntime)({
|
|
@@ -1509,7 +1533,8 @@ async function runClaudeTui(req, cwd, installed) {
|
|
|
1509
1533
|
// is still non-terminal, reopening the server-side reclaim double-spawn
|
|
1510
1534
|
// window. Resolve only after the chain settles, like the print runners
|
|
1511
1535
|
// which `await finalizeTerminal`.
|
|
1512
|
-
const done = (
|
|
1536
|
+
const done = emitSubagentUsage(req.id, cwd, startedAtMs)
|
|
1537
|
+
.then(() => (0, supabase_client_1.rpc)('runtime_get_request_status', { p_token: token(), p_request_id: req.id }))
|
|
1513
1538
|
.then(currentStatus => {
|
|
1514
1539
|
if (currentStatus === 'canceled')
|
|
1515
1540
|
return;
|
|
@@ -1547,6 +1572,10 @@ async function runClaudePrint(req, cwd, installed) {
|
|
|
1547
1572
|
return;
|
|
1548
1573
|
}
|
|
1549
1574
|
const permissionMode = req.agent === 'claude-bypass' ? 'bypassPermissions' : (req.permission_mode ?? undefined);
|
|
1575
|
+
// Floor for "which subagent transcripts belong to this attempt" — see
|
|
1576
|
+
// subagent-usage.ts. Taken before the spawn so nothing this run writes
|
|
1577
|
+
// can fall outside the window.
|
|
1578
|
+
const startedAtMs = Date.now();
|
|
1550
1579
|
let handle;
|
|
1551
1580
|
try {
|
|
1552
1581
|
handle = (0, claude_print_1.spawnClaudePrint)({
|
|
@@ -1621,6 +1650,8 @@ async function runClaudePrint(req, cwd, installed) {
|
|
|
1621
1650
|
hang.stop();
|
|
1622
1651
|
if (handle.sessionId)
|
|
1623
1652
|
await setStatus(req.id, { spawnedSessionId: handle.sessionId });
|
|
1653
|
+
// The agent has exited, so every subagent transcript it wrote is complete.
|
|
1654
|
+
await emitSubagentUsage(req.id, cwd, startedAtMs);
|
|
1624
1655
|
void emit(req.id, 'session_end', { reason: 'claude_exit', exitCode });
|
|
1625
1656
|
const currentStatus = await (0, supabase_client_1.rpc)('runtime_get_request_status', { p_token: token(), p_request_id: req.id }).catch(() => null);
|
|
1626
1657
|
if (currentStatus !== 'canceled') {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface SubagentUsage {
|
|
2
|
+
/** How many subagent transcripts contributed. */
|
|
3
|
+
agents: number;
|
|
4
|
+
/** Anthropic-shaped so the server normalizes it with every other usage
|
|
5
|
+
* block instead of needing a special case. */
|
|
6
|
+
usage: {
|
|
7
|
+
input_tokens: number;
|
|
8
|
+
output_tokens: number;
|
|
9
|
+
cache_read_input_tokens: number;
|
|
10
|
+
cache_creation_input_tokens: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Everything this run's subagents burned, or null if it spawned none.
|
|
15
|
+
*
|
|
16
|
+
* Null rather than zeros on purpose: "no subagents" and "subagents that cost
|
|
17
|
+
* nothing" are different claims, and only the first one is ever true.
|
|
18
|
+
*/
|
|
19
|
+
export declare function readSubagentUsage(cwd: string | null | undefined, sinceMs: number): Promise<SubagentUsage | null>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What the subagents cost.
|
|
3
|
+
//
|
|
4
|
+
// Claude Code streams the MAIN agent loop to stdout and reports that turn's
|
|
5
|
+
// usage on its `result` line — which is what the daemon forwards and what
|
|
6
|
+
// Entity Studio has always displayed. A Task/Agent subagent is invisible to
|
|
7
|
+
// that stream: it runs inside the parent process, and its transcript is
|
|
8
|
+
// written to a SEPARATE `agent-*.jsonl` beside the session file. Its tokens
|
|
9
|
+
// are never mentioned on stdout, and the tool_result the parent receives
|
|
10
|
+
// carries the subagent's text and nothing else.
|
|
11
|
+
//
|
|
12
|
+
// Measured on a real run before this existed: main loop 3,658,620 tokens,
|
|
13
|
+
// twenty subagents 5,479,638 more. The run was reported at 40% of what it
|
|
14
|
+
// actually cost. This reads the other 60% off the disk the agent just wrote.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.readSubagentUsage = readSubagentUsage;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const readline = __importStar(require("readline"));
|
|
53
|
+
const paths_1 = require("./paths");
|
|
54
|
+
const claude_binary_1 = require("./claude-binary");
|
|
55
|
+
const isAgentTranscript = (name) => /^agent-.+\.jsonl$/i.test(name);
|
|
56
|
+
/**
|
|
57
|
+
* Which subagent transcripts belong to this run.
|
|
58
|
+
*
|
|
59
|
+
* +Ai gives most runs their own scratch directory (~/.ainode/sessions/<id>),
|
|
60
|
+
* and Claude Code names the transcript folder after the working directory —
|
|
61
|
+
* so for those the whole folder is this run's and nothing else's. A run pinned
|
|
62
|
+
* to a project checkout shares its folder with every other run against that
|
|
63
|
+
* project, so there we fall back to "written since this attempt started",
|
|
64
|
+
* which is exact unless two runs are working the same checkout concurrently.
|
|
65
|
+
*/
|
|
66
|
+
function transcriptsFor(cwd, sinceMs) {
|
|
67
|
+
const dir = path.join(paths_1.CLAUDE_PROJECTS_DIR, (0, claude_binary_1.encodeProjectPath)(cwd));
|
|
68
|
+
let names;
|
|
69
|
+
try {
|
|
70
|
+
names = fs.readdirSync(dir);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const exclusive = path.resolve(cwd).startsWith(path.resolve(paths_1.RUNTIME_SESSIONS_DIR) + path.sep);
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
if (!isAgentTranscript(name))
|
|
79
|
+
continue;
|
|
80
|
+
const file = path.join(dir, name);
|
|
81
|
+
if (exclusive) {
|
|
82
|
+
out.push(file);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (fs.statSync(file).mtimeMs >= sinceMs)
|
|
87
|
+
out.push(file);
|
|
88
|
+
}
|
|
89
|
+
catch { /* vanished */ }
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
94
|
+
/**
|
|
95
|
+
* Sum one transcript's usage.
|
|
96
|
+
*
|
|
97
|
+
* Claude Code writes the same message id more than once — the text first, the
|
|
98
|
+
* tool_use block after — and both copies repeat the identical usage. Summing
|
|
99
|
+
* blind therefore roughly doubles the answer, so this dedupes by message id.
|
|
100
|
+
*/
|
|
101
|
+
async function sumTranscript(file, into) {
|
|
102
|
+
let sawAny = false;
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
let stream;
|
|
105
|
+
try {
|
|
106
|
+
stream = fs.createReadStream(file);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
for await (const line of readline.createInterface({ input: stream, crlfDelay: Infinity })) {
|
|
113
|
+
if (!line.trim())
|
|
114
|
+
continue;
|
|
115
|
+
let o;
|
|
116
|
+
try {
|
|
117
|
+
o = JSON.parse(line);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (o.type !== 'assistant')
|
|
123
|
+
continue;
|
|
124
|
+
const message = o.message;
|
|
125
|
+
const u = message?.usage;
|
|
126
|
+
if (!u)
|
|
127
|
+
continue;
|
|
128
|
+
const id = typeof message?.id === 'string' ? message.id : '';
|
|
129
|
+
if (id) {
|
|
130
|
+
if (seen.has(id))
|
|
131
|
+
continue;
|
|
132
|
+
seen.add(id);
|
|
133
|
+
}
|
|
134
|
+
sawAny = true;
|
|
135
|
+
into.usage.input_tokens += num(u.input_tokens);
|
|
136
|
+
into.usage.output_tokens += num(u.output_tokens);
|
|
137
|
+
into.usage.cache_read_input_tokens += num(u.cache_read_input_tokens);
|
|
138
|
+
into.usage.cache_creation_input_tokens += num(u.cache_creation_input_tokens);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// A half-written or unreadable transcript costs us that subagent's
|
|
143
|
+
// tokens, not the run's terminal status.
|
|
144
|
+
}
|
|
145
|
+
return sawAny;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Everything this run's subagents burned, or null if it spawned none.
|
|
149
|
+
*
|
|
150
|
+
* Null rather than zeros on purpose: "no subagents" and "subagents that cost
|
|
151
|
+
* nothing" are different claims, and only the first one is ever true.
|
|
152
|
+
*/
|
|
153
|
+
async function readSubagentUsage(cwd, sinceMs) {
|
|
154
|
+
if (!cwd)
|
|
155
|
+
return null;
|
|
156
|
+
const files = transcriptsFor(cwd, sinceMs);
|
|
157
|
+
if (!files.length)
|
|
158
|
+
return null;
|
|
159
|
+
const total = {
|
|
160
|
+
agents: 0,
|
|
161
|
+
usage: {
|
|
162
|
+
input_tokens: 0, output_tokens: 0,
|
|
163
|
+
cache_read_input_tokens: 0, cache_creation_input_tokens: 0,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
if (await sumTranscript(file, total))
|
|
168
|
+
total.agents++;
|
|
169
|
+
}
|
|
170
|
+
if (!total.agents)
|
|
171
|
+
return null;
|
|
172
|
+
return total;
|
|
173
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,14 @@ export type RuntimeEvent = {
|
|
|
25
25
|
type: 'turn_complete';
|
|
26
26
|
usage?: Record<string, unknown>;
|
|
27
27
|
stopReason?: string;
|
|
28
|
+
}
|
|
29
|
+
/** What this run's subagents cost, read off their own transcripts once the
|
|
30
|
+
* agent has exited. Never reaches stdout, so it is never in turn_complete
|
|
31
|
+
* — see subagent-usage.ts. Emitted at most once per attempt. */
|
|
32
|
+
| {
|
|
33
|
+
type: 'subagent_usage';
|
|
34
|
+
agents: number;
|
|
35
|
+
usage: Record<string, unknown>;
|
|
28
36
|
} | {
|
|
29
37
|
type: 'error';
|
|
30
38
|
code: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|