@g3un/pi-orchestra 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 CHANGED
@@ -15,9 +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
- 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
+ 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.
21
23
 
22
24
  ## Core concepts
23
25
 
@@ -29,7 +31,7 @@ Use subagents when you want to delegate a focused task, such as review, research
29
31
 
30
32
  ### Workgroup
31
33
 
32
- 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.
33
35
 
34
36
  Workgroups support two strategies:
35
37
 
@@ -38,12 +40,12 @@ Workgroups support two strategies:
38
40
 
39
41
  ### Workflow
40
42
 
41
- 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.
42
44
 
43
45
  Use workflows for multi-step plans where later stages should depend on the summarized output of earlier stages instead of raw worker transcripts.
44
46
 
45
47
  ## Notes
46
48
 
47
49
  - Create a `bus` before spawning related subagents or workgroups.
48
- - Subagents report completion with `success`, `blocked`, or `failed`.
50
+ - Subagents report completion with `success`, `blocked`, or `failed`; completions are surfaced as pi-orchestra events.
49
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.1",
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",
@@ -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,121 +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.subscribeRuns(
187
- (updatedRun) => {
188
- if (settled) return;
189
- latestRuns.set(updatedRun.id, updatedRun);
190
- resolveIfDone();
191
- },
192
- (updatedRun) => updatedRun.id === run.id,
193
- ),
194
- );
195
- }
196
-
197
- resolveIfDone();
198
- });
199
- }
200
-
201
- waitNextRun(busId: string, options: WaitNextRunOptions = {}): Promise<WaitNextRunResult> {
202
- const timeoutMs = resolveWaitTimeoutMs("waitNextRun", options.timeoutMs);
203
- const bus = this.requireBus(busId);
204
- const initialRuns = this.listRuns({ busId: bus.id });
205
- const excludedRunIds = this.resolveExcludedRunIds(initialRuns, options.excludeRunIds ?? []);
206
- const candidateRuns = initialRuns.filter((run) => !excludedRunIds.has(run.id));
207
- const alreadyDone = candidateRuns.find((run) => isTerminalAgentState(run.state));
208
- if (alreadyDone || candidateRuns.length === 0) {
209
- return Promise.resolve(buildWaitNextRunResult(bus, initialRuns, alreadyDone, false, excludedRunIds));
210
- }
211
-
212
- return new Promise((resolve) => {
213
- const latestRuns = new Map(initialRuns.map((run) => [run.id, run]));
214
- const unsubscribeAll: Array<() => void> = [];
215
- let settled = false;
216
- let timeout: ReturnType<typeof setTimeout> | undefined;
217
-
218
- const cleanup = () => {
219
- if (timeout) clearTimeout(timeout);
220
- for (const unsubscribe of unsubscribeAll.splice(0)) unsubscribe();
221
- };
222
-
223
- const getLatestRuns = () => initialRuns.map((run) => latestRuns.get(run.id) ?? run);
224
-
225
- const resolveWithRun = (run: AgentRun) => {
226
- settled = true;
227
- cleanup();
228
- resolve(buildWaitNextRunResult(bus, getLatestRuns(), run, false, excludedRunIds));
229
- };
230
-
231
- if (timeoutMs !== null) {
232
- timeout = setTimeout(() => {
233
- if (settled) return;
234
-
235
- settled = true;
236
- cleanup();
237
- resolve(buildWaitNextRunResult(bus, getLatestRuns(), undefined, true, excludedRunIds));
238
- }, timeoutMs);
239
- }
240
-
241
- for (const run of initialRuns) {
242
- if (isTerminalAgentState(run.state)) continue;
243
- unsubscribeAll.push(
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
- ),
253
- );
254
- }
255
- });
256
- }
257
-
258
103
  private requireBus(id: string): Bus {
259
104
  const bus = this.findBus(id);
260
105
  if (!bus) throw new Error(`Bus ${id} not found.`);
@@ -279,15 +124,6 @@ export class Orchestra implements OrchestraApi {
279
124
  return this.store.listRuns().find((run) => run.name === id && (!bus || run.busId === bus.id));
280
125
  }
281
126
 
282
- private resolveExcludedRunIds(busRuns: AgentRun[], excludedRunIds: string[]): Set<string> {
283
- const resolvedIds = new Set<string>();
284
- for (const id of excludedRunIds) {
285
- const run = busRuns.find((current) => current.id === id || current.name === id);
286
- resolvedIds.add(run?.id ?? id);
287
- }
288
- return resolvedIds;
289
- }
290
-
291
127
  private createBusIdentity(name: string | undefined) {
292
128
  return createEntityIdentity(name, "bus", this.store.listBuses(), "Bus");
293
129
  }
@@ -296,33 +132,3 @@ export class Orchestra implements OrchestraApi {
296
132
  return createEntityIdentity(name, profile.name, this.store.listRuns(), "Agent");
297
133
  }
298
134
  }
