@zachwill/pi-orchestrate 0.7.2 → 0.9.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.
@@ -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,
@@ -34,35 +36,70 @@ import {
34
36
  import type {
35
37
  AbortTarget,
36
38
  AcceptedRun,
37
- CompletedResult,
38
39
  CompletedRun,
39
40
  OrchestrationContext,
40
- OrchestratorRuntime,
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,7 +157,7 @@ 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
  },
@@ -130,7 +167,7 @@ export function registerOrchestrationTools(
130
167
  async execute(toolCallId, params, signal, onUpdate, ctx) {
131
168
  const decision = deps.getDispatchDecision(toolCallId);
132
169
  const mode = decision.mode;
133
- const runtimeContext = await buildRuntimeContext(ctx, deps, decision.synthesisGroup);
170
+ const runtimeContext = buildRuntimeContext(ctx, deps, decision.synthesisGroup);
134
171
  if (mode === "async") {
135
172
  const acceptedRun = await deps.runtime.orchestrate(
136
173
  runtimeContext,
@@ -140,13 +177,7 @@ export function registerOrchestrationTools(
140
177
  );
141
178
  const readable = acceptedRunDetails(acceptedRun);
142
179
  return {
143
- content: [
144
- {
145
- type: "text",
146
- text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
147
- },
148
- ],
149
- details: readable,
180
+ ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
150
181
  terminate: true,
151
182
  };
152
183
  }
@@ -159,18 +190,10 @@ export function registerOrchestrationTools(
159
190
  createInlineSettlementListener(onUpdate),
160
191
  );
161
192
  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
- };
193
+ return readableToolResult(
194
+ `Completed inline run ${readable.run_id}.`,
195
+ readable,
196
+ );
174
197
  },
175
198
  });
176
199
 
@@ -191,24 +214,14 @@ export function registerOrchestrationTools(
191
214
  return renderDiagnosticsResult(result, isPartial, theme);
192
215
  },
193
216
  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
- ]);
217
+ const ownerSessionId = ctx.sessionManager.getSessionId();
218
+ const catalog = deps.getCatalog(ctx);
219
+ const snapshot = await deps.runtime.snapshot(ownerSessionId);
202
220
  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
- };
221
+ return readableToolResult(
222
+ "Worker diagnostics and recovery snapshot.",
223
+ readable,
224
+ );
212
225
  },
213
226
  });
214
227
 
@@ -229,9 +242,9 @@ export function registerOrchestrationTools(
229
242
  return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
230
243
  },
231
244
  async execute(toolCallId, params, signal, onUpdate, ctx) {
232
- const workerId = asWorkerId(params.worker_id);
245
+ const workerId = params.worker_id;
233
246
  const mode = deps.getDispatchDecision(toolCallId).mode;
234
- const runtimeContext = await buildRuntimeContext(ctx, deps);
247
+ const runtimeContext = buildRuntimeContext(ctx, deps);
235
248
  if (mode === "async") {
236
249
  const acceptedRun = await deps.runtime.sendInteractive(
237
250
  runtimeContext,
@@ -242,13 +255,7 @@ export function registerOrchestrationTools(
242
255
  );
243
256
  const readable = acceptedRunDetails(acceptedRun);
244
257
  return {
245
- content: [
246
- {
247
- type: "text",
248
- text: readableDetails(`Accepted async run ${readable.run_id}.`, readable),
249
- },
250
- ],
251
- details: readable,
258
+ ...readableToolResult(`Accepted async run ${readable.run_id}.`, readable),
252
259
  terminate: true,
253
260
  };
254
261
  }
@@ -262,18 +269,10 @@ export function registerOrchestrationTools(
262
269
  createInlineSettlementListener(onUpdate),
263
270
  );
264
271
  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
- };
272
+ return readableToolResult(
273
+ `Completed inline run ${readable.run_id}.`,
274
+ readable,
275
+ );
277
276
  },
278
277
  });
279
278
 
