pi-ultracode 0.1.1 → 0.1.2

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.
@@ -9,6 +9,8 @@
9
9
  * allowlist), and an alternate cwd for git-worktree isolation.
10
10
  */
11
11
 
12
+ import * as path from "node:path";
13
+ import * as PiCodingAgent from "@earendil-works/pi-coding-agent";
12
14
  import {
13
15
  createAgentSession,
14
16
  createCodingTools,
@@ -16,6 +18,7 @@ import {
16
18
  SessionManager,
17
19
  SettingsManager,
18
20
  VERSION as PI_VERSION,
21
+ type CreateAgentSessionOptions,
19
22
  type ToolDefinition,
20
23
  } from "@earendil-works/pi-coding-agent";
21
24
  import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai";
@@ -27,6 +30,7 @@ import {
27
30
  redactCommand,
28
31
  safeCommandPreview,
29
32
  safeDisplayText,
33
+ safeTranscriptText,
30
34
  truncateDisplay,
31
35
  } from "./display-text.ts";
32
36
  import { jsonSchemaToTypeBox } from "./json-schema.ts";
@@ -52,22 +56,98 @@ export interface ModelLike {
52
56
 
53
57
  export interface ModelRegistryLike {
54
58
  getAvailable(): ModelLike[];
59
+ /** Legacy public projection retained for consumers of this structural type. */
55
60
  getAll?(): ModelLike[];
61
+ getRegisteredProviderIds?(): readonly string[];
62
+ getRegisteredProviderConfig?(provider: string): unknown;
63
+ getProviderAuthStatus?(provider: string): {
64
+ configured: boolean;
65
+ source?: string;
66
+ };
67
+ getApiKeyForProvider?(provider: string): Promise<string | undefined>;
68
+ }
69
+
70
+ /** Structural ModelRuntime seam that remains loadable on pre-0.80.8 Pi. */
71
+ export interface ModelRuntimeLike {
72
+ getModel?(provider: string, modelId: string): ModelLike | undefined;
73
+ registerProvider?(provider: string, config: any): void;
74
+ refresh?(options?: { allowNetwork?: boolean }): Promise<unknown>;
75
+ setRuntimeApiKey?(provider: string, apiKey: string): Promise<void>;
76
+ }
77
+
78
+ export interface ModelRuntimeCreateOptions {
79
+ authPath: string;
80
+ modelsPath: string;
81
+ /** Child sessions reuse the parent's catalog snapshot and must not refresh remotely. */
82
+ allowModelNetwork: false;
83
+ }
84
+
85
+ export type ModelRuntimeFactory = (
86
+ options: ModelRuntimeCreateOptions,
87
+ ) => Promise<ModelRuntimeLike | undefined>;
88
+
89
+ export interface AgentTurnUsage {
90
+ inputTokens: number;
91
+ outputTokens: number;
92
+ cacheReadTokens: number;
93
+ cacheWriteTokens: number;
94
+ totalTokens: number;
95
+ cost: number;
56
96
  }
57
97
 
58
98
  export interface AgentUsage {
99
+ /** Optional on injected legacy runners; production runners always provide it. */
100
+ inputTokens?: number;
59
101
  outputTokens: number;
102
+ /** Compact token use: input + output, excluding cache traffic. */
60
103
  totalTokens: number;
104
+ cacheReadTokens?: number;
105
+ cacheWriteTokens?: number;
61
106
  cost: number;
107
+ /** Completed assistant messages. */
108
+ turns?: number;
109
+ /** Tool executions that reached tool_execution_start. */
110
+ toolUses?: number;
111
+ retries?: number;
112
+ compactions?: number;
62
113
  }
63
114
 
64
115
  export interface AgentRunResult {
65
116
  value: unknown;
66
117
  usage: AgentUsage;
118
+ /** Model id and effort actually applied by the child session. */
119
+ modelId?: string;
120
+ effort?: ThinkingLevel;
67
121
  /** cwd the agent actually ran in (differs from the shared cwd under worktree isolation). */
68
122
  cwd: string;
69
123
  }
70
124
 
125
+ /** Detailed child-session events consumed by the private transcript store. */
126
+ export type AgentTelemetryEvent =
127
+ | { kind: "model_requested"; modelId?: string; effort?: ThinkingLevel }
128
+ | { kind: "model_resolved"; modelId?: string; effort: ThinkingLevel }
129
+ | { kind: "turn_start"; turnIndex?: number }
130
+ | { kind: "text_delta"; delta: string }
131
+ | { kind: "message_end"; text: string; usage?: AgentTurnUsage; error?: string }
132
+ | { kind: "thinking_start" | "thinking_end" }
133
+ | { kind: "tool_start"; toolCallId: string; toolName: string; toolArgs?: string }
134
+ | {
135
+ kind: "tool_end";
136
+ toolCallId: string;
137
+ toolName: string;
138
+ isError: boolean;
139
+ resultPreview?: string;
140
+ }
141
+ | { kind: "retry"; state: "start" | "end"; detail: string }
142
+ | { kind: "compaction"; state: "start" | "end"; detail: string }
143
+ | {
144
+ kind: "run_error";
145
+ error: string;
146
+ usage: AgentUsage;
147
+ modelId?: string;
148
+ effort?: ThinkingLevel;
149
+ };
150
+
71
151
  export interface AgentSessionLike {
72
152
  thinkingLevel: ThinkingLevel;
73
153
  model?: ModelLike;
@@ -83,19 +163,35 @@ export interface AgentSessionLike {
83
163
  getSessionStats?(): unknown;
84
164
  }
85
165
 
166
+ export type AgentSessionCreateOptions = Omit<
167
+ CreateAgentSessionOptions,
168
+ "model" | "modelRegistry" | "modelRuntime"
169
+ > & {
170
+ model?: ModelLike;
171
+ /** Legacy Pi option, selected only when ModelRuntime is unavailable. */
172
+ modelRegistry?: ModelRegistryLike;
173
+ /** Pi 0.80.8+ canonical model/auth runtime. */
174
+ modelRuntime?: ModelRuntimeLike;
175
+ };
176
+
86
177
  export type AgentSessionFactory = (
87
- options: Record<string, unknown>,
178
+ options: AgentSessionCreateOptions,
88
179
  ) => Promise<{ session: AgentSessionLike }>;
89
180
 
90
181
  export interface WorkflowAgentRunnerOptions {
91
182
  cwd: string;
183
+ /** Synchronous extension facade used only for model selection and state replay. */
92
184
  modelRegistry?: ModelRegistryLike;
185
+ /** Canonical runtime to share across child sessions when supplied by an SDK host. */
186
+ modelRuntime?: ModelRuntimeLike;
93
187
  /** Default model used when an agent() call does not override it. */
94
188
  model?: ModelLike;
95
189
  /** Default thinking level for subagents. */
96
190
  thinkingLevel?: ThinkingLevel;
97
191
  /** Test seam for session construction and initialization races. */
98
192
  createSession?: AgentSessionFactory;
193
+ /** Test/compatibility seam for async ModelRuntime initialization. */
194
+ createModelRuntime?: ModelRuntimeFactory;
99
195
  /** Override runtime feature detection for pre-max Pi compatibility tests. */
100
196
  supportsMaxThinking?: boolean;
101
197
  }
@@ -130,24 +226,33 @@ export interface AgentRunCall {
130
226
  agentTypeDef?: AgentTypeDef;
131
227
  /** Override cwd (worktree). */
132
228
  cwd?: string;
133
- /** Live activity stream from the subagent (text deltas / tool calls). */
229
+ /** Safe compact activity stream used by the inline workflow status. */
134
230
  onActivity?: (event: AgentActivityInput) => void;
231
+ /** Private detailed stream used by the task transcript store. */
232
+ onTelemetry?: (event: AgentTelemetryEvent) => void;
135
233
  }
136
234
 
137
235
  export class WorkflowAgentRunner {
138
236
  private readonly baseCwd: string;
139
237
  private readonly modelRegistry?: ModelRegistryLike;
238
+ private readonly providedModelRuntime?: ModelRuntimeLike;
140
239
  private readonly defaultModel?: ModelLike;
141
240
  private readonly defaultThinking?: ThinkingLevel;
142
241
  private readonly createSession: AgentSessionFactory;
242
+ private readonly createModelRuntime?: ModelRuntimeFactory;
243
+ private modelRuntimePromise?: Promise<ModelRuntimeLike | undefined>;
143
244
  private readonly runtimeSupportsMaxThinking: boolean;
144
245
 
145
246
  constructor(options: WorkflowAgentRunnerOptions) {
146
247
  this.baseCwd = options.cwd;
147
248
  this.modelRegistry = options.modelRegistry;
249
+ this.providedModelRuntime = options.modelRuntime;
148
250
  this.defaultModel = options.model;
149
251
  this.defaultThinking = options.thinkingLevel;
252
+ const usesPiSessionFactory = options.createSession === undefined;
150
253
  this.createSession = options.createSession ?? (createAgentSession as unknown as AgentSessionFactory);
254
+ this.createModelRuntime = options.createModelRuntime
255
+ ?? (options.modelRuntime !== undefined || !usesPiSessionFactory ? undefined : createPiModelRuntime);
151
256
  this.runtimeSupportsMaxThinking = options.supportsMaxThinking ?? piVersionSupportsMaxThinking(PI_VERSION);
152
257
  }
153
258
 
@@ -171,9 +276,30 @@ export class WorkflowAgentRunner {
171
276
  if (toolAllowlist) toolAllowlist.push("structured_output");
172
277
  }
173
278
 
174
- const { model, thinkingLevel } = this.resolveModel(call.modelPattern, call.agentTypeDef);
175
-
279
+ const selection = this.resolveModel(call.modelPattern, call.agentTypeDef);
280
+ const thinkingLevel = selection.thinkingLevel;
176
281
  const agentDir = getAgentDir();
282
+
283
+ let modelRuntime: ModelRuntimeLike | undefined;
284
+ let model: ModelLike | undefined;
285
+ try {
286
+ modelRuntime = await waitForSharedInitialization(
287
+ this.getModelRuntime(agentDir),
288
+ call.signal,
289
+ );
290
+ if (call.signal?.aborted) throw abortedError();
291
+ model = selection.model && modelRuntime?.getModel
292
+ ? modelRuntime.getModel(selection.model.provider, selection.model.id) ?? selection.model
293
+ : selection.model;
294
+ } catch (error) {
295
+ safeEmitTelemetry(call.onTelemetry, {
296
+ kind: "run_error",
297
+ error: safeDisplayText(errorText(error), 512),
298
+ usage: emptyAgentUsage(),
299
+ });
300
+ throw error;
301
+ }
302
+
177
303
  const createSession = (level: ThinkingLevel | undefined) => this.createSession({
178
304
  cwd,
179
305
  agentDir,
@@ -183,34 +309,62 @@ export class WorkflowAgentRunner {
183
309
  ...(model ? { model: model as any } : {}),
184
310
  ...(level ? { thinkingLevel: level as any } : {}),
185
311
  ...(toolAllowlist ? { tools: toolAllowlist } : {}),
186
- ...(this.modelRegistry ? { modelRegistry: this.modelRegistry as any } : {}),
312
+ ...(modelRuntime
313
+ ? { modelRuntime }
314
+ : this.modelRegistry
315
+ ? { modelRegistry: this.modelRegistry as any }
316
+ : {}),
187
317
  });
188
318
 
189
319
  const sessionThinking = resolveSessionThinkingLevel(thinkingLevel, model);
190
- let created = await createSession(sessionThinking);
191
- if (call.signal?.aborted) {
192
- disposeQuietly(created.session);
193
- throw abortedError();
194
- }
195
- const selectedModel = created.session.model;
196
- const selectedModelAdvertisesMax = (model ?? selectedModel)?.thinkingLevelMap?.max != null;
197
- if (
198
- thinkingLevel === ULTRACODE_THINKING_LEVEL &&
199
- sessionThinking === ULTRACODE_THINKING_LEVEL &&
200
- created.session.thinkingLevel !== ULTRACODE_THINKING_LEVEL &&
201
- created.session.supportsThinking() &&
202
- (!this.runtimeSupportsMaxThinking || selectedModelAdvertisesMax)
203
- ) {
204
- // A legacy runtime may clamp an unknown `max` to medium/high instead of
205
- // off. Recreate with xhigh; never mutate the user's global default effort.
206
- disposeQuietly(created.session);
207
- created = await createSession(LEGACY_ULTRACODE_THINKING_LEVEL);
320
+ safeEmitTelemetry(call.onTelemetry, {
321
+ kind: "model_requested",
322
+ modelId: model?.id,
323
+ effort: thinkingLevel,
324
+ });
325
+
326
+ let created: { session: AgentSessionLike };
327
+ try {
328
+ created = await createSession(sessionThinking);
208
329
  if (call.signal?.aborted) {
209
330
  disposeQuietly(created.session);
210
331
  throw abortedError();
211
332
  }
333
+ const selectedModel = created.session.model;
334
+ const selectedModelAdvertisesMax = (model ?? selectedModel)?.thinkingLevelMap?.max != null;
335
+ if (
336
+ thinkingLevel === ULTRACODE_THINKING_LEVEL &&
337
+ sessionThinking === ULTRACODE_THINKING_LEVEL &&
338
+ created.session.thinkingLevel !== ULTRACODE_THINKING_LEVEL &&
339
+ created.session.supportsThinking() &&
340
+ (!this.runtimeSupportsMaxThinking || selectedModelAdvertisesMax)
341
+ ) {
342
+ // A legacy runtime may clamp an unknown `max` to medium/high instead of
343
+ // off. Recreate with xhigh; never mutate the user's global default effort.
344
+ disposeQuietly(created.session);
345
+ created = await createSession(LEGACY_ULTRACODE_THINKING_LEVEL);
346
+ if (call.signal?.aborted) {
347
+ disposeQuietly(created.session);
348
+ throw abortedError();
349
+ }
350
+ }
351
+ } catch (error) {
352
+ safeEmitTelemetry(call.onTelemetry, {
353
+ kind: "run_error",
354
+ error: safeDisplayText(errorText(error), 512),
355
+ usage: emptyAgentUsage(),
356
+ });
357
+ throw error;
212
358
  }
213
359
  const { session } = created;
360
+ const actualModelId = session.model?.id ?? model?.id;
361
+ const actualEffort = session.thinkingLevel;
362
+ const telemetryCounters = { retries: 0, compactions: 0, turns: 0, toolUses: 0, observing: false };
363
+ safeEmitTelemetry(call.onTelemetry, {
364
+ kind: "model_resolved",
365
+ modelId: actualModelId,
366
+ effort: actualEffort,
367
+ });
214
368
 
215
369
  let removeAbort: (() => void) | undefined;
216
370
  let unsubscribe: (() => void) | undefined;
@@ -234,11 +388,21 @@ export class WorkflowAgentRunner {
234
388
  }
235
389
  }
236
390
 
237
- // Forward live activity (text deltas / tool calls) so the workflow
238
- // snapshot can show per-agent progress and detect stuck subagents.
239
- if (call.onActivity) {
240
- const onActivity = call.onActivity;
241
- unsubscribe = session.subscribe((event: unknown) => forwardActivity(event, onActivity));
391
+ // One subscription feeds both compact status and the private transcript
392
+ // stream. Telemetry callbacks are isolated so observability can never
393
+ // change the child run's outcome.
394
+ if (call.onActivity || call.onTelemetry) {
395
+ telemetryCounters.observing = true;
396
+ unsubscribe = session.subscribe((event: unknown) => {
397
+ const sessionEvent = event as { type?: unknown; message?: { role?: unknown } };
398
+ const eventType = sessionEvent?.type;
399
+ if (eventType === "message_end" && sessionEvent.message?.role === "assistant") telemetryCounters.turns++;
400
+ if (eventType === "tool_execution_start") telemetryCounters.toolUses++;
401
+ if (eventType === "auto_retry_start") telemetryCounters.retries++;
402
+ if (eventType === "compaction_start") telemetryCounters.compactions++;
403
+ if (call.onActivity) forwardActivity(event, call.onActivity);
404
+ if (call.onTelemetry) forwardTelemetry(event, call.onTelemetry);
405
+ });
242
406
  }
243
407
 
244
408
  await session.prompt(this.buildPrompt(call, Boolean(call.schema)), {
@@ -262,9 +426,22 @@ export class WorkflowAgentRunner {
262
426
  value = lastAssistantText(session.messages as unknown[]);
263
427
  }
264
428
 
265
- return { value, usage: readUsage(session), cwd };
429
+ return {
430
+ value,
431
+ usage: readUsage(session, telemetryCounters),
432
+ modelId: actualModelId,
433
+ effort: actualEffort,
434
+ cwd,
435
+ };
266
436
  } catch (error) {
267
437
  hasPrimaryError = true;
438
+ safeEmitTelemetry(call.onTelemetry, {
439
+ kind: "run_error",
440
+ error: safeDisplayText(errorText(error), 512),
441
+ usage: readUsage(session, telemetryCounters),
442
+ modelId: actualModelId,
443
+ effort: actualEffort,
444
+ });
268
445
  throw error;
269
446
  } finally {
270
447
  let hasCleanupError = false;
@@ -315,6 +492,61 @@ export class WorkflowAgentRunner {
315
492
  return parts.filter(Boolean).join("\n\n");
316
493
  }
317
494
 
495
+ private getModelRuntime(agentDir: string): Promise<ModelRuntimeLike | undefined> {
496
+ if (this.providedModelRuntime) return Promise.resolve(this.providedModelRuntime);
497
+ if (!this.createModelRuntime) return Promise.resolve(undefined);
498
+ if (!this.modelRuntimePromise) {
499
+ // Cache the in-flight promise so parallel agent() calls never initialize
500
+ // separate runtimes or race provider/auth replay. Clear only this failed
501
+ // attempt so a later serial agent can retry transient initialization errors.
502
+ const pending = this.initializeModelRuntime(agentDir);
503
+ this.modelRuntimePromise = pending;
504
+ void pending.catch(() => {
505
+ if (this.modelRuntimePromise === pending) this.modelRuntimePromise = undefined;
506
+ });
507
+ }
508
+ return this.modelRuntimePromise;
509
+ }
510
+
511
+ private async initializeModelRuntime(agentDir: string): Promise<ModelRuntimeLike | undefined> {
512
+ const runtime = await this.createModelRuntime?.({
513
+ authPath: path.join(agentDir, "auth.json"),
514
+ modelsPath: path.join(agentDir, "models.json"),
515
+ allowModelNetwork: false,
516
+ });
517
+ if (!runtime) return undefined;
518
+
519
+ const registeredProviderIds = this.modelRegistry?.getRegisteredProviderIds?.() ?? [];
520
+ let providersChanged = false;
521
+ for (const provider of registeredProviderIds) {
522
+ const config = this.modelRegistry?.getRegisteredProviderConfig?.(provider);
523
+ if (config === undefined || !runtime.registerProvider) continue;
524
+ runtime.registerProvider(provider, config);
525
+ providersChanged = true;
526
+ }
527
+ if (providersChanged && runtime.refresh) {
528
+ await runtime.refresh({ allowNetwork: false });
529
+ }
530
+
531
+ // Only replay CLI/SDK runtime overrides. Stored credentials, OAuth, env,
532
+ // and models.json auth must stay owned by the new runtime so they can refresh.
533
+ if (
534
+ runtime.setRuntimeApiKey
535
+ && this.modelRegistry?.getProviderAuthStatus
536
+ && this.modelRegistry.getApiKeyForProvider
537
+ ) {
538
+ const providers = new Set(this.modelRegistry.getAvailable().map((model) => model.provider));
539
+ for (const provider of registeredProviderIds) providers.add(provider);
540
+ for (const provider of providers) {
541
+ if (this.modelRegistry.getProviderAuthStatus(provider).source !== "runtime") continue;
542
+ const apiKey = await this.modelRegistry.getApiKeyForProvider(provider);
543
+ if (apiKey) await runtime.setRuntimeApiKey(provider, apiKey);
544
+ }
545
+ }
546
+
547
+ return runtime;
548
+ }
549
+
318
550
  private resolveModel(
319
551
  pattern: string | undefined,
320
552
  role: AgentTypeDef | undefined,
@@ -330,6 +562,20 @@ export class WorkflowAgentRunner {
330
562
  }
331
563
  }
332
564
 
565
+ interface ModelRuntimeConstructorLike {
566
+ create(options: ModelRuntimeCreateOptions): Promise<ModelRuntimeLike>;
567
+ }
568
+
569
+ /** Capability detection avoids importing a named export that Pi 0.80.7 lacks. */
570
+ async function createPiModelRuntime(
571
+ options: ModelRuntimeCreateOptions,
572
+ ): Promise<ModelRuntimeLike | undefined> {
573
+ const runtimeClass = (PiCodingAgent as unknown as {
574
+ ModelRuntime?: ModelRuntimeConstructorLike;
575
+ }).ModelRuntime;
576
+ return runtimeClass?.create ? runtimeClass.create(options) : undefined;
577
+ }
578
+
333
579
  export function splitThinkingSuffix(pattern: string): { base: string; thinking?: ThinkingLevel } {
334
580
  const idx = pattern.lastIndexOf(":");
335
581
  if (idx === -1) return { base: pattern };
@@ -417,6 +663,30 @@ export function resolveModelSelection(args: {
417
663
  return { model, thinkingLevel: thinking ?? roleThinking ?? defaultThinking };
418
664
  }
419
665
 
666
+ function waitForSharedInitialization<T>(
667
+ pending: Promise<T>,
668
+ signal: AbortSignal | undefined,
669
+ ): Promise<T> {
670
+ if (!signal) return pending;
671
+ if (signal.aborted) return Promise.reject(abortedError());
672
+
673
+ return new Promise<T>((resolve, reject) => {
674
+ let settled = false;
675
+ const finish = (callback: () => void) => {
676
+ if (settled) return;
677
+ settled = true;
678
+ signal.removeEventListener("abort", onAbort);
679
+ callback();
680
+ };
681
+ const onAbort = () => finish(() => reject(abortedError()));
682
+ signal.addEventListener("abort", onAbort, { once: true });
683
+ pending.then(
684
+ (value) => finish(() => resolve(value)),
685
+ (error) => finish(() => reject(error)),
686
+ );
687
+ });
688
+ }
689
+
420
690
  function abortedError(): Error {
421
691
  return new Error("Subagent was aborted");
422
692
  }
@@ -429,31 +699,237 @@ function disposeQuietly(session: { dispose(): void }): void {
429
699
  }
430
700
  }
431
701
 
432
- function readUsage(session: any): AgentUsage {
702
+ function emptyAgentUsage(): AgentUsage {
703
+ return {
704
+ inputTokens: 0,
705
+ outputTokens: 0,
706
+ cacheReadTokens: 0,
707
+ cacheWriteTokens: 0,
708
+ totalTokens: 0,
709
+ cost: 0,
710
+ turns: 0,
711
+ toolUses: 0,
712
+ retries: 0,
713
+ compactions: 0,
714
+ };
715
+ }
716
+
717
+ function errorText(error: unknown): string {
718
+ return error instanceof Error ? error.message : String(error);
719
+ }
720
+
721
+ function safeEmitTelemetry(
722
+ listener: ((event: AgentTelemetryEvent) => void) | undefined,
723
+ event: AgentTelemetryEvent,
724
+ ): void {
725
+ if (!listener) return;
726
+ try {
727
+ listener(event);
728
+ } catch {
729
+ // Detailed observability is best-effort and must never affect execution.
730
+ }
731
+ }
732
+
733
+ function normalizeTurnUsage(value: unknown): AgentTurnUsage | undefined {
734
+ if (!value || typeof value !== "object") return undefined;
735
+ const usage = value as any;
736
+ const inputTokens = finiteNumber(usage.input) ?? finiteNumber(usage.inputTokens) ?? 0;
737
+ const outputTokens = finiteNumber(usage.output) ?? finiteNumber(usage.outputTokens) ?? 0;
738
+ return {
739
+ inputTokens,
740
+ outputTokens,
741
+ cacheReadTokens: finiteNumber(usage.cacheRead) ?? 0,
742
+ cacheWriteTokens: finiteNumber(usage.cacheWrite) ?? 0,
743
+ // Compact task stats intentionally exclude cache traffic from token use.
744
+ totalTokens: inputTokens + outputTokens,
745
+ cost: finiteNumber(usage.cost?.total) ?? finiteNumber(usage.cost) ?? 0,
746
+ };
747
+ }
748
+
749
+ function assistantText(message: any): string {
750
+ if (!Array.isArray(message?.content)) return "";
751
+ return message.content
752
+ .filter((part: any) => part?.type === "text" && typeof part.text === "string")
753
+ .map((part: any) => part.text)
754
+ .join("");
755
+ }
756
+
757
+ function resultText(result: any): string {
758
+ if (!Array.isArray(result?.content)) return "";
759
+ return result.content
760
+ .filter((part: any) => part?.type === "text" && typeof part.text === "string")
761
+ .map((part: any) => part.text)
762
+ .join("\n");
763
+ }
764
+
765
+ function tailByUtf8Bytes(value: string, maxBytes: number): string {
766
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
767
+ let low = 0;
768
+ let high = value.length;
769
+ while (low < high) {
770
+ const mid = Math.floor((low + high) / 2);
771
+ if (Buffer.byteLength(value.slice(mid), "utf8") <= maxBytes) high = mid;
772
+ else low = mid + 1;
773
+ }
774
+ return value.slice(low);
775
+ }
776
+
777
+ function toolResultPreview(result: unknown): string | undefined {
778
+ const raw = resultText(result);
779
+ if (!raw.trim()) return undefined;
780
+ const safe = safeTranscriptText(raw, 64 * 1024);
781
+ const lines = safe.split(/\r?\n/).filter((line) => line.trim()).slice(-20).join("\n");
782
+ const bounded = tailByUtf8Bytes(lines, 8 * 1024);
783
+ return bounded.trim() || undefined;
784
+ }
785
+
786
+ /** Convert child session events into the private task-detail stream. */
787
+ export function forwardTelemetry(
788
+ event: unknown,
789
+ onTelemetry: (event: AgentTelemetryEvent) => void,
790
+ ): void {
791
+ try {
792
+ const e = event as any;
793
+ switch (e?.type) {
794
+ case "turn_start":
795
+ safeEmitTelemetry(onTelemetry, {
796
+ kind: "turn_start",
797
+ turnIndex: finiteNumber(e.turnIndex),
798
+ });
799
+ return;
800
+ case "message_update": {
801
+ const update = e.assistantMessageEvent;
802
+ if (update?.type === "text_delta" && typeof update.delta === "string") {
803
+ safeEmitTelemetry(onTelemetry, { kind: "text_delta", delta: update.delta });
804
+ } else if (update?.type === "thinking_start") {
805
+ safeEmitTelemetry(onTelemetry, { kind: "thinking_start" });
806
+ } else if (update?.type === "thinking_end") {
807
+ safeEmitTelemetry(onTelemetry, { kind: "thinking_end" });
808
+ }
809
+ return;
810
+ }
811
+ case "message_end":
812
+ if (e.message?.role === "assistant") {
813
+ safeEmitTelemetry(onTelemetry, {
814
+ kind: "message_end",
815
+ text: assistantText(e.message),
816
+ usage: normalizeTurnUsage(e.message.usage),
817
+ error: typeof e.message.errorMessage === "string" ? e.message.errorMessage : undefined,
818
+ });
819
+ }
820
+ return;
821
+ case "tool_execution_start":
822
+ if (typeof e.toolName === "string") {
823
+ const args = toolArgsPreview(e.toolName, e.args);
824
+ safeEmitTelemetry(onTelemetry, {
825
+ kind: "tool_start",
826
+ toolCallId: typeof e.toolCallId === "string" ? e.toolCallId : `${e.toolName}:unknown`,
827
+ toolName: e.toolName,
828
+ ...(args ? { toolArgs: args } : {}),
829
+ });
830
+ }
831
+ return;
832
+ case "tool_execution_end":
833
+ safeEmitTelemetry(onTelemetry, {
834
+ kind: "tool_end",
835
+ toolCallId: typeof e.toolCallId === "string" ? e.toolCallId : `${String(e.toolName ?? "tool")}:unknown`,
836
+ toolName: typeof e.toolName === "string" ? e.toolName : "tool",
837
+ isError: Boolean(e.isError),
838
+ resultPreview: toolResultPreview(e.result),
839
+ });
840
+ return;
841
+ case "auto_retry_start": {
842
+ const attempt = finiteNumber(e.attempt) ?? 0;
843
+ const maxAttempts = finiteNumber(e.maxAttempts) ?? 0;
844
+ const reason = typeof e.errorMessage === "string" ? safeDisplayText(e.errorMessage, 160) : "";
845
+ safeEmitTelemetry(onTelemetry, {
846
+ kind: "retry",
847
+ state: "start",
848
+ detail: `retry ${attempt}/${maxAttempts} in ${formatDelay(finiteNumber(e.delayMs) ?? 0)}${reason ? `: ${reason}` : ""}`,
849
+ });
850
+ return;
851
+ }
852
+ case "auto_retry_end": {
853
+ const failure = typeof e.finalError === "string" ? safeDisplayText(e.finalError, 180) : "";
854
+ safeEmitTelemetry(onTelemetry, {
855
+ kind: "retry",
856
+ state: "end",
857
+ detail: e.success ? "retry succeeded" : failure ? `retry failed: ${failure}` : "retry failed",
858
+ });
859
+ return;
860
+ }
861
+ case "compaction_start":
862
+ safeEmitTelemetry(onTelemetry, {
863
+ kind: "compaction",
864
+ state: "start",
865
+ detail: compactionStartDetail(e.reason),
866
+ });
867
+ return;
868
+ case "compaction_end":
869
+ safeEmitTelemetry(onTelemetry, {
870
+ kind: "compaction",
871
+ state: "end",
872
+ detail: e.aborted ? "compaction aborted" : e.errorMessage ? "compaction failed" : "compaction complete",
873
+ });
874
+ return;
875
+ default:
876
+ return;
877
+ }
878
+ } catch {
879
+ // best-effort: malformed provider events must not affect the run
880
+ }
881
+ }
882
+
883
+ function readUsage(
884
+ session: any,
885
+ counters: {
886
+ retries?: number;
887
+ compactions?: number;
888
+ turns?: number;
889
+ toolUses?: number;
890
+ observing?: boolean;
891
+ } = {},
892
+ ): AgentUsage {
433
893
  try {
434
894
  const stats = session.getSessionStats?.();
435
895
  if (stats?.tokens) {
436
896
  return {
897
+ inputTokens: stats.tokens.input ?? 0,
437
898
  outputTokens: stats.tokens.output ?? 0,
438
- totalTokens: stats.tokens.total ?? 0,
899
+ cacheReadTokens: stats.tokens.cacheRead ?? 0,
900
+ cacheWriteTokens: stats.tokens.cacheWrite ?? 0,
901
+ totalTokens: (stats.tokens.input ?? 0) + (stats.tokens.output ?? 0),
439
902
  cost: stats.cost ?? 0,
903
+ turns: counters.observing ? counters.turns ?? 0 : stats.assistantMessages ?? 0,
904
+ toolUses: counters.observing ? counters.toolUses ?? 0 : stats.toolCalls ?? 0,
905
+ retries: counters.retries ?? 0,
906
+ compactions: counters.compactions ?? 0,
440
907
  };
441
908
  }
442
909
  } catch {
443
910
  // fall through to message-based estimate
444
911
  }
445
912
  // Fallback: sum assistant usage from messages.
446
- let output = 0;
447
- let total = 0;
448
- let cost = 0;
449
- for (const message of (session.messages ?? []) as Array<Partial<AssistantMessage>>) {
913
+ const usage = emptyAgentUsage();
914
+ for (const message of (session.messages ?? []) as Array<any>) {
450
915
  if (message?.role === "assistant" && message.usage) {
451
- output += message.usage.output ?? 0;
452
- total += message.usage.totalTokens ?? 0;
453
- cost += message.usage.cost?.total ?? 0;
916
+ usage.turns = (usage.turns ?? 0) + 1;
917
+ usage.inputTokens = (usage.inputTokens ?? 0) + (message.usage.input ?? 0);
918
+ usage.outputTokens += message.usage.output ?? 0;
919
+ usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + (message.usage.cacheRead ?? 0);
920
+ usage.cacheWriteTokens = (usage.cacheWriteTokens ?? 0) + (message.usage.cacheWrite ?? 0);
921
+ usage.cost += message.usage.cost?.total ?? 0;
454
922
  }
923
+ if (message?.role === "toolResult") usage.toolUses = (usage.toolUses ?? 0) + 1;
924
+ }
925
+ if (counters.observing) {
926
+ usage.turns = counters.turns ?? 0;
927
+ usage.toolUses = counters.toolUses ?? 0;
455
928
  }
456
- return { outputTokens: output, totalTokens: total, cost };
929
+ usage.totalTokens = (usage.inputTokens ?? 0) + usage.outputTokens;
930
+ usage.retries = counters.retries ?? 0;
931
+ usage.compactions = counters.compactions ?? 0;
932
+ return usage;
457
933
  }
458
934
 
459
935
  function lastAssistantText(messages: unknown[]): string {