@runuai/host 0.8.27 → 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,6 +23,7 @@
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,
@@ -119,15 +120,17 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
119
120
  // --- turn result ------------------------------------------------------
120
121
  if (type === "result") {
121
122
  const text = typeof json.result === "string" ? json.result : "";
123
+ const usage = extractResultUsage(json);
122
124
  if (json.is_error === true) {
125
+ // An errored turn still cost tokens — meter it.
123
126
  return [
124
127
  { type: "error", message: text || "claude returned an error" },
125
- { type: "turn_complete" },
128
+ { type: "turn_complete", usage },
126
129
  ];
127
130
  }
128
131
  return [
129
132
  { type: "message_complete", text },
130
- { type: "turn_complete" },
133
+ { type: "turn_complete", usage },
131
134
  ];
132
135
  }
133
136
 
@@ -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));
@@ -82,6 +82,24 @@ export function parseRoster(raw: string): Roster {
82
82
  // these to `uai_messages` and streams them to the browser.
83
83
  // ---------------------------------------------------------------------------
84
84
 
85
+ /**
86
+ * Token + cost accounting for one turn, extracted from the agent CLI's result
87
+ * envelope where it reports it (Claude Code gives `usage` + `total_cost_usd`
88
+ * directly). Powers per-task cost visibility for self-hosted users and the
89
+ * metering pipeline for managed Uai-provided AI. All fields optional — an
90
+ * engine that doesn't report a dimension omits it.
91
+ */
92
+ export interface AgentUsage {
93
+ /** Model the CLI actually billed (may differ from the configured model, e.g. a sub-agent). */
94
+ model?: string;
95
+ inputTokens?: number;
96
+ outputTokens?: number;
97
+ cacheReadTokens?: number;
98
+ cacheCreateTokens?: number;
99
+ /** Total USD for the turn, as reported by the CLI (authoritative when present). */
100
+ costUsd?: number;
101
+ }
102
+
85
103
  export type AgentEvent =
86
104
  /** A chunk of streaming assistant text. Appended to the in-progress message. */
87
105
  | { type: "message_delta"; text: string }
@@ -93,8 +111,9 @@ export type AgentEvent =
93
111
  | { type: "permission_request"; id: string; title: string; detail: string }
94
112
  /** The agent addressed another agent — uai routes this as a peer message. */
95
113
  | { type: "peer_message"; toAgentId: string; text: string }
96
- /** The turn (one request → response cycle) is done; agent is idle. */
97
- | { type: "turn_complete" }
114
+ /** The turn (one request → response cycle) is done; agent is idle.
115
+ * `usage` carries this turn's token/cost accounting when the CLI reports it. */
116
+ | { type: "turn_complete"; usage?: AgentUsage }
98
117
  /** A recoverable error surfaced by the agent. */
99
118
  | { type: "error"; message: string }
100
119
  /** The underlying process exited. */
@@ -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
+ }
@@ -715,6 +715,7 @@ class Orchestrator {
715
715
  taskId: channel.taskId,
716
716
  agentId,
717
717
  aborted,
718
+ usage: event.usage,
718
719
  });
719
720
  break;
720
721
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.8.27",
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>",
package/src/protocol.ts CHANGED
@@ -580,6 +580,16 @@ export type HostEvent =
580
580
  taskId: string;
581
581
  agentId: string;
582
582
  aborted?: boolean;
583
+ /** Token/cost accounting for the turn, when the engine reports it
584
+ * (ADR-071 usage metering — powers per-task cost + managed AI billing). */
585
+ usage?: {
586
+ model?: string;
587
+ inputTokens?: number;
588
+ outputTokens?: number;
589
+ cacheReadTokens?: number;
590
+ cacheCreateTokens?: number;
591
+ costUsd?: number;
592
+ };
583
593
  }
584
594
  | {
585
595
  kind: "agent.tool_call";