@g3un/pi-orchestra 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/docs/orchestration-model.md +69 -0
- package/package.json +56 -0
- package/src/adapters/in-memory-store.ts +85 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/pi-runtime.ts +348 -0
- package/src/core/bus-format.ts +14 -0
- package/src/core/bus.ts +11 -0
- package/src/core/index.ts +8 -0
- package/src/core/orchestra.ts +322 -0
- package/src/core/runtime.ts +14 -0
- package/src/core/store.ts +21 -0
- package/src/core/subagent.ts +27 -0
- package/src/core/workflow.ts +49 -0
- package/src/core/workgroup.ts +12 -0
- package/src/extension/index.ts +58 -0
- package/src/index.ts +4 -0
- package/src/profiles/index.ts +1 -0
- package/src/profiles/stage-leader.ts +20 -0
- package/src/tools/bus.ts +275 -0
- package/src/tools/index.ts +4 -0
- package/src/tools/subagent.ts +243 -0
- package/src/tools/workflow.ts +712 -0
- package/src/tools/workgroup.ts +422 -0
- package/src/utils.ts +101 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import type { AgentResultStatus, AgentRun } from "../core/subagent.ts";
|
|
4
|
+
import type { OrchestraApi, WaitRunResult } from "../core/orchestra.ts";
|
|
5
|
+
import type { AgentStore } from "../core/store.ts";
|
|
6
|
+
import type { WorkflowRun, WorkflowStageOutput, WorkflowStageRun, WorkflowStageSpec } from "../core/workflow.ts";
|
|
7
|
+
import { WORKGROUP_STRATEGY_VALUES, type WorkgroupMember, type WorkgroupStrategy } from "../core/workgroup.ts";
|
|
8
|
+
import { createStageLeaderProfile } from "../profiles/stage-leader.ts";
|
|
9
|
+
import {
|
|
10
|
+
closeAgentRuns,
|
|
11
|
+
createEntityIdentity,
|
|
12
|
+
formatError,
|
|
13
|
+
findWorkflow,
|
|
14
|
+
formatNamedEntityLabel,
|
|
15
|
+
isTerminalAgentState,
|
|
16
|
+
normalizeEntityName,
|
|
17
|
+
requireWorkflow,
|
|
18
|
+
resolveWaitTimeoutMs,
|
|
19
|
+
} from "../utils.ts";
|
|
20
|
+
import {
|
|
21
|
+
createWorkgroupTool,
|
|
22
|
+
settleWorkgroupRuns,
|
|
23
|
+
toWorkgroupMember,
|
|
24
|
+
type RawWorkgroupMemberParams,
|
|
25
|
+
withDefaultModelForWorkgroupMember,
|
|
26
|
+
withDefaultModelsForWorkgroupMembers,
|
|
27
|
+
WorkgroupMemberParams,
|
|
28
|
+
} from "./workgroup.ts";
|
|
29
|
+
|
|
30
|
+
export type WorkflowInput =
|
|
31
|
+
| {
|
|
32
|
+
action: "start";
|
|
33
|
+
name?: string;
|
|
34
|
+
goal: string;
|
|
35
|
+
stages: WorkflowStageSpec[];
|
|
36
|
+
}
|
|
37
|
+
| {
|
|
38
|
+
action: "status";
|
|
39
|
+
id: string;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
action: "cancel";
|
|
43
|
+
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
|
+
};
|
|
51
|
+
|
|
52
|
+
export interface WorkflowOutput {
|
|
53
|
+
workflow?: WorkflowRun;
|
|
54
|
+
timedOut?: boolean;
|
|
55
|
+
message: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface WorkflowTool {
|
|
59
|
+
name: "workflow";
|
|
60
|
+
execute(input: WorkflowInput): Promise<WorkflowOutput>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorkflowToolDeps {
|
|
64
|
+
orchestra: OrchestraApi;
|
|
65
|
+
store: AgentStore;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const WorkflowStageParams = Type.Object(
|
|
69
|
+
{
|
|
70
|
+
name: Type.String({
|
|
71
|
+
description: "Short unique stage name within this linear workflow.",
|
|
72
|
+
}),
|
|
73
|
+
goal: Type.String({
|
|
74
|
+
description: "Stage-specific goal.",
|
|
75
|
+
}),
|
|
76
|
+
strategy: Type.String({
|
|
77
|
+
enum: [...WORKGROUP_STRATEGY_VALUES],
|
|
78
|
+
description: "compete = one success is enough; synthesize = combine complementary findings.",
|
|
79
|
+
}),
|
|
80
|
+
members: Type.Array(WorkgroupMemberParams, {
|
|
81
|
+
description: "Worker subagents for this stage.",
|
|
82
|
+
minItems: 1,
|
|
83
|
+
}),
|
|
84
|
+
leader: Type.Optional(WorkgroupMemberParams),
|
|
85
|
+
},
|
|
86
|
+
{ additionalProperties: false },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const WorkflowActionParams = Type.String({
|
|
90
|
+
enum: ["start", "status", "cancel", "wait"],
|
|
91
|
+
description: "start launches; status inspects; cancel closes active runs; wait awaits terminal workflow state.",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const WorkflowToolParams = Type.Object(
|
|
95
|
+
{
|
|
96
|
+
action: WorkflowActionParams,
|
|
97
|
+
name: Type.Optional(
|
|
98
|
+
Type.String({
|
|
99
|
+
description: "Optional workflow name.",
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
id: Type.Optional(
|
|
103
|
+
Type.String({
|
|
104
|
+
description: "Required for status/cancel/wait. Workflow id/name.",
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
goal: Type.Optional(
|
|
108
|
+
Type.String({
|
|
109
|
+
description: "Required for start. Overall workflow goal.",
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
stages: Type.Optional(
|
|
113
|
+
Type.Array(WorkflowStageParams, {
|
|
114
|
+
description: "Required for action=start. Linear stages executed in order with automatic stage leaders.",
|
|
115
|
+
minItems: 1,
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
timeoutMs: Type.Optional(
|
|
119
|
+
Type.Union(
|
|
120
|
+
[
|
|
121
|
+
Type.Number({
|
|
122
|
+
exclusiveMinimum: 0,
|
|
123
|
+
}),
|
|
124
|
+
Type.Null(),
|
|
125
|
+
],
|
|
126
|
+
{
|
|
127
|
+
description: "Optional for action=wait. Positive ms; default 10 min; null waits indefinitely.",
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
{ additionalProperties: false },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
export function createWorkflowTool({ orchestra, store }: WorkflowToolDeps): WorkflowTool {
|
|
136
|
+
const workgroupTool = createWorkgroupTool({ orchestra });
|
|
137
|
+
const runnerTasks = new Map<string, Promise<void>>();
|
|
138
|
+
|
|
139
|
+
const startRunner = (workflowId: string) => {
|
|
140
|
+
const task = runWorkflow(workflowId, { orchestra, store, workgroupTool })
|
|
141
|
+
.catch((error) => failWorkflow(store, workflowId, formatError(error)))
|
|
142
|
+
.finally(() => runnerTasks.delete(workflowId));
|
|
143
|
+
runnerTasks.set(workflowId, task);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
name: "workflow",
|
|
148
|
+
|
|
149
|
+
async execute(input) {
|
|
150
|
+
if (input.action === "start") {
|
|
151
|
+
const workflow = createWorkflowRun(input, store.listWorkflows());
|
|
152
|
+
store.saveWorkflow(workflow);
|
|
153
|
+
startRunner(workflow.id);
|
|
154
|
+
const startedWorkflow = store.getWorkflow(workflow.id) ?? workflow;
|
|
155
|
+
return { workflow: startedWorkflow, message: formatWorkflowMessage(startedWorkflow) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const workflow = findWorkflow(store, input.id);
|
|
159
|
+
if (!workflow) {
|
|
160
|
+
return input.action === "wait"
|
|
161
|
+
? { timedOut: false, message: formatWorkflowNotFound(input.id) }
|
|
162
|
+
: { message: formatWorkflowNotFound(input.id) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (input.action === "status") {
|
|
166
|
+
return { workflow, message: formatWorkflowMessage(workflow) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (input.action === "wait") {
|
|
170
|
+
const result = await waitWorkflow(store, workflow.id, input.timeoutMs);
|
|
171
|
+
return {
|
|
172
|
+
workflow: result.workflow,
|
|
173
|
+
timedOut: result.timedOut,
|
|
174
|
+
message: formatWaitWorkflowMessage(result.workflow, result.timedOut, input.id),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const closedWorkflow = await closeWorkflow(orchestra, store, workflow);
|
|
179
|
+
return { workflow: closedWorkflow, message: formatWorkflowMessage(closedWorkflow, "Workflow cancelled.") };
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function defineWorkflowPiTool(resolveTool: (ctx: ExtensionContext) => WorkflowTool) {
|
|
185
|
+
return defineTool({
|
|
186
|
+
name: "workflow",
|
|
187
|
+
label: "Workflow",
|
|
188
|
+
description: "Run linear workgroup stages with automatic restricted stage leaders.",
|
|
189
|
+
promptSnippet: "Launch a multi-stage workflow, then use workflow wait/status for progress and final output.",
|
|
190
|
+
promptGuidelines: [
|
|
191
|
+
"Use workflow for ordered multi-stage work; not branching/DAG plans.",
|
|
192
|
+
"Each stage gets its own bus and automatic leader; previous outputs feed the next stage.",
|
|
193
|
+
"Use compete when one worker success is enough; use synthesize when findings must be combined.",
|
|
194
|
+
"Use status for progress, wait for terminal success/blocked/failed/closed.",
|
|
195
|
+
],
|
|
196
|
+
parameters: WorkflowToolParams,
|
|
197
|
+
executionMode: "sequential",
|
|
198
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
199
|
+
const output = await resolveTool(ctx).execute(
|
|
200
|
+
withDefaultModels(toWorkflowInput(params as RawWorkflowParams), ctx),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: output.message }],
|
|
205
|
+
details: output,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface WorkflowRunnerDeps {
|
|
212
|
+
orchestra: OrchestraApi;
|
|
213
|
+
store: AgentStore;
|
|
214
|
+
workgroupTool: ReturnType<typeof createWorkgroupTool>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runWorkflow(workflowId: string, deps: WorkflowRunnerDeps): Promise<void> {
|
|
218
|
+
for (;;) {
|
|
219
|
+
const workflow = deps.store.getWorkflow(workflowId);
|
|
220
|
+
if (!workflow || isTerminalAgentState(workflow.state)) return;
|
|
221
|
+
|
|
222
|
+
const stageIndex = workflow.stages.findIndex((stage) => stage.state === "idle" && !stage.phase);
|
|
223
|
+
if (stageIndex < 0) {
|
|
224
|
+
if (workflow.state === "idle")
|
|
225
|
+
deps.store.saveWorkflow({
|
|
226
|
+
...workflow,
|
|
227
|
+
state: "success",
|
|
228
|
+
currentStageIndex: workflow.stages.length - 1,
|
|
229
|
+
result: workflow.result,
|
|
230
|
+
});
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await runStage(workflowId, stageIndex, deps);
|
|
235
|
+
|
|
236
|
+
const updatedWorkflow = deps.store.getWorkflow(workflowId);
|
|
237
|
+
if (!updatedWorkflow || isTerminalAgentState(updatedWorkflow.state)) return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function runStage(workflowId: string, stageIndex: number, deps: WorkflowRunnerDeps): Promise<void> {
|
|
242
|
+
const workflow = requireWorkflow(deps.store, workflowId);
|
|
243
|
+
const stage = workflow.stages[stageIndex];
|
|
244
|
+
const bus = deps.orchestra.createBus({ name: `${workflow.name}-${stage.name}` });
|
|
245
|
+
updateStage(deps.store, workflow, stageIndex, { state: "idle", phase: "workers", busId: bus.id });
|
|
246
|
+
|
|
247
|
+
const workgroupOutput = await deps.workgroupTool.execute({
|
|
248
|
+
busId: bus.id,
|
|
249
|
+
goal: buildStageWorkerGoal(workflow, stageIndex),
|
|
250
|
+
strategy: stage.strategy as WorkgroupStrategy,
|
|
251
|
+
members: stage.members as WorkgroupMember[],
|
|
252
|
+
});
|
|
253
|
+
if (isWorkflowClosed(deps.store, workflowId)) {
|
|
254
|
+
await closeAgentRuns(
|
|
255
|
+
deps.orchestra,
|
|
256
|
+
workgroupOutput.runs.map((run) => run.id),
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
updateStage(deps.store, requireWorkflow(deps.store, workflowId), stageIndex, {
|
|
262
|
+
workerRunIds: workgroupOutput.runs.map((run) => run.id),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const settledWorkgroup = await settleWorkgroupRuns(deps.orchestra, bus.id, stage.strategy);
|
|
266
|
+
if (isWorkflowClosed(deps.store, workflowId)) return;
|
|
267
|
+
|
|
268
|
+
if (stage.strategy === "compete" && !settledWorkgroup.winner) {
|
|
269
|
+
const output = buildCompeteNoWinnerOutput(settledWorkgroup.completedResults);
|
|
270
|
+
finishStage(deps.store, requireWorkflow(deps.store, workflowId), stageIndex, output);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await runStageLeader(workflowId, stageIndex, bus.id, settledWorkgroup.workerResults, deps);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function runStageLeader(
|
|
278
|
+
workflowId: string,
|
|
279
|
+
stageIndex: number,
|
|
280
|
+
busId: string,
|
|
281
|
+
workerResults: WaitRunResult[],
|
|
282
|
+
deps: WorkflowRunnerDeps,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const workflow = requireWorkflow(deps.store, workflowId);
|
|
285
|
+
const stage = workflow.stages[stageIndex];
|
|
286
|
+
const leaderRun = await deps.orchestra.spawnAgent(
|
|
287
|
+
stage.leader.profile,
|
|
288
|
+
buildLeaderTask(workflow, stageIndex, workerResults),
|
|
289
|
+
busId,
|
|
290
|
+
{ name: stage.leader.name },
|
|
291
|
+
);
|
|
292
|
+
if (isWorkflowClosed(deps.store, workflowId)) {
|
|
293
|
+
await deps.orchestra.closeAgent(leaderRun.id);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
updateStage(deps.store, requireWorkflow(deps.store, workflowId), stageIndex, {
|
|
298
|
+
state: "idle",
|
|
299
|
+
phase: "leader",
|
|
300
|
+
leaderRunId: leaderRun.id,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const leaderSettled = await deps.orchestra.waitBusSettled(busId, { timeoutMs: null });
|
|
304
|
+
if (isWorkflowClosed(deps.store, workflowId)) return;
|
|
305
|
+
|
|
306
|
+
const latestLeaderRun = leaderSettled.runs.find((run) => run.id === leaderRun.id) ?? leaderRun;
|
|
307
|
+
const output = buildStageOutput(latestLeaderRun, leaderRun.id, workerResults);
|
|
308
|
+
finishStage(deps.store, requireWorkflow(deps.store, workflowId), stageIndex, output);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function closeWorkflow(orchestra: OrchestraApi, store: AgentStore, workflow: WorkflowRun): Promise<WorkflowRun> {
|
|
312
|
+
const closedWorkflow = markWorkflowClosed(workflow);
|
|
313
|
+
store.saveWorkflow(closedWorkflow);
|
|
314
|
+
|
|
315
|
+
const runIds = collectWorkflowRunIds(workflow);
|
|
316
|
+
const busRunIds = workflow.stages.flatMap((stage) =>
|
|
317
|
+
stage.busId ? orchestra.listRuns({ busId: stage.busId }).map((run) => run.id) : [],
|
|
318
|
+
);
|
|
319
|
+
await closeAgentRuns(orchestra, [...new Set([...runIds, ...busRunIds])]);
|
|
320
|
+
|
|
321
|
+
const latestWorkflow = store.getWorkflow(workflow.id) ?? closedWorkflow;
|
|
322
|
+
const latestClosedWorkflow = markWorkflowClosed(latestWorkflow);
|
|
323
|
+
store.saveWorkflow(latestClosedWorkflow);
|
|
324
|
+
return latestClosedWorkflow;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function createWorkflowRun(
|
|
328
|
+
input: Extract<WorkflowInput, { action: "start" }>,
|
|
329
|
+
existingWorkflows: WorkflowRun[],
|
|
330
|
+
): WorkflowRun {
|
|
331
|
+
validateStages(input.stages);
|
|
332
|
+
const identity = createEntityIdentity(input.name, "workflow", existingWorkflows, "Workflow");
|
|
333
|
+
return {
|
|
334
|
+
...identity,
|
|
335
|
+
goal: input.goal,
|
|
336
|
+
state: "idle",
|
|
337
|
+
currentStageIndex: 0,
|
|
338
|
+
stages: input.stages.map((stage) => {
|
|
339
|
+
const stageRun = {
|
|
340
|
+
...stage,
|
|
341
|
+
name: normalizeEntityName(stage.name, "Workflow stage"),
|
|
342
|
+
state: "idle" as const,
|
|
343
|
+
workerRunIds: [],
|
|
344
|
+
};
|
|
345
|
+
return { ...stageRun, leader: resolveStageLeader(stage, identity.name) };
|
|
346
|
+
}),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function validateStages(stages: WorkflowStageSpec[]): void {
|
|
351
|
+
if (stages.length === 0) throw new Error("workflow requires at least one stage.");
|
|
352
|
+
|
|
353
|
+
const names = new Set<string>();
|
|
354
|
+
for (const stage of stages) {
|
|
355
|
+
const name = normalizeEntityName(stage.name, "Workflow stage");
|
|
356
|
+
if (names.has(name)) throw new Error(`Workflow stage name "${name}" is already in use.`);
|
|
357
|
+
names.add(name);
|
|
358
|
+
if (stage.members.length === 0) throw new Error(`Workflow stage "${name}" requires at least one member.`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function resolveStageLeader(stage: WorkflowStageSpec, workflowName: string): WorkgroupMember {
|
|
363
|
+
const stageName = normalizeEntityName(stage.name, "Workflow stage");
|
|
364
|
+
const leader = stage.leader ?? {
|
|
365
|
+
profile: createStageLeaderProfile({ name: `${workflowName}-${stageName}-leader` }),
|
|
366
|
+
};
|
|
367
|
+
return {
|
|
368
|
+
...leader,
|
|
369
|
+
profile: {
|
|
370
|
+
...leader.profile,
|
|
371
|
+
tools: [],
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function toWorkflowInput(params: RawWorkflowParams): WorkflowInput {
|
|
377
|
+
if (params.action === "start") {
|
|
378
|
+
if (!params.goal) throw new Error("workflow action=start requires goal.");
|
|
379
|
+
if (!params.stages || params.stages.length === 0) throw new Error("workflow action=start requires stages.");
|
|
380
|
+
return { action: "start", name: params.name, goal: params.goal, stages: params.stages.map(toWorkflowStageSpec) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!params.id) throw new Error(`workflow action=${params.action} requires id.`);
|
|
384
|
+
if (params.action === "wait") return { action: "wait", id: params.id, timeoutMs: params.timeoutMs };
|
|
385
|
+
return { action: params.action, id: params.id };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function toWorkflowStageSpec(stage: RawWorkflowStageParams): WorkflowStageSpec {
|
|
389
|
+
if (!stage.name) throw new Error("workflow stage requires name.");
|
|
390
|
+
if (!stage.goal) throw new Error(`workflow stage ${stage.name} requires goal.`);
|
|
391
|
+
if (!stage.strategy) throw new Error(`workflow stage ${stage.name} requires strategy.`);
|
|
392
|
+
if (!stage.members || stage.members.length === 0) throw new Error(`workflow stage ${stage.name} requires members.`);
|
|
393
|
+
|
|
394
|
+
const spec: WorkflowStageSpec = {
|
|
395
|
+
name: stage.name,
|
|
396
|
+
goal: stage.goal,
|
|
397
|
+
strategy: stage.strategy,
|
|
398
|
+
members: stage.members.map((member, index) => toWorkgroupMember(member, `workflow member ${index + 1}`)),
|
|
399
|
+
};
|
|
400
|
+
if (stage.leader) spec.leader = toWorkgroupMember(stage.leader, "workflow leader");
|
|
401
|
+
return spec;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function withDefaultModels(input: WorkflowInput, ctx: ExtensionContext): WorkflowInput {
|
|
405
|
+
if (input.action !== "start") return input;
|
|
406
|
+
return {
|
|
407
|
+
...input,
|
|
408
|
+
stages: input.stages.map((stage) => ({
|
|
409
|
+
...stage,
|
|
410
|
+
members: withDefaultModelsForWorkgroupMembers(stage.members, ctx),
|
|
411
|
+
leader: stage.leader ? withDefaultModelForWorkgroupMember(stage.leader, ctx) : undefined,
|
|
412
|
+
})),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function updateStage(
|
|
417
|
+
store: AgentStore,
|
|
418
|
+
workflow: WorkflowRun,
|
|
419
|
+
stageIndex: number,
|
|
420
|
+
updates: Partial<WorkflowStageRun>,
|
|
421
|
+
): void {
|
|
422
|
+
if (isWorkflowClosed(store, workflow.id)) return;
|
|
423
|
+
|
|
424
|
+
const stages = workflow.stages.map((stage, index) => (index === stageIndex ? { ...stage, ...updates } : stage));
|
|
425
|
+
store.saveWorkflow({ ...workflow, currentStageIndex: stageIndex, stages });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function finishStage(store: AgentStore, workflow: WorkflowRun, stageIndex: number, output: WorkflowStageOutput): void {
|
|
429
|
+
updateStage(store, workflow, stageIndex, { state: output.status, phase: undefined, output });
|
|
430
|
+
|
|
431
|
+
const updatedWorkflow = requireWorkflow(store, workflow.id);
|
|
432
|
+
const isLastStage = stageIndex === updatedWorkflow.stages.length - 1;
|
|
433
|
+
if (output.status !== "success" || isLastStage) {
|
|
434
|
+
store.saveWorkflow({
|
|
435
|
+
...updatedWorkflow,
|
|
436
|
+
state: output.status,
|
|
437
|
+
result: output,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function markWorkflowClosed(workflow: WorkflowRun): WorkflowRun {
|
|
443
|
+
return {
|
|
444
|
+
...workflow,
|
|
445
|
+
state: "closed",
|
|
446
|
+
stages: workflow.stages.map((stage) => (isTerminalAgentState(stage.state) ? stage : { ...stage, state: "closed" })),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function collectWorkflowRunIds(workflow: WorkflowRun): string[] {
|
|
451
|
+
return workflow.stages
|
|
452
|
+
.flatMap((stage) => [stage.leaderRunId, ...stage.workerRunIds])
|
|
453
|
+
.filter((runId): runId is string => runId !== undefined);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function failWorkflow(store: AgentStore, workflowId: string, error: string): void {
|
|
457
|
+
const workflow = store.getWorkflow(workflowId);
|
|
458
|
+
if (!workflow || isTerminalAgentState(workflow.state)) return;
|
|
459
|
+
|
|
460
|
+
const stages = workflow.stages.map((stage, index) =>
|
|
461
|
+
index === workflow.currentStageIndex && !isTerminalAgentState(stage.state)
|
|
462
|
+
? { ...stage, state: "failed" as const, error }
|
|
463
|
+
: stage,
|
|
464
|
+
);
|
|
465
|
+
store.saveWorkflow({ ...workflow, state: "failed", stages, error });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function isWorkflowClosed(store: AgentStore, id: string): boolean {
|
|
469
|
+
return store.getWorkflow(id)?.state === "closed";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildStageWorkerGoal(workflow: WorkflowRun, stageIndex: number): string {
|
|
473
|
+
const stage = workflow.stages[stageIndex];
|
|
474
|
+
const parts = [
|
|
475
|
+
"Workflow stage context",
|
|
476
|
+
"",
|
|
477
|
+
"Workflow goal:",
|
|
478
|
+
workflow.goal,
|
|
479
|
+
"",
|
|
480
|
+
`Current stage: ${stage.name}`,
|
|
481
|
+
"Stage goal:",
|
|
482
|
+
stage.goal,
|
|
483
|
+
"",
|
|
484
|
+
"Previous stage outputs:",
|
|
485
|
+
"<previous_stage_outputs>",
|
|
486
|
+
formatPreviousStageOutputs(workflow, stageIndex),
|
|
487
|
+
"</previous_stage_outputs>",
|
|
488
|
+
];
|
|
489
|
+
return parts.join("\n");
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function buildLeaderTask(workflow: WorkflowRun, stageIndex: number, workerResults: WaitRunResult[]): string {
|
|
493
|
+
const stage = workflow.stages[stageIndex];
|
|
494
|
+
const strategyInstructions =
|
|
495
|
+
stage.strategy === "compete"
|
|
496
|
+
? ["Compete: condense the winning worker result; do not broaden scope."]
|
|
497
|
+
: ["Synthesize: reconcile worker results into one canonical stage output."];
|
|
498
|
+
|
|
499
|
+
return [
|
|
500
|
+
"You are the leader for this workflow stage.",
|
|
501
|
+
...strategyInstructions,
|
|
502
|
+
"Use supplied context only; prefer finish results over bus context.",
|
|
503
|
+
"",
|
|
504
|
+
"Workflow goal:",
|
|
505
|
+
workflow.goal,
|
|
506
|
+
"",
|
|
507
|
+
`Current stage: ${stage.name}`,
|
|
508
|
+
"Stage goal:",
|
|
509
|
+
stage.goal,
|
|
510
|
+
"",
|
|
511
|
+
"Previous stage outputs:",
|
|
512
|
+
"<previous_stage_outputs>",
|
|
513
|
+
formatPreviousStageOutputs(workflow, stageIndex),
|
|
514
|
+
"</previous_stage_outputs>",
|
|
515
|
+
"",
|
|
516
|
+
"Worker results for this stage:",
|
|
517
|
+
"<worker_results>",
|
|
518
|
+
formatWorkerResults(workerResults),
|
|
519
|
+
"</worker_results>",
|
|
520
|
+
"",
|
|
521
|
+
"Finish:",
|
|
522
|
+
"- Call finish once with concise summary and useful data for the next stage.",
|
|
523
|
+
"- Use blocked/failed worker results as evidence; note gaps.",
|
|
524
|
+
"- Prefer status=success if any useful output exists; blocked if insufficient, failed if synthesis fails.",
|
|
525
|
+
].join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function formatPreviousStageOutputs(workflow: WorkflowRun, stageIndex: number): string {
|
|
529
|
+
const outputs = workflow.stages
|
|
530
|
+
.slice(0, stageIndex)
|
|
531
|
+
.flatMap((stage) => (stage.output ? [formatPreviousStageOutput(stage.name, stage.output)] : []));
|
|
532
|
+
return outputs.length > 0 ? outputs.join("\n\n") : "None.";
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function formatPreviousStageOutput(stageName: string, output: WorkflowStageOutput): string {
|
|
536
|
+
return [`<stage_output name="${stageName}">`, formatStageOutputForPrompt(output), "</stage_output>"].join("\n");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function formatWorkerResults(workerResults: WaitRunResult[]): string {
|
|
540
|
+
if (workerResults.length === 0) return "None.";
|
|
541
|
+
return workerResults.map(formatWorkerResult).join("\n\n");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function formatWorkerResult(result: WaitRunResult): string {
|
|
545
|
+
const lines = [
|
|
546
|
+
`<worker_result run_id="${result.runId}" name="${result.name}" profile="${result.profile}" state="${result.state}">`,
|
|
547
|
+
];
|
|
548
|
+
if (result.result) {
|
|
549
|
+
lines.push(`result: ${result.result.status}`, "summary:", result.result.summary);
|
|
550
|
+
if (result.result.data !== undefined) lines.push("data_json:", formatJsonData(result.result.data));
|
|
551
|
+
} else {
|
|
552
|
+
lines.push("No result payload recorded.");
|
|
553
|
+
}
|
|
554
|
+
lines.push("</worker_result>");
|
|
555
|
+
return lines.join("\n");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function formatStageOutputForPrompt(output: WorkflowStageOutput): string {
|
|
559
|
+
const parts = [`status: ${output.status}`, "summary:", output.summary];
|
|
560
|
+
if (output.data !== undefined) parts.push("data_json:", formatJsonData(output.data));
|
|
561
|
+
return parts.join("\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function formatJsonData(data: unknown): string {
|
|
565
|
+
return JSON.stringify(data, null, 2) ?? String(data);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function buildCompeteNoWinnerOutput(workerResults: WaitRunResult[]): WorkflowStageOutput {
|
|
569
|
+
const status = workerResults.some((worker) => worker.result?.status === "blocked") ? "blocked" : "failed";
|
|
570
|
+
const counts = countWorkerResultStatuses(workerResults);
|
|
571
|
+
const workflowRunResults = workerResults.map(toWorkflowRunResult);
|
|
572
|
+
return {
|
|
573
|
+
status,
|
|
574
|
+
summary: `Compete strategy stage ended without a successful worker result: ${counts.blocked} blocked, ${counts.failed} failed.`,
|
|
575
|
+
data: { workerResults: workflowRunResults },
|
|
576
|
+
workerResults: workflowRunResults,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function countWorkerResultStatuses(workerResults: WaitRunResult[]): Record<AgentResultStatus, number> {
|
|
581
|
+
const counts: Record<AgentResultStatus, number> = { success: 0, blocked: 0, failed: 0 };
|
|
582
|
+
for (const worker of workerResults) {
|
|
583
|
+
if (worker.result) counts[worker.result.status]++;
|
|
584
|
+
}
|
|
585
|
+
return counts;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function buildStageOutput(
|
|
589
|
+
leaderRun: AgentRun,
|
|
590
|
+
leaderRunId: string,
|
|
591
|
+
workerResults: WaitRunResult[],
|
|
592
|
+
): WorkflowStageOutput {
|
|
593
|
+
if (!leaderRun.result) {
|
|
594
|
+
return {
|
|
595
|
+
status: "failed",
|
|
596
|
+
summary: `Stage leader ${leaderRunId} reached ${leaderRun.state} without a result payload.`,
|
|
597
|
+
leaderRunId,
|
|
598
|
+
workerResults: workerResults.map(toWorkflowRunResult),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const output: WorkflowStageOutput = {
|
|
603
|
+
status: leaderRun.result.status,
|
|
604
|
+
summary: leaderRun.result.summary,
|
|
605
|
+
leaderRunId,
|
|
606
|
+
workerResults: workerResults.map(toWorkflowRunResult),
|
|
607
|
+
};
|
|
608
|
+
if (leaderRun.result.data !== undefined) output.data = leaderRun.result.data;
|
|
609
|
+
return output;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function toWorkflowRunResult(result: WaitRunResult) {
|
|
613
|
+
const output = {
|
|
614
|
+
runId: result.runId,
|
|
615
|
+
name: result.name,
|
|
616
|
+
profile: result.profile,
|
|
617
|
+
state: result.state,
|
|
618
|
+
};
|
|
619
|
+
return result.result === undefined ? output : { ...output, result: result.result };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function formatStageOutput(output: WorkflowStageOutput): string {
|
|
623
|
+
const parts = [`status: ${output.status}`, `summary: ${output.summary}`];
|
|
624
|
+
if (output.data !== undefined) parts.push("data:", formatJsonData(output.data));
|
|
625
|
+
return parts.join("\n");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function waitWorkflow(
|
|
629
|
+
store: AgentStore,
|
|
630
|
+
workflowId: string,
|
|
631
|
+
timeoutMs: number | null | undefined,
|
|
632
|
+
): Promise<{ workflow: WorkflowRun; timedOut: boolean }> {
|
|
633
|
+
const resolvedTimeoutMs = resolveWaitTimeoutMs("workflow wait", timeoutMs);
|
|
634
|
+
const initialWorkflow = requireWorkflow(store, workflowId);
|
|
635
|
+
if (isTerminalAgentState(initialWorkflow.state)) {
|
|
636
|
+
return Promise.resolve({ workflow: initialWorkflow, timedOut: false });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return new Promise((resolve) => {
|
|
640
|
+
let settled = false;
|
|
641
|
+
let latestWorkflow = initialWorkflow;
|
|
642
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
643
|
+
|
|
644
|
+
const cleanup = () => {
|
|
645
|
+
if (timeout) clearTimeout(timeout);
|
|
646
|
+
unsubscribe();
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const unsubscribe = store.subscribeWorkflow(workflowId, (workflow) => {
|
|
650
|
+
if (settled) return;
|
|
651
|
+
latestWorkflow = workflow;
|
|
652
|
+
if (!isTerminalAgentState(workflow.state)) return;
|
|
653
|
+
|
|
654
|
+
settled = true;
|
|
655
|
+
cleanup();
|
|
656
|
+
resolve({ workflow, timedOut: false });
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
if (resolvedTimeoutMs !== null) {
|
|
660
|
+
timeout = setTimeout(() => {
|
|
661
|
+
if (settled) return;
|
|
662
|
+
|
|
663
|
+
settled = true;
|
|
664
|
+
cleanup();
|
|
665
|
+
resolve({ workflow: latestWorkflow, timedOut: true });
|
|
666
|
+
}, resolvedTimeoutMs);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function formatWorkflowNotFound(id: string): string {
|
|
672
|
+
return `Workflow ${id} not found.`;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function formatWaitWorkflowMessage(workflow: WorkflowRun | undefined, timedOut: boolean, requestedId?: string): string {
|
|
676
|
+
if (!workflow) return formatWorkflowNotFound(requestedId ?? "");
|
|
677
|
+
const label = requestedId === workflow.id ? workflow.id : formatNamedEntityLabel(workflow);
|
|
678
|
+
const prefix = timedOut ? "Timed out waiting for" : "Workflow reached terminal state:";
|
|
679
|
+
const result = workflow.result ? ` result=${workflow.result.status}` : "";
|
|
680
|
+
return `${prefix} ${label}; state=${workflow.state}${result}.`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function formatWorkflowMessage(
|
|
684
|
+
workflow: WorkflowRun,
|
|
685
|
+
headline = `Workflow ${formatNamedEntityLabel(workflow)} is ${workflow.state}.`,
|
|
686
|
+
): string {
|
|
687
|
+
const parts = [headline, "", `Goal: ${workflow.goal}`, "", "Stages:"];
|
|
688
|
+
for (const [index, stage] of workflow.stages.entries()) {
|
|
689
|
+
const current = index === workflow.currentStageIndex && workflow.state === "idle" ? " current" : "";
|
|
690
|
+
parts.push(`- ${stage.name}: ${stage.state}${current}${stage.busId ? ` bus=${stage.busId}` : ""}`);
|
|
691
|
+
}
|
|
692
|
+
if (workflow.result) parts.push("", "Result:", formatStageOutput(workflow.result));
|
|
693
|
+
if (workflow.error) parts.push("", `Error: ${workflow.error}`);
|
|
694
|
+
return parts.join("\n");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
type RawWorkflowParams = {
|
|
698
|
+
action: "start" | "status" | "cancel" | "wait";
|
|
699
|
+
name?: string;
|
|
700
|
+
id?: string;
|
|
701
|
+
goal?: string;
|
|
702
|
+
stages?: RawWorkflowStageParams[];
|
|
703
|
+
timeoutMs?: number | null;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
type RawWorkflowStageParams = {
|
|
707
|
+
name?: string;
|
|
708
|
+
goal?: string;
|
|
709
|
+
strategy?: WorkgroupStrategy;
|
|
710
|
+
members?: RawWorkgroupMemberParams[];
|
|
711
|
+
leader?: RawWorkgroupMemberParams;
|
|
712
|
+
};
|