@zachwill/pi-orchestrate 0.2.0 → 0.3.0

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.
@@ -23,9 +23,7 @@ import {
23
23
  import { Type } from "typebox";
24
24
  import {
25
25
  type CatalogDiagnostic,
26
- type OrchestrateTaskInput,
27
- type WaveId,
28
- type WaveRecord,
26
+ type RunRecord,
29
27
  type WorkerCatalog,
30
28
  type WorkerDefinition,
31
29
  type WorkerId,
@@ -35,16 +33,17 @@ import {
35
33
  } from "./domain.js";
36
34
  import type {
37
35
  AbortTarget,
38
- AcceptedWave,
36
+ AcceptedRun,
39
37
  CompletedResult,
40
- CompletedWave,
38
+ CompletedRun,
41
39
  OrchestrationContext,
42
40
  OrchestratorRuntime,
43
41
  RuntimeSnapshot,
42
+ SettlementListener,
43
+ WorkerSettlement,
44
44
  } from "./runtime.js";
45
45
 
46
46
  const STRICT_OBJECT = { additionalProperties: false } as const;
47
- const MAX_TASKS_PER_WAVE = 12;
48
47
  const MAX_INSTRUCTION_PREVIEW_LINES = 2;
49
48
 
50
49
  const taskSchema = Type.Object(
@@ -56,15 +55,7 @@ const taskSchema = Type.Object(
56
55
  STRICT_OBJECT,
57
56
  );
58
57
 
59
- const orchestrateSchema = Type.Object(
60
- {
61
- tasks: Type.Array(taskSchema, {
62
- minItems: 1,
63
- maxItems: MAX_TASKS_PER_WAVE,
64
- }),
65
- },
66
- STRICT_OBJECT,
67
- );
58
+ const orchestrateSchema = taskSchema;
68
59
 
69
60
  const statusSchema = Type.Object({}, STRICT_OBJECT);
70
61
 
@@ -83,12 +74,6 @@ const workerAbortSchema = Type.Union([
83
74
  },
84
75
  STRICT_OBJECT,
85
76
  ),
86
- Type.Object(
87
- {
88
- wave_id: Type.String({ minLength: 1 }),
89
- },
90
- STRICT_OBJECT,
91
- ),
92
77
  Type.Object(
93
78
  {
94
79
  all: Type.Literal(true),
@@ -104,10 +89,18 @@ const workerCloseSchema = Type.Object(
104
89
  STRICT_OBJECT,
105
90
  );
106
91
 
92
+ export interface DispatchDecision {
93
+ readonly mode: "async" | "inline";
94
+ readonly synthesisGroup?: {
95
+ readonly id: string;
96
+ readonly size: number;
97
+ };
98
+ }
99
+
107
100
  export interface OrchestrationToolDependencies {
108
101
  readonly runtime: OrchestratorRuntime;
109
102
  getCatalog(ctx: ExtensionContext): WorkerCatalog | Promise<WorkerCatalog>;
110
- getDispatchMode(toolCallId: string): "async" | "inline";
103
+ getDispatchDecision(toolCallId: string): DispatchDecision;
111
104
  }
112
105
 
113
106
  export function registerOrchestrationTools(
@@ -118,66 +111,62 @@ export function registerOrchestrationTools(
118
111
  name: "orchestrate",
119
112
  label: "Orchestrate",
120
113
  description:
121
- "Dispatch 1 to 12 independent, fully briefed tasks as one concurrent wave. A sole tool call runs asynchronously; sibling tool calls make it inline and blocking.",
122
- promptSnippet: "Dispatch one concurrent wave of fully briefed worker tasks",
114
+ "Dispatch one fully briefed task. One or more sibling orchestrate calls run concurrently and asynchronously. Mixing orchestrate with another tool makes it inline and blocking.",
115
+ promptSnippet: "Dispatch one fully briefed worker task",
123
116
  promptGuidelines: [
124
- "Use orchestrate for one independent worker wave, with a complete brief for every task.",
117
+ "Use sibling orchestrate calls for independent tasks, with one complete brief per call.",
125
118
  ],
119
+ executionMode: "parallel",
126
120
  parameters: orchestrateSchema,
127
121
  renderCall(args, theme, { expanded }) {
128
- return renderDispatchCall(theme, args.tasks, expanded);
122
+ return renderDispatchCall(theme, args, expanded);
129
123
  },
130
124
  renderResult(result, { isPartial, expanded }, theme, context) {
131
125
  return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
132
126
  },
133
127
  async execute(toolCallId, params, signal, onUpdate, ctx) {
134
- const mode = deps.getDispatchMode(toolCallId);
135
- const runtimeContext = await buildRuntimeContext(ctx, deps);
136
- const settlements: unknown[] = [];
137
- const onSettlement = mode === "inline" ? (settlement: unknown) => {
138
- settlements.push(settlement);
139
- onUpdate?.({
140
- content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
141
- details: { mode: "inline", settlements: [...settlements] },
142
- });
143
- } : undefined;
144
- const wave = await orchestrateWithMode(
145
- deps.runtime,
146
- runtimeContext,
147
- params.tasks,
148
- mode,
149
- signal,
150
- onSettlement,
151
- );
152
-
128
+ const decision = deps.getDispatchDecision(toolCallId);
129
+ const mode = decision.mode;
130
+ const runtimeContext = await buildRuntimeContext(ctx, deps, decision.synthesisGroup);
153
131
  if (mode === "async") {
154
- const acceptedWave = wave as AcceptedWave;
155
- const readable = acceptedWaveDetails(acceptedWave);
132
+ const acceptedRun = await deps.runtime.orchestrate(
133
+ runtimeContext,
134
+ params,
135
+ "async",
136
+ signal,
137
+ );
138
+ const readable = acceptedRunDetails(acceptedRun);
156
139
  return {
157
140
  content: [
158
141
  {
159
142
  type: "text",
160
- text: readableDetails(`Accepted async wave ${readable.wave_id}.`, readable),
143
+ text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
161
144
  },
162
145
  ],
163
- details: acceptedWave,
146
+ details: readable,
164
147
  terminate: true,
165
148
  };
166
149
  }
167
150
 
168
- const completedWave = wave as CompletedWave;
169
- const readable = completedWaveDetails(completedWave);
151
+ const completedRun = await deps.runtime.orchestrate(
152
+ runtimeContext,
153
+ params,
154
+ "inline",
155
+ signal,
156
+ createInlineSettlementListener(onUpdate),
157
+ );
158
+ const readable = completedRunDetails(completedRun);
170
159
  return {
171
160
  content: [
172
161
  {
173
162
  type: "text",
174
163
  text: readableDetails(
175
- `Completed inline wave ${readable.wave_id} with ${readable.results.length} result(s).`,
164
+ `Completed inline run ${readable.run_id}.`,
176
165
  readable,
177
166
  ),
178
167
  },
179
168
  ],
180
- details: completedWave,
169
+ details: readable,
181
170
  };
182
171
  },
183
172
  });
@@ -238,54 +227,49 @@ export function registerOrchestrationTools(
238
227
  },
239
228
  async execute(toolCallId, params, signal, onUpdate, ctx) {
240
229
  const workerId = asWorkerId(params.worker_id);
241
- const mode = deps.getDispatchMode(toolCallId);
230
+ const mode = deps.getDispatchDecision(toolCallId).mode;
242
231
  const runtimeContext = await buildRuntimeContext(ctx, deps);
243
- const settlements: unknown[] = [];
244
- const onSettlement = mode === "inline" ? (settlement: unknown) => {
245
- settlements.push(settlement);
246
- onUpdate?.({
247
- content: [{ type: "text", text: `${settlements.length} worker response(s) received.` }],
248
- details: { mode: "inline", settlements: [...settlements] },
249
- });
250
- } : undefined;
251
- const wave = await sendWithMode(
252
- deps.runtime,
253
- runtimeContext,
254
- workerId,
255
- params.instructions,
256
- mode,
257
- signal,
258
- onSettlement,
259
- );
260
-
261
232
  if (mode === "async") {
262
- const acceptedWave = wave as AcceptedWave;
263
- const readable = acceptedWaveDetails(acceptedWave);
233
+ const acceptedRun = await deps.runtime.send(
234
+ runtimeContext,
235
+ workerId,
236
+ params.instructions,
237
+ "async",
238
+ signal,
239
+ );
240
+ const readable = acceptedRunDetails(acceptedRun);
264
241
  return {
265
242
  content: [
266
243
  {
267
244
  type: "text",
268
- text: readableDetails(`Accepted async wave ${readable.wave_id}.`, readable),
245
+ text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
269
246
  },
270
247
  ],
271
- details: acceptedWave,
248
+ details: readable,
272
249
  terminate: true,
273
250
  };
274
251
  }
275
252
 
276
- const completedWave = wave as CompletedWave;
277
- const readable = completedWaveDetails(completedWave);
253
+ const completedRun = await deps.runtime.send(
254
+ runtimeContext,
255
+ workerId,
256
+ params.instructions,
257
+ "inline",
258
+ signal,
259
+ createInlineSettlementListener(onUpdate),
260
+ );
261
+ const readable = completedRunDetails(completedRun);
278
262
  return {
279
263
  content: [
280
264
  {
281
265
  type: "text",
282
266
  text: readableDetails(
283
- `Completed inline wave ${readable.wave_id} with ${readable.results.length} result(s).`,
267
+ `Completed inline run ${readable.run_id}.`,
284
268
  readable,
285
269
  ),
286
270
  },
287
271
  ],
288
- details: completedWave,
272
+ details: readable,
289
273
  };
290
274
  },
291
275
  });
@@ -294,18 +278,16 @@ export function registerOrchestrationTools(
294
278
  name: "worker_abort",
295
279
  label: "Worker Abort",
296
280
  description:
297
- "Abort owned active work by worker IDs, wave ID, or all active owned workers. Use worker_close for ready reusable workers.",
298
- promptSnippet: "Abort active owned workers by worker IDs, wave ID, or all",
281
+ "Abort owned active work by worker IDs or all active owned workers. Use worker_close for ready reusable workers.",
282
+ promptSnippet: "Abort active owned workers by worker IDs or all",
299
283
  promptGuidelines: [
300
284
  "Use worker_abort only for active work; use worker_close for a ready reusable worker.",
301
285
  ],
302
286
  parameters: workerAbortSchema,
303
287
  renderCall(args, theme) {
304
- const target = "wave_id" in args
305
- ? args.wave_id
306
- : "worker_ids" in args
307
- ? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
308
- : "all workers";
288
+ const target = "worker_ids" in args
289
+ ? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
290
+ : "all workers";
309
291
  return renderCompactCall(theme, "worker_abort", target);
310
292
  },
311
293
  renderResult(result, { isPartial }, theme) {
@@ -326,7 +308,7 @@ export function registerOrchestrationTools(
326
308
  text: readableDetails("Abort request completed.", readable),
327
309
  },
328
310
  ],
329
- details: { target: target.runtime },
311
+ details: { target: target.external },
330
312
  };
331
313
  },
332
314
  });
