@ghenya/clinn 0.7.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/Logos/StartLogo.txt +7 -0
- package/Mem/history.js +130 -0
- package/Mem/index.js +113 -0
- package/README.md +230 -0
- package/Src/agent.js +342 -0
- package/Src/index.js +984 -0
- package/Src/llm.js +195 -0
- package/Tools/browser.js +133 -0
- package/Tools/custom/.gitkeep +0 -0
- package/Tools/edit_tools.js +93 -0
- package/Tools/extended_tools.js +408 -0
- package/Tools/file_tools.js +201 -0
- package/Tools/index.js +311 -0
- package/Tools/search_tools.js +280 -0
- package/Tools/tokenizer.js +150 -0
- package/config.json +251 -0
- package/package.json +48 -0
package/Src/agent.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const LLMClient = require("./llm");
|
|
3
|
+
const Tools = require("../Tools");
|
|
4
|
+
const { ConversationMemory } = require("../Mem");
|
|
5
|
+
const { saveTurn, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
6
|
+
|
|
7
|
+
const MAX_ITERATIONS = 500;
|
|
8
|
+
const MAX_TOOL_RESULT_CHARS = 3000;
|
|
9
|
+
|
|
10
|
+
function buildSystemInfo() {
|
|
11
|
+
const home = os.homedir();
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
return [
|
|
14
|
+
`系统: ${os.type()} ${os.release()} (${os.arch()})`,
|
|
15
|
+
`主机: ${os.hostname()}`,
|
|
16
|
+
`用户主目录: ${home}`,
|
|
17
|
+
`当前工作目录: ${cwd}`,
|
|
18
|
+
`终端宽度: ${process.stdout.columns || 80} 列`,
|
|
19
|
+
`Node: ${process.version}`,
|
|
20
|
+
`时间: ${new Date().toISOString()}`,
|
|
21
|
+
].join(" | ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function estimateTokens(text) {
|
|
25
|
+
let chars = 0;
|
|
26
|
+
let cjk = 0;
|
|
27
|
+
for (const ch of text) {
|
|
28
|
+
const code = ch.codePointAt(0);
|
|
29
|
+
if (code > 127) cjk++;
|
|
30
|
+
chars++;
|
|
31
|
+
}
|
|
32
|
+
return Math.ceil(cjk * 1.5 + (chars - cjk) * 0.35);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function estimateMessagesTokens(messages) {
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const m of messages) {
|
|
38
|
+
total += estimateTokens(m.role) + estimateTokens(m.content || "");
|
|
39
|
+
}
|
|
40
|
+
return total;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class Agent {
|
|
44
|
+
constructor(config, callbacks = {}) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.llm = new LLMClient(config.llm);
|
|
47
|
+
this.memory = new ConversationMemory(config.memory);
|
|
48
|
+
this.callbacks = callbacks;
|
|
49
|
+
this.maxIterations = MAX_ITERATIONS;
|
|
50
|
+
this.systemInfo = buildSystemInfo();
|
|
51
|
+
this.systemPrompt = `${config.systemPrompt}\n\n[系统环境]\n${this.systemInfo}`;
|
|
52
|
+
this.autoCompressThreshold = config.memory?.autoCompressThreshold || 5000;
|
|
53
|
+
this.maxContextTokens = (config.llm?.maxTokens || 65536) * 0.75;
|
|
54
|
+
this._lastToolCalls = [];
|
|
55
|
+
|
|
56
|
+
Tools.setTrusted(config.tools?.trustedTools || []);
|
|
57
|
+
Tools.setPermissionCallback(async (name, args) => {
|
|
58
|
+
if (callbacks.onPermission) return callbacks.onPermission(name, args);
|
|
59
|
+
return false;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this._injectAgentTools();
|
|
63
|
+
this.toolDeclarations = Tools.toFunctionDeclarations();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_injectAgentTools() {
|
|
67
|
+
const self = this;
|
|
68
|
+
const memTools = [
|
|
69
|
+
"search_memory", "save_memory", "list_memory", "delete_memory",
|
|
70
|
+
"compress_context", "agent_self_invoke", "set_timer",
|
|
71
|
+
"save_tool", "delete_tool_file", "list_saved_tools",
|
|
72
|
+
"forget_conversation", "restart_session",
|
|
73
|
+
"search_history", "list_history_files",
|
|
74
|
+
];
|
|
75
|
+
for (const name of memTools) {
|
|
76
|
+
const tool = Tools.getTool(name);
|
|
77
|
+
if (!tool) continue;
|
|
78
|
+
tool.execute = (args) => self._handleAgentTool(name, args);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _handleAgentTool(name, args) {
|
|
83
|
+
switch (name) {
|
|
84
|
+
case "search_memory":
|
|
85
|
+
return this._fmtEntries(this.memory.searchEntries(args.query, args.limit || 5));
|
|
86
|
+
case "save_memory": {
|
|
87
|
+
const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
|
|
88
|
+
const entry = this.memory.addEntry(args.content, tags);
|
|
89
|
+
return entry ? `[OK] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
|
|
90
|
+
}
|
|
91
|
+
case "list_memory": {
|
|
92
|
+
const all = this.memory.getAllEntries().slice(-(args.limit || 20));
|
|
93
|
+
return this._fmtEntries(all);
|
|
94
|
+
}
|
|
95
|
+
case "delete_memory": {
|
|
96
|
+
const ok = this.memory.removeEntry(args.id);
|
|
97
|
+
return ok ? `[OK] 已删除 #${args.id}` : `[不存在] #${args.id}`;
|
|
98
|
+
}
|
|
99
|
+
case "compress_context": {
|
|
100
|
+
const compressed = this.memory.compressHistory();
|
|
101
|
+
if (!compressed) return "[跳过] 对话太短";
|
|
102
|
+
const summary = await this._summarize(compressed);
|
|
103
|
+
this.memory.addEntry(summary, ["auto-summary"]);
|
|
104
|
+
this.memory.clear();
|
|
105
|
+
return `[OK] 上下文已压缩, 摘要存入记忆: ${summary}`;
|
|
106
|
+
}
|
|
107
|
+
case "agent_self_invoke": {
|
|
108
|
+
if (this.callbacks.onSelfInvoke) return this.callbacks.onSelfInvoke(args.task, args.context);
|
|
109
|
+
return "[跳过] self_invoke 未配置回调";
|
|
110
|
+
}
|
|
111
|
+
case "set_timer": {
|
|
112
|
+
const s = Math.max(1, Math.min(args.seconds || 5, 300));
|
|
113
|
+
const msg = args.message || "定时器";
|
|
114
|
+
if (this.callbacks.onTimer) this.callbacks.onTimer(s, msg);
|
|
115
|
+
return `[OK] 定时器已设置 ${s}秒后通知: ${msg}`;
|
|
116
|
+
}
|
|
117
|
+
case "save_tool": {
|
|
118
|
+
const code = args.code || "";
|
|
119
|
+
const toolName = args.name || "";
|
|
120
|
+
if (!toolName) return "[失败] 必须提供 name 参数";
|
|
121
|
+
if (!code) return "[失败] 必须提供 code 参数";
|
|
122
|
+
const result = Tools.saveToolToFile(toolName, code);
|
|
123
|
+
if (result.startsWith("[OK]")) this.refreshTools();
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
case "delete_tool_file": {
|
|
127
|
+
const toolName = args.name || "";
|
|
128
|
+
if (!toolName) return "[失败] 必须提供 name 参数";
|
|
129
|
+
const result = Tools.deleteToolFile(toolName);
|
|
130
|
+
if (result.startsWith("[OK]")) this.refreshTools();
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
case "list_saved_tools": {
|
|
134
|
+
const saved = Tools.listCustomTools();
|
|
135
|
+
if (saved.length === 0) return "(无持久化工具)";
|
|
136
|
+
return saved.map((s) => `[${s.file}] 导出: ${s.exports.join(", ")}`).join("\n");
|
|
137
|
+
}
|
|
138
|
+
case "forget_conversation": {
|
|
139
|
+
const keepSummary = args.keepSummary !== false;
|
|
140
|
+
let summary = "";
|
|
141
|
+
if (keepSummary && this.memory.getHistory().length > 2) {
|
|
142
|
+
const compressed = this.memory.compressHistory();
|
|
143
|
+
if (compressed) summary = await this._summarize(compressed);
|
|
144
|
+
}
|
|
145
|
+
this.memory.clear();
|
|
146
|
+
if (summary) { this.memory.addEntry(summary, ["auto-summary"]); }
|
|
147
|
+
return summary
|
|
148
|
+
? `[OK] 对话已遗忘, 摘要已保存: ${summary}`
|
|
149
|
+
: "[OK] 对话历史已清空, 像全新对话一样";
|
|
150
|
+
}
|
|
151
|
+
case "restart_session": {
|
|
152
|
+
this.memory.clear();
|
|
153
|
+
this.memory.clearEntries();
|
|
154
|
+
this.llm.resetUsage();
|
|
155
|
+
return "[OK] 会话已完全重启: 历史+记忆+token计数均已重置. 可以开始全新任务.";
|
|
156
|
+
}
|
|
157
|
+
case "search_history": {
|
|
158
|
+
const q = args.query || "";
|
|
159
|
+
if (!q.trim()) return "[错误] 请提供搜索关键词";
|
|
160
|
+
const results = searchHistory(q, args.limit || 10);
|
|
161
|
+
if (results.length === 0) return `[无匹配] 在历史对话中未找到 "${q}"`;
|
|
162
|
+
return results.map((r, i) =>
|
|
163
|
+
`${i + 1}. [${r.file}] ${r.time?.slice(0, 16) || "?"}${r.cwd ? `\n 目录: ${r.cwd}` : ""}\n 用户: ${r.user}\n 回复: ${r.assistant}`
|
|
164
|
+
).join("\n\n");
|
|
165
|
+
}
|
|
166
|
+
case "list_history_files": {
|
|
167
|
+
const files = getFileList();
|
|
168
|
+
if (files.length === 0) return "(暂无历史对话文件)";
|
|
169
|
+
return files.map((f) => `${f.file} | ${f.turns}轮 | ${f.size}KB | ${f.created.slice(0, 10)}`).join("\n");
|
|
170
|
+
}
|
|
171
|
+
default:
|
|
172
|
+
return `[未知内部工具] ${name}`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async _summarize(text) {
|
|
177
|
+
try {
|
|
178
|
+
const msgs = [
|
|
179
|
+
{ role: "system", content: "将以下对话压缩为一条200字以内的中文摘要,只输出摘要。" },
|
|
180
|
+
{ role: "user", content: text.slice(0, 4000) },
|
|
181
|
+
];
|
|
182
|
+
const res = await this.llm.chat(msgs);
|
|
183
|
+
return (res.choices?.[0]?.message?.content || text.slice(0, 190)).slice(0, 190);
|
|
184
|
+
} catch (_) {
|
|
185
|
+
return text.slice(0, 190);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_fmtEntries(entries) {
|
|
190
|
+
if (!entries || entries.length === 0) return "(无记忆条目)";
|
|
191
|
+
return entries.map((e) => `#${e.id} [${e.tags?.join(",") || "-"}] ${e.text}`).join("\n");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
refreshTools() {
|
|
195
|
+
this.toolDeclarations = Tools.toFunctionDeclarations();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_detectLoop(toolCalls) {
|
|
199
|
+
const sig = toolCalls.map((t) => `${t.function.name}:${t.function.arguments}`).join("|");
|
|
200
|
+
if (!this._lastSig || this._lastSig !== sig) {
|
|
201
|
+
this._lastSig = sig;
|
|
202
|
+
this._repeatCount = 1;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
this._repeatCount++;
|
|
206
|
+
return this._repeatCount >= 300;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async run(userMessage, opts = {}) {
|
|
210
|
+
const { onContent, onToolCall, onToolResult, onThinking, onContextPct } = opts;
|
|
211
|
+
this.memory.addUser(userMessage);
|
|
212
|
+
let messages = this._buildMessages();
|
|
213
|
+
let finalResponse = "";
|
|
214
|
+
this._lastSig = null;
|
|
215
|
+
this._repeatCount = 0;
|
|
216
|
+
let allMsgs = [];
|
|
217
|
+
let warned90 = false;
|
|
218
|
+
let warned95 = false;
|
|
219
|
+
|
|
220
|
+
const activeTools = Tools.filterToolDeclarations(userMessage);
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < this.maxIterations; i++) {
|
|
223
|
+
if (onContextPct || onThinking) {
|
|
224
|
+
const baseMsgs = this._buildMessages();
|
|
225
|
+
const curEst = estimateMessagesTokens(baseMsgs);
|
|
226
|
+
const pct = Math.min(100, Math.ceil((curEst / this.maxContextTokens) * 100));
|
|
227
|
+
if (onContextPct) onContextPct(pct);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (onThinking) onThinking();
|
|
231
|
+
|
|
232
|
+
const result = await this.llm.chatStream(messages, onContent || null, activeTools);
|
|
233
|
+
|
|
234
|
+
if (result.toolCalls && result.toolCalls.length > 0) {
|
|
235
|
+
if (this._detectLoop(result.toolCalls)) {
|
|
236
|
+
finalResponse = "(检测到工具调用循环, 已自动终止)";
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const stepAssistant = {
|
|
241
|
+
role: "assistant",
|
|
242
|
+
content: null,
|
|
243
|
+
tool_calls: result.toolCalls.map((tc) => ({
|
|
244
|
+
id: tc.id,
|
|
245
|
+
type: "function",
|
|
246
|
+
function: { name: tc.function.name, arguments: tc.function.arguments },
|
|
247
|
+
})),
|
|
248
|
+
};
|
|
249
|
+
allMsgs.push(stepAssistant);
|
|
250
|
+
|
|
251
|
+
const toolMsgs = [];
|
|
252
|
+
for (const tc of result.toolCalls) {
|
|
253
|
+
const fnName = tc.function.name;
|
|
254
|
+
let fnArgs = {};
|
|
255
|
+
try { fnArgs = JSON.parse(tc.function.arguments); } catch (_) {}
|
|
256
|
+
|
|
257
|
+
if (onToolCall) onToolCall(fnName, fnArgs, i + 1);
|
|
258
|
+
|
|
259
|
+
const tool = Tools.getTool(fnName);
|
|
260
|
+
if (tool?.dangerous) {
|
|
261
|
+
const allowed = await Tools.checkPermission(fnName, fnArgs);
|
|
262
|
+
if (!allowed) {
|
|
263
|
+
if (onToolResult) onToolResult(fnName, "[被拒绝]");
|
|
264
|
+
toolMsgs.push({ role: "tool", tool_call_id: tc.id, name: fnName, content: "[被用户拒绝]" });
|
|
265
|
+
allMsgs.push({ role: "tool", name: fnName, content: "[被用户拒绝]" });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let toolResult;
|
|
271
|
+
try { toolResult = await Tools.executeTool(fnName, fnArgs); }
|
|
272
|
+
catch (e) { toolResult = `error: ${e.message}`; }
|
|
273
|
+
|
|
274
|
+
const capped = String(toolResult).slice(0, MAX_TOOL_RESULT_CHARS);
|
|
275
|
+
if (onToolResult) onToolResult(fnName, capped.slice(0, 300));
|
|
276
|
+
toolMsgs.push({ role: "tool", tool_call_id: tc.id, name: fnName, content: capped });
|
|
277
|
+
allMsgs.push({ role: "tool", name: fnName, content: capped.slice(0, 500) });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
messages.push(stepAssistant);
|
|
281
|
+
messages.push(...toolMsgs);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (result.content) {
|
|
286
|
+
allMsgs.push({ role: "assistant", content: result.content });
|
|
287
|
+
finalResponse = result.content; break;
|
|
288
|
+
}
|
|
289
|
+
allMsgs.push({ role: "assistant", content: "(empty)" });
|
|
290
|
+
finalResponse = "(empty)"; break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.memory.addAssistant(finalResponse);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
saveTurn(userMessage, finalResponse, process.cwd(), allMsgs);
|
|
297
|
+
} catch (_) {}
|
|
298
|
+
|
|
299
|
+
if (opts._noAutoSave) return finalResponse;
|
|
300
|
+
|
|
301
|
+
setImmediate(async () => {
|
|
302
|
+
try {
|
|
303
|
+
const habitEntry = this.memory.searchEntries("habit", 3);
|
|
304
|
+
if (habitEntry.length === 0) {
|
|
305
|
+
const msgs = [
|
|
306
|
+
{ role: "system", content: "用户刚完成一个任务。请生成一条15字以内的用户偏好摘要(用save_memory保存, tags='habit')。只输出纯文本,不要任何格式。" },
|
|
307
|
+
{ role: "user", content: `任务: ${userMessage.slice(0, 300)}\n结果摘要: ${finalResponse.slice(0, 200)}` },
|
|
308
|
+
];
|
|
309
|
+
const res = await this.llm.chat(msgs);
|
|
310
|
+
const habit = res.choices?.[0]?.message?.content?.trim().slice(0, 100);
|
|
311
|
+
if (habit) this.memory.addEntry(habit, ["habit"]);
|
|
312
|
+
}
|
|
313
|
+
} catch (_) {}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
return finalResponse;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
_buildMessages() {
|
|
320
|
+
return [
|
|
321
|
+
{ role: "system", content: this.systemPrompt },
|
|
322
|
+
...this.memory.getHistory(),
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
_emit(event, ...args) {
|
|
327
|
+
if (this.callbacks[event]) this.callbacks[event](...args);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
getUsage() { return this.llm.getUsage(); }
|
|
331
|
+
reset() { this.memory.clear(); this.llm.resetUsage(); }
|
|
332
|
+
|
|
333
|
+
estimateContextPct() {
|
|
334
|
+
const msgs = this._buildMessages();
|
|
335
|
+
const est = estimateMessagesTokens(msgs);
|
|
336
|
+
return Math.min(100, Math.ceil((est / this.maxContextTokens) * 100));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getMaxContextTokens() { return this.maxContextTokens; }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
module.exports = Agent;
|