299
-
300
- function buildWaitBusSettledResult(bus: Bus, runs: AgentRun[], timedOut: boolean): WaitBusSettledResult {
301
- return {
302
- bus,
303
- runs,
304
- runResults: runs.map(toWaitRunResult),
305
- timedOut,
306
- pendingRunIds: runs.filter((run) => !isTerminalAgentState(run.state)).map((run) => run.id),
307
- };
308
- }
309
-
310
- function buildWaitNextRunResult(
311
- bus: Bus,
312
- runs: AgentRun[],
313
- run: AgentRun | undefined,
314
- timedOut: boolean,
315
- excludedRunIds: Set<string>,
316
- ): WaitNextRunResult {
317
- return {
318
- bus,
319
- run,
320
- runResult: run ? toWaitRunResult(run) : undefined,
321
- runs,
322
- runResults: runs.map(toWaitRunResult),
323
- timedOut,
324
- pendingRunIds: runs
325
- .filter((current) => !excludedRunIds.has(current.id) && !isTerminalAgentState(current.state))
326
- .map((current) => current.id),
327
- };
328
- }
@@ -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,6 +6,7 @@ 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";
9
10
  import { WorkflowMonitorController } from "./workflow-monitor.ts";
10
11
 
11
12
  interface ToolBundle {
@@ -14,11 +15,12 @@ interface ToolBundle {
14
15
  workgroupTool: WorkgroupTool;
15
16
  workflowTool: WorkflowTool;
16
17
  workflowMonitor: WorkflowMonitorController;
18
+ orchestraEvents: OrchestraEventController;
17
19
  }
18
20
 
19
21
  export default function piOrchestraExtension(pi: ExtensionAPI): void {
20
22
  const bundles = new Map<string, ToolBundle>();
21
- const getToolBundle = (ctx: ExtensionContext) => getBundle(bundles, ctx);
23
+ const getToolBundle = (ctx: ExtensionContext) => getBundle(pi, bundles, ctx);
22
24
 
23
25
  pi.registerTool(defineBusPiTool((ctx) => getToolBundle(ctx).busTool));
24
26
  pi.registerTool(defineSubagentPiTool((ctx) => getToolBundle(ctx).subagentTool));
@@ -44,7 +46,7 @@ export default function piOrchestraExtension(pi: ExtensionAPI): void {
44
46
  });
45
47
  }
46
48
 
47
- function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): ToolBundle {
49
+ function getBundle(pi: ExtensionAPI, bundles: Map<string, ToolBundle>, ctx: ExtensionContext): ToolBundle {
48
50
  const existing = bundles.get(ctx.cwd);
49
51
  if (existing) return existing;
50
52
 
@@ -55,17 +57,45 @@ function getBundle(bundles: Map<string, ToolBundle>, ctx: ExtensionContext): Too
55
57
  resolveModel: (model) => resolveModel(ctx, model),
56
58
  });
