chainlesschain 0.37.10 → 0.37.12
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 +166 -10
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/init.js +184 -0
- package/src/commands/lowcode.js +320 -0
- package/src/commands/plugin.js +55 -2
- package/src/commands/sandbox.js +366 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +44 -0
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/plugin-manager.js +118 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +117 -112
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A (Agent-to-Agent) Protocol for CLI
|
|
3
|
+
*
|
|
4
|
+
* Implements Google A2A protocol concepts: agent cards, task lifecycle,
|
|
5
|
+
* capability negotiation, and peer discovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Task statuses ───────────────────────────────────────────────
|
|
9
|
+
export const TASK_STATUS = {
|
|
10
|
+
SUBMITTED: "submitted",
|
|
11
|
+
WORKING: "working",
|
|
12
|
+
COMPLETED: "completed",
|
|
13
|
+
FAILED: "failed",
|
|
14
|
+
INPUT_REQUIRED: "input-required",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ─── In-memory task subscriptions ────────────────────────────────
|
|
18
|
+
// Map<taskId, Set<callback>>
|
|
19
|
+
const _subscriptions = new Map();
|
|
20
|
+
|
|
21
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
22
|
+
function generateId(prefix = "a2a") {
|
|
23
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nowISO() {
|
|
27
|
+
return new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Table setup ─────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create A2A protocol tables
|
|
34
|
+
*/
|
|
35
|
+
export function ensureA2ATables(db) {
|
|
36
|
+
db.exec(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS a2a_agent_cards (
|
|
38
|
+
id TEXT PRIMARY KEY,
|
|
39
|
+
name TEXT NOT NULL,
|
|
40
|
+
description TEXT DEFAULT '',
|
|
41
|
+
url TEXT DEFAULT '',
|
|
42
|
+
capabilities TEXT DEFAULT '[]',
|
|
43
|
+
skills TEXT DEFAULT '[]',
|
|
44
|
+
auth_type TEXT DEFAULT 'none',
|
|
45
|
+
status TEXT DEFAULT 'active',
|
|
46
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
47
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
48
|
+
)
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
db.exec(`
|
|
52
|
+
CREATE TABLE IF NOT EXISTS a2a_tasks (
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
agent_id TEXT NOT NULL,
|
|
55
|
+
status TEXT DEFAULT 'submitted',
|
|
56
|
+
input TEXT DEFAULT '',
|
|
57
|
+
output TEXT DEFAULT '',
|
|
58
|
+
artifacts TEXT DEFAULT '[]',
|
|
59
|
+
error TEXT DEFAULT '',
|
|
60
|
+
history TEXT DEFAULT '[]',
|
|
61
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
62
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Agent Cards ─────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register an agent card
|
|
71
|
+
*/
|
|
72
|
+
export function registerCard(db, card) {
|
|
73
|
+
ensureA2ATables(db);
|
|
74
|
+
|
|
75
|
+
if (!card || !card.name) {
|
|
76
|
+
throw new Error("Agent card must have a name");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const id = generateId("agent");
|
|
80
|
+
const now = nowISO();
|
|
81
|
+
const capabilities = JSON.stringify(card.capabilities || []);
|
|
82
|
+
const skills = JSON.stringify(card.skills || []);
|
|
83
|
+
|
|
84
|
+
db.prepare(
|
|
85
|
+
`INSERT INTO a2a_agent_cards (id, name, description, url, capabilities, skills, auth_type, created_at, updated_at)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
87
|
+
).run(
|
|
88
|
+
id,
|
|
89
|
+
card.name,
|
|
90
|
+
card.description || "",
|
|
91
|
+
card.url || "",
|
|
92
|
+
capabilities,
|
|
93
|
+
skills,
|
|
94
|
+
card.auth_type || "none",
|
|
95
|
+
now,
|
|
96
|
+
now,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return { id, name: card.name, status: "active" };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Update an existing agent card
|
|
104
|
+
*/
|
|
105
|
+
export function updateCard(db, id, updates) {
|
|
106
|
+
ensureA2ATables(db);
|
|
107
|
+
|
|
108
|
+
if (!id) throw new Error("Card ID is required");
|
|
109
|
+
|
|
110
|
+
const fields = [];
|
|
111
|
+
const values = [];
|
|
112
|
+
|
|
113
|
+
if (updates.name !== undefined) {
|
|
114
|
+
fields.push("name = ?");
|
|
115
|
+
values.push(updates.name);
|
|
116
|
+
}
|
|
117
|
+
if (updates.description !== undefined) {
|
|
118
|
+
fields.push("description = ?");
|
|
119
|
+
values.push(updates.description);
|
|
120
|
+
}
|
|
121
|
+
if (updates.url !== undefined) {
|
|
122
|
+
fields.push("url = ?");
|
|
123
|
+
values.push(updates.url);
|
|
124
|
+
}
|
|
125
|
+
if (updates.capabilities !== undefined) {
|
|
126
|
+
fields.push("capabilities = ?");
|
|
127
|
+
values.push(JSON.stringify(updates.capabilities));
|
|
128
|
+
}
|
|
129
|
+
if (updates.skills !== undefined) {
|
|
130
|
+
fields.push("skills = ?");
|
|
131
|
+
values.push(JSON.stringify(updates.skills));
|
|
132
|
+
}
|
|
133
|
+
if (updates.auth_type !== undefined) {
|
|
134
|
+
fields.push("auth_type = ?");
|
|
135
|
+
values.push(updates.auth_type);
|
|
136
|
+
}
|
|
137
|
+
if (updates.status !== undefined) {
|
|
138
|
+
fields.push("status = ?");
|
|
139
|
+
values.push(updates.status);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (fields.length === 0) return { id, updated: false };
|
|
143
|
+
|
|
144
|
+
fields.push("updated_at = ?");
|
|
145
|
+
values.push(nowISO());
|
|
146
|
+
values.push(id);
|
|
147
|
+
|
|
148
|
+
const result = db
|
|
149
|
+
.prepare(`UPDATE a2a_agent_cards SET ${fields.join(", ")} WHERE id = ?`)
|
|
150
|
+
.run(...values);
|
|
151
|
+
|
|
152
|
+
return { id, updated: result.changes > 0 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Discover agents matching a filter
|
|
157
|
+
*/
|
|
158
|
+
export function discoverAgents(db, filter = {}) {
|
|
159
|
+
ensureA2ATables(db);
|
|
160
|
+
|
|
161
|
+
let rows = db
|
|
162
|
+
.prepare(`SELECT * FROM a2a_agent_cards WHERE status = 'active'`)
|
|
163
|
+
.all();
|
|
164
|
+
|
|
165
|
+
// Parse JSON fields
|
|
166
|
+
rows = rows.map((r) => ({
|
|
167
|
+
...r,
|
|
168
|
+
capabilities: JSON.parse(r.capabilities || "[]"),
|
|
169
|
+
skills: JSON.parse(r.skills || "[]"),
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
if (filter.capability) {
|
|
173
|
+
rows = rows.filter((r) => r.capabilities.includes(filter.capability));
|
|
174
|
+
}
|
|
175
|
+
if (filter.skill) {
|
|
176
|
+
rows = rows.filter((r) => r.skills.includes(filter.skill));
|
|
177
|
+
}
|
|
178
|
+
if (filter.name) {
|
|
179
|
+
const pattern = filter.name.toLowerCase();
|
|
180
|
+
rows = rows.filter((r) => r.name.toLowerCase().includes(pattern));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Task Lifecycle ──────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Send a task to an agent (creates with status: submitted)
|
|
190
|
+
*/
|
|
191
|
+
export function sendTask(db, agentId, input) {
|
|
192
|
+
ensureA2ATables(db);
|
|
193
|
+
|
|
194
|
+
if (!agentId) throw new Error("Agent ID is required");
|
|
195
|
+
if (!input) throw new Error("Task input is required");
|
|
196
|
+
|
|
197
|
+
const taskId = generateId("task");
|
|
198
|
+
const now = nowISO();
|
|
199
|
+
const history = JSON.stringify([
|
|
200
|
+
{ status: TASK_STATUS.SUBMITTED, timestamp: now },
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
db.prepare(
|
|
204
|
+
`INSERT INTO a2a_tasks (id, agent_id, status, input, history, created_at, updated_at)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
206
|
+
).run(taskId, agentId, TASK_STATUS.SUBMITTED, input, history, now, now);
|
|
207
|
+
|
|
208
|
+
_notifySubscribers(taskId, TASK_STATUS.SUBMITTED);
|
|
209
|
+
|
|
210
|
+
return { taskId, status: TASK_STATUS.SUBMITTED };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Mark a task as completed
|
|
215
|
+
*/
|
|
216
|
+
export function completeTask(db, taskId, output, artifacts = []) {
|
|
217
|
+
ensureA2ATables(db);
|
|
218
|
+
|
|
219
|
+
if (!taskId) throw new Error("Task ID is required");
|
|
220
|
+
|
|
221
|
+
const now = nowISO();
|
|
222
|
+
const task = _getTask(db, taskId);
|
|
223
|
+
const history = JSON.parse(task.history || "[]");
|
|
224
|
+
history.push({ status: TASK_STATUS.COMPLETED, timestamp: now });
|
|
225
|
+
|
|
226
|
+
db.prepare(
|
|
227
|
+
`UPDATE a2a_tasks SET status = ?, output = ?, artifacts = ?, history = ?, updated_at = ? WHERE id = ?`,
|
|
228
|
+
).run(
|
|
229
|
+
TASK_STATUS.COMPLETED,
|
|
230
|
+
output || "",
|
|
231
|
+
JSON.stringify(artifacts),
|
|
232
|
+
JSON.stringify(history),
|
|
233
|
+
now,
|
|
234
|
+
taskId,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
_notifySubscribers(taskId, TASK_STATUS.COMPLETED);
|
|
238
|
+
|
|
239
|
+
return { taskId, status: TASK_STATUS.COMPLETED };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Mark a task as failed
|
|
244
|
+
*/
|
|
245
|
+
export function failTask(db, taskId, error) {
|
|
246
|
+
ensureA2ATables(db);
|
|
247
|
+
|
|
248
|
+
if (!taskId) throw new Error("Task ID is required");
|
|
249
|
+
|
|
250
|
+
const now = nowISO();
|
|
251
|
+
const task = _getTask(db, taskId);
|
|
252
|
+
const history = JSON.parse(task.history || "[]");
|
|
253
|
+
history.push({ status: TASK_STATUS.FAILED, timestamp: now });
|
|
254
|
+
|
|
255
|
+
db.prepare(
|
|
256
|
+
`UPDATE a2a_tasks SET status = ?, error = ?, history = ?, updated_at = ? WHERE id = ?`,
|
|
257
|
+
).run(
|
|
258
|
+
TASK_STATUS.FAILED,
|
|
259
|
+
error || "Unknown error",
|
|
260
|
+
JSON.stringify(history),
|
|
261
|
+
now,
|
|
262
|
+
taskId,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
_notifySubscribers(taskId, TASK_STATUS.FAILED);
|
|
266
|
+
|
|
267
|
+
return { taskId, status: TASK_STATUS.FAILED };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get task status with full history
|
|
272
|
+
*/
|
|
273
|
+
export function getTaskStatus(db, taskId) {
|
|
274
|
+
ensureA2ATables(db);
|
|
275
|
+
|
|
276
|
+
const task = _getTask(db, taskId);
|
|
277
|
+
return {
|
|
278
|
+
...task,
|
|
279
|
+
history: JSON.parse(task.history || "[]"),
|
|
280
|
+
artifacts: JSON.parse(task.artifacts || "[]"),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function _getTask(db, taskId) {
|
|
285
|
+
const task = db.prepare(`SELECT * FROM a2a_tasks WHERE id = ?`).get(taskId);
|
|
286
|
+
if (!task) throw new Error(`Task not found: ${taskId}`);
|
|
287
|
+
return task;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Capability Negotiation ──────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check if an agent supports the required capabilities
|
|
294
|
+
*/
|
|
295
|
+
export function negotiateCapability(db, agentId, requiredCapabilities) {
|
|
296
|
+
ensureA2ATables(db);
|
|
297
|
+
|
|
298
|
+
if (!agentId) throw new Error("Agent ID is required");
|
|
299
|
+
if (!Array.isArray(requiredCapabilities)) {
|
|
300
|
+
throw new Error("requiredCapabilities must be an array");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const card = db
|
|
304
|
+
.prepare(`SELECT * FROM a2a_agent_cards WHERE id = ?`)
|
|
305
|
+
.get(agentId);
|
|
306
|
+
if (!card) throw new Error(`Agent not found: ${agentId}`);
|
|
307
|
+
|
|
308
|
+
const agentCaps = JSON.parse(card.capabilities || "[]");
|
|
309
|
+
const supported = requiredCapabilities.filter((c) => agentCaps.includes(c));
|
|
310
|
+
const missing = requiredCapabilities.filter((c) => !agentCaps.includes(c));
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
compatible: missing.length === 0,
|
|
314
|
+
supported,
|
|
315
|
+
missing,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ─── Peers ───────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* List all registered agents
|
|
323
|
+
*/
|
|
324
|
+
export function listPeers(db) {
|
|
325
|
+
ensureA2ATables(db);
|
|
326
|
+
|
|
327
|
+
const rows = db
|
|
328
|
+
.prepare(`SELECT * FROM a2a_agent_cards ORDER BY created_at DESC`)
|
|
329
|
+
.all();
|
|
330
|
+
return rows.map((r) => ({
|
|
331
|
+
...r,
|
|
332
|
+
capabilities: JSON.parse(r.capabilities || "[]"),
|
|
333
|
+
skills: JSON.parse(r.skills || "[]"),
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ─── Subscriptions ───────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Subscribe to task status changes (in-memory)
|
|
341
|
+
*/
|
|
342
|
+
export function subscribeToTask(taskId, callback) {
|
|
343
|
+
if (!_subscriptions.has(taskId)) {
|
|
344
|
+
_subscriptions.set(taskId, new Set());
|
|
345
|
+
}
|
|
346
|
+
_subscriptions.get(taskId).add(callback);
|
|
347
|
+
|
|
348
|
+
return () => {
|
|
349
|
+
const subs = _subscriptions.get(taskId);
|
|
350
|
+
if (subs) {
|
|
351
|
+
subs.delete(callback);
|
|
352
|
+
if (subs.size === 0) _subscriptions.delete(taskId);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function _notifySubscribers(taskId, status) {
|
|
358
|
+
const subs = _subscriptions.get(taskId);
|
|
359
|
+
if (subs) {
|
|
360
|
+
for (const cb of subs) {
|
|
361
|
+
try {
|
|
362
|
+
cb(taskId, status);
|
|
363
|
+
} catch (_err) {
|
|
364
|
+
// Subscriber error should not break task lifecycle
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Export for testing
|
|
371
|
+
export { _subscriptions };
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Agent Coordinator — decomposes tasks, selects agents, and aggregates results.
|
|
3
|
+
*
|
|
4
|
+
* Provides task decomposition based on keyword matching, agent selection by
|
|
5
|
+
* capability, subtask assignment tracking, and result aggregation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from "crypto";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Keyword map for agent type detection.
|
|
12
|
+
*/
|
|
13
|
+
export const AGENT_TYPE_KEYWORDS = {
|
|
14
|
+
"code-generation": [
|
|
15
|
+
"code",
|
|
16
|
+
"generate",
|
|
17
|
+
"implement",
|
|
18
|
+
"function",
|
|
19
|
+
"class",
|
|
20
|
+
"module",
|
|
21
|
+
"develop",
|
|
22
|
+
"build",
|
|
23
|
+
],
|
|
24
|
+
"code-review": [
|
|
25
|
+
"review",
|
|
26
|
+
"audit",
|
|
27
|
+
"inspect",
|
|
28
|
+
"check",
|
|
29
|
+
"lint",
|
|
30
|
+
"quality",
|
|
31
|
+
"pull request",
|
|
32
|
+
],
|
|
33
|
+
"data-analysis": [
|
|
34
|
+
"data",
|
|
35
|
+
"analyze",
|
|
36
|
+
"statistics",
|
|
37
|
+
"chart",
|
|
38
|
+
"graph",
|
|
39
|
+
"csv",
|
|
40
|
+
"report",
|
|
41
|
+
"dashboard",
|
|
42
|
+
"visualization",
|
|
43
|
+
],
|
|
44
|
+
document: [
|
|
45
|
+
"document",
|
|
46
|
+
"documentation",
|
|
47
|
+
"readme",
|
|
48
|
+
"guide",
|
|
49
|
+
"write",
|
|
50
|
+
"article",
|
|
51
|
+
"tutorial",
|
|
52
|
+
"markdown",
|
|
53
|
+
],
|
|
54
|
+
testing: [
|
|
55
|
+
"test",
|
|
56
|
+
"unit test",
|
|
57
|
+
"integration",
|
|
58
|
+
"e2e",
|
|
59
|
+
"jest",
|
|
60
|
+
"vitest",
|
|
61
|
+
"coverage",
|
|
62
|
+
"mock",
|
|
63
|
+
"spec",
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate a short unique id.
|
|
69
|
+
*/
|
|
70
|
+
function generateId() {
|
|
71
|
+
return crypto.randomUUID().slice(0, 12);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Decompose a task description into subtasks based on keyword matching.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} task - Task description
|
|
78
|
+
* @returns {{ taskId: string, subtasks: Array<{ id: string, agentType: string, description: string, status: string }> }}
|
|
79
|
+
*/
|
|
80
|
+
export function decomposeTask(task) {
|
|
81
|
+
if (!task || typeof task !== "string") {
|
|
82
|
+
return { taskId: generateId(), subtasks: [] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lower = task.toLowerCase();
|
|
86
|
+
const subtasks = [];
|
|
87
|
+
|
|
88
|
+
for (const [agentType, keywords] of Object.entries(AGENT_TYPE_KEYWORDS)) {
|
|
89
|
+
const matched = keywords.filter((kw) => lower.includes(kw));
|
|
90
|
+
if (matched.length > 0) {
|
|
91
|
+
subtasks.push({
|
|
92
|
+
id: generateId(),
|
|
93
|
+
agentType,
|
|
94
|
+
description: `${agentType}: ${matched.join(", ")} — from "${task.substring(0, 80)}"`,
|
|
95
|
+
status: "pending",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// If nothing matched, create a generic subtask
|
|
101
|
+
if (subtasks.length === 0) {
|
|
102
|
+
subtasks.push({
|
|
103
|
+
id: generateId(),
|
|
104
|
+
agentType: "general",
|
|
105
|
+
description: task.substring(0, 200),
|
|
106
|
+
status: "pending",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
taskId: generateId(),
|
|
112
|
+
subtasks,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Select the best matching agent for a subtask from available agents.
|
|
118
|
+
*
|
|
119
|
+
* @param {{ agentType: string }} subtask
|
|
120
|
+
* @param {Array<{ id: string, capabilities: string[] }>} availableAgents
|
|
121
|
+
* @returns {{ id: string, capabilities: string[] } | null}
|
|
122
|
+
*/
|
|
123
|
+
export function selectAgent(subtask, availableAgents) {
|
|
124
|
+
if (
|
|
125
|
+
!subtask ||
|
|
126
|
+
!Array.isArray(availableAgents) ||
|
|
127
|
+
availableAgents.length === 0
|
|
128
|
+
) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Direct capability match
|
|
133
|
+
for (const agent of availableAgents) {
|
|
134
|
+
if (
|
|
135
|
+
Array.isArray(agent.capabilities) &&
|
|
136
|
+
agent.capabilities.includes(subtask.agentType)
|
|
137
|
+
) {
|
|
138
|
+
return agent;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Partial match — check if any capability keyword overlaps
|
|
143
|
+
const keywords = AGENT_TYPE_KEYWORDS[subtask.agentType] || [];
|
|
144
|
+
let bestAgent = null;
|
|
145
|
+
let bestScore = 0;
|
|
146
|
+
|
|
147
|
+
for (const agent of availableAgents) {
|
|
148
|
+
if (!Array.isArray(agent.capabilities)) continue;
|
|
149
|
+
let score = 0;
|
|
150
|
+
for (const cap of agent.capabilities) {
|
|
151
|
+
if (keywords.some((kw) => cap.includes(kw) || kw.includes(cap))) {
|
|
152
|
+
score++;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (score > bestScore) {
|
|
156
|
+
bestScore = score;
|
|
157
|
+
bestAgent = agent;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return bestAgent;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Assign a subtask to an agent and record in the database.
|
|
166
|
+
*
|
|
167
|
+
* @param {object} db - Database instance (better-sqlite3 API)
|
|
168
|
+
* @param {string} subtaskId
|
|
169
|
+
* @param {string} agentId
|
|
170
|
+
* @returns {{ subtaskId: string, agentId: string, status: string }}
|
|
171
|
+
*/
|
|
172
|
+
export function assignSubtask(db, subtaskId, agentId) {
|
|
173
|
+
if (db) {
|
|
174
|
+
const stmt = db.prepare(
|
|
175
|
+
`INSERT OR REPLACE INTO agent_assignments (id, subtask_id, agent_id, status, assigned_at)
|
|
176
|
+
VALUES (?, ?, ?, ?, datetime('now'))`,
|
|
177
|
+
);
|
|
178
|
+
stmt.run(generateId(), subtaskId, agentId, "assigned");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
subtaskId,
|
|
183
|
+
agentId,
|
|
184
|
+
status: "assigned",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Aggregate results from completed subtasks.
|
|
190
|
+
*
|
|
191
|
+
* @param {Array<{ id: string, agentType: string, status: string, result?: any }>} subtasks
|
|
192
|
+
* @returns {{ taskId: string, status: string, results: any[], summary: string }}
|
|
193
|
+
*/
|
|
194
|
+
export function aggregateResults(subtasks) {
|
|
195
|
+
if (!Array.isArray(subtasks) || subtasks.length === 0) {
|
|
196
|
+
return {
|
|
197
|
+
taskId: "",
|
|
198
|
+
status: "empty",
|
|
199
|
+
results: [],
|
|
200
|
+
summary: "No subtasks to aggregate",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const results = subtasks.map((s) => ({
|
|
205
|
+
id: s.id,
|
|
206
|
+
agentType: s.agentType,
|
|
207
|
+
status: s.status,
|
|
208
|
+
result: s.result || null,
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
const completed = subtasks.filter((s) => s.status === "completed").length;
|
|
212
|
+
const failed = subtasks.filter((s) => s.status === "failed").length;
|
|
213
|
+
const total = subtasks.length;
|
|
214
|
+
|
|
215
|
+
let status = "completed";
|
|
216
|
+
if (failed > 0 && completed === 0) status = "failed";
|
|
217
|
+
else if (failed > 0) status = "partial";
|
|
218
|
+
else if (completed < total) status = "in-progress";
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
taskId: subtasks[0]?.taskId || generateId(),
|
|
222
|
+
status,
|
|
223
|
+
results,
|
|
224
|
+
summary: `${completed}/${total} subtasks completed, ${failed} failed`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Return the list of all supported agent types.
|
|
230
|
+
*
|
|
231
|
+
* @returns {string[]}
|
|
232
|
+
*/
|
|
233
|
+
export function getAgentTypes() {
|
|
234
|
+
return Object.keys(AGENT_TYPE_KEYWORDS);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Estimate task complexity based on keyword count and description length.
|
|
239
|
+
*
|
|
240
|
+
* @param {string} task
|
|
241
|
+
* @returns {{ complexity: "low" | "medium" | "high", estimatedSubtasks: number }}
|
|
242
|
+
*/
|
|
243
|
+
export function estimateComplexity(task) {
|
|
244
|
+
if (!task || typeof task !== "string") {
|
|
245
|
+
return { complexity: "low", estimatedSubtasks: 0 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lower = task.toLowerCase();
|
|
249
|
+
let matchedTypes = 0;
|
|
250
|
+
let totalKeywords = 0;
|
|
251
|
+
|
|
252
|
+
for (const [, keywords] of Object.entries(AGENT_TYPE_KEYWORDS)) {
|
|
253
|
+
const matched = keywords.filter((kw) => lower.includes(kw));
|
|
254
|
+
if (matched.length > 0) {
|
|
255
|
+
matchedTypes++;
|
|
256
|
+
totalKeywords += matched.length;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const lengthFactor = task.length > 200 ? 1 : task.length > 100 ? 0.5 : 0;
|
|
261
|
+
|
|
262
|
+
const score = matchedTypes + totalKeywords * 0.3 + lengthFactor;
|
|
263
|
+
|
|
264
|
+
let complexity;
|
|
265
|
+
if (score >= 5) complexity = "high";
|
|
266
|
+
else if (score >= 2) complexity = "medium";
|
|
267
|
+
else complexity = "low";
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
complexity,
|
|
271
|
+
estimatedSubtasks: Math.max(1, matchedTypes),
|
|
272
|
+
};
|
|
273
|
+
}
|