@zachwill/pi-orchestrate 0.1.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { Cause, Effect, Exit, FiberMap, Scope } from "effect";
1
+ import { Cause, Context, Effect, FiberMap, Layer, ManagedRuntime } from "effect";
2
2
 
3
3
  export type WorkflowDefectHandler = (error: unknown) => void;
4
4
 
@@ -6,7 +6,7 @@ export interface WorkflowScheduler<Key> {
6
6
  /** Starts a workflow immediately, interrupting and replacing the previous workflow at the key. */
7
7
  start(
8
8
  key: Key,
9
- workflow: () => Promise<void>,
9
+ workflow: Effect.Effect<void, never>,
10
10
  onDefect: WorkflowDefectHandler,
11
11
  ): void;
12
12
  /** Interrupts the current workflow at the key and waits for its fiber to settle. */
@@ -15,48 +15,67 @@ export interface WorkflowScheduler<Key> {
15
15
  close(): Promise<void>;
16
16
  }
17
17
 
18
+ interface WorkflowSupervisorService {
19
+ readonly start: (
20
+ key: unknown,
21
+ workflow: Effect.Effect<void, never>,
22
+ ) => void;
23
+ readonly remove: (key: unknown) => Effect.Effect<void>;
24
+ }
25
+
26
+ class WorkflowSupervisor extends Context.Service<
27
+ WorkflowSupervisor,
28
+ WorkflowSupervisorService
29
+ >()("@zachwill/pi-orchestrate/WorkflowSupervisor") {}
30
+
31
+ const workflowSupervisorLayer = Layer.effect(
32
+ WorkflowSupervisor,
33
+ Effect.gen(function* () {
34
+ const fibers = yield* FiberMap.make<unknown, void, never>();
35
+ const run = yield* FiberMap.runtime(fibers)<never>();
36
+ return WorkflowSupervisor.of({
37
+ start(key, workflow) {
38
+ run(key, workflow);
39
+ },
40
+ remove: (key) => FiberMap.remove(fibers, key),
41
+ });
42
+ }),
43
+ );
44
+
18
45
  class EffectWorkflowScheduler<Key> implements WorkflowScheduler<Key> {
19
- private readonly scope = Scope.makeUnsafe("parallel");
20
- private readonly fibers: FiberMap.FiberMap<Key, void, never>;
46
+ private readonly managedRuntime = ManagedRuntime.make(workflowSupervisorLayer);
47
+ private readonly supervisor = this.managedRuntime.runSync(WorkflowSupervisor);
21
48
  private closePromise: Promise<void> | undefined;
22
49
 
23
- constructor() {
24
- this.fibers = Effect.runSync(
25
- Scope.provide(this.scope)(FiberMap.make<Key, void, never>()),
26
- );
27
- }
28
-
29
50
  start(
30
51
  key: Key,
31
- workflow: () => Promise<void>,
52
+ workflow: Effect.Effect<void, never>,
32
53
  onDefect: WorkflowDefectHandler,
33
54
  ): void {
34
- const supervised = Effect.promise(workflow).pipe(
55
+ const supervised = workflow.pipe(
35
56
  Effect.catchCause((cause) => {
36
57
  if (!Cause.hasInterruptsOnly(cause)) {
37
- try {
38
- onDefect(Cause.squash(cause));
39
- } catch {
40
- // Defect reporting must not become another unsupervised defect.
41
- }
58
+ return Effect.sync(() => {
59
+ try {
60
+ onDefect(Cause.squash(cause));
61
+ } catch {
62
+ // Defect reporting must not become another unsupervised defect.
63
+ }
64
+ });
42
65
  }
43
66
  return Effect.void;
44
67
  }),
45
68
  );
46
69
 
47
- Effect.runSync(
48
- FiberMap.run(this.fibers, key, supervised, { startImmediately: true }),
49
- );
70
+ this.supervisor.start(key, supervised);
50
71
  }
51
72
 
52
- async remove(key: Key): Promise<void> {
53
- await Effect.runPromise(FiberMap.remove(this.fibers, key));
73
+ remove(key: Key): Promise<void> {
74
+ return this.managedRuntime.runPromise(this.supervisor.remove(key));
54
75
  }
55
76
 
56
77
  close(): Promise<void> {
57
- if (!this.closePromise) {
58
- this.closePromise = Effect.runPromise(Scope.close(this.scope, Exit.void));
59
- }
78
+ this.closePromise ??= this.managedRuntime.dispose();
60
79
  return this.closePromise;
61
80
  }
62
81
  }
@@ -1,10 +1,23 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
1
2
  import type {
2
3
  ExtensionAPI,
3
4
  ExtensionContext,
5
+ Theme,
4
6
  } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ Container,
9
+ Markdown,
10
+ Spacer,
11
+ Text,
12
+ truncateToWidth,
13
+ wrapTextWithAnsi,
14
+ type Component,
15
+ } from "@earendil-works/pi-tui";
5
16
  import {
6
17
  formatSize,
7
18
  getAgentDir,
19
+ getMarkdownTheme,
20
+ keyHint,
8
21
  truncateHead,
9
22
  } from "@earendil-works/pi-coding-agent";
10
23
  import { Type } from "typebox";
@@ -32,6 +45,7 @@ import type {
32
45
 
33
46
  const STRICT_OBJECT = { additionalProperties: false } as const;
34
47
  const MAX_TASKS_PER_WAVE = 12;
48
+ const MAX_INSTRUCTION_PREVIEW_LINES = 2;
35
49
 
36
50
  const taskSchema = Type.Object(
37
51
  {
@@ -110,15 +124,30 @@ export function registerOrchestrationTools(
110
124
  "Use orchestrate for one independent worker wave, with a complete brief for every task.",
111
125
  ],
112
126
  parameters: orchestrateSchema,
113
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
127
+ renderCall(args, theme, { expanded }) {
128
+ return renderDispatchCall(theme, args.tasks, expanded);
129
+ },
130
+ renderResult(result, { isPartial, expanded }, theme, context) {
131
+ return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
132
+ },
133
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
114
134
  const mode = deps.getDispatchMode(toolCallId);
115
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;
116
144
  const wave = await orchestrateWithMode(
117
145
  deps.runtime,
118
146
  runtimeContext,
119
147
  params.tasks,
120
148
  mode,
121
149
  signal,
150
+ onSettlement,
122
151
  );
123
152
 
124
153
  if (mode === "async") {
@@ -163,6 +192,12 @@ export function registerOrchestrationTools(
163
192
  "Use orchestration_status only for diagnostics or recovery; never poll it for completion.",
164
193
  ],
165
194
  parameters: statusSchema,
195
+ renderCall(_args, theme) {
196
+ return new Text(theme.fg("toolTitle", theme.bold("orchestration_status")), 0, 0);
197
+ },
198
+ renderResult(result, { isPartial }, theme) {
199
+ return renderDiagnosticsResult(result, isPartial, theme);
200
+ },
166
201
  async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
167
202
  const ownerSessionId = requireNonblank(
168
203
  "owner session ID",
@@ -195,10 +230,24 @@ export function registerOrchestrationTools(
195
230
  "Use worker_send only for follow-up work on an owned ready reusable worker.",
196
231
  ],
197
232
  parameters: workerSendSchema,
198
- async execute(toolCallId, params, signal, _onUpdate, ctx) {
233
+ renderCall(args, theme, { expanded }) {
234
+ return renderWorkerMessageCall(theme, "worker_send", args.worker_id, args.instructions, expanded);
235
+ },
236
+ renderResult(result, { isPartial, expanded }, theme, context) {
237
+ return renderOrchestrationResult(result, isPartial, expanded, theme, context.lastComponent);
238
+ },
239
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
199
240
  const workerId = asWorkerId(params.worker_id);
200
241
  const mode = deps.getDispatchMode(toolCallId);
201
242
  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;
202
251
  const wave = await sendWithMode(
203
252
  deps.runtime,
204
253
  runtimeContext,
@@ -206,6 +255,7 @@ export function registerOrchestrationTools(
206
255
  params.instructions,
207
256
  mode,
208
257
  signal,
258
+ onSettlement,
209
259
  );
210
260
 
211
261
  if (mode === "async") {
@@ -250,6 +300,17 @@ export function registerOrchestrationTools(
250
300
  "Use worker_abort only for active work; use worker_close for a ready reusable worker.",
251
301
  ],
252
302
  parameters: workerAbortSchema,
303
+ 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";
309
+ return renderCompactCall(theme, "worker_abort", target);
310
+ },
311
+ renderResult(result, { isPartial }, theme) {
312
+ return renderSimpleResult(result, isPartial ? "Requesting worker stop…" : "Worker stop requested", theme, "warning");
313
+ },
253
314
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
254
315
  const ownerSessionId = requireNonblank(
255
316
  "owner session ID",
@@ -279,6 +340,12 @@ export function registerOrchestrationTools(
279
340
  "Use worker_close when an owned ready reusable worker is finished.",
280
341
  ],
281
342
  parameters: workerCloseSchema,
343
+ renderCall(args, theme) {
344
+ return renderCompactCall(theme, "worker_close", args.worker_id);
345
+ },
346
+ renderResult(result, { isPartial }, theme) {
347
+ return renderSimpleResult(result, isPartial ? "Closing worker…" : "✓ Worker closed", theme);
348
+ },
282
349
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
283
350
  const ownerSessionId = requireNonblank(
284
351
  "owner session ID",
@@ -325,16 +392,10 @@ function orchestrateWithMode(
325
392
  tasks: readonly OrchestrateTaskInput[],
326
393
  mode: "async" | "inline",
327
394
  signal: AbortSignal | undefined,
395
+ onSettlement?: (settlement: unknown) => void,
328
396
  ): Promise<AcceptedWave | CompletedWave> {
329
397
  if (mode === "async") return runtime.orchestrate(context, tasks, "async");
330
-
331
- const orchestrateInline = runtime.orchestrate as unknown as (
332
- context: OrchestrationContext,
333
- tasks: readonly OrchestrateTaskInput[],
334
- mode: "inline",
335
- signal?: AbortSignal,
336
- ) => Promise<CompletedWave>;
337
- return orchestrateInline.call(runtime, context, tasks, "inline", signal);
398
+ return runtime.orchestrate(context, tasks, "inline", signal, onSettlement);
338
399
  }
339
400
 
340
401
  function sendWithMode(
@@ -344,17 +405,10 @@ function sendWithMode(
344
405
  instructions: string,
345
406
  mode: "async" | "inline",
346
407
  signal: AbortSignal | undefined,
408
+ onSettlement?: (settlement: unknown) => void,
347
409
  ): Promise<AcceptedWave | CompletedWave> {
348
410
  if (mode === "async") return runtime.send(context, workerId, instructions, "async");
349
-
350
- const sendInline = runtime.send as unknown as (
351
- context: OrchestrationContext,
352
- workerId: WorkerId,
353
- instructions: string,
354
- mode: "inline",
355
- signal?: AbortSignal,
356
- ) => Promise<CompletedWave>;
357
- return sendInline.call(runtime, context, workerId, instructions, "inline", signal);
411
+ return runtime.send(context, workerId, instructions, "inline", signal, onSettlement);
358
412
  }
359
413
 
360
414
  function requireNonblank(name: string, value: string): string {
@@ -467,7 +521,7 @@ function catalogWorkerDetails(worker: WorkerDefinition) {
467
521
  file_path: worker.source.filePath,
468
522
  },
469
523
  tools: [...worker.tools],
470
- skills: [...worker.skills],
524
+ skills: worker.skills === undefined ? undefined : [...worker.skills],
471
525
  model: worker.model
472
526
  ? { provider: worker.model.provider, model_id: worker.model.modelId }
473
527
  : undefined,
@@ -557,3 +611,266 @@ function readableDetails(title: string, details: unknown): string {
557
611
 
558
612
  return `${truncation.content}\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}. Full structured details remain available.]`;
559
613
  }
614
+
615
+ interface RenderableTask {
616
+ readonly worker?: unknown;
617
+ readonly title?: unknown;
618
+ readonly instructions?: unknown;
619
+ }
620
+
621
+ function renderDispatchCall(
622
+ theme: Theme,
623
+ tasks: readonly RenderableTask[] | undefined,
624
+ expanded: boolean,
625
+ ): Component {
626
+ const container = new Container();
627
+ const renderableTasks = Array.isArray(tasks) ? tasks : [];
628
+ const count = renderableTasks.length;
629
+ container.addChild(new Text(
630
+ theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", `${count} worker${count === 1 ? "" : "s"}`),
631
+ 0, 0,
632
+ ));
633
+ 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
+ }
639
+ return new WidthBoundComponent(container);
640
+ }
641
+ container.addChild(new InstructionPreview(renderableTasks, theme));
642
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
643
+ return new WidthBoundComponent(container);
644
+ }
645
+
646
+ class InstructionPreview implements Component {
647
+ constructor(
648
+ private readonly tasks: readonly RenderableTask[],
649
+ private readonly theme: Theme,
650
+ ) {}
651
+ render(width: number): string[] {
652
+ 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}`));
669
+ }
670
+ return lines.map((line) => truncateToWidth(line, bounded, "…"));
671
+ }
672
+ invalidate(): void {}
673
+ }
674
+
675
+ function renderWorkerMessageCall(
676
+ theme: Theme,
677
+ tool: string,
678
+ workerId: unknown,
679
+ instructions: unknown,
680
+ expanded: boolean,
681
+ ): Component {
682
+ const container = new Container();
683
+ container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
684
+ if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
685
+ else {
686
+ container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
687
+ container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full message")), 0, 0));
688
+ }
689
+ return new WidthBoundComponent(container);
690
+ }
691
+
692
+ class WidthBoundComponent implements Component {
693
+ constructor(private readonly child: Component, private readonly maxLines?: number) {}
694
+ render(width: number): string[] {
695
+ const bounded = Math.max(1, Math.floor(width));
696
+ const lines = this.child.render(bounded);
697
+ return (this.maxLines === undefined ? lines : lines.slice(0, this.maxLines))
698
+ .map((line) => truncateToWidth(line, bounded, "…"));
699
+ }
700
+ invalidate(): void { this.child.invalidate(); }
701
+ dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
702
+ }
703
+
704
+ function safeTerminalText(value: unknown): string {
705
+ const text = typeof value === "string" ? value : value == null ? "" : String(value);
706
+ return text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
707
+ const code = character.charCodeAt(0);
708
+ return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
709
+ });
710
+ }
711
+
712
+ function compactInstructionPreview(instructions: unknown, characterLimit: number): { text: string; truncated: boolean } {
713
+ const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
714
+ const source = text.slice(0, characterLimit);
715
+ return {
716
+ text: safeTerminalText(source).replace(/\s+/g, " ").trim(),
717
+ truncated: source.length < text.length,
718
+ };
719
+ }
720
+
721
+ function firstInstructionLine(instructions: unknown): string | undefined {
722
+ const text = typeof instructions === "string" ? instructions : instructions == null ? "" : String(instructions);
723
+ return text.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
724
+ }
725
+
726
+ function truncateInstruction(instructions: unknown, limit: number): string {
727
+ const first = firstInstructionLine(instructions) ?? "";
728
+ return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
729
+ }
730
+
731
+ function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
732
+ return new Text(
733
+ theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
734
+ 0,
735
+ 0,
736
+ );
737
+ }
738
+
739
+ function renderOrchestrationResult(
740
+ result: AgentToolResult<unknown>,
741
+ isPartial: boolean,
742
+ expanded: boolean,
743
+ theme: Theme,
744
+ lastComponent: unknown,
745
+ ): Component {
746
+ 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));
750
+ }
751
+ const settlements = inlineSettlements(details);
752
+ if (settlements.length > 0) {
753
+ const component = lastComponent instanceof InlineResultComponent
754
+ ? lastComponent
755
+ : new InlineResultComponent(theme);
756
+ component.update(settlements, isPartial, expanded);
757
+ return component;
758
+ }
759
+ if (isRecord(details) && (Array.isArray(details.settlements) || Array.isArray(details.results) || "workerIds" in details)) {
760
+ return new WidthBoundComponent(new Text(theme.fg("warning", "Worker result details unavailable"), 0, 0));
761
+ }
762
+ if (isPartial) return new WidthBoundComponent(new Text(theme.fg("warning", "Sending work…"), 0, 0));
763
+ return new WidthBoundComponent(renderSimpleResult(result, firstResultLine(result) || "Work sent", theme, "warning"));
764
+ }
765
+
766
+ interface InlineSettlement {
767
+ worker: string;
768
+ title: string;
769
+ status: "completed" | "ready" | "failed" | "aborted";
770
+ response: string;
771
+ }
772
+
773
+ class InlineResultComponent implements Component {
774
+ private settlements: readonly InlineSettlement[] = [];
775
+ private partial = false;
776
+ private expanded = false;
777
+ private child: Component = new Container();
778
+ constructor(private readonly theme: Theme) {}
779
+ update(settlements: readonly InlineSettlement[], partial: boolean, expanded: boolean): void {
780
+ this.settlements = settlements;
781
+ this.partial = partial;
782
+ this.expanded = expanded;
783
+ this.rebuild();
784
+ }
785
+ render(width: number): string[] { return new WidthBoundComponent(this.child).render(width); }
786
+ invalidate(): void { this.rebuild(); }
787
+ dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
788
+ private rebuild(): void {
789
+ (this.child as Component & { dispose?: () => void }).dispose?.();
790
+ 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));
802
+ }
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));
805
+ this.child = container;
806
+ }
807
+ }
808
+
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;
818
+ }
819
+
820
+ function readInlineSettlement(value: unknown): InlineSettlement | undefined {
821
+ if (!isRecord(value) || typeof value.worker !== "string" || typeof value.title !== "string" || !isRecord(value.outcome)) return undefined;
822
+ const outcome = value.outcome;
823
+ const statuses = ["completed", "ready", "failed", "aborted"] as const;
824
+ const status = statuses.find((item) => item === value.status);
825
+ const outcomeStatus = statuses.find((item) => item === outcome.status);
826
+ if (!status || outcomeStatus !== status) return undefined;
827
+ const message = outcome.message;
828
+ const camelAssistant = outcome.assistantText;
829
+ const snakeAssistant = outcome.assistant_text;
830
+ 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;
834
+ if ((status === "completed" || status === "ready") && typeof assistantText !== "string") return undefined;
835
+ if (status === "failed" && typeof message !== "string") return undefined;
836
+ return {
837
+ worker: value.worker,
838
+ title: value.title,
839
+ status,
840
+ response: [message, assistantText].filter((item): item is string => typeof item === "string" && item.length > 0).join("\n\n"),
841
+ };
842
+ }
843
+
844
+ function renderDiagnosticsResult(result: AgentToolResult<unknown>, isPartial: boolean, theme: Theme): Text {
845
+ if (isPartial) return new Text(theme.fg("muted", "Reading orchestration diagnostics…"), 0, 0);
846
+ const details = result.details;
847
+ if (isRecord(details) && isRecord(details.snapshot) && Array.isArray(details.snapshot.workers)) {
848
+ const workers = details.snapshot.workers.filter(isRecord);
849
+ const active = workers.filter((worker) => ["starting", "running", "stopping"].includes(String(worker.status))).length;
850
+ const ready = workers.filter((worker) => worker.status === "ready").length;
851
+ const diagnostics = isRecord(details.catalog) && Array.isArray(details.catalog.diagnostics) ? details.catalog.diagnostics.length : 0;
852
+ const facts = [active ? `${active} active` : "No active workers", ready ? `${ready} available for follow-up` : undefined, diagnostics ? `${diagnostics} catalog diagnostic${diagnostics === 1 ? "" : "s"}` : undefined].filter(Boolean);
853
+ return new Text(theme.fg("muted", facts.join(" · ")), 0, 0);
854
+ }
855
+ return new Text(theme.fg("muted", firstResultLine(result) || "Diagnostics unavailable"), 0, 0);
856
+ }
857
+
858
+ function renderSimpleResult(
859
+ result: AgentToolResult<unknown>,
860
+ message: string,
861
+ theme: Theme,
862
+ normalColor: "success" | "warning" = "success",
863
+ ): Text {
864
+ const failed = "isError" in result && result.isError === true;
865
+ return new Text(theme.fg(failed ? "error" : normalColor, failed ? firstResultLine(result) || message : message), 0, 0);
866
+ }
867
+
868
+ function firstResultLine(result: AgentToolResult<unknown>): string | undefined {
869
+ const first = result.content[0];
870
+ if (first?.type !== "text") return undefined;
871
+ return first.text.split("\n").find((line) => line.trim())?.trim();
872
+ }
873
+
874
+ function isRecord(value: unknown): value is Record<string, unknown> {
875
+ return typeof value === "object" && value !== null;
876
+ }