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,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Model v0.14 — Skills
|
|
3
|
+
*
|
|
4
|
+
* 在 v0.12 RAG 的基础上,新增对记忆维护场景的意图识别:
|
|
5
|
+
* - "lint 记忆 / 检查记忆" → memory action=lint
|
|
6
|
+
* - "搜记��� xxx / 找记忆 xxx" → memory action=search(结果走 BM25)
|
|
7
|
+
*
|
|
8
|
+
* 拿 system + tools 的指纹做"前缀稳定性"判断:
|
|
9
|
+
* - 第一次见的 prefix → 全部记 cacheWrite
|
|
10
|
+
* - 跟上一次一模一样 → 全部记 cacheRead
|
|
11
|
+
* - prefix 变了(system 改了、工具增减、注入了时间戳)→ 又一次 cacheWrite
|
|
12
|
+
*/
|
|
13
|
+
let retryTestCount = 0;
|
|
14
|
+
let lastPrefixHash = null;
|
|
15
|
+
let cacheEnabled = true;
|
|
16
|
+
export function setCacheEnabled(enabled) {
|
|
17
|
+
cacheEnabled = enabled;
|
|
18
|
+
if (!enabled)
|
|
19
|
+
lastPrefixHash = null;
|
|
20
|
+
}
|
|
21
|
+
function simpleHash(s) {
|
|
22
|
+
let h = 0;
|
|
23
|
+
for (let i = 0; i < s.length; i++) {
|
|
24
|
+
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
|
|
25
|
+
}
|
|
26
|
+
return h.toString(36);
|
|
27
|
+
}
|
|
28
|
+
function approxTokensFromChars(chars) {
|
|
29
|
+
return Math.ceil(chars / 3.5);
|
|
30
|
+
}
|
|
31
|
+
function extractSystemContent(prompt) {
|
|
32
|
+
const sys = (prompt || []).find((m) => m.role === 'system');
|
|
33
|
+
if (!sys)
|
|
34
|
+
return '';
|
|
35
|
+
if (typeof sys.content === 'string')
|
|
36
|
+
return sys.content;
|
|
37
|
+
if (Array.isArray(sys.content))
|
|
38
|
+
return sys.content.map((c) => c.text || '').join('');
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
function approxMessageTokens(prompt) {
|
|
42
|
+
let chars = 0;
|
|
43
|
+
for (const m of prompt || []) {
|
|
44
|
+
if (m.role === 'system')
|
|
45
|
+
continue;
|
|
46
|
+
if (typeof m.content === 'string')
|
|
47
|
+
chars += m.content.length;
|
|
48
|
+
else if (Array.isArray(m.content)) {
|
|
49
|
+
for (const c of m.content) {
|
|
50
|
+
if (c.type === 'text')
|
|
51
|
+
chars += (c.text || '').length;
|
|
52
|
+
else if (c.type === 'tool-call')
|
|
53
|
+
chars += JSON.stringify(c.input || {}).length + 80;
|
|
54
|
+
else if (c.type === 'tool-result') {
|
|
55
|
+
const out = c.output;
|
|
56
|
+
if (typeof out === 'string')
|
|
57
|
+
chars += out.length;
|
|
58
|
+
else if (out?.value)
|
|
59
|
+
chars += String(out.value).length;
|
|
60
|
+
else
|
|
61
|
+
chars += JSON.stringify(out || {}).length;
|
|
62
|
+
chars += 80;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return approxTokensFromChars(chars);
|
|
68
|
+
}
|
|
69
|
+
/** 根据 prompt 算这次调用的 usage,并模拟 cache 命中。 */
|
|
70
|
+
function makeUsage(prompt, outputChars = 80) {
|
|
71
|
+
const system = extractSystemContent(prompt);
|
|
72
|
+
const prefixContent = system;
|
|
73
|
+
const prefixTokens = approxTokensFromChars(prefixContent.length);
|
|
74
|
+
const messageTokens = approxMessageTokens(prompt);
|
|
75
|
+
const outputTokens = approxTokensFromChars(outputChars);
|
|
76
|
+
// 真实模型最小阈值各家不一(Qwen implicit 256、OpenAI 1024、Sonnet 4.7 2048、Opus 4.7 4096)。
|
|
77
|
+
// 课程里用 512 让普通 SYSTEM 也能演示 cache 行为,等到讲生产配置时再讲各家阈值差异。
|
|
78
|
+
const MIN_CACHE = 512;
|
|
79
|
+
const cacheable = cacheEnabled && prefixTokens >= MIN_CACHE;
|
|
80
|
+
const prefixHash = cacheable ? simpleHash(prefixContent) : null;
|
|
81
|
+
let cacheRead = 0;
|
|
82
|
+
let cacheWrite = 0;
|
|
83
|
+
let input = messageTokens;
|
|
84
|
+
if (cacheable) {
|
|
85
|
+
if (lastPrefixHash === prefixHash) {
|
|
86
|
+
cacheRead = prefixTokens;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
cacheWrite = prefixTokens;
|
|
90
|
+
}
|
|
91
|
+
lastPrefixHash = prefixHash;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
input += prefixTokens;
|
|
95
|
+
lastPrefixHash = null;
|
|
96
|
+
}
|
|
97
|
+
// 返回 AI SDK v5 标准字段(number),跟真实模型一致
|
|
98
|
+
// cacheCreationInputTokens 是 Anthropic provider 元数据里的字段名,AI SDK 透传
|
|
99
|
+
return {
|
|
100
|
+
inputTokens: input,
|
|
101
|
+
outputTokens: outputTokens,
|
|
102
|
+
totalTokens: input + outputTokens,
|
|
103
|
+
cachedInputTokens: cacheRead,
|
|
104
|
+
cacheCreationInputTokens: cacheWrite,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const TEXT_RESPONSES = {
|
|
108
|
+
default: '你好!我是 Super Agent v0.15——现在支持 Plugin 动态加载了。试试 /plugin 看看已加载的插件,或者让我帮你查数据库。',
|
|
109
|
+
greeting: '你好!我是 Super Agent v0.15,支持 Plugin 扩展。试试让我查数据库或者 /plugin 管理插件 :)',
|
|
110
|
+
memorySaved: '好的,我已经把这条信息存到记忆里了。下次你重新打开对话,我还会记得这件事。',
|
|
111
|
+
memoryRecalled: '让我查一下记忆...',
|
|
112
|
+
lintFinished: '记忆库 lint 跑完了,详细情况看上面的报告。建议清理掉那些路径已经不存在的条目,或者把同名的合并一下。',
|
|
113
|
+
dreamFinished: '记忆整理完成!这次做了以下操作:\n\n- 删除了 old-build-config(webpack.config.js 已不存在,854 天没读过)\n- 删除了 deploy-process-2(与 deploy-process 重名的早期版本)\n- 保留了 legacy-auth-module(路径过期但内容可能还有参考价值,建议手动更新)\n- 保留了 deploy-process(路径需要更新但部署流程本身还有用)\n- typescript-preference 健康,无需处理\n\n记忆库从 5 条精简到 3 条。',
|
|
114
|
+
};
|
|
115
|
+
function extractUserText(prompt) {
|
|
116
|
+
const userMsgs = (prompt || []).filter((m) => m.role === 'user');
|
|
117
|
+
const last = userMsgs[userMsgs.length - 1];
|
|
118
|
+
if (!last)
|
|
119
|
+
return '';
|
|
120
|
+
if (typeof last.content === 'string')
|
|
121
|
+
return last.content.toLowerCase();
|
|
122
|
+
return (last.content || [])
|
|
123
|
+
.map((c) => c.text || '')
|
|
124
|
+
.join('')
|
|
125
|
+
.toLowerCase();
|
|
126
|
+
}
|
|
127
|
+
function hasToolResults(prompt) {
|
|
128
|
+
const msgs = prompt || [];
|
|
129
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
130
|
+
if (msgs[i].role === 'tool')
|
|
131
|
+
return true;
|
|
132
|
+
if (msgs[i].role === 'user')
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
function getToolResultContent(prompt) {
|
|
138
|
+
const msgs = prompt || [];
|
|
139
|
+
const parts = [];
|
|
140
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
141
|
+
if (msgs[i].role === 'tool') {
|
|
142
|
+
const content = msgs[i].content || [];
|
|
143
|
+
for (const c of content) {
|
|
144
|
+
const val = c.output?.value || c.output || c.result || '';
|
|
145
|
+
parts.push(String(val));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else if (msgs[i].role === 'user')
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
return parts.join('\n');
|
|
152
|
+
}
|
|
153
|
+
function wasToolSearchCalled(prompt) {
|
|
154
|
+
const msgs = prompt || [];
|
|
155
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
156
|
+
if (msgs[i].role === 'assistant') {
|
|
157
|
+
const content = msgs[i].content || [];
|
|
158
|
+
for (const c of content) {
|
|
159
|
+
if (c.type === 'tool-call' && c.toolName === 'tool_search')
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (msgs[i].role === 'user')
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function detectParallelIntent(text) {
|
|
169
|
+
if (text.includes('测试并发') || text.includes('test parallel')) {
|
|
170
|
+
return [
|
|
171
|
+
{ toolName: 'get_weather', args: { city: '北京' } },
|
|
172
|
+
{ toolName: 'get_weather', args: { city: '上海' } },
|
|
173
|
+
{ toolName: 'list_directory', args: { path: '.' } },
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
function detectToolIntent(prompt) {
|
|
179
|
+
const text = extractUserText(prompt);
|
|
180
|
+
const toolResults = getToolResultContent(prompt);
|
|
181
|
+
if (text.includes('测试死循环')) {
|
|
182
|
+
return { toolName: 'get_weather', args: { city: '北京' } };
|
|
183
|
+
}
|
|
184
|
+
// Dream flow — 多步记忆整理
|
|
185
|
+
if (text.includes('阶段 1') && text.includes('阶段 2') && text.includes('记忆整理') || text.includes('dream')) {
|
|
186
|
+
if (!hasToolResults(prompt)) {
|
|
187
|
+
// Step 1: 先 list
|
|
188
|
+
return { toolName: 'memory', args: { action: 'list' } };
|
|
189
|
+
}
|
|
190
|
+
const combined = getToolResultContent(prompt);
|
|
191
|
+
if (combined.includes('记忆列表') && !combined.includes('lint 报告')) {
|
|
192
|
+
// Step 2: list 完了,跑 lint
|
|
193
|
+
return { toolName: 'memory', args: { action: 'lint' } };
|
|
194
|
+
}
|
|
195
|
+
const deletedOld = combined.includes('已删除: project_old-build-config') || combined.includes('文件不存在: project_old-build-config');
|
|
196
|
+
const deletedDup = combined.includes('已删除: project_deploy-process-2') || combined.includes('文件不存在: project_deploy-process-2');
|
|
197
|
+
if (combined.includes('lint 报告') && !deletedOld) {
|
|
198
|
+
// Step 3: lint 完了,删掉 old-build-config
|
|
199
|
+
return { toolName: 'memory', args: { action: 'delete', filename: 'project_old-build-config.md' } };
|
|
200
|
+
}
|
|
201
|
+
if (deletedOld && !deletedDup) {
|
|
202
|
+
// Step 4: 删重名的那条
|
|
203
|
+
return { toolName: 'memory', args: { action: 'delete', filename: 'project_deploy-process-2.md' } };
|
|
204
|
+
}
|
|
205
|
+
// Step 5: done
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
// Memory tool — lint intent(必须排在 save 前面,避免被"记住"误吞)
|
|
209
|
+
if ((text.includes('lint 记忆') || text.includes('检查记忆') || text.includes('记忆体检') || text === 'lint') && !hasToolResults(prompt)) {
|
|
210
|
+
return { toolName: 'memory', args: { action: 'lint' } };
|
|
211
|
+
}
|
|
212
|
+
// Memory tool — save intent
|
|
213
|
+
if ((text.includes('记住') || text.includes('remember')) && !hasToolResults(prompt)) {
|
|
214
|
+
const content = text.replace(/记住|remember/g, '').trim();
|
|
215
|
+
const isPreference = content.includes('喜欢') || content.includes('偏好') || content.includes('prefer');
|
|
216
|
+
const isFeedback = content.includes('不要') || content.includes('别') || content.includes('don\'t');
|
|
217
|
+
const type = isFeedback ? 'feedback' : isPreference ? 'user' : 'project';
|
|
218
|
+
return { toolName: 'memory', args: {
|
|
219
|
+
action: 'save',
|
|
220
|
+
name: content.slice(0, 30),
|
|
221
|
+
description: content.slice(0, 60),
|
|
222
|
+
type,
|
|
223
|
+
content,
|
|
224
|
+
} };
|
|
225
|
+
}
|
|
226
|
+
// Memory tool — list intent
|
|
227
|
+
if ((text.includes('我的记忆') || text.includes('记忆列表') || text === 'memory list') && !hasToolResults(prompt)) {
|
|
228
|
+
return { toolName: 'memory', args: { action: 'list' } };
|
|
229
|
+
}
|
|
230
|
+
// Memory tool — search intent(BM25)
|
|
231
|
+
if ((text.includes('搜记忆') || text.includes('搜索记忆') || text.includes('找记忆') || text.includes('memory search')) && !hasToolResults(prompt)) {
|
|
232
|
+
const query = text
|
|
233
|
+
.replace(/搜记忆|搜索记忆|找记忆|memory search/g, '')
|
|
234
|
+
.replace(/^[关于的有]+/, '')
|
|
235
|
+
.trim() || 'all';
|
|
236
|
+
return { toolName: 'memory', args: { action: 'search', query } };
|
|
237
|
+
}
|
|
238
|
+
// RAG tool — ingest intent
|
|
239
|
+
if ((text.includes('导入') || text.includes('ingest')) && (text.includes('文档') || text.includes('.md')) && !hasToolResults(prompt)) {
|
|
240
|
+
const pathMatch = text.match(/([\w/.-]+\.md)/);
|
|
241
|
+
const path = pathMatch ? pathMatch[1] : 'docs/deployment-guide.md';
|
|
242
|
+
return { toolName: 'rag_ingest', args: { path } };
|
|
243
|
+
}
|
|
244
|
+
// Plugin tools — supabase (直接调用,不需要 tool_search)
|
|
245
|
+
if (!hasToolResults(prompt) && (text.includes('有哪些表') || text.includes('表列表') || text.includes('list table'))) {
|
|
246
|
+
return { toolName: 'supabase__list_tables', args: {} };
|
|
247
|
+
}
|
|
248
|
+
if (!hasToolResults(prompt) && (text.includes('查用户') || text.includes('用户数据') || text.includes('query user'))) {
|
|
249
|
+
return { toolName: 'supabase__query', args: { table: 'users' } };
|
|
250
|
+
}
|
|
251
|
+
if (!hasToolResults(prompt) && (text.includes('查帖子') || text.includes('文章列表') || text.includes('query post'))) {
|
|
252
|
+
return { toolName: 'supabase__query', args: { table: 'posts' } };
|
|
253
|
+
}
|
|
254
|
+
if (!hasToolResults(prompt) && (text.includes('插入') || text.includes('新增') || text.includes('insert'))) {
|
|
255
|
+
return { toolName: 'supabase__insert', args: { table: 'users', data: { name: '赵六', email: 'zhao@example.com', role: 'user' } } };
|
|
256
|
+
}
|
|
257
|
+
if (!hasToolResults(prompt) && (text.includes('数据库') || text.includes('database') || text.includes('supabase') || text.includes('sql'))) {
|
|
258
|
+
return { toolName: 'supabase__list_tables', args: {} };
|
|
259
|
+
}
|
|
260
|
+
// RAG tool — search intent
|
|
261
|
+
if (!hasToolResults(prompt) && (text.includes('部署') || text.includes('deploy') || text.includes('事故') ||
|
|
262
|
+
text.includes('回滚') || text.includes('监控') || text.includes('迁移') ||
|
|
263
|
+
text.includes('知识库') || text.includes('搜索知识') || text.includes('查资料'))) {
|
|
264
|
+
return { toolName: 'rag_search', args: { query: text } };
|
|
265
|
+
}
|
|
266
|
+
// 如果刚刚 tool_search 返回了结果,现在要调用发现的工具
|
|
267
|
+
if (hasToolResults(prompt) && wasToolSearchCalled(prompt)) {
|
|
268
|
+
if (toolResults.includes('list_issues') || toolResults.includes('mcp__github')) {
|
|
269
|
+
const repoMatch = text.match(/(\w+)\/(\w[\w-]*)/);
|
|
270
|
+
const owner = repoMatch ? repoMatch[1] : 'vercel';
|
|
271
|
+
const repo = repoMatch ? repoMatch[2] : 'ai';
|
|
272
|
+
return { toolName: 'mcp__github__list_issues', args: { owner, repo } };
|
|
273
|
+
}
|
|
274
|
+
if (toolResults.includes('search_pages') || toolResults.includes('mcp__notion')) {
|
|
275
|
+
return { toolName: 'mcp__notion__search_pages', args: { query: 'project roadmap' } };
|
|
276
|
+
}
|
|
277
|
+
if (toolResults.includes('navigate') || toolResults.includes('mcp__browser')) {
|
|
278
|
+
return { toolName: 'mcp__browser__navigate', args: { url: 'https://example.com' } };
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (hasToolResults(prompt))
|
|
283
|
+
return null;
|
|
284
|
+
// 延迟工具场景:先 tool_search,传精确的工具名
|
|
285
|
+
if (text.includes('issue') || text.includes('issues') || text.includes('github')) {
|
|
286
|
+
return { toolName: 'tool_search', args: { query: 'mcp__github__list_issues' } };
|
|
287
|
+
}
|
|
288
|
+
if (text.includes('notion') || text.includes('笔记')) {
|
|
289
|
+
return { toolName: 'tool_search', args: { query: 'mcp__notion__search_pages' } };
|
|
290
|
+
}
|
|
291
|
+
if (text.includes('浏览器') || text.includes('browser') || text.includes('网页')) {
|
|
292
|
+
return { toolName: 'tool_search', args: { query: 'mcp__browser__navigate' } };
|
|
293
|
+
}
|
|
294
|
+
// 内置工具(非延迟,直接调用)
|
|
295
|
+
if (text.includes('测试截断') || text.includes('test truncation')) {
|
|
296
|
+
return { toolName: 'read_file', args: { path: 'sample-data.txt' } };
|
|
297
|
+
}
|
|
298
|
+
if (text.includes('测试编辑') || text.includes('test edit')) {
|
|
299
|
+
return { toolName: 'edit_file', args: { path: 'sample-data.txt', old_string: '一、工具注册机制', new_string: '一、工具注册机制(已更新)' } };
|
|
300
|
+
}
|
|
301
|
+
if (text.includes('测试搜索') || text.includes('test grep')) {
|
|
302
|
+
return { toolName: 'grep', args: { pattern: 'export', path: 'src' } };
|
|
303
|
+
}
|
|
304
|
+
if (text.includes('测试glob') || text.includes('test glob')) {
|
|
305
|
+
return { toolName: 'glob', args: { pattern: '**/*.ts' } };
|
|
306
|
+
}
|
|
307
|
+
if (text.includes('测试bash') || text.includes('test bash')) {
|
|
308
|
+
return { toolName: 'bash', args: { command: 'echo "Hello from bash!" && date' } };
|
|
309
|
+
}
|
|
310
|
+
if (text.includes('目录') || text.includes('文件列表') || text.includes('ls')) {
|
|
311
|
+
return { toolName: 'list_directory', args: { path: '.' } };
|
|
312
|
+
}
|
|
313
|
+
const fileMatch = text.match(/(\S+\.[\w]+)/);
|
|
314
|
+
if (fileMatch && (text.includes('读') || text.includes('read') || text.includes('看看') || text.includes('查看') || text.includes('打开') || text.includes('文件') || text.includes('file'))) {
|
|
315
|
+
return { toolName: 'read_file', args: { path: fileMatch[1] } };
|
|
316
|
+
}
|
|
317
|
+
const weatherKeywords = ['天气', 'weather', '温度', '热', '冷', '气温'];
|
|
318
|
+
const hasWeatherIntent = weatherKeywords.some((kw) => text.includes(kw));
|
|
319
|
+
const cities = text.match(/(北京|上海|深圳|广州|杭州|成都)/g);
|
|
320
|
+
if (hasWeatherIntent && cities && cities.length > 0) {
|
|
321
|
+
return { toolName: 'get_weather', args: { city: cities[0] } };
|
|
322
|
+
}
|
|
323
|
+
const calcMatch = text.match(/(\d+)\s*[+\-*/加减乘除]\s*(\d+)/);
|
|
324
|
+
if (calcMatch) {
|
|
325
|
+
const op = text.match(/[+*/]|加|减|乘|除|-/)?.[0] || '+';
|
|
326
|
+
const opMap = { '加': '+', '减': '-', '乘': '*', '除': '/' };
|
|
327
|
+
const expression = `${calcMatch[1]} ${opMap[op] || op} ${calcMatch[2]}`;
|
|
328
|
+
return { toolName: 'calculator', args: { expression } };
|
|
329
|
+
}
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
function pickTextResponse(prompt) {
|
|
333
|
+
const text = extractUserText(prompt);
|
|
334
|
+
if (hasToolResults(prompt)) {
|
|
335
|
+
const combined = getToolResultContent(prompt);
|
|
336
|
+
// Memory tool responses
|
|
337
|
+
if (combined.includes('已保存到记忆') || combined.includes('saved to memory')) {
|
|
338
|
+
return TEXT_RESPONSES.memorySaved;
|
|
339
|
+
}
|
|
340
|
+
// Dream 完成——两条都处理完了
|
|
341
|
+
if ((text.includes('dream') || text.includes('记忆整理')) &&
|
|
342
|
+
(combined.includes('project_deploy-process-2'))) {
|
|
343
|
+
return TEXT_RESPONSES.dreamFinished;
|
|
344
|
+
}
|
|
345
|
+
if (combined.includes('lint 报告') || combined.includes('记忆库健康')) {
|
|
346
|
+
return `${TEXT_RESPONSES.lintFinished}\n\n${combined}`;
|
|
347
|
+
}
|
|
348
|
+
if (combined.includes('BM25 搜索结果')) {
|
|
349
|
+
return `给你按相关度排好的搜索结果:\n${combined}`;
|
|
350
|
+
}
|
|
351
|
+
if (combined.includes('记忆列表') || combined.includes('条记忆')) {
|
|
352
|
+
return `这是你目前的记忆:\n${combined}`;
|
|
353
|
+
}
|
|
354
|
+
// Plugin tool responses (supabase)
|
|
355
|
+
if (combined.includes('tables') && combined.includes('users')) {
|
|
356
|
+
return `数据库里有这些表:\n${combined}`;
|
|
357
|
+
}
|
|
358
|
+
if (combined.includes('"table"') && combined.includes('"rows"')) {
|
|
359
|
+
return `查询结果如下:\n${combined}`;
|
|
360
|
+
}
|
|
361
|
+
if (combined.includes('"success":true') && combined.includes('"inserted"')) {
|
|
362
|
+
return `数据插入成功:\n${combined}`;
|
|
363
|
+
}
|
|
364
|
+
// RAG tool responses
|
|
365
|
+
if (combined.includes('已导入') && combined.includes('文档片段')) {
|
|
366
|
+
return `文档已导入知识库。${combined}`;
|
|
367
|
+
}
|
|
368
|
+
if (combined.includes('综合分') || combined.includes('来源:')) {
|
|
369
|
+
return `根据知识库的检索结果:\n\n${combined}`;
|
|
370
|
+
}
|
|
371
|
+
if (combined.includes('知识库为空')) {
|
|
372
|
+
return combined;
|
|
373
|
+
}
|
|
374
|
+
if (combined.includes('搜索结果') || combined.includes('没有找到')) {
|
|
375
|
+
return combined;
|
|
376
|
+
}
|
|
377
|
+
if (combined.includes('[DIR]') || combined.includes('[FILE]')) {
|
|
378
|
+
return `当前目录的文件列表:\n${combined}`;
|
|
379
|
+
}
|
|
380
|
+
if (combined.includes('°C') || combined.includes('天气')) {
|
|
381
|
+
return `根据查询结果:${combined}`;
|
|
382
|
+
}
|
|
383
|
+
if (combined.includes('已发送') || combined.includes('已导航') || combined.includes('已点击') || combined.includes('已填写')) {
|
|
384
|
+
return `操作完成:${combined}`;
|
|
385
|
+
}
|
|
386
|
+
if (combined.includes('number') || combined.includes('title') || combined.includes('state')) {
|
|
387
|
+
return `查询结果:\n${combined}`;
|
|
388
|
+
}
|
|
389
|
+
return `工具返回了以下信息:\n${combined}`;
|
|
390
|
+
}
|
|
391
|
+
if (text.includes('你好') || text.includes('hello') || text.includes('hi'))
|
|
392
|
+
return TEXT_RESPONSES.greeting;
|
|
393
|
+
return TEXT_RESPONSES.default;
|
|
394
|
+
}
|
|
395
|
+
function createDelayedStream(chunks, delayMs = 30) {
|
|
396
|
+
return new ReadableStream({
|
|
397
|
+
start(controller) {
|
|
398
|
+
let i = 0;
|
|
399
|
+
function next() {
|
|
400
|
+
if (i < chunks.length) {
|
|
401
|
+
controller.enqueue(chunks[i++]);
|
|
402
|
+
setTimeout(next, delayMs);
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
controller.close();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
next();
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function makeToolCallChunks(intents, prompt) {
|
|
413
|
+
const chunks = [];
|
|
414
|
+
for (const intent of intents) {
|
|
415
|
+
const callId = `call-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
416
|
+
const argsJson = JSON.stringify(intent.args);
|
|
417
|
+
chunks.push({ type: 'tool-input-start', id: callId, toolName: intent.toolName }, { type: 'tool-input-delta', id: callId, delta: argsJson }, { type: 'tool-input-end', id: callId }, { type: 'tool-call', toolCallId: callId, toolName: intent.toolName, input: argsJson });
|
|
418
|
+
}
|
|
419
|
+
chunks.push({ type: 'finish', finishReason: { unified: 'tool-calls', raw: undefined }, usage: makeUsage(prompt) });
|
|
420
|
+
return chunks;
|
|
421
|
+
}
|
|
422
|
+
export function createMockModel() {
|
|
423
|
+
return {
|
|
424
|
+
specificationVersion: 'v4',
|
|
425
|
+
provider: 'mock',
|
|
426
|
+
modelId: 'mock-model',
|
|
427
|
+
get supportedUrls() {
|
|
428
|
+
return Promise.resolve({});
|
|
429
|
+
},
|
|
430
|
+
async doGenerate({ prompt }) {
|
|
431
|
+
// Detect compression request (called via generateText with compress system prompt)
|
|
432
|
+
const allText = (prompt || []).map((m) => {
|
|
433
|
+
if (typeof m.content === 'string')
|
|
434
|
+
return m.content;
|
|
435
|
+
if (Array.isArray(m.content))
|
|
436
|
+
return m.content.map((c) => c.text || '').join('');
|
|
437
|
+
return '';
|
|
438
|
+
}).join(' ');
|
|
439
|
+
if (allText.includes('对话压缩系统') || allText.includes('压缩成一份结构化摘要')) {
|
|
440
|
+
const mockSummary = `## 用户意图\n用户在探索项目结构和代码,了解工具系统的设计。\n\n## 已完成的操作\n- 列出了当前目录文件(.env, package.json, sample-data.txt, src/)\n- 读取了 package.json(项目名 super-agent-08-compaction, 版本 0.8.0)\n- 读取了 sample-data.txt(工具系统设计文档)\n- 搜索了 src/ 目录中的 export(找到 ToolRegistry, agentLoop, SessionStore 等导出)\n\n## 关键发现\n- 项目使用 ai@5.0.98 和 @ai-sdk/openai@2.0.44\n- 工具系统包含 ToolRegistry、truncateResult、并发控制(读写锁)\n- 已实现 SessionStore(JSONL 持久化)和 PromptBuilder(模块化 Prompt)\n\n## 当前状态\n用户刚完成项目结构探索,尚未开始修改代码。\n\n## 需要保留的细节\n- 项目路径:当前工作目录\n- 关键文件:src/tool-registry.ts, src/agent-loop.ts, src/context-compressor.ts`;
|
|
441
|
+
return {
|
|
442
|
+
content: [{ type: 'text', text: mockSummary }],
|
|
443
|
+
finishReason: { unified: 'stop', raw: undefined },
|
|
444
|
+
usage: makeUsage(prompt),
|
|
445
|
+
warnings: [],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
const text = extractUserText(prompt);
|
|
449
|
+
if (text.includes('测试重试') || text.includes('test retry')) {
|
|
450
|
+
retryTestCount++;
|
|
451
|
+
if (retryTestCount <= 2) {
|
|
452
|
+
throw new Error('429 Too Many Requests - Rate limit exceeded');
|
|
453
|
+
}
|
|
454
|
+
retryTestCount = 0;
|
|
455
|
+
return {
|
|
456
|
+
content: [{ type: 'text', text: '重试成功!' }],
|
|
457
|
+
finishReason: { unified: 'stop', raw: undefined },
|
|
458
|
+
usage: makeUsage(prompt),
|
|
459
|
+
warnings: [],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const parallelIntents = detectParallelIntent(text);
|
|
463
|
+
if (parallelIntents && !hasToolResults(prompt)) {
|
|
464
|
+
return {
|
|
465
|
+
content: parallelIntents.map(intent => ({
|
|
466
|
+
type: 'tool-call',
|
|
467
|
+
toolCallId: `call-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
468
|
+
toolName: intent.toolName,
|
|
469
|
+
input: intent.args,
|
|
470
|
+
})),
|
|
471
|
+
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
472
|
+
usage: makeUsage(prompt),
|
|
473
|
+
warnings: [],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const intent = detectToolIntent(prompt);
|
|
477
|
+
if (intent) {
|
|
478
|
+
return {
|
|
479
|
+
content: [{
|
|
480
|
+
type: 'tool-call',
|
|
481
|
+
toolCallId: `call-${Date.now()}`,
|
|
482
|
+
toolName: intent.toolName,
|
|
483
|
+
input: intent.args,
|
|
484
|
+
}],
|
|
485
|
+
finishReason: { unified: 'tool-calls', raw: undefined },
|
|
486
|
+
usage: makeUsage(prompt),
|
|
487
|
+
warnings: [],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: pickTextResponse(prompt) }],
|
|
492
|
+
finishReason: { unified: 'stop', raw: undefined },
|
|
493
|
+
usage: makeUsage(prompt),
|
|
494
|
+
warnings: [],
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
async doStream({ prompt }) {
|
|
498
|
+
const text = extractUserText(prompt);
|
|
499
|
+
if (text.includes('测试重试') || text.includes('test retry')) {
|
|
500
|
+
retryTestCount++;
|
|
501
|
+
if (retryTestCount <= 2) {
|
|
502
|
+
throw new Error('429 Too Many Requests - Rate limit exceeded');
|
|
503
|
+
}
|
|
504
|
+
retryTestCount = 0;
|
|
505
|
+
const reply = '重试成功!';
|
|
506
|
+
const id = 'text-1';
|
|
507
|
+
const chunks = [
|
|
508
|
+
{ type: 'text-start', id },
|
|
509
|
+
...reply.split('').map((char) => ({ type: 'text-delta', id, delta: char })),
|
|
510
|
+
{ type: 'text-end', id },
|
|
511
|
+
{ type: 'finish', finishReason: { unified: 'stop', raw: undefined }, usage: makeUsage(prompt) },
|
|
512
|
+
];
|
|
513
|
+
return { stream: createDelayedStream(chunks, 30) };
|
|
514
|
+
}
|
|
515
|
+
const parallelIntents = detectParallelIntent(text);
|
|
516
|
+
if (parallelIntents && !hasToolResults(prompt)) {
|
|
517
|
+
return { stream: createDelayedStream(makeToolCallChunks(parallelIntents, prompt), 15) };
|
|
518
|
+
}
|
|
519
|
+
const intent = detectToolIntent(prompt);
|
|
520
|
+
if (intent) {
|
|
521
|
+
return { stream: createDelayedStream(makeToolCallChunks([intent], prompt), 20) };
|
|
522
|
+
}
|
|
523
|
+
const replyText = pickTextResponse(prompt);
|
|
524
|
+
const id = 'text-1';
|
|
525
|
+
const chunks = [
|
|
526
|
+
{ type: 'text-start', id },
|
|
527
|
+
...replyText.split('').map((char) => ({ type: 'text-delta', id, delta: char })),
|
|
528
|
+
{ type: 'text-end', id },
|
|
529
|
+
{ type: 'finish', finishReason: { unified: 'stop', raw: undefined }, usage: makeUsage(prompt) },
|
|
530
|
+
];
|
|
531
|
+
return { stream: createDelayedStream(chunks, 30) };
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export class PluginManager {
|
|
2
|
+
plugins = new Map();
|
|
3
|
+
registry = {};
|
|
4
|
+
constructor(registry) {
|
|
5
|
+
this.registry = registry;
|
|
6
|
+
}
|
|
7
|
+
async load(definition, config) {
|
|
8
|
+
if (this.plugins.has(definition.name)) {
|
|
9
|
+
throw new Error(`插件"${definition.name}"已加载过`);
|
|
10
|
+
}
|
|
11
|
+
const resolvedConfig = this.resolveEnvVars({
|
|
12
|
+
...definition.config,
|
|
13
|
+
...config, // Agent 配置覆盖默认配置
|
|
14
|
+
});
|
|
15
|
+
const registeredTools = []; // 插件携带的工具名
|
|
16
|
+
const api = {
|
|
17
|
+
registerTools: (tools) => {
|
|
18
|
+
for (const tool of tools) {
|
|
19
|
+
const prefixedName = `${definition.name}__${tool.name}`;
|
|
20
|
+
const prefixedTool = {
|
|
21
|
+
...tool,
|
|
22
|
+
name: prefixedName,
|
|
23
|
+
description: `[Plugin:${definition.name}] ${tool.description}`
|
|
24
|
+
};
|
|
25
|
+
this.registry.register(prefixedTool); // 将插件的工具注册到工具注册表中
|
|
26
|
+
registeredTools.push(prefixedName);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
getConfig: () => resolvedConfig,
|
|
30
|
+
log: (message) => {
|
|
31
|
+
console.log(`[Plugin:${definition.name}] ${message}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
try {
|
|
35
|
+
await definition.activate(api); // 激活插件
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
39
|
+
console.error(`[Plugin:${definition.name}] 激活失败: ${msg}`);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
this.plugins.set(definition.name, {
|
|
43
|
+
definition,
|
|
44
|
+
tools: registeredTools,
|
|
45
|
+
});
|
|
46
|
+
return registeredTools;
|
|
47
|
+
}
|
|
48
|
+
async unload(name) {
|
|
49
|
+
const plugin = this.plugins.get(name);
|
|
50
|
+
if (!plugin)
|
|
51
|
+
return false;
|
|
52
|
+
if (plugin.definition.destroy) {
|
|
53
|
+
try {
|
|
54
|
+
await plugin.definition.destroy();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
58
|
+
console.error(` [plugin:${name}] destroy 出错: ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const toolName of plugin.tools) {
|
|
62
|
+
this.registry.unregister(toolName);
|
|
63
|
+
}
|
|
64
|
+
this.plugins.delete(name);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
async unloadAll() {
|
|
68
|
+
const names = Array.from(this.plugins.keys());
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
await this.unload(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
get(name) {
|
|
74
|
+
return this.plugins.get(name);
|
|
75
|
+
}
|
|
76
|
+
list() {
|
|
77
|
+
return Array.from(this.plugins.values()).map(p => ({
|
|
78
|
+
name: p.definition.name,
|
|
79
|
+
version: p.definition.version,
|
|
80
|
+
description: p.definition.description,
|
|
81
|
+
tools: p.tools,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
resolveEnvVars(config) {
|
|
85
|
+
const resolved = {};
|
|
86
|
+
for (const [key, value] of Object.entries(config)) {
|
|
87
|
+
if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) {
|
|
88
|
+
const envKey = value.slice(2, -1);
|
|
89
|
+
resolved[key] = process.env[envKey] || '';
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
resolved[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return resolved;
|
|
96
|
+
}
|
|
97
|
+
}
|