@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.
- package/README.md +8 -6
- package/docs/orchestration-model.md +8 -4
- package/package.json +12 -2
- package/src/core/orchestra.ts +1 -195
- package/src/core/subagent.ts +8 -0
- package/src/core/workflow.ts +1 -0
- package/src/extension/index.ts +33 -3
- package/src/extension/orchestra-events.ts +231 -0
- package/src/extension/workflow-monitor.ts +58 -9
- package/src/tools/bus.ts +9 -133
- package/src/tools/workflow.ts +37 -105
- package/src/tools/workgroup.ts +186 -71
- package/src/utils.ts +4 -14
|
@@ -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 {
|
|
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(
|
|
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
|
|
85
|
-
|
|
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}
|
|
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
|
|
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"
|
|
61
|
-
description: "create/status/publish
|
|
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
|
|
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
|
-
"
|
|
169
|
-
"publish
|
|
170
|
-
"
|
|
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"
|
|
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
|
};
|
package/src/tools/workflow.ts
CHANGED
|
@@ -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
|
|
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"
|
|
96
|
-
description: "start launches; status inspects; cancel closes active runs
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
639
|
-
store
|
|
640
|
-
|
|
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
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
resolve({ workflow, timedOut: false });
|
|
617
|
+
unsubscribe();
|
|
618
|
+
resolve(run);
|
|
668
619
|
},
|
|
669
|
-
(
|
|
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"
|
|
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 = {
|