@@ -297,22 +296,15 @@ export function registerOrchestrationTools(
297
296
  return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
298
297
  },
299
298
  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 },
299
+ const ownerSessionId = ctx.sessionManager.getSessionId();
300
+ const target = normalizeAbortTarget(params);
301
+ await deps.runtime.abort(ownerSessionId, target);
302
+ const readable = {
303
+ target: "worker_ids" in params
304
+ ? { worker_ids: params.worker_ids }
305
+ : { all: params.all },
315
306
  };
307
+ return readableToolResult("Abort request completed.", readable);
316
308
  },
317
309
  });
318
310
 
@@ -332,41 +324,27 @@ export function registerOrchestrationTools(
332
324
  return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
333
325
  },
334
326
  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);
327
+ const ownerSessionId = ctx.sessionManager.getSessionId();
328
+ const workerId = params.worker_id;
340
329
  await deps.runtime.closeInteractive(ownerSessionId, workerId);
341
330
  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
- };
331
+ return readableToolResult(`Closed worker ${workerId}.`, readable);
351
332
  },
352
333
  });
353
334
  }
354
335
 
355
- async function buildRuntimeContext(
336
+ function buildRuntimeContext(
356
337
  ctx: ExtensionContext,
357
338
  deps: OrchestrationToolDependencies,
358
339
  synthesisGroup?: DispatchDecision["synthesisGroup"],
359
- ): Promise<OrchestrationContext> {
340
+ ): OrchestrationContext {
360
341
  return {
361
- ownerSessionId: requireNonblank(
362
- "owner session ID",
363
- ctx.sessionManager.getSessionId(),
364
- ),
342
+ ownerSessionId: ctx.sessionManager.getSessionId(),
365
343
  cwd: ctx.cwd,
366
344
  agentDir: getAgentDir(),
367
345
  parentSessionFile: ctx.sessionManager.getSessionFile(),
368
346
  projectTrusted: ctx.isProjectTrusted(),
369
- catalog: await deps.getCatalog(ctx),
347
+ catalog: deps.getCatalog(ctx),
370
348
  parentModel: ctx.model,
371
349
  modelRegistry: ctx.modelRegistry,
372
350
  ...(synthesisGroup ? { synthesisGroup } : {}),
@@ -379,53 +357,21 @@ function createInlineSettlementListener(
379
357
  return (settlement) => {
380
358
  onUpdate?.({
381
359
  content: [{ type: "text", text: "Worker response received." }],
382
- details: {
360
+ details: encodeInlineWorkerToolDetails({
383
361
  mode: "inline",
384
- result: inlineResultDetails(settlement),
385
- },
362
+ result: inlineResultValue(settlement),
363
+ }),
386
364
  });
387
365
  };
388
366
  }
389
367
 
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: {
368
+ function normalizeAbortTarget(params: {
402
369
  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
- }
370
+ all?: boolean;
371
+ }): AbortTarget {
426
372
  return {
427
- runtime: { all: true },
428
- external: { all: true },
373
+ ...(params.worker_ids !== undefined ? { workerIds: params.worker_ids } : {}),
374
+ ...(params.all !== undefined ? { all: params.all } : {}),
429
375
  };
430
376
  }
431
377
 
@@ -438,39 +384,29 @@ function acceptedRunDetails(run: AcceptedRun) {
438
384
  }
439
385
 
440
386
  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
- };
387
+ return encodeInlineWorkerToolDetails({
388
+ mode: "inline",
389
+ runId: run.id,
390
+ ownerSessionId: run.ownerSessionId,
391
+ result: inlineResultValue(run.result),
392
+ });
447
393
  }
448
394
 
449
- function completedResultDetails(result: CompletedResult) {
395
+ function inlineResultValue(
396
+ result: RunResult | WorkerSettlement,
397
+ ): InlineWorkerSettlementDetails {
450
398
  return {
451
- worker_id: result.workerId,
399
+ workerId: result.workerId,
452
400
  worker: result.worker,
453
401
  title: result.title,
454
402
  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,
403
+ outcome: result.outcome,
404
+ usage: result.usage,
405
+ startedAt: result.startedAt,
406
+ settledAt: result.settledAt,
407
+ ...(result.sessionFile === undefined
408
+ ? {}
409
+ : { sessionFile: result.sessionFile }),
474
410
  };
475
411
  }
476
412
 
@@ -580,6 +516,13 @@ function outcomeDetails(outcome: WorkerOutcome) {
580
516
  }
581
517
  }
582
518
 
519
+ function readableToolResult<T>(title: string, details: T) {
520
+ return {
521
+ content: [{ type: "text" as const, text: readableDetails(title, details) }],
522
+ details,
523
+ };
524
+ }
525
+
583
526
  function readableDetails(title: string, details: unknown): string {
584
527
  const content = `${title}\n\n${JSON.stringify(details, null, 2)}`;
585
528
  const truncation = truncateHead(content);
@@ -659,18 +602,6 @@ function renderInteractiveMessageCall(
659
602
  return new WidthBoundComponent(container);
660
603
  }
661
604
 
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
605
  function safeTerminalText(value: unknown): string {
675
606
  const text = typeof value === "string" ? value : value == null ? "" : String(value);
676
607
  return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
@@ -714,7 +645,7 @@ function renderOrchestrationResult(
714
645
  lastComponent: unknown,
715
646
  ): Component {
716
647
  const details = result.details;
717
- if (isRecord(details) && typeof details.run_id === "string" && typeof details.worker_id === "string" && details.mode === "async") {
648
+ if (Result.isSuccess(decodeAcceptedRunRenderDetails(details))) {
718
649
  return new WidthBoundComponent(new Text(theme.fg("success", "Sent to worker") + theme.fg("dim", " · response arrives when complete"), 0, 0));
719
650
  }
720
651
  const inlineResult = readInlineResult(details);
@@ -725,28 +656,28 @@ function renderOrchestrationResult(
725
656
  component.update(inlineResult, isPartial, expanded);
726
657
  return component;
727
658
  }
728
- if (isRecord(details) && ("result" in details || "worker_id" in details)) {
659
+ if (Result.isSuccess(decodeUnavailableWorkerRenderDetails(details))) {
729
660
  return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
730
661
  }
731
662
  if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
732
663
  return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
733
664
  }
734
665
 
735
- interface InlineSettlement {
666
+ interface RenderedInlineSettlement {
736
667
  worker: string;
737
668
  title: string;
738
- status: "completed" | "ready" | "failed" | "aborted";
669
+ status: InlineWorkerSettlementDetails["status"];
739
670
  response: string;
740
671
  elapsed?: string;
741
672
  }
742
673
 
743
674
  class InlineResultComponent implements Component {
744
- private result: InlineSettlement | undefined;
675
+ private result: RenderedInlineSettlement | undefined;
745
676
  private partial = false;
746
677
  private expanded = false;
747
678
  private child: Component = new Container();
748
679
  constructor(private readonly theme: Theme) {}
749
- update(result: InlineSettlement, partial: boolean, expanded: boolean): void {
680
+ update(result: RenderedInlineSettlement, partial: boolean, expanded: boolean): void {
750
681
  this.result = result;
751
682
  this.partial = partial;
752
683
  this.expanded = expanded;
@@ -754,22 +685,22 @@ class InlineResultComponent implements Component {
754
685
  }
755
686
  render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
756
687
  invalidate(): void { this.rebuild(); }
757
- dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
688
+ dispose(): void { disposeComponent(this.child); }
758
689
  private rebuild(): void {
759
- (this.child as Component & { dispose?: () => void }).dispose?.();
690
+ disposeComponent(this.child);
760
691
  const container = new Container();
761
692
  const result = this.result;
762
693
  if (!result) {
763
694
  this.child = container;
764
695
  return;
765
696
  }
766
- const appearance = inlineResultAppearance(result.status);
697
+ const appearance = resultAppearance(result.status, "ready for follow-up");
767
698
  const suffix = [appearance.qualifier, result.elapsed].filter(Boolean).join(" · ");
768
699
  const title = this.theme.bold(result.title);
769
- const workerType = this.theme.fg("muted", this.theme.italic(result.worker));
700
+ const workerName = this.theme.fg("muted", this.theme.italic(result.worker));
770
701
  const header = [
771
702
  this.theme.fg(appearance.color, `${appearance.icon} ${title}`),
772
- workerType,
703
+ workerName,
773
704
  ...(suffix ? [this.theme.fg(appearance.color, suffix)] : []),
774
705
  ].join(" · ");
775
706
  container.addChild(new WidthBoundComponent(new Text(header, 0, 0), 1));
@@ -784,58 +715,27 @@ class InlineResultComponent implements Component {
784
715
  }
785
716
  }
786
717
 
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)
718
+ function readInlineResult(details: unknown): RenderedInlineSettlement | undefined {
719
+ const decoded = decodeInlineWorkerToolDetails(details);
720
+ if (Result.isFailure(decoded)) return undefined;
721
+ const settlement = decoded.success.result;
722
+ const outcome = settlement.outcome;
723
+ const message = outcome.status === "failed" || outcome.status === "aborted"
724
+ ? outcome.message
809
725
  : undefined;
726
+ const assistantText = outcome.assistantText;
727
+ const response = [message, assistantText]
728
+ .filter((item): item is string => typeof item === "string" && item.length > 0)
729
+ .join("\n\n");
810
730
  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 } : {}),
731
+ worker: settlement.worker,
732
+ title: settlement.title,
733
+ status: settlement.status,
734
+ response,
735
+ elapsed: formatElapsed(settlement.settledAt - settlement.startedAt),
816
736
  };
817
737
  }
818
738
 
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" };
828
- }
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
739
  function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
840
740
  if (isPartial) return new Text(theme.fg("muted", "Reading worker diagnostics…"), 0, 0);
841
741
  const details = result.details;
@@ -0,0 +1,50 @@
1
+ import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
2
+
3
+ export class WidthBoundComponent implements Component {
4
+ constructor(
5
+ private readonly child: Component,
6
+ private readonly maxLines?: number,
7
+ ) {}
8
+
9
+ render(width: number): string[] {
10
+ const bounded = Math.max(1, Math.floor(width));
11
+ const lines = this.child.render(bounded);
12
+ return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
13
+ .map((line) => truncateToWidth(line, bounded, "…"));
14
+ }
15
+
16
+ invalidate(): void { this.child.invalidate(); }
17
+ dispose(): void { disposeComponent(this.child); }
18
+ }
19
+
20
+ export function disposeComponent(component: Component): void {
21
+ (component as Component & { dispose?: () => void }).dispose?.();
22
+ }
23
+
24
+ export function resultAppearance(
25
+ status: "completed" | "ready" | "failed" | "aborted",
26
+ readyQualifier: string,
27
+ failedQualifier = "failed",
28
+ ): {
29
+ readonly color: "success" | "error" | "warning";
30
+ readonly icon: "✓" | "✗" | "■";
31
+ readonly qualifier?: string;
32
+ } {
33
+ if (status === "failed") {
34
+ return { color: "error", icon: "✗", qualifier: failedQualifier };
35
+ }
36
+ if (status === "aborted") {
37
+ return { color: "warning", icon: "■", qualifier: "aborted" };
38
+ }
39
+ if (status === "ready") {
40
+ return { color: "success", icon: "✓", qualifier: readyQualifier };
41
+ }
42
+ return { color: "success", icon: "✓" };
43
+ }
44
+
45
+ export function formatElapsed(milliseconds: number): string {
46
+ const seconds = Math.floor(milliseconds / 1000);
47
+ if (seconds < 60) return `${seconds}s`;
48
+ const minutes = Math.floor(seconds / 60);
49
+ return seconds % 60 === 0 ? `${minutes}m` : `${minutes}m ${seconds % 60}s`;
50
+ }