@runuai/host 0.8.27 → 0.8.28

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.
@@ -28,6 +28,7 @@ import type {
28
28
  AgentEventHandler,
29
29
  AgentKind,
30
30
  AgentSession,
31
+ AgentUsage,
31
32
  RosterAgent,
32
33
  } from "./types";
33
34
 
@@ -59,6 +60,32 @@ function isObj(v: unknown): v is Record<string, unknown> {
59
60
  return typeof v === "object" && v !== null;
60
61
  }
61
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
+
62
89
  /**
63
90
  * Map one stream-json stdout line to zero or more AgentEvents.
64
91
  *
@@ -119,15 +146,17 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
119
146
  // --- turn result ------------------------------------------------------
120
147
  if (type === "result") {
121
148
  const text = typeof json.result === "string" ? json.result : "";
149
+ const usage = claudeUsage(json);
122
150
  if (json.is_error === true) {
151
+ // An errored turn still cost tokens — meter it.
123
152
  return [
124
153
  { type: "error", message: text || "claude returned an error" },
125
- { type: "turn_complete" },
154
+ { type: "turn_complete", usage },
126
155
  ];
127
156
  }
128
157
  return [
129
158
  { type: "message_complete", text },
130
- { type: "turn_complete" },
159
+ { type: "turn_complete", usage },
131
160
  ];
132
161
  }
133
162
 
@@ -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. */
@@ -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.28",
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";