agentbnb 4.0.2 → 4.0.4
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 +31 -2
- package/dist/card-RNEWSAQ6.js +88 -0
- package/dist/{chunk-7NA43XCG.js → chunk-5QGXARLJ.js} +4 -2
- package/dist/chunk-EVBX22YU.js +68 -0
- package/dist/chunk-JXEOE7HX.js +295 -0
- package/dist/chunk-UB2NPFC7.js +165 -0
- package/dist/cli/index.js +6 -4
- package/dist/conductor-mode-ESGFZ6T5.js +739 -0
- package/dist/{execute-PNGQOMYO.js → execute-QH6F54D7.js} +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -2
- package/dist/peers-E4MKNNDN.js +12 -0
- package/dist/{serve-skill-TPHZH6BS.js → serve-skill-Q6NHX2RA.js} +1 -1
- package/dist/{server-365V3GYD.js → server-B5E566CI.js} +1 -1
- package/dist/skills/agentbnb/bootstrap.js +2001 -0
- package/openclaw.plugin.json +54 -0
- package/package.json +8 -2
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBalance,
|
|
3
|
+
interpolateObject,
|
|
4
|
+
signEscrowReceipt
|
|
5
|
+
} from "./chunk-JXEOE7HX.js";
|
|
6
|
+
import "./chunk-EVBX22YU.js";
|
|
7
|
+
import {
|
|
8
|
+
AgentBnBError
|
|
9
|
+
} from "./chunk-UB2NPFC7.js";
|
|
10
|
+
|
|
11
|
+
// src/conductor/task-decomposer.ts
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
var TEMPLATES = {
|
|
14
|
+
"video-production": {
|
|
15
|
+
keywords: ["video", "demo", "clip", "animation"],
|
|
16
|
+
steps: [
|
|
17
|
+
{
|
|
18
|
+
description: "Generate script from task description",
|
|
19
|
+
required_capability: "text_gen",
|
|
20
|
+
estimated_credits: 2,
|
|
21
|
+
depends_on_indices: []
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
description: "Generate voiceover from script",
|
|
25
|
+
required_capability: "tts",
|
|
26
|
+
estimated_credits: 3,
|
|
27
|
+
depends_on_indices: [0]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
description: "Generate video visuals from script",
|
|
31
|
+
required_capability: "video_gen",
|
|
32
|
+
estimated_credits: 5,
|
|
33
|
+
depends_on_indices: [0]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
description: "Composite voiceover and video into final output",
|
|
37
|
+
required_capability: "video_edit",
|
|
38
|
+
estimated_credits: 3,
|
|
39
|
+
depends_on_indices: [1, 2]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"deep-analysis": {
|
|
44
|
+
keywords: ["analyze", "analysis", "research", "report", "evaluate"],
|
|
45
|
+
steps: [
|
|
46
|
+
{
|
|
47
|
+
description: "Research and gather relevant data",
|
|
48
|
+
required_capability: "web_search",
|
|
49
|
+
estimated_credits: 2,
|
|
50
|
+
depends_on_indices: []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
description: "Analyze gathered data",
|
|
54
|
+
required_capability: "text_gen",
|
|
55
|
+
estimated_credits: 3,
|
|
56
|
+
depends_on_indices: [0]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
description: "Summarize analysis findings",
|
|
60
|
+
required_capability: "text_gen",
|
|
61
|
+
estimated_credits: 2,
|
|
62
|
+
depends_on_indices: [1]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
description: "Format into final report",
|
|
66
|
+
required_capability: "text_gen",
|
|
67
|
+
estimated_credits: 1,
|
|
68
|
+
depends_on_indices: [2]
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
"content-generation": {
|
|
73
|
+
keywords: ["write", "blog", "article", "content", "post", "essay"],
|
|
74
|
+
steps: [
|
|
75
|
+
{
|
|
76
|
+
description: "Create content outline",
|
|
77
|
+
required_capability: "text_gen",
|
|
78
|
+
estimated_credits: 1,
|
|
79
|
+
depends_on_indices: []
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
description: "Draft content from outline",
|
|
83
|
+
required_capability: "text_gen",
|
|
84
|
+
estimated_credits: 3,
|
|
85
|
+
depends_on_indices: [0]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
description: "Review and refine draft",
|
|
89
|
+
required_capability: "text_gen",
|
|
90
|
+
estimated_credits: 2,
|
|
91
|
+
depends_on_indices: [1]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
description: "Finalize and polish content",
|
|
95
|
+
required_capability: "text_gen",
|
|
96
|
+
estimated_credits: 1,
|
|
97
|
+
depends_on_indices: [2]
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function decompose(task, _availableCapabilities) {
|
|
103
|
+
const lower = task.toLowerCase();
|
|
104
|
+
for (const template of Object.values(TEMPLATES)) {
|
|
105
|
+
const matched = template.keywords.some((kw) => lower.includes(kw));
|
|
106
|
+
if (!matched) continue;
|
|
107
|
+
const ids = template.steps.map(() => randomUUID());
|
|
108
|
+
return template.steps.map((step, i) => ({
|
|
109
|
+
id: ids[i],
|
|
110
|
+
description: step.description,
|
|
111
|
+
required_capability: step.required_capability,
|
|
112
|
+
params: {},
|
|
113
|
+
depends_on: step.depends_on_indices.map((idx) => ids[idx]),
|
|
114
|
+
estimated_credits: step.estimated_credits
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/registry/matcher.ts
|
|
121
|
+
function searchCards(db, query, filters = {}) {
|
|
122
|
+
const words = query.trim().split(/\s+/).map((w) => w.replace(/["*^{}():]/g, "")).filter((w) => w.length > 0);
|
|
123
|
+
if (words.length === 0) return [];
|
|
124
|
+
const ftsQuery = words.map((w) => `"${w}"`).join(" OR ");
|
|
125
|
+
const conditions = [];
|
|
126
|
+
const params = [ftsQuery];
|
|
127
|
+
if (filters.level !== void 0) {
|
|
128
|
+
conditions.push(`json_extract(cc.data, '$.level') = ?`);
|
|
129
|
+
params.push(filters.level);
|
|
130
|
+
}
|
|
131
|
+
if (filters.online !== void 0) {
|
|
132
|
+
conditions.push(`json_extract(cc.data, '$.availability.online') = ?`);
|
|
133
|
+
params.push(filters.online ? 1 : 0);
|
|
134
|
+
}
|
|
135
|
+
const whereClause = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
|
|
136
|
+
const sql = `
|
|
137
|
+
SELECT cc.data
|
|
138
|
+
FROM capability_cards cc
|
|
139
|
+
JOIN cards_fts ON cc.rowid = cards_fts.rowid
|
|
140
|
+
WHERE cards_fts MATCH ?
|
|
141
|
+
${whereClause}
|
|
142
|
+
ORDER BY bm25(cards_fts)
|
|
143
|
+
LIMIT 50
|
|
144
|
+
`;
|
|
145
|
+
const stmt = db.prepare(sql);
|
|
146
|
+
const rows = stmt.all(...params);
|
|
147
|
+
const results = rows.map((row) => JSON.parse(row.data));
|
|
148
|
+
if (filters.apis_used && filters.apis_used.length > 0) {
|
|
149
|
+
const requiredApis = filters.apis_used;
|
|
150
|
+
return results.filter((card) => {
|
|
151
|
+
const cardApis = card.metadata?.apis_used ?? [];
|
|
152
|
+
return requiredApis.every((api) => cardApis.includes(api));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/gateway/client.ts
|
|
159
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
160
|
+
async function requestCapability(opts) {
|
|
161
|
+
const { gatewayUrl, token, cardId, params = {}, timeoutMs = 3e5, escrowReceipt, identity } = opts;
|
|
162
|
+
const id = randomUUID2();
|
|
163
|
+
const payload = {
|
|
164
|
+
jsonrpc: "2.0",
|
|
165
|
+
id,
|
|
166
|
+
method: "capability.execute",
|
|
167
|
+
params: {
|
|
168
|
+
card_id: cardId,
|
|
169
|
+
...params,
|
|
170
|
+
...escrowReceipt ? { escrow_receipt: escrowReceipt } : {}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const headers = { "Content-Type": "application/json" };
|
|
174
|
+
if (identity) {
|
|
175
|
+
const signature = signEscrowReceipt(payload, identity.privateKey);
|
|
176
|
+
headers["X-Agent-Id"] = identity.agentId;
|
|
177
|
+
headers["X-Agent-Public-Key"] = identity.publicKey;
|
|
178
|
+
headers["X-Agent-Signature"] = signature;
|
|
179
|
+
} else if (token) {
|
|
180
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
181
|
+
}
|
|
182
|
+
const controller = new AbortController();
|
|
183
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
184
|
+
let response;
|
|
185
|
+
try {
|
|
186
|
+
response = await fetch(`${gatewayUrl}/rpc`, {
|
|
187
|
+
method: "POST",
|
|
188
|
+
headers,
|
|
189
|
+
body: JSON.stringify(payload),
|
|
190
|
+
signal: controller.signal
|
|
191
|
+
});
|
|
192
|
+
} catch (err) {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
195
|
+
throw new AgentBnBError(
|
|
196
|
+
isTimeout ? "Request timed out" : `Network error: ${String(err)}`,
|
|
197
|
+
isTimeout ? "TIMEOUT" : "NETWORK_ERROR"
|
|
198
|
+
);
|
|
199
|
+
} finally {
|
|
200
|
+
clearTimeout(timer);
|
|
201
|
+
}
|
|
202
|
+
const body = await response.json();
|
|
203
|
+
if (body.error) {
|
|
204
|
+
throw new AgentBnBError(body.error.message, `RPC_ERROR_${body.error.code}`);
|
|
205
|
+
}
|
|
206
|
+
return body.result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/autonomy/pending-requests.ts
|
|
210
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
211
|
+
|
|
212
|
+
// src/cli/remote-registry.ts
|
|
213
|
+
var RegistryTimeoutError = class extends AgentBnBError {
|
|
214
|
+
constructor(url) {
|
|
215
|
+
super(
|
|
216
|
+
`Registry at ${url} did not respond within 5s. Showing local results only.`,
|
|
217
|
+
"REGISTRY_TIMEOUT"
|
|
218
|
+
);
|
|
219
|
+
this.name = "RegistryTimeoutError";
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var RegistryConnectionError = class extends AgentBnBError {
|
|
223
|
+
constructor(url) {
|
|
224
|
+
super(
|
|
225
|
+
`Cannot reach ${url}. Is the registry running? Showing local results only.`,
|
|
226
|
+
"REGISTRY_CONNECTION"
|
|
227
|
+
);
|
|
228
|
+
this.name = "RegistryConnectionError";
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
var RegistryAuthError = class extends AgentBnBError {
|
|
232
|
+
constructor(url) {
|
|
233
|
+
super(
|
|
234
|
+
`Authentication failed for ${url}. Run \`agentbnb config set token <your-token>\`.`,
|
|
235
|
+
"REGISTRY_AUTH"
|
|
236
|
+
);
|
|
237
|
+
this.name = "RegistryAuthError";
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
async function fetchRemoteCards(registryUrl, params, timeoutMs = 5e3) {
|
|
241
|
+
let cardsUrl;
|
|
242
|
+
try {
|
|
243
|
+
cardsUrl = new URL("/cards", registryUrl);
|
|
244
|
+
} catch {
|
|
245
|
+
throw new AgentBnBError(`Invalid registry URL: ${registryUrl}`, "INVALID_REGISTRY_URL");
|
|
246
|
+
}
|
|
247
|
+
const searchParams = new URLSearchParams();
|
|
248
|
+
if (params.q !== void 0) searchParams.set("q", params.q);
|
|
249
|
+
if (params.level !== void 0) searchParams.set("level", String(params.level));
|
|
250
|
+
if (params.online !== void 0) searchParams.set("online", String(params.online));
|
|
251
|
+
if (params.tag !== void 0) searchParams.set("tag", params.tag);
|
|
252
|
+
searchParams.set("limit", "100");
|
|
253
|
+
cardsUrl.search = searchParams.toString();
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
256
|
+
let response;
|
|
257
|
+
try {
|
|
258
|
+
response = await fetch(cardsUrl.toString(), { signal: controller.signal });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
const isTimeout = err instanceof Error && err.name === "AbortError";
|
|
262
|
+
if (isTimeout) {
|
|
263
|
+
throw new RegistryTimeoutError(registryUrl);
|
|
264
|
+
}
|
|
265
|
+
throw new RegistryConnectionError(registryUrl);
|
|
266
|
+
} finally {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
}
|
|
269
|
+
if (response.status === 401 || response.status === 403) {
|
|
270
|
+
throw new RegistryAuthError(registryUrl);
|
|
271
|
+
}
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw new RegistryConnectionError(registryUrl);
|
|
274
|
+
}
|
|
275
|
+
const body = await response.json();
|
|
276
|
+
return body.items;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/autonomy/auto-request.ts
|
|
280
|
+
function minMaxNormalize(values) {
|
|
281
|
+
if (values.length === 0) return [];
|
|
282
|
+
if (values.length === 1) return [1];
|
|
283
|
+
const min = Math.min(...values);
|
|
284
|
+
const max = Math.max(...values);
|
|
285
|
+
if (max === min) {
|
|
286
|
+
return values.map(() => 1);
|
|
287
|
+
}
|
|
288
|
+
return values.map((v) => (v - min) / (max - min));
|
|
289
|
+
}
|
|
290
|
+
function scorePeers(candidates, selfOwner) {
|
|
291
|
+
const eligible = candidates.filter((c) => c.card.owner !== selfOwner);
|
|
292
|
+
if (eligible.length === 0) return [];
|
|
293
|
+
const successRates = eligible.map((c) => c.card.metadata?.success_rate ?? 0.5);
|
|
294
|
+
const costEfficiencies = eligible.map((c) => c.cost === 0 ? 1 : 1 / c.cost);
|
|
295
|
+
const idleRates = eligible.map((c) => {
|
|
296
|
+
const internal = c.card._internal;
|
|
297
|
+
const idleRate = internal?.idle_rate;
|
|
298
|
+
return typeof idleRate === "number" ? idleRate : 1;
|
|
299
|
+
});
|
|
300
|
+
const normSuccess = minMaxNormalize(successRates);
|
|
301
|
+
const normCost = minMaxNormalize(costEfficiencies);
|
|
302
|
+
const normIdle = minMaxNormalize(idleRates);
|
|
303
|
+
const scored = eligible.map((c, i) => ({
|
|
304
|
+
...c,
|
|
305
|
+
rawScore: (normSuccess[i] ?? 0) * (normCost[i] ?? 0) * (normIdle[i] ?? 0)
|
|
306
|
+
}));
|
|
307
|
+
scored.sort((a, b) => b.rawScore - a.rawScore);
|
|
308
|
+
return scored;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/conductor/capability-matcher.ts
|
|
312
|
+
var MAX_ALTERNATIVES = 2;
|
|
313
|
+
async function matchSubTasks(opts) {
|
|
314
|
+
const { db, subtasks, conductorOwner, registryUrl } = opts;
|
|
315
|
+
return Promise.all(subtasks.map(async (subtask) => {
|
|
316
|
+
let cards = searchCards(db, subtask.required_capability, { online: true });
|
|
317
|
+
if (cards.length === 0 && registryUrl) {
|
|
318
|
+
try {
|
|
319
|
+
cards = await fetchRemoteCards(registryUrl, { q: subtask.required_capability, online: true });
|
|
320
|
+
} catch {
|
|
321
|
+
cards = [];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const candidates = [];
|
|
325
|
+
for (const card of cards) {
|
|
326
|
+
const cardAsV2 = card;
|
|
327
|
+
if (Array.isArray(cardAsV2.skills)) {
|
|
328
|
+
for (const skill of cardAsV2.skills) {
|
|
329
|
+
candidates.push({
|
|
330
|
+
card,
|
|
331
|
+
cost: skill.pricing.credits_per_call,
|
|
332
|
+
skillId: skill.id
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
candidates.push({
|
|
337
|
+
card,
|
|
338
|
+
cost: card.pricing.credits_per_call,
|
|
339
|
+
skillId: void 0
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const scored = scorePeers(candidates, conductorOwner);
|
|
344
|
+
if (scored.length === 0) {
|
|
345
|
+
return {
|
|
346
|
+
subtask_id: subtask.id,
|
|
347
|
+
selected_agent: "",
|
|
348
|
+
selected_skill: "",
|
|
349
|
+
score: 0,
|
|
350
|
+
credits: 0,
|
|
351
|
+
alternatives: []
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const top = scored[0];
|
|
355
|
+
const alternatives = scored.slice(1, 1 + MAX_ALTERNATIVES).map((s) => ({
|
|
356
|
+
agent: s.card.owner,
|
|
357
|
+
skill: s.skillId ?? "",
|
|
358
|
+
score: s.rawScore,
|
|
359
|
+
credits: s.cost
|
|
360
|
+
}));
|
|
361
|
+
return {
|
|
362
|
+
subtask_id: subtask.id,
|
|
363
|
+
selected_agent: top.card.owner,
|
|
364
|
+
selected_skill: top.skillId ?? "",
|
|
365
|
+
selected_card_id: top.card.id,
|
|
366
|
+
score: top.rawScore,
|
|
367
|
+
credits: top.cost,
|
|
368
|
+
alternatives
|
|
369
|
+
};
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/conductor/budget-controller.ts
|
|
374
|
+
var ORCHESTRATION_FEE = 5;
|
|
375
|
+
var BudgetController = class {
|
|
376
|
+
/**
|
|
377
|
+
* Creates a new BudgetController.
|
|
378
|
+
*
|
|
379
|
+
* @param budgetManager - Underlying BudgetManager for reserve floor enforcement.
|
|
380
|
+
* @param maxBudget - Hard ceiling for the orchestration run.
|
|
381
|
+
*/
|
|
382
|
+
constructor(budgetManager, maxBudget) {
|
|
383
|
+
this.budgetManager = budgetManager;
|
|
384
|
+
this.maxBudget = maxBudget;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Pre-calculates the total budget for an orchestration run.
|
|
388
|
+
*
|
|
389
|
+
* Sums all matched sub-task credits, adds the orchestration fee,
|
|
390
|
+
* and determines whether approval is required (estimated > max).
|
|
391
|
+
*
|
|
392
|
+
* @param matches - MatchResult[] from the CapabilityMatcher.
|
|
393
|
+
* @returns An ExecutionBudget with cost breakdown and approval status.
|
|
394
|
+
*/
|
|
395
|
+
calculateBudget(matches) {
|
|
396
|
+
const perTaskSpending = /* @__PURE__ */ new Map();
|
|
397
|
+
let subTotal = 0;
|
|
398
|
+
for (const match of matches) {
|
|
399
|
+
perTaskSpending.set(match.subtask_id, match.credits);
|
|
400
|
+
subTotal += match.credits;
|
|
401
|
+
}
|
|
402
|
+
const estimatedTotal = subTotal + ORCHESTRATION_FEE;
|
|
403
|
+
return {
|
|
404
|
+
estimated_total: estimatedTotal,
|
|
405
|
+
max_budget: this.maxBudget,
|
|
406
|
+
orchestration_fee: ORCHESTRATION_FEE,
|
|
407
|
+
per_task_spending: perTaskSpending,
|
|
408
|
+
requires_approval: estimatedTotal > this.maxBudget
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Checks whether orchestration can proceed without explicit approval.
|
|
413
|
+
*
|
|
414
|
+
* Returns true only when:
|
|
415
|
+
* 1. The budget does NOT require approval (estimated_total <= max_budget)
|
|
416
|
+
* 2. The BudgetManager confirms sufficient credits (respecting reserve floor)
|
|
417
|
+
*
|
|
418
|
+
* @param budget - ExecutionBudget from calculateBudget().
|
|
419
|
+
* @returns true if execution can proceed autonomously.
|
|
420
|
+
*/
|
|
421
|
+
canExecute(budget) {
|
|
422
|
+
if (budget.requires_approval) return false;
|
|
423
|
+
return this.budgetManager.canSpend(budget.estimated_total);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Checks budget after explicit user/agent approval.
|
|
427
|
+
*
|
|
428
|
+
* Ignores the requires_approval flag — used when the caller has already
|
|
429
|
+
* obtained explicit approval for the over-budget orchestration.
|
|
430
|
+
* Still enforces the reserve floor via BudgetManager.canSpend().
|
|
431
|
+
*
|
|
432
|
+
* @param budget - ExecutionBudget from calculateBudget().
|
|
433
|
+
* @returns true if the agent has sufficient credits (reserve floor check only).
|
|
434
|
+
*/
|
|
435
|
+
approveAndCheck(budget) {
|
|
436
|
+
return this.budgetManager.canSpend(budget.estimated_total);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/credit/budget.ts
|
|
441
|
+
var DEFAULT_BUDGET_CONFIG = {
|
|
442
|
+
reserve_credits: 20
|
|
443
|
+
};
|
|
444
|
+
var BudgetManager = class {
|
|
445
|
+
/**
|
|
446
|
+
* Creates a new BudgetManager.
|
|
447
|
+
*
|
|
448
|
+
* @param creditDb - The credit SQLite database instance.
|
|
449
|
+
* @param owner - Agent owner identifier.
|
|
450
|
+
* @param config - Budget configuration. Defaults to DEFAULT_BUDGET_CONFIG (20 credit reserve).
|
|
451
|
+
*/
|
|
452
|
+
constructor(creditDb, owner, config = DEFAULT_BUDGET_CONFIG) {
|
|
453
|
+
this.creditDb = creditDb;
|
|
454
|
+
this.owner = owner;
|
|
455
|
+
this.config = config;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Returns the number of credits available for spending.
|
|
459
|
+
* Computed as: max(0, balance - reserve_credits).
|
|
460
|
+
* Always returns a non-negative number — never goes below zero.
|
|
461
|
+
*
|
|
462
|
+
* @returns Available credits (balance minus reserve, floored at 0).
|
|
463
|
+
*/
|
|
464
|
+
availableCredits() {
|
|
465
|
+
const balance = getBalance(this.creditDb, this.owner);
|
|
466
|
+
return Math.max(0, balance - this.config.reserve_credits);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Returns true if spending `amount` credits is permitted by budget rules.
|
|
470
|
+
*
|
|
471
|
+
* Rules:
|
|
472
|
+
* - Zero-cost calls (amount <= 0) always return true.
|
|
473
|
+
* - Any positive amount requires availableCredits() >= amount.
|
|
474
|
+
* - If balance is at or below the reserve floor, all positive-cost calls return false.
|
|
475
|
+
*
|
|
476
|
+
* @param amount - Number of credits to spend.
|
|
477
|
+
* @returns true if the spend is allowed, false if it would breach the reserve floor.
|
|
478
|
+
*/
|
|
479
|
+
canSpend(amount) {
|
|
480
|
+
if (amount <= 0) return true;
|
|
481
|
+
return this.availableCredits() >= amount;
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/conductor/pipeline-orchestrator.ts
|
|
486
|
+
function computeWaves(subtasks) {
|
|
487
|
+
const waves = [];
|
|
488
|
+
const completed = /* @__PURE__ */ new Set();
|
|
489
|
+
const remaining = new Map(subtasks.map((s) => [s.id, s]));
|
|
490
|
+
while (remaining.size > 0) {
|
|
491
|
+
const wave = [];
|
|
492
|
+
for (const [id, task] of remaining) {
|
|
493
|
+
const depsResolved = task.depends_on.every((dep) => completed.has(dep));
|
|
494
|
+
if (depsResolved) {
|
|
495
|
+
wave.push(id);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (wave.length === 0) {
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
for (const id of wave) {
|
|
502
|
+
remaining.delete(id);
|
|
503
|
+
completed.add(id);
|
|
504
|
+
}
|
|
505
|
+
waves.push(wave);
|
|
506
|
+
}
|
|
507
|
+
return waves;
|
|
508
|
+
}
|
|
509
|
+
async function orchestrate(opts) {
|
|
510
|
+
const { subtasks, matches, gatewayToken, resolveAgentUrl, timeoutMs = 3e5, maxBudget, relayClient, requesterOwner } = opts;
|
|
511
|
+
const startTime = Date.now();
|
|
512
|
+
if (subtasks.length === 0) {
|
|
513
|
+
return {
|
|
514
|
+
success: true,
|
|
515
|
+
results: /* @__PURE__ */ new Map(),
|
|
516
|
+
total_credits: 0,
|
|
517
|
+
latency_ms: Date.now() - startTime
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const results = /* @__PURE__ */ new Map();
|
|
521
|
+
const errors = [];
|
|
522
|
+
let totalCredits = 0;
|
|
523
|
+
const waves = computeWaves(subtasks);
|
|
524
|
+
const subtaskMap = new Map(subtasks.map((s) => [s.id, s]));
|
|
525
|
+
for (const wave of waves) {
|
|
526
|
+
if (maxBudget !== void 0 && totalCredits >= maxBudget) {
|
|
527
|
+
errors.push(`Budget exceeded: spent ${totalCredits} cr, max ${maxBudget} cr`);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
const executableIds = [];
|
|
531
|
+
for (const taskId of wave) {
|
|
532
|
+
const m = matches.get(taskId);
|
|
533
|
+
if (maxBudget !== void 0 && m && totalCredits + m.credits > maxBudget) {
|
|
534
|
+
errors.push(`Skipping task ${taskId}: would exceed budget (${totalCredits} + ${m.credits} > ${maxBudget})`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
executableIds.push(taskId);
|
|
538
|
+
}
|
|
539
|
+
const waveResults = await Promise.allSettled(
|
|
540
|
+
executableIds.map(async (taskId) => {
|
|
541
|
+
const subtask = subtaskMap.get(taskId);
|
|
542
|
+
const m = matches.get(taskId);
|
|
543
|
+
if (!m) {
|
|
544
|
+
throw new Error(`No match found for subtask ${taskId}`);
|
|
545
|
+
}
|
|
546
|
+
const stepsContext = {};
|
|
547
|
+
for (const [id, val] of results) {
|
|
548
|
+
stepsContext[id] = val;
|
|
549
|
+
}
|
|
550
|
+
const interpContext = { steps: stepsContext, prev: void 0 };
|
|
551
|
+
if (subtask.depends_on.length > 0) {
|
|
552
|
+
const lastDep = subtask.depends_on[subtask.depends_on.length - 1];
|
|
553
|
+
interpContext.prev = results.get(lastDep);
|
|
554
|
+
}
|
|
555
|
+
const interpolatedParams = interpolateObject(
|
|
556
|
+
subtask.params,
|
|
557
|
+
interpContext
|
|
558
|
+
);
|
|
559
|
+
const primary = resolveAgentUrl(m.selected_agent);
|
|
560
|
+
try {
|
|
561
|
+
let res;
|
|
562
|
+
if (primary.url.startsWith("relay://") && relayClient) {
|
|
563
|
+
const targetOwner = primary.url.replace("relay://", "");
|
|
564
|
+
res = await relayClient.request({
|
|
565
|
+
targetOwner,
|
|
566
|
+
cardId: primary.cardId,
|
|
567
|
+
params: interpolatedParams,
|
|
568
|
+
requester: requesterOwner,
|
|
569
|
+
timeoutMs
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
res = await requestCapability({
|
|
573
|
+
gatewayUrl: primary.url,
|
|
574
|
+
token: gatewayToken,
|
|
575
|
+
cardId: primary.cardId,
|
|
576
|
+
params: interpolatedParams,
|
|
577
|
+
timeoutMs
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return { taskId, result: res, credits: m.credits };
|
|
581
|
+
} catch (primaryErr) {
|
|
582
|
+
if (m.alternatives.length > 0) {
|
|
583
|
+
const alt = m.alternatives[0];
|
|
584
|
+
const altAgent = resolveAgentUrl(alt.agent);
|
|
585
|
+
try {
|
|
586
|
+
let altRes;
|
|
587
|
+
if (altAgent.url.startsWith("relay://") && relayClient) {
|
|
588
|
+
const targetOwner = altAgent.url.replace("relay://", "");
|
|
589
|
+
altRes = await relayClient.request({
|
|
590
|
+
targetOwner,
|
|
591
|
+
cardId: altAgent.cardId,
|
|
592
|
+
params: interpolatedParams,
|
|
593
|
+
requester: requesterOwner,
|
|
594
|
+
timeoutMs
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
altRes = await requestCapability({
|
|
598
|
+
gatewayUrl: altAgent.url,
|
|
599
|
+
token: gatewayToken,
|
|
600
|
+
cardId: altAgent.cardId,
|
|
601
|
+
params: interpolatedParams,
|
|
602
|
+
timeoutMs
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
return { taskId, result: altRes, credits: alt.credits };
|
|
606
|
+
} catch (altErr) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
`Task ${taskId}: primary (${m.selected_agent}) failed: ${primaryErr instanceof Error ? primaryErr.message : String(primaryErr)}; alternative (${alt.agent}) failed: ${altErr instanceof Error ? altErr.message : String(altErr)}`
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Task ${taskId}: ${primaryErr instanceof Error ? primaryErr.message : String(primaryErr)}`
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
for (const settlement of waveResults) {
|
|
619
|
+
if (settlement.status === "fulfilled") {
|
|
620
|
+
const { taskId, result, credits } = settlement.value;
|
|
621
|
+
results.set(taskId, result);
|
|
622
|
+
totalCredits += credits;
|
|
623
|
+
} else {
|
|
624
|
+
errors.push(settlement.reason instanceof Error ? settlement.reason.message : String(settlement.reason));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
success: errors.length === 0,
|
|
630
|
+
results,
|
|
631
|
+
total_credits: totalCredits,
|
|
632
|
+
latency_ms: Date.now() - startTime,
|
|
633
|
+
errors: errors.length > 0 ? errors : void 0
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/conductor/conductor-mode.ts
|
|
638
|
+
var ConductorMode = class {
|
|
639
|
+
db;
|
|
640
|
+
creditDb;
|
|
641
|
+
conductorOwner;
|
|
642
|
+
gatewayToken;
|
|
643
|
+
resolveAgentUrl;
|
|
644
|
+
maxBudget;
|
|
645
|
+
constructor(opts) {
|
|
646
|
+
this.db = opts.db;
|
|
647
|
+
this.creditDb = opts.creditDb;
|
|
648
|
+
this.conductorOwner = opts.conductorOwner;
|
|
649
|
+
this.gatewayToken = opts.gatewayToken;
|
|
650
|
+
this.resolveAgentUrl = opts.resolveAgentUrl;
|
|
651
|
+
this.maxBudget = opts.maxBudget ?? 100;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Execute a conductor skill with the given config and params.
|
|
655
|
+
*
|
|
656
|
+
* @param config - SkillConfig with type 'conductor' and conductor_skill field.
|
|
657
|
+
* @param params - Must include `task` string.
|
|
658
|
+
* @returns Execution result without latency_ms (added by SkillExecutor).
|
|
659
|
+
*/
|
|
660
|
+
async execute(config, params, onProgress) {
|
|
661
|
+
const conductorSkill = config.conductor_skill;
|
|
662
|
+
if (conductorSkill !== "orchestrate" && conductorSkill !== "plan") {
|
|
663
|
+
return {
|
|
664
|
+
success: false,
|
|
665
|
+
error: `Unknown conductor skill: "${conductorSkill}"`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
const task = params.task;
|
|
669
|
+
if (typeof task !== "string" || task.length === 0) {
|
|
670
|
+
return {
|
|
671
|
+
success: false,
|
|
672
|
+
error: 'Missing or empty "task" parameter'
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const subtasks = decompose(task);
|
|
676
|
+
if (subtasks.length === 0) {
|
|
677
|
+
return {
|
|
678
|
+
success: false,
|
|
679
|
+
error: "No template matches task"
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
onProgress?.({ step: 1, total: 5, message: `Decomposed into ${subtasks.length} sub-tasks` });
|
|
683
|
+
const matchResults = await matchSubTasks({
|
|
684
|
+
db: this.db,
|
|
685
|
+
subtasks,
|
|
686
|
+
conductorOwner: this.conductorOwner
|
|
687
|
+
});
|
|
688
|
+
onProgress?.({ step: 2, total: 5, message: `Matched ${matchResults.length} sub-tasks to agents` });
|
|
689
|
+
const budgetManager = new BudgetManager(this.creditDb, this.conductorOwner);
|
|
690
|
+
const budgetController = new BudgetController(budgetManager, this.maxBudget);
|
|
691
|
+
const executionBudget = budgetController.calculateBudget(matchResults);
|
|
692
|
+
if (!budgetController.canExecute(executionBudget)) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
error: `Budget exceeded: estimated ${executionBudget.estimated_total} cr, max ${this.maxBudget} cr`
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
onProgress?.({ step: 3, total: 5, message: `Budget approved: ${executionBudget.estimated_total} cr` });
|
|
699
|
+
if (conductorSkill === "plan") {
|
|
700
|
+
return {
|
|
701
|
+
success: true,
|
|
702
|
+
result: {
|
|
703
|
+
subtasks,
|
|
704
|
+
matches: matchResults,
|
|
705
|
+
budget: executionBudget
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const matchMap = new Map(
|
|
710
|
+
matchResults.map((m) => [m.subtask_id, m])
|
|
711
|
+
);
|
|
712
|
+
const orchResult = await orchestrate({
|
|
713
|
+
subtasks,
|
|
714
|
+
matches: matchMap,
|
|
715
|
+
gatewayToken: this.gatewayToken,
|
|
716
|
+
resolveAgentUrl: this.resolveAgentUrl,
|
|
717
|
+
maxBudget: this.maxBudget
|
|
718
|
+
});
|
|
719
|
+
onProgress?.({ step: 4, total: 5, message: "Pipeline execution complete" });
|
|
720
|
+
const resultObj = {};
|
|
721
|
+
for (const [key, value] of orchResult.results) {
|
|
722
|
+
resultObj[key] = value;
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
success: orchResult.success,
|
|
726
|
+
result: {
|
|
727
|
+
plan: subtasks,
|
|
728
|
+
execution: resultObj,
|
|
729
|
+
total_credits: orchResult.total_credits,
|
|
730
|
+
latency_ms: orchResult.latency_ms,
|
|
731
|
+
errors: orchResult.errors
|
|
732
|
+
},
|
|
733
|
+
error: orchResult.success ? void 0 : orchResult.errors?.join("; ")
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
export {
|
|
738
|
+
ConductorMode
|
|
739
|
+
};
|