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