@zachwill/pi-orchestrate 0.8.0 → 0.9.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.
@@ -20,13 +20,15 @@ import {
20
20
  keyHint,
21
21
  truncateHead,
22
22
  } from "@earendil-works/pi-coding-agent";
23
+ import { Result, Schema } from "effect";
23
24
  import { Type } from "typebox";
24
25
  import {
26
+ MAX_WORKER_INSTRUCTIONS_LENGTH,
27
+ MAX_WORKER_TITLE_LENGTH,
25
28
  type CatalogDiagnostic,
26
29
  type RunRecord,
27
30
  type WorkerCatalog,
28
31
  type WorkerDefinition,
29
- type WorkerId,
30
32
  type WorkerOutcome,
31
33
  type WorkerRecord,
32
34
  type WorkerUsage,
@@ -36,33 +38,68 @@ import type {
36
38
  AcceptedRun,
37
39
  CompletedRun,
38
40
  OrchestrationContext,
39
- OrchestratorRuntime,
40
41
  RunResult,
41
42
  RuntimeSnapshot,
42
43
  SettlementListener,
43
- WorkerSettlement,
44
44
  } from "./runtime.js";
45
+ import type { OrchestratorRuntime } from "./host.js";
46
+ import {
47
+ disposeComponent,
48
+ formatElapsed,
49
+ resultAppearance,
50
+ WidthBoundComponent,
51
+ } from "./tui.js";
52
+ import {
53
+ decodeInlineWorkerToolDetails,
54
+ encodeInlineWorkerToolDetails,
55
+ type InlineWorkerSettlementDetails,
56
+ WorkerSettlementDetails,
57
+ type WorkerSettlement,
58
+ } from "./worker-settlement.js";
45
59
 
46
60
  const STRICT_OBJECT = { additionalProperties: false } as const;
47
61
  const MAX_INSTRUCTION_PREVIEW_LINES = 2;
62
+ const shortTextSchema = Type.String({
63
+ pattern: "\\S",
64
+ maxLength: MAX_WORKER_TITLE_LENGTH,
65
+ });
66
+ const instructionsSchema = Type.String({
67
+ pattern: "\\S",
68
+ maxLength: MAX_WORKER_INSTRUCTIONS_LENGTH,
69
+ });
70
+ const workerIdSchema = Type.String({ pattern: "^worker-\\S+$" });
71
+
72
+ const AcceptedRunRenderDetails = Schema.Struct({
73
+ mode: Schema.Literal("async"),
74
+ run_id: WorkerSettlementDetails.fields.runId,
75
+ worker_id: WorkerSettlementDetails.fields.workerId,
76
+ });
77
+ const UnavailableWorkerRenderDetails = Schema.Union([
78
+ Schema.Struct({ result: Schema.Unknown }),
79
+ Schema.Struct({ worker_id: Schema.Unknown }),
80
+ ]);
81
+ const decodeAcceptedRunRenderDetails = Schema.decodeUnknownResult(
82
+ AcceptedRunRenderDetails,
83
+ );
84
+ const decodeUnavailableWorkerRenderDetails = Schema.decodeUnknownResult(
85
+ UnavailableWorkerRenderDetails,
86
+ );
48
87
 
49
88
  const taskSchema = Type.Object(
50
89
  {
51
- worker: Type.String(),
52
- title: Type.String(),
53
- instructions: Type.String(),
90
+ worker: shortTextSchema,
91
+ title: shortTextSchema,
92
+ instructions: instructionsSchema,
54
93
  },
55
94
  STRICT_OBJECT,
56
95
  );
57
96
 
58
- const orchestrateSchema = taskSchema;
59
-
60
97
  const statusSchema = Type.Object({}, STRICT_OBJECT);
61
98
 
62
99
  const interactiveSendSchema = Type.Object(
63
100
  {
64
- worker_id: Type.String({ minLength: 1 }),
65
- instructions: Type.String(),
101
+ worker_id: workerIdSchema,
102
+ instructions: instructionsSchema,
66
103
  },
67
104
  STRICT_OBJECT,
68
105
  );
@@ -70,7 +107,7 @@ const interactiveSendSchema = Type.Object(
70
107
  const workerAbortSchema = Type.Union([
71
108
  Type.Object(
72
109
  {
73
- worker_ids: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
110
+ worker_ids: Type.Array(workerIdSchema, { minItems: 1 }),
74
111
  },
75
112
  STRICT_OBJECT,
76
113
  ),
@@ -84,7 +121,7 @@ const workerAbortSchema = Type.Union([
84
121
 
85
122
  const interactiveCloseSchema = Type.Object(
86
123
  {
87
- worker_id: Type.String({ minLength: 1 }),
124
+ worker_id: workerIdSchema,
88
125
  },
89
126
  STRICT_OBJECT,
90
127
  );
@@ -99,7 +136,7 @@ export interface DispatchDecision {
99
136
 
100
137
  export interface OrchestrationToolDependencies {
101
138
  readonly runtime: OrchestratorRuntime;
102
- getCatalog(ctx: ExtensionContext): WorkerCatalog | Promise<WorkerCatalog>;
139
+ getCatalog(ctx: ExtensionContext): WorkerCatalog;
103
140
  getDispatchDecision(toolCallId: string): DispatchDecision;
104
141
  }
105
142
 
@@ -120,17 +157,24 @@ export function registerOrchestrationTools(
120
157
  "Form all N calls before emitting or finalizing the response. Never emit one call and wait for its result before forming the rest of the wave: a successfully admitted sole async orchestrate call returns terminate=true and ends the turn.",
121
158
  ],
122
159
  executionMode: "parallel",
123
- parameters: orchestrateSchema,
160
+ parameters: taskSchema,
124
161
  renderCall(args, theme, { expanded }) {
125
162
  return renderDispatchCall(theme, args, expanded);
126
163
  },
127
164
  renderResult(result, { isPartial, expanded }, theme, context) {
128
- return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
165
+ return renderOrchestrationResult(
166
+ result,
167
+ isPartial,
168
+ expanded,
169
+ theme,
170
+ context.isError,
171
+ context.lastComponent,
172
+ );
129
173
  },
130
174
  async execute(toolCallId, params, signal, onUpdate, ctx) {
131
175
  const decision = deps.getDispatchDecision(toolCallId);
132
176
  const mode = decision.mode;
133
- const runtimeContext = await buildRuntimeContext(ctx, deps, decision.synthesisGroup);
177
+ const runtimeContext = buildRuntimeContext(ctx, deps, decision.synthesisGroup);
134
178
  if (mode === "async") {
135
179
  const acceptedRun = await deps.runtime.orchestrate(
136
180
  runtimeContext,
@@ -140,13 +184,7 @@ export function registerOrchestrationTools(
140
184
  );
141
185
  const readable = acceptedRunDetails(acceptedRun);
142
186
  return {
143
- content: [
144
- {
145
- type: "text",
146
- text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
147
- },
148
- ],
149
- details: readable,
187
+ ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
150
188
  terminate: true,
151
189
  };
152
190
  }
@@ -159,18 +197,10 @@ export function registerOrchestrationTools(
159
197
  createInlineSettlementListener(onUpdate),
160
198
  );
161
199
  const readable = completedRunDetails(completedRun);
162
- return {
163
- content: [
164
- {
165
- type: "text",
166
- text: readableDetails(
167
- `Completed inline run ${readable.run_id}.`,
168
- readable,
169
- ),
170
- },
171
- ],
172
- details: readable,
173
- };
200
+ return readableToolResult(
201
+ `Completed inline run ${readable.run_id}.`,
202
+ readable,
203
+ );
174
204
  },
175
205
  });
176
206
 
@@ -187,28 +217,18 @@ export function registerOrchestrationTools(
187
217
  renderCall(_args, theme) {
188
218
  return new Text(theme.fg("toolTitle", theme.bold("worker_status")), 0, 0);
189
219
  },
190
- renderResult(result, { isPartial }, theme) {
191
- return renderDiagnosticsResult(result, isPartial, theme);
220
+ renderResult(result, { isPartial }, theme, context) {
221
+ return renderDiagnosticsResult(result, isPartial, context.isError, theme);
192
222
  },
193
223
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
194
- const ownerSessionId = requireNonblank(
195
- "owner session ID",
196
- ctx.sessionManager.getSessionId(),
197
- );
198
- const [catalog, snapshot] = await Promise.all([
199
- deps.getCatalog(ctx),
200
- deps.runtime.snapshot(ownerSessionId),
201
- ]);
224
+ const ownerSessionId = ctx.sessionManager.getSessionId();
225
+ const catalog = deps.getCatalog(ctx);
226
+ const snapshot = await deps.runtime.snapshot(ownerSessionId);
202
227
  const readable = statusDetails(catalog, snapshot);
203
- return {
204
- content: [
205
- {
206
- type: "text",
207
- text: readableDetails("Worker diagnostics and recovery snapshot.", readable),
208
- },
209
- ],
210
- details: readable,
211
- };
228
+ return readableToolResult(
229
+ "Worker diagnostics and recovery snapshot.",
230
+ readable,
231
+ );
212
232
  },
213
233
  });
214
234
 
@@ -223,15 +243,29 @@ export function registerOrchestrationTools(
223
243
  ],
224
244
  parameters: interactiveSendSchema,
225
245
  renderCall(args, theme, { expanded }) {
226
- return renderInteractiveMessageCall(theme, "interactive_send", args.worker_id, args.instructions, expanded);
246
+ const fields = recordFields(args);
247
+ return renderInteractiveMessageCall(
248
+ theme,
249
+ "interactive_send",
250
+ fields.worker_id,
251
+ fields.instructions,
252
+ expanded,
253
+ );
227
254
  },
228
255
  renderResult(result, { isPartial, expanded }, theme, context) {
229
- return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
256
+ return renderOrchestrationResult(
257
+ result,
258
+ isPartial,
259
+ expanded,
260
+ theme,
261
+ context.isError,
262
+ context.lastComponent,
263
+ );
230
264
  },
231
265
  async execute(toolCallId, params, signal, onUpdate, ctx) {
232
- const workerId = asWorkerId(params.worker_id);
266
+ const workerId = params.worker_id;
233
267
  const mode = deps.getDispatchDecision(toolCallId).mode;
234
- const runtimeContext = await buildRuntimeContext(ctx, deps);
268
+ const runtimeContext = buildRuntimeContext(ctx, deps);
235
269
  if (mode === "async") {
236
270
  const acceptedRun = await deps.runtime.sendInteractive(
237
271
  runtimeContext,
@@ -242,13 +276,7 @@ export function registerOrchestrationTools(
242
276
  );
243
277
  const readable = acceptedRunDetails(acceptedRun);
244
278
  return {
245
- content: [
246
- {
247
- type: "text",
248
- text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
249
- },
250
- ],
251
- details: readable,
279
+ ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
252
280
  terminate: true,
253
281
  };
254
282
  }
@@ -262,18 +290,10 @@ export function registerOrchestrationTools(
262
290
  createInlineSettlementListener(onUpdate),
263
291
  );
264
292
  const readable = completedRunDetails(completedRun);
265
- return {
266
- content: [
267
- {
268
- type: "text",
269
- text: readableDetails(
270
- `Completed inline run ${readable.run_id}.`,
271
- readable,
272
- ),
273
- },
274
- ],
275
- details: readable,
276
- };
293
+ return readableToolResult(
294
+ `Completed inline run ${readable.run_id}.`,
295
+ readable,
296
+ );
277
297
  },
278
298
  });
279
299
 
@@ -288,31 +308,34 @@ export function registerOrchestrationTools(
288
308
  ],
289
309
  parameters: workerAbortSchema,
290
310
  renderCall(args, theme) {
291
- const target = "worker_ids" in args
292
- ? `${args.worker_ids.length} worker${args.worker_ids.length === 1 ? "" : "s"}`
293
- : "all workers";
311
+ const fields = recordFields(args);
312
+ const workerIds = Array.isArray(fields.worker_ids)
313
+ ? fields.worker_ids
314
+ : undefined;
315
+ const target = workerIds
316
+ ? `${workerIds.length} worker${workerIds.length === 1 ? "" : "s"}`
317
+ : fields.all === true ? "all workers" : "";
294
318
  return renderCompactCall(theme, "worker_abort", target);
295
319
  },
296
- renderResult(result, { isPartial }, theme) {
297
- return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
320
+ renderResult(result, { isPartial }, theme, context) {
321
+ return renderSimpleResult(
322
+ result,
323
+ context.isError,
324
+ isPartial ? "Requesting worker stop…" : "Worker stop requested",
325
+ theme,
326
+ "warning",
327
+ );
298
328
  },
299
329
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
300
- const ownerSessionId = requireNonblank(
301
- "owner session ID",
302
- ctx.sessionManager.getSessionId(),
303
- );
304
- const target = abortTarget(params);
305
- await deps.runtime.abort(ownerSessionId, target.runtime);
306
- const readable = { target: target.external };
307
- return {
308
- content: [
309
- {
310
- type: "text",
311
- text: readableDetails("Abort request completed.", readable),
312
- },
313
- ],
314
- details: { target: target.external },
330
+ const ownerSessionId = ctx.sessionManager.getSessionId();
331
+ const target = normalizeAbortTarget(params);
332
+ await deps.runtime.abort(ownerSessionId, target);
333
+ const readable = {
334
+ target: "worker_ids" in params
335
+ ? { worker_ids: params.worker_ids }
336
+ : { all: params.all },
315
337
  };
338
+ return readableToolResult("Abort request completed.", readable);
316
339
  },
317
340
  });
318
341
 
@@ -326,47 +349,39 @@ export function registerOrchestrationTools(
326
349
  ],
327
350
  parameters: interactiveCloseSchema,
328
351
  renderCall(args, theme) {
329
- return renderCompactCall(theme, "interactive_close", args.worker_id);
352
+ const fields = recordFields(args);
353
+ return renderCompactCall(theme, "interactive_close", fields.worker_id);
330
354
  },
331
- renderResult(result, { isPartial }, theme) {
332
- return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
355
+ renderResult(result, { isPartial }, theme, context) {
356
+ return renderSimpleResult(
357
+ result,
358
+ context.isError,
359
+ isPartial ? "Closing worker…" : "✓ Worker closed",
360
+ theme,
361
+ );
333
362
  },
334
363
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
335
- const ownerSessionId = requireNonblank(
336
- "owner session ID",
337
- ctx.sessionManager.getSessionId(),
338
- );
339
- const workerId = asWorkerId(params.worker_id);
364
+ const ownerSessionId = ctx.sessionManager.getSessionId();
365
+ const workerId = params.worker_id;
340
366
  await deps.runtime.closeInteractive(ownerSessionId, workerId);
341
367
  const readable = { worker_id: workerId };
342
- return {
343
- content: [
344
- {
345
- type: "text",
346
- text: readableDetails(`Closed worker ${workerId}.`, readable),
347
- },
348
- ],
349
- details: readable,
350
- };
368
+ return readableToolResult(`Closed worker ${workerId}.`, readable);
351
369
  },
352
370
  });
353
371
  }
