pigpig-agent 1.0.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/.skills/code-review/SKILL.md +42 -0
- package/dist/agent/loop-detection.js +135 -0
- package/dist/agent/loop.js +123 -0
- package/dist/agent/retry.js +39 -0
- package/dist/agents/registry.js +59 -0
- package/dist/agents/spawn.js +124 -0
- package/dist/agents/types.js +5 -0
- package/dist/commands/agent.js +33 -0
- package/dist/commands/context.js +27 -0
- package/dist/commands/cron.js +38 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/dream.js +36 -0
- package/dist/commands/index.js +10 -0
- package/dist/commands/memory.js +40 -0
- package/dist/commands/plugin.js +73 -0
- package/dist/commands/rag.js +25 -0
- package/dist/commands/security.js +44 -0
- package/dist/commands/skill.js +84 -0
- package/dist/config/init.js +69 -0
- package/dist/config/loader.js +52 -0
- package/dist/config/schema.js +55 -0
- package/dist/context/compressor.js +165 -0
- package/dist/context/defense.js +201 -0
- package/dist/context/prompt-builder.js +56 -0
- package/dist/context/prompt-pipes.js +14 -0
- package/dist/context/tool-result-output.js +25 -0
- package/dist/context/view.js +185 -0
- package/dist/cron/parser.js +27 -0
- package/dist/cron/service.js +211 -0
- package/dist/cron/store.js +53 -0
- package/dist/cron/types.js +1 -0
- package/dist/index.js +9 -0
- package/dist/main.js +270 -0
- package/dist/memory/store.js +175 -0
- package/dist/memory/validator.js +62 -0
- package/dist/mock-model.js +534 -0
- package/dist/plugins/manager.js +97 -0
- package/dist/plugins/supabase-plugin.js +111 -0
- package/dist/plugins/types.js +1 -0
- package/dist/rag/chunker.js +54 -0
- package/dist/rag/embedder.js +53 -0
- package/dist/rag/search.js +123 -0
- package/dist/rag/sqlite-store.js +195 -0
- package/dist/rag/store.js +29 -0
- package/dist/security/bash-classifier.js +39 -0
- package/dist/security/hook.js +53 -0
- package/dist/security/roles.js +16 -0
- package/dist/session/store.js +60 -0
- package/dist/skills/loader.js +100 -0
- package/dist/tools/cron-tools.js +94 -0
- package/dist/tools/file-tools.js +115 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/mcp-client.js +100 -0
- package/dist/tools/memory-tools.js +81 -0
- package/dist/tools/rag-tools.js +54 -0
- package/dist/tools/registry.js +270 -0
- package/dist/tools/search-tools.js +116 -0
- package/dist/tools/shell-tools.js +39 -0
- package/dist/tools/spawn-tools.js +35 -0
- package/dist/tools/tool-search.js +17 -0
- package/dist/tools/web-search.js +150 -0
- package/dist/usage/tracker.js +83 -0
- package/package.json +55 -0
- package/readme.md +131 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { agentLoop } from '../agent/loop.js';
|
|
2
|
+
const DREAM_PROMPT = [
|
|
3
|
+
'请对记忆库做一次完整的整理(dream),按以下四个阶段执行:',
|
|
4
|
+
'',
|
|
5
|
+
'**阶段 1:定位** — 用 memory lint 扫描全库(lint 结果已包含内容预览和问题清单,不需要再逐条 read)。',
|
|
6
|
+
'**阶段 2:整理** — 根据 lint 报告直接操作:',
|
|
7
|
+
' - 路径过期且长期未用的,直接 memory delete(传 filename)删掉',
|
|
8
|
+
' - 同名重复的,用 memory save 保存合并后的版本(同名自动覆盖),再 delete 多余的',
|
|
9
|
+
' - 内容仍然有效但描述不准确的,用 memory save 覆盖更新',
|
|
10
|
+
'**阶段 3:报告** — 用一段文字总结这次整理做了什么。',
|
|
11
|
+
'',
|
|
12
|
+
'注意:memory 的 read 和 delete 都需要传 filename 参数(如 project_deploy-process.md),不是 name。lint 报告里已经有 filename 了,直接用。',
|
|
13
|
+
].join('\n');
|
|
14
|
+
export const dreamCommands = [
|
|
15
|
+
(cmd, ctx) => {
|
|
16
|
+
if (cmd !== '/dream' && cmd !== 'dream')
|
|
17
|
+
return false;
|
|
18
|
+
console.log('\n[dream] 开始记忆整理...');
|
|
19
|
+
const userMsg = { role: 'user', content: DREAM_PROMPT };
|
|
20
|
+
ctx.messages.push(userMsg);
|
|
21
|
+
ctx.timestamps.set(ctx.messages.length - 1, Date.now());
|
|
22
|
+
ctx.sessionStore.append(userMsg);
|
|
23
|
+
const currentSystem = ctx.builder.build(ctx.makePromptCtx());
|
|
24
|
+
const beforeLen = ctx.messages.length;
|
|
25
|
+
agentLoop(ctx.model, ctx.registry, ctx.messages, currentSystem, ctx.tracker).then(() => {
|
|
26
|
+
const newMessages = ctx.messages.slice(beforeLen);
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (let i = beforeLen; i < ctx.messages.length; i++)
|
|
29
|
+
ctx.timestamps.set(i, now);
|
|
30
|
+
ctx.sessionStore.appendAll(newMessages);
|
|
31
|
+
console.log(` [dream 完成]\n`);
|
|
32
|
+
ctx.ask();
|
|
33
|
+
});
|
|
34
|
+
return 'async';
|
|
35
|
+
},
|
|
36
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const memoryCommands = [
|
|
2
|
+
(cmd, ctx) => {
|
|
3
|
+
if (cmd !== '/memory' && cmd !== 'memory')
|
|
4
|
+
return false;
|
|
5
|
+
const entries = ctx.memoryStore.list();
|
|
6
|
+
console.log(`\n[记忆系统] 共 ${entries.length} 条记忆`);
|
|
7
|
+
for (const e of entries)
|
|
8
|
+
console.log(` [${e.type}] ${e.name} — ${e.description}`);
|
|
9
|
+
console.log('');
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
(cmd, ctx) => {
|
|
13
|
+
if (!cmd.startsWith('/memory search '))
|
|
14
|
+
return false;
|
|
15
|
+
const query = cmd.slice('/memory search '.length).trim();
|
|
16
|
+
const results = ctx.memoryStore.search(query);
|
|
17
|
+
console.log(`\n[记忆搜索] "${query}" → ${results.length} 条结果`);
|
|
18
|
+
for (const e of results)
|
|
19
|
+
console.log(` [${e.type}] ${e.name} — ${e.description}`);
|
|
20
|
+
console.log('');
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
(cmd, ctx) => {
|
|
24
|
+
if (cmd !== '/lint' && cmd !== 'lint')
|
|
25
|
+
return false;
|
|
26
|
+
const reports = ctx.memoryStore.lint();
|
|
27
|
+
if (reports.length === 0) {
|
|
28
|
+
console.log('\n[lint] 记忆库健康,没有发现问题。\n');
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
console.log(`\n[lint] 记忆库 ${reports.length} 条有警告:`);
|
|
32
|
+
for (const r of reports) {
|
|
33
|
+
console.log(` 📁 ${r.entry.filePath.split('/').pop()} [${r.entry.type}] ${r.entry.name}`);
|
|
34
|
+
for (const issue of r.issues)
|
|
35
|
+
console.log(` • ${issue.kind}: ${issue.message}`);
|
|
36
|
+
}
|
|
37
|
+
console.log('');
|
|
38
|
+
return true;
|
|
39
|
+
},
|
|
40
|
+
];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function createPluginCommands(pluginManager, availablePlugins) {
|
|
2
|
+
return [
|
|
3
|
+
// /plugin 或 /plugin list
|
|
4
|
+
(cmd, _ctx) => {
|
|
5
|
+
if (cmd !== '/plugin' && cmd !== '/plugin list')
|
|
6
|
+
return false;
|
|
7
|
+
const loaded = pluginManager.list();
|
|
8
|
+
const unloaded = Array.from(availablePlugins.entries())
|
|
9
|
+
.filter(([name]) => !loaded.find(p => p.name === name));
|
|
10
|
+
if (loaded.length === 0 && unloaded.length === 0) {
|
|
11
|
+
console.log('\n[plugins] 没有可用的插件。\n');
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
console.log('\n[plugins]');
|
|
15
|
+
if (loaded.length > 0) {
|
|
16
|
+
console.log(' 已加载:');
|
|
17
|
+
for (const p of loaded) {
|
|
18
|
+
console.log(` ${p.name} v${p.version} — ${p.description}`);
|
|
19
|
+
console.log(` 工具: ${p.tools.join(', ')}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (unloaded.length > 0) {
|
|
23
|
+
console.log(' 可加载:');
|
|
24
|
+
for (const [name, def] of unloaded) {
|
|
25
|
+
console.log(` ${name} v${def.version} — ${def.description}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
console.log('');
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
// /plugin load <name>
|
|
32
|
+
(cmd, _ctx) => {
|
|
33
|
+
const match = cmd.match(/^\/plugin\s+load\s+(\S+)$/);
|
|
34
|
+
if (!match)
|
|
35
|
+
return false;
|
|
36
|
+
const name = match[1];
|
|
37
|
+
const def = availablePlugins.get(name);
|
|
38
|
+
if (!def) {
|
|
39
|
+
console.log(`\n[plugins] 找不到插件: ${name}\n`);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (pluginManager.get(name)) {
|
|
43
|
+
console.log(`\n[plugins] ${name} 已经加载了\n`);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
pluginManager.load(def).then(tools => {
|
|
47
|
+
console.log(`\n[plugins] 已加载 ${name},注册了 ${tools.length} 个工具:`);
|
|
48
|
+
for (const t of tools)
|
|
49
|
+
console.log(` ${t}`);
|
|
50
|
+
console.log('');
|
|
51
|
+
}).catch(err => {
|
|
52
|
+
console.log(`\n[plugins] 加载 ${name} 失败: ${err.message}\n`);
|
|
53
|
+
});
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
// /plugin unload <name>
|
|
57
|
+
(cmd, _ctx) => {
|
|
58
|
+
const match = cmd.match(/^\/plugin\s+unload\s+(\S+)$/);
|
|
59
|
+
if (!match)
|
|
60
|
+
return false;
|
|
61
|
+
const name = match[1];
|
|
62
|
+
pluginManager.unload(name).then(ok => {
|
|
63
|
+
if (ok) {
|
|
64
|
+
console.log(`\n[plugins] 已卸载 ${name},相关工具已移除\n`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
console.log(`\n[plugins] ${name} 未加载\n`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return true;
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const ragCommands = [
|
|
2
|
+
(cmd, ctx) => {
|
|
3
|
+
if (cmd !== '/rag' && cmd !== 'rag')
|
|
4
|
+
return false;
|
|
5
|
+
const vs = ctx.vectorStore;
|
|
6
|
+
console.log(`\n[知识库] ${vs.size()} 个片段`);
|
|
7
|
+
const sources = vs.sources();
|
|
8
|
+
if (sources.length > 0)
|
|
9
|
+
console.log(` 来源: ${sources.join(', ')}`);
|
|
10
|
+
console.log('');
|
|
11
|
+
return true;
|
|
12
|
+
},
|
|
13
|
+
(cmd, ctx) => {
|
|
14
|
+
if (!cmd.startsWith('ingest '))
|
|
15
|
+
return false;
|
|
16
|
+
const path = cmd.slice('ingest '.length).trim();
|
|
17
|
+
console.log(`\n[导入] 正在处理 ${path}...`);
|
|
18
|
+
const ragIngestTool = ctx.registry.getActiveTools().find(t => t.name === 'rag_ingest');
|
|
19
|
+
ragIngestTool.execute({ path }).then((result) => {
|
|
20
|
+
console.log(` ${result}\n`);
|
|
21
|
+
ctx.ask();
|
|
22
|
+
});
|
|
23
|
+
return 'async';
|
|
24
|
+
},
|
|
25
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function createSecurityCommands(registry, hookPipeline) {
|
|
2
|
+
return [
|
|
3
|
+
// /role [owner|collaborator|guest]
|
|
4
|
+
(cmd, _ctx) => {
|
|
5
|
+
const match = cmd.match(/^\/role(?:\s+(owner|collaborator|guest))?$/);
|
|
6
|
+
if (!match)
|
|
7
|
+
return false;
|
|
8
|
+
if (match[1]) {
|
|
9
|
+
const role = match[1];
|
|
10
|
+
registry.setRole(role);
|
|
11
|
+
const toolCount = registry.getActiveTools().length;
|
|
12
|
+
console.log(`\n[security] 角色切换为 ${role},可用工具: ${toolCount} 个\n`);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
const role = registry.getRole();
|
|
16
|
+
const toolCount = registry.getActiveTools().length;
|
|
17
|
+
console.log(`\n[security] 当前角色: ${role},可用工具: ${toolCount} 个\n`);
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
},
|
|
21
|
+
// /hooks
|
|
22
|
+
(cmd, _ctx) => {
|
|
23
|
+
if (cmd !== '/hooks')
|
|
24
|
+
return false;
|
|
25
|
+
const hooks = hookPipeline.list();
|
|
26
|
+
console.log('\n[hooks]');
|
|
27
|
+
if (hooks.pre.length > 0) {
|
|
28
|
+
console.log(' Pre-Tool Hooks:');
|
|
29
|
+
for (const name of hooks.pre)
|
|
30
|
+
console.log(` - ${name}`);
|
|
31
|
+
}
|
|
32
|
+
if (hooks.post.length > 0) {
|
|
33
|
+
console.log(' Post-Tool Hooks:');
|
|
34
|
+
for (const name of hooks.post)
|
|
35
|
+
console.log(` - ${name}`);
|
|
36
|
+
}
|
|
37
|
+
if (hooks.pre.length === 0 && hooks.post.length === 0) {
|
|
38
|
+
console.log(' 没有注册的 Hook');
|
|
39
|
+
}
|
|
40
|
+
console.log('');
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { agentLoop } from '../agent/loop.js';
|
|
2
|
+
export function createSkillCommands(skillLoader, activeSkills) {
|
|
3
|
+
return [
|
|
4
|
+
// /skill list
|
|
5
|
+
(cmd, ctx) => {
|
|
6
|
+
if (cmd !== '/skill' && cmd !== '/skill list' && cmd !== 'skill list')
|
|
7
|
+
return false;
|
|
8
|
+
const skills = skillLoader.list();
|
|
9
|
+
if (skills.length === 0) {
|
|
10
|
+
console.log('\n[skills] 没有找到任何 skill。在 .skills/ 目录下创建 skill-name/SKILL.md 即可。\n');
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
console.log(`\n[skills] 共 ${skills.length} 个可用:`);
|
|
14
|
+
for (const s of skills) {
|
|
15
|
+
const active = activeSkills.has(s.name) ? ' ✓ 已激活' : '';
|
|
16
|
+
console.log(` /${s.name} — ${s.description}${active}`);
|
|
17
|
+
if (s.whenToUse)
|
|
18
|
+
console.log(` 适用场景: ${s.whenToUse}`);
|
|
19
|
+
}
|
|
20
|
+
console.log('');
|
|
21
|
+
return true;
|
|
22
|
+
},
|
|
23
|
+
// /skill load <name>
|
|
24
|
+
(cmd, ctx) => {
|
|
25
|
+
const match = cmd.match(/^\/skill\s+load\s+(\S+)$/);
|
|
26
|
+
if (!match)
|
|
27
|
+
return false;
|
|
28
|
+
const name = match[1];
|
|
29
|
+
const skill = skillLoader.get(name);
|
|
30
|
+
if (!skill) {
|
|
31
|
+
console.log(`\n[skills] 找不到 skill: ${name}\n`);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
activeSkills.add(name);
|
|
35
|
+
console.log(`\n[skills] 已激活: ${name} — ${skill.description}\n`);
|
|
36
|
+
return true;
|
|
37
|
+
},
|
|
38
|
+
// /skill unload <name>
|
|
39
|
+
(cmd, ctx) => {
|
|
40
|
+
const match = cmd.match(/^\/skill\s+unload\s+(\S+)$/);
|
|
41
|
+
if (!match)
|
|
42
|
+
return false;
|
|
43
|
+
const name = match[1];
|
|
44
|
+
if (!activeSkills.has(name)) {
|
|
45
|
+
console.log(`\n[skills] ${name} 未激活\n`);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
activeSkills.delete(name);
|
|
49
|
+
console.log(`\n[skills] 已卸载: ${name}\n`);
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
// /<skill-name> — 直接用 /code-review 激活并触发
|
|
53
|
+
(cmd, ctx) => {
|
|
54
|
+
if (!cmd.startsWith('/'))
|
|
55
|
+
return false;
|
|
56
|
+
const parts = cmd.slice(1).split(/\s+/);
|
|
57
|
+
const name = parts[0];
|
|
58
|
+
const skill = skillLoader.get(name);
|
|
59
|
+
if (!skill)
|
|
60
|
+
return false;
|
|
61
|
+
activeSkills.add(name);
|
|
62
|
+
console.log(`\n[skills] 激活 ${name},开始执行...`);
|
|
63
|
+
const args = parts.slice(1).join(' ');
|
|
64
|
+
const content = args
|
|
65
|
+
? `${skill.content}\n\n用户指令: ${args}`
|
|
66
|
+
: skill.content;
|
|
67
|
+
const userMsg = { role: 'user', content };
|
|
68
|
+
ctx.messages.push(userMsg);
|
|
69
|
+
ctx.timestamps.set(ctx.messages.length - 1, Date.now());
|
|
70
|
+
ctx.sessionStore.append(userMsg);
|
|
71
|
+
const currentSystem = ctx.builder.build(ctx.makePromptCtx());
|
|
72
|
+
const beforeLen = ctx.messages.length;
|
|
73
|
+
agentLoop(ctx.model, ctx.registry, ctx.messages, currentSystem, ctx.tracker).then(() => {
|
|
74
|
+
const newMessages = ctx.messages.slice(beforeLen);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
for (let i = beforeLen; i < ctx.messages.length; i++)
|
|
77
|
+
ctx.timestamps.set(i, now);
|
|
78
|
+
ctx.sessionStore.appendAll(newMessages);
|
|
79
|
+
ctx.ask();
|
|
80
|
+
});
|
|
81
|
+
return 'async';
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createInterface } from 'node:readline';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { CONFIG_FILE } from './loader.js';
|
|
4
|
+
export async function runInit() {
|
|
5
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6
|
+
const ask = (q) => new Promise((resolve) => {
|
|
7
|
+
console.log(q);
|
|
8
|
+
rl.question(' > ', resolve);
|
|
9
|
+
});
|
|
10
|
+
console.log('\n Pigpig Agent 初始化向导\n');
|
|
11
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
const overwrite = await ask(` ${CONFIG_FILE} 已存在,覆盖? (y/N): `);
|
|
13
|
+
if (overwrite.toLowerCase() !== 'y') {
|
|
14
|
+
console.log(' 已取消\n');
|
|
15
|
+
rl.close();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
console.log(' 选择模型:\n');
|
|
20
|
+
console.log(' 1. glm-5 (推荐,均衡)');
|
|
21
|
+
console.log(' 2. qwen-turbo-latest (快速,便宜)');
|
|
22
|
+
console.log(' 3. qwen-max-latest (最强,贵)\n');
|
|
23
|
+
const modelChoice = (await ask(' 模型 [1]: ')) || '1';
|
|
24
|
+
const models = {
|
|
25
|
+
'1': 'glm-5',
|
|
26
|
+
'2': 'qwen-turbo-latest',
|
|
27
|
+
'3': 'qwen-max-latest',
|
|
28
|
+
};
|
|
29
|
+
const modelName = models[modelChoice] || 'glm-5';
|
|
30
|
+
const apiKey = await ask('\n DashScope API Key (留空则从环境变量 DASHSCOPE_API_KEY 读取): ');
|
|
31
|
+
// const enableFeishu = (await ask('\n 启用飞书 Channel? (y/N): ')).toLowerCase() === 'y';
|
|
32
|
+
// let feishuAppId = '';
|
|
33
|
+
// let feishuAppSecret = '';
|
|
34
|
+
// if (enableFeishu) {
|
|
35
|
+
// feishuAppId = await ask(' 飞书 App ID: ');
|
|
36
|
+
// feishuAppSecret = await ask(' 飞书 App Secret: ');
|
|
37
|
+
// }
|
|
38
|
+
const concurrentStr = await ask('\n 子 Agent 最大并发数 [3]: ');
|
|
39
|
+
const maxConcurrent = parseInt(concurrentStr) || 3;
|
|
40
|
+
const config = {
|
|
41
|
+
version: '1.0',
|
|
42
|
+
model: {
|
|
43
|
+
provider: 'dashscope',
|
|
44
|
+
name: modelName,
|
|
45
|
+
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
46
|
+
apiKey: apiKey || '${DASHSCOPE_API_KEY}',
|
|
47
|
+
},
|
|
48
|
+
plugins: [{ name: 'supabase', enabled: false, config: {} }],
|
|
49
|
+
channels: {},
|
|
50
|
+
agents: { maxSpawnDepth: 1, maxConcurrent, defaultTimeout: 600000 },
|
|
51
|
+
security: { defaultRole: 'developer', auditLog: true, bashTimestamp: true },
|
|
52
|
+
memory: { dataDir: '.' },
|
|
53
|
+
rag: { enabled: true, docsDir: 'docs' },
|
|
54
|
+
cron: { enabled: true, dataDir: '.' },
|
|
55
|
+
session: { id: 'default' },
|
|
56
|
+
usage: { trackingFile: '.usage/today.jsonl' },
|
|
57
|
+
};
|
|
58
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
|
|
59
|
+
console.log(`\n ✓ ${CONFIG_FILE} 已生成`);
|
|
60
|
+
const envLines = [];
|
|
61
|
+
if (apiKey)
|
|
62
|
+
envLines.push(`DASHSCOPE_API_KEY=${apiKey}`);
|
|
63
|
+
if (envLines.length > 0) {
|
|
64
|
+
fs.writeFileSync('.env', envLines.join('\n') + '\n');
|
|
65
|
+
console.log(' ✓ .env 已生成');
|
|
66
|
+
}
|
|
67
|
+
console.log('\n 启动 Agent: pigpig-agent\n');
|
|
68
|
+
rl.close();
|
|
69
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { SuperAgentConfigSchema } from './schema.js';
|
|
3
|
+
export const CONFIG_FILE = 'pigpig-agent.config.json';
|
|
4
|
+
const ENV_VAR_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
5
|
+
function substituteEnvVars(obj) {
|
|
6
|
+
if (typeof obj === 'string') {
|
|
7
|
+
return obj.replace(ENV_VAR_RE, (match, name) => {
|
|
8
|
+
const val = process.env[name];
|
|
9
|
+
if (val === undefined) {
|
|
10
|
+
console.warn(` ⚠ 环境变量 ${name} 未设置,保留原值`);
|
|
11
|
+
return match;
|
|
12
|
+
}
|
|
13
|
+
return val;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(obj))
|
|
17
|
+
return obj.map(substituteEnvVars);
|
|
18
|
+
if (obj !== null && typeof obj === 'object') {
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
21
|
+
result[key] = substituteEnvVars(value);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
return obj;
|
|
26
|
+
}
|
|
27
|
+
export function loadConfig(path = CONFIG_FILE) {
|
|
28
|
+
if (!fs.existsSync(path)) {
|
|
29
|
+
console.log(` 未找到 ${path},使用默认配置`);
|
|
30
|
+
console.log(' 运行 pigpig-agent init 生成配置文件\n');
|
|
31
|
+
return SuperAgentConfigSchema.parse({});
|
|
32
|
+
}
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = JSON.parse(fs.readFileSync(path, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error(` ✗ 解析 ${path} 失败: ${err.message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const substituted = substituteEnvVars(raw);
|
|
42
|
+
const result = SuperAgentConfigSchema.safeParse(substituted);
|
|
43
|
+
if (!result.success) {
|
|
44
|
+
console.error(' ✗ 配置文件校验失败:');
|
|
45
|
+
for (const issue of result.error.issues) {
|
|
46
|
+
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
console.log(` ✓ 已加载 ${path}`);
|
|
51
|
+
return result.data;
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const ModelConfigSchema = z.object({
|
|
3
|
+
provider: z.enum(['dashscope', 'openai', 'custom']).default('dashscope'),
|
|
4
|
+
name: z.string().default('glm-5'),
|
|
5
|
+
baseURL: z.string().default('https://dashscope.aliyuncs.com/compatible-mode/v1'),
|
|
6
|
+
apiKey: z.string().default(''),
|
|
7
|
+
});
|
|
8
|
+
export const PluginConfigSchema = z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
enabled: z.boolean().default(true),
|
|
11
|
+
config: z.record(z.string(), z.any()).default({}),
|
|
12
|
+
});
|
|
13
|
+
export const AgentConfigSchema = z.object({
|
|
14
|
+
maxSpawnDepth: z.number().min(0).max(5).default(1),
|
|
15
|
+
maxConcurrent: z.number().min(1).max(10).default(3),
|
|
16
|
+
defaultTimeout: z.number().default(600000),
|
|
17
|
+
});
|
|
18
|
+
export const SecurityConfigSchema = z.object({
|
|
19
|
+
defaultRole: z.string().default('developer'),
|
|
20
|
+
auditLog: z.boolean().default(true),
|
|
21
|
+
bashTimestamp: z.boolean().default(true),
|
|
22
|
+
});
|
|
23
|
+
export const MemoryConfigSchema = z.object({
|
|
24
|
+
dataDir: z.string().default('.'),
|
|
25
|
+
});
|
|
26
|
+
export const RagConfigSchema = z.object({
|
|
27
|
+
enabled: z.boolean().default(true),
|
|
28
|
+
docsDir: z.string().default('docs'),
|
|
29
|
+
});
|
|
30
|
+
export const CronConfigSchema = z.object({
|
|
31
|
+
enabled: z.boolean().default(true),
|
|
32
|
+
dataDir: z.string().default('.'),
|
|
33
|
+
});
|
|
34
|
+
export const SessionConfigSchema = z.object({
|
|
35
|
+
id: z.string().default('default'),
|
|
36
|
+
});
|
|
37
|
+
export const UsageConfigSchema = z.object({
|
|
38
|
+
trackingFile: z.string().default('.usage/today.jsonl'),
|
|
39
|
+
});
|
|
40
|
+
// Zod v4 中 ZodObject.default() 的入参须是完整 output 类型(字段全必填),
|
|
41
|
+
// 即使内部字段都带 default,传 {} 也会类型报错。
|
|
42
|
+
// 这里用各 schema.parse({}) 生成应用全部默认值后的完整对象作为默认值,
|
|
43
|
+
// 语义与 Zod v3 的 .default({}) 一致:配置缺省该段时套用嵌套默认值。
|
|
44
|
+
export const SuperAgentConfigSchema = z.object({
|
|
45
|
+
version: z.string().default('1.0'),
|
|
46
|
+
model: ModelConfigSchema.default(ModelConfigSchema.parse({})),
|
|
47
|
+
plugins: z.array(PluginConfigSchema).default([]),
|
|
48
|
+
agents: AgentConfigSchema.default(AgentConfigSchema.parse({})),
|
|
49
|
+
security: SecurityConfigSchema.default(SecurityConfigSchema.parse({})),
|
|
50
|
+
memory: MemoryConfigSchema.default(MemoryConfigSchema.parse({})),
|
|
51
|
+
rag: RagConfigSchema.default(RagConfigSchema.parse({})),
|
|
52
|
+
cron: CronConfigSchema.default(CronConfigSchema.parse({})),
|
|
53
|
+
session: SessionConfigSchema.default(SessionConfigSchema.parse({})),
|
|
54
|
+
usage: UsageConfigSchema.default(UsageConfigSchema.parse({})),
|
|
55
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { generateText } from 'ai';
|
|
2
|
+
import { textToolResultOutput, toolResultOutputToText } from './tool-result-output.js';
|
|
3
|
+
// 计算对话历史的token数量
|
|
4
|
+
export function estimateTokens(messages) {
|
|
5
|
+
let chars = 0;
|
|
6
|
+
for (const msg of messages) {
|
|
7
|
+
if (typeof msg.content === 'string') {
|
|
8
|
+
chars += msg.content.length;
|
|
9
|
+
}
|
|
10
|
+
else if (Array.isArray(msg.content)) {
|
|
11
|
+
for (const part of msg.content) {
|
|
12
|
+
if ('text' in part && typeof part.text === 'string') {
|
|
13
|
+
chars += part.text.length;
|
|
14
|
+
}
|
|
15
|
+
else if ('output' in part) {
|
|
16
|
+
chars += toolResultOutputToText(part.output).length;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Math.ceil(chars / 4);
|
|
22
|
+
}
|
|
23
|
+
// 允许被压缩的工具
|
|
24
|
+
const CLEARABLE_TOOLS = new Set([
|
|
25
|
+
'read_file', 'bash', 'glob', 'list_directory', 'edit_file', 'write_file'
|
|
26
|
+
]);
|
|
27
|
+
const KEEP_RECENT_TOOL_RESULT = 3; // 保留最近的3个工具调用结果
|
|
28
|
+
// 1. 紧凑压缩 只将工具调用结果输出压缩为[tool result cleared]格式
|
|
29
|
+
export function microcompact(messages) {
|
|
30
|
+
// 找到所有的工具调用的索引位置
|
|
31
|
+
const toolResultIndices = [];
|
|
32
|
+
for (let i = 0; i < messages.length; i++) {
|
|
33
|
+
if (messages[i].role === 'tool') {
|
|
34
|
+
toolResultIndices.push(i);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// 保留最近的3个工具调用结果
|
|
38
|
+
const toClear = toolResultIndices.slice(0, Math.max(0, toolResultIndices.length - KEEP_RECENT_TOOL_RESULT));
|
|
39
|
+
let cleared = 0;
|
|
40
|
+
const result = messages.map((msg, idx) => {
|
|
41
|
+
if (!toClear.includes(idx))
|
|
42
|
+
return msg;
|
|
43
|
+
if (msg.role !== 'tool' || !Array.isArray(msg.content))
|
|
44
|
+
return msg;
|
|
45
|
+
// 不允许压缩的工具
|
|
46
|
+
const toolName = msg.content[0].toolName || 'unknown';
|
|
47
|
+
if (!CLEARABLE_TOOLS.has(toolName))
|
|
48
|
+
return msg;
|
|
49
|
+
cleared++;
|
|
50
|
+
return {
|
|
51
|
+
...msg,
|
|
52
|
+
content: msg.content.map((part) => ({
|
|
53
|
+
...part,
|
|
54
|
+
output: textToolResultOutput(`[tool result cleared ]`),
|
|
55
|
+
}))
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
messages: result,
|
|
60
|
+
cleared,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// 2. 摘要压缩 LLM摘要压缩
|
|
64
|
+
const COMPRESS_PROMPT = `你是一个对话压缩系统。你的任务是把 Agent 和用户之间的
|
|
65
|
+
对话历史压缩成一份结构化摘要,确保后续对话能够无缝继续。
|
|
66
|
+
|
|
67
|
+
请严格按照以下模板输出,每个字段都要填写:
|
|
68
|
+
|
|
69
|
+
## 用户意图
|
|
70
|
+
(用户在这次对话中想要完成什么)
|
|
71
|
+
|
|
72
|
+
## 已完成的操作
|
|
73
|
+
(Agent 执行了哪些工具调用、产生了什么结果)
|
|
74
|
+
|
|
75
|
+
## 关键发现
|
|
76
|
+
(读取的文件内容要点、搜索结果、命令输出中的关键信息)
|
|
77
|
+
|
|
78
|
+
## 当前状态
|
|
79
|
+
(对话进行到哪一步了、还有什么没做完)
|
|
80
|
+
|
|
81
|
+
## 需要保留的细节
|
|
82
|
+
(文件路径、变量名、配置值、错误信息等不能丢失的具体内容)
|
|
83
|
+
|
|
84
|
+
注意事项:
|
|
85
|
+
- 用对话中使用的语言输出
|
|
86
|
+
- 文件路径、UUID、版本号等标识符必须原样保留,不要翻译或改写
|
|
87
|
+
- 不要写笼统的概述,只保留具体的、可操作的信息
|
|
88
|
+
- 总长度控制在 800 字以内`;
|
|
89
|
+
const CONTEXT_TOKEN_THRESHOLD = 300; // 如果当前的对话消息开销<30000token,就不摘要
|
|
90
|
+
const KEEP_RECENT_MESSAGES = 6; // 保留最近的6条消息的原始内容
|
|
91
|
+
export async function summarize(model, messages, existingSummary) {
|
|
92
|
+
const tokenEstimate = estimateTokens(messages);
|
|
93
|
+
if (tokenEstimate < CONTEXT_TOKEN_THRESHOLD || messages.length <= KEEP_RECENT_MESSAGES) {
|
|
94
|
+
return {
|
|
95
|
+
messages,
|
|
96
|
+
summary: existingSummary || '',
|
|
97
|
+
compressedCount: 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const splitIdx = Math.max(0, messages.length - KEEP_RECENT_MESSAGES);
|
|
101
|
+
let alignedIdx = splitIdx;
|
|
102
|
+
while (alignedIdx > 0 && messages[alignedIdx].role !== 'user') { // 如果要摘要的数据最后一条不是用户消息,那就往前多退一条
|
|
103
|
+
alignedIdx--;
|
|
104
|
+
}
|
|
105
|
+
if (alignedIdx === 0) {
|
|
106
|
+
return {
|
|
107
|
+
messages,
|
|
108
|
+
summary: existingSummary || '',
|
|
109
|
+
compressedCount: 0,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const toCompress = messages.slice(0, alignedIdx); // 要压缩的消息
|
|
113
|
+
const toKeep = messages.slice(alignedIdx); // 要保留的消息
|
|
114
|
+
// 将用户消息、Agent消息、工具调用结果拼接为一个字符串
|
|
115
|
+
const conversationText = toCompress
|
|
116
|
+
.map(msg => {
|
|
117
|
+
const content = typeof msg.content === 'string'
|
|
118
|
+
? msg.content
|
|
119
|
+
: Array.isArray(msg.content)
|
|
120
|
+
? msg.content.map(part => 'text' in part
|
|
121
|
+
? part.text
|
|
122
|
+
: 'output' in part
|
|
123
|
+
? toolResultOutputToText(part.output)
|
|
124
|
+
: '').join('')
|
|
125
|
+
: '';
|
|
126
|
+
return content ? `**${msg.role}**: ${content}` : '';
|
|
127
|
+
})
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.join('\n\n');
|
|
130
|
+
if (!conversationText.trim()) {
|
|
131
|
+
return {
|
|
132
|
+
messages,
|
|
133
|
+
summary: existingSummary || '',
|
|
134
|
+
compressedCount: 0,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const userPrompt = existingSummary
|
|
138
|
+
? `## 已有摘要(上一次压缩的结果)\n\n${existingSummary}\n\n## 需要压缩的新对话\n\n${conversationText}`
|
|
139
|
+
: conversationText;
|
|
140
|
+
try {
|
|
141
|
+
const { text: summary } = await generateText({
|
|
142
|
+
model,
|
|
143
|
+
system: COMPRESS_PROMPT,
|
|
144
|
+
prompt: userPrompt,
|
|
145
|
+
});
|
|
146
|
+
const summaryMessage = {
|
|
147
|
+
role: 'user',
|
|
148
|
+
content: `[以下是之前对话的压缩摘要]\n\n${summary}\n\n[摘要结束,以下是最近的对话]`,
|
|
149
|
+
};
|
|
150
|
+
const newMessages = [summaryMessage, ...toKeep];
|
|
151
|
+
return {
|
|
152
|
+
messages: newMessages,
|
|
153
|
+
summary,
|
|
154
|
+
compressedCount: toCompress.length,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error('[Compaction] LLM摘要失败:', error);
|
|
159
|
+
return {
|
|
160
|
+
messages,
|
|
161
|
+
summary: existingSummary || '',
|
|
162
|
+
compressedCount: 0,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|