@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.
- package/README.md +3 -0
- package/extension/catalog.ts +176 -157
- package/extension/domain.ts +5 -1
- package/extension/host.ts +2 -2
- package/extension/presentation.ts +139 -104
- package/extension/runtime.ts +150 -63
- package/extension/scheduler.ts +44 -25
- package/extension/tools.ts +63 -23
- package/extension/worker-session.ts +488 -295
- package/package.json +1 -1
package/extension/scheduler.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause,
|
|
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:
|
|
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
|
|
20
|
-
private readonly
|
|
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:
|
|
52
|
+
workflow: Effect.Effect<void, never>,
|
|
32
53
|
onDefect: WorkflowDefectHandler,
|
|
33
54
|
): void {
|
|
34
|
-
const supervised =
|
|
55
|
+
const supervised = workflow.pipe(
|
|
35
56
|
Effect.catchCause((cause) => {
|
|
36
57
|
if (!Cause.hasInterruptsOnly(cause)) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
FiberMap.run(this.fibers, key, supervised, { startImmediately: true }),
|
|
49
|
-
);
|
|
70
|
+
this.supervisor.start(key, supervised);
|
|
50
71
|
}
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
}
|
package/extension/tools.ts
CHANGED
|
@@ -4,7 +4,15 @@ import type {
|
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
Theme,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import {
|
|
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
|
|
623
|
+
tasks: readonly RenderableTask[] | undefined,
|
|
609
624
|
expanded: boolean,
|
|
610
625
|
): Component {
|
|
611
626
|
const container = new Container();
|
|
612
|
-
const
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
|
|
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:
|
|
650
|
-
instructions:
|
|
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:
|
|
676
|
-
|
|
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
|
|
683
|
-
|
|
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:
|
|
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:
|
|
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
|
);
|