@g3un/pi-orchestra 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,17 +2,33 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AgentRun } from "../core/subagent.ts";
3
3
  import type { AgentStore } from "../core/store.ts";
4
4
  import type { WorkflowRun, WorkflowStageRun } from "../core/workflow.ts";
5
- import { formatNamedEntityLabel, isTerminalAgentState } from "../utils.ts";
5
+ import { isTerminalAgentState } from "../utils.ts";
6
6
 
7
7
  const WIDGET_KEY = "pi-orchestra.workflow-monitor";
8
8
  const MAX_MONITORED_WORKFLOWS = 2;
9
9
  const MAX_WIDGET_LINES = 10;
10
+ const DEFAULT_TICK_MS = 1_000;
11
+
12
+ export interface WorkflowMonitorControllerOptions {
13
+ now?: () => number;
14
+ /** Defaults to 1000 ms. Use 0 to disable uptime ticks in tests. */
15
+ tickMs?: number;
16
+ }
10
17
 
11
18
  export class WorkflowMonitorController {
19
+ private readonly now: () => number;
20
+ private readonly tickMs: number;
12
21
  private unsubscribe?: () => void;
22
+ private tickTimer?: ReturnType<typeof setInterval>;
13
23
  private ctx?: ExtensionContext;
14
24
 
15
- constructor(private readonly store: AgentStore) {}
25
+ constructor(
26
+ private readonly store: AgentStore,
27
+ options: WorkflowMonitorControllerOptions = {},
28
+ ) {
29
+ this.now = options.now ?? Date.now;
30
+ this.tickMs = options.tickMs ?? DEFAULT_TICK_MS;
31
+ }
16
32
 
17
33
  hasActiveWorkflows(): boolean {
18
34
  return listActiveWorkflows(this.store).length > 0;
@@ -30,6 +46,7 @@ export class WorkflowMonitorController {
30
46
  unsubscribeWorkflows();
31
47
  };
32
48
  }
49
+ this.startTicking();
33
50
 
34
51
  return this.render();
35
52
  }
