@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/src/tools/bus.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import type { AgentRun } from "../core/subagent.ts";
4
3
  import type { Bus, BusMessage } from "../core/bus.ts";
5
- import type { OrchestraApi, WaitBusSettledResult, WaitNextRunResult, WaitRunResult } from "../core/orchestra.ts";
4
+ import type { OrchestraApi } from "../core/orchestra.ts";
6
5
  import { formatNamedEntityLabel } from "../utils.ts";
7
6
 
8
7
  export type BusInput =
@@ -19,31 +18,11 @@ export type BusInput =
19
18
  id: string;
20
19
  message: string;
21
20
  from?: string;
22
- }
23
- | {
24
- action: "wait_settled";
25
- id: string;
26
- /** Defaults to 10 minutes. Use null to wait indefinitely. */
27
- timeoutMs?: number | null;
28
- }
29
- | {
30
- action: "wait_next";
31
- id: string;
32
- /** Run ids or names to ignore. */
33
- excludeRunIds?: string[];
34
- /** Defaults to 10 minutes. Use null to wait indefinitely. */
35
- timeoutMs?: number | null;
36
21
  };
37
22
 
38
23
  export interface BusOutput {
39
24
  bus?: Bus;
40
25
  busMessage?: BusMessage;
41
- run?: AgentRun;
42
- runResult?: WaitRunResult;
43
- runs?: AgentRun[];
44
- runResults?: WaitRunResult[];
45
- timedOut?: boolean;
46
- pendingRunIds?: string[];
47
26
  message: string;
48
27
  }
49
28
 
@@ -57,8 +36,8 @@ export interface BusToolDeps {
57
36
  }
58
37
 
59
38
  const BusActionParams = Type.String({
60
- enum: ["create", "status", "publish", "wait_settled", "wait_next"],
61
- description: "create/status/publish; wait_settled waits all attached runs; wait_next waits the next terminal run.",
39
+ enum: ["create", "status", "publish"],
40
+ description: "create/status/publish shared context buses; completion is delivered through pi-orchestra events.",
62
41
  });
63
42
 
64
43
  const BusToolParams = Type.Object(
@@ -79,24 +58,6 @@ const BusToolParams = Type.Object(
79
58
  description: "Required for action=publish. Shared context for attached agents.",
80
59
  }),
81
60
  ),
82
- excludeRunIds: Type.Optional(
83
- Type.Array(Type.String(), {
84
- description: "Optional for action=wait_next. Already handled run ids/names.",
85
- }),
86
- ),
87
- timeoutMs: Type.Optional(
88
- Type.Union(
89
- [
90
- Type.Number({
91
- exclusiveMinimum: 0,
92
- }),
93
- Type.Null(),
94
- ],
95
- {
96
- description: "Optional for wait actions. Positive ms; default 10 min; null waits indefinitely.",
97
- },
98
- ),
99
- ),
100
61
  },
101
62
  { additionalProperties: false },
102
63
  );
@@ -118,35 +79,6 @@ export function createBusTool({ orchestra }: BusToolDeps): BusTool {
118
79
  return { bus, message: formatBusStatus(bus) };
119
80
  }
120
81
 
121
- if (input.action === "wait_settled") {
122
- const output = await orchestra.waitBusSettled(bus.id, { timeoutMs: input.timeoutMs });
123
- return {
124
- bus: output.bus,
125
- runs: output.runs,
126
- runResults: output.runResults,
127
- timedOut: output.timedOut,
128
- pendingRunIds: output.pendingRunIds,
129
- message: formatWaitBusSettledMessage(output),
130
- };
131
- }
132
-
133
- if (input.action === "wait_next") {
134
- const output = await orchestra.waitNextRun(bus.id, {
135
- excludeRunIds: input.excludeRunIds,
136
- timeoutMs: input.timeoutMs,
137
- });
138
- return {
139
- bus: output.bus,
140
- run: output.run,
141
- runResult: output.runResult,
142
- runs: output.runs,
143
- runResults: output.runResults,
144
- timedOut: output.timedOut,
145
- pendingRunIds: output.pendingRunIds,
146
- message: formatWaitNextRunMessage(output),
147
- };
148
- }
149
-
150
82
  const published = await orchestra.publishBus(input.id, input.message, input.from ?? "main");
