@agento-nexus/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-UXIUTT4T.js +672 -0
- package/dist/chunk-UXIUTT4T.js.map +1 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +261 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/src/index.d.ts +369 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
// src/utils/logger.ts
|
|
2
|
+
var levels = {
|
|
3
|
+
debug: 0,
|
|
4
|
+
info: 1,
|
|
5
|
+
warn: 2,
|
|
6
|
+
error: 3
|
|
7
|
+
};
|
|
8
|
+
var Logger = class {
|
|
9
|
+
constructor(prefix, level = "info") {
|
|
10
|
+
this.prefix = prefix;
|
|
11
|
+
this.minLevel = levels[level];
|
|
12
|
+
}
|
|
13
|
+
minLevel;
|
|
14
|
+
debug(msg, data) {
|
|
15
|
+
this.log("debug", msg, data);
|
|
16
|
+
}
|
|
17
|
+
info(msg, data) {
|
|
18
|
+
this.log("info", msg, data);
|
|
19
|
+
}
|
|
20
|
+
warn(msg, data) {
|
|
21
|
+
this.log("warn", msg, data);
|
|
22
|
+
}
|
|
23
|
+
error(msg, data) {
|
|
24
|
+
this.log("error", msg, data);
|
|
25
|
+
}
|
|
26
|
+
log(level, msg, data) {
|
|
27
|
+
if (levels[level] < this.minLevel) return;
|
|
28
|
+
const entry = {
|
|
29
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30
|
+
level,
|
|
31
|
+
component: this.prefix,
|
|
32
|
+
msg,
|
|
33
|
+
...data
|
|
34
|
+
};
|
|
35
|
+
const out = level === "error" ? console.error : console.log;
|
|
36
|
+
out(JSON.stringify(entry));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/template-builder.ts
|
|
41
|
+
import { Sandbox } from "e2b";
|
|
42
|
+
var DEFAULT_BASE_TEMPLATE = "claude";
|
|
43
|
+
var TemplateBuilder = class {
|
|
44
|
+
log;
|
|
45
|
+
constructor() {
|
|
46
|
+
this.log = new Logger("template-builder");
|
|
47
|
+
}
|
|
48
|
+
/** Build an E2B template with OpenFang installed */
|
|
49
|
+
async build(config = {}) {
|
|
50
|
+
const base = config.baseTemplate ?? DEFAULT_BASE_TEMPLATE;
|
|
51
|
+
this.log.info("Building template", { base });
|
|
52
|
+
const sandbox = await Sandbox.create(base, {
|
|
53
|
+
timeoutMs: 3e5
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const installCmd = config.openfangVersion ? `pip install openfang==${config.openfangVersion}` : "pip install openfang";
|
|
57
|
+
this.log.info("Installing OpenFang", { cmd: installCmd });
|
|
58
|
+
const installResult = await sandbox.commands.run(installCmd, {
|
|
59
|
+
timeoutMs: 12e4
|
|
60
|
+
});
|
|
61
|
+
if (installResult.exitCode !== 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Failed to install OpenFang: ${installResult.stderr}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (config.packages?.length) {
|
|
67
|
+
const pkgCmd = `pip install ${config.packages.join(" ")}`;
|
|
68
|
+
this.log.info("Installing additional packages", { cmd: pkgCmd });
|
|
69
|
+
const pkgResult = await sandbox.commands.run(pkgCmd, {
|
|
70
|
+
timeoutMs: 12e4
|
|
71
|
+
});
|
|
72
|
+
if (pkgResult.exitCode !== 0) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Failed to install packages: ${pkgResult.stderr}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (config.setupScript) {
|
|
79
|
+
this.log.info("Running custom setup script");
|
|
80
|
+
const setupResult = await sandbox.commands.run(config.setupScript, {
|
|
81
|
+
timeoutMs: 12e4
|
|
82
|
+
});
|
|
83
|
+
if (setupResult.exitCode !== 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Setup script failed: ${setupResult.stderr}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const versionResult = await sandbox.commands.run(
|
|
90
|
+
"openfang --version",
|
|
91
|
+
{ timeoutMs: 1e4 }
|
|
92
|
+
);
|
|
93
|
+
if (versionResult.exitCode !== 0) {
|
|
94
|
+
throw new Error("OpenFang installation verification failed");
|
|
95
|
+
}
|
|
96
|
+
const openfangVersion = versionResult.stdout.trim();
|
|
97
|
+
this.log.info("OpenFang installed", { version: openfangVersion });
|
|
98
|
+
return {
|
|
99
|
+
templateId: sandbox.sandboxId,
|
|
100
|
+
openfangVersion
|
|
101
|
+
};
|
|
102
|
+
} finally {
|
|
103
|
+
await sandbox.kill();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Verify an existing template has OpenFang ready */
|
|
107
|
+
async verify(templateId) {
|
|
108
|
+
this.log.info("Verifying template", { templateId });
|
|
109
|
+
const sandbox = await Sandbox.create(templateId, {
|
|
110
|
+
timeoutMs: 6e4
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
const result = await sandbox.commands.run("openfang --version", {
|
|
114
|
+
timeoutMs: 1e4
|
|
115
|
+
});
|
|
116
|
+
if (result.exitCode !== 0) {
|
|
117
|
+
return { healthy: false, error: "openfang not found" };
|
|
118
|
+
}
|
|
119
|
+
return { healthy: true, version: result.stdout.trim() };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
healthy: false,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error)
|
|
124
|
+
};
|
|
125
|
+
} finally {
|
|
126
|
+
await sandbox.kill();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/utils/retry.ts
|
|
132
|
+
var defaults = {
|
|
133
|
+
maxAttempts: 3,
|
|
134
|
+
initialDelayMs: 500,
|
|
135
|
+
maxDelayMs: 1e4,
|
|
136
|
+
backoffMultiplier: 2
|
|
137
|
+
};
|
|
138
|
+
async function retry(fn, opts = {}) {
|
|
139
|
+
const { maxAttempts, initialDelayMs, maxDelayMs, backoffMultiplier } = {
|
|
140
|
+
...defaults,
|
|
141
|
+
...opts
|
|
142
|
+
};
|
|
143
|
+
const retryIf = opts.retryIf ?? (() => true);
|
|
144
|
+
let lastError;
|
|
145
|
+
let delay = initialDelayMs;
|
|
146
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
return await fn();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
lastError = error;
|
|
151
|
+
if (attempt === maxAttempts || !retryIf(error)) {
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
await sleep(delay);
|
|
155
|
+
delay = Math.min(delay * backoffMultiplier, maxDelayMs);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw lastError;
|
|
159
|
+
}
|
|
160
|
+
function sleep(ms) {
|
|
161
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/openfang-client.ts
|
|
165
|
+
var OpenFangClient = class {
|
|
166
|
+
baseUrl;
|
|
167
|
+
timeoutMs;
|
|
168
|
+
retryOpts;
|
|
169
|
+
log;
|
|
170
|
+
constructor(config) {
|
|
171
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
172
|
+
this.timeoutMs = config.timeoutMs ?? 3e4;
|
|
173
|
+
this.retryOpts = config.retryOptions ?? { maxAttempts: 3 };
|
|
174
|
+
this.log = new Logger("openfang-client");
|
|
175
|
+
}
|
|
176
|
+
/** Check daemon health */
|
|
177
|
+
async health() {
|
|
178
|
+
return this.request("GET", "/health");
|
|
179
|
+
}
|
|
180
|
+
/** Wait for daemon to become healthy */
|
|
181
|
+
async waitForHealthy(maxWaitMs = 3e4) {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
return retry(
|
|
184
|
+
async () => {
|
|
185
|
+
if (Date.now() - startTime > maxWaitMs) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`OpenFang daemon not healthy after ${maxWaitMs}ms`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return this.health();
|
|
191
|
+
},
|
|
192
|
+
{ maxAttempts: Math.ceil(maxWaitMs / 2e3), initialDelayMs: 1e3 }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
/** Spawn a new agent from a Hand manifest */
|
|
196
|
+
async spawnAgent(manifest) {
|
|
197
|
+
this.log.info("Spawning agent", { name: manifest.name });
|
|
198
|
+
return this.request("POST", "/agents", manifest);
|
|
199
|
+
}
|
|
200
|
+
/** List all running agents */
|
|
201
|
+
async listAgents() {
|
|
202
|
+
return this.request("GET", "/agents");
|
|
203
|
+
}
|
|
204
|
+
/** Get agent info by ID */
|
|
205
|
+
async getAgent(agentId) {
|
|
206
|
+
return this.request("GET", `/agents/${agentId}`);
|
|
207
|
+
}
|
|
208
|
+
/** Send a message to an agent */
|
|
209
|
+
async messageAgent(agentId, content) {
|
|
210
|
+
return this.request(
|
|
211
|
+
"POST",
|
|
212
|
+
`/agents/${agentId}/messages`,
|
|
213
|
+
{ content }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
/** Stop an agent */
|
|
217
|
+
async stopAgent(agentId) {
|
|
218
|
+
await this.request("DELETE", `/agents/${agentId}`);
|
|
219
|
+
}
|
|
220
|
+
/** List available tools */
|
|
221
|
+
async listTools() {
|
|
222
|
+
return this.request("GET", "/tools");
|
|
223
|
+
}
|
|
224
|
+
async request(method, path, body) {
|
|
225
|
+
return retry(async () => {
|
|
226
|
+
const controller = new AbortController();
|
|
227
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
228
|
+
try {
|
|
229
|
+
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
230
|
+
method,
|
|
231
|
+
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
232
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
233
|
+
signal: controller.signal
|
|
234
|
+
});
|
|
235
|
+
if (!res.ok) {
|
|
236
|
+
const text = await res.text().catch(() => "");
|
|
237
|
+
throw new OpenFangError(
|
|
238
|
+
`${method} ${path} returned ${res.status}: ${text}`,
|
|
239
|
+
res.status
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (res.status === 204) return void 0;
|
|
243
|
+
return await res.json();
|
|
244
|
+
} finally {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
}
|
|
247
|
+
}, this.retryOpts);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
var OpenFangError = class extends Error {
|
|
251
|
+
constructor(message, statusCode) {
|
|
252
|
+
super(message);
|
|
253
|
+
this.statusCode = statusCode;
|
|
254
|
+
this.name = "OpenFangError";
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/claude-bridge.ts
|
|
259
|
+
var ClaudeBridge = class {
|
|
260
|
+
constructor(sandbox) {
|
|
261
|
+
this.sandbox = sandbox;
|
|
262
|
+
this.log = new Logger("claude-bridge");
|
|
263
|
+
}
|
|
264
|
+
log;
|
|
265
|
+
/** Execute a Claude Code prompt inside the sandbox */
|
|
266
|
+
async execute(request) {
|
|
267
|
+
const start = Date.now();
|
|
268
|
+
const args = this.buildArgs(request);
|
|
269
|
+
this.log.info("Executing Claude Code", {
|
|
270
|
+
prompt: request.prompt.slice(0, 100),
|
|
271
|
+
outputFormat: request.outputFormat
|
|
272
|
+
});
|
|
273
|
+
const result = await this.sandbox.commands.run(
|
|
274
|
+
`claude ${args.join(" ")}`,
|
|
275
|
+
{
|
|
276
|
+
cwd: request.cwd ?? "/home/user",
|
|
277
|
+
timeoutMs: (request.maxTokens ?? 120) * 1e3
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
const durationMs = Date.now() - start;
|
|
281
|
+
const output = result.stdout;
|
|
282
|
+
let parsed;
|
|
283
|
+
if (request.outputFormat === "json") {
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(output);
|
|
286
|
+
} catch {
|
|
287
|
+
this.log.warn("Failed to parse JSON output from Claude Code");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
output,
|
|
292
|
+
exitCode: result.exitCode,
|
|
293
|
+
parsed,
|
|
294
|
+
durationMs,
|
|
295
|
+
sessionId: request.sessionId
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Check if Claude Code is installed in the sandbox */
|
|
299
|
+
async isAvailable() {
|
|
300
|
+
try {
|
|
301
|
+
const result = await this.sandbox.commands.run("claude --version", {
|
|
302
|
+
timeoutMs: 1e4
|
|
303
|
+
});
|
|
304
|
+
return result.exitCode === 0;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
buildArgs(request) {
|
|
310
|
+
const args = ["-p", this.shellEscape(request.prompt)];
|
|
311
|
+
if (request.outputFormat === "json") {
|
|
312
|
+
args.push("--output-format", "json");
|
|
313
|
+
}
|
|
314
|
+
if (request.sessionId) {
|
|
315
|
+
args.push("--session-id", request.sessionId);
|
|
316
|
+
}
|
|
317
|
+
if (request.maxTokens) {
|
|
318
|
+
args.push("--max-tokens", String(request.maxTokens));
|
|
319
|
+
}
|
|
320
|
+
if (request.flags) {
|
|
321
|
+
args.push(...request.flags);
|
|
322
|
+
}
|
|
323
|
+
return args;
|
|
324
|
+
}
|
|
325
|
+
shellEscape(str) {
|
|
326
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// src/utils/events.ts
|
|
331
|
+
var TypedEmitter = class {
|
|
332
|
+
listeners = /* @__PURE__ */ new Set();
|
|
333
|
+
on(listener) {
|
|
334
|
+
this.listeners.add(listener);
|
|
335
|
+
return () => this.listeners.delete(listener);
|
|
336
|
+
}
|
|
337
|
+
emit(event) {
|
|
338
|
+
for (const listener of this.listeners) {
|
|
339
|
+
listener(event);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
removeAll() {
|
|
343
|
+
this.listeners.clear();
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// src/fangbox.ts
|
|
348
|
+
import { Sandbox as Sandbox2 } from "e2b";
|
|
349
|
+
var DEFAULT_TEMPLATE = "openfang-claude";
|
|
350
|
+
var DEFAULT_PORT = 4200;
|
|
351
|
+
var DEFAULT_TIMEOUT = 300;
|
|
352
|
+
var FangBox = class _FangBox {
|
|
353
|
+
sandbox;
|
|
354
|
+
client;
|
|
355
|
+
claudeBridge;
|
|
356
|
+
events = new TypedEmitter();
|
|
357
|
+
log;
|
|
358
|
+
constructor(sandbox, client, claudeBridge) {
|
|
359
|
+
this.sandbox = sandbox;
|
|
360
|
+
this.client = client;
|
|
361
|
+
this.claudeBridge = claudeBridge;
|
|
362
|
+
this.log = new Logger("fangbox");
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Create a new FangBox — spins up sandbox, starts daemon, waits for health.
|
|
366
|
+
* All commands run inside the E2B cloud sandbox (not local shell).
|
|
367
|
+
*/
|
|
368
|
+
static async create(config = {}) {
|
|
369
|
+
const emitter = new TypedEmitter();
|
|
370
|
+
const log = new Logger("fangbox");
|
|
371
|
+
const port = config.openfangPort ?? DEFAULT_PORT;
|
|
372
|
+
emitter.emit({ type: "sandbox:creating" });
|
|
373
|
+
log.info("Creating sandbox", {
|
|
374
|
+
template: config.templateId ?? DEFAULT_TEMPLATE
|
|
375
|
+
});
|
|
376
|
+
const sandbox = await Sandbox2.create(config.templateId ?? DEFAULT_TEMPLATE, {
|
|
377
|
+
envs: config.envs,
|
|
378
|
+
timeoutMs: (config.timeout ?? DEFAULT_TIMEOUT) * 1e3,
|
|
379
|
+
metadata: config.metadata
|
|
380
|
+
});
|
|
381
|
+
const sandboxId = sandbox.sandboxId;
|
|
382
|
+
emitter.emit({ type: "sandbox:ready", sandboxId });
|
|
383
|
+
const baseUrl = `https://${sandboxId}-${port}.e2b.dev`;
|
|
384
|
+
const client = new OpenFangClient({ baseUrl });
|
|
385
|
+
const claudeBridge = new ClaudeBridge(sandbox);
|
|
386
|
+
const box = new _FangBox(sandbox, client, claudeBridge);
|
|
387
|
+
box.events.on((e) => emitter.emit(e));
|
|
388
|
+
const daemonCmd = ["openfang", "serve", "--port", String(port)].join(" ");
|
|
389
|
+
emitter.emit({ type: "daemon:starting" });
|
|
390
|
+
await sandbox.commands.run(daemonCmd, { background: true });
|
|
391
|
+
try {
|
|
392
|
+
const health = await client.waitForHealthy(3e4);
|
|
393
|
+
emitter.emit({ type: "daemon:healthy", version: health.version });
|
|
394
|
+
log.info("Daemon healthy", { version: health.version });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
397
|
+
emitter.emit({ type: "daemon:unhealthy", error: msg });
|
|
398
|
+
throw new Error(`OpenFang daemon failed to start: ${msg}`);
|
|
399
|
+
}
|
|
400
|
+
return box;
|
|
401
|
+
}
|
|
402
|
+
get sandboxId() {
|
|
403
|
+
return this.sandbox.sandboxId;
|
|
404
|
+
}
|
|
405
|
+
/** Check daemon health */
|
|
406
|
+
async health() {
|
|
407
|
+
return this.client.health();
|
|
408
|
+
}
|
|
409
|
+
/** Spawn an agent from a Hand manifest or name */
|
|
410
|
+
async spawnAgent(handOrName) {
|
|
411
|
+
const manifest = typeof handOrName === "string" ? { name: handOrName } : handOrName;
|
|
412
|
+
const agent = await this.client.spawnAgent(manifest);
|
|
413
|
+
this.events.emit({
|
|
414
|
+
type: "agent:spawned",
|
|
415
|
+
agentId: agent.id,
|
|
416
|
+
name: agent.name
|
|
417
|
+
});
|
|
418
|
+
return agent;
|
|
419
|
+
}
|
|
420
|
+
/** Send a message to a running agent */
|
|
421
|
+
async messageAgent(agentId, content) {
|
|
422
|
+
const response = await this.client.messageAgent(agentId, content);
|
|
423
|
+
this.events.emit({
|
|
424
|
+
type: "agent:message",
|
|
425
|
+
agentId,
|
|
426
|
+
content: response.content
|
|
427
|
+
});
|
|
428
|
+
return response;
|
|
429
|
+
}
|
|
430
|
+
/** List all running agents */
|
|
431
|
+
async listAgents() {
|
|
432
|
+
return this.client.listAgents();
|
|
433
|
+
}
|
|
434
|
+
/** Stop an agent */
|
|
435
|
+
async stopAgent(agentId) {
|
|
436
|
+
await this.client.stopAgent(agentId);
|
|
437
|
+
this.events.emit({ type: "agent:completed", agentId });
|
|
438
|
+
}
|
|
439
|
+
/** Execute Claude Code in the sandbox */
|
|
440
|
+
async runClaude(request) {
|
|
441
|
+
return this.claudeBridge.execute(request);
|
|
442
|
+
}
|
|
443
|
+
/** Upload a file to the sandbox */
|
|
444
|
+
async uploadFile(path, content) {
|
|
445
|
+
await this.sandbox.files.write(path, content);
|
|
446
|
+
}
|
|
447
|
+
/** Read a file from the sandbox */
|
|
448
|
+
async readFile(path) {
|
|
449
|
+
return this.sandbox.files.read(path);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Run a command inside the E2B cloud sandbox.
|
|
453
|
+
* This uses E2B's sandbox.commands.run() which executes remotely,
|
|
454
|
+
* not on the local machine.
|
|
455
|
+
*/
|
|
456
|
+
async runInSandbox(command, opts) {
|
|
457
|
+
const result = await this.sandbox.commands.run(command, opts);
|
|
458
|
+
return {
|
|
459
|
+
stdout: result.stdout,
|
|
460
|
+
stderr: result.stderr,
|
|
461
|
+
exitCode: result.exitCode
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
/** Destroy the sandbox */
|
|
465
|
+
async destroy() {
|
|
466
|
+
const id = this.sandboxId;
|
|
467
|
+
this.log.info("Destroying sandbox", { sandboxId: id });
|
|
468
|
+
await this.sandbox.kill();
|
|
469
|
+
this.events.emit({ type: "sandbox:destroyed", sandboxId: id });
|
|
470
|
+
this.events.removeAll();
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
async function createFangBox(config) {
|
|
474
|
+
return FangBox.create(config);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/fleet.ts
|
|
478
|
+
var Fleet = class {
|
|
479
|
+
events = new TypedEmitter();
|
|
480
|
+
log;
|
|
481
|
+
maxConcurrency;
|
|
482
|
+
defaultTimeout;
|
|
483
|
+
envs;
|
|
484
|
+
constructor(config = {}) {
|
|
485
|
+
this.maxConcurrency = config.maxConcurrency ?? 3;
|
|
486
|
+
this.defaultTimeout = config.defaultTimeout ?? 300;
|
|
487
|
+
this.envs = config.envs ?? {};
|
|
488
|
+
this.log = new Logger("fleet");
|
|
489
|
+
}
|
|
490
|
+
/** Run a workflow from a parsed definition */
|
|
491
|
+
async run(workflow) {
|
|
492
|
+
const start = Date.now();
|
|
493
|
+
this.log.info("Starting workflow", { name: workflow.name });
|
|
494
|
+
this.events.emit({ type: "workflow:start", name: workflow.name });
|
|
495
|
+
const sorted = this.topologicalSort(workflow.steps);
|
|
496
|
+
const outputs = {};
|
|
497
|
+
const results = [];
|
|
498
|
+
const completed = /* @__PURE__ */ new Set();
|
|
499
|
+
const pending = [...sorted];
|
|
500
|
+
while (pending.length > 0) {
|
|
501
|
+
const ready = pending.filter(
|
|
502
|
+
(step) => (step.dependsOn ?? []).every((dep) => completed.has(dep))
|
|
503
|
+
);
|
|
504
|
+
if (ready.length === 0 && pending.length > 0) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
"Deadlock: no steps can proceed. Check for circular dependencies."
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const batch = ready.slice(0, this.maxConcurrency);
|
|
510
|
+
const batchResults = await Promise.allSettled(
|
|
511
|
+
batch.map((step) => this.executeStep(step, outputs, workflow.envs))
|
|
512
|
+
);
|
|
513
|
+
for (let i = 0; i < batch.length; i++) {
|
|
514
|
+
const step = batch[i];
|
|
515
|
+
const settled = batchResults[i];
|
|
516
|
+
let result;
|
|
517
|
+
if (settled.status === "fulfilled") {
|
|
518
|
+
result = settled.value;
|
|
519
|
+
} else {
|
|
520
|
+
result = {
|
|
521
|
+
stepId: step.id,
|
|
522
|
+
status: "failure",
|
|
523
|
+
error: settled.reason instanceof Error ? settled.reason.message : String(settled.reason),
|
|
524
|
+
durationMs: 0
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
results.push(result);
|
|
528
|
+
completed.add(step.id);
|
|
529
|
+
if (result.status === "success" && step.outputKey && result.output) {
|
|
530
|
+
outputs[step.outputKey] = result.output;
|
|
531
|
+
}
|
|
532
|
+
this.events.emit({ type: "step:complete", stepId: step.id, result });
|
|
533
|
+
const idx = pending.indexOf(step);
|
|
534
|
+
if (idx !== -1) pending.splice(idx, 1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const status = results.every((r) => r.status === "success") ? "success" : results.some((r) => r.status === "success") ? "partial" : "failure";
|
|
538
|
+
const workflowResult = {
|
|
539
|
+
workflowName: workflow.name,
|
|
540
|
+
status,
|
|
541
|
+
steps: results,
|
|
542
|
+
totalDurationMs: Date.now() - start,
|
|
543
|
+
outputs
|
|
544
|
+
};
|
|
545
|
+
this.events.emit({ type: "workflow:complete", result: workflowResult });
|
|
546
|
+
return workflowResult;
|
|
547
|
+
}
|
|
548
|
+
/** Load and run a workflow from a JSON file path */
|
|
549
|
+
async runFromFile(filePath) {
|
|
550
|
+
const fs = await import("fs/promises");
|
|
551
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
552
|
+
const workflow = JSON.parse(content);
|
|
553
|
+
return this.run(workflow);
|
|
554
|
+
}
|
|
555
|
+
async executeStep(step, outputs, workflowEnvs) {
|
|
556
|
+
const start = Date.now();
|
|
557
|
+
if (step.condition) {
|
|
558
|
+
const shouldRun = this.evaluateCondition(step.condition, outputs);
|
|
559
|
+
if (!shouldRun) {
|
|
560
|
+
this.events.emit({
|
|
561
|
+
type: "step:skip",
|
|
562
|
+
stepId: step.id,
|
|
563
|
+
reason: `Condition not met: ${step.condition}`
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
stepId: step.id,
|
|
567
|
+
status: "skipped",
|
|
568
|
+
durationMs: Date.now() - start
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
this.events.emit({ type: "step:start", stepId: step.id });
|
|
573
|
+
this.log.info("Executing step", { stepId: step.id, name: step.name });
|
|
574
|
+
const box = await createFangBox({
|
|
575
|
+
...step.config,
|
|
576
|
+
envs: { ...this.envs, ...workflowEnvs, ...step.config?.envs },
|
|
577
|
+
timeout: step.timeout ?? this.defaultTimeout
|
|
578
|
+
});
|
|
579
|
+
try {
|
|
580
|
+
const prompt = this.interpolate(step.prompt, outputs);
|
|
581
|
+
const manifest = typeof step.hand === "string" ? { name: step.hand } : step.hand;
|
|
582
|
+
const agent = await box.spawnAgent(manifest);
|
|
583
|
+
const response = await box.messageAgent(agent.id, prompt);
|
|
584
|
+
return {
|
|
585
|
+
stepId: step.id,
|
|
586
|
+
status: "success",
|
|
587
|
+
output: response.content,
|
|
588
|
+
durationMs: Date.now() - start
|
|
589
|
+
};
|
|
590
|
+
} catch (error) {
|
|
591
|
+
return {
|
|
592
|
+
stepId: step.id,
|
|
593
|
+
status: "failure",
|
|
594
|
+
error: error instanceof Error ? error.message : String(error),
|
|
595
|
+
durationMs: Date.now() - start
|
|
596
|
+
};
|
|
597
|
+
} finally {
|
|
598
|
+
await box.destroy();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
/** Interpolate {{outputKey}} placeholders in a string */
|
|
602
|
+
interpolate(template, outputs) {
|
|
603
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
604
|
+
return outputs[key] ?? match;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Evaluate a condition against available outputs.
|
|
609
|
+
* Supports safe expressions: "outputKey", "!outputKey",
|
|
610
|
+
* "outputKey == 'value'", "outputKey != 'value'"
|
|
611
|
+
*/
|
|
612
|
+
evaluateCondition(condition, outputs) {
|
|
613
|
+
const trimmed = condition.trim();
|
|
614
|
+
if (trimmed.startsWith("!")) {
|
|
615
|
+
const key = trimmed.slice(1).trim();
|
|
616
|
+
return !outputs[key];
|
|
617
|
+
}
|
|
618
|
+
const eqMatch = trimmed.match(/^(\w+)\s*(==|!=)\s*['"](.*)['"]$/);
|
|
619
|
+
if (eqMatch) {
|
|
620
|
+
const [, key, op, value] = eqMatch;
|
|
621
|
+
const actual = outputs[key];
|
|
622
|
+
return op === "==" ? actual === value : actual !== value;
|
|
623
|
+
}
|
|
624
|
+
return Boolean(outputs[trimmed]);
|
|
625
|
+
}
|
|
626
|
+
/** Topological sort of workflow steps using Kahn's algorithm */
|
|
627
|
+
topologicalSort(steps) {
|
|
628
|
+
const stepMap = new Map(steps.map((s) => [s.id, s]));
|
|
629
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
630
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
631
|
+
for (const step of steps) {
|
|
632
|
+
inDegree.set(step.id, (step.dependsOn ?? []).length);
|
|
633
|
+
for (const dep of step.dependsOn ?? []) {
|
|
634
|
+
const existing = adjacency.get(dep) ?? [];
|
|
635
|
+
existing.push(step.id);
|
|
636
|
+
adjacency.set(dep, existing);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
const queue = [];
|
|
640
|
+
for (const [id, degree] of inDegree) {
|
|
641
|
+
if (degree === 0) queue.push(id);
|
|
642
|
+
}
|
|
643
|
+
const sorted = [];
|
|
644
|
+
while (queue.length > 0) {
|
|
645
|
+
const id = queue.shift();
|
|
646
|
+
sorted.push(stepMap.get(id));
|
|
647
|
+
for (const neighbor of adjacency.get(id) ?? []) {
|
|
648
|
+
const newDegree = (inDegree.get(neighbor) ?? 1) - 1;
|
|
649
|
+
inDegree.set(neighbor, newDegree);
|
|
650
|
+
if (newDegree === 0) queue.push(neighbor);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (sorted.length !== steps.length) {
|
|
654
|
+
throw new Error("Workflow contains circular dependencies");
|
|
655
|
+
}
|
|
656
|
+
return sorted;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
export {
|
|
661
|
+
Logger,
|
|
662
|
+
TemplateBuilder,
|
|
663
|
+
retry,
|
|
664
|
+
OpenFangClient,
|
|
665
|
+
OpenFangError,
|
|
666
|
+
ClaudeBridge,
|
|
667
|
+
TypedEmitter,
|
|
668
|
+
FangBox,
|
|
669
|
+
createFangBox,
|
|
670
|
+
Fleet
|
|
671
|
+
};
|
|
672
|
+
//# sourceMappingURL=chunk-UXIUTT4T.js.map
|