@runuai/host 0.9.164 → 0.9.166

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.
@@ -34,7 +34,7 @@ ENV ASDF_VERSION=v0.14.1
34
34
  # freshness is requalified before any of them moves.
35
35
  ENV UAI_CODEX_CLI_VERSION=0.157.0
36
36
  ENV UAI_CURSOR_AGENT_VERSION=2026.08.25-3e8eec8
37
- ENV UAI_GROK_CLI_VERSION=1.0.5
37
+ ENV UAI_GROK_CLI_VERSION=1.0.67
38
38
 
39
39
  # ---------------------------------------------------------------------------
40
40
  # 1. System packages.
package/lib/agent-cli.ts CHANGED
@@ -322,6 +322,29 @@ async function main() {
322
322
  const launch = (a) => a.kind + (a.model ? "/" + a.model : "") + (a.effort ? "/" + a.effort : "");
323
323
  if (action === "list") { out(await api("GET", "/api/agent/crew")); return; }
324
324
  if (action === "status") { out(await api("GET", "/api/agent/crew-status")); return; }
325
+ if (action === "store") { out(await api("GET", "/api/agent/crew-store")); return; }
326
+ if (action === "suggest") {
327
+ // The Astronaut suggests a persona; a person creates it (ADR-157 addendum).
328
+ // From the Store (--template) or from scratch (--name and --brief).
329
+ if (!flags.why || !(flags.template || flags.name)) {
330
+ console.error('uai: crew suggest needs --template <slug> (see crew store), or --name and --brief for a new one, and --why "<reason>"');
331
+ process.exit(1);
332
+ }
333
+ // --skill repeats, and parseFlags keeps only the last, so read them off the raw argv.
334
+ const skills = [];
335
+ for (let i = 0; i < rest.length; i++) {
336
+ if (rest[i] === "--skill" && rest[i + 1]) skills.push(...rest[++i].split(",").filter(Boolean));
337
+ }
338
+ const body = { why: flags.why };
339
+ if (flags.template) body.template = flags.template;
340
+ for (const k of ["name", "role", "summary", "brief", "model", "effort"]) if (flags[k]) body[k] = flags[k];
341
+ if (flags.handle) body.handle = handleOf(flags.handle);
342
+ if (flags.engine) body.engine = flags.engine;
343
+ if (skills.length) body.skills = skills;
344
+ const r = await api("POST", "/api/agent/crew-suggest", body);
345
+ out("suggested " + r.suggestion.name + " (@" + r.suggestion.handle + "). " + r.message);
346
+ return;
347
+ }
325
348
  if (action === "add") {
326
349
  const personaId = pos[0];
327
350
  if (!personaId || !flags.why) {
@@ -367,7 +390,7 @@ async function main() {
367
390
  out("removed @" + id);
368
391
  return;
369
392
  }
370
- console.error("uai: crew commands are list, status, add, model and remove (Astronaut only)");
393
+ console.error("uai: crew commands are list, status, store, suggest, add, model and remove (Astronaut only)");
371
394
  process.exit(1);
372
395
  }
373
396
  const key = group + " " + (action || "");
@@ -572,8 +595,11 @@ async function main() {
572
595
  " uai task history (list your past tasks)",
573
596
  " uai task read <taskId> (full chat of a past task you were on)",
574
597
  ' uai dispatch @agent [@agent…] "instruction" (Secretary only)',
575
- " uai crew list (Astronaut only: personas, engines, models, limits, roster)",
598
+ " uai crew list (Astronaut only: personas with summary, skills and permissions, engines, models, limits, roster)",
576
599
  " uai crew status (Astronaut only: each agent's engine, model, account; account usage; credits)",
600
+ " uai crew store (Astronaut only: the Store's persona and skill templates, names and types only)",
601
+ ' uai crew suggest --template <slug> --why "<reason>" (Astronaut only: propose a persona from the Store; a person creates it)',
602
+ ' uai crew suggest --name <n> --brief <text> [--role <r>] [--summary <s>] [--handle <h>] [--engine <kind>] [--model <m>] [--effort <e>] [--skill <slug|id>]... --why "<reason>"',
577
603
  ' uai crew add <personaId> [--as <handle>] [--engine <kind>] [--model <m>] [--effort <e>] [--account <id|label|default>] --why "<reason>"',
578
604
  ' uai crew model @agent [--engine <kind>] [--model <m|default>] [--effort <e|default>] [--account <id|label|default>] --why "<reason>"',
579
605
  ' uai crew remove @agent --why "<reason>"',
@@ -19,6 +19,7 @@
19
19
  * item/agentMessage/delta → message_delta
20
20
  * item/completed (agentMessage) → message_complete
21
21
  * item/completed (tool item) → tool_call
22
+ * thread/tokenUsage/updated → accumulated for its turn
22
23
  * turn/completed → turn_complete (+ error if failed)
23
24
  * error → error
24
25
  *
@@ -49,6 +50,7 @@ import type {
49
50
  AgentEventHandler,
50
51
  AgentKind,
51
52
  AgentSession,
53
+ AgentUsage,
52
54
  RosterAgent,
53
55
  } from "./types";
54
56
 
@@ -87,6 +89,120 @@ function str(v: unknown, fallback = ""): string {
87
89
  return typeof v === "string" ? v : fallback;
88
90
  }
89
91
 
92
+ function nonNegativeInt(v: unknown): number | undefined {
93
+ return typeof v === "number" &&
94
+ Number.isSafeInteger(v) &&
95
+ v >= 0
96
+ ? v
97
+ : undefined;
98
+ }
99
+
100
+ interface CodexTokenBreakdown {
101
+ inputTokens: number;
102
+ outputTokens: number;
103
+ cachedInputTokens: number;
104
+ }
105
+
106
+ function codexTokenBreakdown(value: unknown): CodexTokenBreakdown | null {
107
+ if (!isObj(value)) return null;
108
+ const inputTokens = nonNegativeInt(value.inputTokens);
109
+ const outputTokens = nonNegativeInt(value.outputTokens);
110
+ const cachedInputTokens = nonNegativeInt(value.cachedInputTokens);
111
+ if (
112
+ inputTokens === undefined ||
113
+ outputTokens === undefined ||
114
+ cachedInputTokens === undefined
115
+ ) {
116
+ return null;
117
+ }
118
+ return {
119
+ inputTokens,
120
+ outputTokens,
121
+ cachedInputTokens,
122
+ };
123
+ }
124
+
125
+ function addBreakdowns(
126
+ left: CodexTokenBreakdown | undefined,
127
+ right: CodexTokenBreakdown,
128
+ ): CodexTokenBreakdown {
129
+ return {
130
+ inputTokens: (left?.inputTokens ?? 0) + right.inputTokens,
131
+ outputTokens: (left?.outputTokens ?? 0) + right.outputTokens,
132
+ cachedInputTokens:
133
+ (left?.cachedInputTokens ?? 0) + right.cachedInputTokens,
134
+ };
135
+ }
136
+
137
+ function breakdownDelta(
138
+ current: CodexTokenBreakdown,
139
+ previous: CodexTokenBreakdown,
140
+ ): CodexTokenBreakdown | null {
141
+ const delta = {
142
+ inputTokens: current.inputTokens - previous.inputTokens,
143
+ outputTokens: current.outputTokens - previous.outputTokens,
144
+ cachedInputTokens:
145
+ current.cachedInputTokens - previous.cachedInputTokens,
146
+ };
147
+ return Object.values(delta).every((value) => value >= 0) ? delta : null;
148
+ }
149
+
150
+ /**
151
+ * Codex 0.157 emits one token update after every model call. `last` is that
152
+ * call, not the whole turn; `total` is the running thread total. A tool-using
153
+ * turn therefore emits several updates. Seed an unseen thread from `last`,
154
+ * then add positive `total` deltas so repeated snapshots add zero. Usage is
155
+ * kept by turn id and consumed exactly once at its turn/completed boundary.
156
+ */
157
+ class CodexTurnUsage {
158
+ private readonly threadTotals = new Map<string, CodexTokenBreakdown>();
159
+ private readonly turns = new Map<string, CodexTokenBreakdown>();
160
+
161
+ observe(params: unknown): void {
162
+ if (!isObj(params)) return;
163
+ const threadId = str(params.threadId);
164
+ const turnId = str(params.turnId);
165
+ const tokenUsage = isObj(params.tokenUsage) ? params.tokenUsage : null;
166
+ if (!threadId || !turnId || !tokenUsage) return;
167
+ const last = codexTokenBreakdown(tokenUsage.last);
168
+ const total = codexTokenBreakdown(tokenUsage.total);
169
+ if (!last || !total) return;
170
+
171
+ const previousTotal = this.threadTotals.get(threadId);
172
+ const contribution = previousTotal
173
+ ? breakdownDelta(total, previousTotal) ?? last
174
+ : last;
175
+ this.threadTotals.set(threadId, total);
176
+ this.turns.set(
177
+ turnId,
178
+ addBreakdowns(this.turns.get(turnId), contribution),
179
+ );
180
+ }
181
+
182
+ complete(params: unknown, model: string | undefined): AgentUsage | undefined {
183
+ const p = isObj(params) ? params : {};
184
+ const turn = isObj(p.turn) ? p.turn : {};
185
+ const turnId = str(turn.id);
186
+ if (!turnId) return undefined;
187
+ const usage = this.turns.get(turnId);
188
+ this.turns.delete(turnId);
189
+ if (!usage) return undefined;
190
+ return {
191
+ ...(model ? { model } : {}),
192
+ // OpenAI includes cached input inside inputTokens. Uai's shared shape,
193
+ // like Claude's, keeps fresh input and cache reads in separate columns.
194
+ // Codex 0.157 does not say whether cacheWriteInputTokens is already in
195
+ // inputTokens. Leave it in fresh input and do not also report a cache
196
+ // create until the protocol makes that relationship explicit.
197
+ inputTokens: Math.max(0, usage.inputTokens - usage.cachedInputTokens),
198
+ cacheReadTokens: usage.cachedInputTokens,
199
+ // reasoningOutputTokens is a subset of outputTokens in this protocol
200
+ // (totalTokens = inputTokens + outputTokens), so do not add it again.
201
+ outputTokens: usage.outputTokens,
202
+ };
203
+ }
204
+ }
205
+
90
206
  /** `item/completed` item types rendered as tool-call cards. */
91
207
  const TOOL_ITEM_TYPES = new Set([
92
208
  "commandExecution",
@@ -96,6 +212,10 @@ const TOOL_ITEM_TYPES = new Set([
96
212
  "webSearch",
97
213
  ]);
98
214
 
215
+ function turnComplete(usage: AgentUsage | undefined): AgentEvent {
216
+ return usage ? { type: "turn_complete", usage } : { type: "turn_complete" };
217
+ }
218
+
99
219
  /**
100
220
  * Map one Codex JSON-RPC *notification* (method + params) to AgentEvents.
101
221
  * Pure and total — an unrecognised method yields [].
@@ -104,6 +224,7 @@ export function mapCodexNotification(
104
224
  method: string,
105
225
  params: unknown,
106
226
  now: number = Date.now(),
227
+ turnUsage?: AgentUsage,
107
228
  ): AgentEvent[] {
108
229
  const p = isObj(params) ? params : {};
109
230
 
@@ -155,10 +276,10 @@ export function mapCodexNotification(
155
276
  : { resetsAt: accountUsage.resetsAt }),
156
277
  }
157
278
  : { type: "error", message },
158
- { type: "turn_complete" },
279
+ turnComplete(turnUsage),
159
280
  ];
160
281
  }
161
- return [{ type: "turn_complete" }];
282
+ return [turnComplete(turnUsage)];
162
283
  }
163
284
 
164
285
  if (method === "error") {
@@ -468,6 +589,8 @@ export class CodexSession implements AgentSession {
468
589
  private turnRunning = false;
469
590
  private lastRateLimitsReadAt = 0;
470
591
  private rateLimitsTimer: ReturnType<typeof setTimeout> | null = null;
592
+ private readonly turnUsage = new CodexTurnUsage();
593
+ private actualModel: string | undefined;
471
594
 
472
595
  constructor(args: {
473
596
  taskId: string;
@@ -637,6 +760,13 @@ export class CodexSession implements AgentSession {
637
760
  // Notification (method, no id).
638
761
  if ("method" in msg) {
639
762
  const method = str(msg.method);
763
+ if (method === "thread/tokenUsage/updated") {
764
+ this.turnUsage.observe(msg.params);
765
+ }
766
+ const completedUsage =
767
+ method === "turn/completed"
768
+ ? this.turnUsage.complete(msg.params, this.actualModel)
769
+ : undefined;
640
770
  if (method === "turn/completed") {
641
771
  this.turnRunning = false;
642
772
  this.clearRateLimitsTimer();
@@ -649,9 +779,17 @@ export class CodexSession implements AgentSession {
649
779
  if (isObj(thread) && typeof thread.id === "string") {
650
780
  this.threadId = thread.id;
651
781
  this.persistThreadId(thread.id);
782
+ if (typeof thread.model === "string" && thread.model.length > 0) {
783
+ this.actualModel = thread.model;
784
+ }
652
785
  }
653
786
  }
654
- for (const ev of mapCodexNotification(method, msg.params)) {
787
+ for (const ev of mapCodexNotification(
788
+ method,
789
+ msg.params,
790
+ Date.now(),
791
+ completedUsage,
792
+ )) {
655
793
  this.emit(ev);
656
794
  }
657
795
  }
@@ -818,8 +956,14 @@ export class CodexSession implements AgentSession {
818
956
  threadParams.developerInstructions = this.systemPreamble;
819
957
  }
820
958
  const result = await this.request("thread/start", threadParams);
959
+ if (isObj(result) && typeof result.model === "string") {
960
+ this.actualModel = result.model;
961
+ }
821
962
  if (isObj(result) && isObj(result.thread)) {
822
963
  const id = str(result.thread.id) || this.threadId;
964
+ if (!this.actualModel && typeof result.thread.model === "string") {
965
+ this.actualModel = result.thread.model;
966
+ }
823
967
  this.threadId = id;
824
968
  if (id) this.persistThreadId(id);
825
969
  }
@@ -846,6 +990,15 @@ export class CodexSession implements AgentSession {
846
990
  isObj(result) && isObj(result.thread) && str(result.thread.id)
847
991
  ? str(result.thread.id)
848
992
  : threadId;
993
+ if (isObj(result) && typeof result.model === "string") {
994
+ this.actualModel = result.model;
995
+ } else if (
996
+ isObj(result) &&
997
+ isObj(result.thread) &&
998
+ typeof result.thread.model === "string"
999
+ ) {
1000
+ this.actualModel = result.thread.model;
1001
+ }
849
1002
  this.threadId = id;
850
1003
  this.persistThreadId(id);
851
1004
  return true;
@@ -15,11 +15,13 @@
15
15
  *
16
16
  * Stream-json vocabulary (`--output-format streaming-json`, verified):
17
17
  * {"type":"thought","data":"…"} → skipped (internal reasoning)
18
- * {"type":"text","data":"…"} → message_delta (streamed)
19
- * {"type":"end","stopReason","sessionId",…} → message_complete + turn end
20
- * Tool calls happen (num_turns>1) but aren't surfaced as events in headless
21
- * mode, so there are no tool cards — the agent works, you get the streamed
22
- * answer.
18
+ * {"type":"text","data":"…"} → message_delta (streamed)
19
+ * {"type":"tool_call"|"tool_call_update",…} → safely skipped (no cards yet)
20
+ * {"type":"usage",…} → skipped (per model call)
21
+ * {"type":"available_commands",…} → skipped
22
+ * {"type":"end","stopReason","sessionId",…} → message_complete + turn usage/end
23
+ * Grok 1.0.67 surfaces tool calls, but tool cards are deliberately outside
24
+ * this adapter slice. The final `end.usage` aggregates the invocation.
23
25
  *
24
26
  * AUTH is the operator's Grok subscription: `~/.grok/{auth.json, config.toml}`
25
27
  * is docker-cp'd into the container at task-up (the Codex/Kimi config-dir
@@ -49,6 +51,7 @@ import type {
49
51
  AgentEventHandler,
50
52
  AgentKind,
51
53
  AgentSession,
54
+ AgentUsage,
52
55
  RosterAgent,
53
56
  } from "./types";
54
57
 
@@ -77,9 +80,68 @@ export interface MappedGrokLine {
77
80
  end?: boolean;
78
81
  /** The session id from the end event, for `-r` on the next turn. */
79
82
  sessionId?: string;
83
+ /** Per-invocation usage from the final end event. */
84
+ usage?: AgentUsage;
80
85
  }
81
86
 
82
- export function mapGrokLine(line: string): MappedGrokLine {
87
+ function nonNegativeInt(value: unknown): number | undefined {
88
+ return typeof value === "number" &&
89
+ Number.isSafeInteger(value) &&
90
+ value >= 0
91
+ ? value
92
+ : undefined;
93
+ }
94
+
95
+ function grokEndUsage(
96
+ msg: Record<string, unknown>,
97
+ model: string | undefined,
98
+ ): AgentUsage | undefined {
99
+ if (typeof msg.usage !== "object" || msg.usage === null) return undefined;
100
+ const raw = msg.usage as Record<string, unknown>;
101
+ const inputTokens = nonNegativeInt(raw.input_tokens);
102
+ const cacheReadTokens = nonNegativeInt(raw.cache_read_input_tokens);
103
+ const cacheCreateTokens = nonNegativeInt(raw.cache_creation_input_tokens);
104
+ const outputTokens = nonNegativeInt(raw.output_tokens);
105
+ const reasoningTokens = nonNegativeInt(raw.reasoning_tokens);
106
+ if (
107
+ inputTokens === undefined ||
108
+ cacheReadTokens === undefined ||
109
+ cacheCreateTokens === undefined ||
110
+ outputTokens === undefined ||
111
+ reasoningTokens === undefined
112
+ ) {
113
+ return undefined;
114
+ }
115
+ const reportedCost = msg.total_cost_usd;
116
+ const costUsd =
117
+ typeof reportedCost === "number" &&
118
+ Number.isFinite(reportedCost) &&
119
+ reportedCost >= 0
120
+ ? reportedCost
121
+ : undefined;
122
+ return {
123
+ // `modelUsage` exposes the backend build id (for example
124
+ // grok-4.5-build), while `model` is the semantic id passed to `-m` and
125
+ // therefore the id Uai's model guide resolves.
126
+ ...(model ? { model } : {}),
127
+ // Headless input_tokens is already uncached; both cache buckets are
128
+ // additive according to the 1.0.67 headless contract.
129
+ inputTokens,
130
+ cacheReadTokens,
131
+ cacheCreateTokens,
132
+ // The documented total excludes reasoning_tokens even though it includes
133
+ // output_tokens. Uai has no separate reasoning column, so include that
134
+ // distinct billed output once here.
135
+ outputTokens: outputTokens + reasoningTokens,
136
+ // Missing cost means unreported/partial, never free.
137
+ ...(costUsd === undefined ? {} : { costUsd }),
138
+ };
139
+ }
140
+
141
+ export function mapGrokLine(
142
+ line: string,
143
+ model?: string,
144
+ ): MappedGrokLine {
83
145
  const trimmed = line.trim();
84
146
  if (!trimmed.startsWith("{")) return {};
85
147
  let msg: Record<string, unknown>;
@@ -92,9 +154,11 @@ export function mapGrokLine(line: string): MappedGrokLine {
92
154
  return { textDelta: msg.data };
93
155
  }
94
156
  if (msg.type === "end") {
157
+ const usage = grokEndUsage(msg, model);
95
158
  return {
96
159
  end: true,
97
160
  sessionId: typeof msg.sessionId === "string" ? msg.sessionId : undefined,
161
+ ...(usage ? { usage } : {}),
98
162
  };
99
163
  }
100
164
  // "thought" (reasoning) and any other types: no user-facing event.
@@ -196,9 +260,10 @@ export class GrokSession implements AgentSession {
196
260
  // the same reasoning as --permission-mode bypassPermissions above
197
261
  // (live 2026-09-08, first Grok agents with MCP servers).
198
262
  GROK_FOLDER_TRUST: "0",
199
- // grok 1.0.5 otherwise captures a login-shell environment once and
200
- // reuses that static state for LocalNonPersistent commands. Loader
201
- // tasks refresh per command, so disable that stale snapshot path.
263
+ // Grok 1.0.67 still enables login-shell capture by default and honors
264
+ // GROK_LOGIN_ENV. That capture reuses static state for
265
+ // LocalNonPersistent commands; loader tasks refresh per command, so
266
+ // keep the stale snapshot path disabled.
202
267
  ...(this.environment.descriptor.envMode === "loader"
203
268
  ? { GROK_LOGIN_ENV: GROK_LOADER_LOGIN_ENV }
204
269
  : {}),
@@ -215,6 +280,7 @@ export class GrokSession implements AgentSession {
215
280
 
216
281
  let acc = "";
217
282
  let sawText = false;
283
+ let turnUsage: AgentUsage | undefined;
218
284
  let buf = "";
219
285
  const consume = (chunk: string): void => {
220
286
  buf += chunk;
@@ -222,8 +288,9 @@ export class GrokSession implements AgentSession {
222
288
  while ((nl = buf.indexOf("\n")) >= 0) {
223
289
  const line = buf.slice(0, nl);
224
290
  buf = buf.slice(nl + 1);
225
- const m = mapGrokLine(line);
291
+ const m = mapGrokLine(line, this.model);
226
292
  if (m.sessionId) this.sessionId = m.sessionId;
293
+ if (m.end) turnUsage = m.usage;
227
294
  if (typeof m.textDelta === "string") {
228
295
  sawText = true;
229
296
  acc += m.textDelta;
@@ -253,7 +320,11 @@ export class GrokSession implements AgentSession {
253
320
  }
254
321
  // Finalize the streamed message (no-op text if the turn produced none).
255
322
  if (sawText) this.emit({ type: "message_complete", text: acc });
256
- this.emit({ type: "turn_complete" });
323
+ this.emit(
324
+ turnUsage
325
+ ? { type: "turn_complete", usage: turnUsage }
326
+ : { type: "turn_complete" },
327
+ );
257
328
  resolve();
258
329
  };
259
330
  child.on("exit", (code) => finish(code));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.164",
3
+ "version": "0.9.166",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",