151
83
  return {
152
84
  bus: published.bus,
@@ -161,13 +93,12 @@ export function defineBusPiTool(resolveTool: (ctx: ExtensionContext) => BusTool)
161
93
  return defineTool({
162
94
  name: "bus",
163
95
  label: "Bus",
164
- description: "Create, inspect, publish to, and wait on work buses.",
165
- promptSnippet:
166
- "Use one bus per delegated work item; spawn related subagents on it and collect results with wait actions.",
96
+ description: "Create, inspect, and publish to work buses.",
97
+ promptSnippet: "Use one bus per delegated work item; spawn related subagents or workgroups on it.",
167
98
  promptGuidelines: [
168
- "Create a bus before spawning related subagents; reuse it for that work item.",
169
- "publish sends shared context to attached agents; status shows published messages.",
170
- "wait_next handles results as they arrive; wait_settled waits for full fan-in.",
99
+ "Use bus create before spawning related subagents or workgroups; reuse it for that work item.",
100
+ "Use bus publish to send shared context to attached agents; bus status shows published messages.",
101
+ "Do not wait on buses; pi-orchestra delivers subagent and workgroup finish events automatically.",
171
102
  ],
172
103
  parameters: BusToolParams,
173
104
  executionMode: "sequential",
@@ -190,21 +121,6 @@ function toBusInput(params: RawBusParams): BusInput {
190
121
  return { action: "status", id: params.id };
191
122
  }
192
123
 
193
- if (params.action === "wait_settled") {
194
- if (!params.id) throw new Error("bus action=wait_settled requires id.");
195
- return { action: "wait_settled", id: params.id, timeoutMs: params.timeoutMs };
196
- }
197
-
198
- if (params.action === "wait_next") {
199
- if (!params.id) throw new Error("bus action=wait_next requires id.");
200
- return {
201
- action: "wait_next",
202
- id: params.id,
203
- excludeRunIds: params.excludeRunIds,
204
- timeoutMs: params.timeoutMs,
205
- };
206
- }
207
-
208
124
  if (!params.id) throw new Error("bus action=publish requires id.");
209
125
  if (!params.message) throw new Error("bus action=publish requires message.");
210
126
  return { action: "publish", id: params.id, message: params.message };
@@ -227,49 +143,9 @@ function formatBusMessage(message: BusMessage): string {
227
143
  return [`- ${message.id} from ${message.from}:`, message.message].join("\n");
228
144
  }
229
145
 
230
- function formatWaitBusSettledMessage(result: WaitBusSettledResult): string {
231
- const busLabel = formatNamedEntityLabel(result.bus);
232
- const headline = result.timedOut
233
- ? `Timed out waiting for bus ${busLabel} to settle; ${result.pendingRunIds.length} run(s) still pending.`
234
- : `All ${result.runs.length} run(s) attached to bus ${busLabel} reached terminal state.`;
235
- if (result.runs.length === 0) return headline;
236
-
237
- return [headline, "", "Runs:", ...result.runs.map(formatRunSummary)].join("\n");
238
- }
239
-
240
- function formatRunSummary(run: AgentRun): string {
241
- const runLabel = formatNamedEntityLabel(run);
242
- if (!run.result) return `- ${runLabel}: ${run.state}`;
243
- return `- ${runLabel}: ${run.state} result=${run.result.status} summary=${run.result.summary}`;
244
- }
245
-
246
- function formatWaitNextRunMessage(result: WaitNextRunResult): string {
247
- const busLabel = formatNamedEntityLabel(result.bus);
248
- if (result.run) {
249
- return [
250
- `Next terminal run on bus ${busLabel}: ${formatNamedEntityLabel(result.run)} is ${result.run.state}.`,
251
- "",
252
- formatRunResult(result.run),
253
- ].join("\n");
254
- }
255
-
256
- if (result.timedOut) {
257
- return `Timed out waiting for the next run on bus ${busLabel}; ${result.pendingRunIds.length} run(s) still pending.`;
258
- }
259
-
260
- return `No unhandled current runs remain on bus ${busLabel}.`;
261
- }
262
-
263
- function formatRunResult(run: AgentRun): string {
264
- if (!run.result) return "No result payload recorded.";
265
- return [`Result: ${run.result.status}`, run.result.summary].join("\n");
266
- }
267
-
268
146
  type RawBusParams = {
269
- action: "create" | "status" | "publish" | "wait_settled" | "wait_next";
147
+ action: "create" | "status" | "publish";
270
148
  name?: string;
271
149
  id?: string;
272
150
  message?: string;
273
- excludeRunIds?: string[];
274
- timeoutMs?: number | null;
275
151
  };
@@ -1,7 +1,7 @@
1
1
  import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import type { AgentResultStatus, AgentRun } from "../core/subagent.ts";
4
- import type { OrchestraApi, WaitRunResult } from "../core/orchestra.ts";
3
+ import type { AgentResultStatus, AgentRun, AgentRunResult } from "../core/subagent.ts";
4
+ import type { OrchestraApi } from "../core/orchestra.ts";
5
5
  import type { AgentStore } from "../core/store.ts";
6
6
  import type { WorkflowRun, WorkflowStageOutput, WorkflowStageRun, WorkflowStageSpec } from "../core/workflow.ts";
7
7
  import { WORKGROUP_STRATEGY_VALUES, type WorkgroupMember, type WorkgroupStrategy } from "../core/workgroup.ts";
@@ -15,7 +15,6 @@ import {
15
15
  isTerminalAgentState,
16
16
  normalizeEntityName,
17
17
  requireWorkflow,
18
- resolveWaitTimeoutMs,
19
18
  } from "../utils.ts";
20
19
  import {
21
20
  createWorkgroupTool,
@@ -41,17 +40,10 @@ export type WorkflowInput =
41
40
  | {
42
41
  action: "cancel";
43
42
  id: string;
44
- }
45
- | {
46
- action: "wait";
47
- id: string;
48
- /** Defaults to 10 minutes. Use null to wait indefinitely. */
49
- timeoutMs?: number | null;
50
43
  };
51
44
 
52
45
  export interface WorkflowOutput {
53
46
  workflow?: WorkflowRun;
54
- timedOut?: boolean;
55
47
  message: string;
56
48
  }
57
49
 
@@ -92,8 +84,8 @@ const WorkflowStageParams = Type.Object(
92
84
  );
93
85
 
94
86
  const WorkflowActionParams = Type.String({
95
- enum: ["start", "status", "cancel", "wait"],
96
- description: "start launches; status inspects; cancel closes active runs; wait awaits terminal workflow state.",
87
+ enum: ["start", "status", "cancel"],
88
+ description: "start launches; status inspects progress or results; cancel closes active runs.",
97
89
  });
98
90
 
99
91
  const WorkflowToolParams = Type.Object(
@@ -106,7 +98,7 @@ const WorkflowToolParams = Type.Object(
106
98
  ),
107
99
  id: Type.Optional(
108
100
  Type.String({
109
- description: "Required for status/cancel/wait. Workflow id/name.",
101
+ description: "Required for status/cancel. Workflow id/name.",
110
102
  }),
111
103
  ),
112
104
  goal: Type.Optional(
@@ -120,19 +112,6 @@ const WorkflowToolParams = Type.Object(
120
112
  minItems: 1,
121
113
  }),
122
114
  ),
123
- timeoutMs: Type.Optional(
124
- Type.Union(
125
- [
126
- Type.Number({
127
- exclusiveMinimum: 0,
128
- }),
129
- Type.Null(),
130
- ],
131
- {
132
- description: "Optional for action=wait. Positive ms; default 10 min; null waits indefinitely.",
133
- },
134
- ),
135
- ),
136
115
  },
137
116
  { additionalProperties: false },
138
117
  );
@@ -161,24 +140,9 @@ export function createWorkflowTool({ orchestra, store }: WorkflowToolDeps): Work
161
140
  }
162
141
 
163
142
  const workflow = findWorkflow(store, input.id);
164
- if (!workflow) {
165
- return input.action === "wait"
166
- ? { timedOut: false, message: formatWorkflowNotFound(input.id) }
167
- : { message: formatWorkflowNotFound(input.id) };
168
- }
143
+ if (!workflow) return { message: formatWorkflowNotFound(input.id) };
169
144
 
170
- if (input.action === "status") {
171
- return { workflow, message: formatWorkflowMessage(workflow) };
172
- }
173
-
174
- if (input.action === "wait") {
175
- const result = await waitWorkflow(store, workflow.id, input.timeoutMs);
176
- return {
177
- workflow: result.workflow,
178
- timedOut: result.timedOut,
179
- message: formatWaitWorkflowMessage(result.workflow, result.timedOut, input.id),
180
- };
181
- }
145
+ if (input.action === "status") return { workflow, message: formatWorkflowMessage(workflow) };
182
146
 
183
147
  const closedWorkflow = await closeWorkflow(orchestra, store, workflow);
184
148
  return { workflow: closedWorkflow, message: formatWorkflowMessage(closedWorkflow, "Workflow cancelled.") };
@@ -194,12 +158,12 @@ export function defineWorkflowPiTool(
194
158
  name: "workflow",
195
159
  label: "Workflow",
196
160
  description: "Run linear workgroup stages with automatic restricted stage leaders.",
197
- promptSnippet: "Launch a multi-stage workflow, then use workflow wait/status for progress and final output.",
161
+ promptSnippet: "Launch a multi-stage workflow; completion is delivered automatically as a pi-orchestra event.",
198
162
  promptGuidelines: [
199
163
  "Use workflow for ordered multi-stage work; not branching/DAG plans.",
200
164
  "Each stage gets its own bus and automatic leader; previous outputs feed the next stage.",
201
165
  "Use compete when one worker success is enough; use synthesize when findings must be combined.",
202
- "Use status for progress, wait for terminal success/blocked/failed/closed.",
166
+ "Use workflow status for progress; workflow.finished events deliver terminal success/blocked/failed/closed results.",
203
167
  ],
204
168
  parameters: WorkflowToolParams,
205
169
  executionMode: "sequential",
@@ -272,7 +236,13 @@ async function runStage(workflowId: string, stageIndex: number, deps: WorkflowRu
272
236
  workerRunIds: workgroupOutput.runs.map((run) => run.id),
273
237
  });
274
238
 
275
- const settledWorkgroup = await settleWorkgroupRuns(deps.orchestra, bus.id, stage.strategy);
239
+ const settledWorkgroup = await settleWorkgroupRuns(
240
+ deps.orchestra,
241
+ deps.store,
242
+ bus.id,
243
+ workgroupOutput.runs.map((run) => run.id),
244
+ stage.strategy,
245
+ );
276
246
  if (isWorkflowClosed(deps.store, workflowId)) return;
277
247
 
278
248
  if (stage.strategy === "compete" && !settledWorkgroup.winner) {
@@ -288,7 +258,7 @@ async function runStageLeader(
288
258
  workflowId: string,
289
259
  stageIndex: number,
290
260
  busId: string,
291
- workerResults: WaitRunResult[],
261
+ workerResults: AgentRunResult[],
292
262
  deps: WorkflowRunnerDeps,
293
263
  ): Promise<void> {
294
264
  const workflow = requireWorkflow(deps.store, workflowId);
@@ -310,10 +280,9 @@ async function runStageLeader(
310
280
  leaderRunId: leaderRun.id,
311
281
  });
312
282
 
313
- const leaderSettled = await deps.orchestra.waitBusSettled(busId, { timeoutMs: null });
283
+ const latestLeaderRun = await terminalRunEvent(deps.store, leaderRun.id);
314
284
  if (isWorkflowClosed(deps.store, workflowId)) return;
315
285
 
316
- const latestLeaderRun = leaderSettled.runs.find((run) => run.id === leaderRun.id) ?? leaderRun;
317
286
  const output = buildStageOutput(latestLeaderRun, leaderRun.id, workerResults);
318
287
  finishStage(deps.store, requireWorkflow(deps.store, workflowId), stageIndex, output);
319
288
  }
@@ -391,7 +360,6 @@ function toWorkflowInput(params: RawWorkflowParams): WorkflowInput {
391
360
  }
392
361
 
393
362
  if (!params.id) throw new Error(`workflow action=${params.action} requires id.`);
394
- if (params.action === "wait") return { action: "wait", id: params.id, timeoutMs: params.timeoutMs };
395
363
  return { action: params.action, id: params.id };
396
364
  }
