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,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: "以高级工程师的视角审查项目代码,检测 SOLID 违规、安全风险、性能隐患"
|
|
4
|
+
whenToUse: "当用户要求审查代码、review 代码、或检查代码质量"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Code Review
|
|
8
|
+
|
|
9
|
+
## 审查流程
|
|
10
|
+
|
|
11
|
+
**1) 确定审查范围**
|
|
12
|
+
|
|
13
|
+
先问用户要审查哪部分代码。如果用户没指定,用 `list_directory` 看一下 `src/` 的目录结构,让用户选一个模块。不要一次审查整个项目——聚焦到具体的目录或文件。
|
|
14
|
+
|
|
15
|
+
**2) 逐文件阅读和审查**
|
|
16
|
+
|
|
17
|
+
用 `read_file` 读取目标模块的源码文件,关注以下问题:
|
|
18
|
+
- **SRP**:一个模块是不是干了太多不相关的事
|
|
19
|
+
- **OCP**:新功能是靠修改已有代码实现的,还是通过扩展点
|
|
20
|
+
- **DIP**:高层逻辑是不是直接依赖了低层实现
|
|
21
|
+
|
|
22
|
+
如果建议重构,说清楚为什么能改善内聚/耦合,并给出最小改动方案。
|
|
23
|
+
|
|
24
|
+
**3) 安全扫描**
|
|
25
|
+
|
|
26
|
+
检查常见风险:
|
|
27
|
+
- 注入漏洞(SQL/命令/XSS)
|
|
28
|
+
- 认证/授权缺口
|
|
29
|
+
- 密钥泄漏(硬编码在代码里的 API Key、Token)
|
|
30
|
+
- 竞态条件
|
|
31
|
+
|
|
32
|
+
**4) 代码质量**
|
|
33
|
+
|
|
34
|
+
- 错误处理:有没有吞异常、catch 太宽泛、async 错误没处理
|
|
35
|
+
- 性能:N+1 查询、热路径上的重计算、缺少缓存
|
|
36
|
+
- 边界条件:null/undefined、空集合、数字溢出
|
|
37
|
+
|
|
38
|
+
## 输出格式
|
|
39
|
+
|
|
40
|
+
按 P0(必须修复)、P1(建议修复)、P2(可以改进)分级输出。
|
|
41
|
+
每个发现标注文件:行号和具体建议。
|
|
42
|
+
默认只输出审查结果,不直接改代码——除非用户明确要求。
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// 死循环检测
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
// 配置
|
|
4
|
+
const HISTORY_SIZE = 30; // 滑动窗口 最近30轮
|
|
5
|
+
const WARNING_THRESHOLD = 5; // 警告阈值,超过就向大模型注入提示警告
|
|
6
|
+
const CRITICAL_THRESHOLD = 8; // 严重阈值,超过就直接停止工具调用
|
|
7
|
+
const BREAKER_THRESHOLD = 10; // 熔断阈值,超过就直接停止循环
|
|
8
|
+
// 指纹计算
|
|
9
|
+
function stableStringify(value) {
|
|
10
|
+
if (value === null || typeof value !== 'object') {
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
return `[${value.map(stableStringify).join(',')}]`; // ['xxx,yyy']
|
|
15
|
+
}
|
|
16
|
+
const keys = Object.keys(value).sort();
|
|
17
|
+
return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`;
|
|
18
|
+
}
|
|
19
|
+
function hash(input) {
|
|
20
|
+
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
export function hashToolCall(toolName, params) {
|
|
23
|
+
return `${toolName}:${hash(stableStringify(params))}`;
|
|
24
|
+
}
|
|
25
|
+
export function hashResult(result) {
|
|
26
|
+
return hash(stableStringify(result));
|
|
27
|
+
}
|
|
28
|
+
// 滑动窗口
|
|
29
|
+
const history = [];
|
|
30
|
+
export function recordCall(toolName, params) {
|
|
31
|
+
history.push({
|
|
32
|
+
toolName,
|
|
33
|
+
argsHash: hashToolCall(toolName, params),
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
if (history.length > HISTORY_SIZE) {
|
|
37
|
+
history.shift();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function recordResult(toolName, params, result) {
|
|
41
|
+
const argsHash = hashToolCall(toolName, params);
|
|
42
|
+
const resultH = hashResult(result);
|
|
43
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
44
|
+
if (history[i].toolName === toolName && history[i].argsHash === argsHash && !history[i].resultHash) {
|
|
45
|
+
history[i].resultHash = resultH;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function resetHistory() {
|
|
51
|
+
history.length = 0;
|
|
52
|
+
}
|
|
53
|
+
// 检测器
|
|
54
|
+
function getNoProgressStreak(toolName, argsHash) {
|
|
55
|
+
let streak = 0;
|
|
56
|
+
let lastResultHash;
|
|
57
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
58
|
+
const r = history[i];
|
|
59
|
+
if (r.toolName !== toolName || r.argsHash !== argsHash)
|
|
60
|
+
continue;
|
|
61
|
+
if (!r.resultHash)
|
|
62
|
+
continue;
|
|
63
|
+
if (!lastResultHash) {
|
|
64
|
+
lastResultHash = r.resultHash;
|
|
65
|
+
streak = 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (r.resultHash !== lastResultHash)
|
|
69
|
+
break;
|
|
70
|
+
streak++;
|
|
71
|
+
}
|
|
72
|
+
return streak;
|
|
73
|
+
}
|
|
74
|
+
function getPingPongCount(currentHash) {
|
|
75
|
+
if (history.length < 3)
|
|
76
|
+
return 0;
|
|
77
|
+
const last = history[history.length - 1];
|
|
78
|
+
let otherHash;
|
|
79
|
+
for (let i = history.length - 2; i >= 0; i--) {
|
|
80
|
+
if (history[i].argsHash !== last.argsHash) {
|
|
81
|
+
otherHash = history[i].argsHash;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!otherHash)
|
|
86
|
+
return 0;
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
89
|
+
const expected = count % 2 === 0 ? last.argsHash : otherHash;
|
|
90
|
+
if (history[i].argsHash !== expected)
|
|
91
|
+
break;
|
|
92
|
+
count++;
|
|
93
|
+
}
|
|
94
|
+
if (currentHash === otherHash && count >= 2)
|
|
95
|
+
return count + 1;
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
// --- 主检测函数 ---
|
|
99
|
+
export function detect(toolName, params) {
|
|
100
|
+
const argsHash = hashToolCall(toolName, params);
|
|
101
|
+
const noProgress = getNoProgressStreak(toolName, argsHash);
|
|
102
|
+
if (noProgress >= BREAKER_THRESHOLD) {
|
|
103
|
+
return {
|
|
104
|
+
stuck: true, level: 'critical', detector: 'global_circuit_breaker', count: noProgress,
|
|
105
|
+
message: `[熔断] ${toolName} 已重复 ${noProgress} 次且无进展,强制停止`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const pingPong = getPingPongCount(argsHash);
|
|
109
|
+
if (pingPong >= CRITICAL_THRESHOLD) {
|
|
110
|
+
return {
|
|
111
|
+
stuck: true, level: 'critical', detector: 'ping_pong', count: pingPong,
|
|
112
|
+
message: `[熔断] 检测到乒乓循环(${pingPong} 次交替),强制停止`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (pingPong >= WARNING_THRESHOLD) {
|
|
116
|
+
return {
|
|
117
|
+
stuck: true, level: 'warning', detector: 'ping_pong', count: pingPong,
|
|
118
|
+
message: `[警告] 检测到乒乓循环(${pingPong} 次交替),建议换个思路`
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const recentCount = history.filter(h => h.toolName === toolName && h.argsHash === argsHash).length;
|
|
122
|
+
if (recentCount >= CRITICAL_THRESHOLD) {
|
|
123
|
+
return {
|
|
124
|
+
stuck: true, level: 'critical', detector: 'generic_repeat', count: recentCount,
|
|
125
|
+
message: `[熔断] ${toolName} 相同参数已调用 ${recentCount} 次,强制停止`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (recentCount >= WARNING_THRESHOLD) {
|
|
129
|
+
return {
|
|
130
|
+
stuck: true, level: 'warning', detector: 'generic_repeat', count: recentCount,
|
|
131
|
+
message: `[警告] ${toolName} 相同参数已调用 ${recentCount} 次,你可能陷入了重复`
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { stuck: false };
|
|
135
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
import { detect, resetHistory, recordCall, recordResult } from './loop-detection.js';
|
|
3
|
+
import { isRetryable, calculateDelay, sleep } from './retry.js';
|
|
4
|
+
import { normalizeUsage } from "../usage/tracker.js";
|
|
5
|
+
const MAX_STEPS = 50; // 最大循环次数
|
|
6
|
+
const MAX_RETRIES = 3; // 最大重试次数
|
|
7
|
+
const TOKEN_BUDGET = 500000; // 最大token用量
|
|
8
|
+
export async function agentLoop(model, registry, messages, system, tracker) {
|
|
9
|
+
let step = 0; // 当前这轮的循环次数
|
|
10
|
+
let totalTokens = 0; // 总token用量
|
|
11
|
+
resetHistory(); // 重置工具执行的历史记录
|
|
12
|
+
while (step < MAX_STEPS) {
|
|
13
|
+
step++;
|
|
14
|
+
console.log(`\n--- Step ${step} ---`);
|
|
15
|
+
let hasToolCall = false; // 当前这轮是否有工具调用
|
|
16
|
+
let fullText = ''; // 当前这轮的模型输出
|
|
17
|
+
let shouldBreak = false; // 是否需要熔断
|
|
18
|
+
let lastToolCall = null; // 最后一个工具调用记录
|
|
19
|
+
let stepResponse; // 当前这轮的模型输出
|
|
20
|
+
let stepUsage; // 当前这轮的token用量
|
|
21
|
+
// 步骤重试:应该包裹 streamText 和 result 的处理
|
|
22
|
+
for (let attempt = 1;; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
const result = streamText({
|
|
25
|
+
model,
|
|
26
|
+
tools: registry.toAISDKFormat(),
|
|
27
|
+
messages,
|
|
28
|
+
system,
|
|
29
|
+
maxRetries: 0, // 不配置重试,就只会跑一次
|
|
30
|
+
onError: () => { },
|
|
31
|
+
providerOptions: { openai: { parallelCalls: true } } // 开启并行调用,提高效率
|
|
32
|
+
// 不配置 stopwhen,就只会跑一次
|
|
33
|
+
});
|
|
34
|
+
for await (const part of result.fullStream) { // fullStream 是ai库生成一个水桶,里面装的是模型的输出,并且当工具调用完毕后会自动的将结果添加到水桶中
|
|
35
|
+
switch (part.type) {
|
|
36
|
+
case 'text-delta':
|
|
37
|
+
process.stdout.write(part.text);
|
|
38
|
+
fullText += part.text;
|
|
39
|
+
break;
|
|
40
|
+
case 'tool-call':
|
|
41
|
+
hasToolCall = true;
|
|
42
|
+
lastToolCall = { name: part.toolName, input: part.input };
|
|
43
|
+
console.log(`\n [调用: ${part.toolName}(${JSON.stringify(part.input)})]`);
|
|
44
|
+
// 检测是否需要熔断或警告
|
|
45
|
+
const detection = detect(part.toolName, part.input);
|
|
46
|
+
if (detection.stuck) { // 至少到了危险警告阶段
|
|
47
|
+
console.log(` ${detection.message}`);
|
|
48
|
+
if (detection.level === 'critical') { // 直接熔断
|
|
49
|
+
shouldBreak = true;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
messages.push({
|
|
53
|
+
role: 'user',
|
|
54
|
+
content: `[系统提醒] ${detection.message},请换一个思路解决问题,不要重复同样的操作。`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
recordCall(part.toolName, part.input); // 记录当前这次的工具调用
|
|
59
|
+
break;
|
|
60
|
+
case 'tool-result':
|
|
61
|
+
const output = typeof part.output == 'string' ? part.output : JSON.stringify(part.output);
|
|
62
|
+
const preview = output.length > 120 ? output.slice(0, 120) + '...' : output;
|
|
63
|
+
console.log(`[结果: ${part.toolName}] ${preview}`);
|
|
64
|
+
// 记录工具调用结果指纹
|
|
65
|
+
if (lastToolCall) {
|
|
66
|
+
recordResult(lastToolCall.name, lastToolCall.input, output);
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
stepResponse = await result.response;
|
|
72
|
+
stepUsage = await result.usage; // 从LLM获取当前这轮的token用量
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (attempt > MAX_RETRIES || !isRetryable(error))
|
|
77
|
+
throw error;
|
|
78
|
+
const delay = calculateDelay(attempt);
|
|
79
|
+
console.log(`[重试]第${attempt}/${MAX_RETRIES}次失败,${delay}ms 后重试`);
|
|
80
|
+
await sleep(delay);
|
|
81
|
+
hasToolCall = false;
|
|
82
|
+
fullText = '';
|
|
83
|
+
shouldBreak = false;
|
|
84
|
+
lastToolCall = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// 判断是否需要熔断
|
|
88
|
+
if (shouldBreak) {
|
|
89
|
+
console.log('\n [循环检测触发,Agent已停止]');
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
messages.push(...stepResponse.messages);
|
|
93
|
+
// 把 useage 喂给 tracker,tracker内部会按四类 token 分别累计并计算 cost
|
|
94
|
+
const norm = normalizeUsage(stepUsage); // 把 从 AI SDK 返回的 usage 对象规范化成四类 token
|
|
95
|
+
const stepRecord = tracker?.record(model?.modelId || '', norm);
|
|
96
|
+
totalTokens += norm.inputTokens + norm.outputTokens + norm.cacheReadTokens + norm.cacheWriteTokens;
|
|
97
|
+
// cache 命中时打印简洁状态
|
|
98
|
+
if (stepRecord && (norm.cacheReadTokens > 0 || norm.cacheWriteTokens > 0)) {
|
|
99
|
+
const tag = norm.cacheReadTokens > 0 ? 'cache hit' : 'cache miss';
|
|
100
|
+
const detail = norm.cacheReadTokens > 0 ? `read ${norm.cacheReadTokens}` : `write ${norm.cacheWriteTokens}`;
|
|
101
|
+
console.log(` [${tag}] ${detail} tokens ~ 本步骤 $ ${stepRecord.cost.toFixed(5)}`);
|
|
102
|
+
}
|
|
103
|
+
if (totalTokens > TOKEN_BUDGET * 0.9) {
|
|
104
|
+
console.log(` [Token 预算] 已使用 ${totalTokens} / ${TOKEN_BUDGET},(${Math.round((totalTokens / TOKEN_BUDGET) * 100)}%)`);
|
|
105
|
+
}
|
|
106
|
+
// 检查是否超过预算
|
|
107
|
+
if (totalTokens > TOKEN_BUDGET) {
|
|
108
|
+
console.log(`\n [Token 预算耗尽,强制停止]`);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
// 退出条件
|
|
112
|
+
if (!hasToolCall) {
|
|
113
|
+
if (fullText)
|
|
114
|
+
console.log();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
// 还有工具调用,继续循环
|
|
118
|
+
console.log(' --> 模型还在工作,继续下一步...');
|
|
119
|
+
}
|
|
120
|
+
if (step >= MAX_STEPS) {
|
|
121
|
+
console.log('循环次数超过最大限制,退出循环。');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// 判断是否值得重试
|
|
2
|
+
export function isRetryable(error) {
|
|
3
|
+
if (!(error instanceof Error))
|
|
4
|
+
return false;
|
|
5
|
+
const message = error.message || '';
|
|
6
|
+
// http 状态码判断
|
|
7
|
+
const statusMatch = message.match(/(\d{3})/);
|
|
8
|
+
if (statusMatch) {
|
|
9
|
+
const status = parseInt(statusMatch[1]);
|
|
10
|
+
if ([429, 529, 408].includes(status))
|
|
11
|
+
return true;
|
|
12
|
+
if (status >= 500 && status < 600)
|
|
13
|
+
return true; // 服务器错误 LLM 错误
|
|
14
|
+
if (status >= 400 && status < 500)
|
|
15
|
+
return false; // 客户端错误
|
|
16
|
+
}
|
|
17
|
+
// 网络错误
|
|
18
|
+
if (message.includes('ECONNRESET') || message.includes('EPIPE'))
|
|
19
|
+
return true;
|
|
20
|
+
if (message.includes('ETIMEDOUT') || message.includes('timeout'))
|
|
21
|
+
return true;
|
|
22
|
+
if (message.includes('fetch failed') || message.includes('network'))
|
|
23
|
+
return true;
|
|
24
|
+
// AI SDK 会把流式错误包装成 NoOutputGeneratedError
|
|
25
|
+
if (message.includes('No output generated'))
|
|
26
|
+
return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// 指数退避 + 随机抖动
|
|
30
|
+
export function calculateDelay(attempt, baseMs = 500, maxMs = 30000) {
|
|
31
|
+
const exponential = baseMs * Math.pow(2, attempt - 1);
|
|
32
|
+
const capped = Math.min(exponential, maxMs);
|
|
33
|
+
const jitterRange = capped * 0.25;
|
|
34
|
+
const jittered = capped + (Math.random() * 2 - 1) * jitterRange;
|
|
35
|
+
return Math.max(0, Math.round(jittered));
|
|
36
|
+
}
|
|
37
|
+
export function sleep(ms) {
|
|
38
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一个简单的注册表
|
|
3
|
+
* 记录谁在跑
|
|
4
|
+
* 跑到哪里了
|
|
5
|
+
* 结果是什么
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
8
|
+
export class SubAgentRegistry {
|
|
9
|
+
runs = new Map();
|
|
10
|
+
config = DEFAULT_CONFIG;
|
|
11
|
+
idCounter = 0;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
14
|
+
}
|
|
15
|
+
generateId() {
|
|
16
|
+
return `sub-${++this.idCounter}-${Date.now().toString(36).slice(-4)}`;
|
|
17
|
+
}
|
|
18
|
+
canSpawn(currentDepth) {
|
|
19
|
+
if (currentDepth >= this.config.maxSpawnDepth) {
|
|
20
|
+
return { ok: false, reason: `已到达嵌套深度${this.config.maxSpawnDepth}` };
|
|
21
|
+
}
|
|
22
|
+
const activeCount = this.getActiveRuns().length;
|
|
23
|
+
if (activeCount >= this.config.maxConcurrent) {
|
|
24
|
+
return { ok: false, reason: `已到达最大并行子Agent数${this.config.maxConcurrent}` };
|
|
25
|
+
}
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
register(run) {
|
|
29
|
+
this.runs.set(run.id, run);
|
|
30
|
+
}
|
|
31
|
+
complete(id, result) {
|
|
32
|
+
const run = this.runs.get(id);
|
|
33
|
+
if (!run)
|
|
34
|
+
return;
|
|
35
|
+
run.status = 'completed';
|
|
36
|
+
run.finishedAt = new Date().toISOString();
|
|
37
|
+
run.result = result;
|
|
38
|
+
}
|
|
39
|
+
fail(id, error) {
|
|
40
|
+
const run = this.runs.get(id);
|
|
41
|
+
if (!run)
|
|
42
|
+
return;
|
|
43
|
+
run.status = 'error';
|
|
44
|
+
run.finishedAt = new Date().toISOString();
|
|
45
|
+
run.error = error;
|
|
46
|
+
}
|
|
47
|
+
get(id) {
|
|
48
|
+
return this.runs.get(id);
|
|
49
|
+
}
|
|
50
|
+
getActiveRuns() {
|
|
51
|
+
return [...this.runs.values()].filter(run => run.status === 'running');
|
|
52
|
+
}
|
|
53
|
+
getAllRuns() {
|
|
54
|
+
return [...this.runs.values()];
|
|
55
|
+
}
|
|
56
|
+
getConfig() {
|
|
57
|
+
return this.config;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
const EXCLUDED_TOOLS = new Set(['spawn_agent']); // 不允许子Agent调用spawn_agent工具
|
|
3
|
+
const AGENT_COLORS = [
|
|
4
|
+
'\x1b[36m', // cyan
|
|
5
|
+
'\x1b[33m', // yellow
|
|
6
|
+
'\x1b[35m', // magenta
|
|
7
|
+
'\x1b[32m', // green
|
|
8
|
+
'\x1b[34m', // blue
|
|
9
|
+
];
|
|
10
|
+
const RESET = '\x1b[0m';
|
|
11
|
+
function agentTag(index, runId) {
|
|
12
|
+
const color = AGENT_COLORS[index % AGENT_COLORS.length];
|
|
13
|
+
return `${color}[Agent-${index + 1}: ${runId}]${RESET}`;
|
|
14
|
+
}
|
|
15
|
+
export async function spawnAgent(request, ctx, index = 0) {
|
|
16
|
+
const { ok, reason } = ctx.agentRegistry.canSpawn(ctx.currentDepth);
|
|
17
|
+
if (!ok)
|
|
18
|
+
return `[spawn] 拒绝: ${reason}`;
|
|
19
|
+
const runId = ctx.agentRegistry.generateId();
|
|
20
|
+
const tag = agentTag(index, runId);
|
|
21
|
+
const run = {
|
|
22
|
+
id: runId,
|
|
23
|
+
task: request.task,
|
|
24
|
+
status: 'running',
|
|
25
|
+
depth: ctx.currentDepth + 1,
|
|
26
|
+
startedAt: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
ctx.agentRegistry.register(run);
|
|
29
|
+
const timeout = request.timeout || 60000;
|
|
30
|
+
const maxSteps = 30;
|
|
31
|
+
const ac = new AbortController(); // 用于取消子Agent运行
|
|
32
|
+
console.log(` ${tag}启动:${request.task.slice(0, 50)}`);
|
|
33
|
+
// 关键:独立的messages
|
|
34
|
+
const messages = [
|
|
35
|
+
{ role: 'user', content: request.task }
|
|
36
|
+
];
|
|
37
|
+
try {
|
|
38
|
+
const system = ctx.buildSystem() +
|
|
39
|
+
'\n\n[子 Agent 模式] 你是一个被派出去执行具体任务的子 Agent。直接完成任务并输出结论,保持简洁。' +
|
|
40
|
+
'\n当你需要同时获取多个独立信息时(比如读多个文件、搜多个关键词),尽可能在一次回复中并行调用多个工具,不要一个个串行调。';
|
|
41
|
+
// 不能跟父Agent共用一个 Agent Loop
|
|
42
|
+
const tools = ctx.registry.toAISDKFormatUnlocked(EXCLUDED_TOOLS);
|
|
43
|
+
const timer = setTimeout(() => { ac.abort(); }, timeout); // 超时取消子Agent运行
|
|
44
|
+
try {
|
|
45
|
+
let step = 0;
|
|
46
|
+
while (step < maxSteps) {
|
|
47
|
+
step++;
|
|
48
|
+
const isLastStep = step === maxSteps;
|
|
49
|
+
console.log(` ${tag} Step ${step}/${maxSteps}${isLastStep ? ' (总结)' : ''}`);
|
|
50
|
+
if (isLastStep) {
|
|
51
|
+
messages.push({ role: 'user', content: '你已经收集了足够的信息。请直接输出文字总结,不要再调用任何工具。' });
|
|
52
|
+
}
|
|
53
|
+
const result = streamText({
|
|
54
|
+
model: ctx.model, system,
|
|
55
|
+
tools,
|
|
56
|
+
toolChoice: isLastStep ? 'none' : 'auto',
|
|
57
|
+
messages,
|
|
58
|
+
maxRetries: 0, abortSignal: ac.signal,
|
|
59
|
+
providerOptions: { openai: { parallelToolCalls: true } },
|
|
60
|
+
onError: () => { },
|
|
61
|
+
});
|
|
62
|
+
let hasToolCall = false;
|
|
63
|
+
for await (const part of result.fullStream) {
|
|
64
|
+
if (part.type === 'tool-call') {
|
|
65
|
+
hasToolCall = true;
|
|
66
|
+
const argsPreview = JSON.stringify(part.input).slice(0, 80);
|
|
67
|
+
console.log(` ${tag} 调用 ${part.toolName}(${argsPreview})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const response = await result.response;
|
|
71
|
+
messages.push(...response.messages);
|
|
72
|
+
if (!hasToolCall)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
}
|
|
79
|
+
// 提取最后一条assistant回复
|
|
80
|
+
const lastAssistant = [...messages].reverse().find(m => m.role === 'assistant');
|
|
81
|
+
let result = '(无输出)';
|
|
82
|
+
if (lastAssistant) {
|
|
83
|
+
if (typeof lastAssistant.content === 'string') {
|
|
84
|
+
result = lastAssistant.content;
|
|
85
|
+
}
|
|
86
|
+
else if (Array.isArray(lastAssistant.content)) {
|
|
87
|
+
result = lastAssistant.content
|
|
88
|
+
.filter((p) => p.type === 'text')
|
|
89
|
+
.map((p) => p.text)
|
|
90
|
+
.join('');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
ctx.agentRegistry.complete(runId, result);
|
|
94
|
+
console.log(` ${tag} 完成 √ (${result.length}) 字符`);
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const isAbort = err.name === 'AbortError' || ac.signal.aborted;
|
|
99
|
+
const errorMsg = isAbort ? `执行超时 (${timeout / 1000}s)` : (err.message || String(err));
|
|
100
|
+
ctx.agentRegistry.fail(runId, errorMsg);
|
|
101
|
+
console.log(` ${tag} ${isAbort ? '超时' : '失败'} ✗: ${errorMsg}`);
|
|
102
|
+
if (isAbort) {
|
|
103
|
+
const partial = [...messages].reverse().find(m => m.role === 'assistant');
|
|
104
|
+
if (partial) {
|
|
105
|
+
const text = typeof partial.content === 'string' ? partial.content
|
|
106
|
+
: Array.isArray(partial.content)
|
|
107
|
+
? partial.content.filter((p) => p.type === 'text').map((p) => p.text).join('')
|
|
108
|
+
: '';
|
|
109
|
+
if (text)
|
|
110
|
+
return `[部分结果] ${text}`; // 子Agent执行超时,返回部分结果
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return `[sub-agent 执行失败] ${errorMsg}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function spawnParallel(requests, ctx) {
|
|
117
|
+
console.log(`\n ┌─ 派发 ${requests.length} 个子 Agent 并行执行 ─┐`);
|
|
118
|
+
const results = await Promise.all(requests.map(async (req, i) => {
|
|
119
|
+
const result = await spawnAgent(req, ctx, i); // 并行执行每个子Agent
|
|
120
|
+
return { task: req.task, result };
|
|
121
|
+
}));
|
|
122
|
+
console.log(` └─ 全部完成 (${results.length}/${requests.length}) ─┘\n`);
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function createAgentCommands(agentRegistry) {
|
|
2
|
+
const handler = (cmd) => {
|
|
3
|
+
if (!cmd.startsWith('/agents'))
|
|
4
|
+
return false;
|
|
5
|
+
const runs = agentRegistry.getAllRuns();
|
|
6
|
+
if (runs.length === 0) {
|
|
7
|
+
console.log(' 暂无子 Agent 记录');
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const active = runs.filter(r => r.status === 'running');
|
|
11
|
+
const completed = runs.filter(r => r.status === 'completed');
|
|
12
|
+
const failed = runs.filter(r => r.status === 'error');
|
|
13
|
+
console.log(` 子 Agent 记录 (${runs.length}):`);
|
|
14
|
+
for (const r of runs) {
|
|
15
|
+
const icon = r.status === 'running' ? '⟳'
|
|
16
|
+
: r.status === 'completed' ? '✓'
|
|
17
|
+
: '✗';
|
|
18
|
+
const detail = r.status === 'completed'
|
|
19
|
+
? `${r.result?.slice(0, 60)}...`
|
|
20
|
+
: r.status === 'error'
|
|
21
|
+
? r.error
|
|
22
|
+
: '执行中...';
|
|
23
|
+
console.log(` ${icon} ${r.id} (depth=${r.depth}) — ${r.task.slice(0, 40)}`);
|
|
24
|
+
console.log(` ${detail}`);
|
|
25
|
+
}
|
|
26
|
+
const config = agentRegistry.getConfig();
|
|
27
|
+
console.log(`\n 活跃: ${active.length}/${config.maxConcurrent} | 完成: ${completed.length} | 失败: ${failed.length}`);
|
|
28
|
+
console.log(` 最大深度: ${config.maxSpawnDepth} | 最大并发: ${config.maxConcurrent}`);
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
};
|
|
32
|
+
return [handler];
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderUsageView, buildContextSnapshot, renderContextView } from '../context/view.js';
|
|
2
|
+
export const contextCommands = [
|
|
3
|
+
(cmd, ctx) => {
|
|
4
|
+
if (cmd !== '/context' && cmd !== 'context')
|
|
5
|
+
return false;
|
|
6
|
+
const SYSTEM = ctx.builder.build(ctx.makePromptCtx());
|
|
7
|
+
const memoryChars = ctx.memoryStore?.buildPromptSection().length ?? 0;
|
|
8
|
+
const snapshot = buildContextSnapshot({
|
|
9
|
+
modelName: process.env.DASHSCOPE_API_KEY ? 'Qwen Plus' : 'Mock Model (开发用)',
|
|
10
|
+
modelId: process.env.DASHSCOPE_API_KEY ? 'qwen3-6-plus' : 'mock-model',
|
|
11
|
+
windowTokens: 1_000_000,
|
|
12
|
+
systemPromptChars: SYSTEM.length,
|
|
13
|
+
toolDescriptionChars: ctx.registry.getActiveTools().reduce((a, t) => a + t.name.length + (t.description?.length || 0) + JSON.stringify(t.parameters || {}).length, 0),
|
|
14
|
+
memoryChars,
|
|
15
|
+
skillsChars: 0,
|
|
16
|
+
messages: ctx.messages,
|
|
17
|
+
});
|
|
18
|
+
console.log(renderContextView(snapshot));
|
|
19
|
+
return true;
|
|
20
|
+
},
|
|
21
|
+
(cmd, ctx) => {
|
|
22
|
+
if (cmd !== '/usage' && cmd !== 'usage')
|
|
23
|
+
return false;
|
|
24
|
+
console.log(renderUsageView(ctx.tracker));
|
|
25
|
+
return true;
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function createCronCommands(cronService) {
|
|
2
|
+
const handler = (cmd) => {
|
|
3
|
+
if (!cmd.startsWith('/cron'))
|
|
4
|
+
return false;
|
|
5
|
+
const sub = cmd.slice(5).trim();
|
|
6
|
+
if (!sub || sub === 'list') {
|
|
7
|
+
const jobs = cronService.list();
|
|
8
|
+
if (jobs.length === 0) {
|
|
9
|
+
console.log(' 暂无定时任务');
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.log(` 定时任务 (${jobs.length}):`);
|
|
13
|
+
for (const j of jobs) {
|
|
14
|
+
const icon = j.status === 'running' ? '⟳' : j.status === 'scheduled' ? '◉' : j.status === 'disabled' ? '○' : '·';
|
|
15
|
+
console.log(` ${icon} ${j.config.id} — ${j.config.name} [${j.config.schedule}] (${j.status})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (sub === 'logs') {
|
|
21
|
+
const logs = cronService.getRecentLogs(undefined, 10);
|
|
22
|
+
if (logs.length === 0) {
|
|
23
|
+
console.log(' 暂无执行记录');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log(' 最近执行记录:');
|
|
27
|
+
for (const l of logs) {
|
|
28
|
+
const icon = l.status === 'success' ? '✓' : '✗';
|
|
29
|
+
console.log(` ${icon} ${l.jobId} @ ${l.startedAt} — ${l.output?.slice(0, 80) || l.error || ''}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
console.log(' 用法: /cron [list|logs]');
|
|
35
|
+
return true;
|
|
36
|
+
};
|
|
37
|
+
return [handler];
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { estimateMessageTokens, applyDefense } from '../context/defense.js';
|
|
2
|
+
export const debugCommands = [
|
|
3
|
+
(cmd, ctx) => {
|
|
4
|
+
if (cmd !== '模拟长对话' && cmd !== 'sim')
|
|
5
|
+
return false;
|
|
6
|
+
const now = Date.now();
|
|
7
|
+
console.log('\n[模拟] 注入 20 条历史消息(含大量工具结果)...');
|
|
8
|
+
for (let i = 0; i < 5; i++) {
|
|
9
|
+
const age = (20 - i * 4) * 60 * 1000;
|
|
10
|
+
const idx = ctx.messages.length;
|
|
11
|
+
ctx.messages.push({ role: 'user', content: `第 ${i + 1} 轮:帮我读文件 file-${i}.ts` });
|
|
12
|
+
ctx.timestamps.set(idx, now - age);
|
|
13
|
+
ctx.messages.push({ role: 'assistant', content: [{ type: 'tool-call', toolCallId: `sim-${i}`, toolName: 'read_file', input: { path: `file-${i}.ts` } }] });
|
|
14
|
+
ctx.timestamps.set(idx + 1, now - age);
|
|
15
|
+
const bigContent = `// file-${i}.ts\n` + 'export function handler() {\n // ...\n}\n'.repeat(200);
|
|
16
|
+
ctx.messages.push({ role: 'tool', content: [{ type: 'tool-result', toolCallId: `sim-${i}`, toolName: 'read_file', output: bigContent }] });
|
|
17
|
+
ctx.timestamps.set(idx + 2, now - age);
|
|
18
|
+
ctx.messages.push({ role: 'assistant', content: [{ type: 'text', text: `文件 file-${i}.ts 的内容已读取。` }] });
|
|
19
|
+
ctx.timestamps.set(idx + 3, now - age);
|
|
20
|
+
}
|
|
21
|
+
console.log(`[模拟完成] ${ctx.messages.length} 条消息, ~${estimateMessageTokens(ctx.messages)} tokens\n`);
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
(cmd, ctx) => {
|
|
25
|
+
if (cmd !== '执行防线' && cmd !== 'defend')
|
|
26
|
+
return false;
|
|
27
|
+
console.log('\n--- 执行三层防线 ---');
|
|
28
|
+
const before = estimateMessageTokens(ctx.messages);
|
|
29
|
+
const def = applyDefense(ctx.messages, ctx.timestamps);
|
|
30
|
+
ctx.messages = def.messages;
|
|
31
|
+
console.log(` [Layer 2] 截断: ${def.truncated} 条, 预算清理: ${def.compacted} 条`);
|
|
32
|
+
console.log(` [Layer 3] 软修剪: ${def.softPruned}, 硬清除: ${def.hardPruned}`);
|
|
33
|
+
console.log(` [结果] ~${before} → ~${def.tokenEstimate} tokens (节省 ${before - def.tokenEstimate})\n`);
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
(cmd, ctx) => {
|
|
37
|
+
if (cmd !== 'status' && cmd !== '查看状态')
|
|
38
|
+
return false;
|
|
39
|
+
const tokens = estimateMessageTokens(ctx.messages);
|
|
40
|
+
const memCount = ctx.memoryStore?.list().length ?? 0;
|
|
41
|
+
console.log(`\n[状态] ${ctx.messages.length} 条消息, ~${tokens} tokens, ${memCount} 条记忆\n`);
|
|
42
|
+
return true;
|
|
43
|
+
},
|
|
44
|
+
];
|