wispy-cli 0.6.1 → 0.8.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/bin/wispy.mjs +172 -2
- package/core/config.mjs +104 -0
- package/core/cron.mjs +346 -0
- package/core/engine.mjs +705 -0
- package/core/index.mjs +14 -0
- package/core/mcp.mjs +8 -0
- package/core/memory.mjs +275 -0
- package/core/providers.mjs +410 -0
- package/core/session.mjs +196 -0
- package/core/tools.mjs +526 -0
- package/lib/channels/index.mjs +45 -246
- package/lib/wispy-repl.mjs +396 -2452
- package/lib/wispy-tui.mjs +105 -588
- package/package.json +7 -4
package/core/engine.mjs
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/engine.mjs — Main chat engine for Wispy
|
|
3
|
+
*
|
|
4
|
+
* Class WispyEngine:
|
|
5
|
+
* - constructor(config) — loads config, initializes providers/tools/mcp
|
|
6
|
+
* - async init() — async initialization
|
|
7
|
+
* - async processMessage(sessionId, userMessage, opts?) → assistantMessage
|
|
8
|
+
* - async processToolCalls(toolCalls) → results array
|
|
9
|
+
*
|
|
10
|
+
* This is THE single entry point. CLI, TUI, channels all call this.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
16
|
+
|
|
17
|
+
import { WISPY_DIR, CONVERSATIONS_DIR, MEMORY_DIR, MCP_CONFIG_PATH, detectProvider, PROVIDERS } from "./config.mjs";
|
|
18
|
+
import { ProviderRegistry } from "./providers.mjs";
|
|
19
|
+
import { ToolRegistry } from "./tools.mjs";
|
|
20
|
+
import { SessionManager } from "./session.mjs";
|
|
21
|
+
import { MCPManager, ensureDefaultMcpConfig } from "./mcp.mjs";
|
|
22
|
+
import { MemoryManager } from "./memory.mjs";
|
|
23
|
+
|
|
24
|
+
const MAX_TOOL_ROUNDS = 10;
|
|
25
|
+
const MAX_CONTEXT_CHARS = 40_000;
|
|
26
|
+
|
|
27
|
+
export class WispyEngine {
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.providers = new ProviderRegistry();
|
|
31
|
+
this.tools = new ToolRegistry();
|
|
32
|
+
this.sessions = new SessionManager();
|
|
33
|
+
this.mcpManager = new MCPManager(config.mcpConfigPath ?? MCP_CONFIG_PATH);
|
|
34
|
+
this.memory = new MemoryManager(WISPY_DIR);
|
|
35
|
+
this._initialized = false;
|
|
36
|
+
this._activeWorkstream = config.workstream
|
|
37
|
+
?? process.env.WISPY_WORKSTREAM
|
|
38
|
+
?? process.argv.find((a, i) => (process.argv[i-1] === "-w" || process.argv[i-1] === "--workstream"))
|
|
39
|
+
?? "default";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get activeWorkstream() { return this._activeWorkstream; }
|
|
43
|
+
get model() { return this.providers.model; }
|
|
44
|
+
get provider() { return this.providers.provider; }
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the engine (async). Call before processMessage().
|
|
48
|
+
*/
|
|
49
|
+
async init(opts = {}) {
|
|
50
|
+
if (this._initialized) return this;
|
|
51
|
+
|
|
52
|
+
// Initialize provider
|
|
53
|
+
const providerResult = await this.providers.init(opts.providerOverrides ?? {});
|
|
54
|
+
if (!providerResult && !opts.allowNoProvider) {
|
|
55
|
+
return null; // No provider configured
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Register built-in tools
|
|
59
|
+
this.tools.registerBuiltin();
|
|
60
|
+
|
|
61
|
+
// Register memory tools
|
|
62
|
+
this._registerMemoryTools();
|
|
63
|
+
|
|
64
|
+
// Initialize MCP
|
|
65
|
+
if (!opts.skipMcp) {
|
|
66
|
+
await ensureDefaultMcpConfig(this.mcpManager.configPath);
|
|
67
|
+
const mcpResults = await this.mcpManager.autoConnect();
|
|
68
|
+
if (process.env.WISPY_DEBUG) {
|
|
69
|
+
for (const r of mcpResults) {
|
|
70
|
+
if (r.status === "failed") console.error(`[wispy] MCP "${r.name}" failed: ${r.error?.slice(0, 80)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.tools.registerMCP(this.mcpManager);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this._initialized = true;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Process a user message. Runs the full agentic loop.
|
|
82
|
+
*
|
|
83
|
+
* @param {string|null} sessionId - Session ID (null = create new)
|
|
84
|
+
* @param {string} userMessage - The user's message
|
|
85
|
+
* @param {object} opts - Options: { onChunk, systemPrompt, workstream, noSave }
|
|
86
|
+
* @returns {object} { role: "assistant", content: string, usage? }
|
|
87
|
+
*/
|
|
88
|
+
async processMessage(sessionId, userMessage, opts = {}) {
|
|
89
|
+
if (!this._initialized) await this.init();
|
|
90
|
+
|
|
91
|
+
// Get or create session
|
|
92
|
+
let session;
|
|
93
|
+
if (sessionId) {
|
|
94
|
+
session = this.sessions.get(sessionId) ?? await this.sessions.load(sessionId);
|
|
95
|
+
if (!session) {
|
|
96
|
+
// Create new session with given ID context
|
|
97
|
+
session = this.sessions.create({ workstream: opts.workstream ?? this._activeWorkstream });
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
session = this.sessions.create({ workstream: opts.workstream ?? this._activeWorkstream });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build messages array for the provider
|
|
104
|
+
const systemPrompt = opts.systemPrompt ?? await this._buildSystemPrompt(userMessage);
|
|
105
|
+
|
|
106
|
+
// Initialize messages with system prompt if empty
|
|
107
|
+
let messages;
|
|
108
|
+
if (session.messages.length === 0) {
|
|
109
|
+
messages = [{ role: "system", content: systemPrompt }];
|
|
110
|
+
} else {
|
|
111
|
+
messages = [...session.messages];
|
|
112
|
+
// Refresh system prompt
|
|
113
|
+
if (messages[0]?.role === "system") {
|
|
114
|
+
messages[0] = { role: "system", content: systemPrompt };
|
|
115
|
+
} else {
|
|
116
|
+
messages.unshift({ role: "system", content: systemPrompt });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add user message
|
|
121
|
+
messages.push({ role: "user", content: userMessage });
|
|
122
|
+
this.sessions.addMessage(session.id, { role: "user", content: userMessage });
|
|
123
|
+
|
|
124
|
+
// Run agentic loop
|
|
125
|
+
const responseText = await this._agentLoop(messages, session, opts);
|
|
126
|
+
|
|
127
|
+
// Add assistant message to session
|
|
128
|
+
this.sessions.addMessage(session.id, { role: "assistant", content: responseText });
|
|
129
|
+
|
|
130
|
+
// Save session (async, don't await unless needed)
|
|
131
|
+
if (!opts.noSave) {
|
|
132
|
+
this.sessions.save(session.id).catch(() => {});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
role: "assistant",
|
|
137
|
+
content: responseText,
|
|
138
|
+
sessionId: session.id,
|
|
139
|
+
usage: { ...this.providers.sessionTokens },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Process tool calls manually.
|
|
145
|
+
* @param {Array} toolCalls - Array of { name, args }
|
|
146
|
+
* @returns {Array} results
|
|
147
|
+
*/
|
|
148
|
+
async processToolCalls(toolCalls) {
|
|
149
|
+
const results = [];
|
|
150
|
+
for (const call of toolCalls) {
|
|
151
|
+
const result = await this.tools.execute(call.name, call.args);
|
|
152
|
+
results.push({ toolName: call.name, result });
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Internal: agentic loop — keeps calling provider until no more tool calls.
|
|
159
|
+
*/
|
|
160
|
+
async _agentLoop(messages, session, opts = {}) {
|
|
161
|
+
// Optimize context
|
|
162
|
+
messages = this._optimizeContext(messages);
|
|
163
|
+
|
|
164
|
+
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
165
|
+
const result = await this.providers.chat(messages, this.tools.getDefinitions(), {
|
|
166
|
+
onChunk: opts.onChunk,
|
|
167
|
+
model: opts.model,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (result.type === "text") {
|
|
171
|
+
return result.text;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle tool calls
|
|
175
|
+
const toolCallMsg = { role: "assistant", toolCalls: result.calls, content: "" };
|
|
176
|
+
messages.push(toolCallMsg);
|
|
177
|
+
|
|
178
|
+
for (const call of result.calls) {
|
|
179
|
+
if (opts.onToolCall) opts.onToolCall(call.name, call.args);
|
|
180
|
+
|
|
181
|
+
const toolResult = await this._executeTool(call.name, call.args, messages, session, opts);
|
|
182
|
+
|
|
183
|
+
if (opts.onToolResult) opts.onToolResult(call.name, toolResult);
|
|
184
|
+
|
|
185
|
+
messages.push({
|
|
186
|
+
role: "tool_result",
|
|
187
|
+
toolName: call.name,
|
|
188
|
+
toolUseId: call.id ?? call.name,
|
|
189
|
+
result: toolResult,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return "(tool call limit reached)";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Execute a tool, handling engine-level tools (spawn_agent, etc.) specially.
|
|
199
|
+
*/
|
|
200
|
+
async _executeTool(name, args, messages, session, opts) {
|
|
201
|
+
// Engine-level tools that need conversation context
|
|
202
|
+
switch (name) {
|
|
203
|
+
case "spawn_agent":
|
|
204
|
+
return this._toolSpawnAgent(args, messages, session);
|
|
205
|
+
case "list_agents":
|
|
206
|
+
return this._toolListAgents();
|
|
207
|
+
case "get_agent_result":
|
|
208
|
+
return this._toolGetAgentResult(args);
|
|
209
|
+
case "update_plan":
|
|
210
|
+
return this._toolUpdatePlan(args);
|
|
211
|
+
case "pipeline":
|
|
212
|
+
return this._toolPipeline(args, messages, session);
|
|
213
|
+
case "spawn_async_agent":
|
|
214
|
+
return this._toolSpawnAsyncAgent(args, messages, session);
|
|
215
|
+
case "ralph_loop":
|
|
216
|
+
return this._toolRalphLoop(args, messages, session);
|
|
217
|
+
case "memory_save":
|
|
218
|
+
return this._toolMemorySave(args);
|
|
219
|
+
case "memory_search":
|
|
220
|
+
return this._toolMemorySearch(args);
|
|
221
|
+
case "memory_list":
|
|
222
|
+
return this._toolMemoryList();
|
|
223
|
+
case "memory_get":
|
|
224
|
+
return this._toolMemoryGet(args);
|
|
225
|
+
case "memory_append":
|
|
226
|
+
return this._toolMemoryAppend(args);
|
|
227
|
+
case "memory_delete":
|
|
228
|
+
return this._toolMemoryDelete(args);
|
|
229
|
+
default:
|
|
230
|
+
return this.tools.execute(name, args);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Agent tools ─────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
async _toolSpawnAgent(args, parentMessages, session) {
|
|
237
|
+
const role = args.role ?? "worker";
|
|
238
|
+
const agentId = `agent-${Date.now().toString(36)}-${role}`;
|
|
239
|
+
const agentsFile = path.join(WISPY_DIR, "agents.json");
|
|
240
|
+
let agents = [];
|
|
241
|
+
try { agents = JSON.parse(await readFile(agentsFile, "utf8")); } catch {}
|
|
242
|
+
|
|
243
|
+
const agent = {
|
|
244
|
+
id: agentId, role, task: args.task, model: this.model,
|
|
245
|
+
status: "running", createdAt: new Date().toISOString(),
|
|
246
|
+
workstream: this._activeWorkstream, result: null,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const rolePrompts = {
|
|
250
|
+
explorer: "Search and analyze codebases, find relevant files and patterns.",
|
|
251
|
+
planner: "Design implementation strategies and create step-by-step plans.",
|
|
252
|
+
worker: "Implement code changes, write files, execute commands.",
|
|
253
|
+
reviewer: "Review code for bugs, security issues, and best practices.",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const agentMessages = [
|
|
257
|
+
{ role: "system", content: `You are a ${role} sub-agent for Wispy. ${rolePrompts[role] ?? ""} Be concise and deliver actionable results.` },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
if (args.fork_context) {
|
|
261
|
+
const recentContext = parentMessages.filter(m => m.role === "user" || m.role === "assistant").slice(-6);
|
|
262
|
+
agentMessages.push(...recentContext);
|
|
263
|
+
}
|
|
264
|
+
agentMessages.push({ role: "user", content: args.task });
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const result = await this.providers.chat(agentMessages, this.tools.getDefinitions(), {});
|
|
268
|
+
agent.result = result.type === "text" ? result.text : JSON.stringify(result);
|
|
269
|
+
agent.status = "completed";
|
|
270
|
+
agent.completedAt = new Date().toISOString();
|
|
271
|
+
} catch (err) {
|
|
272
|
+
agent.result = `Error: ${err.message}`;
|
|
273
|
+
agent.status = "failed";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
agents.push(agent);
|
|
277
|
+
if (agents.length > 50) agents = agents.slice(-50);
|
|
278
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
279
|
+
await writeFile(agentsFile, JSON.stringify(agents, null, 2) + "\n", "utf8");
|
|
280
|
+
|
|
281
|
+
return { success: true, agent_id: agentId, role, status: agent.status, result_preview: agent.result?.slice(0, 200) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async _toolListAgents() {
|
|
285
|
+
const agentsFile = path.join(WISPY_DIR, "agents.json");
|
|
286
|
+
let agents = [];
|
|
287
|
+
try { agents = JSON.parse(await readFile(agentsFile, "utf8")); } catch {}
|
|
288
|
+
const wsAgents = agents.filter(a => a.workstream === this._activeWorkstream);
|
|
289
|
+
return {
|
|
290
|
+
success: true,
|
|
291
|
+
agents: wsAgents.map(a => ({ id: a.id, role: a.role, status: a.status, task: a.task.slice(0, 60), model: a.model, createdAt: a.createdAt })),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async _toolGetAgentResult(args) {
|
|
296
|
+
const agentsFile = path.join(WISPY_DIR, "agents.json");
|
|
297
|
+
let agents = [];
|
|
298
|
+
try { agents = JSON.parse(await readFile(agentsFile, "utf8")); } catch {}
|
|
299
|
+
const found = agents.find(a => a.id === args.agent_id);
|
|
300
|
+
if (!found) return { success: false, error: `Agent not found: ${args.agent_id}` };
|
|
301
|
+
return { success: true, id: found.id, role: found.role, status: found.status, result: found.result };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async _toolUpdatePlan(args) {
|
|
305
|
+
const planFile = path.join(CONVERSATIONS_DIR, `${this._activeWorkstream}.plan.json`);
|
|
306
|
+
const plan = { explanation: args.explanation, steps: args.steps, updatedAt: new Date().toISOString() };
|
|
307
|
+
await mkdir(CONVERSATIONS_DIR, { recursive: true });
|
|
308
|
+
await writeFile(planFile, JSON.stringify(plan, null, 2) + "\n", "utf8");
|
|
309
|
+
return { success: true, message: "Plan updated" };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async _toolPipeline(args, parentMessages, session) {
|
|
313
|
+
const stages = args.stages ?? ["explorer", "planner", "worker"];
|
|
314
|
+
let stageInput = args.task;
|
|
315
|
+
const results = [];
|
|
316
|
+
|
|
317
|
+
const rolePrompts = {
|
|
318
|
+
explorer: "Find relevant files, patterns, and information.",
|
|
319
|
+
planner: "Design a concrete implementation plan based on the exploration results.",
|
|
320
|
+
worker: "Implement the plan. Write code, create files, run commands.",
|
|
321
|
+
reviewer: "Review the implementation. Check for bugs, security issues, completeness.",
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < stages.length; i++) {
|
|
325
|
+
const role = stages[i];
|
|
326
|
+
const stagePrompt = i === 0
|
|
327
|
+
? stageInput
|
|
328
|
+
: `Previous stage (${stages[i-1]}) output:\n${results[i-1].slice(0, 3000)}\n\nYour task as ${role}: ${args.task}`;
|
|
329
|
+
|
|
330
|
+
const stageMessages = [
|
|
331
|
+
{ role: "system", content: `You are a ${role} agent in a pipeline. Stage ${i + 1} of ${stages.length}. ${rolePrompts[role] ?? ""} Be concise.` },
|
|
332
|
+
{ role: "user", content: stagePrompt },
|
|
333
|
+
];
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const result = await this.providers.chat(stageMessages, this.tools.getDefinitions(), {});
|
|
337
|
+
const output = result.type === "text" ? result.text : JSON.stringify(result);
|
|
338
|
+
results.push(output);
|
|
339
|
+
stageInput = output;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
results.push(`Error: ${err.message}`);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
success: true,
|
|
348
|
+
stages: stages.map((role, i) => ({ role, output: results[i]?.slice(0, 500) ?? "skipped" })),
|
|
349
|
+
final_output: results[results.length - 1]?.slice(0, 1000),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async _toolSpawnAsyncAgent(args, parentMessages, session) {
|
|
354
|
+
const role = args.role ?? "worker";
|
|
355
|
+
const agentId = `async-${Date.now().toString(36)}-${role}`;
|
|
356
|
+
const agentsFile = path.join(WISPY_DIR, "agents.json");
|
|
357
|
+
let agents = [];
|
|
358
|
+
try { agents = JSON.parse(await readFile(agentsFile, "utf8")); } catch {}
|
|
359
|
+
|
|
360
|
+
const agent = {
|
|
361
|
+
id: agentId, role, task: args.task,
|
|
362
|
+
status: "running", async: true,
|
|
363
|
+
createdAt: new Date().toISOString(),
|
|
364
|
+
workstream: this._activeWorkstream, result: null,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
agents.push(agent);
|
|
368
|
+
if (agents.length > 50) agents = agents.slice(-50);
|
|
369
|
+
await mkdir(WISPY_DIR, { recursive: true });
|
|
370
|
+
await writeFile(agentsFile, JSON.stringify(agents, null, 2) + "\n", "utf8");
|
|
371
|
+
|
|
372
|
+
// Fire and forget
|
|
373
|
+
(async () => {
|
|
374
|
+
const agentMessages = [
|
|
375
|
+
{ role: "system", content: `You are a ${role} sub-agent. Be concise and actionable.` },
|
|
376
|
+
{ role: "user", content: args.task },
|
|
377
|
+
];
|
|
378
|
+
try {
|
|
379
|
+
const result = await this.providers.chat(agentMessages, this.tools.getDefinitions(), {});
|
|
380
|
+
agent.result = result.type === "text" ? result.text : JSON.stringify(result);
|
|
381
|
+
agent.status = "completed";
|
|
382
|
+
} catch (err) {
|
|
383
|
+
agent.result = `Error: ${err.message}`;
|
|
384
|
+
agent.status = "failed";
|
|
385
|
+
}
|
|
386
|
+
agent.completedAt = new Date().toISOString();
|
|
387
|
+
|
|
388
|
+
let currentAgents = [];
|
|
389
|
+
try { currentAgents = JSON.parse(await readFile(agentsFile, "utf8")); } catch {}
|
|
390
|
+
const idx = currentAgents.findIndex(a => a.id === agentId);
|
|
391
|
+
if (idx !== -1) currentAgents[idx] = agent;
|
|
392
|
+
await writeFile(agentsFile, JSON.stringify(currentAgents, null, 2) + "\n", "utf8");
|
|
393
|
+
})();
|
|
394
|
+
|
|
395
|
+
return { success: true, agent_id: agentId, role, status: "running", message: "Agent launched in background. Use get_agent_result to check when done." };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async _toolRalphLoop(args, parentMessages, session) {
|
|
399
|
+
const MAX_ITERATIONS = 5;
|
|
400
|
+
const criteria = args.success_criteria ?? "Task is fully completed and verified";
|
|
401
|
+
let lastResult = "";
|
|
402
|
+
|
|
403
|
+
for (let attempt = 1; attempt <= MAX_ITERATIONS; attempt++) {
|
|
404
|
+
const workerPrompt = attempt === 1
|
|
405
|
+
? args.task
|
|
406
|
+
: `Previous attempt output:\n${lastResult.slice(0, 2000)}\n\nThe reviewer said this is NOT complete yet. Try again.\nTask: ${args.task}\nSuccess criteria: ${criteria}`;
|
|
407
|
+
|
|
408
|
+
const workerMessages = [
|
|
409
|
+
{ role: "system", content: "You are a worker agent. Execute the task thoroughly. Do not stop until the task is fully done." },
|
|
410
|
+
{ role: "user", content: workerPrompt },
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const workerResult = await this.providers.chat(workerMessages, this.tools.getDefinitions(), {});
|
|
415
|
+
lastResult = workerResult.type === "text" ? workerResult.text : JSON.stringify(workerResult);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const reviewerMessages = [
|
|
421
|
+
{ role: "system", content: 'You are a reviewer agent. Reply with JSON: {"complete": true/false, "reason": "why"}' },
|
|
422
|
+
{ role: "user", content: `Task: ${args.task}\nSuccess criteria: ${criteria}\n\nWorker output:\n${lastResult.slice(0, 3000)}\n\nIs this task TRULY complete? Reply with JSON only.` },
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const reviewResult = await this.providers.chat(reviewerMessages, [], {});
|
|
427
|
+
const reviewText = reviewResult.type === "text" ? reviewResult.text : "";
|
|
428
|
+
const jsonMatch = reviewText.match(/\{[\s\S]*"complete"[\s\S]*\}/);
|
|
429
|
+
if (jsonMatch) {
|
|
430
|
+
const verdict = JSON.parse(jsonMatch[0]);
|
|
431
|
+
if (verdict.complete) {
|
|
432
|
+
return { success: true, iterations: attempt, result: lastResult, verified: true };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch { /* parse failed, continue */ }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return { success: true, iterations: MAX_ITERATIONS, result: lastResult, verified: false, message: "Max iterations reached" };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Context optimization ─────────────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
_optimizeContext(messages, maxTokens = 30_000) {
|
|
444
|
+
const estimateTokens = (text) => Math.ceil((text?.length ?? 0) / 4);
|
|
445
|
+
const estimateMessages = (msgs) => msgs.reduce((sum, m) => sum + estimateTokens(m.content ?? JSON.stringify(m)), 0);
|
|
446
|
+
|
|
447
|
+
const total = estimateMessages(messages);
|
|
448
|
+
if (total <= maxTokens) return messages;
|
|
449
|
+
|
|
450
|
+
const system = messages.filter(m => m.role === "system");
|
|
451
|
+
let rest = messages.filter(m => m.role !== "system");
|
|
452
|
+
|
|
453
|
+
while (estimateMessages([...system, ...rest]) > maxTokens && rest.length > 4) {
|
|
454
|
+
rest.shift();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (estimateMessages([...system, ...rest]) > maxTokens) {
|
|
458
|
+
rest = rest.map(m => ({ ...m, content: m.content ? m.content.slice(0, 2000) : m.content }));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return [...system, ...rest];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── System prompt ────────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
async _buildSystemPrompt(lastUserMessage = "") {
|
|
467
|
+
const parts = [
|
|
468
|
+
"You are Wispy 🌿 — a small ghost that lives in terminals.",
|
|
469
|
+
"You float between code, files, and servers. You're playful, honest, and curious.",
|
|
470
|
+
"",
|
|
471
|
+
"## Personality",
|
|
472
|
+
"- Playful with a bit of humor, but serious when working",
|
|
473
|
+
"- Always use casual speech (반말). Never formal/polite speech.",
|
|
474
|
+
"- Honest — if you don't know, say so.",
|
|
475
|
+
"- Concise — don't over-explain.",
|
|
476
|
+
"",
|
|
477
|
+
"## Speech rules",
|
|
478
|
+
"- ALWAYS end your response with exactly one 🌿 emoji (signature)",
|
|
479
|
+
"- Use 🌿 ONLY at the very end, not in the middle",
|
|
480
|
+
"- CRITICAL RULE: You MUST reply in the SAME language the user writes in.",
|
|
481
|
+
" - User writes English → Reply ENTIRELY in English. Use casual English tone.",
|
|
482
|
+
" - User writes Korean → Reply in Korean 반말.",
|
|
483
|
+
"",
|
|
484
|
+
`## Tools`,
|
|
485
|
+
`You have ${this.tools.getDefinitions().length} tools available: ${this.tools.getDefinitions().map(t => t.name).join(", ")}.`,
|
|
486
|
+
"Use them proactively. Briefly mention what you're doing.",
|
|
487
|
+
"",
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
// Load WISPY.md context
|
|
491
|
+
const wispyMd = await this._loadWispyMd();
|
|
492
|
+
if (wispyMd) {
|
|
493
|
+
parts.push("## Project Context (WISPY.md)", wispyMd, "");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Load memories via MemoryManager
|
|
497
|
+
try {
|
|
498
|
+
const memories = await this.memory.getContextForPrompt(lastUserMessage);
|
|
499
|
+
if (memories) {
|
|
500
|
+
parts.push("## Persistent Memory", memories, "");
|
|
501
|
+
}
|
|
502
|
+
} catch {
|
|
503
|
+
// Fallback to old method
|
|
504
|
+
const memories = await this._loadMemories();
|
|
505
|
+
if (memories) {
|
|
506
|
+
parts.push("## Persistent Memory", memories, "");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return parts.join("\n");
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async _loadWispyMd() {
|
|
514
|
+
const paths = [
|
|
515
|
+
path.resolve("WISPY.md"),
|
|
516
|
+
path.resolve(".wispy", "WISPY.md"),
|
|
517
|
+
path.join(WISPY_DIR, "WISPY.md"),
|
|
518
|
+
];
|
|
519
|
+
for (const p of paths) {
|
|
520
|
+
try {
|
|
521
|
+
const content = await readFile(p, "utf8");
|
|
522
|
+
if (content) return content.slice(0, MAX_CONTEXT_CHARS);
|
|
523
|
+
} catch { /* not found */ }
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async _loadMemories() {
|
|
529
|
+
const types = ["user", "feedback", "project", "references"];
|
|
530
|
+
const sections = [];
|
|
531
|
+
for (const type of types) {
|
|
532
|
+
try {
|
|
533
|
+
const content = await readFile(path.join(MEMORY_DIR, `${type}.md`), "utf8");
|
|
534
|
+
if (content?.trim()) sections.push(`## ${type} memory\n${content.trim()}`);
|
|
535
|
+
} catch { /* not found */ }
|
|
536
|
+
}
|
|
537
|
+
return sections.length ? sections.join("\n\n") : null;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── Memory tools ─────────────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
_registerMemoryTools() {
|
|
543
|
+
const memoryTools = [
|
|
544
|
+
{
|
|
545
|
+
name: "memory_save",
|
|
546
|
+
description: "Save important information to persistent memory. Use this to remember facts, preferences, or information for future conversations.",
|
|
547
|
+
parameters: {
|
|
548
|
+
type: "object",
|
|
549
|
+
properties: {
|
|
550
|
+
key: { type: "string", description: "Memory key/filename (e.g., 'user', 'MEMORY', 'projects/myapp', 'daily/2025-01-01')" },
|
|
551
|
+
content: { type: "string", description: "Content to save" },
|
|
552
|
+
title: { type: "string", description: "Optional title for the memory file" },
|
|
553
|
+
},
|
|
554
|
+
required: ["key", "content"],
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: "memory_append",
|
|
559
|
+
description: "Append a new entry to an existing memory file without overwriting it.",
|
|
560
|
+
parameters: {
|
|
561
|
+
type: "object",
|
|
562
|
+
properties: {
|
|
563
|
+
key: { type: "string", description: "Memory key/filename" },
|
|
564
|
+
content: { type: "string", description: "Content to append" },
|
|
565
|
+
},
|
|
566
|
+
required: ["key", "content"],
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: "memory_search",
|
|
571
|
+
description: "Search across all memory files for information. Returns matching snippets.",
|
|
572
|
+
parameters: {
|
|
573
|
+
type: "object",
|
|
574
|
+
properties: {
|
|
575
|
+
query: { type: "string", description: "Search query" },
|
|
576
|
+
},
|
|
577
|
+
required: ["query"],
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
name: "memory_list",
|
|
582
|
+
description: "List all memory files with their keys and previews.",
|
|
583
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "memory_get",
|
|
587
|
+
description: "Get the full content of a specific memory file.",
|
|
588
|
+
parameters: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: {
|
|
591
|
+
key: { type: "string", description: "Memory key to retrieve" },
|
|
592
|
+
},
|
|
593
|
+
required: ["key"],
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: "memory_delete",
|
|
598
|
+
description: "Delete a memory file.",
|
|
599
|
+
parameters: {
|
|
600
|
+
type: "object",
|
|
601
|
+
properties: {
|
|
602
|
+
key: { type: "string", description: "Memory key to delete" },
|
|
603
|
+
},
|
|
604
|
+
required: ["key"],
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
|
|
609
|
+
for (const tool of memoryTools) {
|
|
610
|
+
this.tools._definitions.set(tool.name, tool);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async _toolMemorySave(args) {
|
|
615
|
+
try {
|
|
616
|
+
const result = await this.memory.save(args.key, args.content, { title: args.title });
|
|
617
|
+
return { success: true, key: args.key, message: `Saved to memory: ${args.key}` };
|
|
618
|
+
} catch (err) {
|
|
619
|
+
return { success: false, error: err.message };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async _toolMemoryAppend(args) {
|
|
624
|
+
try {
|
|
625
|
+
const result = await this.memory.append(args.key, args.content);
|
|
626
|
+
return { success: true, key: args.key, message: `Appended to memory: ${args.key}` };
|
|
627
|
+
} catch (err) {
|
|
628
|
+
return { success: false, error: err.message };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async _toolMemorySearch(args) {
|
|
633
|
+
try {
|
|
634
|
+
const results = await this.memory.search(args.query, { limit: 10 });
|
|
635
|
+
if (results.length === 0) {
|
|
636
|
+
return { success: true, results: [], message: "No memories found matching your query." };
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
success: true,
|
|
640
|
+
results: results.map(r => ({
|
|
641
|
+
key: r.key,
|
|
642
|
+
matchCount: r.matchCount,
|
|
643
|
+
snippets: r.snippets.map(s => `[line ${s.lineNumber}] ${s.text}`),
|
|
644
|
+
})),
|
|
645
|
+
};
|
|
646
|
+
} catch (err) {
|
|
647
|
+
return { success: false, error: err.message };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async _toolMemoryList() {
|
|
652
|
+
try {
|
|
653
|
+
const keys = await this.memory.list();
|
|
654
|
+
return {
|
|
655
|
+
success: true,
|
|
656
|
+
memories: keys.map(k => ({
|
|
657
|
+
key: k.key,
|
|
658
|
+
preview: k.preview,
|
|
659
|
+
size: k.size,
|
|
660
|
+
updatedAt: k.updatedAt,
|
|
661
|
+
})),
|
|
662
|
+
total: keys.length,
|
|
663
|
+
};
|
|
664
|
+
} catch (err) {
|
|
665
|
+
return { success: false, error: err.message };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
async _toolMemoryGet(args) {
|
|
670
|
+
try {
|
|
671
|
+
const mem = await this.memory.get(args.key);
|
|
672
|
+
if (!mem) return { success: false, error: `Memory "${args.key}" not found` };
|
|
673
|
+
return { success: true, key: args.key, content: mem.content };
|
|
674
|
+
} catch (err) {
|
|
675
|
+
return { success: false, error: err.message };
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async _toolMemoryDelete(args) {
|
|
680
|
+
try {
|
|
681
|
+
const result = await this.memory.delete(args.key);
|
|
682
|
+
return result;
|
|
683
|
+
} catch (err) {
|
|
684
|
+
return { success: false, error: err.message };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
|
689
|
+
|
|
690
|
+
destroy() {
|
|
691
|
+
try { this.mcpManager.disconnectAll(); } catch {}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Convenience: create and initialize a default engine
|
|
696
|
+
let _defaultEngine = null;
|
|
697
|
+
|
|
698
|
+
export async function getDefaultEngine(opts = {}) {
|
|
699
|
+
if (_defaultEngine) return _defaultEngine;
|
|
700
|
+
const engine = new WispyEngine(opts);
|
|
701
|
+
const result = await engine.init();
|
|
702
|
+
if (!result) return null;
|
|
703
|
+
_defaultEngine = engine;
|
|
704
|
+
return engine;
|
|
705
|
+
}
|