354
372
 
355
- async function buildRuntimeContext(
373
+ function buildRuntimeContext(
356
374
  ctx: ExtensionContext,
357
375
  deps: OrchestrationToolDependencies,
358
376
  synthesisGroup?: DispatchDecision["synthesisGroup"],
359
- ): Promise<OrchestrationContext> {
377
+ ): OrchestrationContext {
360
378
  return {
361
- ownerSessionId: requireNonblank(
362
- "owner session ID",
363
- ctx.sessionManager.getSessionId(),
364
- ),
379
+ ownerSessionId: ctx.sessionManager.getSessionId(),
365
380
  cwd: ctx.cwd,
366
381
  agentDir: getAgentDir(),
367
382
  parentSessionFile: ctx.sessionManager.getSessionFile(),
368
383
  projectTrusted: ctx.isProjectTrusted(),
369
- catalog: await deps.getCatalog(ctx),
384
+ catalog: deps.getCatalog(ctx),
370
385
  parentModel: ctx.model,
371
386
  modelRegistry: ctx.modelRegistry,
372
387
  ...(synthesisGroup ? { synthesisGroup } : {}),
@@ -379,53 +394,21 @@ function createInlineSettlementListener(
379
394
  return (settlement) => {
380
395
  onUpdate?.({
381
396
  content: [{ type: "text", text: "Worker response received." }],
382
- details: {
397
+ details: encodeInlineWorkerToolDetails({
383
398
  mode: "inline",
384
- result: inlineResultDetails(settlement),
385
- },
399
+ result: inlineResultValue(settlement),
400
+ }),
386
401
  });
387
402
  };
388
403
  }
389
404
 
390
- function requireNonblank(name: string, value: string): string {
391
- if (typeof value !== "string" || value.trim() === "") {
392
- throw new Error(`${name} must not be blank`);
393
- }
394
- return value;
395
- }
396
-
397
- function asWorkerId(value: string): WorkerId {
398
- return requireNonblank("worker_id", value) as WorkerId;
399
- }
400
-
401
- function abortTarget(params: {
405
+ function normalizeAbortTarget(params: {
402
406
  worker_ids?: string[];
403
- all?: true;
404
- }): {
405
- runtime: AbortTarget;
406
- external: { worker_ids: readonly WorkerId[] } | { all: true };
407
- } {
408
- const selectedTargetCount = [
409
- params.worker_ids !== undefined,
410
- params.all !== undefined,
411
- ].filter(Boolean).length;
412
- if (selectedTargetCount !== 1 || (params.all !== undefined && params.all !== true)) {
413
- throw new Error("Abort target must specify exactly one target");
414
- }
415
-
416
- if (params.worker_ids !== undefined) {
417
- if (!Array.isArray(params.worker_ids) || params.worker_ids.length === 0) {
418
- throw new Error("worker_ids must contain at least one worker ID");
419
- }
420
- const workerIds = params.worker_ids.map(asWorkerId);
421
- return {
422
- runtime: { workerIds },
423
- external: { worker_ids: workerIds },
424
- };
425
- }
407
+ all?: boolean;
408
+ }): AbortTarget {
426
409
  return {
427
- runtime: { all: true },
428
- external: { all: true },
410
+ ...(params.worker_ids !== undefined ? { workerIds: params.worker_ids } : {}),
411
+ ...(params.all !== undefined ? { all: params.all } : {}),
429
412
  };
430
413
  }
431
414
 
@@ -438,39 +421,29 @@ function acceptedRunDetails(run: AcceptedRun) {
438
421
  }
439
422
 
440
423
  function completedRunDetails(run: CompletedRun) {
441
- return {
442
- mode: run.mode,
443
- run_id: run.id,
444
- owner_session_id: run.ownerSessionId,
445
- result: completedResultDetails(run.result),
446
- };
424
+ return encodeInlineWorkerToolDetails({
425
+ mode: "inline",
426
+ runId: run.id,
427
+ ownerSessionId: run.ownerSessionId,
428
+ result: inlineResultValue(run.result),
429
+ });
447
430
  }
448
431
 
449
- function completedResultDetails(result: RunResult) {
432
+ function inlineResultValue(
433
+ result: RunResult | WorkerSettlement,
434
+ ): InlineWorkerSettlementDetails {
450
435
  return {
451
- worker_id: result.workerId,
436
+ workerId: result.workerId,
452
437
  worker: result.worker,
453
438
  title: result.title,
454
439
  status: result.status,
455
- outcome: outcomeDetails(result.outcome),
456
- usage: usageDetails(result.usage),
457
- started_at: result.startedAt,
458
- settled_at: result.settledAt,
459
- session_file: result.sessionFile,
460
- };
461
- }
462
-
463
- function inlineResultDetails(settlement: WorkerSettlement) {
464
- return {
465
- worker_id: settlement.workerId,
466
- worker: settlement.worker,
467
- title: settlement.title,
468
- status: settlement.status,
469
- outcome: outcomeDetails(settlement.outcome),
470
- usage: usageDetails(settlement.usage),
471
- started_at: settlement.startedAt,
472
- settled_at: settlement.settledAt,
473
- session_file: settlement.sessionFile,
440
+ outcome: result.outcome,
441
+ usage: result.usage,
442
+ startedAt: result.startedAt,
443
+ settledAt: result.settledAt,
444
+ ...(result.sessionFile === undefined
445
+ ? {}
446
+ : { sessionFile: result.sessionFile }),
474
447
  };
475
448
  }
476
449
 
@@ -580,6 +553,13 @@ function outcomeDetails(outcome: WorkerOutcome) {
580
553
  }
581
554
  }
582
555
 
556
+ function readableToolResult<T>(title: string, details: T) {
557
+ return {
558
+ content: [{ type: "text" as const, text: readableDetails(title, details) }],
559
+ details,
560
+ };
561
+ }
562
+
583
563
  function readableDetails(title: string, details: unknown): string {
584
564
  const content = `${title}\n\n${JSON.stringify(details, null, 2)}`;
585
565
  const truncation = truncateHead(content);
@@ -588,31 +568,26 @@ function readableDetails(title: string, details: unknown): string {
588
568
  return `${truncation.content}\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}. Full structured details remain available.]`;
589
569
  }
590
570
 
591
- interface RenderableTask {
592
- readonly worker?: unknown;
593
- readonly title?: unknown;
594
- readonly instructions?: unknown;
595
- }
596
-
597
571
  function renderDispatchCall(
598
572
  theme: Theme,
599
- task: RenderableTask,
573
+ task: unknown,
600
574
  expanded: boolean,
601
575
  ): Component {
576
+ const fields = isRecord(task) ? task : {};
602
577
  const container = new Container();
603
578
  container.addChild(new Text(
604
- theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(task.worker)),
579
+ theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", safeTerminalText(fields.worker)),
605
580
  0, 0,
606
581
  ));
607
582
  container.addChild(new Text(
608
- `${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`,
583
+ `${theme.fg("accent", "→")} ${theme.fg("text", theme.bold(safeTerminalText(fields.title)))}`,
609
584
  0, 0,
610
585
  ));
611
586
  if (expanded) {
612
- container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
587
+ container.addChild(new Text(safeTerminalText(fields.instructions), 2, 0));
613
588
  return new WidthBoundComponent(container);
614
589
  }
615
- container.addChild(new InstructionPreview(task.instructions, theme));
590
+ container.addChild(new InstructionPreview(fields.instructions, theme));
616
591
  container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
617
592
  return new WidthBoundComponent(container);
618
593
  }
@@ -659,18 +634,6 @@ function renderInteractiveMessageCall(
659
634
  return new WidthBoundComponent(container);
660
635
  }
661
636
 
662
- class WidthBoundComponent implements Component {
663
- constructor(private readonly child: Component, private readonly maxLines?: number) {}
664
- render(width: number): string[] {
665
- const bounded = Math.max(1, Math.floor(width));
666
- const lines = this.child.render(bounded);
667
- return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
668
- .map((line) => truncateToWidth(line, bounded, "…"));
669
- }
670
- invalidate(): void { this.child.invalidate(); }
671
- dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
672
- }
673
-
674
637
  function safeTerminalText(value: unknown): string {
675
638
  const text = typeof value === "string" ? value : value == null ? "" : String(value);
676
639
  return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
@@ -711,10 +674,20 @@ function renderOrchestrationResult(
711
674
  isPartial: boolean,
712
675
  expanded: boolean,
713
676
  theme: Theme,
677
+ isError: boolean,
714
678
  lastComponent: unknown,
715
679
  ): Component {
680
+ if (isError) {
681
+ return new WidthBoundComponent(renderSimpleResult(
682
+ result,
683
+ true,
684
+ firstResultLine(result) || "Worker operation failed",
685
+ theme,
686
+ "warning",
687
+ ));
688
+ }
716
689
  const details = result.details;
717
- if (isRecord(details) && typeof details.run_id === "string" && typeof details.worker_id === "string" && details.mode === "async") {
690
+ if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
718
691
  return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
719
692
  }
720
693
  const inlineResult = readInlineResult(details);
@@ -725,28 +698,34 @@ function renderOrchestrationResult(
725
698
  component.update(inlineResult, isPartial, expanded);
726
699
  return component;
727
700
  }
728
- if (isRecord(details) && ("result" in details || "worker_id" in details)) {
701
+ if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
729
702
  return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
730
703
  }
731
704
  if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
732
- return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
705
+ return new WidthBoundComponent(renderSimpleResult(
706
+ result,
707
+ false,
708
+ firstResultLine(result) || "Work sent",
709
+ theme,
710
+ "warning",
711
+ ));
733
712
  }
734
713
 
735
- interface InlineSettlement {
714
+ interface RenderedInlineSettlement {
736
715
  worker: string;
737
716
  title: string;
738
- status: "completed" | "ready" | "failed" | "aborted";
717
+ status: InlineWorkerSettlementDetails["status"];
739
718
  response: string;
740
719
  elapsed?: string;
741
720
  }
742
721
 
743
722
  class InlineResultComponent implements Component {
744
- private result: InlineSettlement | undefined;
723
+ private result: RenderedInlineSettlement | undefined;
745
724
  private partial = false;
746
725
  private expanded = false;
747
726
  private child: Component = new Container();
748
727
  constructor(private readonly theme: Theme) {}
749
- update(result: InlineSettlement, partial: boolean, expanded: boolean): void {
728
+ update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
750
729
  this.result = result;
751
730
  this.partial = partial;
752
731
  this.expanded = expanded;
@@ -754,16 +733,16 @@ class InlineResultComponent implements Component {
754
733
  }
755
734
  render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
756
735
  invalidate(): void { this.rebuild(); }
757
- dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
736
+ dispose(): void { disposeComponent(this.child); }
758
737
  private rebuild(): void {
759
- (this.child as Component & { dispose?: () => void }).dispose?.();
738
+ disposeComponent(this.child);
760
739
  const container = new Container();
761
740
  const result = this.result;
762
741
  if (!result) {
763
742
  this.child = container;
764
743
  return;
765
744
  }
766
- const appearance = inlineResultAppearance(result.status);
745
+ const appearance = resultAppearance(result.status, "ready for follow-up");
767
746
  const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
768
747
  const title = this.theme.bold(result.title);
769
748
  const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
@@ -784,59 +763,41 @@ class InlineResultComponent implements Component {
784
763
  }
785
764
  }
786
765
 
787
- function readInlineResult(details: unknown): InlineSettlement | undefined {
788
- if (!isRecord(details)) return undefined;
789
- return readInlineSettlement(details.result);
790
- }
791
-
792
- function readInlineSettlement(value: unknown): InlineSettlement | undefined {
793
- if (!isRecord(value) || typeof value.worker !== "string" || typeof value.title !== "string" || !isRecord(value.outcome)) return undefined;
794
- const outcome = value.outcome;
795
- const statuses = ["completed", "ready", "failed", "aborted"] as const;
796
- const status = statuses.find((item) => item === value.status);
797
- const outcomeStatus = statuses.find((item) => item === outcome.status);
798
- if (!status || outcomeStatus !== status) return undefined;
799
- const message = outcome.message;
800
- const assistantText = outcome.assistant_text;
801
- if (message !== undefined && typeof message !== "string") return undefined;
802
- if (assistantText !== undefined && typeof assistantText !== "string") return undefined;
803
- if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
804
- if (status === "failed" && typeof message !== "string") return undefined;
805
- const startedAt = value.started_at;
806
- const settledAt = value.settled_at;
807
- const elapsed = typeof startedAt === "number" && typeof settledAt === "number" && settledAt >= startedAt
808
- ? formatElapsed(settledAt - startedAt)
766
+ function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
767
+ const decoded = decodeInlineWorkerToolDetails(details);
768
+ if (Result.isFailure(decoded)) return undefined;
769
+ const settlement = decoded.success.result;
770
+ const outcome = settlement.outcome;
771
+ const message = outcome.status === "failed" || outcome.status === "aborted"
772
+ ? outcome.message
809
773
  : undefined;
774
+ const assistantText = outcome.assistantText;
775
+ const response = [message, assistantText]
776
+ .filter((item): item is string => typeof item === "string" && item.length > 0)
777
+ .join("\n\n");
810
778
  return {
811
- worker: value.worker,
812
- title: value.title,
813
- status,
814
- response: [message, assistantText].filter((item): item is string => typeof item === "string" && item.length > 0).join("\n\n"),
815
- ...(elapsed ? { elapsed } : {}),
779
+ worker: settlement.worker,
780
+ title: settlement.title,
781
+ status: settlement.status,
782
+ response,
783
+ elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
816
784
  };
817
785
  }
818
786
 
819
- function inlineResultAppearance(status: InlineSettlement["status"]): {
820
- readonly color: "success" | "error" | "warning";
821
- readonly icon: "✓" | "✗" | "■";
822
- readonly qualifier?: string;
823
- } {
824
- if (status === "failed") return { color: "error", icon: "✗", qualifier: "failed" };
825
- if (status === "aborted") return { color: "warning", icon: "■", qualifier: "aborted" };
826
- if (status === "ready") {
827
- return { color: "success", icon: "✓", qualifier: "ready for follow-up" };
787
+ function renderDiagnosticsResult(
788
+ result: AgentToolResult<unknown>,
789
+ isPartial: boolean,
790
+ isError: boolean,
791
+ theme: Theme,
792
+ ): Text {
793
+ if (isError) {
794
+ return renderSimpleResult(
795
+ result,
796
+ true,
797
+ firstResultLine(result) || "Worker diagnostics failed",
798
+ theme,
799
+ );
828
800
  }
829
- return { color: "success", icon: "✓" };
830
- }
831
-
832
- function formatElapsed(milliseconds: number): string {
833
- const seconds = Math.floor(milliseconds / 1000);
834
- if (seconds < 60) return `${seconds}s`;
835
- const minutes = Math.floor(seconds / 60);
836
- return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
837
- }
838
-
839
- function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
840
801
  if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
841
802
  const details = result.details;
842
803
  if (isRecord(details) && isRecord(details.state) && Array.isArray(details.state.workers)) {
@@ -852,12 +813,13 @@ function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: bo
852
813
 
853
814
  function renderSimpleResult(
854
815
  result: AgentToolResult<unknown>,
816
+ isError: boolean,
855
817
  message: string,
856
818
  theme: Theme,
857
819
  normalColor: "success" | "warning" = "success",
858
820
  ): Text {
859
- const failed = "isError" in result && result.isError === true;
860
- return new Text(theme.fg(failed ? "error" : normalColor, failed ? firstResultLine(result) || message : message), 0, 0);
821
+ const text = isError ? firstResultLine(result) || message : message;
822
+ return new Text(theme.fg(isError ? "error" : normalColor, text), 0, 0);
861
823
  }
862
824
 
863
825
  function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
@@ -866,6 +828,10 @@ function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
866
828
  return first.text.split("\n").find((line) => line.trim())?.trim();
867
829
  }
868
830
 
831
+ function recordFields(value: unknown): Record<string, unknown> {
832
+ return isRecord(value) ? value : {};
833
+ }
834
+
869
835
  function isRecord(value: unknown): value is Record<string, unknown> {
870
836
  return typeof value === "object" && value !== null;
871
837
  }