@runuai/host 0.8.26 → 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.
- package/lib/agents/claude.ts +31 -2
- package/lib/agents/types.ts +21 -2
- package/lib/orchestrator.ts +1 -0
- package/lib/standard-image.ts +28 -6
- package/package.json +1 -1
- package/src/index.ts +22 -11
- package/src/protocol.ts +10 -0
package/lib/agents/claude.ts
CHANGED
|
@@ -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
|
|
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. */
|
package/lib/orchestrator.ts
CHANGED
package/lib/standard-image.ts
CHANGED
|
@@ -305,13 +305,22 @@ async function upgradeVolumeAgentClis(): Promise<void> {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
/** Outcome of ensureStandardImage — `error` carries WHY when not ready, so
|
|
309
|
+
* the taskUp path can surface a build failure to the cloud (not just the host
|
|
310
|
+
* log). Callers that only fire-and-forget at boot ignore the return. */
|
|
311
|
+
export interface StandardImageResult {
|
|
312
|
+
ok: boolean;
|
|
313
|
+
error?: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
308
316
|
/**
|
|
309
317
|
* Ensure the standard image and the shared asdf data volume exist, and that the
|
|
310
318
|
* agent CLIs are present inside the volume. Builds the image only when `docker
|
|
311
|
-
* image inspect` fails
|
|
312
|
-
*
|
|
319
|
+
* image inspect` fails or the content hash is stale. Never throws — returns
|
|
320
|
+
* `{ ok, error? }` so the caller decides (boot: fire-and-forget; taskUp:
|
|
321
|
+
* surface the reason).
|
|
313
322
|
*/
|
|
314
|
-
export async function ensureStandardImage(): Promise<
|
|
323
|
+
export async function ensureStandardImage(): Promise<StandardImageResult> {
|
|
315
324
|
// 1. Shared asdf data volume — idempotent.
|
|
316
325
|
const vol = await run("docker", ["volume", "create", ASDF_DATA_VOLUME]);
|
|
317
326
|
if (vol.code !== 0) {
|
|
@@ -321,7 +330,9 @@ export async function ensureStandardImage(): Promise<void> {
|
|
|
321
330
|
);
|
|
322
331
|
// If docker itself is unavailable, the image step will also fail; bail
|
|
323
332
|
// early so we don't double-log a confusing build error.
|
|
324
|
-
if (vol.code === null)
|
|
333
|
+
if (vol.code === null) {
|
|
334
|
+
return { ok: false, error: "Docker is not available on this host." };
|
|
335
|
+
}
|
|
325
336
|
}
|
|
326
337
|
|
|
327
338
|
// 2. Ensure the image is built AND current. "Present" is not enough — a
|
|
@@ -330,6 +341,7 @@ export async function ensureStandardImage(): Promise<void> {
|
|
|
330
341
|
// landed). The build context is content-hashed into an image label;
|
|
331
342
|
// a mismatch triggers a rebuild (layer cache keeps it cheap).
|
|
332
343
|
let imageReady = false;
|
|
344
|
+
let buildError: string | null = null;
|
|
333
345
|
// Install only the optional engines the operator has configured; the flags
|
|
334
346
|
// are build args AND part of the content hash (so a new login rebuilds).
|
|
335
347
|
const engines = configuredOptionalEngines();
|
|
@@ -356,7 +368,7 @@ export async function ensureStandardImage(): Promise<void> {
|
|
|
356
368
|
"[host-agent] docker unavailable; skipping standard image build. " +
|
|
357
369
|
"Tasks will fail until docker is running.",
|
|
358
370
|
);
|
|
359
|
-
return;
|
|
371
|
+
return { ok: false, error: "Docker is not available on this host." };
|
|
360
372
|
}
|
|
361
373
|
const labeledHash = inspect.code === 0 ? inspect.stdout.trim() : null;
|
|
362
374
|
if (inspect.code === 0 && contextHash !== null && labeledHash === contextHash) {
|
|
@@ -387,10 +399,13 @@ export async function ensureStandardImage(): Promise<void> {
|
|
|
387
399
|
console.log(`[host-agent] built standard image ${STANDARD_IMAGE_TAG}`);
|
|
388
400
|
imageReady = true;
|
|
389
401
|
} else {
|
|
402
|
+
buildError =
|
|
403
|
+
build.stderr.trim() ||
|
|
404
|
+
`docker build exited ${build.code ?? "spawn error"}`;
|
|
390
405
|
console.warn(
|
|
391
406
|
`[host-agent] standard image build failed (exit ${build.code ?? "spawn"}); ` +
|
|
392
407
|
"continuing. Tasks needing the image will surface this error.\n" +
|
|
393
|
-
|
|
408
|
+
buildError,
|
|
394
409
|
);
|
|
395
410
|
// A stale-but-working image is better than none.
|
|
396
411
|
imageReady = labeledHash !== null;
|
|
@@ -402,5 +417,12 @@ export async function ensureStandardImage(): Promise<void> {
|
|
|
402
417
|
if (imageReady) {
|
|
403
418
|
await ensureVolumeAgentClis();
|
|
404
419
|
await upgradeVolumeAgentClis();
|
|
420
|
+
return { ok: true };
|
|
405
421
|
}
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
error:
|
|
425
|
+
buildError ??
|
|
426
|
+
"the standard base image (uai-standard:dev) is not built and no build error was captured.",
|
|
427
|
+
};
|
|
406
428
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { agent, AgentError } from "../lib/agent";
|
|
1
|
+
import { agent, AgentError, toolStderrDetail } from "../lib/agent";
|
|
2
2
|
import { ensureStandardImage } from "../lib/standard-image";
|
|
3
3
|
import { cloneRepo } from "../lib/repo-clone";
|
|
4
4
|
import { handleFilesOp } from "../lib/shared-files";
|
|
@@ -120,16 +120,27 @@ export const hostCommands: HostCommands = {
|
|
|
120
120
|
storeTaskCliSecret(input.task.id, input.task.cliSecret);
|
|
121
121
|
}
|
|
122
122
|
recordHostEvent(input.task.id, "task.created");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
const result = await wrapAgent(ctx, "taskUp", async () => {
|
|
124
|
+
// Self-heal the standard base image before task-up needs it. The
|
|
125
|
+
// boot-time build is best-effort + one-shot: a host that started before
|
|
126
|
+
// Docker was ready (common on Docker Desktop) never built
|
|
127
|
+
// uai-standard:dev, so every task-up died on a doomed registry pull
|
|
128
|
+
// (live 2026-07-22). This is idempotent — a fast inspect when present,
|
|
129
|
+
// a real build only when missing/stale (Docker is definitely up now, a
|
|
130
|
+
// task just arrived). On a build FAILURE, surface WHY to the cloud (not
|
|
131
|
+
// just the host log — the operator can't read a remote user's machine).
|
|
132
|
+
const img = await ensureStandardImage();
|
|
133
|
+
if (!img.ok) {
|
|
134
|
+
throw new AgentError(
|
|
135
|
+
"STANDARD_IMAGE_UNAVAILABLE",
|
|
136
|
+
img.error
|
|
137
|
+
? `could not prepare the host base image uai-standard:dev — the build failed. ${toolStderrDetail(img.error)}`
|
|
138
|
+
: "could not prepare the host base image uai-standard:dev — see the host log.",
|
|
139
|
+
{},
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return agent.taskUp(input);
|
|
143
|
+
});
|
|
133
144
|
if (result.ok) {
|
|
134
145
|
recordTaskUpResult(input.task.id, result.value);
|
|
135
146
|
recordHostEvent(input.task.id, "task.started");
|
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";
|