@g3un/pi-orchestra 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.
- package/README.md +8 -3
- package/docs/orchestration-model.md +8 -4
- package/package.json +12 -1
- package/src/adapters/in-memory-store.ts +25 -21
- package/src/core/orchestra.ts +1 -189
- package/src/core/store.ts +5 -2
- package/src/core/subagent.ts +8 -0
- package/src/extension/index.ts +55 -4
- package/src/extension/orchestra-events.ts +231 -0
- package/src/extension/workflow-monitor.ts +143 -0
- package/src/tools/bus.ts +9 -133
- package/src/tools/workflow.ts +53 -109
- package/src/tools/workgroup.ts +186 -71
- package/src/utils.ts +4 -14
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ pi -e npm:@g3un/pi-orchestra
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Pi-Orchestra registers four tools: `bus`, `subagent`, `workgroup`, and `workflow`.
|
|
18
|
+
Subagent, workgroup-member, and workflow completions are delivered back to the
|
|
19
|
+
main agent as pi-orchestra events, so the main conversation stays responsive
|
|
20
|
+
while delegated work runs. Active workflows are also shown in a TUI progress
|
|
21
|
+
widget with the current stage and agent completion counts. Use
|
|
22
|
+
`/orchestra-workflows` to reopen the widget if needed.
|
|
18
23
|
|
|
19
24
|
## Core concepts
|
|
20
25
|
|
|
@@ -26,7 +31,7 @@ Use subagents when you want to delegate a focused task, such as review, research
|
|
|
26
31
|
|
|
27
32
|
### Workgroup
|
|
28
33
|
|
|
29
|
-
A workgroup starts multiple subagents on the same bus for one shared goal. Each member can have a different profile or assignment.
|
|
34
|
+
A workgroup starts multiple subagents on the same bus for one shared goal. Each member can have a different profile or assignment. Member completions are delivered as `workgroup.member_finished` events with the strategy and pending run ids.
|
|
30
35
|
|
|
31
36
|
Workgroups support two strategies:
|
|
32
37
|
|
|
@@ -35,12 +40,12 @@ Workgroups support two strategies:
|
|
|
35
40
|
|
|
36
41
|
### Workflow
|
|
37
42
|
|
|
38
|
-
A workflow runs ordered workgroup stages. Each stage gets a fresh bus, starts its workers, collects results, and uses a stage leader to produce a canonical stage output.
|
|
43
|
+
A workflow runs ordered workgroup stages. Each stage gets a fresh bus, starts its workers, collects results through internal finish-event subscriptions, and uses a stage leader to produce a canonical stage output. The main agent receives a single `workflow.finished` event for the whole workflow.
|
|
39
44
|
|
|
40
45
|
Use workflows for multi-step plans where later stages should depend on the summarized output of earlier stages instead of raw worker transcripts.
|
|
41
46
|
|
|
42
47
|
## Notes
|
|
43
48
|
|
|
44
49
|
- Create a `bus` before spawning related subagents or workgroups.
|
|
45
|
-
- Subagents report completion with `success`, `blocked`, or `failed
|
|
50
|
+
- Subagents report completion with `success`, `blocked`, or `failed`; completions are surfaced as pi-orchestra events.
|
|
46
51
|
- Use `workflow` for linear staged work, not branching/DAG execution.
|
|
@@ -46,10 +46,12 @@ Strategies:
|
|
|
46
46
|
- `compete`: one successful member can be enough.
|
|
47
47
|
- `synthesize`: collect complementary findings and combine them.
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Main receives finish events instead of blocking on completion calls:
|
|
50
50
|
|
|
51
|
-
-
|
|
52
|
-
- `
|
|
51
|
+
- Standalone subagent completions arrive as `subagent.finished` events.
|
|
52
|
+
- Workgroup member completions arrive as `workgroup.member_finished` events with the strategy and pending run ids.
|
|
53
|
+
- For `compete`, a successful member may be enough; close pending losers when appropriate.
|
|
54
|
+
- For `synthesize`, use each member event to decide whether to steer active members, spawn follow-up work, publish more context, or continue collecting results.
|
|
53
55
|
|
|
54
56
|
## Workflow
|
|
55
57
|
|
|
@@ -60,10 +62,12 @@ For each stage:
|
|
|
60
62
|
|
|
61
63
|
1. Create a fresh bus.
|
|
62
64
|
2. Spawn the worker workgroup.
|
|
63
|
-
3. Collect worker results.
|
|
65
|
+
3. Collect worker results through store finish-event subscriptions in the background.
|
|
64
66
|
4. Spawn a restricted stage leader.
|
|
65
67
|
5. Store the leader's canonical output as the stage output.
|
|
66
68
|
|
|
69
|
+
Workflow-internal worker and leader completions are consumed by the workflow runner. Main receives a single `workflow.finished` event when the whole workflow reaches `success`, `blocked`, `failed`, or `closed`.
|
|
70
|
+
|
|
67
71
|
The next stage receives the previous stage output, not raw worker transcripts.
|
|
68
72
|
If no leader is provided, `createStageLeaderProfile` supplies a restricted
|
|
69
73
|
leader with no tools.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@g3un/pi-orchestra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Subagent orchestration tools for Pi.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"orchestration",
|
|
@@ -9,8 +9,16 @@
|
|
|
9
9
|
"pi-package",
|
|
10
10
|
"subagent"
|
|
11
11
|
],
|
|
12
|
+
"homepage": "https://codeberg.org/g3un/pi-orchestra",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://codeberg.org/g3un/pi-orchestra/issues"
|
|
15
|
+
},
|
|
12
16
|
"license": "MIT",
|
|
13
17
|
"author": "Changeun Park <g3un@protonmail.ch>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://codeberg.org/g3un/pi-orchestra.git"
|
|
21
|
+
},
|
|
14
22
|
"files": [
|
|
15
23
|
"src/**/*.ts",
|
|
16
24
|
"!src/**/*.test.ts",
|
|
@@ -19,6 +27,9 @@
|
|
|
19
27
|
"LICENSE"
|
|
20
28
|
],
|
|
21
29
|
"type": "module",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
22
33
|
"scripts": {
|
|
23
34
|
"dev": "pi -e .",
|
|
24
35
|
"fmt": "oxfmt",
|
|
@@ -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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentProfile, AgentRun } from "./subagent.ts";
|
|
2
2
|
import type { Bus, BusMessage } from "./bus.ts";
|
|
3
3
|
import type { AgentRuntime } from "./runtime.ts";
|
|
4
|
-
import { createEntityIdentity
|
|
4
|
+
import { createEntityIdentity } from "../utils.ts";
|
|
5
5
|
import type { AgentStore } from "./store.ts";
|
|
6
6
|
|
|
7
7
|
export interface OrchestraApi {
|
|
@@ -14,8 +14,6 @@ export interface OrchestraApi {
|
|
|
14
14
|
listRuns(options?: ListRunsOptions): AgentRun[];
|
|
15
15
|
messageAgent(id: string, message: string, options?: RunLookupOptions): Promise<AgentRun>;
|
|
16
16
|
closeAgent(id: string, options?: RunLookupOptions): Promise<AgentRun | undefined>;
|
|
17
|
-
waitBusSettled(busId: string, options?: WaitBusSettledOptions): Promise<WaitBusSettledResult>;
|
|
18
|
-
waitNextRun(busId: string, options?: WaitNextRunOptions): Promise<WaitNextRunResult>;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
export interface CreateBusOptions {
|
|
@@ -31,44 +29,6 @@ export interface PublishedBusMessage {
|
|
|
31
29
|
busMessage: BusMessage;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
export interface WaitBusSettledResult {
|
|
35
|
-
bus: Bus;
|
|
36
|
-
runs: AgentRun[];
|
|
37
|
-
runResults: WaitRunResult[];
|
|
38
|
-
timedOut: boolean;
|
|
39
|
-
pendingRunIds: string[];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface WaitNextRunResult {
|
|
43
|
-
bus: Bus;
|
|
44
|
-
run?: AgentRun;
|
|
45
|
-
runResult?: WaitRunResult;
|
|
46
|
-
runs: AgentRun[];
|
|
47
|
-
runResults: WaitRunResult[];
|
|
48
|
-
timedOut: boolean;
|
|
49
|
-
pendingRunIds: string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface WaitRunResult {
|
|
53
|
-
runId: string;
|
|
54
|
-
name: string;
|
|
55
|
-
profile: string;
|
|
56
|
-
state: AgentRun["state"];
|
|
57
|
-
result?: AgentRun["result"];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface WaitBusSettledOptions {
|
|
61
|
-
/** Defaults to 10 minutes. Use null to wait indefinitely. */
|
|
62
|
-
timeoutMs?: number | null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface WaitNextRunOptions {
|
|
66
|
-
/** Defaults to 10 minutes. Use null to wait indefinitely. */
|
|
67
|
-
timeoutMs?: number | null;
|
|
68
|
-
/** Run ids or names that have already been handled by the leader. */
|
|
69
|
-
excludeRunIds?: string[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
32
|
export interface ListRunsOptions {
|
|
73
33
|
busId?: string;
|
|
74
34
|
}
|
|
@@ -140,115 +100,6 @@ export class Orchestra implements OrchestraApi {
|
|
|
140
100
|
return await this.runtime.close(run?.id ?? id);
|
|
141
101
|
}
|
|
142
102
|
|
|
143
|
-
waitBusSettled(busId: string, options: WaitBusSettledOptions = {}): Promise<WaitBusSettledResult> {
|
|
144
|
-
const timeoutMs = resolveWaitTimeoutMs("waitBusSettled", options.timeoutMs);
|
|
145
|
-
const bus = this.requireBus(busId);
|
|
146
|
-
const initialRuns = this.listRuns({ busId: bus.id });
|
|
147
|
-
if (initialRuns.every((run) => isTerminalAgentState(run.state))) {
|
|
148
|
-
return Promise.resolve(buildWaitBusSettledResult(bus, initialRuns, false));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return new Promise((resolve) => {
|
|
152
|
-
const latestRuns = new Map(initialRuns.map((run) => [run.id, run]));
|
|
153
|
-
const unsubscribeAll: Array<() => void> = [];
|
|
154
|
-
let settled = false;
|
|
155
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
156
|
-
|
|
157
|
-
const cleanup = () => {
|
|
158
|
-
if (timeout) clearTimeout(timeout);
|
|
159
|
-
for (const unsubscribe of unsubscribeAll.splice(0)) unsubscribe();
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
const getLatestRuns = () => initialRuns.map((run) => latestRuns.get(run.id) ?? run);
|
|
163
|
-
|
|
164
|
-
const resolveIfDone = () => {
|
|
165
|
-
const runs = getLatestRuns();
|
|
166
|
-
if (runs.some((run) => !isTerminalAgentState(run.state))) return;
|
|
167
|
-
|
|
168
|
-
settled = true;
|
|
169
|
-
cleanup();
|
|
170
|
-
resolve(buildWaitBusSettledResult(bus, runs, false));
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
if (timeoutMs !== null) {
|
|
174
|
-
timeout = setTimeout(() => {
|
|
175
|
-
if (settled) return;
|
|
176
|
-
|
|
177
|
-
settled = true;
|
|
178
|
-
cleanup();
|
|
179
|
-
resolve(buildWaitBusSettledResult(bus, getLatestRuns(), true));
|
|
180
|
-
}, timeoutMs);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const run of initialRuns) {
|
|
184
|
-
if (isTerminalAgentState(run.state)) continue;
|
|
185
|
-
unsubscribeAll.push(
|
|
186
|
-
this.store.subscribeRun(run.id, (updatedRun) => {
|
|
187
|
-
if (settled) return;
|
|
188
|
-
latestRuns.set(updatedRun.id, updatedRun);
|
|
189
|
-
resolveIfDone();
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
resolveIfDone();
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
waitNextRun(busId: string, options: WaitNextRunOptions = {}): Promise<WaitNextRunResult> {
|
|
199
|
-
const timeoutMs = resolveWaitTimeoutMs("waitNextRun", options.timeoutMs);
|
|
200
|
-
const bus = this.requireBus(busId);
|
|
201
|
-
const initialRuns = this.listRuns({ busId: bus.id });
|
|
202
|
-
const excludedRunIds = this.resolveExcludedRunIds(initialRuns, options.excludeRunIds ?? []);
|
|
203
|
-
const candidateRuns = initialRuns.filter((run) => !excludedRunIds.has(run.id));
|
|
204
|
-
const alreadyDone = candidateRuns.find((run) => isTerminalAgentState(run.state));
|
|
205
|
-
if (alreadyDone || candidateRuns.length === 0) {
|
|
206
|
-
return Promise.resolve(buildWaitNextRunResult(bus, initialRuns, alreadyDone, false, excludedRunIds));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return new Promise((resolve) => {
|
|
210
|
-
const latestRuns = new Map(initialRuns.map((run) => [run.id, run]));
|
|
211
|
-
const unsubscribeAll: Array<() => void> = [];
|
|
212
|
-
let settled = false;
|
|
213
|
-
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
214
|
-
|
|
215
|
-
const cleanup = () => {
|
|
216
|
-
if (timeout) clearTimeout(timeout);
|
|
217
|
-
for (const unsubscribe of unsubscribeAll.splice(0)) unsubscribe();
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
const getLatestRuns = () => initialRuns.map((run) => latestRuns.get(run.id) ?? run);
|
|
221
|
-
|
|
222
|
-
const resolveWithRun = (run: AgentRun) => {
|
|
223
|
-
settled = true;
|
|
224
|
-
cleanup();
|
|
225
|
-
resolve(buildWaitNextRunResult(bus, getLatestRuns(), run, false, excludedRunIds));
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
if (timeoutMs !== null) {
|
|
229
|
-
timeout = setTimeout(() => {
|
|
230
|
-
if (settled) return;
|
|
231
|
-
|
|
232
|
-
settled = true;
|
|
233
|
-
cleanup();
|
|
234
|
-
resolve(buildWaitNextRunResult(bus, getLatestRuns(), undefined, true, excludedRunIds));
|
|
235
|
-
}, timeoutMs);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
for (const run of initialRuns) {
|
|
239
|
-
if (isTerminalAgentState(run.state)) continue;
|
|
240
|
-
unsubscribeAll.push(
|
|
241
|
-
this.store.subscribeRun(run.id, (updatedRun) => {
|
|
242
|
-
if (settled) return;
|
|
243
|
-
latestRuns.set(updatedRun.id, updatedRun);
|
|
244
|
-
if (!excludedRunIds.has(updatedRun.id) && isTerminalAgentState(updatedRun.state))
|
|
245
|
-
resolveWithRun(updatedRun);
|
|
246
|
-
}),
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
103
|
private requireBus(id: string): Bus {
|
|
253
104
|
const bus = this.findBus(id);
|
|
254
105
|
if (!bus) throw new Error(`Bus ${id} not found.`);
|
|
@@ -273,15 +124,6 @@ export class Orchestra implements OrchestraApi {
|
|
|
273
124
|
return this.store.listRuns().find((run) => run.name === id && (!bus || run.busId === bus.id));
|
|
274
125
|
}
|
|
275
126
|
|
|
276
|
-
private resolveExcludedRunIds(busRuns: AgentRun[], excludedRunIds: string[]): Set<string> {
|
|
277
|
-
const resolvedIds = new Set<string>();
|
|
278
|
-
for (const id of excludedRunIds) {
|
|
279
|
-
const run = busRuns.find((current) => current.id === id || current.name === id);
|
|
280
|
-
resolvedIds.add(run?.id ?? id);
|
|
281
|
-
}
|
|
282
|
-
return resolvedIds;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
127
|
private createBusIdentity(name: string | undefined) {
|
|
286
128
|
return createEntityIdentity(name, "bus", this.store.listBuses(), "Bus");
|
|
287
129
|
}
|
|
@@ -290,33 +132,3 @@ export class Orchestra implements OrchestraApi {
|
|
|
290
132
|
return createEntityIdentity(name, profile.name, this.store.listRuns(), "Agent");
|
|
291
133
|
}
|
|
292
134
|
}
|
|
293
|
-
|
|
294
|
-
function buildWaitBusSettledResult(bus: Bus, runs: AgentRun[], timedOut: boolean): WaitBusSettledResult {
|
|
295
|
-
return {
|
|
296
|
-
bus,
|
|
297
|
-
runs,
|
|
298
|
-
runResults: runs.map(toWaitRunResult),
|
|
299
|
-
timedOut,
|
|
300
|
-
pendingRunIds: runs.filter((run) => !isTerminalAgentState(run.state)).map((run) => run.id),
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function buildWaitNextRunResult(
|
|
305
|
-
bus: Bus,
|
|
306
|
-
runs: AgentRun[],
|
|
307
|
-
run: AgentRun | undefined,
|
|
308
|
-
timedOut: boolean,
|
|
309
|
-
excludedRunIds: Set<string>,
|
|
310
|
-
): WaitNextRunResult {
|
|
311
|
-
return {
|
|
312
|
-
bus,
|
|
313
|
-
run,
|
|
314
|
-
runResult: run ? toWaitRunResult(run) : undefined,
|
|
315
|
-
runs,
|
|
316
|
-
runResults: runs.map(toWaitRunResult),
|
|
317
|
-
timedOut,
|
|
318
|
-
pendingRunIds: runs
|
|
319
|
-
.filter((current) => !excludedRunIds.has(current.id) && !isTerminalAgentState(current.state))
|
|
320
|
-
.map((current) => current.id),
|
|
321
|
-
};
|
|
322
|
-
}
|
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/core/subagent.ts
CHANGED
package/src/extension/index.ts
CHANGED
|
@@ -6,25 +6,47 @@ 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 { ORCHESTRA_EVENT_CUSTOM_TYPE, OrchestraEventController, type OrchestraMainEvent } from "./orchestra-events.ts";
|
|
10
|
+
import { WorkflowMonitorController } from "./workflow-monitor.ts";
|
|
9
11
|
|
|
10
12
|
interface ToolBundle {
|
|
11
13
|
busTool: BusTool;
|
|
12
14
|
subagentTool: SubagentTool;
|
|
13
15
|
workgroupTool: WorkgroupTool;
|
|
14
16
|
workflowTool: WorkflowTool;
|
|
17
|
+
workflowMonitor: WorkflowMonitorController;
|
|
18
|
+
orchestraEvents: OrchestraEventController;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export default function piOrchestraExtension(pi: ExtensionAPI): void {
|
|
18
22
|
const bundles = new Map<string, ToolBundle>();
|
|
19
|
-
const getToolBundle = (ctx: ExtensionContext) => getBundle(bundles, ctx);
|
|
23
|
+
const getToolBundle = (ctx: ExtensionContext) => getBundle(pi, bundles, ctx);
|
|
20
24
|
|
|
21
25
|
pi.registerTool(defineBusPiTool((ctx) => getToolBundle(ctx).busTool));
|
|
22
26
|
pi.registerTool(defineSubagentPiTool((ctx) => getToolBundle(ctx).subagentTool));
|
|
23
27
|
pi.registerTool(defineWorkgroupPiTool((ctx) => getToolBundle(ctx).workgroupTool));
|
|
24
|
-
pi.registerTool(
|
|
28
|
+
pi.registerTool(
|
|
29
|
+
defineWorkflowPiTool((ctx) => getToolBundle(ctx).workflowTool, {
|
|
30
|
+
onWorkflowInput: (ctx) => getToolBundle(ctx).workflowMonitor.show(ctx),
|
|
31
|
+
onWorkflowOutput: (ctx) => getToolBundle(ctx).workflowMonitor.show(ctx),
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
pi.registerCommand("orchestra-workflows", {
|
|
36
|
+
description: "Show the active pi-orchestra workflow progress widget.",
|
|
37
|
+
handler: async (_args, ctx) => {
|
|
38
|
+
const monitor = getToolBundle(ctx).workflowMonitor;
|
|
39
|
+
if (monitor.show(ctx)) return;
|
|
40
|
+
ctx.ui.notify("No active pi-orchestra workflows.", "info");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
45
|
+
bundles.get(ctx.cwd)?.workflowMonitor.dispose();
|
|
46
|
+
});
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): ToolBundle {
|
|
49
|
+
function getBundle(pi: ExtensionAPI, bundles: Map<string, ToolBundle>, ctx: ExtensionContext): ToolBundle {
|
|
28
50
|
const existing = bundles.get(ctx.cwd);
|
|
29
51
|
if (existing) return existing;
|
|
30
52
|
|
|
@@ -35,16 +57,45 @@ function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): Too
|
|
|
35
57
|
resolveModel: (model) => resolveModel(ctx, model),
|
|
36
58
|
});
|
|
37
59
|
const orchestra = new Orchestra({ runtime, store });
|
|
60
|
+
const orchestraEvents = new OrchestraEventController({
|
|
61
|
+
store,
|
|
62
|
+
sendEvents: (events, content) => sendOrchestraEvents(pi, events, content),
|
|
63
|
+
});
|
|
64
|
+
const workgroupTool = createWorkgroupTool({
|
|
65
|
+
orchestra,
|
|
66
|
+
onWorkgroupLaunching: ({ input, bus }) => orchestraEvents.beginWorkgroup(bus.id, input.strategy),
|
|
67
|
+
onWorkgroupLaunched: ({ input, output }) =>
|
|
68
|
+
orchestraEvents.registerWorkgroup({
|
|
69
|
+
busId: output.bus.id,
|
|
70
|
+
strategy: input.strategy,
|
|
71
|
+
runIds: output.runs.map((run) => run.id),
|
|
72
|
+
}),
|
|
73
|
+
onWorkgroupLaunchFailed: ({ bus }) => orchestraEvents.cancelWorkgroupLaunch(bus.id),
|
|
74
|
+
});
|
|
38
75
|
const bundle = {
|
|
39
76
|
busTool: createBusTool({ orchestra }),
|
|
40
77
|
subagentTool: createSubagentTool({ orchestra }),
|
|
41
|
-
workgroupTool
|
|
78
|
+
workgroupTool,
|
|
42
79
|
workflowTool: createWorkflowTool({ orchestra, store }),
|
|
80
|
+
workflowMonitor: new WorkflowMonitorController(store),
|
|
81
|
+
orchestraEvents,
|
|
43
82
|
};
|
|
44
83
|
bundles.set(ctx.cwd, bundle);
|
|
45
84
|
return bundle;
|
|
46
85
|
}
|
|
47
86
|
|
|
87
|
+
function sendOrchestraEvents(pi: ExtensionAPI, events: OrchestraMainEvent[], content: string): void {
|
|
88
|
+
pi.sendMessage(
|
|
89
|
+
{
|
|
90
|
+
customType: ORCHESTRA_EVENT_CUSTOM_TYPE,
|
|
91
|
+
content,
|
|
92
|
+
display: true,
|
|
93
|
+
details: { events },
|
|
94
|
+
},
|
|
95
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
48
99
|
function resolveModel(ctx: ExtensionContext, model: string): ReturnType<ExtensionContext["modelRegistry"]["find"]> {
|
|
49
100
|
const slashIndex = model.indexOf("/");
|
|
50
101
|
if (slashIndex < 0) {
|