57
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
+ });
58
75
  const bundle = {
59
76
  busTool: createBusTool({ orchestra }),
60
77
  subagentTool: createSubagentTool({ orchestra }),
61
- workgroupTool: createWorkgroupTool({ orchestra }),
78
+ workgroupTool,
62
79
  workflowTool: createWorkflowTool({ orchestra, store }),
63
80
  workflowMonitor: new WorkflowMonitorController(store),
81
+ orchestraEvents,
64
82
  };
65
83
  bundles.set(ctx.cwd, bundle);
66
84
  return bundle;
67
85
  }
68
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
+
69
99
  function resolveModel(ctx: ExtensionContext, model: string): ReturnType<ExtensionContext["modelRegistry"]["find"]> {
70
100
  const slashIndex = model.indexOf("/");
71
101
  if (slashIndex < 0) {
@@ -0,0 +1,231 @@
1
+ import type { AgentRun, AgentRunResult, AgentState } from "../core/subagent.ts";
2
+ import type { AgentStore } from "../core/store.ts";
3
+ import type { WorkflowRun } from "../core/workflow.ts";
4
+ import type { WorkgroupStrategy } from "../core/workgroup.ts";
5
+ import { formatNamedEntityLabel, isTerminalAgentState, toAgentRunResult } from "../utils.ts";
6
+
7
+ export const ORCHESTRA_EVENT_CUSTOM_TYPE = "pi-orchestra.event";
8
+
9
+ export type OrchestraMainEvent =
10
+ | {
11
+ type: "subagent.finished";
12
+ busId: string;
13
+ run: AgentRunResult;
14
+ }
15
+ | {
16
+ type: "workgroup.member_finished";
17
+ busId: string;
18
+ strategy: WorkgroupStrategy;
19
+ run: AgentRunResult;
20
+ pendingRunIds: string[];
21
+ }
22
+ | {
23
+ type: "workflow.finished";
24
+ workflow: WorkflowRun;
25
+ };
26
+
27
+ export interface WorkgroupRegistration {
28
+ busId: string;
29
+ strategy: WorkgroupStrategy;
30
+ runIds: string[];
31
+ }
32
+
33
+ export interface OrchestraEventControllerOptions {
34
+ store: AgentStore;
35
+ sendEvents: (events: OrchestraMainEvent[], content: string) => void;
36
+ /** Defaults to 50 ms. Use 0 in tests for immediate delivery. */
37
+ flushDelayMs?: number;
38
+ }
39
+
40
+ interface RegisteredWorkgroup {
41
+ strategy: WorkgroupStrategy;
42
+ runIds: Set<string>;
43
+ }
44
+
45
+ interface LaunchingWorkgroup {
46
+ strategy: WorkgroupStrategy;
47
+ finishedRunIds: Set<string>;
48
+ }
49
+
50
+ export class OrchestraEventController {
51
+ private readonly store: AgentStore;
52
+ private readonly sendEvents: OrchestraEventControllerOptions["sendEvents"];
53
+ private readonly flushDelayMs: number;
54
+ private readonly runStates = new Map<string, AgentState>();
55
+ private readonly workflowStates = new Map<string, AgentState>();
56
+ private readonly mainWorkgroupsByBusId = new Map<string, RegisteredWorkgroup>();
57
+ private readonly launchingWorkgroupsByBusId = new Map<string, LaunchingWorkgroup>();
58
+ private readonly queuedEvents: OrchestraMainEvent[] = [];
59
+ private readonly unsubscribeRuns: () => void;
60
+ private readonly unsubscribeWorkflows: () => void;
61
+ private flushTimer: ReturnType<typeof setTimeout> | undefined;
62
+
63
+ constructor(options: OrchestraEventControllerOptions) {
64
+ this.store = options.store;
65
+ this.sendEvents = options.sendEvents;
66
+ this.flushDelayMs = options.flushDelayMs ?? 50;
67
+
68
+ for (const run of this.store.listRuns()) this.runStates.set(run.id, run.state);
69
+ for (const workflow of this.store.listWorkflows()) this.workflowStates.set(workflow.id, workflow.state);
70
+
71
+ this.unsubscribeRuns = this.store.subscribeRuns((run) => this.handleRunSaved(run));
72
+ this.unsubscribeWorkflows = this.store.subscribeWorkflows((workflow) => this.handleWorkflowSaved(workflow));
73
+ }
74
+
75
+ beginWorkgroup(busId: string, strategy: WorkgroupStrategy): void {
76
+ this.launchingWorkgroupsByBusId.set(busId, { strategy, finishedRunIds: new Set() });
77
+ }
78
+
79
+ registerWorkgroup(registration: WorkgroupRegistration): void {
80
+ const launching = this.launchingWorkgroupsByBusId.get(registration.busId);
81
+ const existing = this.mainWorkgroupsByBusId.get(registration.busId);
82
+ const runIds = new Set<string>(existing?.runIds);
83
+ for (const runId of launching?.finishedRunIds ?? []) runIds.add(runId);
84
+ for (const runId of registration.runIds) runIds.add(runId);
85
+
86
+ this.mainWorkgroupsByBusId.set(registration.busId, {
87
+ strategy: registration.strategy,
88
+ runIds,
89
+ });
90
+ this.launchingWorkgroupsByBusId.delete(registration.busId);
91
+ }
92
+
93
+ cancelWorkgroupLaunch(busId: string): void {
94
+ this.launchingWorkgroupsByBusId.delete(busId);
95
+ }
96
+
97
+ dispose(): void {
98
+ if (this.flushTimer) clearTimeout(this.flushTimer);
99
+ this.flushTimer = undefined;
100
+ this.queuedEvents.length = 0;
101
+ this.unsubscribeRuns();
102
+ this.unsubscribeWorkflows();
103
+ }
104
+
105
+ flush(): void {
106
+ if (this.flushTimer) clearTimeout(this.flushTimer);
107
+ this.flushTimer = undefined;
108
+ if (this.queuedEvents.length === 0) return;
109
+
110
+ const events = this.queuedEvents.splice(0);
111
+ this.sendEvents(events, formatOrchestraEvents(events));
112
+ }
113
+
114
+ private handleRunSaved(run: AgentRun): void {
115
+ const previousState = this.runStates.get(run.id);
116
+ this.runStates.set(run.id, run.state);
117
+
118
+ if (!isAgentFinishState(run.state) || isAgentFinishState(previousState)) return;
119
+ if (this.isWorkflowBus(run.busId)) return;
120
+
121
+ const registeredWorkgroup = this.mainWorkgroupsByBusId.get(run.busId);
122
+ if (registeredWorkgroup?.runIds.has(run.id)) {
123
+ this.queueEvent({
124
+ type: "workgroup.member_finished",
125
+ busId: run.busId,
126
+ strategy: registeredWorkgroup.strategy,
127
+ run: toAgentRunResult(run),
128
+ pendingRunIds: this.getPendingWorkgroupRunIds(run.busId, registeredWorkgroup.runIds),
129
+ });
130
+ return;
131
+ }
132
+
133
+ const launchingWorkgroup = this.launchingWorkgroupsByBusId.get(run.busId);
134
+ if (launchingWorkgroup) {
135
+ launchingWorkgroup.finishedRunIds.add(run.id);
136
+ this.queueEvent({
137
+ type: "workgroup.member_finished",
138
+ busId: run.busId,
139
+ strategy: launchingWorkgroup.strategy,
140
+ run: toAgentRunResult(run),
141
+ pendingRunIds: this.getPendingBusRunIds(run.busId),
142
+ });
143
+ return;
144
+ }
145
+
146
+ this.queueEvent({ type: "subagent.finished", busId: run.busId, run: toAgentRunResult(run) });
147
+ }
148
+
149
+ private handleWorkflowSaved(workflow: WorkflowRun): void {
150
+ const previousState = this.workflowStates.get(workflow.id);
151
+ this.workflowStates.set(workflow.id, workflow.state);
152
+
153
+ if (!isTerminalAgentState(workflow.state) || isTerminalWorkflowState(previousState)) return;
154
+ this.queueEvent({ type: "workflow.finished", workflow });
155
+ }
156
+
157
+ private isWorkflowBus(busId: string): boolean {
158
+ return this.store.listWorkflows().some((workflow) => workflow.stages.some((stage) => stage.busId === busId));
159
+ }
160
+
161
+ private getPendingWorkgroupRunIds(busId: string, runIds: Set<string>): string[] {
162
+ return this.store
163
+ .listRuns()
164
+ .filter((run) => run.busId === busId && runIds.has(run.id) && run.state === "idle")
165
+ .map((run) => run.id);
166
+ }
167
+
168
+ private getPendingBusRunIds(busId: string): string[] {
169
+ return this.store
170
+ .listRuns()
171
+ .filter((run) => run.busId === busId && run.state === "idle")
172
+ .map((run) => run.id);
173
+ }
174
+
175
+ private queueEvent(event: OrchestraMainEvent): void {
176
+ this.queuedEvents.push(event);
177
+ if (this.flushDelayMs === 0) {
178
+ this.flush();
179
+ return;
180
+ }
181
+
182
+ this.flushTimer ??= setTimeout(() => this.flush(), this.flushDelayMs);
183
+ }
184
+ }
185
+
186
+ export function formatOrchestraEvents(events: OrchestraMainEvent[]): string {
187
+ const headline = events.length === 1 ? "Pi-orchestra event:" : `Pi-orchestra events (${events.length}):`;
188
+ return [headline, ...events.flatMap((event) => ["", formatOrchestraEvent(event)])].join("\n");
189
+ }
190
+
191
+ function formatOrchestraEvent(event: OrchestraMainEvent): string {
192
+ if (event.type === "workflow.finished") return formatWorkflowFinishedEvent(event.workflow);
193
+
194
+ const runLabel = event.run.name === event.run.runId ? event.run.runId : `${event.run.name} (${event.run.runId})`;
195
+ const lines =
196
+ event.type === "workgroup.member_finished"
197
+ ? [
198
+ `- Workgroup member finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`,
199
+ ` Strategy: ${event.strategy}`,
200
+ ` Pending workgroup run ids: ${event.pendingRunIds.length > 0 ? event.pendingRunIds.join(", ") : "none"}`,
201
+ ]
202
+ : [`- Subagent finished on bus ${event.busId}: ${runLabel} is ${event.run.state}.`];
203
+
204
+ lines.push(...formatRunResultLines(event.run));
205
+ return lines.join("\n");
206
+ }
207
+
208
+ function formatWorkflowFinishedEvent(workflow: WorkflowRun): string {
209
+ const lines = [`- Workflow finished: ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`];
210
+ if (workflow.result) {
211
+ lines.push(` Result: ${workflow.result.status}`, ` Summary: ${workflow.result.summary}`);
212
+ }
213
+ if (workflow.error) lines.push(` Error: ${workflow.error}`);
214
+ return lines.join("\n");
215
+ }
216
+
217
+ function formatRunResultLines(run: AgentRunResult): string[] {
218
+ if (!run.result) return [" No result payload recorded."];
219
+
220
+ const lines = [` Result: ${run.result.status}`, ` Summary: ${run.result.summary}`];
221
+ if (run.result.data !== undefined) lines.push(` Data: ${JSON.stringify(run.result.data)}`);
222
+ return lines;
223
+ }
224
+
225
+ function isAgentFinishState(state: AgentState | undefined): boolean {
226
+ return state === "success" || state === "blocked" || state === "failed";
227
+ }
228
+
229
+ function isTerminalWorkflowState(state: AgentState | undefined): boolean {
230
+ return state !== undefined && isTerminalAgentState(state);
231
+ }