@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.
Files changed (50) hide show
  1. package/README.md +63 -0
  2. package/dist/agents/jarvis.md +11 -0
  3. package/dist/components/MessageItem.js +9 -5
  4. package/dist/components/StatusBar.d.ts +2 -1
  5. package/dist/components/StatusBar.js +5 -4
  6. package/dist/core/AgentMessageBus.d.ts +63 -0
  7. package/dist/core/AgentMessageBus.js +107 -0
  8. package/dist/core/AgentRegistry.d.ts +22 -0
  9. package/dist/core/AgentRegistry.js +16 -0
  10. package/dist/core/QueryEngine.d.ts +6 -0
  11. package/dist/core/QueryEngine.js +11 -0
  12. package/dist/core/SubAgentBridge.d.ts +20 -0
  13. package/dist/core/SubAgentBridge.js +191 -0
  14. package/dist/core/WorkerBridge.js +68 -0
  15. package/dist/core/busAccess.d.ts +9 -0
  16. package/dist/core/busAccess.js +32 -0
  17. package/dist/core/query.d.ts +4 -0
  18. package/dist/core/query.js +89 -3
  19. package/dist/core/queryWorker.d.ts +62 -0
  20. package/dist/core/queryWorker.js +35 -0
  21. package/dist/core/spawnRegistry.d.ts +16 -0
  22. package/dist/core/spawnRegistry.js +32 -0
  23. package/dist/core/subAgentWorker.d.ts +89 -0
  24. package/dist/core/subAgentWorker.js +107 -0
  25. package/dist/core/workerBusProxy.d.ts +10 -0
  26. package/dist/core/workerBusProxy.js +57 -0
  27. package/dist/hooks/useSlashMenu.d.ts +2 -0
  28. package/dist/hooks/useSlashMenu.js +5 -1
  29. package/dist/hooks/useTokenDisplay.d.ts +1 -0
  30. package/dist/hooks/useTokenDisplay.js +5 -0
  31. package/dist/screens/repl.js +34 -16
  32. package/dist/screens/slashCommands.js +2 -1
  33. package/dist/services/api/llm.d.ts +2 -0
  34. package/dist/services/api/llm.js +15 -1
  35. package/dist/tools/index.d.ts +7 -1
  36. package/dist/tools/index.js +13 -2
  37. package/dist/tools/publishMessage.d.ts +8 -0
  38. package/dist/tools/publishMessage.js +41 -0
  39. package/dist/tools/readChannel.d.ts +8 -0
  40. package/dist/tools/readChannel.js +44 -0
  41. package/dist/tools/runAgent.d.ts +11 -0
  42. package/dist/tools/runAgent.js +111 -0
  43. package/dist/tools/sendToAgent.d.ts +11 -0
  44. package/dist/tools/sendToAgent.js +35 -0
  45. package/dist/tools/spawnAgent.d.ts +6 -0
  46. package/dist/tools/spawnAgent.js +163 -0
  47. package/dist/tools/subscribeMessage.d.ts +8 -0
  48. package/dist/tools/subscribeMessage.js +59 -0
  49. package/dist/types/index.d.ts +49 -1
  50. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ /**
2
+ * workerBusProxy — Worker 线程侧 MessageBus IPC 代理
3
+ *
4
+ * 在 SubAgent Worker 线程中,通过 parentPort 消息将 bus 操作
5
+ * 转发给主线程(SubAgentBridge)代理执行,绕过线程内存隔离。
6
+ *
7
+ * 此文件独立于 subAgentWorker.ts,避免循环依赖。
8
+ * subAgentWorker.ts 负责注册 pendingBusRequests 的回复分发。
9
+ */
10
+ import { parentPort } from 'worker_threads';
11
+ /** 等待中的 bus 请求,key = requestId */
12
+ export const pendingBusRequests = new Map();
13
+ function requestId() {
14
+ return `bus-${Date.now()}-${Math.random().toString(36).slice(2)}`;
15
+ }
16
+ function send(msg) {
17
+ if (!parentPort)
18
+ throw new Error('workerBusProxy: 不在 Worker 线程中');
19
+ parentPort.postMessage(msg);
20
+ }
21
+ export const workerBusProxy = {
22
+ publish(from, channel, payload) {
23
+ return new Promise((resolve) => {
24
+ const reqId = requestId();
25
+ pendingBusRequests.set(reqId, () => resolve());
26
+ send({ type: 'bus_publish', requestId: reqId, from, channel, payload });
27
+ });
28
+ },
29
+ subscribe(channel, timeoutMs = 30_000, fromOffset) {
30
+ return new Promise((resolve) => {
31
+ const reqId = requestId();
32
+ pendingBusRequests.set(reqId, (msg) => resolve(msg));
33
+ send({ type: 'bus_subscribe', requestId: reqId, channel, timeoutMs, fromOffset });
34
+ });
35
+ },
36
+ getHistory(channel, limit) {
37
+ return new Promise((resolve) => {
38
+ const reqId = requestId();
39
+ pendingBusRequests.set(reqId, (msgs) => resolve(msgs));
40
+ send({ type: 'bus_read_history', requestId: reqId, channel, limit });
41
+ });
42
+ },
43
+ getOffset(channel) {
44
+ return new Promise((resolve) => {
45
+ const reqId = requestId();
46
+ pendingBusRequests.set(reqId, (offset) => resolve(offset));
47
+ send({ type: 'bus_get_offset', requestId: reqId, channel });
48
+ });
49
+ },
50
+ listChannels() {
51
+ return new Promise((resolve) => {
52
+ const reqId = requestId();
53
+ pendingBusRequests.set(reqId, (channels) => resolve(channels));
54
+ send({ type: 'bus_list_channels', requestId: reqId });
55
+ });
56
+ },
57
+ };
@@ -13,6 +13,8 @@ interface UseSlashMenuOptions {
13
13
  setInput: React.Dispatch<React.SetStateAction<string>>;
14
14
  /** resume 时重置所有流式状态(含 thinkingIdRef) */
15
15
  stopAll: () => void;
16
+ /** /new 命令:开启新会话 */
17
+ onNewSession: () => void;
16
18
  }
