@zachwill/pi-orchestrate 0.1.1 → 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
  }
@@ -4,7 +4,15 @@ import type {
4
4
  ExtensionContext,
5
5
  Theme,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import { Container, Markdown, Spacer, Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
7
+ import {
8
+ Container,
9
+ Markdown,
10
+ Spacer,
11
+ Text,
12
+ truncateToWidth,
13
+ wrapTextWithAnsi,
14
+ type Component,
15
+ } from "@earendil-works/pi-tui";
8
16
  import {
9
17
  formatSize,
10
18
  getAgentDir,
@@ -37,6 +45,7 @@ import type {
37
45
 
38
46
  const STRICT_OBJECT = { additionalProperties: false } as const;
39
47
  const MAX_TASKS_PER_WAVE = 12;
48
+ const MAX_INSTRUCTION_PREVIEW_LINES = 2;
40
49
 
41
50
  const taskSchema = Type.Object(
42
51
  {
@@ -512,7 +521,7 @@ function catalogWorkerDetails(worker: WorkerDefinition) {
512
521
  file_path: worker.source.filePath,
513
522
  },
514
523
  tools: [...worker.tools],
515
- skills: [...worker.skills],
524
+ skills: worker.skills === undefined ? undefined : [...worker.skills],
516
525
  model: worker.model
517
526
  ? { provider: worker.model.provider, model_id: worker.model.modelId }
518
527
  : undefined,
@@ -603,42 +612,62 @@ function readableDetails(title: string, details: unknown): string {
603
612
  return `${truncation.content}\n\n[Output truncated: ${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}. Full structured details remain available.]`;
604
613
  }
605
614
 
615
+ interface RenderableTask {
616
+ readonly worker?: unknown;
617
+ readonly title?: unknown;
618
+ readonly instructions?: unknown;
619
+ }
620
+
606
621
  function renderDispatchCall(
607
622
  theme: Theme,
608
- tasks: readonly { worker: string; title: string; instructions: string }[],
623
+ tasks: readonly RenderableTask[] | undefined,
609
624
  expanded: boolean,
610
625
  ): Component {
611
626
  const container = new Container();
612
- const count = tasks.length;
627
+ const renderableTasks = Array.isArray(tasks) ? tasks : [];
628
+ const count = renderableTasks.length;
613
629
  container.addChild(new Text(
614
630
  theme.fg("toolTitle", theme.bold("orchestrate ")) + theme.fg("muted", `${count} worker${count === 1 ? "" : "s"}`),
615
631
  0, 0,
616
632
  ));
617
633
  if (expanded) {
618
- for (const task of tasks) {
634
+ for (const task of renderableTasks) {
619
635
  container.addChild(new Spacer(1));
620
- container.addChild(new Text(`${theme.fg("accent", "→")} ${theme.fg("muted", task.worker)} · ${theme.fg("text", theme.bold(task.title))}`, 0, 0));
636
+ container.addChild(new Text(`${theme.fg("accent", "→")} ${theme.fg("muted", safeTerminalText(task.worker))} · ${theme.fg("text", theme.bold(safeTerminalText(task.title)))}`, 0, 0));
621
637
  container.addChild(new Text(safeTerminalText(task.instructions), 2, 0));
622
638
  }
623
639
  return new WidthBoundComponent(container);
624
640
  }
625
- container.addChild(new InstructionPreview(tasks, theme));
641
+ container.addChild(new InstructionPreview(renderableTasks, theme));
626
642
  container.addChild(new Text(theme.fg("dim", keyHint("app.tools.expand", "to inspect full instructions")), 0, 0));
627
643
  return new WidthBoundComponent(container);
628
644
  }
629
645
 
630
646
  class InstructionPreview implements Component {
631
647
  constructor(
632
- private readonly tasks: readonly { worker: string; title: string; instructions: string }[],
648
+ private readonly tasks: readonly RenderableTask[],
633
649
  private readonly theme: Theme,
634
650
  ) {}
635
651
  render(width: number): string[] {
636
652
  const bounded = Math.max(1, width);
637
- return this.tasks.map((task) => {
638
- const preview = firstInstructionLine(task.instructions) ?? "";
639
- const row = `${this.theme.fg("accent", "→")} ${safeTerminalText(task.worker)} · ${safeTerminalText(task.title)}${preview ? ` — ${safeTerminalText(preview)}` : ""}`;
640
- return truncateToWidth(row, bounded, "…");
641
- });
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, "…"));
642
671
  }
643
672
  invalidate(): void {}
644
673
  }
@@ -646,12 +675,12 @@ class InstructionPreview implements Component {
646
675
  function renderWorkerMessageCall(
647
676
  theme: Theme,
648
677
  tool: string,
649
- workerId: string,
650
- instructions: string,
678
+ workerId: unknown,
679
+ instructions: unknown,
651
680
  expanded: boolean,
652
681
  ): Component {
653
682
  const container = new Container();
654
- container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", workerId), 0, 0));
683
+ container.addChild(new Text(theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(workerId)), 0, 0));
655
684
  if (expanded) container.addChild(new Text(safeTerminalText(instructions), 2, 0));
656
685
  else {
657
686
  container.addChild(new Text(`${theme.fg("accent", "→")} ${truncateInstruction(instructions, 240)}`, 0, 0));
@@ -672,25 +701,36 @@ class WidthBoundComponent implements Component {
672
701
  dispose(): void { (this.child as Component & { dispose?: () => void }).dispose?.(); }
673
702
  }
674
703
 
675
- function safeTerminalText(value: string): string {
676
- return value.replace(/\r\n?/g, "\n").replace(/\t/g, " ").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, (character) => {
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) => {
677
707
  const code = character.charCodeAt(0);
678
708
  return code === 0x7f ? "␡" : String.fromCodePoint(0x2400 + code);
679
709
  });
680
710
  }
681
711
 
682
- function firstInstructionLine(instructions: string): string | undefined {
683
- return instructions.split(/\r\n?|\n/).find((line) => line.trim().length > 0);
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);
684
724
  }
685
725
 
686
- function truncateInstruction(instructions: string, limit: number): string {
726
+ function truncateInstruction(instructions: unknown, limit: number): string {
687
727
  const first = firstInstructionLine(instructions) ?? "";
688
728
  return first.length > limit ? `${first.slice(0, limit - 1)}…` : first;
689
729
  }
690
730
 
691
- function renderCompactCall(theme: Theme, tool: string, target: string): Text {
731
+ function renderCompactCall(theme: Theme, tool: string, target: unknown): Text {
692
732
  return new Text(
693
- theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", target),
733
+ theme.fg("toolTitle", theme.bold(`${tool} `)) + theme.fg("muted", safeTerminalText(target)),
694
734
  0,
695
735
  0,
696
736
  );