@code4bug/jarvis-agent 1.0.4 → 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/components/MessageItem.js +9 -5
- package/dist/components/StatusBar.d.ts +2 -1
- package/dist/components/StatusBar.js +5 -4
- 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 +6 -0
- package/dist/core/QueryEngine.js +11 -0
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +191 -0
- 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/query.d.ts +4 -0
- package/dist/core/query.js +89 -3
- 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 +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/screens/repl.js +34 -16
- package/dist/screens/slashCommands.js +2 -1
- package/dist/services/api/llm.d.ts +2 -0
- package/dist/services/api/llm.js +15 -1
- 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/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/types/index.d.ts +49 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|
|
@@ -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
|
+
}
|