@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 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
- Leaders collect results with bus wait actions:
49
+ Main receives finish events instead of blocking on completion calls:
50
50
 
51
- - `wait_next`: handle terminal runs as they finish.
52
- - `wait_settled`: wait until every attached run is terminal.
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.1.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 runListeners = new Map<string, Set<(run: AgentRun) => void>>();
11
- private readonly workflowListeners = new Map<string, Set<(workflow: WorkflowRun) => void>>();
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
- for (const listener of this.runListeners.get(run.id) ?? []) listener(run);
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
- subscribeRun(id: string, listener: (run: AgentRun) => void): () => void {
27
- const listeners = this.runListeners.get(id) ?? new Set<(run: AgentRun) => void>();
28
- listeners.add(listener);
29
- this.runListeners.set(id, listeners);
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
- for (const listener of this.workflowListeners.get(workflow.id) ?? []) listener(workflow);
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
- subscribeWorkflow(id: string, listener: (workflow: WorkflowRun) => void): () => void {
76
- const listeners = this.workflowListeners.get(id) ?? new Set<(workflow: WorkflowRun) => void>();
77
- listeners.add(listener);
78
- this.workflowListeners.set(id, listeners);
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
- return () => {
81
- listeners.delete(listener);
82
- if (listeners.size === 0) this.workflowListeners.delete(id);
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
  }
@@ -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, isTerminalAgentState, resolveWaitTimeoutMs, toWaitRunResult } from "../utils.ts";
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
- subscribeRun(id: string, listener: (run: AgentRun) => void): () => void;
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
- subscribeWorkflow(id: string, listener: (workflow: WorkflowRun) => void): () => void;
20
+ subscribeWorkflows(
21
+ listener: (workflow: WorkflowRun) => void,
22
+ filter?: (workflow: WorkflowRun) => boolean,
23
+ ): () => void;
21
24
  }
@@ -25,3 +25,11 @@ export interface AgentRun {
25
25
  state: AgentState;
26
26
  result?: AgentResult;
27
27
  }
28
+
29
+ export interface AgentRunResult {
30
+ runId: string;
31
+ name: string;
32
+ profile: string;
33
+ state: AgentRun["state"];
34
+ result?: AgentRun["result"];
35
+ }
@@ -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(defineWorkflowPiTool((ctx) => getToolBundle(ctx).workflowTool));
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: createWorkgroupTool({ orchestra }),
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) {