min-agent 0.1.4 → 0.1.6
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 +95 -194
- package/bin/min-agent.js +0 -0
- package/dist/agent.js +431 -28
- package/dist/cli.js +177 -27
- package/dist/clipboard.js +106 -0
- package/dist/code-mode.js +166 -0
- package/dist/compaction.js +243 -48
- package/dist/config.js +34 -7
- package/dist/context-window.js +185 -0
- package/dist/doom-loop.js +36 -0
- package/dist/instructions.js +42 -0
- package/dist/mcp.js +82 -28
- package/dist/output.js +15 -2
- package/dist/paste-handler.js +41 -0
- package/dist/serve.js +351 -3
- package/dist/sessions.js +13 -4
- package/dist/skills.js +15 -7
- package/dist/structured-output.js +29 -0
- package/dist/title-gen.js +48 -0
- package/dist/tools/bash.js +85 -74
- package/dist/tools/code_search.js +91 -0
- package/dist/tools/explore.js +104 -0
- package/dist/tools/index.js +10 -1
- package/dist/tools/question.js +53 -0
- package/dist/tools/read.js +14 -3
- package/dist/tools/task.js +98 -0
- package/dist/tools/todo.js +88 -0
- package/docs/API.md +219 -123
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -7,9 +7,11 @@ import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "
|
|
|
7
7
|
import { discoverSkills, getSkillsTool, getSkillsSystemPrompt, getSkills } from "./skills.js";
|
|
8
8
|
import { loadInstructions } from "./instructions.js";
|
|
9
9
|
import { getMemorySystemPrompt, getMemoryTools } from "./memory.js";
|
|
10
|
-
import { needsCompaction, compactMessages, estimateTokens } from "./compaction.js";
|
|
10
|
+
import { needsCompaction, compactMessages, estimateTokens, TokenTracker } from "./compaction.js";
|
|
11
11
|
import { loadPluginTools } from "./plugins.js";
|
|
12
12
|
import { MarkdownRenderer } from "./markdown.js";
|
|
13
|
+
import { DoomLoopDetector } from "./doom-loop.js";
|
|
14
|
+
import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
|
|
13
15
|
import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
|
|
14
16
|
import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
|
|
15
17
|
import readline from "readline";
|
|
@@ -51,7 +53,7 @@ function buildSystemPrompt(instructions) {
|
|
|
51
53
|
`Platform: ${process.platform}`,
|
|
52
54
|
`Date: ${new Date().toDateString()}`,
|
|
53
55
|
"",
|
|
54
|
-
|
|
56
|
+
`When the user asks to configure, install, or manage MCP servers, skills, rules, memory, or other min-agent features, use the read tool on the file at ${path.resolve(path.dirname(new URL(import.meta.url).pathname), "../README.md")} first, then follow what it says.`,
|
|
55
57
|
];
|
|
56
58
|
const skillsPrompt = getSkillsSystemPrompt();
|
|
57
59
|
if (skillsPrompt) {
|
|
@@ -109,7 +111,8 @@ export async function runAgent(message, modelId, imagePaths) {
|
|
|
109
111
|
console.log(`\x1b[36m> ${message}\x1b[0m\n`);
|
|
110
112
|
const content = await buildUserContent(message, imagePaths);
|
|
111
113
|
const messages = [{ role: "user", content }];
|
|
112
|
-
|
|
114
|
+
const tracker = new TokenTracker();
|
|
115
|
+
await runOnce(messages, instructions, modelId, undefined, undefined, tracker);
|
|
113
116
|
await shutdownMcp();
|
|
114
117
|
}
|
|
115
118
|
/** Interactive multi-turn chat session */
|
|
@@ -123,6 +126,7 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
123
126
|
printInitReady();
|
|
124
127
|
let messages = [];
|
|
125
128
|
let sessionId = resumeSessionId;
|
|
129
|
+
const tracker = new TokenTracker();
|
|
126
130
|
// Resume existing session
|
|
127
131
|
if (resumeSessionId) {
|
|
128
132
|
const { loadSession } = await import("./sessions.js");
|
|
@@ -144,7 +148,7 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
144
148
|
if (abortController) {
|
|
145
149
|
abortController.abort();
|
|
146
150
|
abortController = null;
|
|
147
|
-
console.log("\n\x1b[90m(
|
|
151
|
+
console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
|
|
148
152
|
rl.prompt();
|
|
149
153
|
}
|
|
150
154
|
else {
|
|
@@ -155,32 +159,166 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
155
159
|
});
|
|
156
160
|
console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,/exit 退出\x1b[0m\n");
|
|
157
161
|
rl.prompt();
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
// Multi-line paste detection: collect rapid successive lines
|
|
163
|
+
let pasteBuffer = [];
|
|
164
|
+
let pasteTimer = null;
|
|
165
|
+
const PASTE_DEBOUNCE_MS = 50;
|
|
166
|
+
const processInput = async (text) => {
|
|
167
|
+
const input = text.trim();
|
|
160
168
|
if (!input) {
|
|
161
169
|
rl.prompt();
|
|
162
|
-
|
|
170
|
+
return;
|
|
163
171
|
}
|
|
164
|
-
// Handle slash commands
|
|
165
172
|
if (input.startsWith("/")) {
|
|
166
|
-
const handled = await handleSlashCommand(input, messages, instructions, modelId, rl);
|
|
167
|
-
if (handled === "exit")
|
|
168
|
-
|
|
173
|
+
const handled = await handleSlashCommand(input, messages, instructions, modelId, rl, tracker);
|
|
174
|
+
if (handled === "exit") {
|
|
175
|
+
rl.close();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (handled === "paste") {
|
|
179
|
+
console.log();
|
|
180
|
+
abortController = new AbortController();
|
|
181
|
+
await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
|
|
182
|
+
abortController = null;
|
|
183
|
+
console.log();
|
|
184
|
+
}
|
|
169
185
|
rl.prompt();
|
|
170
|
-
|
|
186
|
+
return;
|
|
171
187
|
}
|
|
188
|
+
// Show paste feedback for large inputs
|
|
189
|
+
const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
|
|
190
|
+
const pasteResult = processPastedInput(input);
|
|
191
|
+
printPasteFeedback(pasteResult);
|
|
172
192
|
console.log();
|
|
173
|
-
messages.push({ role: "user", content:
|
|
193
|
+
messages.push({ role: "user", content: pasteResult.fullText });
|
|
174
194
|
abortController = new AbortController();
|
|
175
|
-
await runOnce(messages, instructions, modelId, abortController.signal);
|
|
195
|
+
await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
|
|
176
196
|
abortController = null;
|
|
177
197
|
console.log();
|
|
178
198
|
rl.prompt();
|
|
199
|
+
};
|
|
200
|
+
rl.on("line", (line) => {
|
|
201
|
+
pasteBuffer.push(line);
|
|
202
|
+
if (pasteTimer)
|
|
203
|
+
clearTimeout(pasteTimer);
|
|
204
|
+
pasteTimer = setTimeout(() => {
|
|
205
|
+
const combined = pasteBuffer.join("\n");
|
|
206
|
+
pasteBuffer = [];
|
|
207
|
+
pasteTimer = null;
|
|
208
|
+
processInput(combined);
|
|
209
|
+
}, PASTE_DEBOUNCE_MS);
|
|
210
|
+
});
|
|
211
|
+
// Wait for close
|
|
212
|
+
await new Promise((resolve) => rl.on("close", resolve));
|
|
213
|
+
// Auto-save session on exit with LLM-generated title
|
|
214
|
+
if (messages.length > 0) {
|
|
215
|
+
const { saveSessionWithTitle } = await import("./sessions.js");
|
|
216
|
+
const model = resolveModel(modelId);
|
|
217
|
+
sessionId = await saveSessionWithTitle(messages, model, sessionId);
|
|
218
|
+
console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
|
|
179
219
|
}
|
|
180
|
-
|
|
220
|
+
printDivider();
|
|
221
|
+
console.log("\x1b[90mBye!\x1b[0m");
|
|
222
|
+
await shutdownMcp();
|
|
223
|
+
}
|
|
224
|
+
/** AI Coding mode: project-aware interactive session */
|
|
225
|
+
export async function runCode(modelId, resumeSessionId) {
|
|
226
|
+
printHeader(modelId);
|
|
227
|
+
console.log("\x1b[90m Mode: code\x1b[0m");
|
|
228
|
+
printDivider();
|
|
229
|
+
// Scan project
|
|
230
|
+
console.log("\x1b[90m⟳ Scanning project...\x1b[0m");
|
|
231
|
+
const project = scanProject();
|
|
232
|
+
console.log(`\x1b[90m ${project.languages.join(", ") || "Unknown language"}${project.framework ? ` (${project.framework})` : ""} | ${project.isGitRepo ? `git:${project.branch}` : "no git"}\x1b[0m`);
|
|
233
|
+
await initMcp();
|
|
234
|
+
discoverSkills();
|
|
235
|
+
const instructions = await loadInstructions();
|
|
236
|
+
const codePrompt = buildCodeSystemPrompt(project, instructions);
|
|
237
|
+
console.log("\x1b[90m✓ Ready\x1b[0m");
|
|
238
|
+
let messages = [];
|
|
239
|
+
let sessionId = resumeSessionId;
|
|
240
|
+
const tracker = new TokenTracker();
|
|
241
|
+
if (resumeSessionId) {
|
|
242
|
+
const { loadSession } = await import("./sessions.js");
|
|
243
|
+
const session = loadSession(resumeSessionId);
|
|
244
|
+
if (session) {
|
|
245
|
+
messages = session.messages;
|
|
246
|
+
sessionId = resumeSessionId;
|
|
247
|
+
console.log(`\x1b[90m Resumed: ${session.meta.title} (${messages.length} msgs)\x1b[0m`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const rl = readline.createInterface({
|
|
251
|
+
input: process.stdin,
|
|
252
|
+
output: process.stdout,
|
|
253
|
+
prompt: "\x1b[32m❯ \x1b[0m",
|
|
254
|
+
});
|
|
255
|
+
let abortController = null;
|
|
256
|
+
process.on("SIGINT", () => {
|
|
257
|
+
if (abortController) {
|
|
258
|
+
abortController.abort();
|
|
259
|
+
abortController = null;
|
|
260
|
+
console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
|
|
261
|
+
rl.prompt();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.log();
|
|
265
|
+
rl.close();
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
console.log("\x1b[90m输入任务开始编码,/help 查看命令,Ctrl+C 中断\x1b[0m\n");
|
|
269
|
+
rl.prompt();
|
|
270
|
+
let pasteBuffer = [];
|
|
271
|
+
let pasteTimer = null;
|
|
272
|
+
const PASTE_DEBOUNCE_MS = 50;
|
|
273
|
+
const processCodeInput = async (text) => {
|
|
274
|
+
const input = text.trim();
|
|
275
|
+
if (!input) {
|
|
276
|
+
rl.prompt();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (input.startsWith("/")) {
|
|
280
|
+
const handled = await handleSlashCommand(input, messages, [codePrompt], modelId, rl, tracker);
|
|
281
|
+
if (handled === "exit") {
|
|
282
|
+
rl.close();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (handled === "paste") {
|
|
286
|
+
console.log();
|
|
287
|
+
abortController = new AbortController();
|
|
288
|
+
await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
|
|
289
|
+
abortController = null;
|
|
290
|
+
console.log();
|
|
291
|
+
}
|
|
292
|
+
rl.prompt();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
|
|
296
|
+
const pasteResult = processPastedInput(input);
|
|
297
|
+
printPasteFeedback(pasteResult);
|
|
298
|
+
console.log();
|
|
299
|
+
messages.push({ role: "user", content: pasteResult.fullText });
|
|
300
|
+
abortController = new AbortController();
|
|
301
|
+
await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
|
|
302
|
+
abortController = null;
|
|
303
|
+
console.log();
|
|
304
|
+
rl.prompt();
|
|
305
|
+
};
|
|
306
|
+
rl.on("line", (line) => {
|
|
307
|
+
pasteBuffer.push(line);
|
|
308
|
+
if (pasteTimer)
|
|
309
|
+
clearTimeout(pasteTimer);
|
|
310
|
+
pasteTimer = setTimeout(() => {
|
|
311
|
+
const combined = pasteBuffer.join("\n");
|
|
312
|
+
pasteBuffer = [];
|
|
313
|
+
pasteTimer = null;
|
|
314
|
+
processCodeInput(combined);
|
|
315
|
+
}, PASTE_DEBOUNCE_MS);
|
|
316
|
+
});
|
|
317
|
+
await new Promise((resolve) => rl.on("close", resolve));
|
|
181
318
|
if (messages.length > 0) {
|
|
182
|
-
const {
|
|
183
|
-
|
|
319
|
+
const { saveSessionWithTitle } = await import("./sessions.js");
|
|
320
|
+
const model = resolveModel(modelId);
|
|
321
|
+
sessionId = await saveSessionWithTitle(messages, model, sessionId);
|
|
184
322
|
console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
|
|
185
323
|
}
|
|
186
324
|
rl.close();
|
|
@@ -188,13 +326,216 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
188
326
|
console.log("\x1b[90mBye!\x1b[0m");
|
|
189
327
|
await shutdownMcp();
|
|
190
328
|
}
|
|
191
|
-
|
|
329
|
+
/** runOnce variant that accepts a pre-built system prompt (for code mode) */
|
|
330
|
+
export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker) {
|
|
331
|
+
const model = resolveModel(modelId);
|
|
332
|
+
const api = !!callbacks;
|
|
333
|
+
if (needsCompaction(messages, tracker)) {
|
|
334
|
+
console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
|
|
335
|
+
const result = await compactMessages(messages, model);
|
|
336
|
+
if (result.compacted) {
|
|
337
|
+
messages.length = 0;
|
|
338
|
+
messages.push(...result.messages);
|
|
339
|
+
if (result.shouldContinue) {
|
|
340
|
+
messages.push({ role: "user", content: "Continue with your task." });
|
|
341
|
+
}
|
|
342
|
+
if (tracker)
|
|
343
|
+
tracker.resetContext();
|
|
344
|
+
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const builtinTools = createTools();
|
|
348
|
+
const mcpTools = getMcpTools();
|
|
349
|
+
const memoryTools = getMemoryTools();
|
|
350
|
+
const pluginTools = await loadPluginTools();
|
|
351
|
+
const { createTaskTool } = await import("./tools/task.js");
|
|
352
|
+
const { createExploreTool } = await import("./tools/explore.js");
|
|
353
|
+
const skills = getSkills();
|
|
354
|
+
const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
|
|
355
|
+
for (const [id, t] of Object.entries(mcpTools))
|
|
356
|
+
allTools[id] = t;
|
|
357
|
+
if (skills.length > 0)
|
|
358
|
+
allTools["skill"] = getSkillsTool();
|
|
359
|
+
allTools["task"] = createTaskTool(modelId);
|
|
360
|
+
allTools["explore"] = createExploreTool(modelId);
|
|
361
|
+
let stepCount = 0;
|
|
362
|
+
let hasError = false;
|
|
363
|
+
const doomLoop = new DoomLoopDetector();
|
|
364
|
+
const result = streamText({
|
|
365
|
+
model,
|
|
366
|
+
system: systemPrompt,
|
|
367
|
+
messages,
|
|
368
|
+
tools: allTools,
|
|
369
|
+
stopWhen: stepCountIs(MAX_STEPS),
|
|
370
|
+
maxRetries: 3,
|
|
371
|
+
abortSignal,
|
|
372
|
+
onStepFinish() { stepCount++; },
|
|
373
|
+
onError() { },
|
|
374
|
+
});
|
|
375
|
+
Promise.resolve(result.usage).catch(() => { });
|
|
376
|
+
let rawText = "";
|
|
377
|
+
let assistantText = "";
|
|
378
|
+
const md = new MarkdownRenderer();
|
|
379
|
+
const thinkingSplit = new ThinkingBodySplitter();
|
|
380
|
+
try {
|
|
381
|
+
for await (const event of result.fullStream) {
|
|
382
|
+
switch (event.type) {
|
|
383
|
+
case "text-delta": {
|
|
384
|
+
const { display, thinking } = thinkingSplit.feed(event.text);
|
|
385
|
+
if (thinking) {
|
|
386
|
+
if (callbacks?.onThinkingDelta)
|
|
387
|
+
callbacks.onThinkingDelta(thinking);
|
|
388
|
+
else
|
|
389
|
+
writeThinkingDelta(thinking);
|
|
390
|
+
}
|
|
391
|
+
if (display) {
|
|
392
|
+
rawText += display;
|
|
393
|
+
assistantText += display;
|
|
394
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
395
|
+
callbacks.onAssistantDisplayDelta(display);
|
|
396
|
+
else {
|
|
397
|
+
const formatted = md.write(display);
|
|
398
|
+
if (formatted)
|
|
399
|
+
process.stdout.write(formatted);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case "tool-call": {
|
|
405
|
+
if (doomLoop.record(event.toolName, event.input)) {
|
|
406
|
+
const warn = `Doom loop detected: "${event.toolName}" — breaking.`;
|
|
407
|
+
if (callbacks?.onStreamError)
|
|
408
|
+
callbacks.onStreamError(warn);
|
|
409
|
+
else
|
|
410
|
+
console.log(`\n\x1b[33m⚠ ${warn}\x1b[0m`);
|
|
411
|
+
hasError = true;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
const flush = thinkingSplit.flush();
|
|
415
|
+
if (flush.thinking) {
|
|
416
|
+
if (callbacks?.onThinkingDelta)
|
|
417
|
+
callbacks.onThinkingDelta(flush.thinking);
|
|
418
|
+
else
|
|
419
|
+
writeThinkingDelta(flush.thinking);
|
|
420
|
+
}
|
|
421
|
+
if (flush.display) {
|
|
422
|
+
rawText += flush.display;
|
|
423
|
+
assistantText += flush.display;
|
|
424
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
425
|
+
callbacks.onAssistantDisplayDelta(flush.display);
|
|
426
|
+
else {
|
|
427
|
+
const extra = md.write(flush.display);
|
|
428
|
+
if (extra)
|
|
429
|
+
process.stdout.write(extra);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!api) {
|
|
433
|
+
const flushed = md.flush();
|
|
434
|
+
if (flushed)
|
|
435
|
+
process.stdout.write(flushed);
|
|
436
|
+
if (rawText.trim())
|
|
437
|
+
console.log();
|
|
438
|
+
}
|
|
439
|
+
rawText = "";
|
|
440
|
+
if (callbacks?.onToolCall)
|
|
441
|
+
callbacks.onToolCall(event.toolName, event.input);
|
|
442
|
+
else
|
|
443
|
+
printToolCall(event.toolName, event.input);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
case "tool-result":
|
|
447
|
+
if (callbacks?.onToolResult)
|
|
448
|
+
callbacks.onToolResult(event.toolName, event.output);
|
|
449
|
+
else
|
|
450
|
+
printToolResult(event.toolName, event.output);
|
|
451
|
+
break;
|
|
452
|
+
case "error":
|
|
453
|
+
hasError = true;
|
|
454
|
+
if (callbacks?.onStreamError)
|
|
455
|
+
callbacks.onStreamError(String(event.error));
|
|
456
|
+
else
|
|
457
|
+
console.error(`\x1b[31mError: ${event.error}\x1b[0m`);
|
|
458
|
+
break;
|
|
459
|
+
case "finish":
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const end = thinkingSplit.flush();
|
|
464
|
+
if (end.thinking) {
|
|
465
|
+
if (callbacks?.onThinkingDelta)
|
|
466
|
+
callbacks.onThinkingDelta(end.thinking);
|
|
467
|
+
else
|
|
468
|
+
writeThinkingDelta(end.thinking);
|
|
469
|
+
}
|
|
470
|
+
if (end.display) {
|
|
471
|
+
assistantText += end.display;
|
|
472
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
473
|
+
callbacks.onAssistantDisplayDelta(end.display);
|
|
474
|
+
else {
|
|
475
|
+
const tail = md.write(end.display);
|
|
476
|
+
if (tail)
|
|
477
|
+
process.stdout.write(tail);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (!api) {
|
|
481
|
+
const remaining = md.flush();
|
|
482
|
+
if (remaining)
|
|
483
|
+
process.stdout.write(remaining);
|
|
484
|
+
if (rawText.trim())
|
|
485
|
+
console.log();
|
|
486
|
+
}
|
|
487
|
+
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
488
|
+
if (cleaned.trim())
|
|
489
|
+
messages.push({ role: "assistant", content: cleaned });
|
|
490
|
+
let usage;
|
|
491
|
+
try {
|
|
492
|
+
usage = await result.usage;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
usage = undefined;
|
|
496
|
+
}
|
|
497
|
+
if (usage && tracker)
|
|
498
|
+
tracker.update(usage);
|
|
499
|
+
if (callbacks?.onRunFinish) {
|
|
500
|
+
callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
if (!hasError && usage) {
|
|
504
|
+
printDivider();
|
|
505
|
+
const { getContextWindow } = await import("./context-window.js");
|
|
506
|
+
const ctxWindow = await getContextWindow(modelId);
|
|
507
|
+
printDone(stepCount, usage, ctxWindow);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
printDivider();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
if (err.name === "AbortError" || abortSignal?.aborted) {
|
|
516
|
+
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
517
|
+
if (cleaned.trim())
|
|
518
|
+
messages.push({ role: "assistant", content: cleaned });
|
|
519
|
+
if (callbacks?.onRunFinish)
|
|
520
|
+
callbacks.onRunFinish({ stepCount, usage: undefined, hasError: false, aborted: true });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (callbacks?.onStreamError)
|
|
524
|
+
callbacks.onStreamError(err.message);
|
|
525
|
+
else {
|
|
526
|
+
printDivider();
|
|
527
|
+
console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function handleSlashCommand(input, messages, instructions, modelId, rl, tracker) {
|
|
192
532
|
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|
|
193
533
|
const arg = rest.join(" ");
|
|
194
534
|
switch (cmd) {
|
|
195
535
|
case "exit":
|
|
196
536
|
case "quit":
|
|
197
537
|
case "q":
|
|
538
|
+
console.log("\x1b[90m⟳ Exiting...\x1b[0m");
|
|
198
539
|
return "exit";
|
|
199
540
|
case "clear":
|
|
200
541
|
messages.length = 0;
|
|
@@ -207,9 +548,11 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
|
207
548
|
else {
|
|
208
549
|
console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
|
|
209
550
|
const model = resolveModel(modelId);
|
|
210
|
-
const result = await compactMessages(messages, model, { keepRecentTurns: 2 });
|
|
551
|
+
const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
|
|
211
552
|
messages.length = 0;
|
|
212
553
|
messages.push(...result.messages);
|
|
554
|
+
if (tracker)
|
|
555
|
+
tracker.resetContext();
|
|
213
556
|
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
214
557
|
}
|
|
215
558
|
return "handled";
|
|
@@ -275,12 +618,11 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
|
275
618
|
discoverSkills();
|
|
276
619
|
const skills = getSkills();
|
|
277
620
|
if (skills.length === 0) {
|
|
278
|
-
console.log("\x1b[90m No skills
|
|
621
|
+
console.log("\x1b[90m No skills available\x1b[0m");
|
|
279
622
|
}
|
|
280
623
|
else {
|
|
281
|
-
console.log(`\x1b[90m Available skills (${skills.length}):\x1b[0m`);
|
|
282
624
|
for (const skill of skills) {
|
|
283
|
-
console.log(`\x1b[90m
|
|
625
|
+
console.log(`\x1b[90m ${skill.name} enabled\x1b[0m`);
|
|
284
626
|
}
|
|
285
627
|
}
|
|
286
628
|
return "handled";
|
|
@@ -305,10 +647,41 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
|
305
647
|
}
|
|
306
648
|
return "handled";
|
|
307
649
|
}
|
|
308
|
-
case "tokens":
|
|
309
|
-
|
|
650
|
+
case "tokens": {
|
|
651
|
+
if (tracker && tracker.lastInputTokens > 0) {
|
|
652
|
+
const { getContextWindow } = await import("./context-window.js");
|
|
653
|
+
const ctxWindow = await getContextWindow(modelId);
|
|
654
|
+
const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
|
|
655
|
+
console.log(`\x1b[90m Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)\x1b[0m`);
|
|
656
|
+
console.log(`\x1b[90m Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out\x1b[0m`);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
|
|
660
|
+
}
|
|
310
661
|
console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
|
|
311
662
|
return "handled";
|
|
663
|
+
}
|
|
664
|
+
case "path":
|
|
665
|
+
case "pwd":
|
|
666
|
+
console.log(`\x1b[90m ${process.cwd()}\x1b[0m`);
|
|
667
|
+
return "handled";
|
|
668
|
+
case "paste": {
|
|
669
|
+
const { getClipboardImage } = await import("./clipboard.js");
|
|
670
|
+
const img = getClipboardImage();
|
|
671
|
+
if (!img) {
|
|
672
|
+
console.log("\x1b[90m No image found in clipboard\x1b[0m");
|
|
673
|
+
return "handled";
|
|
674
|
+
}
|
|
675
|
+
console.log(`\x1b[90m 📎 Clipboard image (${(img.data.length / 1024).toFixed(1)} KB)\x1b[0m`);
|
|
676
|
+
const text = arg || "What's in this image?";
|
|
677
|
+
const content = [
|
|
678
|
+
{ type: "text", text },
|
|
679
|
+
{ type: "image", image: img.data, mimeType: img.mimeType },
|
|
680
|
+
];
|
|
681
|
+
messages.push({ role: "user", content });
|
|
682
|
+
// Return a special signal to trigger runOnce
|
|
683
|
+
return "paste";
|
|
684
|
+
}
|
|
312
685
|
case "help":
|
|
313
686
|
console.log(`\x1b[90m Slash commands:
|
|
314
687
|
/clear Clear conversation history
|
|
@@ -319,6 +692,8 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
|
319
692
|
/skills List discovered skills
|
|
320
693
|
/mcp List MCP servers and connection status
|
|
321
694
|
/tokens Show estimated token usage
|
|
695
|
+
/path Show current working directory
|
|
696
|
+
/paste [text] Paste clipboard image + optional prompt
|
|
322
697
|
/help Show this help
|
|
323
698
|
/exit Exit the chat\x1b[0m`);
|
|
324
699
|
return "handled";
|
|
@@ -327,11 +702,11 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
|
327
702
|
return "handled";
|
|
328
703
|
}
|
|
329
704
|
}
|
|
330
|
-
export async function runOnce(messages, instructions, modelId, abortSignal, callbacks) {
|
|
705
|
+
export async function runOnce(messages, instructions, modelId, abortSignal, callbacks, tracker) {
|
|
331
706
|
const model = resolveModel(modelId);
|
|
332
707
|
const api = !!callbacks;
|
|
333
708
|
// Auto-compact if context is getting too large
|
|
334
|
-
if (needsCompaction(messages)) {
|
|
709
|
+
if (needsCompaction(messages, tracker)) {
|
|
335
710
|
if (api) {
|
|
336
711
|
callbacks.onCompaction?.("compacting_start");
|
|
337
712
|
}
|
|
@@ -342,6 +717,15 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
342
717
|
if (result.compacted) {
|
|
343
718
|
messages.length = 0;
|
|
344
719
|
messages.push(...result.messages);
|
|
720
|
+
// Auto-continue: inject a message so the agent keeps working
|
|
721
|
+
if (result.shouldContinue) {
|
|
722
|
+
messages.push({
|
|
723
|
+
role: "user",
|
|
724
|
+
content: "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.",
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
if (tracker)
|
|
728
|
+
tracker.resetContext();
|
|
345
729
|
if (api) {
|
|
346
730
|
callbacks.onCompaction?.(`compacted_ok estimated_tokens=${estimateTokens(messages)}`);
|
|
347
731
|
}
|
|
@@ -350,11 +734,12 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
350
734
|
}
|
|
351
735
|
}
|
|
352
736
|
}
|
|
353
|
-
// Merge all tools: builtin + MCP + skill + memory + plugins
|
|
737
|
+
// Merge all tools: builtin + MCP + skill + memory + plugins + task
|
|
354
738
|
const builtinTools = createTools();
|
|
355
739
|
const mcpTools = getMcpTools();
|
|
356
740
|
const memoryTools = getMemoryTools();
|
|
357
741
|
const pluginTools = await loadPluginTools();
|
|
742
|
+
const { createTaskTool } = await import("./tools/task.js");
|
|
358
743
|
const skills = getSkills();
|
|
359
744
|
const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
|
|
360
745
|
for (const [id, t] of Object.entries(mcpTools)) {
|
|
@@ -363,8 +748,10 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
363
748
|
if (skills.length > 0) {
|
|
364
749
|
allTools["skill"] = getSkillsTool();
|
|
365
750
|
}
|
|
751
|
+
allTools["task"] = createTaskTool(modelId);
|
|
366
752
|
let stepCount = 0;
|
|
367
753
|
let hasError = false;
|
|
754
|
+
const doomLoop = new DoomLoopDetector();
|
|
368
755
|
const result = streamText({
|
|
369
756
|
model,
|
|
370
757
|
system: buildSystemPrompt(instructions),
|
|
@@ -411,6 +798,16 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
411
798
|
break;
|
|
412
799
|
}
|
|
413
800
|
case "tool-call": {
|
|
801
|
+
// Doom loop detection
|
|
802
|
+
if (doomLoop.record(event.toolName, event.input)) {
|
|
803
|
+
const warning = `\x1b[33m⚠ Doom loop detected: "${event.toolName}" called ${3} times with same args. Breaking loop.\x1b[0m`;
|
|
804
|
+
if (callbacks?.onStreamError)
|
|
805
|
+
callbacks.onStreamError(warning);
|
|
806
|
+
else
|
|
807
|
+
console.log(`\n${warning}`);
|
|
808
|
+
hasError = true;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
414
811
|
const splitFlush = thinkingSplit.flush();
|
|
415
812
|
emitThinking(splitFlush.thinking);
|
|
416
813
|
if (splitFlush.display) {
|
|
@@ -492,6 +889,10 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
492
889
|
catch {
|
|
493
890
|
usage = undefined;
|
|
494
891
|
}
|
|
892
|
+
// Update token tracker with real usage from API
|
|
893
|
+
if (usage && tracker) {
|
|
894
|
+
tracker.update(usage);
|
|
895
|
+
}
|
|
495
896
|
if (callbacks?.onRunFinish) {
|
|
496
897
|
callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
|
|
497
898
|
}
|
|
@@ -501,7 +902,9 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
501
902
|
return;
|
|
502
903
|
}
|
|
503
904
|
printDivider();
|
|
504
|
-
|
|
905
|
+
const { getContextWindow } = await import("./context-window.js");
|
|
906
|
+
const ctxWindow = await getContextWindow(modelId);
|
|
907
|
+
printDone(stepCount, usage, ctxWindow);
|
|
505
908
|
}
|
|
506
909
|
}
|
|
507
910
|
catch (err) {
|