397
365
 
@@ -499,7 +467,7 @@ function buildStageWorkerGoal(workflow: WorkflowRun, stageIndex: number): string
499
467
  return parts.join("\n");
500
468
  }
501
469
 
502
- function buildLeaderTask(workflow: WorkflowRun, stageIndex: number, workerResults: WaitRunResult[]): string {
470
+ function buildLeaderTask(workflow: WorkflowRun, stageIndex: number, workerResults: AgentRunResult[]): string {
503
471
  const stage = workflow.stages[stageIndex];
504
472
  const strategyInstructions =
505
473
  stage.strategy === "compete"
@@ -546,12 +514,12 @@ function formatPreviousStageOutput(stageName: string, output: WorkflowStageOutpu
546
514
  return [`<stage_output name="${stageName}">`, formatStageOutputForPrompt(output), "</stage_output>"].join("\n");
547
515
  }
548
516
 
549
- function formatWorkerResults(workerResults: WaitRunResult[]): string {
517
+ function formatWorkerResults(workerResults: AgentRunResult[]): string {
550
518
  if (workerResults.length === 0) return "None.";
551
519
  return workerResults.map(formatWorkerResult).join("\n\n");
552
520
  }
553
521
 
554
- function formatWorkerResult(result: WaitRunResult): string {
522
+ function formatWorkerResult(result: AgentRunResult): string {
555
523
  const lines = [
556
524
  `<worker_result run_id="${result.runId}" name="${result.name}" profile="${result.profile}" state="${result.state}">`,
557
525
  ];
@@ -575,7 +543,7 @@ function formatJsonData(data: unknown): string {
575
543
  return JSON.stringify(data, null, 2) ?? String(data);
576
544
  }
577
545
 
578
- function buildCompeteNoWinnerOutput(workerResults: WaitRunResult[]): WorkflowStageOutput {
546
+ function buildCompeteNoWinnerOutput(workerResults: AgentRunResult[]): WorkflowStageOutput {
579
547
  const status = workerResults.some((worker) => worker.result?.status === "blocked") ? "blocked" : "failed";
580
548
  const counts = countWorkerResultStatuses(workerResults);
581
549
  const workflowRunResults = workerResults.map(toWorkflowRunResult);
@@ -587,7 +555,7 @@ function buildCompeteNoWinnerOutput(workerResults: WaitRunResult[]): WorkflowSta
587
555
  };
588
556
  }
589
557
 
590
- function countWorkerResultStatuses(workerResults: WaitRunResult[]): Record<AgentResultStatus, number> {
558
+ function countWorkerResultStatuses(workerResults: AgentRunResult[]): Record<AgentResultStatus, number> {
591
559
  const counts: Record<AgentResultStatus, number> = { success: 0, blocked: 0, failed: 0 };
592
560
  for (const worker of workerResults) {
593
561
  if (worker.result) counts[worker.result.status]++;
@@ -598,7 +566,7 @@ function countWorkerResultStatuses(workerResults: WaitRunResult[]): Record<Agent
598
566
  function buildStageOutput(
599
567
  leaderRun: AgentRun,
600
568
  leaderRunId: string,
601
- workerResults: WaitRunResult[],
569
+ workerResults: AgentRunResult[],
602
570
  ): WorkflowStageOutput {
603
571
  if (!leaderRun.result) {
604
572
  return {
@@ -619,7 +587,7 @@ function buildStageOutput(
619
587
  return output;
620
588
  }
621
589
 
622
- function toWorkflowRunResult(result: WaitRunResult) {
590
+ function toWorkflowRunResult(result: AgentRunResult) {
623
591
  const output = {
624
592
  runId: result.runId,
625
593
  name: result.name,
@@ -635,49 +603,21 @@ function formatStageOutput(output: WorkflowStageOutput): string {
635
603
  return parts.join("\n");
636
604
  }
637
605
 
638
- function waitWorkflow(
639
- store: AgentStore,
640
- workflowId: string,
641
- timeoutMs: number | null | undefined,
642
- ): Promise<{ workflow: WorkflowRun; timedOut: boolean }> {
643
- const resolvedTimeoutMs = resolveWaitTimeoutMs("workflow wait", timeoutMs);
644
- const initialWorkflow = requireWorkflow(store, workflowId);
645
- if (isTerminalAgentState(initialWorkflow.state)) {
646
- return Promise.resolve({ workflow: initialWorkflow, timedOut: false });
647
- }
606
+ function terminalRunEvent(store: AgentStore, runId: string): Promise<AgentRun> {
607
+ const initialRun = store.getRun(runId);
608
+ if (initialRun && isTerminalAgentState(initialRun.state)) return Promise.resolve(initialRun);
648
609
 
649
610
  return new Promise((resolve) => {
650
- let settled = false;
651
- let latestWorkflow = initialWorkflow;
652
- let timeout: ReturnType<typeof setTimeout> | undefined;
653
-
654
- const cleanup = () => {
655
- if (timeout) clearTimeout(timeout);
656
- unsubscribe();
657
- };
658
-
659
- const unsubscribe = store.subscribeWorkflows(
660
- (workflow) => {
661
- if (settled) return;
662
- latestWorkflow = workflow;
663
- if (!isTerminalAgentState(workflow.state)) return;
611
+ let unsubscribe: () => void = () => undefined;
612
+ unsubscribe = store.subscribeRuns(
613
+ (run) => {
614
+ if (!isTerminalAgentState(run.state)) return;
664
615
 
665
- settled = true;
666
- cleanup();
667
- resolve({ workflow, timedOut: false });
616
+ unsubscribe();
617
+ resolve(run);
668
618
  },
669
- (workflow) => workflow.id === workflowId,
619
+ (run) => run.id === runId,
670
620
  );
671
-
672
- if (resolvedTimeoutMs !== null) {
673
- timeout = setTimeout(() => {
674
- if (settled) return;
675
-
676
- settled = true;
677
- cleanup();
678
- resolve({ workflow: latestWorkflow, timedOut: true });
679
- }, resolvedTimeoutMs);
680
- }
681
621
  });
682
622
  }
683
623
 
@@ -685,14 +625,6 @@ function formatWorkflowNotFound(id: string): string {
685
625
  return `Workflow ${id} not found.`;
686
626
  }
687
627
 
688
- function formatWaitWorkflowMessage(workflow: WorkflowRun | undefined, timedOut: boolean, requestedId?: string): string {
689
- if (!workflow) return formatWorkflowNotFound(requestedId ?? "");
690
- const label = requestedId === workflow.id ? workflow.id : formatNamedEntityLabel(workflow);
691
- const prefix = timedOut ? "Timed out waiting for" : "Workflow reached terminal state:";
692
- const result = workflow.result ? ` result=${workflow.result.status}` : "";
693
- return `${prefix} ${label}; state=${workflow.state}${result}.`;
694
- }
695
-
696
628
  function formatWorkflowMessage(
697
629
  workflow: WorkflowRun,
698
630
  headline = `Workflow ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`,
@@ -708,12 +640,11 @@ function formatWorkflowMessage(
708
640
  }
709
641
 
710
642
  type RawWorkflowParams = {
711
- action: "start" | "status" | "cancel" | "wait";
643
+ action: "start" | "status" | "cancel";
712
644
  name?: string;
713
645
  id?: string;
714
646
  goal?: string;
715
647
  stages?: RawWorkflowStageParams[];
716
- timeoutMs?: number | null;
717
648
  };
718
649
 
719
650
  type RawWorkflowStageParams = {