@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.
- package/lib/agents/claude.ts +2 -28
- package/lib/agents/cursor.ts +11 -1
- package/lib/agents/usage.ts +37 -0
- package/package.json +1 -1
package/lib/agents/claude.ts
CHANGED
|
@@ -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 =
|
|
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 [
|
package/lib/agents/cursor.ts
CHANGED
|
@@ -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