@@ -361,7 +343,7 @@ export function registerOrchestrationTools(
361
343
  text: readableDetails(`Closed worker ${workerId}.`, readable),
362
344
  },
363
345
  ],
364
- details: { workerId },
346
+ details: readable,
365
347
  };
366
348
  },
367
349
  });
@@ -370,6 +352,7 @@ export function registerOrchestrationTools(
370
352
  async function buildRuntimeContext(
371
353
  ctx: ExtensionContext,
372
354
  deps: OrchestrationToolDependencies,
355
+ synthesisGroup?: DispatchDecision["synthesisGroup"],
373
356
  ): Promise<OrchestrationContext> {
374
357
  return {
375
358
  ownerSessionId: requireNonblank(
@@ -383,32 +366,22 @@ async function buildRuntimeContext(
383
366
  catalog: await deps.getCatalog(ctx),
384
367
  parentModel: ctx.model,
385
368
  modelRegistry: ctx.modelRegistry,
369
+ ...(synthesisGroup ? { synthesisGroup } : {}),
386
370
  };
387
371
  }
388
372
 
389
- function orchestrateWithMode(
390
- runtime: OrchestratorRuntime,
391
- context: OrchestrationContext,
392
- tasks: readonly OrchestrateTaskInput[],
393
- mode: "async" | "inline",
394
- signal: AbortSignal | undefined,
395
- onSettlement?: (settlement: unknown) => void,
396
- ): Promise<AcceptedWave | CompletedWave> {
397
- if (mode === "async") return runtime.orchestrate(context, tasks, "async");
398
- return runtime.orchestrate(context, tasks, "inline", signal, onSettlement);
399
- }
400
-
401
- function sendWithMode(
402
- runtime: OrchestratorRuntime,
403
- context: OrchestrationContext,
404
- workerId: WorkerId,
405
- instructions: string,
406
- mode: "async" | "inline",
407
- signal: AbortSignal | undefined,
408
- onSettlement?: (settlement: unknown) => void,
409
- ): Promise<AcceptedWave | CompletedWave> {
410
- if (mode === "async") return runtime.send(context, workerId, instructions, "async");
411
- return runtime.send(context, workerId, instructions, "inline", signal, onSettlement);
373
+ function createInlineSettlementListener(
374
+ onUpdate: ((result: AgentToolResult<unknown>) => void) | undefined,
375
+ ): SettlementListener {
376
+ return (settlement) => {
377
+ onUpdate?.({
378
+ content: [{ type: "text", text: "Worker response received." }],
379
+ details: {
380
+ mode: "inline",
381
+ result: inlineResultDetails(settlement),
382
+ },
383
+ });
384
+ };
412
385
  }
413
386
 
414
387
  function requireNonblank(name: string, value: string): string {
@@ -422,24 +395,15 @@ function asWorkerId(value: string): WorkerId {
422
395
  return requireNonblank("worker_id", value) as WorkerId;
423
396
  }
424
397
 
425
- function asWaveId(value: string): WaveId {
426
- return requireNonblank("wave_id", value) as WaveId;
427
- }
428
-
429
398
  function abortTarget(params: {
430
399
  worker_ids?: string[];
431
- wave_id?: string;
432
400
  all?: true;
433
401
  }): {
434
402
  runtime: AbortTarget;
435
- external:
436
- | { worker_ids: readonly WorkerId[] }
437
- | { wave_id: WaveId }
438
- | { all: true };
403
+ external: { worker_ids: readonly WorkerId[] } | { all: true };
439
404
  } {
440
405
  const selectedTargetCount = [
441
406
  params.worker_ids !== undefined,
442
- params.wave_id !== undefined,
443
407
  params.all !== undefined,
444
408
  ].filter(Boolean).length;
445
409
  if (selectedTargetCount !== 1 || (params.all !== undefined && params.all !== true)) {
@@ -456,33 +420,26 @@ function abortTarget(params: {
456
420
  external: { worker_ids: workerIds },
457
421
  };
458
422
  }
459
- if (params.wave_id !== undefined) {
460
- const waveId = asWaveId(params.wave_id);
461
- return {
462
- runtime: { waveId },
463
- external: { wave_id: waveId },
464
- };
465
- }
466
423
  return {
467
424
  runtime: { all: true },
468
425
  external: { all: true },
469
426
  };
470
427
  }
471
428
 
472
- function acceptedWaveDetails(wave: AcceptedWave) {
429
+ function acceptedRunDetails(run: AcceptedRun) {
473
430
  return {
474
431
  mode: "async" as const,
475
- wave_id: wave.id,
476
- worker_ids: [...wave.workerIds],
432
+ run_id: run.id,
433
+ worker_id: run.workerId,
477
434
  };
478
435
  }
479
436
 
480
- function completedWaveDetails(wave: CompletedWave) {
437
+ function completedRunDetails(run: CompletedRun) {
481
438
  return {
482
- mode: wave.mode,
483
- wave_id: wave.id,
484
- owner_session_id: wave.ownerSessionId,
485
- results: wave.results.map(completedResultDetails),
439
+ mode: run.mode,
440
+ run_id: run.id,
441
+ owner_session_id: run.ownerSessionId,
442
+ result: completedResultDetails(run.result),
486
443
  };
487
444
  }
488
445
 
@@ -494,18 +451,34 @@ function completedResultDetails(result: CompletedResult) {
494
451
  status: result.status,
495
452
  outcome: outcomeDetails(result.outcome),
496
453
  usage: usageDetails(result.usage),
454
+ started_at: result.startedAt,
455
+ settled_at: result.settledAt,
497
456
  session_file: result.sessionFile,
498
457
  };
499
458
  }
500
459
 
460
+ function inlineResultDetails(settlement: WorkerSettlement) {
461
+ return {
462
+ worker_id: settlement.workerId,
463
+ worker: settlement.worker,
464
+ title: settlement.title,
465
+ status: settlement.status,
466
+ outcome: outcomeDetails(settlement.outcome),
467
+ usage: usageDetails(settlement.usage),
468
+ started_at: settlement.startedAt,
469
+ settled_at: settlement.settledAt,
470
+ session_file: settlement.sessionFile,
471
+ };
472
+ }
473
+
501
474
  function statusDetails(catalog: WorkerCatalog, snapshot: RuntimeSnapshot) {
502
475
  return {
503
476
  catalog: {
504
477
  workers: catalog.workers.map(catalogWorkerDetails),
505
478
  diagnostics: catalog.diagnostics.map(diagnosticDetails),
506
479
  },
507
- snapshot: {
508
- waves: snapshot.waves.map(waveDetails),
480
+ state: {
481
+ runs: snapshot.runs.map(runDetails),
509
482
  workers: snapshot.workers.map(workerDetails),
510
483
  },
511
484
  };
@@ -545,14 +518,14 @@ function diagnosticDetails(diagnostic: CatalogDiagnostic) {
545
518
  };
546
519
  }
547
520
 
548
- function waveDetails(wave: WaveRecord) {
521
+ function runDetails(run: RunRecord) {
549
522
  return {
550
- wave_id: wave.id,
551
- owner_session_id: wave.ownerSessionId,
552
- worker_ids: [...wave.workerIds],
553
- mode: wave.mode,
554
- state: wave.state,
555
- created_at: wave.createdAt,
523
+ run_id: run.id,
524
+ owner_session_id: run.ownerSessionId,
525
+ worker_id: run.workerId,
526
+ mode: run.mode,
527
+ state: run.state,
528
+ created_at: run.createdAt,
556
529
  };
557
530
  }
558
531
 
@@ -561,7 +534,7 @@ function workerDetails(worker: WorkerRecord) {
561
534
  worker_id: worker.id,
562
535
  worker: worker.worker,
563
536
  owner_session_id: worker.ownerSessionId,
564
- wave_id: worker.waveId,
537
+ run_id: worker.runId,
565
538
  title: worker.title,
566
539
  lifecycle: worker.lifecycle,
567
540
  status: worker.status,
@@ -620,54 +593,48 @@ interface RenderableTask {
620
593
 
621
594
  function renderDispatchCall(
622
595
  theme: Theme,
623
- tasks: readonly RenderableTask[] | undefined,
596
+ task: RenderableTask,
624
597
  expanded: boolean,
625
598
  ): Component {
626
599
  const container = new Container();
627
- const renderableTasks = Array.isArray(tasks) ? tasks : [];
628
- const count = renderableTasks.length;
629
600
  container.addChild(new Text(
630
- theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", `${count} worker${count === 1 ? "" : "s"}`),
601
+ theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(task.worker)),
602
+ 0, 0,
603
+ ));
604
+ container.addChild(new Text(
605
+ `${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`,
631
606
  0, 0,
632
607
  ));
633
608
  if (expanded) {
634
- for (const task of renderableTasks) {
635
- container.addChild(new Spacer(1));
636
- container.addChild(new Text(`${theme.fg("accent", "→")} ${theme.fg("muted", safeTerminalText(task.worker))} · ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`, 0, 0));
637
- container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
638
- }
609
+ container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
639
610
  return new WidthBoundComponent(container);
640
611
  }
641
- container.addChild(new InstructionPreview(renderableTasks, theme));
612
+ container.addChild(new InstructionPreview(task.instructions, theme));
642
613
  container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
643
614
  return new WidthBoundComponent(container);
644
615
  }
645
616
 
646
617
  class InstructionPreview implements Component {
647
618
  constructor(
648
- private readonly tasks: readonly RenderableTask[],
619
+ private readonly instructions: unknown,
649
620
  private readonly theme: Theme,
650
621
  ) {}
651
622
  render(width: number): string[] {
652
623
  const bounded = Math.max(1, width);
653
- const lines: string[] = [];
654
- for (const task of this.tasks) {
655
- const heading = `${this.theme.fg("accent", "→")} ${this.theme.fg("muted", safeTerminalText(task.worker))} · ${this.theme.fg("text", this.theme.bold(safeTerminalText(task.title)))}`;
656
- lines.push(truncateToWidth(heading, bounded, "…"));
657
-
658
- const contentWidth = Math.max(1, bounded - 2);
659
- const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
660
- const preview = compactInstructionPreview(task.instructions, characterLimit);
661
- if (!preview.text) continue;
662
- const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
663
- const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
664
- if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
665
- const lastIndex = previewLines.length - 1;
666
- previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
667
- }
668
- for (const line of previewLines) lines.push(this.theme.fg("dim", ` ${line}`));
624
+ const contentWidth = Math.max(1, bounded - 2);
625
+ const characterLimit = Math.max(256, Math.min(4096, contentWidth * 3));
626
+ const preview = compactInstructionPreview(this.instructions, characterLimit);
627
+ if (!preview.text) return [];
628
+
629
+ const wrapped = wrapTextWithAnsi(preview.text, contentWidth);
630
+ const previewLines = wrapped.slice(0, MAX_INSTRUCTION_PREVIEW_LINES);
631
+ if (preview.truncated || wrapped.length > MAX_INSTRUCTION_PREVIEW_LINES) {
632
+ const lastIndex = previewLines.length - 1;
633
+ previewLines[lastIndex] = truncateToWidth(`${previewLines[lastIndex] ?? ""}…`, contentWidth, "…");
669
634
  }
670
- return lines.map((line) => truncateToWidth(line, bounded, "…"));
635
+ return previewLines.map((line) =>
636
+ truncateToWidth(this.theme.fg("dim", ` ${line}`), bounded, "…")
637
+ );
671
638
  }
672
639
  invalidate(): void {}
673
640
  }
@@ -744,19 +711,18 @@ function renderOrchestrationResult(
744
711
  lastComponent: unknown,
745
712
  ): Component {
746
713
  const details = result.details;
747
- if (isRecord(details) && typeof details.id === "string" && Array.isArray(details.workerIds) && details.workerIds.every((id) => typeof id === "string")) {
748
- const count = details.workerIds.length;
749
- return new WidthBoundComponent(new Text(theme.fg("success", `Sent to ${count} worker${count === 1 ? "" : "s"}`) + theme.fg("dim", " · responses arrive as they complete"), 0, 0));
714
+ if (isRecord(details) && typeof details.run_id === "string" && typeof details.worker_id === "string" && details.mode === "async") {
715
+ return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
750
716
  }
751
- const settlements = inlineSettlements(details);
752
- if (settlements.length > 0) {
717
+ const inlineResult = readInlineResult(details);
718
+ if (inlineResult) {
753
719
  const component = lastComponent instanceof InlineResultComponent
754
720
  ? lastComponent
755
721
  : new InlineResultComponent(theme);
756
- component.update(settlements, isPartial, expanded);
722
+ component.update(inlineResult, isPartial, expanded);
757
723
  return component;
758
724
  }
759
- if (isRecord(details) && (Array.isArray(details.settlements) || Array.isArray(details.results) || "workerIds" in details)) {
725
+ if (isRecord(details) && ("result" in details || "worker_id" in details)) {
760
726
  return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
761
727
  }
762
728
  if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
@@ -768,16 +734,17 @@ interface InlineSettlement {
768
734
  title: string;
769
735
  status: "completed" | "ready" | "failed" | "aborted";
770
736
  response: string;
737
+ elapsed?: string;
771
738
  }
772
739
 
773
740
  class InlineResultComponent implements Component {
774
- private settlements: readonly InlineSettlement[] = [];
741
+ private result: InlineSettlement | undefined;
775
742
  private partial = false;
776
743
  private expanded = false;
777
744
  private child: Component = new Container();
778
745
  constructor(private readonly theme: Theme) {}
779
- update(settlements: readonly InlineSettlement[], partial: boolean, expanded: boolean): void {
780
- this.settlements = settlements;
746
+ update(result: InlineSettlement, partial: boolean, expanded: boolean): void {
747
+ this.result = result;
781
748
  this.partial = partial;
782
749
  this.expanded = expanded;
783
750
  this.rebuild();
@@ -788,33 +755,35 @@ class InlineResultComponent implements Component {
788
755
  private rebuild(): void {
789
756
  (this.child as Component & { dispose?: () => void }).dispose?.();
790
757
  const container = new Container();
791
- for (const settlement of this.settlements) {
792
- const failed = settlement.status === "failed";
793
- const aborted = settlement.status === "aborted";
794
- const color = failed ? "error" : aborted ? "warning" : "success";
795
- const icon = failed ? "✗" : aborted ? "■" : "✓";
796
- container.addChild(new WidthBoundComponent(new Text(this.theme.fg(color, this.theme.bold(`${icon} ${settlement.worker} · ${settlement.title} · ${settlement.status}`)), 0, 0), 1));
797
- if (settlement.response) {
798
- const markdown = new Markdown(settlement.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
799
- container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
800
- }
801
- container.addChild(new Spacer(1));
758
+ const result = this.result;
759
+ if (!result) {
760
+ this.child = container;
761
+ return;
762
+ }
763
+ const appearance = inlineResultAppearance(result.status);
764
+ const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
765
+ const title = this.theme.bold(result.title);
766
+ const workerType = this.theme.fg("muted", this.theme.italic(result.worker));
767
+ const header = [
768
+ this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
769
+ workerType,
770
+ ...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
771
+ ].join(" · ");
772
+ container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
773
+ if (result.response) {
774
+ const markdown = new Markdown(result.response, this.expanded ? 2 : 0, 0, getMarkdownTheme());
775
+ container.addChild(new WidthBoundComponent(markdown, this.expanded ? undefined : 2));
802
776
  }
803
- if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Waiting for remaining workers…"), 0, 0));
804
- else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full responses")), 0, 0));
777
+ container.addChild(new Spacer(1));
778
+ if (this.partial) container.addChild(new Text(this.theme.fg("warning", "Receiving worker response…"), 0, 0));
779
+ else if (!this.expanded) container.addChild(new Text(this.theme.fg("dim", keyHint("app.tools.expand", "to inspect full response")), 0, 0));
805
780
  this.child = container;
806
781
  }
807
782
  }
808
783
 
809
- function inlineSettlements(details: unknown): InlineSettlement[] {
810
- if (!isRecord(details)) return [];
811
- const values = Array.isArray(details.settlements) ? details.settlements : Array.isArray(details.results) ? details.results : [];
812
- const parsed: InlineSettlement[] = [];
813
- for (const value of values) {
814
- const settlement = readInlineSettlement(value);
815
- if (settlement) parsed.push(settlement);
816
- }
817
- return parsed;
784
+ function readInlineResult(details: unknown): InlineSettlement | undefined {
785
+ if (!isRecord(details)) return undefined;
786
+ return readInlineSettlement(details.result);
818
787
  }
819
788
 
820
789
  function readInlineSettlement(value: unknown): InlineSettlement | undefined {
@@ -825,27 +794,50 @@ function readInlineSettlement(value: unknown): InlineSettlement | undefined {
825
794
  const outcomeStatus = statuses.find((item) => item === outcome.status);
826
795
  if (!status || outcomeStatus !== status) return undefined;
827
796
  const message = outcome.message;
828
- const camelAssistant = outcome.assistantText;
829
- const snakeAssistant = outcome.assistant_text;
797
+ const assistantText = outcome.assistant_text;
830
798
  if (message !== undefined && typeof message !== "string") return undefined;
831
- if (camelAssistant !== undefined && typeof camelAssistant !== "string") return undefined;
832
- if (snakeAssistant !== undefined && typeof snakeAssistant !== "string") return undefined;
833
- const assistantText = typeof camelAssistant === "string" ? camelAssistant : snakeAssistant;
799
+ if (assistantText !== undefined && typeof assistantText !== "string") return undefined;
834
800
  if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
835
801
  if (status === "failed" && typeof message !== "string") return undefined;
802
+ const startedAt = value.started_at;
803
+ const settledAt = value.settled_at;
804
+ const elapsed = typeof startedAt === "number" && typeof settledAt === "number" && settledAt >= startedAt
805
+ ? formatElapsed(settledAt - startedAt)
806
+ : undefined;
836
807
  return {
837
808
  worker: value.worker,
838
809
  title: value.title,
839
810
  status,
840
811
  response: [message, assistantText].filter((item): item is string => typeof item === "string" && item.length > 0).join("\n\n"),
812
+ ...(elapsed ? { elapsed } : {}),
841
813
  };
842
814
  }
843
815
 
816
+ function inlineResultAppearance(status: InlineSettlement["status"]): {
817
+ readonly color: "success" | "error" | "warning";
818
+ readonly icon: "✓" | "✗" | "■";
819
+ readonly qualifier?: string;
820
+ } {
821
+ if (status === "failed") return { color: "error", icon: "✗", qualifier: "failed" };
822
+ if (status === "aborted") return { color: "warning", icon: "■", qualifier: "aborted" };
823
+ if (status === "ready") {
824
+ return { color: "success", icon: "✓", qualifier: "ready for follow-up" };
825
+ }
826
+ return { color: "success", icon: "✓" };
827
+ }
828
+
829
+ function formatElapsed(milliseconds: number): string {
830
+ const seconds = Math.floor(milliseconds / 1000);
831
+ if (seconds < 60) return `${seconds}s`;
832
+ const minutes = Math.floor(seconds / 60);
833
+ return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
834
+ }
835
+
844
836
  function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
845
837
  if (isPartial) return new Text(theme.fg("muted", "Reading orchestration diagnostics…"), 0, 0);
846
838
  const details = result.details;
847
- if (isRecord(details) && isRecord(details.snapshot) && Array.isArray(details.snapshot.workers)) {
848
- const workers = details.snapshot.workers.filter(isRecord);
839
+ if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
840
+ const workers = details.state.workers.filter(isRecord);
849
841
  const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
850
842
  const ready = workers.filter((worker) => worker.status === "ready").length;
851
843
  const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;