@code4bug/jarvis-agent 1.0.3 → 1.1.4
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 +63 -0
- package/dist/agents/jarvis.md +11 -0
- package/dist/cli.js +2 -2
- package/dist/commands/index.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/components/MessageItem.d.ts +1 -1
- package/dist/components/MessageItem.js +10 -6
- package/dist/components/SlashCommandMenu.d.ts +1 -1
- package/dist/components/StatusBar.d.ts +2 -1
- package/dist/components/StatusBar.js +6 -5
- package/dist/components/StreamingText.js +1 -1
- package/dist/components/WelcomeHeader.js +1 -1
- package/dist/config/constants.js +3 -3
- package/dist/core/AgentMessageBus.d.ts +63 -0
- package/dist/core/AgentMessageBus.js +107 -0
- package/dist/core/AgentRegistry.d.ts +22 -0
- package/dist/core/AgentRegistry.js +16 -0
- package/dist/core/QueryEngine.d.ts +7 -1
- package/dist/core/QueryEngine.js +18 -7
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +191 -0
- package/dist/core/WorkerBridge.d.ts +2 -2
- package/dist/core/WorkerBridge.js +68 -0
- package/dist/core/busAccess.d.ts +9 -0
- package/dist/core/busAccess.js +32 -0
- package/dist/core/hint.js +4 -4
- package/dist/core/query.d.ts +4 -0
- package/dist/core/query.js +91 -5
- package/dist/core/queryWorker.d.ts +62 -0
- package/dist/core/queryWorker.js +35 -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 +107 -0
- package/dist/core/workerBusProxy.d.ts +10 -0
- package/dist/core/workerBusProxy.js +57 -0
- package/dist/hooks/useSlashMenu.d.ts +7 -5
- package/dist/hooks/useSlashMenu.js +9 -5
- package/dist/hooks/useStreamThrottle.d.ts +1 -1
- package/dist/hooks/useTokenDisplay.d.ts +1 -0
- package/dist/hooks/useTokenDisplay.js +5 -0
- package/dist/index.js +1 -1
- package/dist/screens/repl.js +52 -34
- package/dist/screens/slashCommands.d.ts +1 -1
- package/dist/screens/slashCommands.js +7 -6
- package/dist/services/api/llm.d.ts +4 -2
- package/dist/services/api/llm.js +20 -6
- package/dist/services/api/mock.d.ts +1 -1
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +2 -2
- package/dist/tools/createSkill.d.ts +1 -1
- package/dist/tools/createSkill.js +3 -3
- package/dist/tools/index.d.ts +15 -9
- package/dist/tools/index.js +21 -10
- package/dist/tools/listDirectory.d.ts +1 -1
- 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/readFile.d.ts +1 -1
- package/dist/tools/runAgent.d.ts +11 -0
- package/dist/tools/runAgent.js +111 -0
- package/dist/tools/runCommand.d.ts +1 -1
- package/dist/tools/runCommand.js +1 -1
- package/dist/tools/searchFiles.d.ts +1 -1
- package/dist/tools/semanticSearch.d.ts +1 -1
- package/dist/tools/semanticSearch.js +1 -1
- 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 +163 -0
- package/dist/tools/subscribeMessage.d.ts +8 -0
- package/dist/tools/subscribeMessage.js +59 -0
- package/dist/tools/writeFile.d.ts +1 -1
- package/dist/tools/writeFile.js +1 -1
- package/dist/types/index.d.ts +49 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index';
|
|
1
|
+
import { LLMService, StreamCallbacks, TranscriptMessage, Tool, AbortSignal as AppAbortSignal } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Mock LLM 服务 - 模拟智能体行为,支持工具调用
|
|
4
4
|
*/
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 如果 skill 目录下存在 skill.py,execute 时直接调用 Python 脚本获取真实结果;
|
|
10
10
|
* 否则回退为返回 skill 指令文本(由 LLM 解释执行)。
|
|
11
11
|
*/
|
|
12
|
-
import { Tool } from '../types/index';
|
|
13
|
-
import { SkillDefinition } from './loader';
|
|
12
|
+
import { Tool } from '../types/index.js';
|
|
13
|
+
import { SkillDefinition } from './loader.js';
|
|
14
14
|
/** 加载所有外部 skills(带缓存) */
|
|
15
15
|
export declare function loadExternalSkills(): SkillDefinition[];
|
|
16
16
|
/** 获取合并后的所有工具:内置 tools + 外部 skills */
|
package/dist/skills/index.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
import { exec } from 'child_process';
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import { scanExternalSkills, getExternalSkillsDir } from './loader';
|
|
16
|
-
import { allTools as builtinTools } from '../tools/index';
|
|
15
|
+
import { scanExternalSkills, getExternalSkillsDir } from './loader.js';
|
|
16
|
+
import { allTools as builtinTools } from '../tools/index.js';
|
|
17
17
|
// ===== 缓存 =====
|
|
18
18
|
let _skillCache = null;
|
|
19
19
|
let _mergedTools = null;
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import os from 'os';
|
|
10
|
-
import { getExternalSkillsDir } from '../skills/loader';
|
|
11
|
-
import { reloadSkills } from '../skills/index';
|
|
12
|
-
import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm';
|
|
10
|
+
import { getExternalSkillsDir } from '../skills/loader.js';
|
|
11
|
+
import { reloadSkills } from '../skills/index.js';
|
|
12
|
+
import { LLMServiceImpl, getDefaultConfig } from '../services/api/llm.js';
|
|
13
13
|
// SKILL_INSTRUCTIONS.md 查找路径:项目根目录 > 用户主目录
|
|
14
14
|
function loadSkillInstructions() {
|
|
15
15
|
const candidates = [
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { Tool } from '../types/index';
|
|
2
|
-
import { readFile } from './readFile';
|
|
3
|
-
import { writeFile } from './writeFile';
|
|
4
|
-
import { runCommand } from './runCommand';
|
|
5
|
-
import { listDirectory } from './listDirectory';
|
|
6
|
-
import { searchFiles } from './searchFiles';
|
|
7
|
-
import { semanticSearch } from './semanticSearch';
|
|
8
|
-
import { createSkill } from './createSkill';
|
|
9
|
-
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
|
+
import { readFile } from './readFile.js';
|
|
3
|
+
import { writeFile } from './writeFile.js';
|
|
4
|
+
import { runCommand } from './runCommand.js';
|
|
5
|
+
import { listDirectory } from './listDirectory.js';
|
|
6
|
+
import { searchFiles } from './searchFiles.js';
|
|
7
|
+
import { semanticSearch } from './semanticSearch.js';
|
|
8
|
+
import { createSkill } from './createSkill.js';
|
|
9
|
+
import { runAgent } from './runAgent.js';
|
|
10
|
+
import { spawnAgent } from './spawnAgent.js';
|
|
11
|
+
import { sendToAgent } from './sendToAgent.js';
|
|
12
|
+
import { publishMessage } from './publishMessage.js';
|
|
13
|
+
import { subscribeMessage } from './subscribeMessage.js';
|
|
14
|
+
import { readChannel } from './readChannel.js';
|
|
15
|
+
export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
|
|
10
16
|
/** 所有内置工具 */
|
|
11
17
|
export declare const allTools: Tool[];
|
|
12
18
|
/** 按名称查找内置工具 */
|
package/dist/tools/index.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import { readFile } from './readFile';
|
|
2
|
-
import { writeFile } from './writeFile';
|
|
3
|
-
import { runCommand } from './runCommand';
|
|
4
|
-
import { listDirectory } from './listDirectory';
|
|
5
|
-
import { searchFiles } from './searchFiles';
|
|
6
|
-
import { semanticSearch } from './semanticSearch';
|
|
7
|
-
import { createSkill } from './createSkill';
|
|
8
|
-
|
|
1
|
+
import { readFile } from './readFile.js';
|
|
2
|
+
import { writeFile } from './writeFile.js';
|
|
3
|
+
import { runCommand } from './runCommand.js';
|
|
4
|
+
import { listDirectory } from './listDirectory.js';
|
|
5
|
+
import { searchFiles } from './searchFiles.js';
|
|
6
|
+
import { semanticSearch } from './semanticSearch.js';
|
|
7
|
+
import { createSkill } from './createSkill.js';
|
|
8
|
+
import { runAgent } from './runAgent.js';
|
|
9
|
+
import { spawnAgent } from './spawnAgent.js';
|
|
10
|
+
import { sendToAgent } from './sendToAgent.js';
|
|
11
|
+
import { publishMessage } from './publishMessage.js';
|
|
12
|
+
import { subscribeMessage } from './subscribeMessage.js';
|
|
13
|
+
import { readChannel } from './readChannel.js';
|
|
14
|
+
export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
|
|
9
15
|
/** 所有内置工具 */
|
|
10
|
-
export const allTools = [
|
|
16
|
+
export const allTools = [
|
|
17
|
+
readFile, writeFile, runCommand, listDirectory, searchFiles,
|
|
18
|
+
semanticSearch, createSkill,
|
|
19
|
+
runAgent, spawnAgent, sendToAgent,
|
|
20
|
+
publishMessage, subscribeMessage, readChannel,
|
|
21
|
+
];
|
|
11
22
|
/** 按名称查找内置工具 */
|
|
12
23
|
export function findTool(name) {
|
|
13
24
|
return allTools.find((t) => t.name === name);
|
|
14
25
|
}
|
|
15
26
|
// ===== 合并工具(内置 + 外部 Skills)=====
|
|
16
|
-
import { getMergedTools, findMergedTool } from '../skills/index';
|
|
27
|
+
import { getMergedTools, findMergedTool } from '../skills/index.js';
|
|
17
28
|
/** 获取所有工具(内置 + 外部 skills),供 QueryEngine 使用 */
|
|
18
29
|
export function getAllTools() {
|
|
19
30
|
return getMergedTools();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Tool } from '../types/index';
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
2
|
export declare const listDirectory: Tool;
|
|
@@ -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
|
+
};
|
package/dist/tools/readFile.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Tool } from '../types/index';
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
2
|
export declare const readFile: Tool;
|
|
@@ -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
|
+
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Tool } from '../types/index';
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
2
|
export declare const runCommand: Tool;
|
package/dist/tools/runCommand.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Tool } from '../types/index';
|
|
1
|
+
import { Tool } from '../types/index.js';
|
|
2
2
|
export declare const searchFiles: Tool;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { loadConfig, getActiveModel } from '../config/loader';
|
|
10
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
11
11
|
/** 通过 LLM 扩展关键词,返回扩展后的词列表(含原始关键词) */
|
|
12
12
|
async function expandKeywords(keyword) {
|
|
13
13
|
const config = loadConfig();
|
|
@@ -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,163 @@
|
|
|
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
|
+
/** 运行中的 task_id 集合(主线程侧,防止重复启动) */
|
|
19
|
+
const runningAgents = new Set();
|
|
20
|
+
function resolveAgentLabel(taskId, role) {
|
|
21
|
+
if (role) {
|
|
22
|
+
const match = role.match(/[\u4e00-\u9fa5a-zA-Z0-9]+/g);
|
|
23
|
+
if (match && match.length > 0) {
|
|
24
|
+
return `${match.slice(0, 2).join('').slice(0, 8)}Agent`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return `SubAgent-${taskId.slice(0, 8)}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 在主线程侧直接启动 SubAgent(供 WorkerBridge 调用)
|
|
31
|
+
*/
|
|
32
|
+
export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allowedTools) {
|
|
33
|
+
if (runningAgents.has(taskId)) {
|
|
34
|
+
return `子 Agent [${taskId}] 已在运行中,请使用 send_to_agent 向其发送消息,或使用不同的 task_id 启动新实例。`;
|
|
35
|
+
}
|
|
36
|
+
const replyChannel = `agent-reply:${taskId}`;
|
|
37
|
+
const bridge = new SubAgentBridge();
|
|
38
|
+
runningAgents.add(taskId);
|
|
39
|
+
incrementActiveAgents();
|
|
40
|
+
bridge.run({ taskId, instruction, allowedTools }, {
|
|
41
|
+
onMessage: (_tid, msg) => {
|
|
42
|
+
const tagged = { ...msg, subAgentId: agentLabel };
|
|
43
|
+
agentUIBus.push(tagged);
|
|
44
|
+
},
|
|
45
|
+
onUpdateMessage: (_tid, id, updates) => {
|
|
46
|
+
agentUIBus.update(id, updates);
|
|
47
|
+
},
|
|
48
|
+
onStreamText: () => { },
|
|
49
|
+
onLoopStateChange: () => { },
|
|
50
|
+
onConfirmDangerousCommand: async () => 'cancel',
|
|
51
|
+
}).then((result) => {
|
|
52
|
+
runningAgents.delete(taskId);
|
|
53
|
+
decrementActiveAgents();
|
|
54
|
+
agentMessageBus.publish(taskId, replyChannel, `[AGENT_DONE] ${result.output || '子 Agent 已完成'}`);
|
|
55
|
+
}).catch((err) => {
|
|
56
|
+
runningAgents.delete(taskId);
|
|
57
|
+
decrementActiveAgents();
|
|
58
|
+
agentMessageBus.publish(taskId, replyChannel, `[AGENT_ERROR] ${err.message}`);
|
|
59
|
+
});
|
|
60
|
+
const inboxChannel = `agent-inbox:${taskId}`;
|
|
61
|
+
return [
|
|
62
|
+
`子 Agent 已在后台启动`,
|
|
63
|
+
`task_id: ${taskId}`,
|
|
64
|
+
`角色标签: ${agentLabel}`,
|
|
65
|
+
`收件箱频道: ${inboxChannel}`,
|
|
66
|
+
`回复频道: ${replyChannel}`,
|
|
67
|
+
``,
|
|
68
|
+
`使用 send_to_agent 向其发送消息,使用 subscribe_message 订阅 "${replyChannel}" 等待回复。`,
|
|
69
|
+
].join('\n');
|
|
70
|
+
}
|
|
71
|
+
export const spawnAgent = {
|
|
72
|
+
name: 'spawn_agent',
|
|
73
|
+
description: [
|
|
74
|
+
'异步启动一个 SubAgent,立即返回 task_id,子 Agent 在后台独立运行。',
|
|
75
|
+
'与 run_agent 的区别:',
|
|
76
|
+
' - run_agent:阻塞等待子 Agent 完成后返回结果(适合一次性子任务)',
|
|
77
|
+
' - spawn_agent:立即返回,主 Agent 可继续执行,通过消息总线与子 Agent 双向通信(适合多轮对话)',
|
|
78
|
+
'',
|
|
79
|
+
'子 Agent 通信约定:',
|
|
80
|
+
' - 主 Agent → 子 Agent:使用 send_to_agent 工具,或向 "agent-inbox:{task_id}" 频道 publish',
|
|
81
|
+
' - 子 Agent → 主 Agent:子 Agent 向 "agent-reply:{task_id}" 频道 publish,主 Agent 用 subscribe_message 接收',
|
|
82
|
+
' - 子 Agent 的 instruction 中应包含订阅自己 inbox 的指令,例如:',
|
|
83
|
+
' "每次回复前先调用 subscribe_message 订阅频道 agent-inbox:{task_id} 获取新问题"',
|
|
84
|
+
'',
|
|
85
|
+
'查看后台 Agent 状态:使用 read_channel 工具查看 "agent-reply:{task_id}" 频道历史。',
|
|
86
|
+
].join('\n'),
|
|
87
|
+
parameters: {
|
|
88
|
+
instruction: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: [
|
|
91
|
+
'发给子 Agent 的初始指令。',
|
|
92
|
+
'若需要多轮交互,指令中应告知子 Agent:',
|
|
93
|
+
' 1. 订阅频道 "agent-inbox:{task_id}" 等待主 Agent 的消息',
|
|
94
|
+
' 2. 处理后将回复发布到 "agent-reply:{task_id}" 频道',
|
|
95
|
+
' 3. 循环执行直到收到结束信号',
|
|
96
|
+
].join('\n'),
|
|
97
|
+
required: true,
|
|
98
|
+
},
|
|
99
|
+
role: {
|
|
100
|
+
type: 'string',
|
|
101
|
+
description: '可选。子 Agent 的角色描述,例如"你是一个好奇的物理学学生"',
|
|
102
|
+
required: false,
|
|
103
|
+
},
|
|
104
|
+
task_id: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: '可选。任务唯一标识,建议使用语义化名称如 "student-agent"。不填则自动生成',
|
|
107
|
+
required: false,
|
|
108
|
+
},
|
|
109
|
+
allowed_tools: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
description: '可选。逗号分隔的工具名列表,限制子 Agent 可用工具。为空则继承全部工具',
|
|
112
|
+
required: false,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
execute: async (args) => {
|
|
116
|
+
const instruction = (args.instruction || '').trim();
|
|
117
|
+
if (!instruction)
|
|
118
|
+
throw new Error('缺少必填参数: instruction');
|
|
119
|
+
const role = (args.role || '').trim() || undefined;
|
|
120
|
+
const taskId = (args.task_id || '').trim() || uuid();
|
|
121
|
+
const allowedToolsRaw = (args.allowed_tools || '').trim();
|
|
122
|
+
const allowedTools = allowedToolsRaw
|
|
123
|
+
? allowedToolsRaw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
124
|
+
: undefined;
|
|
125
|
+
const agentLabel = resolveAgentLabel(taskId, role);
|
|
126
|
+
const inboxChannel = `agent-inbox:${taskId}`;
|
|
127
|
+
const replyChannel = `agent-reply:${taskId}`;
|
|
128
|
+
// 获取当前 inbox offset(通过 bus 代理,确保跨线程正确)
|
|
129
|
+
const bus = await getBus();
|
|
130
|
+
const currentOffset = await bus.getOffset(inboxChannel);
|
|
131
|
+
const fullInstruction = [
|
|
132
|
+
role ? `[角色设定] ${role}` : '',
|
|
133
|
+
'',
|
|
134
|
+
'[通信约定]',
|
|
135
|
+
`- 你的收件箱频道:${inboxChannel}`,
|
|
136
|
+
`- 订阅时必须传入 from_offset: "${currentOffset}",确保不错过已发布的消息`,
|
|
137
|
+
`- 你的回复频道:${replyChannel}(用 publish_message 发送回复,agent_id 填 "${taskId}")`,
|
|
138
|
+
'',
|
|
139
|
+
'[任务]',
|
|
140
|
+
instruction,
|
|
141
|
+
].filter((l, i) => i !== 0 || l).join('\n').trim();
|
|
142
|
+
// 若在主线程(直接调用场景),直接启动
|
|
143
|
+
if (isMainThread) {
|
|
144
|
+
return spawnSubAgentInMainThread(taskId, fullInstruction, agentLabel, allowedTools);
|
|
145
|
+
}
|
|
146
|
+
// 在 Worker 线程中:通过 IPC 委托主线程创建 SubAgentBridge
|
|
147
|
+
// SubAgentBridge 必须在主线程运行,才能访问共享的 agentMessageBus 单例
|
|
148
|
+
if (!parentPort)
|
|
149
|
+
throw new Error('spawn_agent: 无法访问 parentPort');
|
|
150
|
+
const requestId = `spawn-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
pendingSpawnRequests.set(requestId, resolve);
|
|
153
|
+
parentPort.postMessage({
|
|
154
|
+
type: 'spawn_subagent',
|
|
155
|
+
requestId,
|
|
156
|
+
taskId,
|
|
157
|
+
instruction: fullInstruction,
|
|
158
|
+
agentLabel,
|
|
159
|
+
allowedTools,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
};
|