@@ -37,6 +54,7 @@ export class WorkflowMonitorController {
37
54
  dispose(): void {
38
55
  this.unsubscribe?.();
39
56
  this.unsubscribe = undefined;
57
+ this.stopTicking();
40
58
 
41
59
  if (this.ctx?.hasUI) {
42
60
  this.ctx.ui.setWidget(WIDGET_KEY, undefined);
@@ -48,7 +66,7 @@ export class WorkflowMonitorController {
48
66
  const ctx = this.ctx;
49
67
  if (!ctx?.hasUI) return false;
50
68
 
51
- const lines = buildWorkflowMonitorLines(this.store);
69
+ const lines = buildWorkflowMonitorLines(this.store, this.now());
52
70
  if (lines.length === 0) {
53
71
  this.dispose();
54
72
  return false;
@@ -57,15 +75,28 @@ export class WorkflowMonitorController {
57
75
  ctx.ui.setWidget(WIDGET_KEY, lines, { placement: "belowEditor" });
58
76
  return true;
59
77
  }
78
+
79
+ private startTicking(): void {
80
+ if (this.tickMs <= 0 || this.tickTimer) return;
81
+ const timer = setInterval(() => this.render(), this.tickMs);
82
+ (timer as typeof timer & { unref?: () => void }).unref?.();
83
+ this.tickTimer = timer;
84
+ }
85
+
86
+ private stopTicking(): void {
87
+ if (!this.tickTimer) return;
88
+ clearInterval(this.tickTimer);
89
+ this.tickTimer = undefined;
90
+ }
60
91
  }
61
92
 
62
- export function buildWorkflowMonitorLines(store: AgentStore): string[] {
93
+ export function buildWorkflowMonitorLines(store: AgentStore, nowMs = Date.now()): string[] {
63
94
  const workflows = listActiveWorkflows(store);
64
95
  if (workflows.length === 0) return [];
65
96
 
66
97
  const lines: string[] = [];
67
98
  for (const workflow of workflows.slice(0, MAX_MONITORED_WORKFLOWS)) {
68
- appendWorkflowLines(lines, store, workflow);
99
+ appendWorkflowLines(lines, store, workflow, nowMs);
69
100
  if (lines.length >= MAX_WIDGET_LINES) break;
70
101
  }
71
102
 
@@ -77,12 +108,14 @@ export function buildWorkflowMonitorLines(store: AgentStore): string[] {
77
108
  return lines.slice(0, MAX_WIDGET_LINES);
78
109
  }
79
110
 
80
- function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun): void {
111
+ function appendWorkflowLines(lines: string[], store: AgentStore, workflow: WorkflowRun, nowMs: number): void {
81
112
  if (lines.length >= MAX_WIDGET_LINES) return;
82
113
 
83
114
  const stage = getCurrentStage(workflow);
84
- const stageLabel = stage ? formatStageLabel(store, workflow, stage) : "none · agents 0/0";
85
- lines.push(`${formatNamedEntityLabel(workflow)} | ${stageLabel}`);
115
+ const stageLabel = stage
116
+ ? formatStageLabel(store, workflow, stage)
117
+ : `none (0/${workflow.stages.length}) | agents (0/0)`;
118
+ lines.push(`${workflow.name} | ${stageLabel} | ${formatWorkflowUptime(workflow, nowMs)}`);
86
119
  }
87
120
 
88
121
  function listActiveWorkflows(store: AgentStore): WorkflowRun[] {
@@ -98,7 +131,7 @@ function getCurrentStage(workflow: WorkflowRun): WorkflowStageRun | undefined {
98
131
  function formatStageLabel(store: AgentStore, workflow: WorkflowRun, stage: WorkflowStageRun): string {
99
132
  const stageIndex = workflow.stages.indexOf(stage);
100
133
  const stagePosition = stageIndex >= 0 ? `${stageIndex + 1}/${workflow.stages.length}` : `?/${workflow.stages.length}`;
101
- return `${stage.name} · step ${stagePosition} · agents ${formatStageProgress(store, stage)}`;
134
+ return `${stage.name} (${stagePosition}) | agents (${formatStageProgress(store, stage)})`;
102
135
  }
103
136
 
104
137
  function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string {
@@ -106,6 +139,22 @@ function formatStageProgress(store: AgentStore, stage: WorkflowStageRun): string
106
139
  return `${progress.completed}/${progress.total}`;
107
140
  }
108
141
 
142
+ function formatWorkflowUptime(workflow: WorkflowRun, nowMs: number): string {
143
+ const elapsedSeconds = Math.max(0, Math.floor((nowMs - workflow.startedAtMs) / 1_000));
144
+ const seconds = elapsedSeconds % 60;
145
+ const totalMinutes = Math.floor(elapsedSeconds / 60);
146
+ const minutes = totalMinutes % 60;
147
+ const hours = Math.floor(totalMinutes / 60);
148
+
149
+ if (hours > 0) return `${hours}h ${pad2(minutes)}m`;
150
+ if (totalMinutes > 0) return `${totalMinutes}m ${pad2(seconds)}s`;
151
+ return `${seconds}s`;
152
+ }
153
+
154
+ function pad2(value: number): string {
155
+ return value.toString().padStart(2, "0");
156
+ }
157
+
109
158
  function calculateStageProgress(store: AgentStore, stage: WorkflowStageRun): { completed: number; total: number } {
110
159
  const runs = collectStageRuns(store, stage);
111
160
  const completed = runs.filter((run) => isTerminalAgentState(run.state)).length;
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
  }
@@ -343,6 +312,7 @@ function createWorkflowRun(
343
312
  return {
344
313
  ...identity,
345
314
  goal: input.goal,
315
+ startedAtMs: Date.now(),
346
316
  state: "idle",
347
317
  currentStageIndex: 0,
348
318
  stages: input.stages.map((stage) => {
@@ -391,7 +361,6 @@ function toWorkflowInput(params: RawWorkflowParams): WorkflowInput {
391
361
  }
392
362
 
393
363
  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
364
  return { action: params.action, id: params.id };
396
365
  }
397
366
 
@@ -499,7 +468,7 @@ function buildStageWorkerGoal(workflow: WorkflowRun, stageIndex: number): string
499
468
  return parts.join("\n");
500
469
  }
501
470
 
502
- function buildLeaderTask(workflow: WorkflowRun, stageIndex: number, workerResults: WaitRunResult[]): string {
471
+ function buildLeaderTask(workflow: WorkflowRun, stageIndex: number, workerResults: AgentRunResult[]): string {
503
472
  const stage = workflow.stages[stageIndex];
504
473
  const strategyInstructions =
505
474
  stage.strategy === "compete"
@@ -546,12 +515,12 @@ function formatPreviousStageOutput(stageName: string, output: WorkflowStageOutpu
546
515
  return [`<stage_output name="${stageName}">`, formatStageOutputForPrompt(output), "</stage_output>"].join("\n");
547
516
  }
548
517
 
549
- function formatWorkerResults(workerResults: WaitRunResult[]): string {
518
+ function formatWorkerResults(workerResults: AgentRunResult[]): string {
550
519
  if (workerResults.length === 0) return "None.";
551
520
  return workerResults.map(formatWorkerResult).join("\n\n");
552
521
  }
553
522
 
554
- function formatWorkerResult(result: WaitRunResult): string {
523
+ function formatWorkerResult(result: AgentRunResult): string {
555
524
  const lines = [
556
525
  `<worker_result run_id="${result.runId}" name="${result.name}" profile="${result.profile}" state="${result.state}">`,
557
526
  ];
@@ -575,7 +544,7 @@ function formatJsonData(data: unknown): string {
575
544
  return JSON.stringify(data, null, 2) ?? String(data);
576
545
  }
577
546
 
578
- function buildCompeteNoWinnerOutput(workerResults: WaitRunResult[]): WorkflowStageOutput {
547
+ function buildCompeteNoWinnerOutput(workerResults: AgentRunResult[]): WorkflowStageOutput {
579
548
  const status = workerResults.some((worker) => worker.result?.status === "blocked") ? "blocked" : "failed";
580
549
  const counts = countWorkerResultStatuses(workerResults);
581
550
  const workflowRunResults = workerResults.map(toWorkflowRunResult);
@@ -587,7 +556,7 @@ function buildCompeteNoWinnerOutput(workerResults: WaitRunResult[]): WorkflowSta
587
556
  };
588
557
  }
589
558
 
590
- function countWorkerResultStatuses(workerResults: WaitRunResult[]): Record<AgentResultStatus, number> {
559
+ function countWorkerResultStatuses(workerResults: AgentRunResult[]): Record<AgentResultStatus, number> {
591
560
  const counts: Record<AgentResultStatus, number> = { success: 0, blocked: 0, failed: 0 };
592
561
  for (const worker of workerResults) {
593
562
  if (worker.result) counts[worker.result.status]++;
@@ -598,7 +567,7 @@ function countWorkerResultStatuses(workerResults: WaitRunResult[]): Record<Agent
598
567
  function buildStageOutput(
599
568
  leaderRun: AgentRun,
600
569
  leaderRunId: string,
601
- workerResults: WaitRunResult[],
570
+ workerResults: AgentRunResult[],
602
571
  ): WorkflowStageOutput {
603
572
  if (!leaderRun.result) {
604
573
  return {
@@ -619,7 +588,7 @@ function buildStageOutput(
619
588
  return output;
620
589
  }
621
590
 
622
- function toWorkflowRunResult(result: WaitRunResult) {
591
+ function toWorkflowRunResult(result: AgentRunResult) {
623
592
  const output = {
624
593
  runId: result.runId,
625
594
  name: result.name,
@@ -635,49 +604,21 @@ function formatStageOutput(output: WorkflowStageOutput): string {
635
604
  return parts.join("\n");
636
605
  }
637
606
 
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
- }
607
+ function terminalRunEvent(store: AgentStore, runId: string): Promise<AgentRun> {
608
+ const initialRun = store.getRun(runId);
609
+ if (initialRun && isTerminalAgentState(initialRun.state)) return Promise.resolve(initialRun);
648
610
 
649
611
  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;
612
+ let unsubscribe: () => void = () => undefined;
613
+ unsubscribe = store.subscribeRuns(
614
+ (run) => {
615
+ if (!isTerminalAgentState(run.state)) return;
664
616
 
665
- settled = true;
666
- cleanup();
667
- resolve({ workflow, timedOut: false });
617
+ unsubscribe();
618
+ resolve(run);
668
619
  },
669
- (workflow) => workflow.id === workflowId,
620
+ (run) => run.id === runId,
670
621
  );
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
622
  });
682
623
  }
683
624
 
@@ -685,14 +626,6 @@ function formatWorkflowNotFound(id: string): string {
685
626
  return `Workflow ${id} not found.`;
686
627
  }
687
628
 
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
629
  function formatWorkflowMessage(
697
630
  workflow: WorkflowRun,
698
631
  headline = `Workflow ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`,
@@ -708,12 +641,11 @@ function formatWorkflowMessage(
708
641
  }
709
642
 
710
643
  type RawWorkflowParams = {
711
- action: "start" | "status" | "cancel" | "wait";
644
+ action: "start" | "status" | "cancel";
712
645
  name?: string;
713
646
  id?: string;
714
647
  goal?: string;
715
648
  stages?: RawWorkflowStageParams[];
716
- timeoutMs?: number | null;
717
649
  };
718
650
 
719
651
  type RawWorkflowStageParams = {