@runuai/host 0.8.28 → 0.8.29

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.
@@ -23,12 +23,12 @@
23
23
  import { newId } from "../ulid";
24
24
  import { createAgentTransport, type LineTransport } from "./transport";
25
25
  import { register } from "./registry";
26
+ import { extractResultUsage } from "./usage";
26
27
  import type {
27
28
  AgentEvent,
28
29
  AgentEventHandler,
29
30
  AgentKind,
30
31
  AgentSession,
31
- AgentUsage,
32
32
  RosterAgent,
33
33
  } from "./types";
34
34
 
@@ -60,32 +60,6 @@ function isObj(v: unknown): v is Record<string, unknown> {
60
60
  return typeof v === "object" && v !== null;
61
61
  }
62
62
 
63
- function num(v: unknown): number | undefined {
64
- return typeof v === "number" && Number.isFinite(v) ? v : undefined;
65
- }
66
-
67
- /**
68
- * Token + cost accounting from a Claude `result` line. Claude Code reports the
69
- * exact `total_cost_usd` (no estimation needed) plus a token `usage` breakdown
70
- * and per-model `modelUsage`. The billed model is the single modelUsage key
71
- * (or, on a multi-model turn, joined).
72
- */
73
- function claudeUsage(json: Record<string, unknown>): AgentUsage | undefined {
74
- const u = isObj(json.usage) ? json.usage : {};
75
- const cost = num(json.total_cost_usd);
76
- const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
77
- const usage: AgentUsage = {
78
- model: models.length ? models.join(", ") : undefined,
79
- inputTokens: num(u.input_tokens),
80
- outputTokens: num(u.output_tokens),
81
- cacheReadTokens: num(u.cache_read_input_tokens),
82
- cacheCreateTokens: num(u.cache_creation_input_tokens),
83
- costUsd: cost,
84
- };
85
- // Nothing usable reported → omit rather than send an empty object.
86
- return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
87
- }
88
-
89
63
  /**
90
64
  * Map one stream-json stdout line to zero or more AgentEvents.
91
65
  *
@@ -146,7 +120,7 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
146
120
  // --- turn result ------------------------------------------------------
147
121
  if (type === "result") {
148
122
  const text = typeof json.result === "string" ? json.result : "";
149
- const usage = claudeUsage(json);
123
+ const usage = extractResultUsage(json);
150
124
  if (json.is_error === true) {
151
125
  // An errored turn still cost tokens — meter it.
152
126
  return [
@@ -31,11 +31,13 @@ import { spawn, type ChildProcess } from "node:child_process";
31
31
 
32
32
  import { newId } from "../ulid";
33
33
  import { register } from "./registry";
34
+ import { extractResultUsage } from "./usage";
34
35
  import type {
35
36
  AgentEvent,
36
37
  AgentEventHandler,
37
38
  AgentKind,
38
39
  AgentSession,
40
+ AgentUsage,
39
41
  RosterAgent,
40
42
  } from "./types";
41
43
 
@@ -83,6 +85,9 @@ export interface MappedCursorLine {
83
85
  finalText?: string;
84
86
  sessionId?: string;
85
87
  errorText?: string;
88
+ /** Token/cost from the result envelope — Cursor uses the Claude-compatible
89
+ * shape (usage + total_cost_usd + modelUsage). ADR-071 metering. */
90
+ usage?: AgentUsage;
86
91
  }
87
92
 
88
93
  export function mapCursorLine(line: string): MappedCursorLine {
@@ -131,6 +136,7 @@ export function mapCursorLine(line: string): MappedCursorLine {
131
136
  ? m.result
132
137
  : "cursor turn failed"
133
138
  : undefined,
139
+ usage: extractResultUsage(m),
134
140
  };
135
141
  }
136
142
  // thinking / user / other → nothing.
@@ -235,6 +241,9 @@ export class CursorSession implements AgentSession {
235
241
  let acc = "";
236
242
  let sawText = false;
237
243
  let buf = "";
244
+ // Usage rides the result line but turn_complete fires on process exit;
245
+ // capture it here and attach it below (ADR-071 metering).
246
+ let turnUsage: AgentUsage | undefined;
238
247
  const consume = (chunk: string): void => {
239
248
  buf += chunk;
240
249
  let nl: number;
@@ -279,6 +288,7 @@ export class CursorSession implements AgentSession {
279
288
  });
280
289
  }
281
290
  if (m.end) {
291
+ if (m.usage) turnUsage = m.usage;
282
292
  if (m.errorText) this.emit({ type: "error", message: m.errorText });
283
293
  const finalText = m.finalText ?? acc;
284
294
  if (sawText || finalText) {
@@ -307,7 +317,7 @@ export class CursorSession implements AgentSession {
307
317
  message: `cursor exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`,
308
318
  });
309
319
  }
310
- this.emit({ type: "turn_complete" });
320
+ this.emit({ type: "turn_complete", usage: turnUsage });
311
321
  resolve();
312
322
  };
313
323
  child.on("exit", (code) => finish(code));
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared token/cost extraction for the Claude-Code-compatible stream-json
3
+ * `result` envelope (ADR-071 metering). Claude Code AND Cursor Agent both emit
4
+ * this exact shape — `usage` token breakdown, an exact `total_cost_usd`, and
5
+ * per-model `modelUsage` — so both engines share this one extractor.
6
+ *
7
+ * Codex (app-server protocol) and Kimi/Grok (plain text streams) report no
8
+ * token/cost data, so they have no extractor.
9
+ */
10
+
11
+ import type { AgentUsage } from "./types";
12
+
13
+ function isObj(v: unknown): v is Record<string, unknown> {
14
+ return typeof v === "object" && v !== null;
15
+ }
16
+
17
+ function num(v: unknown): number | undefined {
18
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
19
+ }
20
+
21
+ /** Pull AgentUsage from a `result` line, or undefined when none is reported. */
22
+ export function extractResultUsage(
23
+ json: Record<string, unknown>,
24
+ ): AgentUsage | undefined {
25
+ const u = isObj(json.usage) ? json.usage : {};
26
+ const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
27
+ const usage: AgentUsage = {
28
+ model: models.length ? models.join(", ") : undefined,
29
+ inputTokens: num(u.input_tokens),
30
+ outputTokens: num(u.output_tokens),
31
+ cacheReadTokens: num(u.cache_read_input_tokens),
32
+ cacheCreateTokens: num(u.cache_creation_input_tokens),
33
+ costUsd: num(json.total_cost_usd),
34
+ };
35
+ // Nothing usable reported → omit rather than send an empty object.
36
+ return Object.values(usage).some((v) => v !== undefined) ? usage : undefined;
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.8.28",
3
+ "version": "0.8.29",
4
4
  "description": "Uai host — runs ephemeral AI coding tasks in Docker on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Diogo Perillo <diogo.perillo@gmail.com>",