@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.
- package/lib/agents/claude.ts +5 -2
- package/lib/agents/cursor.ts +11 -1
- package/lib/agents/types.ts +21 -2
- package/lib/agents/usage.ts +37 -0
- package/lib/orchestrator.ts +1 -0
- package/package.json +1 -1
- package/src/protocol.ts +10 -0
package/lib/agents/claude.ts
CHANGED
|
@@ -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
|
|
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));
|
package/lib/agents/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/lib/orchestrator.ts
CHANGED
package/package.json
CHANGED
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";
|