@ghenya/clinn 0.8.8 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Logos/StartLogo.txt +1 -1
- package/Mem/history.js +326 -24
- package/Mem/index.js +123 -14
- package/Src/agent.cjs +74 -11
- package/Src/index.js +81 -8
- package/Src/index.jsx +366 -108
- package/Tools/extended_tools.js +83 -8
- package/Tools/index.js +1 -1
- package/Tools/search_tools.js +55 -1
- package/config.json +1 -1
- package/package.json +8 -8
package/Src/agent.cjs
CHANGED
|
@@ -2,7 +2,12 @@ const os = require("os");
|
|
|
2
2
|
const LLMClient = require("./llm.cjs");
|
|
3
3
|
const Tools = require("../Tools");
|
|
4
4
|
const { ConversationMemory } = require("../Mem");
|
|
5
|
-
const {
|
|
5
|
+
const {
|
|
6
|
+
saveTurn, searchHistory, getFileList, loadFileTurns,
|
|
7
|
+
startSession, endSession, listSessions, getSession,
|
|
8
|
+
loadSessionTurns, searchSessions, globalSearch,
|
|
9
|
+
migrateLegacySessions,
|
|
10
|
+
} = require("../Mem/history");
|
|
6
11
|
|
|
7
12
|
const MAX_ITERATIONS = 500;
|
|
8
13
|
const MAX_TOOL_RESULT_CHARS = 3000;
|
|
@@ -84,6 +89,10 @@ class Agent {
|
|
|
84
89
|
this.maxContextTokens = (config.llm?.contextWindow || getContextWindow(config.llm?.model)) * 0.8;
|
|
85
90
|
this._lastToolCalls = [];
|
|
86
91
|
|
|
92
|
+
// session management
|
|
93
|
+
migrateLegacySessions();
|
|
94
|
+
this.sessionId = startSession();
|
|
95
|
+
|
|
87
96
|
Tools.setTrusted(config.tools?.trustedTools || []);
|
|
88
97
|
Tools.setPermissionCallback(async (name, args) => {
|
|
89
98
|
if (callbacks.onPermission) return callbacks.onPermission(name, args);
|
|
@@ -98,10 +107,12 @@ class Agent {
|
|
|
98
107
|
const self = this;
|
|
99
108
|
const memTools = [
|
|
100
109
|
"search_memory", "save_memory", "list_memory", "delete_memory",
|
|
110
|
+
"mem_rom", "mem_ram",
|
|
101
111
|
"compress_context", "agent_self_invoke", "set_timer",
|
|
102
112
|
"save_tool", "delete_tool_file", "list_saved_tools",
|
|
103
113
|
"forget_conversation", "restart_session",
|
|
104
114
|
"search_history", "list_history_files",
|
|
115
|
+
"list_sessions", "view_session", "search_sessions",
|
|
105
116
|
];
|
|
106
117
|
for (const name of memTools) {
|
|
107
118
|
const tool = Tools.getTool(name);
|
|
@@ -114,10 +125,16 @@ class Agent {
|
|
|
114
125
|
switch (name) {
|
|
115
126
|
case "search_memory":
|
|
116
127
|
return this._fmtEntries(this.memory.searchEntries(args.query, args.limit || 5));
|
|
117
|
-
case "save_memory":
|
|
128
|
+
case "save_memory":
|
|
129
|
+
case "mem_rom": {
|
|
130
|
+
const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
|
|
131
|
+
const entry = this.memory.addRomEntry(args.content, tags);
|
|
132
|
+
return entry ? `[ROM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
|
|
133
|
+
}
|
|
134
|
+
case "mem_ram": {
|
|
118
135
|
const tags = args.tags ? args.tags.split(",").map((t) => t.trim()) : [];
|
|
119
|
-
const entry = this.memory.
|
|
120
|
-
return entry ? `[
|
|
136
|
+
const entry = this.memory.addRamEntry(args.content, tags);
|
|
137
|
+
return entry ? `[RAM] 已保存 #${entry.id}: ${entry.text}` : "[失败] 内容为空";
|
|
121
138
|
}
|
|
122
139
|
case "list_memory": {
|
|
123
140
|
const all = this.memory.getAllEntries().slice(-(args.limit || 20));
|
|
@@ -131,7 +148,7 @@ class Agent {
|
|
|
131
148
|
const compressed = this.memory.compressHistory();
|
|
132
149
|
if (!compressed) return "[跳过] 对话太短";
|
|
133
150
|
const summary = await this._summarize(compressed);
|
|
134
|
-
this.memory.
|
|
151
|
+
this.memory.addRamEntry(summary, ["auto-summary"]);
|
|
135
152
|
this.memory.clear();
|
|
136
153
|
return `[OK] 上下文已压缩, 摘要存入记忆: ${summary}`;
|
|
137
154
|
}
|
|
@@ -174,16 +191,18 @@ class Agent {
|
|
|
174
191
|
if (compressed) summary = await this._summarize(compressed);
|
|
175
192
|
}
|
|
176
193
|
this.memory.clear();
|
|
177
|
-
if (summary) { this.memory.
|
|
194
|
+
if (summary) { this.memory.addRamEntry(summary, ["auto-summary"]); }
|
|
178
195
|
return summary
|
|
179
196
|
? `[OK] 对话已遗忘, 摘要已保存: ${summary}`
|
|
180
197
|
: "[OK] 对话历史已清空, 像全新对话一样";
|
|
181
198
|
}
|
|
182
199
|
case "restart_session": {
|
|
200
|
+
endSession(this.sessionId);
|
|
183
201
|
this.memory.clear();
|
|
184
|
-
this.memory.
|
|
202
|
+
this.memory.clearRamEntries();
|
|
185
203
|
this.llm.resetUsage();
|
|
186
|
-
|
|
204
|
+
this.sessionId = startSession();
|
|
205
|
+
return "[OK] 会话已完全重启: 历史+记忆+token计数均已重置, 新session已创建. 可以开始全新任务.";
|
|
187
206
|
}
|
|
188
207
|
case "search_history": {
|
|
189
208
|
const q = args.query || "";
|
|
@@ -199,6 +218,47 @@ class Agent {
|
|
|
199
218
|
if (files.length === 0) return "(暂无历史对话文件)";
|
|
200
219
|
return files.map((f) => `${f.file} | ${f.turns}轮 | ${f.size}KB | ${f.created.slice(0, 10)}`).join("\n");
|
|
201
220
|
}
|
|
221
|
+
case "list_sessions": {
|
|
222
|
+
const sessions = listSessions(args.limit || 20);
|
|
223
|
+
if (sessions.length === 0) return "(暂无对话 session)";
|
|
224
|
+
const now = this.sessionId;
|
|
225
|
+
return sessions.map((s, i) => {
|
|
226
|
+
const marker = s.id === now ? " ◀ 当前" : "";
|
|
227
|
+
const status = s.status === "active" ? "●" : "○";
|
|
228
|
+
const date = s.started ? s.started.slice(0, 16) : "?";
|
|
229
|
+
return `${i + 1}. ${status} [${date}] ${s.title || "(无标题)"} | ${s.turnCount}轮${marker}`;
|
|
230
|
+
}).join("\n");
|
|
231
|
+
}
|
|
232
|
+
case "view_session": {
|
|
233
|
+
const sid = args.session_id || "";
|
|
234
|
+
if (!sid) return "[错误] 请提供 session_id (用 list_sessions 获取)";
|
|
235
|
+
const session = getSession(sid);
|
|
236
|
+
if (!session) return `[未找到] session ${sid}`;
|
|
237
|
+
const turns = loadSessionTurns(sid, args.limit || 50);
|
|
238
|
+
if (turns.length === 0) return `[空] session ${sid} 中没有对话轮次`;
|
|
239
|
+
const header = [
|
|
240
|
+
`=== Session: ${session.title || "(无标题)"} ===`,
|
|
241
|
+
`ID: ${session.id} | 开始: ${session.started?.slice(0, 16) || "?"} | 共 ${session.turnCount} 轮`,
|
|
242
|
+
``,
|
|
243
|
+
];
|
|
244
|
+
const body = turns.map((t, i) => {
|
|
245
|
+
return `[${i + 1}] ${t.time?.slice(0, 16) || "?"}\n` +
|
|
246
|
+
` > 用户: ${t.user ? t.user.slice(0, 200) : ""}\n` +
|
|
247
|
+
` < 回复: ${t.assistant ? t.assistant.slice(0, 300) : ""}`;
|
|
248
|
+
});
|
|
249
|
+
return header.join("\n") + "\n" + body.join("\n\n");
|
|
250
|
+
}
|
|
251
|
+
case "search_sessions": {
|
|
252
|
+
const q = args.query || "";
|
|
253
|
+
if (!q.trim()) return "[错误] 请提供搜索关键词";
|
|
254
|
+
const results = globalSearch(q, args.limit || 10);
|
|
255
|
+
if (results.length === 0) return `[无匹配] 在所有历史 session 中未找到 "${q}"`;
|
|
256
|
+
return results.map((r, i) => {
|
|
257
|
+
const date = r.sessionStarted ? r.sessionStarted.slice(0, 16) : "?";
|
|
258
|
+
const previews = r.topMatches.map((m) => ` · ${m.user.slice(0, 80)}`).join("\n");
|
|
259
|
+
return `${i + 1}. [${date}] ${r.sessionTitle} | 匹配 ${r.matchCount} 处 | 得分 ${r.totalScore}\n${previews}`;
|
|
260
|
+
}).join("\n\n");
|
|
261
|
+
}
|
|
202
262
|
default:
|
|
203
263
|
return `[未知内部工具] ${name}`;
|
|
204
264
|
}
|
|
@@ -219,7 +279,10 @@ class Agent {
|
|
|
219
279
|
|
|
220
280
|
_fmtEntries(entries) {
|
|
221
281
|
if (!entries || entries.length === 0) return "(无记忆条目)";
|
|
222
|
-
return entries.map((e) =>
|
|
282
|
+
return entries.map((e) => {
|
|
283
|
+
const tag = (e._type === "ram") ? "[RAM]" : "[ROM]";
|
|
284
|
+
return `${tag} #${e.id} [${e.tags?.join(",") || "-"}] ${e.text}`;
|
|
285
|
+
}).join("\n");
|
|
223
286
|
}
|
|
224
287
|
|
|
225
288
|
refreshTools() {
|
|
@@ -326,7 +389,7 @@ class Agent {
|
|
|
326
389
|
this.memory.addAssistant(finalResponse);
|
|
327
390
|
|
|
328
391
|
try {
|
|
329
|
-
saveTurn(userMessage, finalResponse, process.cwd(), allMsgs);
|
|
392
|
+
saveTurn(userMessage, finalResponse, process.cwd(), allMsgs, this.sessionId);
|
|
330
393
|
} catch (_) {}
|
|
331
394
|
|
|
332
395
|
if (opts._noAutoSave) return finalResponse;
|
|
@@ -341,7 +404,7 @@ class Agent {
|
|
|
341
404
|
];
|
|
342
405
|
const res = await this.llm.chat(msgs);
|
|
343
406
|
const habit = res.choices?.[0]?.message?.content?.trim().slice(0, 100);
|
|
344
|
-
if (habit) this.memory.
|
|
407
|
+
if (habit) this.memory.addRamEntry(habit, ["habit"]);
|
|
345
408
|
}
|
|
346
409
|
} catch (_) {}
|
|
347
410
|
});
|
package/Src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const readline = require("readline");
|
|
|
4
4
|
const { spawn } = require("child_process");
|
|
5
5
|
const Agent = require("./agent");
|
|
6
6
|
const Tools = require("../Tools");
|
|
7
|
-
const { listRecentTurns, searchHistory, getFileList, loadFileTurns } = require("../Mem/history");
|
|
7
|
+
const { listRecentTurns, searchHistory, getFileList, loadFileTurns, listSessions, getSession, loadSessionTurns, globalSearch, endSession, startSession } = require("../Mem/history");
|
|
8
8
|
|
|
9
9
|
const C = {
|
|
10
10
|
reset: "\x1b[0m",
|
|
@@ -345,6 +345,8 @@ function showHelp() {
|
|
|
345
345
|
["/memory_clear", "清空所有记忆"], ["/compress", "手动压缩上下文"],
|
|
346
346
|
["/history [n]", "查看最近n条历史对话"], ["/history files", "查看历史文件列表"],
|
|
347
347
|
["/history search <q>", "搜索历史对话"], ["/history read <f>", "读取文件含完整工具调用"],
|
|
348
|
+
["/sessions [n]", "列出对话session"], ["/session <id>", "查看某个session"],
|
|
349
|
+
["/session_search <q>", "全局搜索session"],
|
|
348
350
|
["/tool_save <name>", "持久化保存工具"], ["/tool_list_saved", "列出持久化工具"],
|
|
349
351
|
["/tool_del_saved <name>", "删除持久化工具"],
|
|
350
352
|
["/trusted", "查看受信任工具"], ["/trust <name>", "永久信任工具"],
|
|
@@ -369,7 +371,7 @@ function showStatus() {
|
|
|
369
371
|
console.log(` 模型: ${C.bold}${config.llm.model}${C.reset} 温度: ${config.llm.temperature}`);
|
|
370
372
|
console.log(` Tokens 累计: ${emoji("tokenIn")}${C.cyan}${usage.prompt}${C.reset} ${emoji("tokenOut")}${C.magenta}${usage.completion}${C.reset}`);
|
|
371
373
|
console.log(` 上下文使用: ${ctxBar(ctxPct)}`);
|
|
372
|
-
console.log(` 记忆: ${mem.entries || 0}
|
|
374
|
+
console.log(` 记忆: ROM${mem.romEntries || mem.entries || 0} RAM${mem.ramEntries || 0} · 历史 ${mem.historyMessages || 0} 条`);
|
|
373
375
|
const tools = Tools.listToolNames();
|
|
374
376
|
console.log(` 工具: ${tools.length} 个 — ${tools.slice(0, 8).join(", ")}${tools.length > 8 ? " ..." : ""}`);
|
|
375
377
|
console.log(div("="));
|
|
@@ -436,8 +438,10 @@ async function handleSlashCommand(input) {
|
|
|
436
438
|
console.log(C.dim + `再见~ ${emoji("done")}` + C.reset);
|
|
437
439
|
process.exit(0);
|
|
438
440
|
case "reset":
|
|
441
|
+
endSession(agent.sessionId);
|
|
439
442
|
agent.reset();
|
|
440
|
-
|
|
443
|
+
agent.sessionId = startSession();
|
|
444
|
+
console.log(`${C.green}对话已重置, 新 session 已创建${C.reset}`);
|
|
441
445
|
break;
|
|
442
446
|
case "tools": {
|
|
443
447
|
const names = Tools.listToolNames();
|
|
@@ -563,7 +567,7 @@ async function handleSlashCommand(input) {
|
|
|
563
567
|
case "memory": {
|
|
564
568
|
const s = agent.memory.stats();
|
|
565
569
|
console.log(div());
|
|
566
|
-
console.log(` 条目: ${s.entries}
|
|
570
|
+
console.log(` 条目: ROM${s.romEntries || s.entries} RAM${s.ramEntries || 0} 历史消息: ${s.historyMessages}`);
|
|
567
571
|
console.log(div());
|
|
568
572
|
break;
|
|
569
573
|
}
|
|
@@ -572,7 +576,10 @@ async function handleSlashCommand(input) {
|
|
|
572
576
|
const entries = agent.memory.getAllEntries().slice(-n);
|
|
573
577
|
if (entries.length === 0) { console.log("(无记忆条目)"); break; }
|
|
574
578
|
console.log(div());
|
|
575
|
-
for (const e of entries)
|
|
579
|
+
for (const e of entries) {
|
|
580
|
+
const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
|
|
581
|
+
console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${C.dim}[${e.tags?.join(",") || "-"}]${C.reset} ${e.text}`);
|
|
582
|
+
}
|
|
576
583
|
console.log(div());
|
|
577
584
|
break;
|
|
578
585
|
}
|
|
@@ -581,7 +588,10 @@ async function handleSlashCommand(input) {
|
|
|
581
588
|
const results = agent.memory.searchEntries(rest, 10);
|
|
582
589
|
if (results.length === 0) { console.log(`无匹配: ${rest}`); break; }
|
|
583
590
|
console.log(div());
|
|
584
|
-
for (const e of results)
|
|
591
|
+
for (const e of results) {
|
|
592
|
+
const tag = e._type === "ram" ? `${C.yellow}[RAM]${C.reset}` : `${C.green}[ROM]${C.reset}`;
|
|
593
|
+
console.log(` ${tag} ${C.yellow}#${e.id}${C.reset} ${e.text}`);
|
|
594
|
+
}
|
|
585
595
|
console.log(div());
|
|
586
596
|
break;
|
|
587
597
|
}
|
|
@@ -593,8 +603,8 @@ async function handleSlashCommand(input) {
|
|
|
593
603
|
break;
|
|
594
604
|
}
|
|
595
605
|
case "memory_clear":
|
|
596
|
-
agent.memory.
|
|
597
|
-
console.log(`${C.green}
|
|
606
|
+
agent.memory.clearAllEntries();
|
|
607
|
+
console.log(`${C.green}所有记忆条目已清空 (ROM+RAM)${C.reset}`);
|
|
598
608
|
break;
|
|
599
609
|
case "history": {
|
|
600
610
|
const subCmd = rest ? rest.split(/\s+/)[0] : "";
|
|
@@ -662,6 +672,68 @@ async function handleSlashCommand(input) {
|
|
|
662
672
|
await pipeToPager(wrapped);
|
|
663
673
|
break;
|
|
664
674
|
}
|
|
675
|
+
case "sessions": {
|
|
676
|
+
const limit = rest ? parseInt(rest, 10) || 20 : 20;
|
|
677
|
+
const sessions = listSessions(limit);
|
|
678
|
+
if (sessions.length === 0) { console.log("(暂无对话 session)"); break; }
|
|
679
|
+
let out = `${C.bold + C.cyan}对话 Sessions${C.reset} (${sessions.length})\n` + div() + "\n";
|
|
680
|
+
for (const [i, s] of sessions.entries()) {
|
|
681
|
+
const marker = s.status === "active" ? `${C.green}●${C.reset}` : `${C.dim}○${C.reset}`;
|
|
682
|
+
const active = s.status === "active" ? ` ${C.green}◀ 当前${C.reset}` : "";
|
|
683
|
+
out += ` ${String(i + 1).padStart(2)}. ${marker} [${(s.started || "?").slice(0, 16)}] ${s.title || "(无标题)"} | ${s.turnCount}轮${active}\n`;
|
|
684
|
+
}
|
|
685
|
+
out += "\n" + div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情, ${C.yellow}/session_search <关键词>${C.reset} 搜索`;
|
|
686
|
+
console.log(out);
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case "session": {
|
|
690
|
+
if (!rest) { console.log(`用法: /session <session_id 或列表序号>\n请先用 /sessions 查看列表`); break; }
|
|
691
|
+
const sessions = listSessions(999);
|
|
692
|
+
let sid = rest.trim();
|
|
693
|
+
if (/^\d+$/.test(sid)) {
|
|
694
|
+
const idx = parseInt(sid) - 1;
|
|
695
|
+
if (idx < 0 || idx >= sessions.length) { console.log(`序号超出范围 (1-${sessions.length})`); break; }
|
|
696
|
+
sid = sessions[idx].id;
|
|
697
|
+
}
|
|
698
|
+
const session = getSession(sid);
|
|
699
|
+
if (!session) { console.log(`未找到 session: ${sid}`); break; }
|
|
700
|
+
const turns = loadSessionTurns(sid, 30);
|
|
701
|
+
let out = `${C.bold + C.cyan}=== ${session.title || "(无标题)"} ===${C.reset}\n`;
|
|
702
|
+
out += `ID: ${C.dim}${session.id}${C.reset} | ${(session.started || "?").slice(0, 16)} | ${session.turnCount}轮\n` + div() + "\n";
|
|
703
|
+
if (turns.length === 0) { out += "(这个 session 暂无对话轮次)\n"; }
|
|
704
|
+
else {
|
|
705
|
+
for (const [i, t] of turns.entries()) {
|
|
706
|
+
out += `${C.yellow}[${i + 1}] ${(t.time || "?").slice(0, 16)}${C.reset}\n`;
|
|
707
|
+
if (t.cwd) out += ` ${C.dim}${t.cwd}${C.reset}\n`;
|
|
708
|
+
out += ` ${C.cyan}▸${C.reset} ${(t.user || "").slice(0, 400)}\n`;
|
|
709
|
+
out += ` ${C.green}◂${C.reset} ${(t.assistant || "").slice(0, 400)}\n\n`;
|
|
710
|
+
}
|
|
711
|
+
out += `(共 ${turns.length} 轮, 显示最近 30 轮)`;
|
|
712
|
+
}
|
|
713
|
+
out += "\n" + div();
|
|
714
|
+
await pipeToPager(out);
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case "session_search": {
|
|
718
|
+
if (!rest) { console.log("用法: /session_search <关键词> [数量]\n在整个历史中搜索相关 session"); break; }
|
|
719
|
+
const parts = rest.trim().split(/\s+/);
|
|
720
|
+
const maybeNum = parseInt(parts[parts.length - 1], 10);
|
|
721
|
+
const limit = !isNaN(maybeNum) ? maybeNum : 10;
|
|
722
|
+
const query = !isNaN(maybeNum) ? parts.slice(0, -1).join(" ") : rest.trim();
|
|
723
|
+
const results = globalSearch(query, limit);
|
|
724
|
+
if (results.length === 0) { console.log(`全史搜索未找到 "${query}"`); break; }
|
|
725
|
+
let out = `${C.bold + C.yellow}搜索: "${query}"${C.reset} — ${results.length} 个 session\n` + div() + "\n";
|
|
726
|
+
for (const [i, r] of results.entries()) {
|
|
727
|
+
out += `${C.yellow}${i + 1}.${C.reset} [${(r.sessionStarted || "?").slice(0, 16)}] ${r.sessionTitle} | ${r.matchCount}处 | 得分${r.totalScore}\n`;
|
|
728
|
+
for (const m of r.topMatches) {
|
|
729
|
+
out += ` ${C.dim}·${C.reset} ${m.user.slice(0, 100)}\n`;
|
|
730
|
+
}
|
|
731
|
+
out += "\n";
|
|
732
|
+
}
|
|
733
|
+
out += div() + `\n用 ${C.cyan}/session <编号>${C.reset} 查看详情`;
|
|
734
|
+
console.log(out);
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
665
737
|
case "compress": {
|
|
666
738
|
console.log(`${C.yellow}正在压缩上下文...${C.reset}`);
|
|
667
739
|
const result = await agent._handleAgentTool("compress_context", {});
|
|
@@ -963,6 +1035,7 @@ async function main() {
|
|
|
963
1035
|
`${C.cyan}tools${C.reset}`, `${C.cyan}status${C.reset}`, `${C.cyan}ctx${C.reset}`,
|
|
964
1036
|
`${C.cyan}temp${C.reset}`, `${C.cyan}token${C.reset}`,
|
|
965
1037
|
`${C.cyan}compress${C.reset}`, `${C.cyan}memory${C.reset}`, `${C.cyan}history${C.reset}`,
|
|
1038
|
+
`${C.cyan}sessions${C.reset}`, `${C.cyan}session_search${C.reset}`,
|
|
966
1039
|
`${C.cyan}tool_save${C.reset}`, `${C.cyan}tool_del${C.reset}`,
|
|
967
1040
|
];
|
|
968
1041
|
process.stdout.write("\n" + C.dim + menu.join(" ") + C.reset + "\n");
|