pi-ultracode 0.1.0 → 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.
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import vm from "node:vm";
13
+ import { AsyncLocalStorage } from "node:async_hooks";
13
14
  import * as fs from "node:fs";
14
15
  import * as os from "node:os";
15
16
  import * as path from "node:path";
@@ -18,7 +19,17 @@ import { parseWorkflowScript, type WorkflowMeta } from "./parser.ts";
18
19
  // loader ("WorkflowAgentRunner is not a constructor"). Tests inject a runner, so
19
20
  // they never construct this class; production builds it via getRunner().
20
21
  import { WorkflowAgentRunner } from "./agent-runner.ts";
21
- import type { AgentActivityInput, AgentRunResult, ModelLike, ModelRegistryLike, ThinkingLevel } from "./agent-runner.ts";
22
+ import type {
23
+ AgentActivityInput,
24
+ AgentRunResult,
25
+ AgentTelemetryEvent,
26
+ AgentUsage,
27
+ ModelLike,
28
+ ModelRegistryLike,
29
+ ModelRuntimeLike,
30
+ ThinkingLevel,
31
+ } from "./agent-runner.ts";
32
+ import { safeDisplayText } from "./display-text.ts";
22
33
 
23
34
  /**
24
35
  * A frozen copy of `Math` with `random` replaced by a throwing function. Workflow
@@ -54,7 +65,7 @@ export function createDeterministicMath(): Record<string, unknown> {
54
65
 
55
66
  const DETERMINISTIC_MATH = createDeterministicMath();
56
67
  import { discoverAgentTypes, resolveAgentType, type AgentTypeDef } from "./agent-types.ts";
57
- import { agentCallKey, RunJournal } from "./journal.ts";
68
+ import { agentCallKey, RunJournal, type JournalAgentRecord } from "./journal.ts";
58
69
  import {
59
70
  applyPatch,
60
71
  captureWorktreeDiff,
@@ -70,18 +81,18 @@ import {
70
81
  const MAX_CONCURRENCY = 16;
71
82
  const MAX_AGENTS_PER_RUN = 1000;
72
83
  const MAX_ITEMS_PER_CALL = 4096;
84
+ export const MAX_WORKFLOW_LOGS = 256;
85
+ export const WORKFLOW_LOG_OMITTED_TEXT = "additional workflow logs omitted";
73
86
 
74
87
  export interface AgentEventBase {
75
88
  id: number;
76
89
  label: string;
77
90
  phase?: string;
91
+ workflowPath?: string[];
78
92
  }
79
93
 
80
- /** Live activity observed inside a running subagent (text deltas / tool calls). */
81
- export interface AgentActivityEvent extends AgentEventBase {
82
- kind: "text" | "thinking" | "tool";
83
- detail?: string;
84
- }
94
+ /** Live activity observed inside a running subagent. */
95
+ export type AgentActivityEvent = AgentEventBase & AgentActivityInput;
85
96
 