17
19
  /**
18
20
  * 斜杠命令菜单状态管理 hook
@@ -9,7 +9,7 @@ import { executeSlashCommand } from '../screens/slashCommands.js';
9
9
  * 管理菜单可见性、选中项、二级菜单(agent / resume)等。
10
10
  */
11
11
  export function useSlashMenu(opts) {
12
- const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, } = opts;
12
+ const { engineRef, sessionRef, tokenCountRef, setMessages, setDisplayTokens, setLoopState, setIsProcessing, setShowWelcome, setInput, stopAll, onNewSession, } = opts;
13
13
  const [slashMenuVisible, setSlashMenuVisible] = useState(false);
14
14
  const [slashMenuItems, setSlashMenuItems] = useState([]);
15
15
  const [slashMenuIndex, setSlashMenuIndex] = useState(0);
@@ -138,6 +138,10 @@ export function useSlashMenu(opts) {
138
138
  if (cmd.category === 'builtin') {
139
139
  setInput('');
140
140
  setSlashMenuVisible(false);
141
+ if (cmd.name === 'new') {
142
+ onNewSession();
143
+ return;
144
+ }
141
145
  const msg = executeSlashCommand(cmd.name);
142
146
  if (msg)
143
147
  setMessages((prev) => [...prev, msg]);
@@ -9,5 +9,6 @@ export declare function useTokenDisplay(): {
9
9
  startTokenTimer: () => void;
10
10
  stopTokenTimer: () => void;
11
11
  updateTokenCount: (count: number) => void;
12
+ syncTokenDisplay: (count: number) => void;
12
13
  resetTokens: () => void;
13
14
  };
@@ -25,6 +25,10 @@ export function useTokenDisplay() {
25
25
  const updateTokenCount = useCallback((count) => {
26
26
  tokenCountRef.current = count;
27
27
  }, []);
28
+ const syncTokenDisplay = useCallback((count) => {
29
+ tokenCountRef.current = count;
30
+ setDisplayTokens(count);
31
+ }, []);
28
32
  const resetTokens = useCallback(() => {
29
33
  tokenCountRef.current = 0;
30
34
  setDisplayTokens(0);
@@ -40,6 +44,7 @@ export function useTokenDisplay() {
40
44
  startTokenTimer,
41
45
  stopTokenTimer,
42
46
  updateTokenCount,
47
+ syncTokenDisplay,
43
48
  resetTokens,
44
49
  };
45
50
  }
@@ -20,6 +20,7 @@ import { executeSlashCommand } from './slashCommands.js';
20
20
  import { QueryEngine } from '../core/QueryEngine.js';
21
21
  import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
22
22
  import { generateAgentHint } from '../core/hint.js';
23
+ import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
23
24
  export default function REPL() {
24
25
  const { exit } = useApp();
25
26
  const width = useTerminalWidth();
@@ -33,6 +34,7 @@ export default function REPL() {
33
34
  const [showWelcome, setShowWelcome] = useState(true);
34
35
  const [showDetails, setShowDetails] = useState(false);
35
36
  const [placeholder, setPlaceholder] = useState('');
37
+ const [activeAgents, setActiveAgents] = useState(getActiveAgentCount());
36
38
  const lastEscRef = useRef(0);
37
39
  const sessionRef = useRef({
38
40
  id: '', messages: [], createdAt: 0, updatedAt: 0, totalTokens: 0, totalCost: 0,
@@ -40,24 +42,45 @@ export default function REPL() {
40
42
  const engineRef = useRef(null);
41
43
  // 节流 hooks
42
44
  const { streamText, streamBufferRef, startStreamTimer, stopStreamTimer, appendStreamChunk, clearStream, handleThinkingUpdate, finishThinking, thinkingIdRef, stopAll, } = useStreamThrottle(setMessages);
43
- const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, resetTokens, } = useTokenDisplay();
45
+ const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
46
+ // ===== 新会话逻辑 =====
47
+ const handleNewSession = useCallback(() => {
48
+ if (engineRef.current) {
49
+ engineRef.current.reset();
50
+ sessionRef.current = engineRef.current.getSession();
51
+ }
52
+ setMessages([]);
53
+ clearStream();
54
+ setLoopState(null);
55
+ setIsProcessing(false);
56
+ setShowWelcome(true);
57
+ resetTokens();
58
+ generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
59
+ }, [clearStream, resetTokens]);
44
60
  // 斜杠菜单
45
61
  const slashMenu = useSlashMenu({
46
62
  engineRef, sessionRef, tokenCountRef,
47
63
  setMessages,
48
- setDisplayTokens: (n) => updateTokenCount(n),
64
+ setDisplayTokens: (n) => syncTokenDisplay(n),
49
65
  setLoopState, setIsProcessing, setShowWelcome, setInput,
50
66
  stopAll,
67
+ onNewSession: handleNewSession,
51
68
  });
52
69
  // 危险命令确认
53
70
  const [dangerConfirm, setDangerConfirm] = useState(null);
54
71
  useEffect(() => {
55
72
  engineRef.current = new QueryEngine();
56
73
  sessionRef.current = engineRef.current.getSession();
74
+ // 注册持久 UI 回调,供 spawn_agent 后台子 Agent 跨轮次推送消息
75
+ engineRef.current.registerUIBus((msg) => setMessages((prev) => [...prev, msg]), (id, updates) => setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))));
57
76
  generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
58
77
  console.error('[hint] 初始化提示失败:', err);
59
78
  });
60
79
  }, []);
80
+ // 订阅后台 SubAgent 计数变化
81
+ useEffect(() => {
82
+ return subscribeAgentCount((count) => setActiveAgents(count));
83
+ }, []);
61
84
  // ===== Engine Callbacks =====
62
85
  const callbacks = {
63
86
  onMessage: (msg) => {
@@ -106,6 +129,12 @@ export default function REPL() {
106
129
  setDangerConfirm({ command, reason, ruleName, resolve });
107
130
  });
108
131
  },
132
+ onSubAgentMessage: (msg) => {
133
+ setMessages((prev) => [...prev, msg]);
134
+ },
135
+ onSubAgentUpdateMessage: (id, updates) => {
136
+ setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, ...updates } : msg)));
137
+ },
109
138
  };
