min-agent 0.1.0 → 0.1.1
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 +6 -0
- package/bin/min-agent.js +2 -2
- package/dist/agent.d.ts +23 -0
- package/dist/agent.js +566 -0
- package/dist/assistant-stream.d.ts +23 -0
- package/dist/assistant-stream.js +114 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +471 -0
- package/dist/compaction.d.ts +14 -0
- package/dist/compaction.js +99 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +142 -0
- package/dist/confirm.d.ts +6 -0
- package/dist/confirm.js +37 -0
- package/dist/instructions.d.ts +1 -0
- package/dist/instructions.js +115 -0
- package/dist/markdown.d.ts +15 -0
- package/dist/markdown.js +130 -0
- package/dist/mcp.d.ts +61 -0
- package/dist/mcp.js +237 -0
- package/dist/memory.d.ts +31 -0
- package/dist/memory.js +131 -0
- package/dist/output.d.ts +6 -0
- package/dist/output.js +52 -0
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +66 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +41 -0
- package/dist/serve.d.ts +9 -0
- package/dist/serve.js +351 -0
- package/dist/sessions.d.ts +17 -0
- package/dist/sessions.js +74 -0
- package/dist/skills.d.ts +12 -0
- package/dist/skills.js +127 -0
- package/dist/tool-output.d.ts +31 -0
- package/dist/tool-output.js +119 -0
- package/dist/tools/bash.d.ts +6 -0
- package/dist/tools/bash.js +93 -0
- package/dist/tools/edit.d.ts +7 -0
- package/dist/tools/edit.js +51 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.js +36 -0
- package/dist/tools/grep.d.ts +7 -0
- package/dist/tools/grep.js +35 -0
- package/dist/tools/index.d.ts +37 -0
- package/dist/tools/index.js +20 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.js +36 -0
- package/dist/tools/web_fetch.d.ts +6 -0
- package/dist/tools/web_fetch.js +83 -0
- package/dist/tools/web_search.d.ts +6 -0
- package/dist/tools/web_search.js +40 -0
- package/dist/tools/write.d.ts +6 -0
- package/dist/tools/write.js +32 -0
- package/package.json +4 -5
- package/src/agent.ts +0 -609
- package/src/assistant-stream.ts +0 -128
- package/src/cli.ts +0 -494
- package/src/compaction.ts +0 -119
- package/src/config.ts +0 -172
- package/src/confirm.ts +0 -42
- package/src/instructions.ts +0 -123
- package/src/markdown.ts +0 -140
- package/src/mcp.ts +0 -300
- package/src/memory.ts +0 -164
- package/src/output.ts +0 -58
- package/src/plugins.ts +0 -94
- package/src/provider.ts +0 -50
- package/src/serve.ts +0 -400
- package/src/sessions.ts +0 -94
- package/src/skills.ts +0 -146
- package/src/tool-output.ts +0 -146
- package/src/tools/bash.ts +0 -108
- package/src/tools/edit.ts +0 -65
- package/src/tools/glob.ts +0 -37
- package/src/tools/grep.ts +0 -37
- package/src/tools/index.ts +0 -21
- package/src/tools/read.ts +0 -38
- package/src/tools/web_fetch.ts +0 -87
- package/src/tools/web_search.ts +0 -42
- package/src/tools/write.ts +0 -36
- package/tsconfig.json +0 -15
package/README.md
CHANGED
|
@@ -5,10 +5,16 @@
|
|
|
5
5
|
|
|
6
6
|
## 安装
|
|
7
7
|
|
|
8
|
+
需要本机已安装 **Node.js 20+**(LTS)。`npm install -g` 与 `npx -y min-agent` 使用发布包内的 **编译产物**,**不要求安装 Bun**。
|
|
9
|
+
|
|
8
10
|
```bash
|
|
9
11
|
npm install -g min-agent
|
|
12
|
+
# 或
|
|
13
|
+
npx -y min-agent --help
|
|
10
14
|
```
|
|
11
15
|
|
|
16
|
+
从本仓库开发时:先执行 `npm install` 与 `npm run build`,再运行 `node bin/min-agent.js …`(或继续用 `bun run src/cli.ts` 做开发)。
|
|
17
|
+
|
|
12
18
|
## 快速开始
|
|
13
19
|
|
|
14
20
|
```bash
|
package/bin/min-agent.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
import "../
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../dist/cli.js"
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ModelMessage, type LanguageModelUsage } from "ai";
|
|
2
|
+
/** Build user message content, optionally with images */
|
|
3
|
+
export declare function buildUserContent(message: string, imagePaths?: string[]): Promise<any>;
|
|
4
|
+
/** Single-shot: send one message, get response, exit */
|
|
5
|
+
export declare function runAgent(message: string, modelId?: string, imagePaths?: string[]): Promise<void>;
|
|
6
|
+
/** Interactive multi-turn chat session */
|
|
7
|
+
export declare function runChat(modelId?: string, resumeSessionId?: string): Promise<void>;
|
|
8
|
+
/** When set, `runOnce` does not write to TTY; use for HTTP / programmatic callers. */
|
|
9
|
+
export interface RunOnceCallbacks {
|
|
10
|
+
onAssistantDisplayDelta?: (delta: string) => void;
|
|
11
|
+
onThinkingDelta?: (delta: string) => void;
|
|
12
|
+
onToolCall?: (toolName: string, input: unknown) => void;
|
|
13
|
+
onToolResult?: (toolName: string, output: unknown) => void;
|
|
14
|
+
onCompaction?: (line: string) => void;
|
|
15
|
+
onStreamError?: (message: string) => void;
|
|
16
|
+
onRunFinish?: (info: {
|
|
17
|
+
stepCount: number;
|
|
18
|
+
usage: LanguageModelUsage | undefined;
|
|
19
|
+
hasError: boolean;
|
|
20
|
+
aborted: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function runOnce(messages: ModelMessage[], instructions: string[], modelId?: string, abortSignal?: AbortSignal, callbacks?: RunOnceCallbacks): Promise<void>;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { streamText, stepCountIs } from "ai";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { resolveModel } from "./provider.js";
|
|
5
|
+
import { createTools } from "./tools/index.js";
|
|
6
|
+
import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "./mcp.js";
|
|
7
|
+
import { discoverSkills, getSkillsTool, getSkillsSystemPrompt, getSkills } from "./skills.js";
|
|
8
|
+
import { loadInstructions } from "./instructions.js";
|
|
9
|
+
import { getMemorySystemPrompt, getMemoryTools } from "./memory.js";
|
|
10
|
+
import { needsCompaction, compactMessages, estimateTokens } from "./compaction.js";
|
|
11
|
+
import { loadPluginTools } from "./plugins.js";
|
|
12
|
+
import { MarkdownRenderer } from "./markdown.js";
|
|
13
|
+
import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
|
|
14
|
+
import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
|
|
15
|
+
import readline from "readline";
|
|
16
|
+
const MAX_STEPS = 30;
|
|
17
|
+
function dimStyle() {
|
|
18
|
+
if ("NO_COLOR" in process.env)
|
|
19
|
+
return { dim: "", reset: "" };
|
|
20
|
+
return { dim: "\x1b[90m", reset: "\x1b[0m" };
|
|
21
|
+
}
|
|
22
|
+
/** Shown immediately so the terminal does not look frozen while MCP / rules load. */
|
|
23
|
+
function printInitLoading() {
|
|
24
|
+
const { dim, reset } = dimStyle();
|
|
25
|
+
console.log(`${dim}⟳ 正在初始化(MCP、技能、规则)…${reset}`);
|
|
26
|
+
}
|
|
27
|
+
function printInitReady() {
|
|
28
|
+
const { dim, reset } = dimStyle();
|
|
29
|
+
console.log(`${dim}✓ 就绪${reset}`);
|
|
30
|
+
}
|
|
31
|
+
/** Stream thinking to stderr (dim). Set MIN_AGENT_SHOW_THINKING=0 to hide. */
|
|
32
|
+
function writeThinkingDelta(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return;
|
|
35
|
+
if (process.env.MIN_AGENT_SHOW_THINKING === "0" || process.env.MIN_AGENT_SHOW_THINKING === "false")
|
|
36
|
+
return;
|
|
37
|
+
const noColor = "NO_COLOR" in process.env;
|
|
38
|
+
const dim = noColor ? "" : "\x1b[2m";
|
|
39
|
+
const reset = noColor ? "" : "\x1b[0m";
|
|
40
|
+
process.stderr.write(`${dim}${text}${reset}`);
|
|
41
|
+
}
|
|
42
|
+
function buildSystemPrompt(instructions) {
|
|
43
|
+
const parts = [
|
|
44
|
+
`You are a helpful coding agent. You can read files, write files, run shell commands, search the web, and search the codebase to help the user with software engineering tasks.`,
|
|
45
|
+
"",
|
|
46
|
+
"Be concise and direct. When you run a command, briefly explain why.",
|
|
47
|
+
"Use the available tools to complete tasks. When multiple independent operations are needed, call tools in parallel.",
|
|
48
|
+
"When the user asks about current events, news, or anything requiring up-to-date information, use the web_search tool.",
|
|
49
|
+
"",
|
|
50
|
+
`Working directory: ${process.cwd()}`,
|
|
51
|
+
`Platform: ${process.platform}`,
|
|
52
|
+
`Date: ${new Date().toDateString()}`,
|
|
53
|
+
"",
|
|
54
|
+
"min-agent: when the user asks to configure or install this agent (MCP, skills, rules, CLI, HTTP API, etc.), use the **read** tool on `README.md` in the working directory first, then follow what it says.",
|
|
55
|
+
];
|
|
56
|
+
const skillsPrompt = getSkillsSystemPrompt();
|
|
57
|
+
if (skillsPrompt) {
|
|
58
|
+
parts.push("", skillsPrompt);
|
|
59
|
+
}
|
|
60
|
+
const memoryPrompt = getMemorySystemPrompt();
|
|
61
|
+
if (memoryPrompt) {
|
|
62
|
+
parts.push("", memoryPrompt);
|
|
63
|
+
}
|
|
64
|
+
if (instructions.length > 0) {
|
|
65
|
+
parts.push("", "# User Instructions", "");
|
|
66
|
+
parts.push(...instructions);
|
|
67
|
+
}
|
|
68
|
+
return parts.join("\n");
|
|
69
|
+
}
|
|
70
|
+
/** Build user message content, optionally with images */
|
|
71
|
+
export async function buildUserContent(message, imagePaths) {
|
|
72
|
+
if (!imagePaths || imagePaths.length === 0)
|
|
73
|
+
return message;
|
|
74
|
+
const parts = [{ type: "text", text: message }];
|
|
75
|
+
for (const imgPath of imagePaths) {
|
|
76
|
+
const resolved = path.resolve(process.cwd(), imgPath);
|
|
77
|
+
if (!existsSync(resolved)) {
|
|
78
|
+
console.error(`\x1b[33m Warning: Image not found: ${imgPath}\x1b[0m`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const data = readFileSync(resolved);
|
|
82
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
83
|
+
const mimeMap = {
|
|
84
|
+
".png": "image/png",
|
|
85
|
+
".jpg": "image/jpeg",
|
|
86
|
+
".jpeg": "image/jpeg",
|
|
87
|
+
".gif": "image/gif",
|
|
88
|
+
".webp": "image/webp",
|
|
89
|
+
};
|
|
90
|
+
const mimeType = mimeMap[ext] ?? "image/png";
|
|
91
|
+
parts.push({
|
|
92
|
+
type: "image",
|
|
93
|
+
image: data,
|
|
94
|
+
mimeType,
|
|
95
|
+
});
|
|
96
|
+
console.log(`\x1b[90m 📎 ${imgPath}\x1b[0m`);
|
|
97
|
+
}
|
|
98
|
+
return parts;
|
|
99
|
+
}
|
|
100
|
+
/** Single-shot: send one message, get response, exit */
|
|
101
|
+
export async function runAgent(message, modelId, imagePaths) {
|
|
102
|
+
printHeader(modelId);
|
|
103
|
+
printDivider();
|
|
104
|
+
printInitLoading();
|
|
105
|
+
await initMcp();
|
|
106
|
+
discoverSkills();
|
|
107
|
+
const instructions = await loadInstructions();
|
|
108
|
+
printInitReady();
|
|
109
|
+
console.log(`\x1b[36m> ${message}\x1b[0m\n`);
|
|
110
|
+
const content = await buildUserContent(message, imagePaths);
|
|
111
|
+
const messages = [{ role: "user", content }];
|
|
112
|
+
await runOnce(messages, instructions, modelId);
|
|
113
|
+
await shutdownMcp();
|
|
114
|
+
}
|
|
115
|
+
/** Interactive multi-turn chat session */
|
|
116
|
+
export async function runChat(modelId, resumeSessionId) {
|
|
117
|
+
printHeader(modelId);
|
|
118
|
+
printDivider();
|
|
119
|
+
printInitLoading();
|
|
120
|
+
await initMcp();
|
|
121
|
+
discoverSkills();
|
|
122
|
+
const instructions = await loadInstructions();
|
|
123
|
+
printInitReady();
|
|
124
|
+
let messages = [];
|
|
125
|
+
let sessionId = resumeSessionId;
|
|
126
|
+
// Resume existing session
|
|
127
|
+
if (resumeSessionId) {
|
|
128
|
+
const { loadSession } = await import("./sessions.js");
|
|
129
|
+
const session = loadSession(resumeSessionId);
|
|
130
|
+
if (session) {
|
|
131
|
+
messages = session.messages;
|
|
132
|
+
sessionId = resumeSessionId;
|
|
133
|
+
console.log(`\x1b[90m Resumed session: ${session.meta.title} (${messages.length} messages)\x1b[0m`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const rl = readline.createInterface({
|
|
137
|
+
input: process.stdin,
|
|
138
|
+
output: process.stdout,
|
|
139
|
+
prompt: "\x1b[36m> \x1b[0m",
|
|
140
|
+
});
|
|
141
|
+
// Handle Ctrl+C: abort current generation, don't exit
|
|
142
|
+
let abortController = null;
|
|
143
|
+
process.on("SIGINT", () => {
|
|
144
|
+
if (abortController) {
|
|
145
|
+
abortController.abort();
|
|
146
|
+
abortController = null;
|
|
147
|
+
console.log("\n\x1b[90m(interrupted)\x1b[0m\n");
|
|
148
|
+
rl.prompt();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// No active generation, exit
|
|
152
|
+
console.log();
|
|
153
|
+
rl.close();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,/exit 退出\x1b[0m\n");
|
|
157
|
+
rl.prompt();
|
|
158
|
+
for await (const line of rl) {
|
|
159
|
+
const input = line.trim();
|
|
160
|
+
if (!input) {
|
|
161
|
+
rl.prompt();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// Handle slash commands
|
|
165
|
+
if (input.startsWith("/")) {
|
|
166
|
+
const handled = await handleSlashCommand(input, messages, instructions, modelId, rl);
|
|
167
|
+
if (handled === "exit")
|
|
168
|
+
break;
|
|
169
|
+
rl.prompt();
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
console.log();
|
|
173
|
+
messages.push({ role: "user", content: input });
|
|
174
|
+
abortController = new AbortController();
|
|
175
|
+
await runOnce(messages, instructions, modelId, abortController.signal);
|
|
176
|
+
abortController = null;
|
|
177
|
+
console.log();
|
|
178
|
+
rl.prompt();
|
|
179
|
+
}
|
|
180
|
+
// Auto-save session on exit
|
|
181
|
+
if (messages.length > 0) {
|
|
182
|
+
const { saveSession } = await import("./sessions.js");
|
|
183
|
+
sessionId = saveSession(messages, sessionId);
|
|
184
|
+
console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
|
|
185
|
+
}
|
|
186
|
+
rl.close();
|
|
187
|
+
printDivider();
|
|
188
|
+
console.log("\x1b[90mBye!\x1b[0m");
|
|
189
|
+
await shutdownMcp();
|
|
190
|
+
}
|
|
191
|
+
async function handleSlashCommand(input, messages, instructions, modelId, rl) {
|
|
192
|
+
const [cmd, ...rest] = input.slice(1).split(/\s+/);
|
|
193
|
+
const arg = rest.join(" ");
|
|
194
|
+
switch (cmd) {
|
|
195
|
+
case "exit":
|
|
196
|
+
case "quit":
|
|
197
|
+
case "q":
|
|
198
|
+
return "exit";
|
|
199
|
+
case "clear":
|
|
200
|
+
messages.length = 0;
|
|
201
|
+
console.log("\x1b[90m ✓ Conversation cleared\x1b[0m");
|
|
202
|
+
return "handled";
|
|
203
|
+
case "compact":
|
|
204
|
+
if (messages.length < 4) {
|
|
205
|
+
console.log("\x1b[90m Not enough messages to compact\x1b[0m");
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
|
|
209
|
+
const model = resolveModel(modelId);
|
|
210
|
+
const result = await compactMessages(messages, model, { keepRecentTurns: 2 });
|
|
211
|
+
messages.length = 0;
|
|
212
|
+
messages.push(...result.messages);
|
|
213
|
+
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
214
|
+
}
|
|
215
|
+
return "handled";
|
|
216
|
+
case "model":
|
|
217
|
+
if (arg) {
|
|
218
|
+
const config = await import("./config.js");
|
|
219
|
+
const cfg = config.loadConfig();
|
|
220
|
+
if (cfg.provider) {
|
|
221
|
+
cfg.provider.defaultModel = arg;
|
|
222
|
+
config.saveConfig(cfg);
|
|
223
|
+
console.log(`\x1b[90m ✓ Default model set to: ${arg}\x1b[0m`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const config = await import("./config.js");
|
|
228
|
+
const cfg = config.loadConfig();
|
|
229
|
+
console.log(`\x1b[90m Current model: ${cfg.provider?.defaultModel ?? "not set"}\x1b[0m`);
|
|
230
|
+
}
|
|
231
|
+
return "handled";
|
|
232
|
+
case "models": {
|
|
233
|
+
const config = await import("./config.js");
|
|
234
|
+
const cfg = config.loadConfig();
|
|
235
|
+
if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
|
|
236
|
+
console.log("\x1b[90m Not configured. Run: min-agent setup\x1b[0m");
|
|
237
|
+
return "handled";
|
|
238
|
+
}
|
|
239
|
+
console.log("\x1b[90m Fetching models...\x1b[0m");
|
|
240
|
+
const models = await config.fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
|
|
241
|
+
if (models.length === 0) {
|
|
242
|
+
console.log("\x1b[90m No models found or unable to fetch model list\x1b[0m");
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
console.log(`\x1b[90m Available models (${models.length}):\x1b[0m`);
|
|
246
|
+
for (const m of models) {
|
|
247
|
+
const marker = m === cfg.provider.defaultModel ? " ← default" : "";
|
|
248
|
+
console.log(`\x1b[90m - ${m}${marker}\x1b[0m`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return "handled";
|
|
252
|
+
}
|
|
253
|
+
case "memory":
|
|
254
|
+
if (arg) {
|
|
255
|
+
const { addMemory } = await import("./memory.js");
|
|
256
|
+
addMemory(arg);
|
|
257
|
+
console.log(`\x1b[90m ✓ Memory saved: "${arg}"\x1b[0m`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const { loadMemories } = await import("./memory.js");
|
|
261
|
+
const memories = loadMemories();
|
|
262
|
+
if (memories.length === 0) {
|
|
263
|
+
console.log("\x1b[90m No memories stored\x1b[0m");
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
for (let i = 0; i < memories.length; i++) {
|
|
267
|
+
const m = memories[i];
|
|
268
|
+
const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
|
|
269
|
+
console.log(`\x1b[90m #${i + 1}: ${m.content}${tags}\x1b[0m`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return "handled";
|
|
274
|
+
case "skills": {
|
|
275
|
+
discoverSkills();
|
|
276
|
+
const skills = getSkills();
|
|
277
|
+
if (skills.length === 0) {
|
|
278
|
+
console.log("\x1b[90m No skills found\x1b[0m");
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.log(`\x1b[90m Available skills (${skills.length}):\x1b[0m`);
|
|
282
|
+
for (const skill of skills) {
|
|
283
|
+
console.log(`\x1b[90m - ${skill.name}: ${skill.description}\x1b[0m`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return "handled";
|
|
287
|
+
}
|
|
288
|
+
case "mcp": {
|
|
289
|
+
const config = loadMcpConfig();
|
|
290
|
+
const servers = Object.entries(config.mcpServers);
|
|
291
|
+
if (servers.length === 0) {
|
|
292
|
+
console.log("\x1b[90m No MCP servers configured\x1b[0m");
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const status = getMcpStatus();
|
|
296
|
+
console.log(`\x1b[90m MCP servers (${servers.length}):\x1b[0m`);
|
|
297
|
+
for (const [name, cfg] of servers) {
|
|
298
|
+
const disabled = cfg.enabled === false;
|
|
299
|
+
const connected = status[name]?.connected ?? false;
|
|
300
|
+
const toolCount = status[name]?.tools.length ?? 0;
|
|
301
|
+
const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
|
|
302
|
+
const toolsText = toolCount > 0 ? `, ${toolCount} tools` : "";
|
|
303
|
+
console.log(`\x1b[90m - ${name}: ${state}${toolsText}\x1b[0m`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return "handled";
|
|
307
|
+
}
|
|
308
|
+
case "tokens":
|
|
309
|
+
console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
|
|
310
|
+
console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
|
|
311
|
+
return "handled";
|
|
312
|
+
case "help":
|
|
313
|
+
console.log(`\x1b[90m Slash commands:
|
|
314
|
+
/clear Clear conversation history
|
|
315
|
+
/compact Force context compaction
|
|
316
|
+
/model [name] Show or change current model
|
|
317
|
+
/models List available models from provider
|
|
318
|
+
/memory [text] List memories or save a new one
|
|
319
|
+
/skills List discovered skills
|
|
320
|
+
/mcp List MCP servers and connection status
|
|
321
|
+
/tokens Show estimated token usage
|
|
322
|
+
/help Show this help
|
|
323
|
+
/exit Exit the chat\x1b[0m`);
|
|
324
|
+
return "handled";
|
|
325
|
+
default:
|
|
326
|
+
console.log(`\x1b[90m Unknown command: /${cmd}. Type /help for available commands.\x1b[0m`);
|
|
327
|
+
return "handled";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function runOnce(messages, instructions, modelId, abortSignal, callbacks) {
|
|
331
|
+
const model = resolveModel(modelId);
|
|
332
|
+
const api = !!callbacks;
|
|
333
|
+
// Auto-compact if context is getting too large
|
|
334
|
+
if (needsCompaction(messages)) {
|
|
335
|
+
if (api) {
|
|
336
|
+
callbacks.onCompaction?.("compacting_start");
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
|
|
340
|
+
}
|
|
341
|
+
const result = await compactMessages(messages, model);
|
|
342
|
+
if (result.compacted) {
|
|
343
|
+
messages.length = 0;
|
|
344
|
+
messages.push(...result.messages);
|
|
345
|
+
if (api) {
|
|
346
|
+
callbacks.onCompaction?.(`compacted_ok estimated_tokens=${estimateTokens(messages)}`);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Merge all tools: builtin + MCP + skill + memory + plugins
|
|
354
|
+
const builtinTools = createTools();
|
|
355
|
+
const mcpTools = getMcpTools();
|
|
356
|
+
const memoryTools = getMemoryTools();
|
|
357
|
+
const pluginTools = await loadPluginTools();
|
|
358
|
+
const skills = getSkills();
|
|
359
|
+
const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
|
|
360
|
+
for (const [id, t] of Object.entries(mcpTools)) {
|
|
361
|
+
allTools[id] = t;
|
|
362
|
+
}
|
|
363
|
+
if (skills.length > 0) {
|
|
364
|
+
allTools["skill"] = getSkillsTool();
|
|
365
|
+
}
|
|
366
|
+
let stepCount = 0;
|
|
367
|
+
let hasError = false;
|
|
368
|
+
const result = streamText({
|
|
369
|
+
model,
|
|
370
|
+
system: buildSystemPrompt(instructions),
|
|
371
|
+
messages,
|
|
372
|
+
tools: allTools,
|
|
373
|
+
stopWhen: stepCountIs(MAX_STEPS),
|
|
374
|
+
maxRetries: 3,
|
|
375
|
+
abortSignal,
|
|
376
|
+
onStepFinish() {
|
|
377
|
+
stepCount++;
|
|
378
|
+
},
|
|
379
|
+
onError() { },
|
|
380
|
+
});
|
|
381
|
+
Promise.resolve(result.usage).catch(() => { });
|
|
382
|
+
let rawText = "";
|
|
383
|
+
let assistantText = "";
|
|
384
|
+
const md = new MarkdownRenderer();
|
|
385
|
+
const thinkingSplit = new ThinkingBodySplitter();
|
|
386
|
+
const emitThinking = (t) => {
|
|
387
|
+
if (!t)
|
|
388
|
+
return;
|
|
389
|
+
if (callbacks?.onThinkingDelta)
|
|
390
|
+
callbacks.onThinkingDelta(t);
|
|
391
|
+
else
|
|
392
|
+
writeThinkingDelta(t);
|
|
393
|
+
};
|
|
394
|
+
try {
|
|
395
|
+
for await (const event of result.fullStream) {
|
|
396
|
+
switch (event.type) {
|
|
397
|
+
case "text-delta": {
|
|
398
|
+
const { display, thinking } = thinkingSplit.feed(event.text);
|
|
399
|
+
emitThinking(thinking);
|
|
400
|
+
if (display) {
|
|
401
|
+
rawText += display;
|
|
402
|
+
assistantText += display;
|
|
403
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
404
|
+
callbacks.onAssistantDisplayDelta(display);
|
|
405
|
+
else {
|
|
406
|
+
const formatted = md.write(display);
|
|
407
|
+
if (formatted)
|
|
408
|
+
process.stdout.write(formatted);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
case "tool-call": {
|
|
414
|
+
const splitFlush = thinkingSplit.flush();
|
|
415
|
+
emitThinking(splitFlush.thinking);
|
|
416
|
+
if (splitFlush.display) {
|
|
417
|
+
rawText += splitFlush.display;
|
|
418
|
+
assistantText += splitFlush.display;
|
|
419
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
420
|
+
callbacks.onAssistantDisplayDelta(splitFlush.display);
|
|
421
|
+
else {
|
|
422
|
+
const extra = md.write(splitFlush.display);
|
|
423
|
+
if (extra)
|
|
424
|
+
process.stdout.write(extra);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!callbacks) {
|
|
428
|
+
const flushed = md.flush();
|
|
429
|
+
if (flushed)
|
|
430
|
+
process.stdout.write(flushed);
|
|
431
|
+
if (rawText.trim())
|
|
432
|
+
console.log();
|
|
433
|
+
}
|
|
434
|
+
rawText = "";
|
|
435
|
+
if (callbacks?.onToolCall)
|
|
436
|
+
callbacks.onToolCall(event.toolName, event.input);
|
|
437
|
+
else
|
|
438
|
+
printToolCall(event.toolName, event.input);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "tool-result":
|
|
442
|
+
if (callbacks?.onToolResult)
|
|
443
|
+
callbacks.onToolResult(event.toolName, event.output);
|
|
444
|
+
else
|
|
445
|
+
printToolResult(event.toolName, event.output);
|
|
446
|
+
break;
|
|
447
|
+
case "error":
|
|
448
|
+
hasError = true;
|
|
449
|
+
const errorMsg = String(event.error);
|
|
450
|
+
if (callbacks?.onStreamError) {
|
|
451
|
+
callbacks.onStreamError(errorMsg);
|
|
452
|
+
}
|
|
453
|
+
else if (errorMsg.includes("Forbidden") || errorMsg.includes("Unauthorized") || errorMsg.includes("API key")) {
|
|
454
|
+
console.error(`\x1b[31mAuthentication error: Check your API key.\x1b[0m`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
console.error(`\x1b[31mError: ${errorMsg}\x1b[0m`);
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
case "finish":
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const splitEnd = thinkingSplit.flush();
|
|
465
|
+
emitThinking(splitEnd.thinking);
|
|
466
|
+
if (splitEnd.display) {
|
|
467
|
+
rawText += splitEnd.display;
|
|
468
|
+
assistantText += splitEnd.display;
|
|
469
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
470
|
+
callbacks.onAssistantDisplayDelta(splitEnd.display);
|
|
471
|
+
else {
|
|
472
|
+
const tail = md.write(splitEnd.display);
|
|
473
|
+
if (tail)
|
|
474
|
+
process.stdout.write(tail);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (!callbacks) {
|
|
478
|
+
const remaining = md.flush();
|
|
479
|
+
if (remaining)
|
|
480
|
+
process.stdout.write(remaining);
|
|
481
|
+
if (rawText.trim())
|
|
482
|
+
console.log();
|
|
483
|
+
}
|
|
484
|
+
const cleanedAssistant = stripThinkingFromAssistantText(assistantText);
|
|
485
|
+
if (cleanedAssistant.trim()) {
|
|
486
|
+
messages.push({ role: "assistant", content: cleanedAssistant });
|
|
487
|
+
}
|
|
488
|
+
let usage;
|
|
489
|
+
try {
|
|
490
|
+
usage = await result.usage;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
usage = undefined;
|
|
494
|
+
}
|
|
495
|
+
if (callbacks?.onRunFinish) {
|
|
496
|
+
callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
if (hasError) {
|
|
500
|
+
printDivider();
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
printDivider();
|
|
504
|
+
printDone(stepCount, usage);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
if (!callbacks && rawText.trim())
|
|
509
|
+
console.log();
|
|
510
|
+
if (err.name === "AbortError" || abortSignal?.aborted) {
|
|
511
|
+
const splitAbort = thinkingSplit.flush();
|
|
512
|
+
emitThinking(splitAbort.thinking);
|
|
513
|
+
if (splitAbort.display) {
|
|
514
|
+
assistantText += splitAbort.display;
|
|
515
|
+
if (callbacks?.onAssistantDisplayDelta)
|
|
516
|
+
callbacks.onAssistantDisplayDelta(splitAbort.display);
|
|
517
|
+
else {
|
|
518
|
+
const w = md.write(splitAbort.display);
|
|
519
|
+
if (w)
|
|
520
|
+
process.stdout.write(w);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (!callbacks)
|
|
524
|
+
process.stdout.write(md.flush());
|
|
525
|
+
const cleaned = stripThinkingFromAssistantText(assistantText);
|
|
526
|
+
if (cleaned.trim()) {
|
|
527
|
+
messages.push({ role: "assistant", content: cleaned });
|
|
528
|
+
}
|
|
529
|
+
let usage;
|
|
530
|
+
try {
|
|
531
|
+
usage = await result.usage;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
usage = undefined;
|
|
535
|
+
}
|
|
536
|
+
callbacks?.onRunFinish?.({ stepCount, usage, hasError: false, aborted: true });
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (!callbacks)
|
|
540
|
+
printDivider();
|
|
541
|
+
const msg = err.message ?? String(err);
|
|
542
|
+
if (callbacks?.onStreamError) {
|
|
543
|
+
callbacks.onStreamError(msg);
|
|
544
|
+
}
|
|
545
|
+
else if (msg.includes("API key") || msg.includes("Unauthorized") || msg.includes("Forbidden")) {
|
|
546
|
+
console.error(`\x1b[31mAuthentication error: Check your API key.\x1b[0m`);
|
|
547
|
+
}
|
|
548
|
+
else if (msg.includes("429") || msg.includes("rate limit") || msg.includes("Rate limit")) {
|
|
549
|
+
console.error(`\x1b[31mRate limited after retries. Please wait and try again.\x1b[0m`);
|
|
550
|
+
}
|
|
551
|
+
else if (msg.includes("timeout") || msg.includes("ETIMEDOUT") || msg.includes("ECONNRESET")) {
|
|
552
|
+
console.error(`\x1b[31mNetwork error (retries exhausted): ${msg}\x1b[0m`);
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
console.error(`\x1b[31mError: ${msg}\x1b[0m`);
|
|
556
|
+
}
|
|
557
|
+
let usage;
|
|
558
|
+
try {
|
|
559
|
+
usage = await result.usage;
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
usage = undefined;
|
|
563
|
+
}
|
|
564
|
+
callbacks?.onRunFinish?.({ stepCount, usage, hasError: true, aborted: false });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split "thinking" fragments (inline XML-style blocks some models leak into text)
|
|
3
|
+
* from displayable assistant text. Thinking is emitted to stderr; display goes to Markdown.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Incremental filter: feed raw model text deltas; get display text (for stdout / history)
|
|
7
|
+
* and thinking text (for stderr). Handles tags split across chunks.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ThinkingBodySplitter {
|
|
10
|
+
private buf;
|
|
11
|
+
feed(chunk: string): {
|
|
12
|
+
display: string;
|
|
13
|
+
thinking: string;
|
|
14
|
+
};
|
|
15
|
+
/** End of stream: flush remainder; incomplete thinking block → stderr only. */
|
|
16
|
+
flush(): {
|
|
17
|
+
display: string;
|
|
18
|
+
thinking: string;
|
|
19
|
+
};
|
|
20
|
+
private drain;
|
|
21
|
+
}
|
|
22
|
+
/** Final strip for assistant message (complete tags only). */
|
|
23
|
+
export declare function stripThinkingFromAssistantText(text: string): string;
|