@g3un/pi-orchestra 0.1.0 → 0.1.1
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/package.json +1 -1
- package/src/adapters/in-memory-store.ts +25 -21
- package/src/core/orchestra.ts +17 -11
- package/src/core/store.ts +5 -2
- package/src/extension/index.ts +22 -1
- package/src/extension/workflow-monitor.ts +143 -0
- package/src/tools/workflow.ts +25 -12
package/README.md
CHANGED
|
@@ -15,6 +15,9 @@ pi -e npm:@g3un/pi-orchestra
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Pi-Orchestra registers four tools: `bus`, `subagent`, `workgroup`, and `workflow`.
|
|
18
|
+
Active workflows are also shown in a TUI progress widget with the current stage
|
|
19
|
+
and agent completion counts. Use `/orchestra-workflows` to reopen the widget if
|
|
20
|
+
needed.
|
|
18
21
|
|
|
19
22
|
## Core concepts
|
|
20
23
|
|
package/package.json
CHANGED
|
@@ -3,16 +3,21 @@ import type { Bus, BusMessage } from "../core/bus.ts";
|
|
|
3
3
|
import type { AgentStore } from "../core/store.ts";
|
|
4
4
|
import type { WorkflowRun } from "../core/workflow.ts";
|
|
5
5
|
|
|
6
|
+
interface StoreSubscription<T> {
|
|
7
|
+
listener(value: T): void;
|
|
8
|
+
filter?: (value: T) => boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export class InMemoryAgentStore implements AgentStore {
|
|
7
12
|
private readonly runs = new Map<string, AgentRun>();
|
|
8
13
|
private readonly buses = new Map<string, Bus>();
|
|
9
14
|
private readonly workflows = new Map<string, WorkflowRun>();
|
|
10
|
-
private readonly
|
|
11
|
-
private readonly
|
|
15
|
+
private readonly runSubscriptions = new Set<StoreSubscription<AgentRun>>();
|
|
16
|
+
private readonly workflowSubscriptions = new Set<StoreSubscription<WorkflowRun>>();
|
|
12
17
|
|
|
13
18
|
saveRun(run: AgentRun): void {
|
|
14
19
|
this.runs.set(run.id, run);
|
|
15
|
-
|
|
20
|
+
notifySubscribers(this.runSubscriptions, run);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
getRun(id: string): AgentRun | undefined {
|
|
@@ -23,15 +28,10 @@ export class InMemoryAgentStore implements AgentStore {
|
|
|
23
28
|
return [...this.runs.values()];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
this.
|
|
30
|
-
|
|
31
|
-
return () => {
|
|
32
|
-
listeners.delete(listener);
|
|
33
|
-
if (listeners.size === 0) this.runListeners.delete(id);
|
|
34
|
-
};
|
|
31
|
+
subscribeRuns(listener: (run: AgentRun) => void, filter?: (run: AgentRun) => boolean): () => void {
|
|
32
|
+
const subscription = { listener, filter };
|
|
33
|
+
this.runSubscriptions.add(subscription);
|
|
34
|
+
return () => this.runSubscriptions.delete(subscription);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
saveBus(bus: Bus): void {
|
|
@@ -61,7 +61,7 @@ export class InMemoryAgentStore implements AgentStore {
|
|
|
61
61
|
|
|
62
62
|
saveWorkflow(workflow: WorkflowRun): void {
|
|
63
63
|
this.workflows.set(workflow.id, workflow);
|
|
64
|
-
|
|
64
|
+
notifySubscribers(this.workflowSubscriptions, workflow);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
getWorkflow(id: string): WorkflowRun | undefined {
|
|
@@ -72,14 +72,18 @@ export class InMemoryAgentStore implements AgentStore {
|
|
|
72
72
|
return [...this.workflows.values()];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
subscribeWorkflows(
|
|
76
|
+
listener: (workflow: WorkflowRun) => void,
|
|
77
|
+
filter?: (workflow: WorkflowRun) => boolean,
|
|
78
|
+
): () => void {
|
|
79
|
+
const subscription = { listener, filter };
|
|
80
|
+
this.workflowSubscriptions.add(subscription);
|
|
81
|
+
return () => this.workflowSubscriptions.delete(subscription);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
85
|
+
function notifySubscribers<T>(subscriptions: Set<StoreSubscription<T>>, value: T): void {
|
|
86
|
+
for (const subscription of subscriptions) {
|
|
87
|
+
if (!subscription.filter || subscription.filter(value)) subscription.listener(value);
|
|
84
88
|
}
|
|
85
89
|
}
|
package/src/core/orchestra.ts
CHANGED
|
@@ -183,11 +183,14 @@ export class Orchestra implements OrchestraApi {
|
|
|
183
183
|
for (const run of initialRuns) {
|
|
184
184
|
if (isTerminalAgentState(run.state)) continue;
|
|
185
185
|
unsubscribeAll.push(
|
|
186
|
-
this.store.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
this.store.subscribeRuns(
|
|
187
|
+
(updatedRun) => {
|
|
188
|
+
if (settled) return;
|
|
189
|
+
latestRuns.set(updatedRun.id, updatedRun);
|
|
190
|
+
resolveIfDone();
|
|
191
|
+
},
|
|
192
|
+
(updatedRun) => updatedRun.id === run.id,
|
|
193
|
+
),
|
|
191
194
|
);
|
|
192
195
|
}
|
|
193
196
|
|
|
@@ -238,12 +241,15 @@ export class Orchestra implements OrchestraApi {
|
|
|
238
241
|
for (const run of initialRuns) {
|
|
239
242
|
if (isTerminalAgentState(run.state)) continue;
|
|
240
243
|
unsubscribeAll.push(
|
|
241
|
-
this.store.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
this.store.subscribeRuns(
|
|
245
|
+
(updatedRun) => {
|
|
246
|
+
if (settled) return;
|
|
247
|
+
latestRuns.set(updatedRun.id, updatedRun);
|
|
248
|
+
if (!excludedRunIds.has(updatedRun.id) && isTerminalAgentState(updatedRun.state))
|
|
249
|
+
resolveWithRun(updatedRun);
|
|
250
|
+
},
|
|
251
|
+
(updatedRun) => updatedRun.id === run.id,
|
|
252
|
+
),
|
|
247
253
|
);
|
|
248
254
|
}
|
|
249
255
|
});
|
package/src/core/store.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface AgentStore {
|
|
|
6
6
|
saveRun(run: AgentRun): void;
|
|
7
7
|
getRun(id: string): AgentRun | undefined;
|
|
8
8
|
listRuns(): AgentRun[];
|
|
9
|
-
|
|
9
|
+
subscribeRuns(listener: (run: AgentRun) => void, filter?: (run: AgentRun) => boolean): () => void;
|
|
10
10
|
|
|
11
11
|
saveBus(bus: Bus): void;
|
|
12
12
|
getBus(id: string): Bus | undefined;
|
|
@@ -17,5 +17,8 @@ export interface AgentStore {
|
|
|
17
17
|
saveWorkflow(workflow: WorkflowRun): void;
|
|
18
18
|
getWorkflow(id: string): WorkflowRun | undefined;
|
|
19
19
|
listWorkflows(): WorkflowRun[];
|
|
20
|
-
|
|
20
|
+
subscribeWorkflows(
|
|
21
|
+
listener: (workflow: WorkflowRun) => void,
|
|
22
|
+
filter?: (workflow: WorkflowRun) => boolean,
|
|
23
|
+
): () => void;
|
|
21
24
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -6,12 +6,14 @@ import { createBusTool, defineBusPiTool, type BusTool } from "../tools/bus.ts";
|
|
|
6
6
|
import { createSubagentTool, defineSubagentPiTool, type SubagentTool } from "../tools/subagent.ts";
|
|
7
7
|
import { createWorkflowTool, defineWorkflowPiTool, type WorkflowTool } from "../tools/workflow.ts";
|
|
8
8
|
import { createWorkgroupTool, defineWorkgroupPiTool, type WorkgroupTool } from "../tools/workgroup.ts";
|
|
9
|
+
import { WorkflowMonitorController } from "./workflow-monitor.ts";
|
|
9
10
|
|
|
10
11
|
interface ToolBundle {
|
|
11
12
|
busTool: BusTool;
|
|
12
13
|
subagentTool: SubagentTool;
|
|
13
14
|
workgroupTool: WorkgroupTool;
|
|
14
15
|
workflowTool: WorkflowTool;
|
|
16
|
+
workflowMonitor: WorkflowMonitorController;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export default function piOrchestraExtension(pi: ExtensionAPI): void {
|
|
@@ -21,7 +23,25 @@ export default function piOrchestraExtension(pi: ExtensionAPI): void {
|
|
|
21
23
|
pi.registerTool(defineBusPiTool((ctx) => getToolBundle(ctx).busTool));
|
|
22
24
|
pi.registerTool(defineSubagentPiTool((ctx) => getToolBundle(ctx).subagentTool));
|
|
23
25
|
pi.registerTool(defineWorkgroupPiTool((ctx) => getToolBundle(ctx).workgroupTool));
|
|
24
|
-
pi.registerTool(
|
|
26
|
+
pi.registerTool(
|
|
27
|
+
defineWorkflowPiTool((ctx) => getToolBundle(ctx).workflowTool, {
|
|
28
|
+
onWorkflowInput: (ctx) => getToolBundle(ctx).workflowMonitor.show(ctx),
|
|
29
|
+
onWorkflowOutput: (ctx) => getToolBundle(ctx).workflowMonitor.show(ctx),
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
pi.registerCommand("orchestra-workflows", {
|
|
34
|
+
description: "Show the active pi-orchestra workflow progress widget.",
|
|
35
|
+
handler: async (_args, ctx) => {
|
|
36
|
+
const monitor = getToolBundle(ctx).workflowMonitor;
|
|
37
|
+
if (monitor.show(ctx)) return;
|
|
38
|
+
ctx.ui.notify("No active pi-orchestra workflows.", "info");
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
43
|
+
bundles.get(ctx.cwd)?.workflowMonitor.dispose();
|
|
44
|
+
});
|
|
25
45
|
}
|
|
26
46
|
|
|
27
47
|
function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): ToolBundle {
|
|
@@ -40,6 +60,7 @@ function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): Too
|
|
|
40
60
|
subagentTool: createSubagentTool({ orchestra }),
|
|
41
61
|
workgroupTool: createWorkgroupTool({ orchestra }),
|
|
42
62
|
workflowTool: createWorkflowTool({ orchestra, store }),
|
|
63
|
+
workflowMonitor: new WorkflowMonitorController(store),
|
|
43
64
|
};
|
|
44
65
|
bundles.set(ctx.cwd, bundle);
|
|
45
66
|
return bundle;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentRun } from "../core/subagent.ts";
|
|
3
|
+
import type { AgentStore } from "../core/store.ts";
|
|
4
|
+
import type { WorkflowRun, WorkflowStageRun } from "../core/workflow.ts";
|
|
5
|
+
import { formatNamedEntityLabel, isTerminalAgentState } from "../utils.ts";
|
|
6
|
+
|
|
7
|
+
const WIDGET_KEY = "pi-orchestra.workflow-monitor";
|
|
8
|
+
const MAX_MONITORED_WORKFLOWS = 2;
|
|
9
|
+
const MAX_WIDGET_LINES = 10;
|
|
10
|
+
|
|
11
|
+
export class WorkflowMonitorController {
|
|
12
|
+
private unsubscribe?: () => void;
|
|
13
|
+
private ctx?: ExtensionContext;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly store: AgentStore) {}
|
|
16
|
+
|
|
17
|
+
hasActiveWorkflows(): boolean {
|
|
18
|
+
return listActiveWorkflows(this.store).length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
show(ctx: ExtensionContext): boolean {
|
|
22
|
+
if (!ctx.hasUI) return false;
|
|
23
|
+
|
|
24
|
+
this.ctx = ctx;
|
|
25
|
+
if (!this.unsubscribe) {
|
|
26
|
+
const unsubscribeRuns = this.store.subscribeRuns(() => this.render());
|
|
27
|
+
const unsubscribeWorkflows = this.store.subscribeWorkflows(() => this.render());
|
|
28
|
+
this.unsubscribe = () => {
|
|
29
|
+
unsubscribeRuns();
|
|
30
|
+
unsubscribeWorkflows();
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return this.render();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dispose(): void {
|
|
38
|
+
this.unsubscribe?.();
|
|
39
|
+
this.unsubscribe = undefined;
|
|
40
|
+
|
|
41
|
+
if (this.ctx?.hasUI) {
|
|
42
|
+
this.ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
43
|
+
}
|
|
44
|
+
this.ctx = undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private render(): boolean {
|
|
48
|
+
const ctx = this.ctx;
|
|
49
|
+
if (!ctx?.hasUI) return false;
|
|
50
|
+
|
|
51
|
+
const lines = buildWorkflowMonitorLines(this.store);
|
|
52
|
+
if (lines.length === 0) {
|
|
53
|
+
this.dispose();
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildWorkflowMonitorLines(store: AgentStore): string[] {
|
|
63
|
+
const workflows = listActiveWorkflows(store);
|
|
64
|
+
if (workflows.length === 0) return [];
|
|
65
|
+
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
for (const workflow of workflows.slice(0, MAX_MONITORED_WORKFLOWS)) {
|
|
68
|
+
appendWorkflowLines(lines, store, workflow);
|
|
69
|
+
if (lines.length >= MAX_WIDGET_LINES) break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hiddenWorkflowCount = workflows.length - MAX_MONITORED_WORKFLOWS;
|
|
73
|
+
if (hiddenWorkflowCount > 0 && lines.length < MAX_WIDGET_LINES) {
|
|
74
|
+
lines.push(`... +${hiddenWorkflowCount} more active ${pluralize("workflow", hiddenWorkflowCount)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.slice(0, MAX_WIDGET_LINES);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun): void {
|
|
81
|
+
if (lines.length >= MAX_WIDGET_LINES) return;
|
|
82
|
+
|
|
83
|
+
const stage = getCurrentStage(workflow);
|
|
84
|
+
const stageLabel = stage ? formatStageLabel(store, workflow, stage) : "none · agents 0/0";
|
|
85
|
+
lines.push(`${formatNamedEntityLabel(workflow)} | ${stageLabel}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function listActiveWorkflows(store: AgentStore): WorkflowRun[] {
|
|
89
|
+
return store.listWorkflows().filter((workflow) => !isTerminalAgentState(workflow.state));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getCurrentStage(workflow: WorkflowRun): WorkflowStageRun | undefined {
|
|
93
|
+
return (
|
|
94
|
+
workflow.stages[workflow.currentStageIndex] ?? workflow.stages.find((stage) => !isTerminalAgentState(stage.state))
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun): string {
|
|
99
|
+
const stageIndex = workflow.stages.indexOf(stage);
|
|
100
|
+
const stagePosition = stageIndex >= 0 ? `${stageIndex + 1}/${workflow.stages.length}` : `?/${workflow.stages.length}`;
|
|
101
|
+
return `${stage.name} · step ${stagePosition} · agents ${formatStageProgress(store, stage)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string {
|
|
105
|
+
const progress = calculateStageProgress(store, stage);
|
|
106
|
+
return `${progress.completed}/${progress.total}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function calculateStageProgress(store: AgentStore, stage: WorkflowStageRun): { completed: number; total: number } {
|
|
110
|
+
const runs = collectStageRuns(store, stage);
|
|
111
|
+
const completed = runs.filter((run) => isTerminalAgentState(run.state)).length;
|
|
112
|
+
const workerRunCount = runs.filter((run) => run.id !== stage.leaderRunId).length;
|
|
113
|
+
const workerTotal = Math.max(stage.members.length, stage.workerRunIds.length, workerRunCount);
|
|
114
|
+
const leaderTotal = stage.phase === "leader" || stage.leaderRunId !== undefined ? 1 : 0;
|
|
115
|
+
const total = Math.max(workerTotal + leaderTotal, runs.length);
|
|
116
|
+
return { completed: Math.min(completed, total), total };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function collectStageRuns(store: AgentStore, stage: WorkflowStageRun): AgentRun[] {
|
|
120
|
+
const runsById = new Map<string, AgentRun>();
|
|
121
|
+
|
|
122
|
+
for (const runId of stage.workerRunIds) {
|
|
123
|
+
const run = store.getRun(runId);
|
|
124
|
+
if (run) runsById.set(run.id, run);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (stage.leaderRunId) {
|
|
128
|
+
const leaderRun = store.getRun(stage.leaderRunId);
|
|
129
|
+
if (leaderRun) runsById.set(leaderRun.id, leaderRun);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (stage.busId) {
|
|
133
|
+
for (const run of store.listRuns().filter((current) => current.busId === stage.busId)) {
|
|
134
|
+
runsById.set(run.id, run);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return [...runsById.values()];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pluralize(noun: string, count: number): string {
|
|
142
|
+
return count === 1 ? noun : `${noun}s`;
|
|
143
|
+
}
|
package/src/tools/workflow.ts
CHANGED
|
@@ -65,6 +65,11 @@ export interface WorkflowToolDeps {
|
|
|
65
65
|
store: AgentStore;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export interface WorkflowPiToolOptions {
|
|
69
|
+
onWorkflowInput?: (ctx: ExtensionContext, input: WorkflowInput) => void;
|
|
70
|
+
onWorkflowOutput?: (ctx: ExtensionContext, output: WorkflowOutput) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
68
73
|
const WorkflowStageParams = Type.Object(
|
|
69
74
|
{
|
|
70
75
|
name: Type.String({
|
|
@@ -181,7 +186,10 @@ export function createWorkflowTool({ orchestra, store }: WorkflowToolDeps): Work
|
|
|
181
186
|
};
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
export function defineWorkflowPiTool(
|
|
189
|
+
export function defineWorkflowPiTool(
|
|
190
|
+
resolveTool: (ctx: ExtensionContext) => WorkflowTool,
|
|
191
|
+
options: WorkflowPiToolOptions = {},
|
|
192
|
+
) {
|
|
185
193
|
return defineTool({
|
|
186
194
|
name: "workflow",
|
|
187
195
|
label: "Workflow",
|
|
@@ -196,9 +204,11 @@ export function defineWorkflowPiTool(resolveTool: (ctx: ExtensionContext) => Wor
|
|
|
196
204
|
parameters: WorkflowToolParams,
|
|
197
205
|
executionMode: "sequential",
|
|
198
206
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
const input = withDefaultModels(toWorkflowInput(params as RawWorkflowParams), ctx);
|
|
208
|
+
options.onWorkflowInput?.(ctx, input);
|
|
209
|
+
|
|
210
|
+
const output = await resolveTool(ctx).execute(input);
|
|
211
|
+
if (output.workflow) options.onWorkflowOutput?.(ctx, output);
|
|
202
212
|
|
|
203
213
|
return {
|
|
204
214
|
content: [{ type: "text", text: output.message }],
|
|
@@ -646,15 +656,18 @@ function waitWorkflow(
|
|
|
646
656
|
unsubscribe();
|
|
647
657
|
};
|
|
648
658
|
|
|
649
|
-
const unsubscribe = store.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
659
|
+
const unsubscribe = store.subscribeWorkflows(
|
|
660
|
+
(workflow) => {
|
|
661
|
+
if (settled) return;
|
|
662
|
+
latestWorkflow = workflow;
|
|
663
|
+
if (!isTerminalAgentState(workflow.state)) return;
|
|
653
664
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
665
|
+
settled = true;
|
|
666
|
+
cleanup();
|
|
667
|
+
resolve({ workflow, timedOut: false });
|
|
668
|
+
},
|
|
669
|
+
(workflow) => workflow.id === workflowId,
|
|
670
|
+
);
|
|
658
671
|
|
|
659
672
|
if (resolvedTimeoutMs !== null) {
|
|
660
673
|
timeout = setTimeout(() => {
|