@code4bug/jarvis-agent 1.0.4 → 1.1.5
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/README.md +67 -0
- package/dist/agents/jarvis.md +11 -0
- package/dist/cli.js +13 -0
- package/dist/components/MessageItem.js +9 -5
- package/dist/components/StatusBar.d.ts +2 -1
- package/dist/components/StatusBar.js +5 -4
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +3 -1
- package/dist/config/userProfile.d.ts +4 -0
- package/dist/config/userProfile.js +25 -0
- package/dist/core/AgentMessageBus.d.ts +50 -0
- package/dist/core/AgentMessageBus.js +128 -0
- package/dist/core/AgentRegistry.d.ts +22 -0
- package/dist/core/AgentRegistry.js +16 -0
- package/dist/core/QueryEngine.d.ts +7 -0
- package/dist/core/QueryEngine.js +82 -30
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +208 -0
- package/dist/core/WorkerBridge.js +80 -0
- package/dist/core/busAccess.d.ts +9 -0
- package/dist/core/busAccess.js +32 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +63 -0
- package/dist/core/query.d.ts +4 -0
- package/dist/core/query.js +169 -4
- package/dist/core/queryWorker.d.ts +62 -0
- package/dist/core/queryWorker.js +46 -0
- package/dist/core/spawnRegistry.d.ts +16 -0
- package/dist/core/spawnRegistry.js +32 -0
- package/dist/core/subAgentWorker.d.ts +89 -0
- package/dist/core/subAgentWorker.js +121 -0
- package/dist/core/workerBusProxy.d.ts +10 -0
- package/dist/core/workerBusProxy.js +57 -0
- package/dist/hooks/useSlashMenu.d.ts +2 -0
- package/dist/hooks/useSlashMenu.js +5 -1
- package/dist/hooks/useTokenDisplay.d.ts +1 -0
- package/dist/hooks/useTokenDisplay.js +5 -0
- package/dist/index.js +2 -0
- package/dist/screens/repl.js +48 -16
- package/dist/screens/slashCommands.js +2 -1
- package/dist/services/api/llm.d.ts +2 -0
- package/dist/services/api/llm.js +23 -1
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +127 -0
- package/dist/tools/index.d.ts +7 -1
- package/dist/tools/index.js +13 -2
- package/dist/tools/publishMessage.d.ts +8 -0
- package/dist/tools/publishMessage.js +41 -0
- package/dist/tools/readChannel.d.ts +8 -0
- package/dist/tools/readChannel.js +44 -0
- package/dist/tools/runAgent.d.ts +11 -0
- package/dist/tools/runAgent.js +111 -0
- package/dist/tools/runCommand.js +16 -0
- package/dist/tools/sendToAgent.d.ts +11 -0
- package/dist/tools/sendToAgent.js +35 -0
- package/dist/tools/spawnAgent.d.ts +6 -0
- package/dist/tools/spawnAgent.js +180 -0
- package/dist/tools/subscribeMessage.d.ts +8 -0
- package/dist/tools/subscribeMessage.js +59 -0
- package/dist/types/index.d.ts +49 -1
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getBus } from '../core/busAccess.js';
|
|
2
|
+
export const publishMessage = {
|
|
3
|
+
name: 'publish_message',
|
|
4
|
+
description: [
|
|
5
|
+
'向指定频道发布一条消息,供其他 SubAgent 或主 Agent 订阅接收。',
|
|
6
|
+
'适用场景:',
|
|
7
|
+
' - SubAgent 完成阶段性任务后,将中间结果广播给协作的其他 SubAgent',
|
|
8
|
+
' - 多 Agent 流水线中,上游 Agent 通知下游 Agent 开始处理',
|
|
9
|
+
' - Agent 间共享数据,无需等待主 Agent 中转',
|
|
10
|
+
'注意:消息会保留在频道历史中,可通过 read_channel 工具查看。',
|
|
11
|
+
].join('\n'),
|
|
12
|
+
parameters: {
|
|
13
|
+
channel: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: '频道名称,建议使用语义化命名,如 "research-result" / "code-review-done"',
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
payload: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: '消息内容,可以是纯文本、JSON 字符串或任意结构化数据',
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
agent_id: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: '可选。发布者标识,默认为 "unknown"。建议填入当前 Agent 的 taskId 或角色名',
|
|
26
|
+
required: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
execute: async (args) => {
|
|
30
|
+
const channel = (args.channel || '').trim();
|
|
31
|
+
const payload = (args.payload || '').trim();
|
|
32
|
+
const agentId = (args.agent_id || 'unknown').trim();
|
|
33
|
+
if (!channel)
|
|
34
|
+
throw new Error('缺少必填参数: channel');
|
|
35
|
+
if (!payload)
|
|
36
|
+
throw new Error('缺少必填参数: payload');
|
|
37
|
+
const bus = await getBus();
|
|
38
|
+
await bus.publish(agentId, channel, payload);
|
|
39
|
+
return `已向频道 "${channel}" 发布消息(来自: ${agentId})`;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { getBus } from '../core/busAccess.js';
|
|
2
|
+
export const readChannel = {
|
|
3
|
+
name: 'read_channel',
|
|
4
|
+
description: [
|
|
5
|
+
'读取指定频道的历史消息,或列出所有活跃频道。非阻塞,立即返回。',
|
|
6
|
+
'适用场景:',
|
|
7
|
+
' - 主 Agent 汇总所有 SubAgent 的通讯结果',
|
|
8
|
+
' - 查看某个频道的完整消息历史',
|
|
9
|
+
' - 调试多 Agent 协作流程',
|
|
10
|
+
].join('\n'),
|
|
11
|
+
parameters: {
|
|
12
|
+
channel: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: '频道名称。若填 "*" 则列出所有活跃频道名称',
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
limit: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: '可选。最多返回最近 N 条消息,默认返回全部',
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
execute: async (args) => {
|
|
24
|
+
const channel = (args.channel || '').trim();
|
|
25
|
+
if (!channel)
|
|
26
|
+
throw new Error('缺少必填参数: channel');
|
|
27
|
+
const bus = await getBus();
|
|
28
|
+
if (channel === '*') {
|
|
29
|
+
const channels = await bus.listChannels();
|
|
30
|
+
if (channels.length === 0)
|
|
31
|
+
return '当前没有活跃频道';
|
|
32
|
+
return `活跃频道列表(${channels.length} 个):\n${channels.map((c) => ` - ${c}`).join('\n')}`;
|
|
33
|
+
}
|
|
34
|
+
const limit = args.limit ? parseInt(args.limit, 10) : undefined;
|
|
35
|
+
const msgs = await bus.getHistory(channel, limit);
|
|
36
|
+
if (msgs.length === 0)
|
|
37
|
+
return `频道 "${channel}" 暂无消息`;
|
|
38
|
+
const lines = msgs.map((msg, i) => {
|
|
39
|
+
const time = new Date(msg.timestamp).toISOString();
|
|
40
|
+
return `[${i + 1}] [来自: ${msg.from}] [${time}]\n${msg.payload}`;
|
|
41
|
+
});
|
|
42
|
+
return `频道 "${channel}" 共 ${msgs.length} 条消息:\n\n${lines.join('\n\n---\n\n')}`;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run_agent 工具
|
|
3
|
+
*
|
|
4
|
+
* 允许主 Agent 在独立 Worker 线程中创建并运行一个 SubAgent,
|
|
5
|
+
* SubAgent 拥有完整的 react_loop 能力,可自主调用工具完成子任务。
|
|
6
|
+
*
|
|
7
|
+
* 主 Agent 调用此工具后会阻塞等待 SubAgent 完成,并返回其最终输出。
|
|
8
|
+
* 若需并行执行多个子任务,主 Agent 可在同一轮中多次调用此工具(并行工具调用)。
|
|
9
|
+
*/
|
|
10
|
+
import { Tool } from '../types/index.js';
|
|
11
|
+
export declare const runAgent: Tool;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run_agent 工具
|
|
3
|
+
*
|
|
4
|
+
* 允许主 Agent 在独立 Worker 线程中创建并运行一个 SubAgent,
|
|
5
|
+
* SubAgent 拥有完整的 react_loop 能力,可自主调用工具完成子任务。
|
|
6
|
+
*
|
|
7
|
+
* 主 Agent 调用此工具后会阻塞等待 SubAgent 完成,并返回其最终输出。
|
|
8
|
+
* 若需并行执行多个子任务,主 Agent 可在同一轮中多次调用此工具(并行工具调用)。
|
|
9
|
+
*/
|
|
10
|
+
import { v4 as uuid } from 'uuid';
|
|
11
|
+
import { SubAgentBridge } from '../core/SubAgentBridge.js';
|
|
12
|
+
/** 将 task_id / role 转换为可读的 Agent 标签,例如 "ResearchAgent" */
|
|
13
|
+
function resolveAgentLabel(taskId, role) {
|
|
14
|
+
if (role) {
|
|
15
|
+
// 从角色描述中提取关键词,例如 "你是一个代码审查助手" → "代码审查Agent"
|
|
16
|
+
const match = role.match(/[\u4e00-\u9fa5a-zA-Z0-9]+/g);
|
|
17
|
+
if (match && match.length > 0) {
|
|
18
|
+
// 取前两个词拼接,最长 8 个字符
|
|
19
|
+
const label = match.slice(0, 2).join('').slice(0, 8);
|
|
20
|
+
return `${label}Agent`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// 回退:用 taskId 前 8 位
|
|
24
|
+
return `SubAgent-${taskId.slice(0, 8)}`;
|
|
25
|
+
}
|
|
26
|
+
export const runAgent = {
|
|
27
|
+
name: 'run_agent',
|
|
28
|
+
description: [
|
|
29
|
+
'在独立线程中同步运行一个 SubAgent,阻塞等待其完成后返回最终结果。',
|
|
30
|
+
'适用场景:',
|
|
31
|
+
' - 将复杂任务拆解为多个独立子任务并行执行',
|
|
32
|
+
' - 需要隔离执行上下文的子任务(如沙箱式文件操作)',
|
|
33
|
+
' - 多角色协同:为不同子任务指定不同的角色和工具权限',
|
|
34
|
+
'注意:SubAgent 与主 Agent 共享文件系统,但拥有独立的对话上下文。',
|
|
35
|
+
].join('\n'),
|
|
36
|
+
parameters: {
|
|
37
|
+
instruction: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
description: '发给 SubAgent 的任务指令,应清晰描述目标、输入和期望输出格式',
|
|
40
|
+
required: true,
|
|
41
|
+
},
|
|
42
|
+
role: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
description: '可选。SubAgent 的角色描述,用于约束其行为,例如"你是一个专注于代码审查的助手"',
|
|
45
|
+
required: false,
|
|
46
|
+
},
|
|
47
|
+
allowed_tools: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
description: '可选。逗号分隔的工具名列表,限制 SubAgent 可用的工具范围,例如 "read_file,search_files"。为空则继承全部工具',
|
|
50
|
+
required: false,
|
|
51
|
+
},
|
|
52
|
+
task_id: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: '可选。任务唯一标识,用于在并行场景中区分多个 SubAgent。不填则自动生成',
|
|
55
|
+
required: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
execute: async (args, _abortSignal, toolCallbacks) => {
|
|
59
|
+
const instruction = (args.instruction || '').trim();
|
|
60
|
+
if (!instruction) {
|
|
61
|
+
throw new Error('缺少必填参数: instruction(SubAgent 任务指令)');
|
|
62
|
+
}
|
|
63
|
+
const role = (args.role || '').trim() || undefined;
|
|
64
|
+
const taskId = (args.task_id || '').trim() || uuid();
|
|
65
|
+
// 解析 allowed_tools
|
|
66
|
+
const allowedToolsRaw = (args.allowed_tools || '').trim();
|
|
67
|
+
const allowedTools = allowedToolsRaw
|
|
68
|
+
? allowedToolsRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
69
|
+
: undefined;
|
|
70
|
+
// 构造任务指令:若指定了 role,将其注入到 instruction 前缀
|
|
71
|
+
const fullInstruction = role
|
|
72
|
+
? `[角色设定] ${role}\n\n[任务]\n${instruction}`
|
|
73
|
+
: instruction;
|
|
74
|
+
// 生成 UI 展示用的 Agent 标签,例如 "代码审查Agent"
|
|
75
|
+
const agentLabel = resolveAgentLabel(taskId, role);
|
|
76
|
+
const bridge = new SubAgentBridge();
|
|
77
|
+
const result = await bridge.run({
|
|
78
|
+
taskId,
|
|
79
|
+
instruction: fullInstruction,
|
|
80
|
+
allowedTools,
|
|
81
|
+
}, {
|
|
82
|
+
// 将 SubAgent 消息注入 subAgentId 后推送到主线程 UI
|
|
83
|
+
onMessage: (_tid, msg) => {
|
|
84
|
+
const tagged = { ...msg, subAgentId: agentLabel };
|
|
85
|
+
toolCallbacks?.onSubAgentMessage?.(tagged);
|
|
86
|
+
},
|
|
87
|
+
onUpdateMessage: (_tid, id, updates) => {
|
|
88
|
+
toolCallbacks?.onSubAgentUpdateMessage?.(id, updates);
|
|
89
|
+
},
|
|
90
|
+
onStreamText: () => { },
|
|
91
|
+
onLoopStateChange: () => { },
|
|
92
|
+
// 危险命令确认:SubAgent 默认取消,避免无人值守时阻塞
|
|
93
|
+
onConfirmDangerousCommand: async () => 'cancel',
|
|
94
|
+
});
|
|
95
|
+
if (result.status === 'error') {
|
|
96
|
+
throw new Error(`SubAgent [${taskId}] 执行失败: ${result.error}`);
|
|
97
|
+
}
|
|
98
|
+
if (result.status === 'aborted') {
|
|
99
|
+
return `SubAgent [${taskId}] 已中断,部分结果:\n${result.output || '(无输出)'}`;
|
|
100
|
+
}
|
|
101
|
+
// 构造返回给主 Agent 的摘要
|
|
102
|
+
const lines = [
|
|
103
|
+
`SubAgent [${taskId}] 执行完成`,
|
|
104
|
+
`执行步骤数: ${result.messages.length}`,
|
|
105
|
+
'',
|
|
106
|
+
'=== 最终输出 ===',
|
|
107
|
+
result.output || '(SubAgent 未产生文本输出)',
|
|
108
|
+
];
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
},
|
|
111
|
+
};
|
package/dist/tools/runCommand.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
2
|
import { sanitizeOutput } from '../core/safeguard.js';
|
|
3
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
3
4
|
/**
|
|
4
5
|
* 异步执行命令,支持通过 abortSignal 中断子进程
|
|
5
6
|
*/
|
|
6
7
|
function execAsync(command, options, abortSignal) {
|
|
7
8
|
return new Promise((resolve, reject) => {
|
|
9
|
+
logInfo('bash.exec.start', { command });
|
|
8
10
|
const child = exec(command, options, (error, stdout, stderr) => {
|
|
9
11
|
// 清理轮询
|
|
10
12
|
if (pollTimer !== null)
|
|
@@ -12,6 +14,10 @@ function execAsync(command, options, abortSignal) {
|
|
|
12
14
|
// 被中断时直接返回已有输出,不视为错误
|
|
13
15
|
if (abortSignal?.aborted) {
|
|
14
16
|
const partial = sanitizeOutput(String(stdout ?? '').trim());
|
|
17
|
+
logWarn('bash.exec.aborted', {
|
|
18
|
+
command,
|
|
19
|
+
stdoutLength: String(stdout ?? '').length,
|
|
20
|
+
});
|
|
15
21
|
resolve(partial ? `(命令被中断)\n${partial}` : '(命令被中断)');
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
@@ -25,9 +31,18 @@ function execAsync(command, options, abortSignal) {
|
|
|
25
31
|
parts.push(`[exit code] ${error.code}`);
|
|
26
32
|
if (parts.length === 0)
|
|
27
33
|
parts.push(error.message);
|
|
34
|
+
logError('bash.exec.failed', error, {
|
|
35
|
+
command,
|
|
36
|
+
stdoutLength: String(stdout ?? '').length,
|
|
37
|
+
stderrLength: String(stderr ?? '').length,
|
|
38
|
+
});
|
|
28
39
|
reject(new Error(`命令执行失败:\n${parts.join('\n')}`));
|
|
29
40
|
return;
|
|
30
41
|
}
|
|
42
|
+
logInfo('bash.exec.done', {
|
|
43
|
+
command,
|
|
44
|
+
stdoutLength: String(stdout ?? '').length,
|
|
45
|
+
});
|
|
31
46
|
resolve(sanitizeOutput(String(stdout).trim()) || '(命令执行完成,无输出)');
|
|
32
47
|
});
|
|
33
48
|
// 轮询 abortSignal,检测到中断时 kill 子进程
|
|
@@ -38,6 +53,7 @@ function execAsync(command, options, abortSignal) {
|
|
|
38
53
|
if (pollTimer !== null)
|
|
39
54
|
clearInterval(pollTimer);
|
|
40
55
|
pollTimer = null;
|
|
56
|
+
logWarn('bash.exec.kill_requested', { command, pid: child.pid });
|
|
41
57
|
// 先尝试 SIGTERM,给进程优雅退出的机会
|
|
42
58
|
try {
|
|
43
59
|
child.kill('SIGTERM');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* send_to_agent 工具
|
|
3
|
+
*
|
|
4
|
+
* 主 Agent 向指定后台子 Agent 发送消息。
|
|
5
|
+
* 本质是向 "agent-inbox:{task_id}" 频道 publish,
|
|
6
|
+
* 子 Agent 通过 subscribe_message 订阅该频道接收。
|
|
7
|
+
*
|
|
8
|
+
* 配合 spawn_agent 使用,实现主 Agent ↔ 子 Agent 的多轮对话。
|
|
9
|
+
*/
|
|
10
|
+
import { Tool } from '../types/index.js';
|
|
11
|
+
export declare const sendToAgent: Tool;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getBus } from '../core/busAccess.js';
|
|
2
|
+
export const sendToAgent = {
|
|
3
|
+
name: 'send_to_agent',
|
|
4
|
+
description: [
|
|
5
|
+
'向指定后台子 Agent 发送一条消息,子 Agent 会从其收件箱频道接收。',
|
|
6
|
+
'前提:子 Agent 必须已通过 spawn_agent 启动,且在其 instruction 中包含订阅收件箱的逻辑。',
|
|
7
|
+
'',
|
|
8
|
+
'消息流向:主 Agent → agent-inbox:{task_id} → 子 Agent',
|
|
9
|
+
'等待回复:使用 subscribe_message 订阅 "agent-reply:{task_id}" 频道。',
|
|
10
|
+
].join('\n'),
|
|
11
|
+
parameters: {
|
|
12
|
+
task_id: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: '目标子 Agent 的 task_id(由 spawn_agent 返回)',
|
|
15
|
+
required: true,
|
|
16
|
+
},
|
|
17
|
+
message: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: '要发送给子 Agent 的消息内容',
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
execute: async (args) => {
|
|
24
|
+
const taskId = (args.task_id || '').trim();
|
|
25
|
+
const message = (args.message || '').trim();
|
|
26
|
+
if (!taskId)
|
|
27
|
+
throw new Error('缺少必填参数: task_id');
|
|
28
|
+
if (!message)
|
|
29
|
+
throw new Error('缺少必填参数: message');
|
|
30
|
+
const inboxChannel = `agent-inbox:${taskId}`;
|
|
31
|
+
const bus = await getBus();
|
|
32
|
+
await bus.publish('main-agent', inboxChannel, message);
|
|
33
|
+
return `消息已发送至子 Agent [${taskId}]\n频道: ${inboxChannel}\n内容: ${message}`;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 在主线程侧直接启动 SubAgent(供 WorkerBridge 调用)
|
|
4
|
+
*/
|
|
5
|
+
export declare function spawnSubAgentInMainThread(taskId: string, instruction: string, agentLabel: string, allowedTools?: string[]): string;
|
|
6
|
+
export declare const spawnAgent: Tool;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn_agent 工具
|
|
3
|
+
*
|
|
4
|
+
* 异步启动一个 SubAgent(不阻塞主 Agent),子 Agent 在后台独立运行。
|
|
5
|
+
* 主 Agent 可通过 send_to_agent 工具向其发送消息,子 Agent 通过
|
|
6
|
+
* subscribe_message 订阅自己的 inbox 频道(agent-inbox:{task_id})接收。
|
|
7
|
+
*
|
|
8
|
+
* 重要:SubAgentBridge 必须在主线程创建,才能访问共享的 agentMessageBus 单例。
|
|
9
|
+
* 因此本工具通过 IPC(parentPort)将 spawn 请求委托给主线程(WorkerBridge)执行。
|
|
10
|
+
*/
|
|
11
|
+
import { v4 as uuid } from 'uuid';
|
|
12
|
+
import { isMainThread, parentPort } from 'worker_threads';
|
|
13
|
+
import { SubAgentBridge } from '../core/SubAgentBridge.js';
|
|
14
|
+
import { agentUIBus } from '../core/AgentRegistry.js';
|
|
15
|
+
import { agentMessageBus } from '../core/AgentMessageBus.js';
|
|
16
|
+
import { getBus } from '../core/busAccess.js';
|
|
17
|
+
import { pendingSpawnRequests, incrementActiveAgents, decrementActiveAgents } from '../core/spawnRegistry.js';
|
|
18
|
+
import { logError, logInfo } from '../core/logger.js';
|
|
19
|
+
/** 运行中的 task_id 集合(主线程侧,防止重复启动) */
|
|
20
|
+
const runningAgents = new Set();
|
|
21
|
+
function resolveAgentLabel(taskId, role) {
|
|
22
|
+
if (role) {
|
|
23
|
+
const match = role.match(/[\u4e00-\u9fa5a-zA-Z0-9]+/g);
|
|
24
|
+
if (match && match.length > 0) {
|
|
25
|
+
return `${match.slice(0, 2).join('').slice(0, 8)}Agent`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return `SubAgent-${taskId.slice(0, 8)}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 在主线程侧直接启动 SubAgent(供 WorkerBridge 调用)
|
|
32
|
+
*/
|
|
33
|
+
export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allowedTools) {
|
|
34
|
+
if (runningAgents.has(taskId)) {
|
|
35
|
+
logInfo('spawn_agent.duplicate', { taskId, agentLabel });
|
|
36
|
+
return `子 Agent [${taskId}] 已在运行中,请使用 send_to_agent 向其发送消息,或使用不同的 task_id 启动新实例。`;
|
|
37
|
+
}
|
|
38
|
+
const replyChannel = `agent-reply:${taskId}`;
|
|
39
|
+
const bridge = new SubAgentBridge();
|
|
40
|
+
runningAgents.add(taskId);
|
|
41
|
+
incrementActiveAgents();
|
|
42
|
+
logInfo('spawn_agent.started', {
|
|
43
|
+
taskId,
|
|
44
|
+
agentLabel,
|
|
45
|
+
allowedTools,
|
|
46
|
+
});
|
|
47
|
+
bridge.run({ taskId, instruction, allowedTools }, {
|
|
48
|
+
onMessage: (_tid, msg) => {
|
|
49
|
+
const tagged = { ...msg, subAgentId: agentLabel };
|
|
50
|
+
agentUIBus.push(tagged);
|
|
51
|
+
},
|
|
52
|
+
onUpdateMessage: (_tid, id, updates) => {
|
|
53
|
+
agentUIBus.update(id, updates);
|
|
54
|
+
},
|
|
55
|
+
onStreamText: () => { },
|
|
56
|
+
onLoopStateChange: () => { },
|
|
57
|
+
onConfirmDangerousCommand: async () => 'cancel',
|
|
58
|
+
}).then((result) => {
|
|
59
|
+
runningAgents.delete(taskId);
|
|
60
|
+
decrementActiveAgents();
|
|
61
|
+
logInfo('spawn_agent.done', {
|
|
62
|
+
taskId,
|
|
63
|
+
agentLabel,
|
|
64
|
+
status: result.status,
|
|
65
|
+
outputLength: result.output.length,
|
|
66
|
+
});
|
|
67
|
+
agentMessageBus.publish(taskId, replyChannel, `[AGENT_DONE] ${result.output || '子 Agent 已完成'}`);
|
|
68
|
+
}).catch((err) => {
|
|
69
|
+
runningAgents.delete(taskId);
|
|
70
|
+
decrementActiveAgents();
|
|
71
|
+
logError('spawn_agent.failed', err, {
|
|
72
|
+
taskId,
|
|
73
|
+
agentLabel,
|
|
74
|
+
});
|
|
75
|
+
agentMessageBus.publish(taskId, replyChannel, `[AGENT_ERROR] ${err.message}`);
|
|
76
|
+
});
|
|
77
|
+
const inboxChannel = `agent-inbox:${taskId}`;
|
|
78
|
+
return [
|
|
79
|
+
`子 Agent 已在后台启动`,
|
|
80
|
+
`task_id: ${taskId}`,
|
|
81
|
+
`角色标签: ${agentLabel}`,
|
|
82
|
+
`收件箱频道: ${inboxChannel}`,
|
|
83
|
+
`回复频道: ${replyChannel}`,
|
|
84
|
+
``,
|
|
85
|
+
`使用 send_to_agent 向其发送消息,使用 subscribe_message 订阅 "${replyChannel}" 等待回复。`,
|
|
86
|
+
].join('\n');
|
|
87
|
+
}
|
|
88
|
+
export const spawnAgent = {
|
|
89
|
+
name: 'spawn_agent',
|
|
90
|
+
description: [
|
|
91
|
+
'异步启动一个 SubAgent,立即返回 task_id,子 Agent 在后台独立运行。',
|
|
92
|
+
'与 run_agent 的区别:',
|
|
93
|
+
' - run_agent:阻塞等待子 Agent 完成后返回结果(适合一次性子任务)',
|
|
94
|
+
' - spawn_agent:立即返回,主 Agent 可继续执行,通过消息总线与子 Agent 双向通信(适合多轮对话)',
|
|
95
|
+
'',
|
|
96
|
+
'子 Agent 通信约定:',
|
|
97
|
+
' - 主 Agent → 子 Agent:使用 send_to_agent 工具,或向 "agent-inbox:{task_id}" 频道 publish',
|
|
98
|
+
' - 子 Agent → 主 Agent:子 Agent 向 "agent-reply:{task_id}" 频道 publish,主 Agent 用 subscribe_message 接收',
|
|
99
|
+
' - 子 Agent 的 instruction 中应包含订阅自己 inbox 的指令,例如:',
|
|
100
|
+
' "每次回复前先调用 subscribe_message 订阅频道 agent-inbox:{task_id} 获取新问题"',
|
|
101
|
+
'',
|
|
102
|
+
'查看后台 Agent 状态:使用 read_channel 工具查看 "agent-reply:{task_id}" 频道历史。',
|
|
103
|
+
].join('\n'),
|
|
104
|
+
parameters: {
|
|
105
|
+
instruction: {
|
|
106
|
+
type: 'string',
|
|
107
|
+
description: [
|
|
108
|
+
'发给子 Agent 的初始指令。',
|
|
109
|
+
'若需要多轮交互,指令中应告知子 Agent:',
|
|
110
|
+
' 1. 订阅频道 "agent-inbox:{task_id}" 等待主 Agent 的消息',
|
|
111
|
+
' 2. 处理后将回复发布到 "agent-reply:{task_id}" 频道',
|
|
112
|
+
' 3. 循环执行直到收到结束信号',
|
|
113
|
+
].join('\n'),
|
|
114
|
+
required: true,
|
|
115
|
+
},
|
|
116
|
+
role: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: '可选。子 Agent 的角色描述,例如"你是一个好奇的物理学学生"',
|
|
119
|
+
required: false,
|
|
120
|
+
},
|
|
121
|
+
task_id: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: '可选。任务唯一标识,建议使用语义化名称如 "student-agent"。不填则自动生成',
|
|
124
|
+
required: false,
|
|
125
|
+
},
|
|
126
|
+
allowed_tools: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: '可选。逗号分隔的工具名列表,限制子 Agent 可用工具。为空则继承全部工具',
|
|
129
|
+
required: false,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
execute: async (args) => {
|
|
133
|
+
const instruction = (args.instruction || '').trim();
|
|
134
|
+
if (!instruction)
|
|
135
|
+
throw new Error('缺少必填参数: instruction');
|
|
136
|
+
const role = (args.role || '').trim() || undefined;
|
|
137
|
+
const taskId = (args.task_id || '').trim() || uuid();
|
|
138
|
+
const allowedToolsRaw = (args.allowed_tools || '').trim();
|
|
139
|
+
const allowedTools = allowedToolsRaw
|
|
140
|
+
? allowedToolsRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
141
|
+
: undefined;
|
|
142
|
+
const agentLabel = resolveAgentLabel(taskId, role);
|
|
143
|
+
const inboxChannel = `agent-inbox:${taskId}`;
|
|
144
|
+
const replyChannel = `agent-reply:${taskId}`;
|
|
145
|
+
// 获取当前 inbox offset(通过 bus 代理,确保跨线程正确)
|
|
146
|
+
const bus = await getBus();
|
|
147
|
+
const currentOffset = await bus.getOffset(inboxChannel);
|
|
148
|
+
const fullInstruction = [
|
|
149
|
+
role ? `[角色设定] ${role}` : '',
|
|
150
|
+
'',
|
|
151
|
+
'[通信约定]',
|
|
152
|
+
`- 你的收件箱频道:${inboxChannel}`,
|
|
153
|
+
`- 订阅时必须传入 from_offset: "${currentOffset}",确保不错过已发布的消息`,
|
|
154
|
+
`- 你的回复频道:${replyChannel}(用 publish_message 发送回复,agent_id 填 "${taskId}")`,
|
|
155
|
+
'',
|
|
156
|
+
'[任务]',
|
|
157
|
+
instruction,
|
|
158
|
+
].filter((l, i) => i !== 0 || l).join('\n').trim();
|
|
159
|
+
// 若在主线程(直接调用场景),直接启动
|
|
160
|
+
if (isMainThread) {
|
|
161
|
+
return spawnSubAgentInMainThread(taskId, fullInstruction, agentLabel, allowedTools);
|
|
162
|
+
}
|
|
163
|
+
// 在 Worker 线程中:通过 IPC 委托主线程创建 SubAgentBridge
|
|
164
|
+
// SubAgentBridge 必须在主线程运行,才能访问共享的 agentMessageBus 单例
|
|
165
|
+
if (!parentPort)
|
|
166
|
+
throw new Error('spawn_agent: 无法访问 parentPort');
|
|
167
|
+
const requestId = `spawn-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
168
|
+
return new Promise((resolve) => {
|
|
169
|
+
pendingSpawnRequests.set(requestId, resolve);
|
|
170
|
+
parentPort.postMessage({
|
|
171
|
+
type: 'spawn_subagent',
|
|
172
|
+
requestId,
|
|
173
|
+
taskId,
|
|
174
|
+
instruction: fullInstruction,
|
|
175
|
+
agentLabel,
|
|
176
|
+
allowedTools,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getBus } from '../core/busAccess.js';
|
|
2
|
+
export const subscribeMessage = {
|
|
3
|
+
name: 'subscribe_message',
|
|
4
|
+
description: [
|
|
5
|
+
'订阅指定频道,等待其他 Agent 发布消息后返回内容。',
|
|
6
|
+
'适用场景:',
|
|
7
|
+
' - SubAgent B 等待 SubAgent A 完成并发布结果后再继续执行',
|
|
8
|
+
' - 实现 Agent 间的同步协调(生产者-消费者模式)',
|
|
9
|
+
' - 读取频道最新历史消息(设置 read_latest=true 时不阻塞)',
|
|
10
|
+
'注意:默认超时 30 秒,超时后返回 null,Agent 应处理超时情况。',
|
|
11
|
+
].join('\n'),
|
|
12
|
+
parameters: {
|
|
13
|
+
channel: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: '要订阅的频道名称',
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
read_latest: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: '可选。设为 "true" 时,若频道已有历史消息则立即返回最新一条,不阻塞等待。默认 false',
|
|
21
|
+
required: false,
|
|
22
|
+
},
|
|
23
|
+
from_offset: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: '可选。从指定位置(0-based)开始消费。若该位置已有消息则立即返回,否则阻塞等待。用于避免错过在订阅前已发布的消息',
|
|
26
|
+
required: false,
|
|
27
|
+
},
|
|
28
|
+
timeout_seconds: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: '可选。等待超时秒数,默认 30 秒。超时返回空结果',
|
|
31
|
+
required: false,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
execute: async (args) => {
|
|
35
|
+
const channel = (args.channel || '').trim();
|
|
36
|
+
if (!channel)
|
|
37
|
+
throw new Error('缺少必填参数: channel');
|
|
38
|
+
const readLatest = (args.read_latest || '').toLowerCase() === 'true';
|
|
39
|
+
const timeoutSec = parseInt(args.timeout_seconds || '30', 10);
|
|
40
|
+
const timeoutMs = Math.max(1000, Math.min(timeoutSec * 1000, 300_000));
|
|
41
|
+
const fromOffsetRaw = (args.from_offset || '').trim();
|
|
42
|
+
const fromOffset = fromOffsetRaw !== '' ? parseInt(fromOffsetRaw, 10) : undefined;
|
|
43
|
+
const bus = await getBus();
|
|
44
|
+
if (readLatest) {
|
|
45
|
+
const history = await bus.getHistory(channel, 1);
|
|
46
|
+
if (history.length === 0)
|
|
47
|
+
return `频道 "${channel}" 暂无历史消息`;
|
|
48
|
+
return formatMessage(history[0]);
|
|
49
|
+
}
|
|
50
|
+
const msg = await bus.subscribe(channel, timeoutMs, fromOffset);
|
|
51
|
+
if (!msg)
|
|
52
|
+
return `订阅频道 "${channel}" 超时(${timeoutSec}s),未收到消息`;
|
|
53
|
+
return formatMessage(msg);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
function formatMessage(msg) {
|
|
57
|
+
const time = new Date(msg.timestamp).toISOString();
|
|
58
|
+
return `[频道: ${msg.channel}] [来自: ${msg.from}] [时间: ${time}]\n${msg.payload}`;
|
|
59
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface Message {
|
|
|
24
24
|
abortHint?: string;
|
|
25
25
|
/** 并行执行组 ID,同组工具同时运行 */
|
|
26
26
|
parallelGroupId?: string;
|
|
27
|
+
/** 来源 SubAgent 标识,格式如 "ResearchAgent",用于 UI 前缀展示 */
|
|
28
|
+
subAgentId?: string;
|
|
27
29
|
}
|
|
28
30
|
export type ContentBlock = {
|
|
29
31
|
type: 'text';
|
|
@@ -45,11 +47,18 @@ export interface ToolParameter {
|
|
|
45
47
|
description: string;
|
|
46
48
|
required?: boolean;
|
|
47
49
|
}
|
|
50
|
+
/** 工具执行时可选的额外回调,用于特殊工具(如 dispatch_subagent)向主线程推送实时事件 */
|
|
51
|
+
export interface ToolCallbacks {
|
|
52
|
+
/** SubAgent 产生新消息时推送到主线程 UI */
|
|
53
|
+
onSubAgentMessage?: (msg: Message) => void;
|
|
54
|
+
/** SubAgent 更新已有消息 */
|
|
55
|
+
onSubAgentUpdateMessage?: (id: string, updates: Partial<Message>) => void;
|
|
56
|
+
}
|
|
48
57
|
export interface Tool {
|
|
49
58
|
name: string;
|
|
50
59
|
description: string;
|
|
51
60
|
parameters: Record<string, ToolParameter>;
|
|
52
|
-
execute: (args: Record<string, unknown>, abortSignal?: AbortSignal) => Promise<string>;
|
|
61
|
+
execute: (args: Record<string, unknown>, abortSignal?: AbortSignal, toolCallbacks?: ToolCallbacks) => Promise<string>;
|
|
53
62
|
}
|
|
54
63
|
export interface Session {
|
|
55
64
|
id: string;
|
|
@@ -93,3 +102,42 @@ export interface TranscriptMessage {
|
|
|
93
102
|
export interface LLMService {
|
|
94
103
|
streamMessage: (transcript: TranscriptMessage[], tools: Tool[], callbacks: StreamCallbacks, abortSignal?: AbortSignal) => Promise<void>;
|
|
95
104
|
}
|
|
105
|
+
/** SubAgent 状态 */
|
|
106
|
+
export type SubAgentStatus = 'idle' | 'running' | 'done' | 'error' | 'aborted';
|
|
107
|
+
/** SubAgent 任务描述 */
|
|
108
|
+
export interface SubAgentTask {
|
|
109
|
+
/** 任务唯一 ID */
|
|
110
|
+
taskId: string;
|
|
111
|
+
/** 任务描述(发给 SubAgent 的指令) */
|
|
112
|
+
instruction: string;
|
|
113
|
+
/** 可选:限制 SubAgent 可用的工具名列表(为空则继承全部工具) */
|
|
114
|
+
allowedTools?: string[];
|
|
115
|
+
/** 可选:SubAgent 角色描述,注入 system prompt */
|
|
116
|
+
role?: string;
|
|
117
|
+
/** 可选:初始上下文 transcript */
|
|
118
|
+
contextTranscript?: TranscriptMessage[];
|
|
119
|
+
/** 可选:直接指定 SubAgent 的 system prompt,跳过主 Agent 的 agent 文件加载 */
|
|
120
|
+
systemPrompt?: string;
|
|
121
|
+
}
|
|
122
|
+
/** SubAgent 执行结果 */
|
|
123
|
+
export interface SubAgentResult {
|
|
124
|
+
taskId: string;
|
|
125
|
+
status: 'done' | 'error' | 'aborted';
|
|
126
|
+
/** 最终输出文本 */
|
|
127
|
+
output: string;
|
|
128
|
+
/** 执行过程中产生的消息列表(用于主 Agent 展示) */
|
|
129
|
+
messages: Message[];
|
|
130
|
+
/** 更新后的 transcript */
|
|
131
|
+
transcript: TranscriptMessage[];
|
|
132
|
+
/** 错误信息(status=error 时) */
|
|
133
|
+
error?: string;
|
|
134
|
+
}
|
|
135
|
+
/** AgentManager 向外暴露的任务派发回调 */
|
|
136
|
+
export interface AgentManagerCallbacks {
|
|
137
|
+
/** SubAgent 产生新消息时(用于 UI 展示) */
|
|
138
|
+
onSubAgentMessage: (taskId: string, msg: Message) => void;
|
|
139
|
+
/** SubAgent 状态变更 */
|
|
140
|
+
onSubAgentStatusChange: (taskId: string, status: SubAgentStatus) => void;
|
|
141
|
+
/** 所有 SubAgent 完成后汇总回调 */
|
|
142
|
+
onAllDone: (results: SubAgentResult[]) => void;
|
|
143
|
+
}
|