agent-worker 0.3.0 → 0.4.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.
@@ -0,0 +1,1784 @@
1
+ import { a as createMockBackend, b as createModelAsync, g as parseModel, n as createBackend } from "./backends-DenGdkrj.mjs";
2
+ import { c as CONTEXT_DEFAULTS, i as resolveContextDir, n as createFileContextProvider, t as FileContextProvider } from "./cli/index.mjs";
3
+ import { a as createMemoryContextProvider, t as createContextMCPServer } from "./mcp-server-DtIApaBD.mjs";
4
+ import { generateText, jsonSchema, stepCountIs, tool } from "ai";
5
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
+ import { parse } from "yaml";
8
+ import { tmpdir } from "node:os";
9
+ import { exec, execSync } from "node:child_process";
10
+ import { promisify } from "node:util";
11
+ import { createServer } from "node:http";
12
+ import { randomUUID } from "node:crypto";
13
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
+ import { MockLanguageModelV3, mockValues } from "ai/test";
15
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
16
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
17
+ import chalk from "chalk";
18
+ import wrapAnsi from "wrap-ansi";
19
+ import stringWidth from "string-width";
20
+
21
+ //#region src/workflow/parser.ts
22
+ /**
23
+ * Workflow file parser
24
+ */
25
+ /**
26
+ * Parse a workflow file
27
+ */
28
+ async function parseWorkflowFile(filePath, options) {
29
+ const absolutePath = resolve(filePath);
30
+ const workflow = options?.workflow ?? options?.instance ?? "global";
31
+ const tag = options?.tag ?? "main";
32
+ if (!existsSync(absolutePath)) throw new Error(`Workflow file not found: ${absolutePath}`);
33
+ const content = readFileSync(absolutePath, "utf-8");
34
+ const workflowDir = dirname(absolutePath);
35
+ let raw;
36
+ try {
37
+ raw = parse(content);
38
+ } catch (error) {
39
+ throw new Error(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`);
40
+ }
41
+ const validation = validateWorkflow(raw);
42
+ if (!validation.valid) {
43
+ const messages = validation.errors.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
44
+ throw new Error(`Invalid workflow file:\n${messages}`);
45
+ }
46
+ const name = raw.name || basename(absolutePath, ".yml").replace(".yaml", "");
47
+ const agents = {};
48
+ for (const [agentName, agentDef] of Object.entries(raw.agents)) agents[agentName] = await resolveAgent(agentDef, workflowDir);
49
+ return {
50
+ name,
51
+ filePath: absolutePath,
52
+ agents,
53
+ context: resolveContext(raw.context, workflowDir, name, workflow, tag),
54
+ setup: raw.setup || [],
55
+ kickoff: raw.kickoff
56
+ };
57
+ }
58
+ /**
59
+ * Resolve context configuration
60
+ *
61
+ * - undefined (not set): default file provider enabled
62
+ * - null: default file provider enabled (YAML `context:` syntax)
63
+ * - false: explicitly disabled
64
+ * - { provider: 'file', config?: { dir | bind } }: file provider (ephemeral or persistent)
65
+ * - { provider: 'memory' }: memory provider (for testing)
66
+ */
67
+ function resolveContext(config, workflowDir, workflowName, workflow, tag) {
68
+ const resolve = (template) => resolveContextDir(template, {
69
+ workflowName,
70
+ workflow,
71
+ tag,
72
+ instance: workflow,
73
+ baseDir: workflowDir
74
+ });
75
+ if (config === false) return;
76
+ if (config === void 0 || config === null) return {
77
+ provider: "file",
78
+ dir: resolve(CONTEXT_DEFAULTS.dir)
79
+ };
80
+ if (config.provider === "memory") return {
81
+ provider: "memory",
82
+ documentOwner: config.documentOwner
83
+ };
84
+ const bindPath = config.config?.bind;
85
+ if (bindPath) return {
86
+ provider: "file",
87
+ dir: resolve(bindPath),
88
+ persistent: true,
89
+ documentOwner: config.documentOwner
90
+ };
91
+ return {
92
+ provider: "file",
93
+ dir: resolve(config.config?.dir || CONTEXT_DEFAULTS.dir),
94
+ documentOwner: config.documentOwner
95
+ };
96
+ }
97
+ /**
98
+ * Resolve agent definition (load system prompt from file if needed)
99
+ */
100
+ async function resolveAgent(agent, workflowDir) {
101
+ let resolvedSystemPrompt = agent.system_prompt;
102
+ if (resolvedSystemPrompt?.endsWith(".txt") || resolvedSystemPrompt?.endsWith(".md")) {
103
+ const promptPath = resolvedSystemPrompt.startsWith("/") ? resolvedSystemPrompt : join(workflowDir, resolvedSystemPrompt);
104
+ if (existsSync(promptPath)) resolvedSystemPrompt = readFileSync(promptPath, "utf-8");
105
+ }
106
+ return {
107
+ ...agent,
108
+ resolvedSystemPrompt
109
+ };
110
+ }
111
+ /**
112
+ * Validate workflow structure
113
+ */
114
+ function validateWorkflow(workflow) {
115
+ const errors = [];
116
+ if (!workflow || typeof workflow !== "object") {
117
+ errors.push({
118
+ path: "",
119
+ message: "Workflow must be an object"
120
+ });
121
+ return {
122
+ valid: false,
123
+ errors
124
+ };
125
+ }
126
+ const w = workflow;
127
+ if (!w.agents || typeof w.agents !== "object") errors.push({
128
+ path: "agents",
129
+ message: "Required field \"agents\" must be an object"
130
+ });
131
+ else {
132
+ const agents = w.agents;
133
+ for (const [name, agent] of Object.entries(agents)) validateAgent(name, agent, errors);
134
+ }
135
+ if (w.context !== void 0 && w.context !== null && w.context !== false) validateContext(w.context, errors);
136
+ if (w.setup !== void 0) if (!Array.isArray(w.setup)) errors.push({
137
+ path: "setup",
138
+ message: "Setup must be an array"
139
+ });
140
+ else for (let i = 0; i < w.setup.length; i++) validateSetupTask(`setup[${i}]`, w.setup[i], errors);
141
+ if (w.kickoff !== void 0 && typeof w.kickoff !== "string") errors.push({
142
+ path: "kickoff",
143
+ message: "Kickoff must be a string"
144
+ });
145
+ return {
146
+ valid: errors.length === 0,
147
+ errors
148
+ };
149
+ }
150
+ function validateContext(context, errors) {
151
+ if (typeof context !== "object" || context === null) {
152
+ errors.push({
153
+ path: "context",
154
+ message: "Context must be an object or false"
155
+ });
156
+ return;
157
+ }
158
+ const c = context;
159
+ if (!c.provider || typeof c.provider !== "string") {
160
+ errors.push({
161
+ path: "context.provider",
162
+ message: "Context requires \"provider\" field (file or memory)"
163
+ });
164
+ return;
165
+ }
166
+ if (c.provider !== "file" && c.provider !== "memory") {
167
+ errors.push({
168
+ path: "context.provider",
169
+ message: "Context provider must be \"file\" or \"memory\""
170
+ });
171
+ return;
172
+ }
173
+ if (c.documentOwner !== void 0 && typeof c.documentOwner !== "string") errors.push({
174
+ path: "context.documentOwner",
175
+ message: "Context documentOwner must be a string"
176
+ });
177
+ if (c.provider === "file" && c.config !== void 0) {
178
+ if (typeof c.config !== "object" || c.config === null) {
179
+ errors.push({
180
+ path: "context.config",
181
+ message: "Context config must be an object"
182
+ });
183
+ return;
184
+ }
185
+ const cfg = c.config;
186
+ if (cfg.dir !== void 0 && cfg.bind !== void 0) {
187
+ errors.push({
188
+ path: "context.config",
189
+ message: "\"dir\" and \"bind\" are mutually exclusive — use one or the other"
190
+ });
191
+ return;
192
+ }
193
+ if (cfg.dir !== void 0 && typeof cfg.dir !== "string") errors.push({
194
+ path: "context.config.dir",
195
+ message: "Context config dir must be a string"
196
+ });
197
+ if (cfg.bind !== void 0 && typeof cfg.bind !== "string") errors.push({
198
+ path: "context.config.bind",
199
+ message: "Context config bind must be a string path"
200
+ });
201
+ }
202
+ }
203
+ function validateSetupTask(path, task, errors) {
204
+ if (!task || typeof task !== "object") {
205
+ errors.push({
206
+ path,
207
+ message: "Setup task must be an object"
208
+ });
209
+ return;
210
+ }
211
+ const t = task;
212
+ if (!t.shell || typeof t.shell !== "string") errors.push({
213
+ path: `${path}.shell`,
214
+ message: "Setup task requires \"shell\" field as string"
215
+ });
216
+ if (t.as !== void 0 && typeof t.as !== "string") errors.push({
217
+ path: `${path}.as`,
218
+ message: "Setup task \"as\" field must be a string"
219
+ });
220
+ }
221
+ /** Backends that don't require an explicit model field */
222
+ const CLI_BACKENDS = [
223
+ "claude",
224
+ "cursor",
225
+ "codex",
226
+ "mock"
227
+ ];
228
+ function validateAgent(name, agent, errors) {
229
+ const path = `agents.${name}`;
230
+ if (!agent || typeof agent !== "object") {
231
+ errors.push({
232
+ path,
233
+ message: "Agent must be an object"
234
+ });
235
+ return;
236
+ }
237
+ const a = agent;
238
+ const backend = typeof a.backend === "string" ? a.backend : "sdk";
239
+ if (a.model !== void 0 && typeof a.model !== "string") errors.push({
240
+ path: `${path}.model`,
241
+ message: "Field \"model\" must be a string"
242
+ });
243
+ else if (!a.model && !CLI_BACKENDS.includes(backend)) errors.push({
244
+ path: `${path}.model`,
245
+ message: "Required field \"model\" must be a string (required for sdk backend)"
246
+ });
247
+ if (a.system_prompt !== void 0 && typeof a.system_prompt !== "string") errors.push({
248
+ path: `${path}.system_prompt`,
249
+ message: "Optional field \"system_prompt\" must be a string"
250
+ });
251
+ if (a.tools !== void 0 && !Array.isArray(a.tools)) errors.push({
252
+ path: `${path}.tools`,
253
+ message: "Optional field \"tools\" must be an array"
254
+ });
255
+ }
256
+
257
+ //#endregion
258
+ //#region src/workflow/interpolate.ts
259
+ const VARIABLE_PATTERN = /\$\{\{\s*([^}]+)\s*\}\}/g;
260
+ /**
261
+ * Interpolate variables in a template string
262
+ * @param warn Optional callback for unresolved variables
263
+ */
264
+ function interpolate(template, context, warn) {
265
+ return template.replace(VARIABLE_PATTERN, (match, expression) => {
266
+ const value = resolveExpression(expression.trim(), context);
267
+ if (value === void 0 && warn) warn("Unresolved variable: ${{ " + expression.trim() + " }} — no setup task defines it");
268
+ return value ?? match;
269
+ });
270
+ }
271
+ /**
272
+ * Resolve a variable expression
273
+ */
274
+ function resolveExpression(expression, context) {
275
+ if (expression.startsWith("env.")) {
276
+ const varName = expression.slice(4);
277
+ return context.env?.[varName] ?? process.env[varName];
278
+ }
279
+ if (expression.startsWith("workflow.")) {
280
+ const field = expression.slice(9);
281
+ if (field === "name") return context.workflow?.name;
282
+ if (field === "tag") return context.workflow?.tag;
283
+ if (field === "instance") return context.workflow?.instance || context.workflow?.tag;
284
+ return;
285
+ }
286
+ const value = context[expression];
287
+ return typeof value === "string" ? value : void 0;
288
+ }
289
+ /**
290
+ * Create a context from workflow metadata
291
+ */
292
+ function createContext(workflowName, tag, taskOutputs = {}) {
293
+ return {
294
+ ...taskOutputs,
295
+ env: process.env,
296
+ workflow: {
297
+ name: workflowName,
298
+ tag,
299
+ instance: tag
300
+ }
301
+ };
302
+ }
303
+
304
+ //#endregion
305
+ //#region src/workflow/context/http-transport.ts
306
+ /**
307
+ * HTTP-based MCP Transport
308
+ *
309
+ * Hosts MCP server over HTTP using StreamableHTTPServerTransport.
310
+ * CLI agents (cursor, claude, codex) connect directly via URL — no subprocess bridge needed.
311
+ *
312
+ * Each agent gets a unique URL: http://localhost:<port>/mcp?agent=<name>
313
+ * The agent name is used as the MCP session ID, so tool handlers
314
+ * receive it via extra.sessionId → getAgentId().
315
+ */
316
+ /**
317
+ * Parse request body as JSON
318
+ */
319
+ function parseRequestBody(req) {
320
+ return new Promise((resolve, reject) => {
321
+ const chunks = [];
322
+ req.on("data", (chunk) => chunks.push(chunk));
323
+ req.on("end", () => {
324
+ try {
325
+ const body = Buffer.concat(chunks).toString();
326
+ resolve(body ? JSON.parse(body) : void 0);
327
+ } catch (err) {
328
+ reject(err);
329
+ }
330
+ });
331
+ req.on("error", reject);
332
+ });
333
+ }
334
+ /**
335
+ * Check if a JSON-RPC message is an initialize request
336
+ */
337
+ function isInitializeRequest(body) {
338
+ if (Array.isArray(body)) return body.some((msg) => msg?.method === "initialize");
339
+ return body?.method === "initialize";
340
+ }
341
+ /**
342
+ * Start an HTTP MCP server
343
+ *
344
+ * Agents connect via: http://localhost:<port>/mcp?agent=<name>
345
+ * The server creates a per-session StreamableHTTPServerTransport and McpServer.
346
+ */
347
+ async function runWithHttp(options) {
348
+ const { createServerInstance, port = 0, onConnect, onDisconnect } = options;
349
+ const sessions = /* @__PURE__ */ new Map();
350
+ const httpServer = createServer(async (req, res) => {
351
+ const reqUrl = new URL(req.url || "/", `http://localhost`);
352
+ if (!reqUrl.pathname.startsWith("/mcp")) {
353
+ res.writeHead(404, { "Content-Type": "application/json" });
354
+ res.end(JSON.stringify({ error: "Not found" }));
355
+ return;
356
+ }
357
+ const agentName = reqUrl.searchParams.get("agent") || "anonymous";
358
+ const sessionId = req.headers["mcp-session-id"];
359
+ if (sessionId && sessions.has(sessionId)) {
360
+ const session = sessions.get(sessionId);
361
+ if (req.method === "DELETE") {
362
+ await session.transport.close();
363
+ sessions.delete(sessionId);
364
+ if (onDisconnect) onDisconnect(session.agentId, sessionId);
365
+ res.writeHead(200);
366
+ res.end();
367
+ return;
368
+ }
369
+ const body = req.method === "POST" ? await parseRequestBody(req) : void 0;
370
+ await session.transport.handleRequest(req, res, body);
371
+ return;
372
+ }
373
+ if (req.method === "POST") {
374
+ const body = await parseRequestBody(req);
375
+ if (!isInitializeRequest(body)) {
376
+ res.writeHead(400, { "Content-Type": "application/json" });
377
+ res.end(JSON.stringify({ error: "Bad request: session required" }));
378
+ return;
379
+ }
380
+ const transport = new StreamableHTTPServerTransport({
381
+ sessionIdGenerator: () => `${agentName}-${randomUUID().slice(0, 8)}`,
382
+ onsessioninitialized: (sid) => {
383
+ sessions.set(sid, {
384
+ transport,
385
+ agentId: agentName
386
+ });
387
+ if (onConnect) onConnect(agentName, sid);
388
+ }
389
+ });
390
+ Object.defineProperty(transport, "_agentId", {
391
+ value: agentName,
392
+ writable: true
393
+ });
394
+ await createServerInstance().connect(transport);
395
+ await transport.handleRequest(req, res, body);
396
+ return;
397
+ }
398
+ if (req.method === "GET") {
399
+ res.writeHead(400, { "Content-Type": "application/json" });
400
+ res.end(JSON.stringify({ error: "Session ID required for GET requests" }));
401
+ return;
402
+ }
403
+ res.writeHead(405, { "Content-Type": "application/json" });
404
+ res.end(JSON.stringify({ error: "Method not allowed" }));
405
+ });
406
+ const actualPort = await new Promise((resolve, reject) => {
407
+ httpServer.on("error", reject);
408
+ httpServer.listen(port, "127.0.0.1", () => {
409
+ httpServer.removeListener("error", reject);
410
+ const addr = httpServer.address();
411
+ if (typeof addr === "object" && addr) resolve(addr.port);
412
+ else reject(/* @__PURE__ */ new Error("Failed to get server address"));
413
+ });
414
+ });
415
+ return {
416
+ httpServer,
417
+ url: `http://127.0.0.1:${actualPort}/mcp`,
418
+ port: actualPort,
419
+ sessions,
420
+ async close() {
421
+ for (const [sid, session] of sessions) {
422
+ await session.transport.close();
423
+ if (onDisconnect) onDisconnect(session.agentId, sid);
424
+ }
425
+ sessions.clear();
426
+ await new Promise((resolve) => {
427
+ httpServer.close(() => resolve());
428
+ });
429
+ }
430
+ };
431
+ }
432
+
433
+ //#endregion
434
+ //#region src/workflow/controller/types.ts
435
+ /** Default controller configuration values */
436
+ const CONTROLLER_DEFAULTS = {
437
+ pollInterval: 5e3,
438
+ retry: {
439
+ maxAttempts: 3,
440
+ backoffMs: 1e3,
441
+ backoffMultiplier: 2
442
+ },
443
+ recentChannelLimit: 50,
444
+ idleDebounceMs: 2e3
445
+ };
446
+
447
+ //#endregion
448
+ //#region src/workflow/controller/prompt.ts
449
+ /**
450
+ * Format inbox messages for display
451
+ */
452
+ function formatInbox(inbox) {
453
+ if (inbox.length === 0) return "(no messages)";
454
+ return inbox.map((m) => {
455
+ const priority = m.priority === "high" ? " [HIGH]" : "";
456
+ const time = m.entry.timestamp.slice(11, 19);
457
+ const dm = m.entry.to ? " [DM]" : "";
458
+ return `- [${time}] From @${m.entry.from}${priority}${dm}: ${m.entry.content}`;
459
+ }).join("\n");
460
+ }
461
+ /**
462
+ * Format channel messages for display
463
+ */
464
+ function formatChannel(entries) {
465
+ if (entries.length === 0) return "(no messages)";
466
+ return entries.map((e) => {
467
+ const dm = e.to ? ` [DM→@${e.to}]` : "";
468
+ return `[${e.timestamp.slice(11, 19)}] @${e.from}${dm}: ${e.content}`;
469
+ }).join("\n");
470
+ }
471
+ /**
472
+ * Build the complete agent prompt from run context
473
+ */
474
+ function buildAgentPrompt(ctx) {
475
+ const sections = [];
476
+ sections.push("## Project");
477
+ sections.push(`Working on: ${ctx.projectDir}`);
478
+ sections.push("");
479
+ sections.push(`## Inbox (${ctx.inbox.length} message${ctx.inbox.length === 1 ? "" : "s"} for you)`);
480
+ sections.push(formatInbox(ctx.inbox));
481
+ sections.push("");
482
+ sections.push(`## Recent Activity (last ${ctx.recentChannel.length} messages)`);
483
+ sections.push(formatChannel(ctx.recentChannel));
484
+ if (ctx.documentContent) {
485
+ sections.push("");
486
+ sections.push("## Shared Document");
487
+ sections.push(ctx.documentContent);
488
+ }
489
+ if (ctx.retryAttempt > 1) {
490
+ sections.push("");
491
+ sections.push(`## Note`);
492
+ sections.push(`This is retry attempt ${ctx.retryAttempt}. Previous attempt failed.`);
493
+ }
494
+ sections.push("");
495
+ sections.push("## Instructions");
496
+ sections.push("You are an agent in a multi-agent workflow. Communicate ONLY through the MCP tools below.");
497
+ sections.push("Your text output is NOT seen by other agents — you MUST use channel_send to communicate.");
498
+ sections.push("");
499
+ sections.push("### Channel Tools");
500
+ sections.push("- **channel_send**: Send a message to the shared channel. Use @agentname to mention/notify.");
501
+ sections.push(" Use the \"to\" parameter for private DMs: channel_send({ message: \"...\", to: \"bob\" })");
502
+ sections.push("- **channel_read**: Read recent channel messages (DMs and logs are auto-filtered).");
503
+ sections.push("");
504
+ sections.push("### Team Tools");
505
+ sections.push("- **team_members**: List all agents you can @mention.");
506
+ sections.push("- **team_doc_read/write/append/list/create**: Shared team documents.");
507
+ sections.push("");
508
+ sections.push("### Personal Tools");
509
+ sections.push("- **my_inbox**: Check your unread messages.");
510
+ sections.push("- **my_inbox_ack**: Acknowledge messages after processing (pass the latest timestamp).");
511
+ sections.push("");
512
+ sections.push("### Proposal & Voting Tools");
513
+ sections.push("- **team_proposal_create**: Create a proposal for team voting (types: election, decision, approval, assignment).");
514
+ sections.push("- **team_vote**: Cast your vote on an active proposal. You can change your vote by voting again.");
515
+ sections.push("- **team_proposal_status**: Check status of a proposal, or list all active proposals.");
516
+ sections.push("- **team_proposal_cancel**: Cancel a proposal you created.");
517
+ sections.push("");
518
+ sections.push("### Resource Tools");
519
+ sections.push("- **resource_create**: Store large content, get a reference (resource:id) for use anywhere.");
520
+ sections.push("- **resource_read**: Read resource content by ID.");
521
+ if (ctx.feedback) {
522
+ sections.push("");
523
+ sections.push("### Feedback Tool");
524
+ sections.push("- **feedback_submit**: Report workflow improvement needs — a missing tool, an awkward step, or a capability gap.");
525
+ sections.push(" Only use when you genuinely hit a pain point during your work.");
526
+ }
527
+ sections.push("");
528
+ sections.push("### Workflow");
529
+ sections.push("1. Read your inbox messages above");
530
+ sections.push("2. Do your assigned work using channel_send with @mentions");
531
+ sections.push("3. Acknowledge your inbox with my_inbox_ack");
532
+ sections.push("4. Exit when your task is complete");
533
+ sections.push("");
534
+ sections.push("### IMPORTANT: When to stop");
535
+ sections.push("- Once your assigned task is complete, acknowledge your inbox and exit. Do NOT keep chatting.");
536
+ sections.push("- Do NOT send pleasantries (\"you're welcome\", \"glad to help\", \"thanks again\") — they trigger unnecessary cycles.");
537
+ sections.push("- Do NOT @mention another agent in your final message unless you need them to do more work.");
538
+ sections.push("- If you receive a thank-you or acknowledgment, just call my_inbox_ack and exit. Do not reply.");
539
+ return sections.join("\n");
540
+ }
541
+
542
+ //#endregion
543
+ //#region src/workflow/controller/mcp-config.ts
544
+ /**
545
+ * Generate MCP config for workflow context server.
546
+ *
547
+ * Uses HTTP transport — CLI agents connect directly via URL:
548
+ * { type: "http", url: "http://127.0.0.1:<port>/mcp?agent=<name>" }
549
+ */
550
+ function generateWorkflowMCPConfig(mcpUrl, agentName) {
551
+ const url = `${mcpUrl}?agent=${encodeURIComponent(agentName)}`;
552
+ return { mcpServers: { "workflow-context": {
553
+ type: "http",
554
+ url
555
+ } } };
556
+ }
557
+
558
+ //#endregion
559
+ //#region src/workflow/controller/mock-runner.ts
560
+ /**
561
+ * Mock Agent Runner
562
+ *
563
+ * Orchestrates mock agent execution for workflow integration testing.
564
+ * Uses AI SDK generateText with MockLanguageModelV3 and real MCP tool calls.
565
+ *
566
+ * This lives in the controller layer (not backends) because it does orchestration:
567
+ * connecting to MCP, building prompts, managing tool loops.
568
+ * The mock backend itself is just a simple send() adapter.
569
+ */
570
+ /**
571
+ * Connect to workflow MCP server via HTTP and create AI SDK tool wrappers
572
+ */
573
+ async function createMCPToolBridge$1(mcpUrl, agentName) {
574
+ const transport = new StreamableHTTPClientTransport(new URL(`${mcpUrl}?agent=${encodeURIComponent(agentName)}`));
575
+ const client = new Client({
576
+ name: agentName,
577
+ version: "1.0.0"
578
+ });
579
+ await client.connect(transport);
580
+ const { tools: mcpTools } = await client.listTools();
581
+ const aiTools = {};
582
+ for (const mcpTool of mcpTools) {
583
+ const toolName = mcpTool.name;
584
+ aiTools[toolName] = tool({
585
+ description: mcpTool.description || toolName,
586
+ inputSchema: jsonSchema(mcpTool.inputSchema),
587
+ execute: async (args) => {
588
+ return (await client.callTool({
589
+ name: toolName,
590
+ arguments: args
591
+ })).content;
592
+ }
593
+ });
594
+ }
595
+ return {
596
+ tools: aiTools,
597
+ close: () => client.close()
598
+ };
599
+ }
600
+ /**
601
+ * Run a mock agent with AI SDK and real MCP tools.
602
+ *
603
+ * Used by the controller when backend.type === 'mock'.
604
+ * Unlike real backends that just send(), the mock runner needs to:
605
+ * 1. Connect to MCP server for real tool execution
606
+ * 2. Generate scripted tool calls via MockLanguageModelV3
607
+ * 3. Execute the full tool loop to test channel/document flow
608
+ */
609
+ async function runMockAgent(ctx, debugLog) {
610
+ const startTime = Date.now();
611
+ const log = debugLog || (() => {});
612
+ try {
613
+ if (!ctx.mcpUrl) return {
614
+ success: false,
615
+ error: "Mock runner requires mcpUrl (HTTP MCP server)",
616
+ duration: 0
617
+ };
618
+ const mcp = await createMCPToolBridge$1(ctx.mcpUrl, ctx.name);
619
+ log(`MCP connected, ${Object.keys(mcp.tools).length} tools`);
620
+ const inboxSummary = ctx.inbox.map((m) => `${m.entry.from}: ${m.entry.content.slice(0, 80).replace(/@/g, "")}`).join("; ");
621
+ const mockModel = new MockLanguageModelV3({ doGenerate: mockValues({
622
+ content: [{
623
+ type: "tool-call",
624
+ toolCallId: `call-${ctx.name}-${Date.now()}`,
625
+ toolName: "channel_send",
626
+ input: JSON.stringify({ message: `[${ctx.name}] Processed: ${inboxSummary.slice(0, 200)}` })
627
+ }],
628
+ finishReason: {
629
+ unified: "tool-calls",
630
+ raw: "tool_use"
631
+ },
632
+ usage: {
633
+ inputTokens: {
634
+ total: 100,
635
+ noCache: 100,
636
+ cacheRead: 0,
637
+ cacheWrite: 0
638
+ },
639
+ outputTokens: {
640
+ total: 50,
641
+ text: 50,
642
+ reasoning: 0
643
+ }
644
+ }
645
+ }, {
646
+ content: [{
647
+ type: "text",
648
+ text: `${ctx.name} done.`
649
+ }],
650
+ finishReason: {
651
+ unified: "stop",
652
+ raw: "end_turn"
653
+ },
654
+ usage: {
655
+ inputTokens: {
656
+ total: 50,
657
+ noCache: 50,
658
+ cacheRead: 0,
659
+ cacheWrite: 0
660
+ },
661
+ outputTokens: {
662
+ total: 10,
663
+ text: 10,
664
+ reasoning: 0
665
+ }
666
+ }
667
+ }) });
668
+ const prompt = buildAgentPrompt(ctx);
669
+ log(`Prompt (${prompt.length} chars)`);
670
+ const result = await generateText({
671
+ model: mockModel,
672
+ tools: mcp.tools,
673
+ prompt,
674
+ system: ctx.agent.resolvedSystemPrompt,
675
+ stopWhen: stepCountIs(3)
676
+ });
677
+ const totalToolCalls = result.steps.reduce((n, s) => n + s.toolCalls.length, 0);
678
+ await mcp.close();
679
+ return {
680
+ success: true,
681
+ duration: Date.now() - startTime,
682
+ steps: result.steps.length,
683
+ toolCalls: totalToolCalls
684
+ };
685
+ } catch (error) {
686
+ return {
687
+ success: false,
688
+ error: error instanceof Error ? error.message : String(error),
689
+ duration: Date.now() - startTime
690
+ };
691
+ }
692
+ }
693
+
694
+ //#endregion
695
+ //#region src/workflow/controller/sdk-runner.ts
696
+ /**
697
+ * SDK Agent Runner
698
+ *
699
+ * Runs SDK agents with full tool access in workflows:
700
+ * - MCP context tools (channel_send, document_write, etc.)
701
+ * - Bash tool for shell commands
702
+ *
703
+ * Same pattern as mock-runner.ts but with real models via createModelAsync.
704
+ * This is the standard execution path for SDK backends in workflows —
705
+ * all agents get MCP + bash regardless of backend type.
706
+ */
707
+ /** Extract useful details from AI SDK errors (statusCode, url, responseBody) */
708
+ function formatError(error) {
709
+ if (!(error instanceof Error)) return String(error);
710
+ const e = error;
711
+ const parts = [error.message];
712
+ if (e.statusCode) parts[0] = `HTTP ${e.statusCode}: ${error.message}`;
713
+ if (e.url) parts.push(`url=${e.url}`);
714
+ if (e.responseBody && typeof e.responseBody === "string") {
715
+ const body = e.responseBody.length > 200 ? e.responseBody.slice(0, 200) + "…" : e.responseBody;
716
+ parts.push(`body=${body}`);
717
+ }
718
+ return parts.join(" ");
719
+ }
720
+ /** Truncate string, flatten newlines */
721
+ function truncate(s, max) {
722
+ const flat = s.replace(/\s+/g, " ").trim();
723
+ return flat.length > max ? flat.slice(0, max) + "…" : flat;
724
+ }
725
+ /** Format a tool call for concise single-line debug output (function call syntax) */
726
+ function formatToolCall(tc) {
727
+ const input = tc.input ?? tc.args ?? {};
728
+ const pairs = Object.entries(input).map(([k, v]) => {
729
+ return `${k}=${truncate(typeof v === "string" ? v : JSON.stringify(v), 60)}`;
730
+ });
731
+ return `${tc.toolName}(${pairs.join(", ")})`;
732
+ }
733
+ /**
734
+ * Connect to workflow MCP server and create AI SDK tool wrappers.
735
+ * Same bridge as mock-runner — extracted here for SDK agents.
736
+ */
737
+ async function createMCPToolBridge(mcpUrl, agentName) {
738
+ const transport = new StreamableHTTPClientTransport(new URL(`${mcpUrl}?agent=${encodeURIComponent(agentName)}`));
739
+ const client = new Client({
740
+ name: agentName,
741
+ version: "1.0.0"
742
+ });
743
+ await client.connect(transport);
744
+ const { tools: mcpTools } = await client.listTools();
745
+ const aiTools = {};
746
+ for (const mcpTool of mcpTools) {
747
+ const toolName = mcpTool.name;
748
+ aiTools[toolName] = tool({
749
+ description: mcpTool.description || toolName,
750
+ inputSchema: jsonSchema(mcpTool.inputSchema),
751
+ execute: async (args) => {
752
+ return (await client.callTool({
753
+ name: toolName,
754
+ arguments: args
755
+ })).content;
756
+ }
757
+ });
758
+ }
759
+ return {
760
+ tools: aiTools,
761
+ close: () => client.close()
762
+ };
763
+ }
764
+ function createBashTool() {
765
+ return tool({
766
+ description: "Execute a shell command and return stdout/stderr.",
767
+ inputSchema: jsonSchema({
768
+ type: "object",
769
+ properties: { command: {
770
+ type: "string",
771
+ description: "The shell command to execute"
772
+ } },
773
+ required: ["command"]
774
+ }),
775
+ execute: async ({ command }) => {
776
+ try {
777
+ return execSync(command, {
778
+ encoding: "utf-8",
779
+ timeout: 12e4
780
+ }).trim() || "(no output)";
781
+ } catch (error) {
782
+ return `Error (exit ${error.status}): ${error.stderr || error.message}`;
783
+ }
784
+ }
785
+ });
786
+ }
787
+ /**
788
+ * Run an SDK agent with real model + MCP tools + bash.
789
+ *
790
+ * Used by the controller when backend.type === 'sdk'.
791
+ * Unlike the simple SdkBackend.send() (text-only), this runner:
792
+ * 1. Connects to MCP server for context tools (channel, document)
793
+ * 2. Adds bash tool for shell access
794
+ * 3. Runs generateText with full tool loop
795
+ */
796
+ async function runSdkAgent(ctx, debugLog) {
797
+ const startTime = Date.now();
798
+ const log = debugLog || (() => {});
799
+ try {
800
+ if (!ctx.mcpUrl) return {
801
+ success: false,
802
+ error: "SDK runner requires mcpUrl",
803
+ duration: 0
804
+ };
805
+ const mcp = await createMCPToolBridge(ctx.mcpUrl, ctx.name);
806
+ log(`MCP connected, ${Object.keys(mcp.tools).length} context tools`);
807
+ const model = await createModelAsync(ctx.agent.model);
808
+ const tools = {
809
+ ...mcp.tools,
810
+ bash: createBashTool()
811
+ };
812
+ const prompt = buildAgentPrompt(ctx);
813
+ log(`Prompt (${prompt.length} chars) → sdk with ${Object.keys(tools).length} tools`);
814
+ let _stepNum = 0;
815
+ const result = await generateText({
816
+ model,
817
+ tools,
818
+ system: ctx.agent.resolvedSystemPrompt,
819
+ prompt,
820
+ maxOutputTokens: ctx.agent.max_tokens ?? 8192,
821
+ stopWhen: stepCountIs(ctx.agent.max_steps ?? 30),
822
+ onStepFinish: (step) => {
823
+ _stepNum++;
824
+ if (step.toolCalls?.length) for (const tc of step.toolCalls) log(`CALL ${formatToolCall(tc)}`);
825
+ }
826
+ });
827
+ const totalToolCalls = result.steps.reduce((n, s) => n + s.toolCalls.length, 0);
828
+ await mcp.close();
829
+ return {
830
+ success: true,
831
+ duration: Date.now() - startTime,
832
+ content: result.text,
833
+ steps: result.steps.length,
834
+ toolCalls: totalToolCalls
835
+ };
836
+ } catch (error) {
837
+ return {
838
+ success: false,
839
+ error: formatError(error),
840
+ duration: Date.now() - startTime
841
+ };
842
+ }
843
+ }
844
+
845
+ //#endregion
846
+ //#region src/workflow/controller/controller.ts
847
+ /** Check if controller should continue running */
848
+ function shouldContinue(state) {
849
+ return state !== "stopped";
850
+ }
851
+ /**
852
+ * Create an agent controller
853
+ *
854
+ * The controller:
855
+ * 1. Polls for inbox messages on an interval
856
+ * 2. Runs the agent when messages are found
857
+ * 3. Acknowledges inbox only on successful run
858
+ * 4. Retries with exponential backoff on failure
859
+ * 5. Can be woken early via wake()
860
+ */
861
+ function createAgentController(config) {
862
+ const { name, agent, contextProvider, mcpUrl, workspaceDir, projectDir, backend, onRunComplete, log = () => {}, feedback } = config;
863
+ const errorLog = config.errorLog ?? log;
864
+ const pollInterval = config.pollInterval ?? CONTROLLER_DEFAULTS.pollInterval;
865
+ const retryConfig = {
866
+ maxAttempts: config.retry?.maxAttempts ?? CONTROLLER_DEFAULTS.retry.maxAttempts,
867
+ backoffMs: config.retry?.backoffMs ?? CONTROLLER_DEFAULTS.retry.backoffMs,
868
+ backoffMultiplier: config.retry?.backoffMultiplier ?? CONTROLLER_DEFAULTS.retry.backoffMultiplier
869
+ };
870
+ let state = "stopped";
871
+ let wakeResolver = null;
872
+ let pollTimeout = null;
873
+ /**
874
+ * Wait for either poll interval or wake() call
875
+ */
876
+ async function waitForWakeOrPoll() {
877
+ return new Promise((resolve) => {
878
+ wakeResolver = resolve;
879
+ pollTimeout = setTimeout(() => {
880
+ wakeResolver = null;
881
+ resolve();
882
+ }, pollInterval);
883
+ });
884
+ }
885
+ /**
886
+ * Main controller loop
887
+ */
888
+ async function runLoop() {
889
+ while (shouldContinue(state)) {
890
+ await waitForWakeOrPoll();
891
+ if (!shouldContinue(state)) break;
892
+ const inbox = await contextProvider.getInbox(name);
893
+ if (inbox.length === 0) {
894
+ state = "idle";
895
+ continue;
896
+ }
897
+ const senders = inbox.map((m) => m.entry.from);
898
+ log(`Inbox: ${inbox.length} message(s) from [${senders.join(", ")}]`);
899
+ for (const msg of inbox) {
900
+ const preview = msg.entry.content.length > 120 ? msg.entry.content.slice(0, 120) + "..." : msg.entry.content;
901
+ log(` from @${msg.entry.from}: ${preview}`);
902
+ }
903
+ const latestId = inbox[inbox.length - 1].entry.id;
904
+ await contextProvider.markInboxSeen(name, latestId);
905
+ let attempt = 0;
906
+ let lastResult = null;
907
+ while (attempt < retryConfig.maxAttempts && shouldContinue(state)) {
908
+ attempt++;
909
+ state = "running";
910
+ log(`Running (attempt ${attempt}/${retryConfig.maxAttempts})`);
911
+ lastResult = await runAgent(backend, {
912
+ name,
913
+ agent,
914
+ inbox,
915
+ recentChannel: await contextProvider.readChannel({
916
+ limit: CONTROLLER_DEFAULTS.recentChannelLimit,
917
+ agent: name
918
+ }),
919
+ documentContent: await contextProvider.readDocument(),
920
+ mcpUrl,
921
+ workspaceDir,
922
+ projectDir,
923
+ retryAttempt: attempt,
924
+ feedback
925
+ }, log);
926
+ if (lastResult.success) {
927
+ log(`DONE ${lastResult.steps ? `${lastResult.steps} steps, ${lastResult.toolCalls} tool calls, ${lastResult.duration}ms` : `${lastResult.duration}ms`}`);
928
+ if (lastResult.content) await contextProvider.appendChannel(name, lastResult.content);
929
+ await contextProvider.ackInbox(name, latestId);
930
+ break;
931
+ }
932
+ errorLog(`ERROR ${lastResult.error}`);
933
+ if (attempt < retryConfig.maxAttempts && shouldContinue(state)) {
934
+ const delay = retryConfig.backoffMs * Math.pow(retryConfig.backoffMultiplier, attempt - 1);
935
+ log(`Retrying in ${delay}ms...`);
936
+ await sleep$2(delay);
937
+ }
938
+ }
939
+ if (lastResult && !lastResult.success) {
940
+ errorLog(`ERROR max retries exhausted, acknowledging to prevent loop`);
941
+ await contextProvider.ackInbox(name, latestId);
942
+ }
943
+ if (lastResult && onRunComplete) onRunComplete(lastResult);
944
+ state = "idle";
945
+ }
946
+ }
947
+ return {
948
+ get name() {
949
+ return name;
950
+ },
951
+ get state() {
952
+ return state;
953
+ },
954
+ async start() {
955
+ if (state !== "stopped") throw new Error(`Controller ${name} is already running`);
956
+ state = "idle";
957
+ log(`Starting`);
958
+ runLoop().catch((error) => {
959
+ errorLog(`ERROR ${error instanceof Error ? error.message : String(error)}`);
960
+ state = "stopped";
961
+ });
962
+ },
963
+ async stop() {
964
+ log(`Stopping`);
965
+ state = "stopped";
966
+ if (pollTimeout) {
967
+ clearTimeout(pollTimeout);
968
+ pollTimeout = null;
969
+ }
970
+ if (wakeResolver) {
971
+ wakeResolver();
972
+ wakeResolver = null;
973
+ }
974
+ },
975
+ wake() {
976
+ if (state === "idle" && wakeResolver) {
977
+ log(`Waking`);
978
+ if (pollTimeout) {
979
+ clearTimeout(pollTimeout);
980
+ pollTimeout = null;
981
+ }
982
+ wakeResolver();
983
+ wakeResolver = null;
984
+ }
985
+ }
986
+ };
987
+ }
988
+ /**
989
+ * Run an agent: build prompt, configure workspace, call backend.send()
990
+ *
991
+ * This is the single orchestration function that the controller calls.
992
+ * All the "how to run an agent" logic lives here — backends just send().
993
+ *
994
+ * SDK and mock backends get special runners with MCP tool bridge + bash,
995
+ * because they can't manage tools on their own (unlike CLI backends).
996
+ */
997
+ async function runAgent(backend, ctx, log) {
998
+ if (backend.type === "mock") return runMockAgent(ctx, (msg) => log(msg));
999
+ if (backend.type === "sdk") return runSdkAgent(ctx, (msg) => log(msg));
1000
+ const startTime = Date.now();
1001
+ try {
1002
+ if (backend.setWorkspace) {
1003
+ const mcpConfig = generateWorkflowMCPConfig(ctx.mcpUrl, ctx.name);
1004
+ backend.setWorkspace(ctx.workspaceDir, mcpConfig);
1005
+ }
1006
+ const prompt = buildAgentPrompt(ctx);
1007
+ log(`Prompt (${prompt.length} chars) → ${backend.type} backend`);
1008
+ await backend.send(prompt, { system: ctx.agent.resolvedSystemPrompt });
1009
+ return {
1010
+ success: true,
1011
+ duration: Date.now() - startTime
1012
+ };
1013
+ } catch (error) {
1014
+ return {
1015
+ success: false,
1016
+ error: error instanceof Error ? error.message : String(error),
1017
+ duration: Date.now() - startTime
1018
+ };
1019
+ }
1020
+ }
1021
+ /**
1022
+ * Sleep helper
1023
+ */
1024
+ function sleep$2(ms) {
1025
+ return new Promise((resolve) => setTimeout(resolve, ms));
1026
+ }
1027
+ /**
1028
+ * Check if workflow is complete (all agents idle, no pending work)
1029
+ */
1030
+ async function checkWorkflowIdle(controllers, provider, debounceMs = CONTROLLER_DEFAULTS.idleDebounceMs) {
1031
+ if (![...controllers.values()].every((c) => c.state === "idle")) return false;
1032
+ for (const [name] of controllers) if ((await provider.getInbox(name)).length > 0) return false;
1033
+ await sleep$2(debounceMs);
1034
+ return [...controllers.values()].every((c) => c.state === "idle");
1035
+ }
1036
+
1037
+ //#endregion
1038
+ //#region src/workflow/controller/backend.ts
1039
+ /**
1040
+ * Get backend by explicit backend type
1041
+ *
1042
+ * All backends are created via the canonical createBackend() factory
1043
+ * from backends/index.ts. Mock backend is handled specially (no model needed).
1044
+ */
1045
+ function getBackendByType(backendType, options) {
1046
+ if (backendType === "mock") return createMockBackend(options?.debugLog);
1047
+ return createBackend({
1048
+ type: backendType,
1049
+ model: options?.model
1050
+ });
1051
+ }
1052
+ /**
1053
+ * Get appropriate backend for a model identifier
1054
+ *
1055
+ * Infers backend type from model name and delegates to getBackendByType.
1056
+ * Prefer using getBackendByType with explicit backend field in workflow configs.
1057
+ */
1058
+ function getBackendForModel(model, options) {
1059
+ const { provider } = parseModel(model);
1060
+ switch (provider) {
1061
+ case "anthropic": return getBackendByType("sdk", {
1062
+ ...options,
1063
+ model
1064
+ });
1065
+ case "claude": return getBackendByType("claude", {
1066
+ ...options,
1067
+ model
1068
+ });
1069
+ case "codex": return getBackendByType("codex", {
1070
+ ...options,
1071
+ model
1072
+ });
1073
+ default: throw new Error(`Unknown provider: ${provider}. Specify backend explicitly.`);
1074
+ }
1075
+ }
1076
+
1077
+ //#endregion
1078
+ //#region src/workflow/layout.ts
1079
+ /**
1080
+ * Adaptive Terminal Layout System
1081
+ *
1082
+ * Best practices implementation:
1083
+ * - Terminal-aware: Auto-detect width and adapt layout
1084
+ * - Smart wrapping: Preserve readability on long messages
1085
+ * - Background-agnostic: Colors work on any terminal theme
1086
+ * - Human-first: Optimize for visual clarity and scanning
1087
+ *
1088
+ * References:
1089
+ * - https://clig.dev/
1090
+ * - https://relay.sh/blog/command-line-ux-in-2020/
1091
+ */
1092
+ /**
1093
+ * Calculate optimal layout based on terminal size and agent names
1094
+ *
1095
+ * Layout structure: TIME NAME SEP CONTENT
1096
+ * Example: "47:08 student │ Message here"
1097
+ * └──┬─┘ └───┬──┘ │ └─────┬──────┘
1098
+ * 5 8 3 remaining
1099
+ */
1100
+ function calculateLayout(options) {
1101
+ const { agentNames, compact = false } = options;
1102
+ const terminalWidth = options.terminalWidth ?? process.stdout.columns ?? 80;
1103
+ const compactTime = compact || isShortSession();
1104
+ const timeWidth = compactTime ? 5 : 8;
1105
+ const longestName = Math.max(...agentNames.map((n) => stringWidth(n)), 6);
1106
+ const nameWidth = Math.min(longestName + 1, 20);
1107
+ const separatorWidth = 3;
1108
+ const availableWidth = terminalWidth - (timeWidth + 1 + nameWidth + 1 + separatorWidth);
1109
+ const requestedMaxContent = options.maxContentWidth ?? 80;
1110
+ return {
1111
+ terminalWidth,
1112
+ timeWidth,
1113
+ nameWidth,
1114
+ maxContentWidth: Math.max(Math.min(requestedMaxContent, availableWidth), 40),
1115
+ compactTime,
1116
+ separatorWidth
1117
+ };
1118
+ }
1119
+ /**
1120
+ * Detect if session is likely short (< 1 hour) based on current time
1121
+ * Heuristic: if it's early in the day, likely a short session
1122
+ */
1123
+ function isShortSession() {
1124
+ return false;
1125
+ }
1126
+ let lastTimestamp = null;
1127
+ /**
1128
+ * Format timestamp according to layout config
1129
+ */
1130
+ function formatTime(timestamp, layout) {
1131
+ const date = new Date(timestamp);
1132
+ const current = {
1133
+ hour: date.getHours(),
1134
+ minute: date.getMinutes(),
1135
+ second: date.getSeconds()
1136
+ };
1137
+ const last = lastTimestamp ? {
1138
+ hour: lastTimestamp.getHours(),
1139
+ minute: lastTimestamp.getMinutes()
1140
+ } : null;
1141
+ const hourChanged = !last || last.hour !== current.hour;
1142
+ const minuteChanged = !last || last.minute !== current.minute;
1143
+ lastTimestamp = date;
1144
+ return {
1145
+ formatted: layout.compactTime ? `${pad(current.minute)}:${pad(current.second)}` : `${pad(current.hour)}:${pad(current.minute)}:${pad(current.second)}`,
1146
+ hourChanged,
1147
+ minuteChanged
1148
+ };
1149
+ }
1150
+ /**
1151
+ * Reset time tracking (call when starting new workflow)
1152
+ */
1153
+ function resetTimeTracking() {
1154
+ lastTimestamp = null;
1155
+ }
1156
+ function pad(num) {
1157
+ return num.toString().padStart(2, "0");
1158
+ }
1159
+ function createGroupingState() {
1160
+ return {
1161
+ lastAgent: null,
1162
+ lastMinute: null,
1163
+ messageCount: 0
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Check if message should be grouped with previous one
1168
+ * Groups consecutive messages from same agent within same minute
1169
+ */
1170
+ function shouldGroup(agent, timestamp, state, enableGrouping) {
1171
+ if (!enableGrouping) return false;
1172
+ const date = new Date(timestamp);
1173
+ const minute = date.getHours() * 60 + date.getMinutes();
1174
+ const isSameAgent = agent === state.lastAgent;
1175
+ const isSameMinute = minute === state.lastMinute;
1176
+ state.lastAgent = agent;
1177
+ state.lastMinute = minute;
1178
+ state.messageCount++;
1179
+ return isSameAgent && isSameMinute && state.messageCount > 1;
1180
+ }
1181
+
1182
+ //#endregion
1183
+ //#region src/workflow/layout-log.ts
1184
+ /**
1185
+ * Log-oriented layout strategies
1186
+ *
1187
+ * Optimized for workflow logs where readability > strict alignment
1188
+ * Provides alternative formatting styles for different use cases
1189
+ */
1190
+ /**
1191
+ * Timeline style: Time with colored dot indicator for each agent
1192
+ *
1193
+ * Example:
1194
+ * 01:17:13 ● workflow
1195
+ * │ Running workflow: test-simple with a very long message
1196
+ * │ that wraps to the next line automatically
1197
+ * 01:17:20 ● alice
1198
+ * │ @bob What are AI agents?
1199
+ * 01:17:25 ● bob
1200
+ * │ @alice AI agents are autonomous software entities
1201
+ */
1202
+ function formatTimelineLog(entry, layout, showTime = true) {
1203
+ const time = formatTime(entry.timestamp, layout).formatted;
1204
+ const timeStr = showTime ? chalk.dim(time) : " ".repeat(layout.timeWidth);
1205
+ const agentColors = [
1206
+ chalk.cyan,
1207
+ chalk.yellow,
1208
+ chalk.magenta,
1209
+ chalk.green,
1210
+ chalk.blue,
1211
+ chalk.redBright
1212
+ ];
1213
+ let dotColor;
1214
+ if (entry.from === "workflow" || entry.from === "system") dotColor = agentColors[0];
1215
+ else dotColor = agentColors[entry.from.split("").reduce((acc, c) => acc + c.charCodeAt(0), 0) % (agentColors.length - 1) + 1];
1216
+ const dot = dotColor("●");
1217
+ const separator = chalk.dim("│");
1218
+ const prefixWidth = layout.timeWidth + 3;
1219
+ const contentWidth = Math.min(layout.terminalWidth - prefixWidth - 3, 80);
1220
+ const wrappedLines = wrapAnsi(entry.content, contentWidth, {
1221
+ hard: true,
1222
+ trim: false
1223
+ }).split("\n");
1224
+ const result = [];
1225
+ result.push(`${timeStr} ${dot} ${chalk.bold(entry.from)}`);
1226
+ const indent = " ".repeat(layout.timeWidth + 1) + `${separator} `;
1227
+ result.push(...wrappedLines.map((line) => indent + line));
1228
+ return result.join("\n");
1229
+ }
1230
+ /**
1231
+ * Standard log format: Minimal colors for TTY, plain text for pipes
1232
+ *
1233
+ * Format: TIMESTAMP SOURCE: MESSAGE
1234
+ * Example: 2026-02-09T01:17:13Z workflow: Running workflow: test
1235
+ *
1236
+ * TTY Mode (terminal):
1237
+ * - Dim timestamps for less visual weight
1238
+ * - Cyan source names for quick scanning
1239
+ * - Yellow [WARN] for warnings
1240
+ * - Red [ERROR] for errors
1241
+ *
1242
+ * Pipe Mode (file/grep):
1243
+ * - No colors (chalk auto-detects non-TTY)
1244
+ * - Pure text for maximum compatibility
1245
+ *
1246
+ * Designed for --debug mode, CI/CD logs, and piping to other tools
1247
+ */
1248
+ function formatStandardLog(entry, includeMillis = false) {
1249
+ const timestamp = includeMillis ? entry.timestamp : entry.timestamp.split(".")[0] + "Z";
1250
+ const lines = entry.content.split("\n");
1251
+ const isWarn = entry.content.includes("[WARN]");
1252
+ const isError = entry.content.includes("[ERROR]");
1253
+ let contentColor = (s) => s;
1254
+ if (isWarn) contentColor = chalk.yellow;
1255
+ if (isError) contentColor = chalk.red;
1256
+ const result = [`${chalk.dim(timestamp)} ${chalk.cyan(entry.from)}: ${contentColor(lines[0])}`];
1257
+ if (lines.length > 1) {
1258
+ const indent = " ".repeat(timestamp.length + 1 + entry.from.length + 2);
1259
+ result.push(...lines.slice(1).map((line) => indent + contentColor(line)));
1260
+ }
1261
+ return result.join("\n");
1262
+ }
1263
+
1264
+ //#endregion
1265
+ //#region src/workflow/display.ts
1266
+ /**
1267
+ * Channel display formatting
1268
+ *
1269
+ * All output flows through the channel. This module formats and filters
1270
+ * channel entries for terminal display:
1271
+ * - kind=undefined (agent messages): always shown, colored
1272
+ * - kind="log" (operational logs): always shown, dimmed
1273
+ * - kind="debug" (debug details): only shown with --debug flag
1274
+ *
1275
+ * Two display modes:
1276
+ * - TTY (human): colored, aligned columns, smart wrapping
1277
+ * - Non-TTY (agent/pipe): plain text, no colors, simple separators
1278
+ *
1279
+ * Best practices implemented:
1280
+ * - Adaptive layout based on terminal width and agent names
1281
+ * - Smart text wrapping preserving ANSI colors
1282
+ * - Message grouping to reduce visual noise
1283
+ * - Background-agnostic color scheme
1284
+ */
1285
+ /** Whether to use rich terminal formatting */
1286
+ const isTTY = !!process.stdout.isTTY && !process.env.NO_COLOR;
1287
+ /**
1288
+ * Background-agnostic color scheme
1289
+ * Uses only bold and standard colors that work on any theme
1290
+ */
1291
+ const C = {
1292
+ dim: isTTY ? chalk.dim : (s) => s,
1293
+ bold: isTTY ? chalk.bold : (s) => s,
1294
+ yellow: isTTY ? chalk.yellow : (s) => s,
1295
+ red: isTTY ? chalk.red : (s) => s,
1296
+ agents: isTTY ? [
1297
+ chalk.cyan,
1298
+ chalk.yellow,
1299
+ chalk.magenta,
1300
+ chalk.green,
1301
+ chalk.blue,
1302
+ chalk.redBright
1303
+ ] : Array(6).fill((s) => s),
1304
+ system: isTTY ? chalk.gray : (s) => s
1305
+ };
1306
+ /**
1307
+ * Create display context for a workflow
1308
+ */
1309
+ function createDisplayContext(agentNames, options) {
1310
+ return {
1311
+ layout: calculateLayout({ agentNames }),
1312
+ grouping: createGroupingState(),
1313
+ agentNames,
1314
+ enableGrouping: options?.enableGrouping ?? true,
1315
+ debugMode: options?.debugMode ?? false
1316
+ };
1317
+ }
1318
+ function sleep$1(ms) {
1319
+ return new Promise((resolve) => setTimeout(resolve, ms));
1320
+ }
1321
+ /**
1322
+ * Format a channel entry for display
1323
+ *
1324
+ * Two modes:
1325
+ * - Normal mode: Timeline-style layout for visual clarity
1326
+ * - Debug mode: Standard log format (timestamp source: message)
1327
+ */
1328
+ function formatChannelEntry(entry, context) {
1329
+ if (context.debugMode) return formatStandardLog(entry, false);
1330
+ const isFirstMessage = !shouldGroup(entry.from, entry.timestamp, context.grouping, false);
1331
+ return formatTimelineLog(entry, context.layout, isFirstMessage);
1332
+ }
1333
+ /**
1334
+ * Start watching channel and displaying new entries
1335
+ * with adaptive layout and smart formatting
1336
+ */
1337
+ function startChannelWatcher(config) {
1338
+ const { contextProvider, agentNames, log, showDebug = false, pollInterval = 500, enableGrouping = true } = config;
1339
+ resetTimeTracking();
1340
+ const context = createDisplayContext(agentNames, {
1341
+ enableGrouping: !showDebug && enableGrouping,
1342
+ debugMode: showDebug
1343
+ });
1344
+ let cursor = config.initialCursor ?? 0;
1345
+ let running = true;
1346
+ const poll = async () => {
1347
+ while (running) {
1348
+ try {
1349
+ const tail = await contextProvider.tailChannel(cursor);
1350
+ for (const entry of tail.entries) {
1351
+ if (entry.kind === "debug" && !showDebug) continue;
1352
+ log(formatChannelEntry(entry, context));
1353
+ }
1354
+ cursor = tail.cursor;
1355
+ } catch {}
1356
+ await sleep$1(pollInterval);
1357
+ }
1358
+ };
1359
+ poll();
1360
+ return { stop: () => {
1361
+ running = false;
1362
+ } };
1363
+ }
1364
+
1365
+ //#endregion
1366
+ //#region src/workflow/logger.ts
1367
+ /**
1368
+ * Create a silent logger (no output)
1369
+ */
1370
+ function createSilentLogger() {
1371
+ const noop = () => {};
1372
+ return {
1373
+ debug: noop,
1374
+ info: noop,
1375
+ warn: noop,
1376
+ error: noop,
1377
+ isDebug: () => false,
1378
+ child: () => createSilentLogger()
1379
+ };
1380
+ }
1381
+ /**
1382
+ * Create a logger that writes to the channel.
1383
+ *
1384
+ * - info/warn/error → channel entry with kind="log" (always shown to user)
1385
+ * - debug → channel entry with kind="debug" (only shown with --debug)
1386
+ *
1387
+ * The display layer handles formatting and filtering.
1388
+ */
1389
+ function createChannelLogger(config) {
1390
+ const { provider, from = "system" } = config;
1391
+ const formatContent = (level, message, args) => {
1392
+ const argsStr = args.length > 0 ? " " + args.map(formatArg).join(" ") : "";
1393
+ if (level === "warn") return `[WARN] ${message}${argsStr}`;
1394
+ if (level === "error") return `[ERROR] ${message}${argsStr}`;
1395
+ return `${message}${argsStr}`;
1396
+ };
1397
+ const write = (level, message, args) => {
1398
+ const content = formatContent(level, message, args);
1399
+ const kind = level === "debug" ? "debug" : "log";
1400
+ provider.appendChannel(from, content, { kind }).catch(() => {});
1401
+ };
1402
+ return {
1403
+ debug: (message, ...args) => write("debug", message, args),
1404
+ info: (message, ...args) => write("info", message, args),
1405
+ warn: (message, ...args) => write("warn", message, args),
1406
+ error: (message, ...args) => write("error", message, args),
1407
+ isDebug: () => true,
1408
+ child: (childPrefix) => {
1409
+ return createChannelLogger({
1410
+ provider,
1411
+ from: from ? `${from}:${childPrefix}` : childPrefix
1412
+ });
1413
+ }
1414
+ };
1415
+ }
1416
+ /** Format an argument for logging */
1417
+ function formatArg(arg) {
1418
+ if (arg === null || arg === void 0) return String(arg);
1419
+ if (typeof arg === "object") try {
1420
+ return JSON.stringify(arg);
1421
+ } catch {
1422
+ return String(arg);
1423
+ }
1424
+ return String(arg);
1425
+ }
1426
+
1427
+ //#endregion
1428
+ //#region src/workflow/runner.ts
1429
+ /**
1430
+ * Workflow Runner
1431
+ *
1432
+ * All output flows through the channel:
1433
+ * - Operational events (init, setup, connect) → kind="log" (always visible)
1434
+ * - Debug details (MCP traces, idle checks) → kind="debug" (visible with --debug)
1435
+ * - Agent messages → kind=undefined (always visible)
1436
+ *
1437
+ * The display layer (display.ts) handles filtering and formatting.
1438
+ */
1439
+ const execAsync = promisify(exec);
1440
+ /**
1441
+ * Create context provider and resolve context directory from workflow config.
1442
+ * Extracted so the channel logger can be created before full init.
1443
+ */
1444
+ function createWorkflowProvider(workflow, _workflowName, tag) {
1445
+ const agentNames = Object.keys(workflow.agents);
1446
+ if (!workflow.context) throw new Error("Workflow context is disabled. Remove \"context: false\" to enable agent collaboration.");
1447
+ const resolvedContext = workflow.context;
1448
+ if (resolvedContext.provider === "memory") return {
1449
+ contextProvider: createMemoryContextProvider(agentNames),
1450
+ contextDir: join(tmpdir(), `agent-worker-${workflow.name}-${tag}`),
1451
+ persistent: false
1452
+ };
1453
+ const fileContext = resolvedContext;
1454
+ const contextDir = fileContext.dir;
1455
+ const persistent = fileContext.persistent === true;
1456
+ if (!existsSync(contextDir)) mkdirSync(contextDir, { recursive: true });
1457
+ const fileProvider = createFileContextProvider(contextDir, agentNames);
1458
+ fileProvider.acquireLock();
1459
+ return {
1460
+ contextProvider: fileProvider,
1461
+ contextDir,
1462
+ persistent
1463
+ };
1464
+ }
1465
+ /**
1466
+ * Initialize workflow runtime
1467
+ *
1468
+ * This sets up:
1469
+ * 1. Context provider (file or memory) — or uses pre-created one
1470
+ * 2. Context directory (for file provider)
1471
+ * 3. MCP server (HTTP)
1472
+ * 4. Runs setup commands
1473
+ */
1474
+ async function initWorkflow(config) {
1475
+ const { workflow, workflowName: workflowNameParam, tag: tagParam, instance, onMention, debugLog, feedback: feedbackEnabled } = config;
1476
+ const workflowName = workflowNameParam ?? instance ?? "global";
1477
+ const tag = tagParam ?? "main";
1478
+ const logger = config.logger ?? createSilentLogger();
1479
+ const startTime = Date.now();
1480
+ const agentNames = Object.keys(workflow.agents);
1481
+ let contextProvider;
1482
+ let contextDir;
1483
+ let isPersistent = false;
1484
+ if (config.contextProvider && config.contextDir) {
1485
+ contextProvider = config.contextProvider;
1486
+ contextDir = config.contextDir;
1487
+ isPersistent = config.persistent ?? false;
1488
+ logger.debug("Using pre-created context provider");
1489
+ } else {
1490
+ const created = createWorkflowProvider(workflow, workflowName, tag);
1491
+ contextProvider = created.contextProvider;
1492
+ contextDir = created.contextDir;
1493
+ isPersistent = created.persistent;
1494
+ const mode = isPersistent ? "persistent (bind)" : "ephemeral";
1495
+ logger.debug(`Context directory: ${contextDir} [${mode}]`);
1496
+ await contextProvider.markRunStart();
1497
+ }
1498
+ const projectDir = process.cwd();
1499
+ let mcpGetFeedback;
1500
+ const createMCPServerInstance = () => {
1501
+ const mcp = createContextMCPServer({
1502
+ provider: contextProvider,
1503
+ validAgents: agentNames,
1504
+ name: `${workflow.name}-context`,
1505
+ version: "1.0.0",
1506
+ onMention,
1507
+ feedback: feedbackEnabled,
1508
+ debugLog
1509
+ });
1510
+ mcpGetFeedback = mcp.getFeedback;
1511
+ return mcp.server;
1512
+ };
1513
+ const httpMcpServer = await runWithHttp({
1514
+ createServerInstance: createMCPServerInstance,
1515
+ port: 0,
1516
+ onConnect: (agentId, sessionId) => {
1517
+ logger.debug(`Agent connected: ${agentId} (${sessionId.slice(0, 8)})`);
1518
+ },
1519
+ onDisconnect: (agentId, sessionId) => {
1520
+ logger.debug(`Agent disconnected: ${agentId} (${sessionId.slice(0, 8)})`);
1521
+ }
1522
+ });
1523
+ logger.debug(`MCP server: ${httpMcpServer.url}`);
1524
+ const setupResults = {};
1525
+ const context = createContext(workflow.name, tag, setupResults);
1526
+ if (workflow.setup && workflow.setup.length > 0) {
1527
+ logger.info("Running setup...");
1528
+ for (const task of workflow.setup) try {
1529
+ const result = await runSetupTask(task, context, logger);
1530
+ if (task.as) {
1531
+ setupResults[task.as] = result;
1532
+ context[task.as] = result;
1533
+ }
1534
+ } catch (error) {
1535
+ if (contextProvider instanceof FileContextProvider) contextProvider.releaseLock();
1536
+ await httpMcpServer.close();
1537
+ throw new Error(`Setup failed: ${error instanceof Error ? error.message : String(error)}`);
1538
+ }
1539
+ }
1540
+ const interpolatedKickoff = workflow.kickoff ? interpolate(workflow.kickoff, context, (msg) => logger.warn(msg)) : void 0;
1541
+ const runtime = {
1542
+ name: workflow.name,
1543
+ instance: instance ?? `${workflowName}:${tag}`,
1544
+ contextDir,
1545
+ projectDir,
1546
+ contextProvider,
1547
+ httpMcpServer,
1548
+ mcpUrl: httpMcpServer.url,
1549
+ agentNames,
1550
+ setupResults,
1551
+ async sendKickoff() {
1552
+ if (!interpolatedKickoff) {
1553
+ logger.debug("No kickoff message configured");
1554
+ return;
1555
+ }
1556
+ logger.debug(`Kickoff: ${interpolatedKickoff.slice(0, 100)}...`);
1557
+ await contextProvider.appendChannel("system", interpolatedKickoff);
1558
+ },
1559
+ async shutdown() {
1560
+ logger.debug("Shutting down...");
1561
+ if (isPersistent) {
1562
+ if (contextProvider instanceof FileContextProvider) contextProvider.releaseLock();
1563
+ } else await contextProvider.destroy();
1564
+ await httpMcpServer.close();
1565
+ },
1566
+ getFeedback: mcpGetFeedback
1567
+ };
1568
+ logger.debug(`Workflow initialized in ${Date.now() - startTime}ms`);
1569
+ logger.debug(`Agents: ${agentNames.join(", ")}`);
1570
+ return runtime;
1571
+ }
1572
+ /**
1573
+ * Run a setup task
1574
+ */
1575
+ async function runSetupTask(task, context, logger) {
1576
+ const command = interpolate(task.shell, context);
1577
+ const displayCmd = command.length > 60 ? command.slice(0, 60) + "..." : command;
1578
+ logger.debug(` $ ${displayCmd}`);
1579
+ try {
1580
+ const { stdout, stderr } = await execAsync(command);
1581
+ const result = stdout.trim();
1582
+ if (stderr && stderr.trim()) logger.debug(` stderr: ${stderr.trim().slice(0, 100)}${stderr.length > 100 ? "..." : ""}`);
1583
+ if (task.as) {
1584
+ const displayResult = result.length > 60 ? result.slice(0, 60) + "..." : result;
1585
+ logger.debug(` ${task.as} = ${displayResult}`);
1586
+ }
1587
+ return result;
1588
+ } catch (error) {
1589
+ throw new Error(`Command failed: ${command}\n${error instanceof Error ? error.message : String(error)}`);
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Run a workflow with agent controllers
1594
+ *
1595
+ * All output flows through the channel. The channel watcher (display layer)
1596
+ * filters what to show: --debug includes kind="debug" entries.
1597
+ */
1598
+ async function runWorkflowWithControllers(config) {
1599
+ const { workflow, workflowName: workflowNameParam, tag: tagParam, instance, debug = false, log = console.log, mode = "run", pollInterval = 5e3, createBackend, feedback: feedbackEnabled } = config;
1600
+ const startTime = Date.now();
1601
+ const workflowName = workflowNameParam ?? instance ?? "global";
1602
+ const tag = tagParam ?? "main";
1603
+ try {
1604
+ const { contextProvider, contextDir, persistent } = createWorkflowProvider(workflow, workflowName, tag);
1605
+ const { cursor: channelStart } = await contextProvider.tailChannel(0);
1606
+ await contextProvider.markRunStart();
1607
+ const logger = createChannelLogger({
1608
+ provider: contextProvider,
1609
+ from: "workflow"
1610
+ });
1611
+ logger.info(`Running workflow: ${workflow.name}`);
1612
+ logger.info(`Agents: ${Object.keys(workflow.agents).join(", ")}`);
1613
+ logger.debug("Starting workflow with controllers", {
1614
+ mode,
1615
+ instance,
1616
+ pollInterval
1617
+ });
1618
+ const controllers = /* @__PURE__ */ new Map();
1619
+ logger.debug("Initializing workflow runtime...");
1620
+ const runtime = await initWorkflow({
1621
+ workflow,
1622
+ instance,
1623
+ startAgent: async () => {},
1624
+ logger,
1625
+ contextProvider,
1626
+ contextDir,
1627
+ persistent,
1628
+ onMention: (from, target, entry) => {
1629
+ const controller = controllers.get(target);
1630
+ if (controller) {
1631
+ const preview = entry.content.length > 80 ? entry.content.slice(0, 80) + "..." : entry.content;
1632
+ logger.debug(`@mention: ${from} → @${target} (state=${controller.state}): ${preview}`);
1633
+ controller.wake();
1634
+ } else logger.debug(`@mention: ${from} → @${target} (no controller found!)`);
1635
+ },
1636
+ debugLog: (msg) => {
1637
+ if (msg.startsWith("CALL ")) logger.info(msg);
1638
+ else logger.debug(msg);
1639
+ },
1640
+ feedback: feedbackEnabled
1641
+ });
1642
+ logger.debug("Runtime initialized", {
1643
+ agentNames: runtime.agentNames,
1644
+ mcpUrl: runtime.mcpUrl
1645
+ });
1646
+ logger.info("Starting agents...");
1647
+ for (const agentName of runtime.agentNames) {
1648
+ const agentDef = workflow.agents[agentName];
1649
+ logger.debug(`Creating controller for ${agentName}`, {
1650
+ backend: agentDef.backend,
1651
+ model: agentDef.model
1652
+ });
1653
+ const backendDebugLog = (msg) => {
1654
+ if (msg.startsWith("CALL ")) logger.info(msg);
1655
+ else logger.debug(msg);
1656
+ };
1657
+ let backend;
1658
+ if (createBackend) backend = createBackend(agentName, agentDef);
1659
+ else if (agentDef.backend) backend = getBackendByType(agentDef.backend, {
1660
+ model: agentDef.model,
1661
+ debugLog: backendDebugLog
1662
+ });
1663
+ else if (agentDef.model) backend = getBackendForModel(agentDef.model, { debugLog: backendDebugLog });
1664
+ else throw new Error(`Agent "${agentName}" requires either a backend or model field`);
1665
+ logger.debug(`Using backend: ${backend.type} for ${agentName}`);
1666
+ const workspaceDir = join(runtime.contextDir, "workspaces", agentName);
1667
+ if (!existsSync(workspaceDir)) mkdirSync(workspaceDir, { recursive: true });
1668
+ const controllerLogger = logger.child(agentName);
1669
+ const controller = createAgentController({
1670
+ name: agentName,
1671
+ agent: agentDef,
1672
+ contextProvider: runtime.contextProvider,
1673
+ mcpUrl: runtime.mcpUrl,
1674
+ workspaceDir,
1675
+ projectDir: runtime.projectDir,
1676
+ backend,
1677
+ pollInterval,
1678
+ log: (msg) => controllerLogger.debug(msg),
1679
+ errorLog: (msg) => controllerLogger.error(msg),
1680
+ feedback: feedbackEnabled
1681
+ });
1682
+ controllers.set(agentName, controller);
1683
+ await controller.start();
1684
+ logger.debug(`Controller started: ${agentName}`);
1685
+ }
1686
+ logger.debug("Sending kickoff message...");
1687
+ await runtime.sendKickoff();
1688
+ logger.debug("Kickoff sent");
1689
+ const channelWatcher = startChannelWatcher({
1690
+ contextProvider: runtime.contextProvider,
1691
+ agentNames: runtime.agentNames,
1692
+ log,
1693
+ showDebug: debug,
1694
+ initialCursor: channelStart,
1695
+ pollInterval: 250
1696
+ });
1697
+ if (mode === "run") {
1698
+ logger.debug("Running in \"run\" mode, waiting for completion...");
1699
+ let idleCheckCount = 0;
1700
+ while (true) {
1701
+ const isIdle = await checkWorkflowIdle(controllers, runtime.contextProvider);
1702
+ idleCheckCount++;
1703
+ if (idleCheckCount % 10 === 0) {
1704
+ const states = [...controllers.entries()].map(([n, c]) => `${n}=${c.state}`).join(", ");
1705
+ logger.debug(`Idle check #${idleCheckCount}: ${states}`);
1706
+ for (const [agentName] of controllers) {
1707
+ const inbox = await runtime.contextProvider.getInbox(agentName);
1708
+ if (inbox.length > 0) {
1709
+ const unseenCount = inbox.filter((m) => !m.seen).length;
1710
+ const seenCount = inbox.filter((m) => m.seen).length;
1711
+ const parts = [];
1712
+ if (seenCount > 0) parts.push(`${seenCount} processing`);
1713
+ if (unseenCount > 0) parts.push(`${unseenCount} unread`);
1714
+ logger.debug(` ${agentName} inbox: ${parts.join(", ")} from [${inbox.map((m) => m.entry.from).join(", ")}]`);
1715
+ }
1716
+ }
1717
+ }
1718
+ if (isIdle) {
1719
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
1720
+ logger.info(`Workflow complete (${elapsed}s)`);
1721
+ break;
1722
+ }
1723
+ await sleep(1e3);
1724
+ }
1725
+ channelWatcher?.stop();
1726
+ await shutdownControllers(controllers, logger);
1727
+ await runtime.shutdown();
1728
+ logger.debug(`Workflow finished in ${Date.now() - startTime}ms`);
1729
+ return {
1730
+ success: true,
1731
+ setupResults: runtime.setupResults,
1732
+ duration: Date.now() - startTime,
1733
+ mcpUrl: runtime.mcpUrl,
1734
+ contextProvider: runtime.contextProvider,
1735
+ feedback: runtime.getFeedback?.()
1736
+ };
1737
+ }
1738
+ logger.debug("Running in \"start\" mode, returning control to caller");
1739
+ return {
1740
+ success: true,
1741
+ setupResults: runtime.setupResults,
1742
+ duration: Date.now() - startTime,
1743
+ mcpUrl: runtime.mcpUrl,
1744
+ contextProvider: runtime.contextProvider,
1745
+ controllers,
1746
+ shutdown: async () => {
1747
+ channelWatcher?.stop();
1748
+ await shutdownControllers(controllers, logger);
1749
+ await runtime.shutdown();
1750
+ },
1751
+ getFeedback: runtime.getFeedback
1752
+ };
1753
+ } catch (error) {
1754
+ const errorMsg = error instanceof Error ? error.message : String(error);
1755
+ log(`Error: ${errorMsg}`);
1756
+ return {
1757
+ success: false,
1758
+ error: errorMsg,
1759
+ setupResults: {},
1760
+ duration: Date.now() - startTime
1761
+ };
1762
+ }
1763
+ }
1764
+ /**
1765
+ * Gracefully shutdown all controllers
1766
+ */
1767
+ async function shutdownControllers(controllers, logger) {
1768
+ logger.debug("Stopping controllers...");
1769
+ const stopPromises = [...controllers.values()].map(async (controller) => {
1770
+ await controller.stop();
1771
+ logger.debug(`Stopped controller: ${controller.name}`);
1772
+ });
1773
+ await Promise.all(stopPromises);
1774
+ logger.debug("All controllers stopped");
1775
+ }
1776
+ /**
1777
+ * Sleep helper
1778
+ */
1779
+ function sleep(ms) {
1780
+ return new Promise((resolve) => setTimeout(resolve, ms));
1781
+ }
1782
+
1783
+ //#endregion
1784
+ export { parseWorkflowFile, runWorkflowWithControllers };