bosun 0.40.21 → 0.41.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/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-custom-tools.mjs +23 -5
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +131 -30
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/primary-agent.mjs +81 -7
- package/agent/retry-queue.mjs +164 -0
- package/bench/swebench/bosun-swebench.mjs +5 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +267 -8
- package/config/config-doctor.mjs +51 -2
- package/config/config.mjs +232 -5
- package/github/github-auth-manager.mjs +70 -19
- package/infra/library-manager.mjs +894 -60
- package/infra/monitor.mjs +701 -69
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +95 -28
- package/infra/test-runtime.mjs +267 -0
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +30 -8
- package/server/setup-web-server.mjs +29 -1
- package/server/ui-server.mjs +1571 -49
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-claims.mjs +6 -10
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/chat-view.js +18 -1
- package/ui/components/workspace-switcher.js +321 -9
- package/ui/demo-defaults.js +17830 -10433
- package/ui/demo.html +9 -1
- package/ui/modules/router.js +1 -1
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +376 -37
- package/ui/modules/voice-client.js +173 -33
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +571 -1
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/library.js +410 -55
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +1083 -507
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +38 -1
- package/ui/tabs/workflows.js +1275 -402
- package/voice/voice-agents-sdk.mjs +2 -2
- package/voice/voice-relay.mjs +28 -20
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/project-detection.mjs +559 -0
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-contract.mjs +433 -232
- package/workflow/workflow-engine.mjs +510 -47
- package/workflow/workflow-nodes/custom-loader.mjs +251 -0
- package/workflow/workflow-nodes.mjs +2024 -184
- package/workflow/workflow-templates.mjs +118 -24
- package/workflow-templates/agents.mjs +20 -20
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/code-quality.mjs +20 -14
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +27 -10
- package/workflow-templates/task-execution.mjs +752 -0
- package/workflow-templates/task-lifecycle.mjs +117 -14
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +153 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
package/workflow/pipeline.mjs
CHANGED
|
@@ -1,319 +1,832 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pipeline Orchestration Primitives
|
|
3
|
-
*
|
|
4
|
-
* Declarative multi-agent workflows with:
|
|
5
|
-
* - Pipeline stages
|
|
6
|
-
* - Parallel execution
|
|
7
|
-
* - Conditional branching
|
|
8
|
-
* - Data passing between stages
|
|
9
|
-
* - Rollback support
|
|
10
|
-
*
|
|
11
2
|
* @module pipeline
|
|
3
|
+
* @description Declarative multi-agent pipeline primitives with fresh-context handoff.
|
|
12
4
|
*/
|
|
13
5
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { MsgHub } from "./msg-hub.mjs";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
10
|
+
const DEFAULT_SUMMARY_CHARS = 320;
|
|
11
|
+
|
|
12
|
+
function safeClone(value) {
|
|
13
|
+
if (value == null) return value;
|
|
14
|
+
if (typeof structuredClone === "function") {
|
|
15
|
+
try {
|
|
16
|
+
return structuredClone(value);
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(JSON.stringify(value));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncateText(value, limit = DEFAULT_SUMMARY_CHARS) {
|
|
24
|
+
const text = String(value ?? "").trim();
|
|
25
|
+
if (!text) return "";
|
|
26
|
+
if (text.length <= limit) return text;
|
|
27
|
+
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeError(error) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
return {
|
|
33
|
+
name: error.name,
|
|
34
|
+
message: error.message,
|
|
35
|
+
stack: error.stack || null,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
name: "Error",
|
|
40
|
+
message: String(error ?? "Unknown error"),
|
|
41
|
+
stack: null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeUsage(usage) {
|
|
46
|
+
if (usage == null) return { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
47
|
+
if (typeof usage === "number" && Number.isFinite(usage)) {
|
|
48
|
+
return { promptTokens: 0, completionTokens: 0, totalTokens: Math.max(0, usage) };
|
|
49
|
+
}
|
|
50
|
+
const promptTokens = Number(
|
|
51
|
+
usage.promptTokens ?? usage.inputTokens ?? usage.prompt_tokens ?? 0,
|
|
52
|
+
);
|
|
53
|
+
const completionTokens = Number(
|
|
54
|
+
usage.completionTokens ?? usage.outputTokens ?? usage.completion_tokens ?? 0,
|
|
55
|
+
);
|
|
56
|
+
const totalCandidate = Number(
|
|
57
|
+
usage.totalTokens ?? usage.total_tokens ?? promptTokens + completionTokens,
|
|
58
|
+
);
|
|
59
|
+
return {
|
|
60
|
+
promptTokens: Number.isFinite(promptTokens) ? Math.max(0, promptTokens) : 0,
|
|
61
|
+
completionTokens: Number.isFinite(completionTokens)
|
|
62
|
+
? Math.max(0, completionTokens)
|
|
63
|
+
: 0,
|
|
64
|
+
totalTokens: Number.isFinite(totalCandidate) ? Math.max(0, totalCandidate) : 0,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function addUsageTotals(total, usage) {
|
|
69
|
+
const normalized = normalizeUsage(usage);
|
|
70
|
+
total.promptTokens += normalized.promptTokens;
|
|
71
|
+
total.completionTokens += normalized.completionTokens;
|
|
72
|
+
total.totalTokens += normalized.totalTokens;
|
|
73
|
+
return total;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveValueAtPath(value, path) {
|
|
77
|
+
const normalizedPath = String(path || "").trim();
|
|
78
|
+
if (!normalizedPath) return undefined;
|
|
79
|
+
return normalizedPath.split(".").reduce((acc, part) => {
|
|
80
|
+
if (acc == null) return undefined;
|
|
81
|
+
return acc[part];
|
|
82
|
+
}, value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveTemplate(template, scope) {
|
|
86
|
+
if (typeof template !== "string") return template;
|
|
87
|
+
return template.replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_, path) => {
|
|
88
|
+
const resolved = resolveValueAtPath(scope, path);
|
|
89
|
+
if (resolved == null) return "";
|
|
90
|
+
if (typeof resolved === "string") return resolved;
|
|
91
|
+
return JSON.stringify(resolved, null, 2);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeFilePaths(paths) {
|
|
96
|
+
if (!Array.isArray(paths)) return [];
|
|
97
|
+
return [...new Set(paths.map((entry) => String(entry || "").trim()).filter(Boolean))];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function summarizeOutput(result) {
|
|
101
|
+
const explicit = String(result.summary || "").trim();
|
|
102
|
+
if (explicit) return truncateText(explicit);
|
|
103
|
+
const output = typeof result.output === "string"
|
|
104
|
+
? result.output
|
|
105
|
+
: result.output == null
|
|
106
|
+
? ""
|
|
107
|
+
: JSON.stringify(result.output, null, 2);
|
|
108
|
+
return truncateText(output);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeTransfer(result, fallback = {}) {
|
|
112
|
+
const filePaths = normalizeFilePaths(
|
|
113
|
+
result.filePaths || result.files || result.changedFiles || fallback.filePaths,
|
|
114
|
+
);
|
|
115
|
+
return {
|
|
116
|
+
taskId: result.taskId || fallback.taskId || null,
|
|
117
|
+
branch: result.branch || fallback.branch || null,
|
|
118
|
+
filePaths,
|
|
119
|
+
summary: summarizeOutput(result),
|
|
120
|
+
output: safeClone(result.output),
|
|
121
|
+
metadata:
|
|
122
|
+
result.metadata && typeof result.metadata === "object"
|
|
123
|
+
? safeClone(result.metadata)
|
|
124
|
+
: null,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeAgent(agent, index, options = {}) {
|
|
129
|
+
const agentRunner =
|
|
130
|
+
typeof options.agentRunner === "function" ? options.agentRunner : null;
|
|
131
|
+
if (typeof agent === "function") {
|
|
132
|
+
return {
|
|
133
|
+
id: `agent-${index + 1}`,
|
|
134
|
+
name: agent.name || `agent-${index + 1}`,
|
|
135
|
+
run: agent,
|
|
136
|
+
config: {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
if (agent && typeof agent === "object") {
|
|
140
|
+
const run =
|
|
141
|
+
typeof agent.run === "function"
|
|
142
|
+
? agent.run.bind(agent)
|
|
143
|
+
: typeof agent.execute === "function"
|
|
144
|
+
? agent.execute.bind(agent)
|
|
145
|
+
: null;
|
|
146
|
+
if (!run && !agentRunner) {
|
|
147
|
+
throw new TypeError(`Pipeline agent at index ${index} must expose run() or execute().`);
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
id: String(agent.id || agent.name || `agent-${index + 1}`),
|
|
151
|
+
name: String(agent.name || agent.id || `agent-${index + 1}`),
|
|
152
|
+
run: run || ((descriptor, context) => agentRunner(agent, descriptor, context)),
|
|
153
|
+
config: { ...agent },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (agentRunner) {
|
|
157
|
+
return {
|
|
158
|
+
id: `agent-${index + 1}`,
|
|
159
|
+
name: `agent-${index + 1}`,
|
|
160
|
+
run: (descriptor, context) => agentRunner(agent, descriptor, context),
|
|
161
|
+
config: { value: agent },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
throw new TypeError(`Invalid pipeline agent at index ${index}.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeAgentResult(rawResult) {
|
|
168
|
+
if (rawResult == null) {
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
output: null,
|
|
172
|
+
summary: "",
|
|
173
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
174
|
+
metadata: null,
|
|
175
|
+
branch: null,
|
|
176
|
+
filePaths: [],
|
|
177
|
+
raw: rawResult,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof rawResult !== "object" || Array.isArray(rawResult)) {
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
output: rawResult,
|
|
185
|
+
summary: truncateText(rawResult),
|
|
186
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
187
|
+
metadata: null,
|
|
188
|
+
branch: null,
|
|
189
|
+
filePaths: [],
|
|
190
|
+
raw: rawResult,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const output =
|
|
195
|
+
rawResult.output ?? rawResult.result ?? rawResult.finalResponse ?? rawResult.message ?? null;
|
|
196
|
+
return {
|
|
197
|
+
success: rawResult.success !== false,
|
|
198
|
+
output,
|
|
199
|
+
summary: truncateText(rawResult.summary || output || rawResult.error || ""),
|
|
200
|
+
usage: normalizeUsage(rawResult.usage ?? rawResult.tokensUsed),
|
|
201
|
+
metadata:
|
|
202
|
+
rawResult.metadata && typeof rawResult.metadata === "object"
|
|
203
|
+
? safeClone(rawResult.metadata)
|
|
204
|
+
: null,
|
|
205
|
+
branch: rawResult.branch || null,
|
|
206
|
+
filePaths: normalizeFilePaths(
|
|
207
|
+
rawResult.filePaths || rawResult.files || rawResult.changedFiles,
|
|
208
|
+
),
|
|
209
|
+
error: rawResult.error ? String(rawResult.error) : null,
|
|
210
|
+
raw: safeClone(rawResult),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function makeFreshContext(taskInput, previousOutput) {
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
task: safeClone(taskInput),
|
|
217
|
+
previous: previousOutput ? safeClone(previousOutput) : null,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function relayAbort(sourceSignal, controller) {
|
|
222
|
+
if (!sourceSignal) return () => {};
|
|
223
|
+
const abort = () => controller.abort(sourceSignal.reason);
|
|
224
|
+
if (sourceSignal.aborted) {
|
|
225
|
+
abort();
|
|
226
|
+
return () => {};
|
|
227
|
+
}
|
|
228
|
+
sourceSignal.addEventListener("abort", abort, { once: true });
|
|
229
|
+
return () => sourceSignal.removeEventListener("abort", abort);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function createPipelineResult(kind, runId) {
|
|
233
|
+
return {
|
|
234
|
+
runId,
|
|
235
|
+
kind,
|
|
236
|
+
success: false,
|
|
237
|
+
outputs: [],
|
|
238
|
+
timing: {
|
|
239
|
+
startedAt: new Date().toISOString(),
|
|
240
|
+
startedAtMs: Date.now(),
|
|
241
|
+
finishedAt: null,
|
|
242
|
+
finishedAtMs: null,
|
|
243
|
+
durationMs: 0,
|
|
244
|
+
},
|
|
245
|
+
tokensUsed: {
|
|
246
|
+
promptTokens: 0,
|
|
247
|
+
completionTokens: 0,
|
|
248
|
+
totalTokens: 0,
|
|
249
|
+
},
|
|
250
|
+
errors: [],
|
|
251
|
+
finalOutput: null,
|
|
252
|
+
winner: null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
class BasePipeline {
|
|
257
|
+
constructor(kind, agents, options = {}) {
|
|
258
|
+
this.kind = kind;
|
|
259
|
+
this.agents = (Array.isArray(agents) ? agents : []).map((agent, index) =>
|
|
260
|
+
normalizeAgent(agent, index, options));
|
|
261
|
+
if (this.agents.length === 0) {
|
|
262
|
+
throw new Error(`${kind} pipeline requires at least one agent.`);
|
|
263
|
+
}
|
|
264
|
+
this.options = { ...options };
|
|
265
|
+
this.id = String(options.id || `${kind}-${randomUUID()}`);
|
|
266
|
+
this.name = String(options.name || this.id);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async run(input = {}, runOptions = {}) {
|
|
270
|
+
const result = createPipelineResult(this.kind, randomUUID());
|
|
271
|
+
const managedHub = !this.options.hub && this.options.createHub !== false;
|
|
272
|
+
const hub = this.options.hub || (await MsgHub.create(this.agents, this.options.hubOptions || {}));
|
|
273
|
+
try {
|
|
274
|
+
await this._runInternal(input, runOptions, result, hub);
|
|
275
|
+
} finally {
|
|
276
|
+
result.timing.finishedAtMs = Date.now();
|
|
277
|
+
result.timing.finishedAt = new Date(result.timing.finishedAtMs).toISOString();
|
|
278
|
+
result.timing.durationMs =
|
|
279
|
+
result.timing.finishedAtMs - result.timing.startedAtMs;
|
|
280
|
+
if (managedHub) {
|
|
281
|
+
await hub.close();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async _runInternal() {
|
|
288
|
+
throw new Error("_runInternal must be implemented by subclasses.");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async _invokeAgent(agent, descriptor, runOptions, result, hub, extra = {}) {
|
|
292
|
+
const startedAtMs = Date.now();
|
|
293
|
+
const controller = new AbortController();
|
|
294
|
+
const disconnectAbort = relayAbort(runOptions.signal, controller);
|
|
295
|
+
try {
|
|
296
|
+
const rawResult = await agent.run(descriptor, {
|
|
297
|
+
signal: controller.signal,
|
|
298
|
+
pipeline: {
|
|
299
|
+
id: this.id,
|
|
300
|
+
name: this.name,
|
|
301
|
+
kind: this.kind,
|
|
302
|
+
runId: result.runId,
|
|
303
|
+
},
|
|
304
|
+
hub,
|
|
305
|
+
agent,
|
|
306
|
+
agentIndex: extra.agentIndex ?? -1,
|
|
307
|
+
previousOutput: safeClone(extra.previousOutput ?? null),
|
|
308
|
+
options: { ...this.options, ...runOptions },
|
|
309
|
+
});
|
|
310
|
+
const normalized = normalizeAgentResult(rawResult);
|
|
311
|
+
const finishedAtMs = Date.now();
|
|
312
|
+
const outputEntry = {
|
|
313
|
+
agentId: agent.id,
|
|
314
|
+
agentName: agent.name,
|
|
315
|
+
index: extra.agentIndex ?? -1,
|
|
316
|
+
success: normalized.success,
|
|
317
|
+
output: normalized.output,
|
|
318
|
+
summary: normalized.summary,
|
|
319
|
+
metadata: normalized.metadata,
|
|
320
|
+
branch: normalized.branch,
|
|
321
|
+
filePaths: normalized.filePaths,
|
|
322
|
+
timing: {
|
|
323
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
324
|
+
startedAtMs,
|
|
325
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
326
|
+
finishedAtMs,
|
|
327
|
+
durationMs: finishedAtMs - startedAtMs,
|
|
328
|
+
},
|
|
329
|
+
tokensUsed: normalized.usage,
|
|
330
|
+
raw: normalized.raw,
|
|
331
|
+
error: normalized.error,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
addUsageTotals(result.tokensUsed, normalized.usage);
|
|
335
|
+
if (!normalized.success) {
|
|
336
|
+
const err = normalizeError(normalized.error || `Agent ${agent.name} failed.`);
|
|
337
|
+
result.errors.push({
|
|
338
|
+
agentId: agent.id,
|
|
339
|
+
agentName: agent.name,
|
|
340
|
+
index: outputEntry.index,
|
|
341
|
+
error: err,
|
|
342
|
+
});
|
|
343
|
+
} else if (hub) {
|
|
344
|
+
await hub.publish(agent.id, {
|
|
345
|
+
kind: "agent-output",
|
|
346
|
+
taskId: descriptor.task?.taskId || descriptor.task?.id || null,
|
|
347
|
+
branch: outputEntry.branch || descriptor.task?.branch || null,
|
|
348
|
+
filePaths: outputEntry.filePaths,
|
|
349
|
+
summary: outputEntry.summary,
|
|
350
|
+
metadata: outputEntry.metadata,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { outputEntry, transfer: normalizeTransfer(outputEntry, descriptor.task || {}) };
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const normalizedError = normalizeError(error);
|
|
357
|
+
const finishedAtMs = Date.now();
|
|
358
|
+
const outputEntry = {
|
|
359
|
+
agentId: agent.id,
|
|
360
|
+
agentName: agent.name,
|
|
361
|
+
index: extra.agentIndex ?? -1,
|
|
362
|
+
success: false,
|
|
363
|
+
output: null,
|
|
364
|
+
summary: normalizedError.message,
|
|
365
|
+
metadata: null,
|
|
366
|
+
branch: null,
|
|
367
|
+
filePaths: [],
|
|
368
|
+
timing: {
|
|
369
|
+
startedAt: new Date(startedAtMs).toISOString(),
|
|
370
|
+
startedAtMs,
|
|
371
|
+
finishedAt: new Date(finishedAtMs).toISOString(),
|
|
372
|
+
finishedAtMs,
|
|
373
|
+
durationMs: finishedAtMs - startedAtMs,
|
|
374
|
+
},
|
|
375
|
+
tokensUsed: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
376
|
+
raw: null,
|
|
377
|
+
error: normalizedError.message,
|
|
378
|
+
};
|
|
379
|
+
result.errors.push({
|
|
380
|
+
agentId: agent.id,
|
|
381
|
+
agentName: agent.name,
|
|
382
|
+
index: outputEntry.index,
|
|
383
|
+
error: normalizedError,
|
|
384
|
+
});
|
|
385
|
+
return { outputEntry, transfer: null };
|
|
386
|
+
} finally {
|
|
387
|
+
disconnectAbort();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export class SequentialPipeline extends BasePipeline {
|
|
393
|
+
constructor(agents, options = {}) {
|
|
394
|
+
super("sequential", agents, options);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async _runInternal(input, runOptions, result, hub) {
|
|
398
|
+
let previousOutput = null;
|
|
399
|
+
for (let index = 0; index < this.agents.length; index += 1) {
|
|
400
|
+
const agent = this.agents[index];
|
|
401
|
+
const descriptor = makeFreshContext(input, previousOutput);
|
|
402
|
+
const { outputEntry, transfer } = await this._invokeAgent(
|
|
403
|
+
agent,
|
|
404
|
+
descriptor,
|
|
405
|
+
runOptions,
|
|
406
|
+
result,
|
|
407
|
+
hub,
|
|
408
|
+
{ agentIndex: index, previousOutput },
|
|
409
|
+
);
|
|
410
|
+
result.outputs.push(outputEntry);
|
|
411
|
+
if (!outputEntry.success) {
|
|
412
|
+
result.success = false;
|
|
413
|
+
result.finalOutput = previousOutput;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
previousOutput = transfer;
|
|
417
|
+
result.finalOutput = safeClone(previousOutput);
|
|
418
|
+
}
|
|
419
|
+
result.success = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export class FanoutPipeline extends BasePipeline {
|
|
424
|
+
constructor(agents, options = {}) {
|
|
425
|
+
super("fanout", agents, options);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async _runInternal(input, runOptions, result, hub) {
|
|
429
|
+
const descriptor = makeFreshContext(input, null);
|
|
430
|
+
const settled = await Promise.all(
|
|
431
|
+
this.agents.map((agent, index) =>
|
|
432
|
+
this._invokeAgent(agent, descriptor, runOptions, result, hub, { agentIndex: index }))
|
|
433
|
+
);
|
|
434
|
+
result.outputs = settled.map((entry) => entry.outputEntry);
|
|
435
|
+
result.success = result.outputs.some((entry) => entry.success);
|
|
436
|
+
const firstSuccess = settled.find((entry) => entry.outputEntry.success);
|
|
437
|
+
result.finalOutput = firstSuccess ? safeClone(firstSuccess.transfer) : null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export class RacePipeline extends BasePipeline {
|
|
442
|
+
constructor(agents, options = {}) {
|
|
443
|
+
super("race", agents, options);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async _runInternal(input, runOptions, result, hub) {
|
|
447
|
+
const descriptor = makeFreshContext(input, null);
|
|
448
|
+
const runners = this.agents.map((agent, index) => {
|
|
449
|
+
const controller = new AbortController();
|
|
450
|
+
const disconnectAbort = relayAbort(runOptions.signal, controller);
|
|
451
|
+
const promise = this._invokeAgent(
|
|
452
|
+
{
|
|
453
|
+
...agent,
|
|
454
|
+
run: (agentInput, agentContext) =>
|
|
455
|
+
agent.run(agentInput, { ...agentContext, signal: controller.signal }),
|
|
456
|
+
},
|
|
457
|
+
descriptor,
|
|
458
|
+
{ ...runOptions, signal: controller.signal },
|
|
459
|
+
result,
|
|
460
|
+
hub,
|
|
461
|
+
{ agentIndex: index },
|
|
462
|
+
).finally(disconnectAbort);
|
|
463
|
+
return { agent, index, controller, promise };
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const unresolved = new Map(
|
|
467
|
+
runners.map((entry) => [
|
|
468
|
+
entry.index,
|
|
469
|
+
entry.promise.then((value) => ({ index: entry.index, value })),
|
|
470
|
+
]),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
let winner = null;
|
|
474
|
+
while (unresolved.size > 0) {
|
|
475
|
+
const { index, value } = await Promise.race(unresolved.values());
|
|
476
|
+
unresolved.delete(index);
|
|
477
|
+
result.outputs[index] = value.outputEntry;
|
|
478
|
+
if (value.outputEntry.success) {
|
|
479
|
+
winner = { index, transfer: value.transfer, output: value.outputEntry };
|
|
480
|
+
for (const runner of runners) {
|
|
481
|
+
if (runner.index !== index) {
|
|
482
|
+
runner.controller.abort("race_lost");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
await Promise.allSettled(runners.map((entry) => entry.promise));
|
|
490
|
+
result.outputs = result.outputs.filter(Boolean);
|
|
491
|
+
result.success = Boolean(winner);
|
|
492
|
+
result.winner = winner
|
|
493
|
+
? {
|
|
494
|
+
index: winner.index,
|
|
495
|
+
agentId: winner.output.agentId,
|
|
496
|
+
agentName: winner.output.agentName,
|
|
497
|
+
}
|
|
498
|
+
: null;
|
|
499
|
+
result.finalOutput = winner ? safeClone(winner.transfer) : null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildTaskDescription(taskInput) {
|
|
504
|
+
const task = taskInput?.task || taskInput || {};
|
|
505
|
+
const lines = [];
|
|
506
|
+
const title = String(task.title || task.name || task.taskId || task.id || "").trim();
|
|
507
|
+
const prompt = String(task.prompt || task.description || task.goal || "").trim();
|
|
508
|
+
const branch = String(task.branch || "").trim();
|
|
509
|
+
const filePaths = normalizeFilePaths(task.filePaths || task.files);
|
|
510
|
+
if (title) lines.push(`Title: ${title}`);
|
|
511
|
+
if (prompt) lines.push(`Task: ${prompt}`);
|
|
512
|
+
if (branch) lines.push(`Branch: ${branch}`);
|
|
513
|
+
if (filePaths.length > 0) {
|
|
514
|
+
lines.push(`Files: ${filePaths.join(", ")}`);
|
|
515
|
+
}
|
|
516
|
+
return lines.join("\n");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function buildPreviousStageBlock(input) {
|
|
520
|
+
if (!input?.previous) return "";
|
|
521
|
+
const previous = input.previous;
|
|
522
|
+
const lines = ["Previous stage output:"];
|
|
523
|
+
if (previous.summary) lines.push(previous.summary);
|
|
524
|
+
if (previous.branch) lines.push(`Branch: ${previous.branch}`);
|
|
525
|
+
if (previous.filePaths?.length) lines.push(`Files: ${previous.filePaths.join(", ")}`);
|
|
526
|
+
return lines.join("\n");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const STAGE_PROMPTS = Object.freeze({
|
|
530
|
+
implement: (input) => [
|
|
531
|
+
"You are the implementation stage in a Bosun fresh-context pipeline.",
|
|
532
|
+
"Work only from the structured task descriptor and any prior stage summary.",
|
|
533
|
+
buildTaskDescription(input.task),
|
|
534
|
+
buildPreviousStageBlock(input),
|
|
535
|
+
"Respond with a concise implementation summary, changed files, and branch notes.",
|
|
536
|
+
].filter(Boolean).join("\n\n"),
|
|
537
|
+
test: (input) => [
|
|
538
|
+
"You are the verification stage in a Bosun fresh-context pipeline.",
|
|
539
|
+
buildTaskDescription(input.task),
|
|
540
|
+
buildPreviousStageBlock(input),
|
|
541
|
+
"Validate the work, list tests run or needed, and highlight any gaps.",
|
|
542
|
+
].filter(Boolean).join("\n\n"),
|
|
543
|
+
review: (input) => [
|
|
544
|
+
"You are the review stage in a Bosun fresh-context pipeline.",
|
|
545
|
+
buildTaskDescription(input.task),
|
|
546
|
+
buildPreviousStageBlock(input),
|
|
547
|
+
"Produce a code-review style verdict with risks, blockers, and follow-ups.",
|
|
548
|
+
].filter(Boolean).join("\n\n"),
|
|
549
|
+
search: (input) => [
|
|
550
|
+
"You are a parallel research stage in a Bosun fanout pipeline.",
|
|
551
|
+
buildTaskDescription(input.task),
|
|
552
|
+
"Generate a concise approach, findings, and recommended next step.",
|
|
553
|
+
].filter(Boolean).join("\n\n"),
|
|
554
|
+
vote: (input) => [
|
|
555
|
+
"You are a voting stage in a Bosun consensus pipeline.",
|
|
556
|
+
buildTaskDescription(input.task),
|
|
557
|
+
buildPreviousStageBlock(input),
|
|
558
|
+
"Reply with Verdict: approve|reject|abstain, Confidence: 0-100, and a short rationale.",
|
|
559
|
+
].filter(Boolean).join("\n\n"),
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
function normalizePipelineInput(input) {
|
|
563
|
+
if (typeof input === "string") {
|
|
564
|
+
return { title: "Workflow Task", prompt: input, description: input };
|
|
565
|
+
}
|
|
566
|
+
if (input && typeof input === "object") return safeClone(input);
|
|
567
|
+
return {};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function buildAgentPrompt(spec, input, context) {
|
|
571
|
+
const scope = {
|
|
572
|
+
task: input.task,
|
|
573
|
+
previous: input.previous,
|
|
574
|
+
pipeline: context.pipeline,
|
|
575
|
+
agent: { id: context.agent.id, name: context.agent.name },
|
|
576
|
+
};
|
|
577
|
+
if (typeof spec.promptBuilder === "function") {
|
|
578
|
+
return spec.promptBuilder(input, context);
|
|
579
|
+
}
|
|
580
|
+
if (typeof spec.promptTemplate === "string") {
|
|
581
|
+
return resolveTemplate(spec.promptTemplate, scope);
|
|
582
|
+
}
|
|
583
|
+
if (typeof spec.prompt === "string" && spec.prompt.trim()) {
|
|
584
|
+
return resolveTemplate(spec.prompt, scope);
|
|
585
|
+
}
|
|
586
|
+
const stageKey = String(spec.stage || spec.role || spec.name || "implement")
|
|
587
|
+
.trim()
|
|
588
|
+
.toLowerCase();
|
|
589
|
+
const factory = STAGE_PROMPTS[stageKey] || STAGE_PROMPTS.implement;
|
|
590
|
+
return factory(input);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function buildAgentTaskKey(workflowName, spec, pipelineContext) {
|
|
594
|
+
const parts = [
|
|
595
|
+
workflowName || pipelineContext?.name || "pipeline",
|
|
596
|
+
spec.name || spec.id || spec.stage || spec.sdk || "agent",
|
|
597
|
+
pipelineContext?.runId || randomUUID(),
|
|
598
|
+
];
|
|
599
|
+
return parts.map((entry) => String(entry || "").trim()).filter(Boolean).join(":");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function createBosunAgent(spec = {}, services = {}) {
|
|
603
|
+
const normalizedSpec = { timeoutMs: DEFAULT_TIMEOUT_MS, sdk: "auto", ...spec };
|
|
604
|
+
return {
|
|
605
|
+
id: String(normalizedSpec.id || normalizedSpec.name || normalizedSpec.stage || randomUUID()),
|
|
606
|
+
name: String(normalizedSpec.name || normalizedSpec.id || normalizedSpec.stage || "agent"),
|
|
607
|
+
async run(input, context = {}) {
|
|
608
|
+
const agentPool = services.agentPool || normalizedSpec.agentPool;
|
|
609
|
+
if (!agentPool) {
|
|
610
|
+
throw new Error(`Bosun pipeline agent "${normalizedSpec.name || normalizedSpec.id || "agent"}" requires agentPool service.`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const taskInput = {
|
|
614
|
+
task: normalizePipelineInput(input.task),
|
|
615
|
+
previous: input.previous ? safeClone(input.previous) : null,
|
|
616
|
+
};
|
|
617
|
+
const prompt = buildAgentPrompt(normalizedSpec, taskInput, context);
|
|
618
|
+
const timeoutMs = Number(normalizedSpec.timeoutMs || normalizedSpec.timeout || DEFAULT_TIMEOUT_MS);
|
|
619
|
+
const cwd = resolveTemplate(
|
|
620
|
+
normalizedSpec.cwd || taskInput.task.cwd || process.cwd(),
|
|
621
|
+
{ task: taskInput.task, previous: taskInput.previous },
|
|
622
|
+
);
|
|
623
|
+
const sdk = normalizedSpec.sdk || "auto";
|
|
624
|
+
const model = normalizedSpec.model || undefined;
|
|
625
|
+
const abortController = new AbortController();
|
|
626
|
+
const disconnectAbort = relayAbort(context.signal, abortController);
|
|
627
|
+
const taskKey = buildAgentTaskKey(context.pipeline?.name, normalizedSpec, context.pipeline);
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
let rawResult = null;
|
|
631
|
+
if (normalizedSpec.autoRecover !== false && typeof agentPool.execWithRetry === "function") {
|
|
632
|
+
rawResult = await agentPool.execWithRetry(prompt, {
|
|
633
|
+
taskKey,
|
|
634
|
+
cwd,
|
|
635
|
+
timeoutMs,
|
|
636
|
+
sdk,
|
|
637
|
+
model,
|
|
638
|
+
abortController,
|
|
639
|
+
maxRetries: Number(normalizedSpec.maxRetries ?? 1),
|
|
640
|
+
maxContinues: Number(normalizedSpec.maxContinues ?? 1),
|
|
641
|
+
});
|
|
642
|
+
} else if (typeof agentPool.launchOrResumeThread === "function") {
|
|
643
|
+
rawResult = await agentPool.launchOrResumeThread(prompt, cwd, timeoutMs, {
|
|
644
|
+
taskKey,
|
|
645
|
+
sdk,
|
|
646
|
+
model,
|
|
647
|
+
abortController,
|
|
648
|
+
});
|
|
649
|
+
} else if (typeof agentPool.launchEphemeralThread === "function") {
|
|
650
|
+
rawResult = await agentPool.launchEphemeralThread(prompt, cwd, timeoutMs, {
|
|
651
|
+
sdk,
|
|
652
|
+
model,
|
|
653
|
+
abortController,
|
|
654
|
+
});
|
|
655
|
+
} else {
|
|
656
|
+
throw new Error("agentPool does not expose a supported execution method.");
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
success: rawResult?.success !== false,
|
|
661
|
+
output: rawResult?.output ?? rawResult?.finalResponse ?? rawResult?.message ?? "",
|
|
662
|
+
summary:
|
|
663
|
+
rawResult?.summary || rawResult?.output || rawResult?.finalResponse || rawResult?.message || "",
|
|
664
|
+
usage: rawResult?.usage || rawResult?.tokensUsed || null,
|
|
665
|
+
metadata: {
|
|
666
|
+
sdk: rawResult?.sdk || sdk,
|
|
667
|
+
model,
|
|
668
|
+
threadId: rawResult?.threadId || null,
|
|
669
|
+
stage: normalizedSpec.stage || null,
|
|
670
|
+
},
|
|
671
|
+
branch: taskInput.task.branch || null,
|
|
672
|
+
filePaths: normalizeFilePaths(taskInput.task.filePaths || taskInput.task.files),
|
|
673
|
+
error: rawResult?.error || null,
|
|
674
|
+
raw: rawResult,
|
|
675
|
+
};
|
|
676
|
+
} finally {
|
|
677
|
+
disconnectAbort();
|
|
678
|
+
}
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export const BUILTIN_WORKFLOWS = Object.freeze({
|
|
684
|
+
"code-review-chain": {
|
|
685
|
+
type: "sequential",
|
|
686
|
+
description: "Implementation -> verification -> review with fresh-context handoff.",
|
|
687
|
+
stages: ["implement", "test", "review"],
|
|
688
|
+
},
|
|
689
|
+
"parallel-search": {
|
|
690
|
+
type: "fanout",
|
|
691
|
+
description: "Broadcast the same task to multiple SDKs and collect all findings.",
|
|
692
|
+
agents: [
|
|
693
|
+
{ name: "codex-search", stage: "search", sdk: "codex" },
|
|
694
|
+
{ name: "claude-search", stage: "search", sdk: "claude" },
|
|
695
|
+
{ name: "copilot-search", stage: "search", sdk: "copilot" },
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
"consensus-vote": {
|
|
699
|
+
type: "fanout",
|
|
700
|
+
description: "Ask multiple agents for approve/reject/abstain votes on the same task.",
|
|
701
|
+
agents: [
|
|
702
|
+
{ name: "codex-vote", stage: "vote", sdk: "codex" },
|
|
703
|
+
{ name: "claude-vote", stage: "vote", sdk: "claude" },
|
|
704
|
+
{ name: "copilot-vote", stage: "vote", sdk: "copilot" },
|
|
705
|
+
],
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
function normalizeConfiguredAgent(agent, index, services) {
|
|
710
|
+
if (typeof agent === "function") return agent;
|
|
711
|
+
if (agent && typeof agent === "object" && (typeof agent.run === "function" || typeof agent.execute === "function")) {
|
|
712
|
+
return agent;
|
|
713
|
+
}
|
|
714
|
+
if (typeof agent === "string") {
|
|
715
|
+
return createBosunAgent({ name: agent, stage: agent }, services);
|
|
716
|
+
}
|
|
717
|
+
if (agent && typeof agent === "object") {
|
|
718
|
+
return createBosunAgent(agent, services);
|
|
719
|
+
}
|
|
720
|
+
throw new TypeError(`Invalid workflow agent at index ${index}.`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function resolveWorkflowDefinition(name, configuredWorkflows = {}) {
|
|
724
|
+
const configValue = configuredWorkflows?.[name];
|
|
725
|
+
if (configValue && typeof configValue === "object") {
|
|
726
|
+
return { name, source: "config", definition: { ...configValue, id: name, name } };
|
|
727
|
+
}
|
|
728
|
+
const builtin = BUILTIN_WORKFLOWS[name];
|
|
729
|
+
if (builtin) {
|
|
730
|
+
return { name, source: "builtin", definition: { ...builtin, id: name, name } };
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function listWorkflowDefinitions(configuredWorkflows = {}) {
|
|
736
|
+
const names = new Set([
|
|
737
|
+
...Object.keys(BUILTIN_WORKFLOWS),
|
|
738
|
+
...Object.keys(configuredWorkflows || {}),
|
|
739
|
+
]);
|
|
740
|
+
return [...names]
|
|
741
|
+
.sort((left, right) => left.localeCompare(right))
|
|
742
|
+
.map((name) => resolveWorkflowDefinition(name, configuredWorkflows));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function createConfiguredPipeline(definition, options = {}) {
|
|
746
|
+
const normalized = definition?.definition ? definition.definition : definition;
|
|
747
|
+
if (!normalized || typeof normalized !== "object") {
|
|
748
|
+
throw new Error("Workflow definition must be an object.");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const type = String(normalized.type || "sequential").trim().toLowerCase();
|
|
752
|
+
const rawAgents =
|
|
753
|
+
type === "sequential"
|
|
754
|
+
? normalized.stages || normalized.agents
|
|
755
|
+
: normalized.agents || normalized.stages;
|
|
756
|
+
if (!Array.isArray(rawAgents) || rawAgents.length === 0) {
|
|
757
|
+
throw new Error(`Workflow "${normalized.name || normalized.id || "workflow"}" must define at least one stage/agent.`);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const services = options.services || {};
|
|
761
|
+
const agents = rawAgents.map((agent, index) => normalizeConfiguredAgent(agent, index, services));
|
|
762
|
+
const pipelineOptions = {
|
|
763
|
+
id: normalized.id,
|
|
764
|
+
name: normalized.name,
|
|
765
|
+
hub: options.hub,
|
|
766
|
+
createHub: options.createHub,
|
|
767
|
+
hubOptions: options.hubOptions,
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
switch (type) {
|
|
771
|
+
case "sequential":
|
|
772
|
+
return new SequentialPipeline(agents, pipelineOptions);
|
|
773
|
+
case "fanout":
|
|
774
|
+
return new FanoutPipeline(agents, pipelineOptions);
|
|
775
|
+
case "race":
|
|
776
|
+
return new RacePipeline(agents, pipelineOptions);
|
|
777
|
+
default:
|
|
778
|
+
throw new Error(`Unsupported workflow type: ${type}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
export async function runConfiguredWorkflow(name, input = {}, options = {}) {
|
|
783
|
+
const resolved = resolveWorkflowDefinition(name, options.workflows || {});
|
|
784
|
+
if (!resolved) {
|
|
785
|
+
throw new Error(`Unknown workflow: ${name}`);
|
|
786
|
+
}
|
|
787
|
+
const pipeline = createConfiguredPipeline(resolved, options);
|
|
788
|
+
return pipeline.run(input, options.runOptions || {});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function parsePipelineDefinition(definition) {
|
|
792
|
+
if (typeof definition === "string") {
|
|
793
|
+
return JSON.parse(definition);
|
|
794
|
+
}
|
|
795
|
+
return safeClone(definition);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function createPipeline(definition, options = {}) {
|
|
799
|
+
return createConfiguredPipeline(parsePipelineDefinition(definition), options);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export class Pipeline extends SequentialPipeline {
|
|
803
|
+
constructor(config = {}, options = {}) {
|
|
804
|
+
const definition = parsePipelineDefinition(config);
|
|
805
|
+
const stages = definition.stages || definition.agents || [];
|
|
806
|
+
super(stages, { ...options, id: definition.id, name: definition.name });
|
|
807
|
+
this.definition = definition;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export class PipelineStage {
|
|
812
|
+
constructor(config = {}) {
|
|
813
|
+
Object.assign(this, config);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export default {
|
|
818
|
+
Pipeline,
|
|
819
|
+
PipelineStage,
|
|
820
|
+
SequentialPipeline,
|
|
821
|
+
FanoutPipeline,
|
|
822
|
+
RacePipeline,
|
|
823
|
+
MsgHub,
|
|
824
|
+
BUILTIN_WORKFLOWS,
|
|
825
|
+
createBosunAgent,
|
|
826
|
+
createConfiguredPipeline,
|
|
827
|
+
createPipeline,
|
|
828
|
+
listWorkflowDefinitions,
|
|
829
|
+
parsePipelineDefinition,
|
|
830
|
+
resolveWorkflowDefinition,
|
|
831
|
+
runConfiguredWorkflow,
|
|
832
|
+
};
|