wispy-cli 0.8.0 → 0.9.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/core/engine.mjs +95 -0
- package/core/index.mjs +1 -0
- package/core/subagents.mjs +343 -0
- package/core/tools.mjs +60 -0
- package/lib/wispy-repl.mjs +72 -0
- package/package.json +1 -1
package/core/engine.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { ToolRegistry } from "./tools.mjs";
|
|
|
20
20
|
import { SessionManager } from "./session.mjs";
|
|
21
21
|
import { MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
22
22
|
import { MemoryManager } from "./memory.mjs";
|
|
23
|
+
import { SubAgentManager } from "./subagents.mjs";
|
|
23
24
|
|
|
24
25
|
const MAX_TOOL_ROUNDS = 10;
|
|
25
26
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
@@ -32,6 +33,7 @@ export class WispyEngine {
|
|
|
32
33
|
this.sessions = new SessionManager();
|
|
33
34
|
this.mcpManager = new MCPManager(config.mcpConfigPath ?? MCP_CONFIG_PATH);
|
|
34
35
|
this.memory = new MemoryManager(WISPY_DIR);
|
|
36
|
+
this.subagents = new SubAgentManager(this, this.sessions);
|
|
35
37
|
this._initialized = false;
|
|
36
38
|
this._activeWorkstream = config.workstream
|
|
37
39
|
?? process.env.WISPY_WORKSTREAM
|
|
@@ -226,6 +228,17 @@ export class WispyEngine {
|
|
|
226
228
|
return this._toolMemoryAppend(args);
|
|
227
229
|
case "memory_delete":
|
|
228
230
|
return this._toolMemoryDelete(args);
|
|
231
|
+
// Sub-agent tools (v0.9)
|
|
232
|
+
case "spawn_subagent":
|
|
233
|
+
return this._toolSpawnSubagent(args, opts);
|
|
234
|
+
case "list_subagents":
|
|
235
|
+
return this._toolListSubagents();
|
|
236
|
+
case "get_subagent_result":
|
|
237
|
+
return this._toolGetSubagentResult(args);
|
|
238
|
+
case "kill_subagent":
|
|
239
|
+
return this._toolKillSubagent(args);
|
|
240
|
+
case "steer_subagent":
|
|
241
|
+
return this._toolSteerSubagent(args);
|
|
229
242
|
default:
|
|
230
243
|
return this.tools.execute(name, args);
|
|
231
244
|
}
|
|
@@ -438,6 +451,88 @@ export class WispyEngine {
|
|
|
438
451
|
return { success: true, iterations: MAX_ITERATIONS, result: lastResult, verified: false, message: "Max iterations reached" };
|
|
439
452
|
}
|
|
440
453
|
|
|
454
|
+
// ── Sub-agent tools (v0.9) ──────────────────────────────────────────────────
|
|
455
|
+
|
|
456
|
+
async _toolSpawnSubagent(args, parentOpts = {}) {
|
|
457
|
+
const agent = await this.subagents.spawn({
|
|
458
|
+
task: args.task,
|
|
459
|
+
label: args.label,
|
|
460
|
+
model: args.model ?? null,
|
|
461
|
+
timeout: args.timeout_seconds ?? 300,
|
|
462
|
+
workstream: this._activeWorkstream,
|
|
463
|
+
onNotify: parentOpts?.onSubagentNotify ?? null,
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
success: true,
|
|
468
|
+
id: agent.id,
|
|
469
|
+
label: agent.label,
|
|
470
|
+
status: agent.status,
|
|
471
|
+
message: `Sub-agent '${agent.label}' spawned (id: ${agent.id}). Use get_subagent_result to check when done.`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async _toolListSubagents() {
|
|
476
|
+
const inMemory = this.subagents.list();
|
|
477
|
+
const history = await this.subagents.listHistory(20);
|
|
478
|
+
|
|
479
|
+
// Merge: in-memory takes precedence
|
|
480
|
+
const inMemoryIds = new Set(inMemory.map(a => a.id));
|
|
481
|
+
const combined = [
|
|
482
|
+
...inMemory.map(a => a.toJSON()),
|
|
483
|
+
...history.filter(h => !inMemoryIds.has(h.id)),
|
|
484
|
+
].slice(0, 30);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
agents: combined.map(a => ({
|
|
489
|
+
id: a.id,
|
|
490
|
+
label: a.label,
|
|
491
|
+
status: a.status,
|
|
492
|
+
task: a.task?.slice(0, 80),
|
|
493
|
+
model: a.model,
|
|
494
|
+
createdAt: a.createdAt,
|
|
495
|
+
completedAt: a.completedAt,
|
|
496
|
+
})),
|
|
497
|
+
total: combined.length,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async _toolGetSubagentResult(args) {
|
|
502
|
+
const inMemory = this.subagents.get(args.id);
|
|
503
|
+
if (inMemory) {
|
|
504
|
+
return {
|
|
505
|
+
success: true,
|
|
506
|
+
...inMemory.toJSON(),
|
|
507
|
+
result_preview: inMemory.result?.slice(0, 500),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Try disk
|
|
512
|
+
const disk = await this.subagents.loadFromDisk(args.id);
|
|
513
|
+
if (disk) {
|
|
514
|
+
return { success: true, ...disk, result_preview: disk.result?.slice(0, 500) };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { success: false, error: `Sub-agent not found: ${args.id}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
_toolKillSubagent(args) {
|
|
521
|
+
const agent = this.subagents.get(args.id);
|
|
522
|
+
if (!agent) return { success: false, error: `Sub-agent not found: ${args.id}` };
|
|
523
|
+
this.subagents.kill(args.id);
|
|
524
|
+
return { success: true, id: args.id, message: `Sub-agent '${agent.label}' killed.` };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_toolSteerSubagent(args) {
|
|
528
|
+
try {
|
|
529
|
+
this.subagents.steer(args.id, args.message);
|
|
530
|
+
return { success: true, id: args.id, message: `Guidance sent to sub-agent.` };
|
|
531
|
+
} catch (err) {
|
|
532
|
+
return { success: false, error: err.message };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
441
536
|
// ── Context optimization ─────────────────────────────────────────────────────
|
|
442
537
|
|
|
443
538
|
_optimizeContext(messages, maxTokens = 30_000) {
|
package/core/index.mjs
CHANGED
|
@@ -12,3 +12,4 @@ export { MCPClient, MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
|
12
12
|
export { loadConfig, saveConfig, detectProvider, PROVIDERS, WISPY_DIR, MCP_CONFIG_PATH, SESSIONS_DIR, CONVERSATIONS_DIR, MEMORY_DIR } from "./config.mjs";
|
|
13
13
|
export { MemoryManager } from "./memory.mjs";
|
|
14
14
|
export { CronManager } from "./cron.mjs";
|
|
15
|
+
export { SubAgentManager, SubAgent } from "./subagents.mjs";
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/subagents.mjs — Sub-agent orchestration for Wispy v0.9.0
|
|
3
|
+
*
|
|
4
|
+
* Class SubAgentManager:
|
|
5
|
+
* - async spawn(opts) → SubAgent
|
|
6
|
+
* - list() → SubAgent[]
|
|
7
|
+
* - get(id) → SubAgent
|
|
8
|
+
* - kill(id) → void
|
|
9
|
+
* - steer(id, message) → void
|
|
10
|
+
* - async waitFor(id, timeoutMs?) → Result
|
|
11
|
+
* - async waitForAll(ids) → Result[]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
17
|
+
import { WISPY_DIR } from "./config.mjs";
|
|
18
|
+
|
|
19
|
+
const SUBAGENTS_DIR = path.join(WISPY_DIR, "subagents");
|
|
20
|
+
|
|
21
|
+
function makeId() {
|
|
22
|
+
return `sa-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SubAgent {
|
|
26
|
+
constructor({ id, task, label, model, timeout, workstream, status = "pending" }) {
|
|
27
|
+
this.id = id;
|
|
28
|
+
this.task = task;
|
|
29
|
+
this.label = label ?? `sub-agent-${id}`;
|
|
30
|
+
this.model = model ?? null;
|
|
31
|
+
this.timeout = timeout ?? 300_000; // ms
|
|
32
|
+
this.workstream = workstream ?? "default";
|
|
33
|
+
this.status = status; // pending → running → completed | failed | killed | timeout
|
|
34
|
+
this.result = null;
|
|
35
|
+
this.error = null;
|
|
36
|
+
this.createdAt = new Date().toISOString();
|
|
37
|
+
this.startedAt = null;
|
|
38
|
+
this.completedAt = null;
|
|
39
|
+
this._abortController = new AbortController();
|
|
40
|
+
this._steerMessages = []; // guidance queue
|
|
41
|
+
this._promise = null; // internal execution promise
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
id: this.id,
|
|
47
|
+
task: this.task,
|
|
48
|
+
label: this.label,
|
|
49
|
+
model: this.model,
|
|
50
|
+
timeout: this.timeout,
|
|
51
|
+
workstream: this.workstream,
|
|
52
|
+
status: this.status,
|
|
53
|
+
result: this.result,
|
|
54
|
+
error: this.error,
|
|
55
|
+
createdAt: this.createdAt,
|
|
56
|
+
startedAt: this.startedAt,
|
|
57
|
+
completedAt: this.completedAt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class SubAgentManager {
|
|
63
|
+
/**
|
|
64
|
+
* @param {import('./engine.mjs').WispyEngine} engine
|
|
65
|
+
* @param {import('./session.mjs').SessionManager} sessionManager
|
|
66
|
+
*/
|
|
67
|
+
constructor(engine, sessionManager) {
|
|
68
|
+
this._engine = engine;
|
|
69
|
+
this._sessions = sessionManager;
|
|
70
|
+
this._agents = new Map(); // id → SubAgent
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Spawn a new sub-agent.
|
|
75
|
+
* @param {object} opts
|
|
76
|
+
* @param {string} opts.task
|
|
77
|
+
* @param {string} [opts.label]
|
|
78
|
+
* @param {string} [opts.model]
|
|
79
|
+
* @param {number} [opts.timeout] - milliseconds (default 300_000)
|
|
80
|
+
* @param {string} [opts.workstream]
|
|
81
|
+
* @param {Function} [opts.onComplete] - callback(result)
|
|
82
|
+
* @param {Function} [opts.onNotify] - channel notification callback(type, text)
|
|
83
|
+
* @returns {Promise<SubAgent>}
|
|
84
|
+
*/
|
|
85
|
+
async spawn(opts) {
|
|
86
|
+
const agent = new SubAgent({
|
|
87
|
+
id: makeId(),
|
|
88
|
+
task: opts.task,
|
|
89
|
+
label: opts.label,
|
|
90
|
+
model: opts.model,
|
|
91
|
+
timeout: opts.timeout ? opts.timeout * 1000 : 300_000,
|
|
92
|
+
workstream: opts.workstream ?? this._engine._activeWorkstream,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this._agents.set(agent.id, agent);
|
|
96
|
+
|
|
97
|
+
// Run async without awaiting
|
|
98
|
+
agent._promise = this._run(agent, opts).catch((err) => {
|
|
99
|
+
if (agent.status === "running" || agent.status === "pending") {
|
|
100
|
+
agent.status = "failed";
|
|
101
|
+
agent.error = err.message;
|
|
102
|
+
agent.completedAt = new Date().toISOString();
|
|
103
|
+
this._persist(agent);
|
|
104
|
+
opts.onNotify?.("error", `❌ Sub-agent '${agent.label}' failed: ${err.message}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return agent;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Internal: run the sub-agent's agentic loop.
|
|
113
|
+
*/
|
|
114
|
+
async _run(agent, opts) {
|
|
115
|
+
agent.status = "running";
|
|
116
|
+
agent.startedAt = new Date().toISOString();
|
|
117
|
+
|
|
118
|
+
// Create an isolated session for this sub-agent
|
|
119
|
+
const session = this._sessions.create({ workstream: agent.workstream });
|
|
120
|
+
|
|
121
|
+
// Build initial messages
|
|
122
|
+
const systemPrompt = `You are Wispy 🌿 — a sub-agent handling a delegated task.
|
|
123
|
+
Be focused, thorough, and efficient. Complete the task fully.
|
|
124
|
+
Reply in the same language as the task. Sign off with 🌿.`;
|
|
125
|
+
|
|
126
|
+
const messages = [
|
|
127
|
+
{ role: "system", content: systemPrompt },
|
|
128
|
+
{ role: "user", content: agent.task },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
// Timeout logic
|
|
132
|
+
let timedOut = false;
|
|
133
|
+
const timeoutHandle = setTimeout(() => {
|
|
134
|
+
timedOut = true;
|
|
135
|
+
agent._abortController.abort();
|
|
136
|
+
}, agent.timeout);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const MAX_ROUNDS = 15;
|
|
140
|
+
let round = 0;
|
|
141
|
+
|
|
142
|
+
while (round < MAX_ROUNDS) {
|
|
143
|
+
// Check if killed
|
|
144
|
+
if (agent.status === "killed") break;
|
|
145
|
+
if (timedOut) {
|
|
146
|
+
agent.status = "timeout";
|
|
147
|
+
agent.error = "Timed out";
|
|
148
|
+
agent.completedAt = new Date().toISOString();
|
|
149
|
+
await this._persist(agent);
|
|
150
|
+
opts?.onNotify?.("error", `⏰ Sub-agent '${agent.label}' timed out.`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Inject any steering messages
|
|
155
|
+
while (agent._steerMessages.length > 0) {
|
|
156
|
+
const steerMsg = agent._steerMessages.shift();
|
|
157
|
+
messages.push({ role: "user", content: `[Guidance from orchestrator]: ${steerMsg}` });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Call provider
|
|
161
|
+
const result = await this._engine.providers.chat(
|
|
162
|
+
messages,
|
|
163
|
+
this._engine.tools.getDefinitions(),
|
|
164
|
+
{ model: agent.model }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (result.type === "text") {
|
|
168
|
+
// Final answer
|
|
169
|
+
agent.result = result.text;
|
|
170
|
+
agent.status = "completed";
|
|
171
|
+
agent.completedAt = new Date().toISOString();
|
|
172
|
+
await this._persist(agent);
|
|
173
|
+
|
|
174
|
+
const summary = result.text.slice(0, 200).replace(/\n/g, " ");
|
|
175
|
+
opts?.onNotify?.("success", `✅ Sub-agent '${agent.label}' completed: ${summary}`);
|
|
176
|
+
opts?.onComplete?.(agent);
|
|
177
|
+
clearTimeout(timeoutHandle);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Handle tool calls
|
|
182
|
+
messages.push({ role: "assistant", toolCalls: result.calls, content: "" });
|
|
183
|
+
|
|
184
|
+
for (const call of result.calls) {
|
|
185
|
+
const toolResult = await this._engine._executeTool(
|
|
186
|
+
call.name, call.args, messages, session, {}
|
|
187
|
+
);
|
|
188
|
+
messages.push({
|
|
189
|
+
role: "tool_result",
|
|
190
|
+
toolName: call.name,
|
|
191
|
+
toolUseId: call.id ?? call.name,
|
|
192
|
+
result: toolResult,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
round++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Max rounds reached
|
|
200
|
+
agent.result = "(max rounds reached — partial work above)";
|
|
201
|
+
agent.status = "completed";
|
|
202
|
+
agent.completedAt = new Date().toISOString();
|
|
203
|
+
await this._persist(agent);
|
|
204
|
+
opts?.onNotify?.("success", `✅ Sub-agent '${agent.label}' completed (max rounds).`);
|
|
205
|
+
} catch (err) {
|
|
206
|
+
clearTimeout(timeoutHandle);
|
|
207
|
+
if (agent.status !== "killed") {
|
|
208
|
+
agent.status = "failed";
|
|
209
|
+
agent.error = err.message;
|
|
210
|
+
agent.completedAt = new Date().toISOString();
|
|
211
|
+
await this._persist(agent);
|
|
212
|
+
opts?.onNotify?.("error", `❌ Sub-agent '${agent.label}' failed: ${err.message}`);
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
clearTimeout(timeoutHandle);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* List all sub-agents (active + recent in-memory).
|
|
221
|
+
*/
|
|
222
|
+
list() {
|
|
223
|
+
return Array.from(this._agents.values());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get a sub-agent by ID.
|
|
228
|
+
*/
|
|
229
|
+
get(id) {
|
|
230
|
+
return this._agents.get(id) ?? null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Kill (cancel) a running sub-agent.
|
|
235
|
+
*/
|
|
236
|
+
kill(id) {
|
|
237
|
+
const agent = this._agents.get(id);
|
|
238
|
+
if (!agent) return;
|
|
239
|
+
if (agent.status === "running" || agent.status === "pending") {
|
|
240
|
+
agent.status = "killed";
|
|
241
|
+
agent.completedAt = new Date().toISOString();
|
|
242
|
+
agent._abortController.abort();
|
|
243
|
+
this._persist(agent).catch(() => {});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Send guidance/steering to a running sub-agent.
|
|
249
|
+
*/
|
|
250
|
+
steer(id, message) {
|
|
251
|
+
const agent = this._agents.get(id);
|
|
252
|
+
if (!agent) throw new Error(`Sub-agent not found: ${id}`);
|
|
253
|
+
if (agent.status !== "running" && agent.status !== "pending") {
|
|
254
|
+
throw new Error(`Sub-agent ${id} is not running (status: ${agent.status})`);
|
|
255
|
+
}
|
|
256
|
+
agent._steerMessages.push(message);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Wait for a specific sub-agent to complete.
|
|
261
|
+
* @param {string} id
|
|
262
|
+
* @param {number} [timeoutMs]
|
|
263
|
+
* @returns {Promise<{id, status, result, error}>}
|
|
264
|
+
*/
|
|
265
|
+
async waitFor(id, timeoutMs) {
|
|
266
|
+
const agent = this._agents.get(id);
|
|
267
|
+
if (!agent) throw new Error(`Sub-agent not found: ${id}`);
|
|
268
|
+
|
|
269
|
+
if (agent.status === "completed" || agent.status === "failed" ||
|
|
270
|
+
agent.status === "killed" || agent.status === "timeout") {
|
|
271
|
+
return agent.toJSON();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!agent._promise) throw new Error(`Sub-agent ${id} has no active promise`);
|
|
275
|
+
|
|
276
|
+
if (timeoutMs) {
|
|
277
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
278
|
+
setTimeout(() => reject(new Error(`waitFor timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
279
|
+
);
|
|
280
|
+
await Promise.race([agent._promise, timeoutPromise]);
|
|
281
|
+
} else {
|
|
282
|
+
await agent._promise;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return agent.toJSON();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Wait for multiple sub-agents to complete.
|
|
290
|
+
* @param {string[]} ids
|
|
291
|
+
* @returns {Promise<Array>}
|
|
292
|
+
*/
|
|
293
|
+
async waitForAll(ids) {
|
|
294
|
+
return Promise.all(ids.map(id => this.waitFor(id)));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Persist a sub-agent's result to disk.
|
|
299
|
+
*/
|
|
300
|
+
async _persist(agent) {
|
|
301
|
+
try {
|
|
302
|
+
await mkdir(SUBAGENTS_DIR, { recursive: true });
|
|
303
|
+
const filePath = path.join(SUBAGENTS_DIR, `${agent.id}.json`);
|
|
304
|
+
await writeFile(filePath, JSON.stringify(agent.toJSON(), null, 2) + "\n", "utf8");
|
|
305
|
+
} catch {
|
|
306
|
+
// Non-fatal
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Load persisted sub-agent from disk (for history).
|
|
312
|
+
*/
|
|
313
|
+
async loadFromDisk(id) {
|
|
314
|
+
try {
|
|
315
|
+
const filePath = path.join(SUBAGENTS_DIR, `${id}.json`);
|
|
316
|
+
const data = JSON.parse(await readFile(filePath, "utf8"));
|
|
317
|
+
return data;
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* List persisted sub-agent history from disk.
|
|
325
|
+
*/
|
|
326
|
+
async listHistory(limit = 20) {
|
|
327
|
+
try {
|
|
328
|
+
const { readdir } = await import("node:fs/promises");
|
|
329
|
+
const files = await readdir(SUBAGENTS_DIR);
|
|
330
|
+
const jsonFiles = files.filter(f => f.endsWith(".json")).sort().reverse().slice(0, limit);
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const f of jsonFiles) {
|
|
333
|
+
try {
|
|
334
|
+
const data = JSON.parse(await readFile(path.join(SUBAGENTS_DIR, f), "utf8"));
|
|
335
|
+
results.push(data);
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
return results;
|
|
339
|
+
} catch {
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
package/core/tools.mjs
CHANGED
|
@@ -221,6 +221,60 @@ export class ToolRegistry {
|
|
|
221
221
|
required: ["task"],
|
|
222
222
|
},
|
|
223
223
|
},
|
|
224
|
+
// ── Sub-agent tools (v0.9) ───────────────────────────────────────────────
|
|
225
|
+
{
|
|
226
|
+
name: "spawn_subagent",
|
|
227
|
+
description: "Spawn an isolated sub-agent to handle a task independently. Use for parallel work, long-running tasks, or delegating to specialized models.",
|
|
228
|
+
parameters: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
task: { type: "string", description: "The task description/prompt for the sub-agent" },
|
|
232
|
+
label: { type: "string", description: "Human-readable label for the sub-agent" },
|
|
233
|
+
model: { type: "string", description: "Model override (e.g., 'flash' for cheap tasks, 'claude-opus-4-5' for complex)" },
|
|
234
|
+
timeout_seconds: { type: "number", description: "Max runtime in seconds (default 300)" },
|
|
235
|
+
},
|
|
236
|
+
required: ["task"],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "list_subagents",
|
|
241
|
+
description: "List all sub-agents (active and recent)",
|
|
242
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "get_subagent_result",
|
|
246
|
+
description: "Get the result of a completed sub-agent",
|
|
247
|
+
parameters: {
|
|
248
|
+
type: "object",
|
|
249
|
+
properties: {
|
|
250
|
+
id: { type: "string", description: "Sub-agent ID" },
|
|
251
|
+
},
|
|
252
|
+
required: ["id"],
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "kill_subagent",
|
|
257
|
+
description: "Cancel a running sub-agent",
|
|
258
|
+
parameters: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
id: { type: "string", description: "Sub-agent ID" },
|
|
262
|
+
},
|
|
263
|
+
required: ["id"],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: "steer_subagent",
|
|
268
|
+
description: "Send additional guidance to a running sub-agent",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
id: { type: "string", description: "Sub-agent ID" },
|
|
273
|
+
message: { type: "string", description: "Guidance message" },
|
|
274
|
+
},
|
|
275
|
+
required: ["id", "message"],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
224
278
|
];
|
|
225
279
|
|
|
226
280
|
for (const def of builtins) {
|
|
@@ -514,6 +568,12 @@ export class ToolRegistry {
|
|
|
514
568
|
case "pipeline":
|
|
515
569
|
case "spawn_async_agent":
|
|
516
570
|
case "ralph_loop":
|
|
571
|
+
// Sub-agent tools (v0.9) — handled at engine level
|
|
572
|
+
case "spawn_subagent":
|
|
573
|
+
case "list_subagents":
|
|
574
|
+
case "get_subagent_result":
|
|
575
|
+
case "kill_subagent":
|
|
576
|
+
case "steer_subagent":
|
|
517
577
|
return { success: false, error: `Tool "${name}" requires engine context. Call via WispyEngine.processMessage().` };
|
|
518
578
|
|
|
519
579
|
default:
|
package/lib/wispy-repl.mjs
CHANGED
|
@@ -330,6 +330,12 @@ ${bold("Wispy Commands:")}
|
|
|
330
330
|
${cyan("/forget")} <key> Delete a memory file
|
|
331
331
|
${cyan("/memories")} List all memory files
|
|
332
332
|
${cyan("/recall")} <query> Search memories
|
|
333
|
+
|
|
334
|
+
${bold("Sub-agent Commands (v0.9):")}
|
|
335
|
+
${cyan("/agents")} List active/recent sub-agents
|
|
336
|
+
${cyan("/agent")} <id> Show sub-agent details and result
|
|
337
|
+
${cyan("/kill")} <id> Cancel a running sub-agent
|
|
338
|
+
|
|
333
339
|
${cyan("/quit")} or ${cyan("/exit")} Exit
|
|
334
340
|
`);
|
|
335
341
|
return true;
|
|
@@ -614,6 +620,72 @@ ${bold("Wispy Commands:")}
|
|
|
614
620
|
return true;
|
|
615
621
|
}
|
|
616
622
|
|
|
623
|
+
// ── Sub-agent commands (v0.9) ──────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
if (cmd === "/agents") {
|
|
626
|
+
const inMemory = engine.subagents.list();
|
|
627
|
+
const history = await engine.subagents.listHistory(20);
|
|
628
|
+
const inMemoryIds = new Set(inMemory.map(a => a.id));
|
|
629
|
+
const all = [
|
|
630
|
+
...inMemory.map(a => a.toJSON()),
|
|
631
|
+
...history.filter(h => !inMemoryIds.has(h.id)),
|
|
632
|
+
].slice(0, 30);
|
|
633
|
+
|
|
634
|
+
if (all.length === 0) {
|
|
635
|
+
console.log(dim("No sub-agents yet. Ask wispy to spawn one!"));
|
|
636
|
+
} else {
|
|
637
|
+
console.log(bold(`\n🤖 Sub-agents (${all.length}):\n`));
|
|
638
|
+
for (const a of all) {
|
|
639
|
+
const icon = { completed: green("✓"), failed: red("✗"), running: cyan("⟳"), killed: dim("✕"), timeout: yellow("⏰"), pending: dim("…") }[a.status] ?? "?";
|
|
640
|
+
const elapsed = a.completedAt ? "" : a.startedAt ? ` ${dim(`(running ${Math.round((Date.now() - new Date(a.startedAt)) / 1000)}s)`)}` : "";
|
|
641
|
+
console.log(` ${icon} ${bold(a.id)} ${dim("·")} ${a.label}${elapsed}`);
|
|
642
|
+
console.log(` ${dim(`task: ${(a.task ?? "").slice(0, 70)}${(a.task?.length ?? 0) > 70 ? "…" : ""}`)}`);
|
|
643
|
+
if (a.result) console.log(` ${dim(`result: ${a.result.slice(0, 80)}${a.result.length > 80 ? "…" : ""}`)}`);
|
|
644
|
+
if (a.error) console.log(` ${red(`error: ${a.error.slice(0, 80)}`)}`);
|
|
645
|
+
console.log("");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (cmd === "/agent") {
|
|
652
|
+
const id = parts[1];
|
|
653
|
+
if (!id) { console.log(yellow("Usage: /agent <id>")); return true; }
|
|
654
|
+
|
|
655
|
+
let agent = engine.subagents.get(id)?.toJSON();
|
|
656
|
+
if (!agent) agent = await engine.subagents.loadFromDisk(id);
|
|
657
|
+
|
|
658
|
+
if (!agent) {
|
|
659
|
+
console.log(red(`Sub-agent not found: ${id}`));
|
|
660
|
+
} else {
|
|
661
|
+
console.log(bold(`\n🤖 Sub-agent: ${agent.id}\n`));
|
|
662
|
+
console.log(` ${dim("Label:")} ${agent.label}`);
|
|
663
|
+
console.log(` ${dim("Status:")} ${agent.status}`);
|
|
664
|
+
console.log(` ${dim("Model:")} ${agent.model ?? "(default)"}`);
|
|
665
|
+
console.log(` ${dim("Created:")} ${agent.createdAt}`);
|
|
666
|
+
if (agent.startedAt) console.log(` ${dim("Started:")} ${agent.startedAt}`);
|
|
667
|
+
if (agent.completedAt) console.log(` ${dim("Done:")} ${agent.completedAt}`);
|
|
668
|
+
console.log(`\n ${bold("Task:")}\n ${dim(agent.task)}\n`);
|
|
669
|
+
if (agent.result) {
|
|
670
|
+
console.log(` ${bold("Result:")}\n ${agent.result.slice(0, 2000)}`);
|
|
671
|
+
}
|
|
672
|
+
if (agent.error) {
|
|
673
|
+
console.log(` ${bold("Error:")} ${red(agent.error)}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (cmd === "/kill") {
|
|
680
|
+
const id = parts[1];
|
|
681
|
+
if (!id) { console.log(yellow("Usage: /kill <id>")); return true; }
|
|
682
|
+
const agent = engine.subagents.get(id);
|
|
683
|
+
if (!agent) { console.log(red(`Sub-agent not found: ${id}`)); return true; }
|
|
684
|
+
engine.subagents.kill(id);
|
|
685
|
+
console.log(green(`🛑 Sub-agent '${agent.label}' killed.`));
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
617
689
|
if (cmd === "/quit" || cmd === "/exit") {
|
|
618
690
|
console.log(dim(`🌿 Bye! (${engine.providers.formatCost()})`));
|
|
619
691
|
engine.destroy();
|
package/package.json
CHANGED