@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.
@@ -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 = (0, supabase_client_1.rpc)('runtime_get_request_status', { p_token: token(), p_request_id: req.id })
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.25.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": [