@addai/node 0.25.0 → 0.26.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/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/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.26.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": [
|