86
97
  export interface WorkflowRunOptions {
87
98
  cwd?: string;
@@ -89,7 +100,10 @@ export interface WorkflowRunOptions {
89
100
  signal?: AbortSignal;
90
101
  concurrency?: number;
91
102
  tokenBudget?: number | null;
103
+ /** Synchronous extension facade used for model selection. */
92
104
  modelRegistry?: ModelRegistryLike;
105
+ /** Canonical model/auth runtime shared across child sessions. */
106
+ modelRuntime?: ModelRuntimeLike;
93
107
  model?: ModelLike;
94
108
  thinkingLevel?: ThinkingLevel;
95
109
  /** Inject a runner (tests). */
@@ -99,9 +113,27 @@ export interface WorkflowRunOptions {
99
113
  loadSavedWorkflow?: (nameOrRef: string | { scriptPath: string }) => { meta: WorkflowMeta; body: string };
100
114
  onLog?: (message: string) => void;
101
115
  onPhase?: (title: string) => void;
102
- onAgentStart?: (event: AgentEventBase & { prompt: string; cached: boolean }) => void;
103
- onAgentEnd?: (event: AgentEventBase & { result: unknown; status: "done" | "error" }) => void;
116
+ onAgentStart?: (event: AgentEventBase & {
117
+ prompt: string;
118
+ cached: boolean;
119
+ modelPattern?: string;
120
+ requestedEffort?: ThinkingLevel;
121
+ agentType?: string;
122
+ isolation?: string;
123
+ structuredOutput?: boolean;
124
+ cachedRecord?: JournalAgentRecord;
125
+ }) => void;
126
+ onAgentEnd?: (event: AgentEventBase & {
127
+ result: unknown;
128
+ status: "done" | "error";
129
+ usage?: AgentUsage;
130
+ modelId?: string;
131
+ effort?: ThinkingLevel;
132
+ error?: string;
133
+ cachedRecord?: JournalAgentRecord;
134
+ }) => void;
104
135
  onAgentActivity?: (event: AgentActivityEvent) => void;
136
+ onAgentTelemetry?: (event: AgentEventBase & AgentTelemetryEvent) => void;
105
137
  }
106
138
 
107
139
  export interface WorkflowRunResult<T = unknown> {
@@ -112,6 +144,10 @@ export interface WorkflowRunResult<T = unknown> {
112
144
  agentCount: number;
113
145
  cachedCount: number;
114
146
  spentTokens: number;
147
+ /** Actual input+output tokens incurred by live child sessions. */
148
+ newTokens: number;
149
+ /** Original input+output usage represented by cached replayed tasks. */
150
+ replayedTokens: number;
115
151
  durationMs: number;
116
152
  }
117
153
 
@@ -121,7 +157,9 @@ interface RuntimeState {
121
157
  phases: string[];
122
158
  agentCount: number; // number of agent() invocations (for ids / cap)
123
159
  cachedCount: number;
124
- spent: number; // real output tokens
160
+ spent: number; // real output tokens (budget compatibility)
161
+ newTokens: number;
162
+ replayedTokens: number;
125
163
  }
126
164
 
127
165
  export async function runWorkflow<T = unknown>(
@@ -144,12 +182,22 @@ export async function runWorkflow<T = unknown>(
144
182
  agentCount: runtime.state.agentCount,
145
183
  cachedCount: runtime.state.cachedCount,
146
184
  spentTokens: runtime.state.spent,
185
+ newTokens: runtime.state.newTokens,
186
+ replayedTokens: runtime.state.replayedTokens,
147
187
  durationMs: Date.now() - started,
148
188
  };
149
189
  }
150
190
 
151
191
  class Runtime {
152
- readonly state: RuntimeState = { logs: [], phases: [], agentCount: 0, cachedCount: 0, spent: 0 };
192
+ readonly state: RuntimeState = {
193
+ logs: [],
194
+ phases: [],
195
+ agentCount: 0,
196
+ cachedCount: 0,
197
+ spent: 0,
198
+ newTokens: 0,
199
+ replayedTokens: 0,
200
+ };
153
201
  private readonly options: WorkflowRunOptions;
154
202
  private readonly cwd: string;
155
203
  private runnerInstance: { run: WorkflowAgentRunner["run"] } | undefined;
@@ -158,7 +206,7 @@ class Runtime {
158
206
  private readonly pending = new Set<Promise<unknown>>();
159
207
  private readonly tokenBudget: number | null;
160
208
  private readonly applyLock = new Mutex();
161
- private depth = 0;
209
+ private readonly executionContext = new AsyncLocalStorage<{ workflowPath: string[]; depth: number; currentPhase?: string }>();
162
210
 
163
211
  constructor(options: WorkflowRunOptions) {
164
212
  this.options = options;
@@ -190,6 +238,7 @@ class Runtime {
190
238
  this.runnerInstance = new WorkflowAgentRunner({
191
239
  cwd: this.cwd,
192
240
  modelRegistry: this.options.modelRegistry,
241
+ modelRuntime: this.options.modelRuntime,
193
242
  model: this.options.model,
194
243
  thinkingLevel: this.options.thinkingLevel,
195
244
  });
@@ -201,15 +250,17 @@ class Runtime {
201
250
  async runBody(body: string, args: unknown, depth: number, name: string): Promise<unknown> {
202
251
  const context = vm.createContext(this.buildSandbox(args));
203
252
  const wrapped = `(async () => {\n${body}\n})()`;
204
- return new vm.Script(wrapped, { filename: `${name || "workflow"}.js` }).runInContext(context);
253
+ const parent = this.executionContext.getStore();
254
+ const parentPath = parent?.workflowPath ?? [];
255
+ const execution = { workflowPath: [...parentPath, name || `workflow-${depth}`], depth, currentPhase: parent?.currentPhase };
256
+ return this.executionContext.run(
257
+ execution,
258
+ async () => await new vm.Script(wrapped, { filename: `${name || "workflow"}.js` }).runInContext(context),
259
+ );
205
260
  }
206
261
 
207
262
  private buildSandbox(args: unknown): Record<string, unknown> {
208
- const log = (message: unknown) => {
209
- const text = String(message);
210
- this.state.logs.push(text);
211
- this.options.onLog?.(text);
212
- };
263
+ const log = (message: unknown) => this.logLine(String(message));
213
264
  return {
214
265
  agent: this.agent.bind(this),
215
266
  parallel: this.parallel.bind(this),
@@ -248,6 +299,8 @@ class Runtime {
248
299
  private phase(title: unknown): void {
249
300
  const text = requireString(title, "phase title");
250
301
  this.state.currentPhase = text;
302
+ const execution = this.executionContext.getStore();
303
+ if (execution) execution.currentPhase = text;
251
304
  if (!this.state.phases.includes(text)) this.state.phases.push(text);
252
305
  this.options.onPhase?.(text);
253
306
  }
@@ -259,7 +312,7 @@ class Runtime {
259
312
  }
260
313
  const prompt = requireString(promptValue, "agent prompt");
261
314
  const opts = normalizeAgentOptions(optionsValue);
262
- const assignedPhase = opts.phase ?? this.state.currentPhase;
315
+ const assignedPhase = opts.phase ?? this.executionContext.getStore()?.currentPhase ?? this.state.currentPhase;
263
316
 
264
317
  const seq = ++this.state.agentCount;
265
318
  if (seq > MAX_AGENTS_PER_RUN) {
@@ -267,6 +320,7 @@ class Runtime {
267
320
  }
268
321
  const id = seq;
269
322
  const label = opts.label?.trim() || defaultLabel(assignedPhase, id);
323
+ const workflowPath = [...(this.executionContext.getStore()?.workflowPath ?? [])];
270
324
  const key = agentCallKey(prompt, { ...opts, phase: assignedPhase });
271
325
 
272
326
  // Resume: cached prefix replay.
@@ -274,13 +328,62 @@ class Runtime {
274
328
  if (cached) {
275
329
  this.state.cachedCount++;
276
330
  this.state.spent += cached.outputTokens ?? 0;
277
- this.options.onAgentStart?.({ id, label, phase: assignedPhase, prompt, cached: true });
278
- this.options.onAgentEnd?.({ id, label, phase: assignedPhase, result: cached.value, status: "done" });
331
+ this.state.replayedTokens += cached.totalTokens ?? 0;
332
+ const cachedUsage: AgentUsage = {
333
+ inputTokens: cached.inputTokens,
334
+ outputTokens: cached.outputTokens ?? 0,
335
+ totalTokens: cached.totalTokens ?? 0,
336
+ cacheReadTokens: cached.cacheReadTokens,
337
+ cacheWriteTokens: cached.cacheWriteTokens,
338
+ cost: cached.cost ?? 0,
339
+ turns: cached.turns,
340
+ toolUses: cached.toolUses,
341
+ retries: cached.retries,
342
+ compactions: cached.compactions,
343
+ };
344
+ this.options.onAgentStart?.({
345
+ id,
346
+ label,
347
+ phase: assignedPhase,
348
+ workflowPath,
349
+ prompt,
350
+ cached: true,
351
+ modelPattern: opts.model,
352
+ requestedEffort: this.options.thinkingLevel,
353
+ agentType: opts.agentType,
354
+ isolation: opts.isolation,
355
+ structuredOutput: opts.schema != null,
356
+ cachedRecord: cached,
357
+ });
358
+ this.options.onAgentEnd?.({
359
+ id,
360
+ label,
361
+ phase: assignedPhase,
362
+ workflowPath,
363
+ result: cached.value,
364
+ status: "done",
365
+ usage: cachedUsage,
366
+ modelId: cached.modelId,
367
+ effort: cached.effort,
368
+ cachedRecord: cached,
369
+ });
279
370
  return cached.value;
280
371
  }
281
372
 
282
373
  const run = this.limiter(async () => {
283
- this.options.onAgentStart?.({ id, label, phase: assignedPhase, prompt, cached: false });
374
+ this.options.onAgentStart?.({
375
+ id,
376
+ label,
377
+ phase: assignedPhase,
378
+ workflowPath,
379
+ prompt,
380
+ cached: false,
381
+ modelPattern: opts.model,
382
+ requestedEffort: this.options.thinkingLevel,
383
+ agentType: opts.agentType,
384
+ isolation: opts.isolation,
385
+ structuredOutput: opts.schema != null,
386
+ });
284
387
  let worktree: Worktree | undefined;
285
388
  let keepWorktree = false;
286
389
  try {
@@ -294,7 +397,11 @@ class Runtime {
294
397
  const runner = this.getRunner();
295
398
  const onActivity: ((e: AgentActivityInput) => void) | undefined = this.options.onAgentActivity
296
399
  ? (e: AgentActivityInput) =>
297
- this.options.onAgentActivity!({ id, label, phase: assignedPhase, ...e })
400
+ this.options.onAgentActivity!({ id, label, phase: assignedPhase, workflowPath, ...e })
401
+ : undefined;
402
+ const onTelemetry: ((e: AgentTelemetryEvent) => void) | undefined = this.options.onAgentTelemetry
403
+ ? (e: AgentTelemetryEvent) =>
404
+ this.options.onAgentTelemetry!({ id, label, phase: assignedPhase, workflowPath, ...e })
298
405
  : undefined;
299
406
  const agentStartedAt = Date.now();
300
407
  const result: AgentRunResult = await runner.run({
@@ -307,28 +414,71 @@ class Runtime {
307
414
  agentTypeDef,
308
415
  cwd: worktree?.agentCwd,
309
416
  onActivity,
417
+ onTelemetry,
310
418
  });
311
419
  this.throwIfAborted();
312
420
 
313
421
  if (worktree) keepWorktree = await this.integrateWorktree(worktree, id, label);
314
422
 
315
- this.state.spent += result.usage.outputTokens;
423
+ const inputTokens = result.usage.inputTokens
424
+ ?? Math.max(0, result.usage.totalTokens - result.usage.outputTokens);
425
+ const usage: AgentUsage = {
426
+ ...result.usage,
427
+ inputTokens,
428
+ totalTokens: inputTokens + result.usage.outputTokens,
429
+ };
430
+ this.state.spent += usage.outputTokens;
431
+ this.state.newTokens += usage.totalTokens;
316
432
  this.options.journal?.recordAgent({
317
433
  seq,
318
434
  key,
319
435
  label,
320
436
  value: result.value,
321
- outputTokens: result.usage.outputTokens,
437
+ inputTokens: usage.inputTokens,
438
+ outputTokens: usage.outputTokens,
439
+ totalTokens: usage.totalTokens,
440
+ cacheReadTokens: usage.cacheReadTokens,
441
+ cacheWriteTokens: usage.cacheWriteTokens,
442
+ cost: usage.cost,
443
+ turns: usage.turns,
444
+ toolUses: usage.toolUses,
445
+ retries: usage.retries,
446
+ compactions: usage.compactions,
447
+ requestedModelId: opts.model,
448
+ requestedEffort: this.options.thinkingLevel,
449
+ modelId: result.modelId,
450
+ effort: result.effort,
451
+ agentType: opts.agentType,
452
+ isolation: opts.isolation,
453
+ structuredOutput: opts.schema != null,
322
454
  startedAt: agentStartedAt,
323
455
  durationMs: Date.now() - agentStartedAt,
324
456
  });
325
- this.options.onAgentEnd?.({ id, label, phase: assignedPhase, result: result.value, status: "done" });
457
+ this.options.onAgentEnd?.({
458
+ id,
459
+ label,
460
+ phase: assignedPhase,
461
+ workflowPath,
462
+ result: result.value,
463
+ status: "done",
464
+ usage,
465
+ modelId: result.modelId,
466
+ effort: result.effort,
467
+ });
326
468
  return result.value;
327
469
  } catch (error) {
328
470
  if (this.options.signal?.aborted) throw error;
329
471
  const message = error instanceof Error ? error.message : String(error);
330
472
  this.logLine(`agent ${label} failed: ${message}`);
331
- this.options.onAgentEnd?.({ id, label, phase: assignedPhase, result: null, status: "error" });
473
+ this.options.onAgentEnd?.({
474
+ id,
475
+ label,
476
+ phase: assignedPhase,
477
+ workflowPath,
478
+ result: null,
479
+ status: "error",
480
+ error: message,
481
+ });
332
482
  return null;
333
483
  } finally {
334
484
  if (worktree && !keepWorktree) {
@@ -399,20 +549,15 @@ class Runtime {
399
549
 
400
550
  private async workflow(nameOrRef: unknown, args: unknown): Promise<unknown> {
401
551
  this.throwIfAborted();
402
- if (this.depth >= 1) {
552
+ const depth = this.executionContext.getStore()?.depth ?? 0;
553
+ if (depth >= 1) {
403
554
  throw new Error("workflow() nesting is one level deep only; cannot call workflow() inside a child workflow");
404
555
  }
405
556
  const ref = normalizeWorkflowRef(nameOrRef);
406
557
  const loader = this.options.loadSavedWorkflow ?? ((r) => loadSavedWorkflowFromDisk(r, this.cwd));
407
558
  const { meta, body } = loader(ref);
408
- this.depth++;
409
- try {
410
- this.options.onLog?.(`▸ nested workflow: ${meta.name}`);
411
- const value = await this.runBody(body, args, this.depth, meta.name);
412
- return value;
413
- } finally {
414
- this.depth--;
415
- }
559
+ this.logLine(`▸ nested workflow: ${meta.name}`);
560
+ return this.runBody(body, args, depth + 1, meta.name);
416
561
  }
417
562
 
418
563
  private tryCreateWorktree(index: number): Worktree | undefined {
@@ -515,8 +660,14 @@ class Runtime {
515
660
  }
516
661
 
517
662
  private logLine(text: string): void {
518
- this.state.logs.push(text);
519
- this.options.onLog?.(text);
663
+ if (this.state.logs.length > MAX_WORKFLOW_LOGS) return;
664
+ const safe = safeDisplayText(text, 512);
665
+ if (!safe) return;
666
+ const entry = this.state.logs.length === MAX_WORKFLOW_LOGS
667
+ ? WORKFLOW_LOG_OMITTED_TEXT
668
+ : safe;
669
+ this.state.logs.push(entry);
670
+ this.options.onLog?.(entry);
520
671
  }
521
672
  }
522
673