110
139
  // ===== 提交处理 =====
111
140
  const handleSubmit = useCallback(async (value) => {
@@ -121,18 +150,7 @@ export default function REPL() {
121
150
  setInput('');
122
151
  slashMenu.setSlashMenuVisible(false);
123
152
  if (cmdName === 'new') {
124
- // 新会话:重置引擎 + 清空所有状态
125
- if (engineRef.current) {
126
- engineRef.current.reset();
127
- sessionRef.current = engineRef.current.getSession();
128
- }
129
- setMessages([]);
130
- clearStream();
131
- setLoopState(null);
132
- setIsProcessing(false);
133
- setShowWelcome(true);
134
- resetTokens();
135
- generateAgentHint().then((hint) => setPlaceholder(hint)).catch(() => { });
153
+ handleNewSession();
136
154
  }
137
155
  else if (cmdName === 'session_clear') {
138
156
  if (engineRef.current) {
@@ -228,7 +246,7 @@ export default function REPL() {
228
246
  setInput('');
229
247
  clearStream();
230
248
  await engineRef.current.handleQuery(trimmed, callbacks);
231
- }, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu]);
249
+ }, [isProcessing, pushHistory, clearStream, resetTokens, slashMenu, handleNewSession]);
232
250
  // ===== 输入处理 =====
233
251
  const handleUpArrow = useCallback(() => {
234
252
  const result = navigateUp(input);
@@ -366,5 +384,5 @@ export default function REPL() {
366
384
  return (_jsxs(Box, { flexDirection: "column", width: width, children: [showWelcome && _jsx(WelcomeHeader, { width: width }), _jsxs(Box, { flexDirection: "column", paddingX: 1, marginTop: showWelcome ? 0 : 1, children: [messages.map((msg) => (_jsx(MessageItem, { msg: msg, showDetails: showDetails }, msg.id))), streamText && _jsx(StreamingText, { text: streamText }), dangerConfirm && (_jsx(DangerConfirm, { command: dangerConfirm.command, reason: dangerConfirm.reason, ruleName: dangerConfirm.ruleName, onSelect: (choice) => {
367
385
  dangerConfirm.resolve(choice);
368
386
  setDangerConfirm(null);
369
- } })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens })] })] }));
387
+ } })), loopState?.isRunning && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { color: "gray", children: [" iteration ", loopState.iteration, "/", loopState.maxIterations] })] }))] }), _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), slashMenu.slashMenuVisible && !isProcessing && (_jsx(SlashCommandMenu, { commands: slashMenu.slashMenuItems, selectedIndex: slashMenu.slashMenuIndex })), _jsx(Box, { children: countdown !== null ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", dimColor: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: "Press " }), _jsx(Text, { color: "yellow", bold: true, children: "Ctrl+C" }), _jsx(Text, { color: "yellow", children: " again to exit " }), _jsxs(Text, { color: "gray", dimColor: true, children: ["(", countdown, "s)"] })] })) : isProcessing ? (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: "gray", italic: true, children: " processing..." })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: "\u276F " }), _jsx(MultilineInput, { value: input, onChange: handleInputChange, onSubmit: handleSubmit, onUpArrow: handleUpArrow, onDownArrow: handleDownArrow, placeholder: placeholder, isActive: !isProcessing, showCursor: windowFocused && !isProcessing, slashMenuActive: slashMenu.slashMenuVisible, onSlashMenuUp: slashMenu.handleSlashMenuUp, onSlashMenuDown: slashMenu.handleSlashMenuDown, onSlashMenuSelect: slashMenu.handleSlashMenuSelect, onSlashMenuClose: slashMenu.handleSlashMenuClose, onTabFillPlaceholder: handleTabFillPlaceholder })] })) }), _jsx(Text, { color: "gray", children: '─'.repeat(Math.max(width - 2, 1)) }), _jsx(StatusBar, { width: width - 2, totalTokens: displayTokens, activeAgents: activeAgents })] })] }));
370
388
  }
@@ -89,7 +89,8 @@ export function executeSlashCommand(cmdName) {
89
89
  const parts = [];
90
90
  parts.push('### Built-in Tools\n');
91
91
  allTools.forEach((t, i) => {
92
- parts.push(`${i + 1}. \`${t.name}\` - ${t.description.slice(0, 60)}`);
92
+ const summary = t.description.split('\n')[0].slice(0, 80);
93
+ parts.push(`${i + 1}. \`${t.name}\` - ${summary}`);
93
94
  });
94
95
  parts.push('');
95
96
  parts.push(`### External Skills\n`);
@@ -16,6 +16,8 @@ export interface LLMConfig {
16
16
  temperature?: number;
17
17
  /** 额外请求体参数,直接合并到 API 请求 body */
18
18
  extraBody?: Record<string, unknown>;
19
+ /** 直接指定 system prompt,跳过 agent 文件加载(SubAgent 场景使用) */
20
+ systemPrompt?: string;
19
21
  }
20
22
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
21
23
  export declare function getDefaultConfig(): LLMConfig;
@@ -142,6 +142,11 @@ export class LLMServiceImpl {
142
142
  if (!this.config.apiKey) {
143
143
  throw new Error('API_KEY 未配置。请在 .jarvis/config.json 或环境变量中设置。');
144
144
  }
145
+ // 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
146
+ if (this.config.systemPrompt) {
147
+ this.systemPrompt = this.config.systemPrompt;
148
+ return;
149
+ }
145
150
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
146
151
  const currentAgent = getActiveAgent(DEFAULT_AGENT);
147
152
  const agent = getAgent(currentAgent);
@@ -157,7 +162,16 @@ export class LLMServiceImpl {
157
162
  '安全围栏会自动处理拦截和用户确认流程。';
158
163
  // 追加系统环境信息,帮助 LLM 感知用户运行环境
159
164
  const systemInfo = getSystemInfoPrompt();
160
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo;
165
+ // 追加当前激活模型信息
166
+ const jarvisCfg = loadConfig();
167
+ const activeModelCfg = getActiveModel(jarvisCfg);
168
+ const activeModelKey = jarvisCfg.system.model ?? 'unknown';
169
+ const modelInfo = '\n\n---\n[当前模型] 以下是本次会话使用的 LLM 模型信息:' +
170
+ `\n- 模型标识: ${activeModelKey}` +
171
+ `\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
172
+ `\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
173
+ `\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
174
+ this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo;
161
175
  }
162
176
  async streamMessage(transcript, tools, callbacks, abortSignal) {
163
177
  const messages = toOpenAIMessages(transcript, this.systemPrompt);
@@ -6,7 +6,13 @@ import { listDirectory } from './listDirectory.js';
6
6
  import { searchFiles } from './searchFiles.js';
7
7
  import { semanticSearch } from './semanticSearch.js';
8
8
  import { createSkill } from './createSkill.js';
9
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
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
  /** 按名称查找内置工具 */
@@ -5,9 +5,20 @@ import { listDirectory } from './listDirectory.js';
5
5
  import { searchFiles } from './searchFiles.js';
6
6
  import { semanticSearch } from './semanticSearch.js';
7
7
  import { createSkill } from './createSkill.js';
8
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill };
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 = [readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill];
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);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * publish_message 工具
3
+ *
4
+ * 允许 SubAgent(或主 Agent)向命名频道发布消息,
5
+ * 其他正在订阅该频道的 Agent 会立即收到通知。
6
+ */
7
+ import { Tool } from '../types/index.js';
8
+ export declare const publishMessage: 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,8 @@
1
+ /**
2
+ * read_channel 工具
3
+ *
4
+ * 读取频道历史消息(非阻塞),支持查看所有活跃频道列表。
5
+ * 主 Agent 可用此工具汇总所有 SubAgent 的通讯记录。
6
+ */
7
+ import { Tool } from '../types/index.js';
8
+ export declare const readChannel: Tool;
@@ -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
+ };
@@ -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;