@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.
Files changed (77) hide show
  1. package/README.md +63 -0
  2. package/dist/agents/jarvis.md +11 -0
  3. package/dist/cli.js +2 -2
  4. package/dist/commands/index.js +2 -2
  5. package/dist/commands/init.js +1 -1
  6. package/dist/components/MessageItem.d.ts +1 -1
  7. package/dist/components/MessageItem.js +10 -6
  8. package/dist/components/SlashCommandMenu.d.ts +1 -1
  9. package/dist/components/StatusBar.d.ts +2 -1
  10. package/dist/components/StatusBar.js +6 -5
  11. package/dist/components/StreamingText.js +1 -1
  12. package/dist/components/WelcomeHeader.js +1 -1
  13. package/dist/config/constants.js +3 -3
  14. package/dist/core/AgentMessageBus.d.ts +63 -0
  15. package/dist/core/AgentMessageBus.js +107 -0
  16. package/dist/core/AgentRegistry.d.ts +22 -0
  17. package/dist/core/AgentRegistry.js +16 -0
  18. package/dist/core/QueryEngine.d.ts +7 -1
  19. package/dist/core/QueryEngine.js +18 -7
  20. package/dist/core/SubAgentBridge.d.ts +20 -0
  21. package/dist/core/SubAgentBridge.js +191 -0
  22. package/dist/core/WorkerBridge.d.ts +2 -2
  23. package/dist/core/WorkerBridge.js +68 -0
  24. package/dist/core/busAccess.d.ts +9 -0
  25. package/dist/core/busAccess.js +32 -0
  26. package/dist/core/hint.js +4 -4
  27. package/dist/core/query.d.ts +4 -0
  28. package/dist/core/query.js +91 -5
  29. package/dist/core/queryWorker.d.ts +62 -0
  30. package/dist/core/queryWorker.js +35 -0
  31. package/dist/core/spawnRegistry.d.ts +16 -0
  32. package/dist/core/spawnRegistry.js +32 -0
  33. package/dist/core/subAgentWorker.d.ts +89 -0
  34. package/dist/core/subAgentWorker.js +107 -0
  35. package/dist/core/workerBusProxy.d.ts +10 -0
  36. package/dist/core/workerBusProxy.js +57 -0
  37. package/dist/hooks/useSlashMenu.d.ts +7 -5
  38. package/dist/hooks/useSlashMenu.js +9 -5
  39. package/dist/hooks/useStreamThrottle.d.ts +1 -1
  40. package/dist/hooks/useTokenDisplay.d.ts +1 -0
  41. package/dist/hooks/useTokenDisplay.js +5 -0
  42. package/dist/index.js +1 -1
  43. package/dist/screens/repl.js +52 -34
  44. package/dist/screens/slashCommands.d.ts +1 -1
  45. package/dist/screens/slashCommands.js +7 -6
  46. package/dist/services/api/llm.d.ts +4 -2
  47. package/dist/services/api/llm.js +20 -6
  48. package/dist/services/api/mock.d.ts +1 -1
  49. package/dist/skills/index.d.ts +2 -2
  50. package/dist/skills/index.js +2 -2
  51. package/dist/tools/createSkill.d.ts +1 -1
  52. package/dist/tools/createSkill.js +3 -3
  53. package/dist/tools/index.d.ts +15 -9
  54. package/dist/tools/index.js +21 -10
  55. package/dist/tools/listDirectory.d.ts +1 -1
  56. package/dist/tools/publishMessage.d.ts +8 -0
  57. package/dist/tools/publishMessage.js +41 -0
  58. package/dist/tools/readChannel.d.ts +8 -0
  59. package/dist/tools/readChannel.js +44 -0
  60. package/dist/tools/readFile.d.ts +1 -1
  61. package/dist/tools/runAgent.d.ts +11 -0
  62. package/dist/tools/runAgent.js +111 -0
  63. package/dist/tools/runCommand.d.ts +1 -1
  64. package/dist/tools/runCommand.js +1 -1
  65. package/dist/tools/searchFiles.d.ts +1 -1
  66. package/dist/tools/semanticSearch.d.ts +1 -1
  67. package/dist/tools/semanticSearch.js +1 -1
  68. package/dist/tools/sendToAgent.d.ts +11 -0
  69. package/dist/tools/sendToAgent.js +35 -0
  70. package/dist/tools/spawnAgent.d.ts +6 -0
  71. package/dist/tools/spawnAgent.js +163 -0
  72. package/dist/tools/subscribeMessage.d.ts +8 -0
  73. package/dist/tools/subscribeMessage.js +59 -0
  74. package/dist/tools/writeFile.d.ts +1 -1
  75. package/dist/tools/writeFile.js +1 -1
  76. package/dist/types/index.d.ts +49 -1
  77. package/package.json +1 -1
@@ -0,0 +1,191 @@
1
+ /**
2
+ * SubAgentBridge — 单个 SubAgent 的 Worker 线程桥接
3
+ *
4
+ * 职责:
5
+ * - 创建并管理 SubAgent Worker 线程
6
+ * - 转发 Worker 事件到上层回调
7
+ * - 处理危险命令确认的双向通信
8
+ * - 代理 MessageBus publish/subscribe 请求(跨线程通讯)
9
+ * - 支持中断
10
+ */
11
+ import { Worker } from 'worker_threads';
12
+ import { fileURLToPath } from 'url';
13
+ import path from 'path';
14
+ import { agentMessageBus } from './AgentMessageBus.js';
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ /** 创建 SubAgent Worker(兼容 tsx 开发模式与编译后 .js) */
18
+ function createSubAgentWorker() {
19
+ const workerTsPath = path.join(__dirname, 'subAgentWorker.ts');
20
+ const isTsx = __filename.endsWith('.ts');
21
+ if (isTsx) {
22
+ const inlineScript = `
23
+ import { tsImport } from 'tsx/esm/api';
24
+ import { workerData } from 'worker_threads';
25
+ import { pathToFileURL } from 'url';
26
+ await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).href);
27
+ `;
28
+ return new Worker(inlineScript, {
29
+ eval: true,
30
+ workerData: { __workerFile: workerTsPath },
31
+ });
32
+ }
33
+ return new Worker(workerTsPath.replace(/\.ts$/, '.js'));
34
+ }
35
+ export class SubAgentBridge {
36
+ worker = null;
37
+ /**
38
+ * 在独立 Worker 线程中执行 SubAgent 任务
39
+ * @returns SubAgentResult(包含最终输出、消息列表、transcript)
40
+ */
41
+ run(task, callbacks) {
42
+ return new Promise((resolve, reject) => {
43
+ const worker = createSubAgentWorker();
44
+ this.worker = worker;
45
+ // 收集 SubAgent 产生的所有消息,用于汇总结果
46
+ const collectedMessages = [];
47
+ let finalTranscript = [];
48
+ worker.on('message', async (msg) => {
49
+ switch (msg.type) {
50
+ case 'message':
51
+ collectedMessages.push(msg.msg);
52
+ callbacks.onMessage(msg.taskId, msg.msg);
53
+ break;
54
+ case 'update_message':
55
+ callbacks.onUpdateMessage(msg.taskId, msg.id, msg.updates);
56
+ break;
57
+ case 'stream_text':
58
+ callbacks.onStreamText(msg.taskId, msg.text);
59
+ break;
60
+ case 'loop_state':
61
+ callbacks.onLoopStateChange(msg.taskId, msg.state);
62
+ break;
63
+ case 'danger_confirm_request': {
64
+ const choice = callbacks.onConfirmDangerousCommand
65
+ ? await callbacks.onConfirmDangerousCommand(msg.taskId, msg.command, msg.reason, msg.ruleName)
66
+ : 'cancel';
67
+ const reply = {
68
+ type: 'danger_confirm_result',
69
+ requestId: msg.requestId,
70
+ choice,
71
+ };
72
+ worker.postMessage(reply);
73
+ break;
74
+ }
75
+ // ===== MessageBus IPC 代理 =====
76
+ case 'bus_publish': {
77
+ agentMessageBus.publish(msg.from, msg.channel, msg.payload);
78
+ const ack = { type: 'bus_publish_ack', requestId: msg.requestId };
79
+ worker.postMessage(ack);
80
+ break;
81
+ }
82
+ case 'bus_subscribe': {
83
+ // 在主线程侧等待消息,结果回传 Worker
84
+ agentMessageBus.subscribe(msg.channel, msg.timeoutMs, msg.fromOffset).then((busMsg) => {
85
+ const reply = {
86
+ type: 'bus_subscribe_result',
87
+ requestId: msg.requestId,
88
+ message: busMsg,
89
+ };
90
+ worker.postMessage(reply);
91
+ });
92
+ break;
93
+ }
94
+ case 'bus_read_history': {
95
+ const history = agentMessageBus.getHistory(msg.channel, msg.limit);
96
+ const reply = {
97
+ type: 'bus_read_history_result',
98
+ requestId: msg.requestId,
99
+ messages: history,
100
+ };
101
+ worker.postMessage(reply);
102
+ break;
103
+ }
104
+ case 'bus_get_offset': {
105
+ const offset = agentMessageBus.getOffset(msg.channel);
106
+ const reply = {
107
+ type: 'bus_get_offset_result',
108
+ requestId: msg.requestId,
109
+ offset,
110
+ };
111
+ worker.postMessage(reply);
112
+ break;
113
+ }
114
+ case 'bus_list_channels': {
115
+ const channels = agentMessageBus.listChannels();
116
+ const reply = {
117
+ type: 'bus_list_channels_result',
118
+ requestId: msg.requestId,
119
+ channels,
120
+ };
121
+ worker.postMessage(reply);
122
+ break;
123
+ }
124
+ case 'done':
125
+ finalTranscript = msg.transcript;
126
+ this.worker = null;
127
+ worker.terminate();
128
+ // 从 transcript 中提取最终输出文本
129
+ resolve({
130
+ taskId: msg.taskId,
131
+ status: 'done',
132
+ output: extractFinalOutput(msg.transcript),
133
+ messages: collectedMessages,
134
+ transcript: finalTranscript,
135
+ });
136
+ break;
137
+ case 'error':
138
+ this.worker = null;
139
+ worker.terminate();
140
+ resolve({
141
+ taskId: msg.taskId,
142
+ status: 'error',
143
+ output: '',
144
+ messages: collectedMessages,
145
+ transcript: finalTranscript,
146
+ error: msg.message,
147
+ });
148
+ break;
149
+ }
150
+ });
151
+ worker.on('error', (err) => {
152
+ this.worker = null;
153
+ reject(err);
154
+ });
155
+ worker.on('exit', (code) => {
156
+ if (this.worker) {
157
+ // Worker 退出但未发送 done/error,说明异常终止
158
+ this.worker = null;
159
+ reject(new Error(`SubAgent Worker 意外退出,code=${code}`));
160
+ }
161
+ });
162
+ const runMsg = { type: 'run', task };
163
+ worker.postMessage(runMsg);
164
+ });
165
+ }
166
+ /** 中断 SubAgent 执行 */
167
+ abort() {
168
+ if (this.worker) {
169
+ const msg = { type: 'abort' };
170
+ this.worker.postMessage(msg);
171
+ }
172
+ }
173
+ }
174
+ /** 从 transcript 中提取最后一条 assistant 文本作为最终输出 */
175
+ function extractFinalOutput(transcript) {
176
+ for (let i = transcript.length - 1; i >= 0; i--) {
177
+ const msg = transcript[i];
178
+ if (msg.role === 'assistant') {
179
+ if (typeof msg.content === 'string')
180
+ return msg.content;
181
+ // ContentBlock[] — 取最后一个 text block
182
+ const blocks = msg.content;
183
+ for (let j = blocks.length - 1; j >= 0; j--) {
184
+ if (blocks[j].type === 'text' && blocks[j].text) {
185
+ return blocks[j].text;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return '';
191
+ }
@@ -1,5 +1,5 @@
1
- import { TranscriptMessage } from '../types/index';
2
- import { EngineCallbacks } from './QueryEngine';
1
+ import { TranscriptMessage } from '../types/index.js';
2
+ import { EngineCallbacks } from './QueryEngine.js';
3
3
  export declare class WorkerBridge {
4
4
  private worker;
5
5
  /** 在独立 Worker 线程中执行查询,返回更新后的 transcript */
@@ -5,6 +5,8 @@
5
5
  import { Worker } from 'worker_threads';
6
6
  import { fileURLToPath } from 'url';
7
7
  import path from 'path';
8
+ import { agentMessageBus } from './AgentMessageBus.js';
9
+ import { spawnSubAgentInMainThread } from '../tools/spawnAgent.js';
8
10
  // 兼容 ESM __dirname
9
11
  const __filename = fileURLToPath(import.meta.url);
10
12
  const __dirname = path.dirname(__filename);
@@ -72,6 +74,72 @@ export class WorkerBridge {
72
74
  worker.postMessage(reply);
73
75
  break;
74
76
  }
77
+ case 'subagent_message':
78
+ // SubAgent 消息:推送到 UI(已携带 subAgentId)
79
+ callbacks.onSubAgentMessage?.(msg.msg);
80
+ break;
81
+ case 'subagent_update_message':
82
+ callbacks.onSubAgentUpdateMessage?.(msg.id, msg.updates);
83
+ break;
84
+ // ===== MessageBus IPC 代理(queryWorker → 主线程) =====
85
+ case 'bus_publish': {
86
+ agentMessageBus.publish(msg.from, msg.channel, msg.payload);
87
+ const ack = { type: 'bus_publish_ack', requestId: msg.requestId };
88
+ worker.postMessage(ack);
89
+ break;
90
+ }
91
+ case 'bus_subscribe': {
92
+ agentMessageBus.subscribe(msg.channel, msg.timeoutMs, msg.fromOffset).then((busMsg) => {
93
+ const reply = {
94
+ type: 'bus_subscribe_result',
95
+ requestId: msg.requestId,
96
+ message: busMsg,
97
+ };
98
+ worker.postMessage(reply);
99
+ });
100
+ break;
101
+ }
102
+ case 'bus_read_history': {
103
+ const history = agentMessageBus.getHistory(msg.channel, msg.limit);
104
+ const reply = {
105
+ type: 'bus_read_history_result',
106
+ requestId: msg.requestId,
107
+ messages: history,
108
+ };
109
+ worker.postMessage(reply);
110
+ break;
111
+ }
112
+ case 'bus_get_offset': {
113
+ const offset = agentMessageBus.getOffset(msg.channel);
114
+ const reply = {
115
+ type: 'bus_get_offset_result',
116
+ requestId: msg.requestId,
117
+ offset,
118
+ };
119
+ worker.postMessage(reply);
120
+ break;
121
+ }
122
+ case 'bus_list_channels': {
123
+ const channels = agentMessageBus.listChannels();
124
+ const reply = {
125
+ type: 'bus_list_channels_result',
126
+ requestId: msg.requestId,
127
+ channels,
128
+ };
129
+ worker.postMessage(reply);
130
+ break;
131
+ }
132
+ // ===== spawn_subagent IPC(在主线程创建 SubAgentBridge)=====
133
+ case 'spawn_subagent': {
134
+ const result = spawnSubAgentInMainThread(msg.taskId, msg.instruction, msg.agentLabel, msg.allowedTools);
135
+ const reply = {
136
+ type: 'spawn_subagent_result',
137
+ requestId: msg.requestId,
138
+ result,
139
+ };
140
+ worker.postMessage(reply);
141
+ break;
142
+ }
75
143
  case 'done':
76
144
  this.worker = null;
77
145
  worker.terminate();
@@ -0,0 +1,9 @@
1
+ import type { BusMessage } from './AgentMessageBus.js';
2
+ export interface BusAccessor {
3
+ publish(from: string, channel: string, payload: string): Promise<void> | void;
4
+ subscribe(channel: string, timeoutMs?: number, fromOffset?: number): Promise<BusMessage | null>;
5
+ getHistory(channel: string, limit?: number): Promise<BusMessage[]> | BusMessage[];
6
+ getOffset(channel: string): Promise<number> | number;
7
+ listChannels(): Promise<string[]> | string[];
8
+ }
9
+ export declare function getBus(): Promise<BusAccessor>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * busAccess — 统一的 MessageBus 访问入口
3
+ *
4
+ * 解决跨线程访问问题:
5
+ * - 主线程:直接使用 agentMessageBus 单例
6
+ * - SubAgent Worker 线程:使用 workerBusProxy(通过 IPC 代理到主线程)
7
+ *
8
+ * 工具层统一通过此模块访问,无需关心当前运行环境。
9
+ */
10
+ import { isMainThread } from 'worker_threads';
11
+ let _bus = null;
12
+ export async function getBus() {
13
+ if (_bus)
14
+ return _bus;
15
+ if (isMainThread) {
16
+ // 主线程:直接使用单例
17
+ const { agentMessageBus } = await import('./AgentMessageBus.js');
18
+ _bus = {
19
+ publish: (from, channel, payload) => { agentMessageBus.publish(from, channel, payload); },
20
+ subscribe: (channel, timeoutMs, fromOffset) => agentMessageBus.subscribe(channel, timeoutMs, fromOffset),
21
+ getHistory: (channel, limit) => agentMessageBus.getHistory(channel, limit),
22
+ getOffset: (channel) => agentMessageBus.getOffset(channel),
23
+ listChannels: () => agentMessageBus.listChannels(),
24
+ };
25
+ }
26
+ else {
27
+ // Worker 线程:使用独立的 IPC 代理模块(避免循环依赖)
28
+ const { workerBusProxy } = await import('./workerBusProxy.js');
29
+ _bus = workerBusProxy;
30
+ }
31
+ return _bus;
32
+ }
package/dist/core/hint.js CHANGED
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
- import { getAgent } from '../agents/index';
10
- import { DEFAULT_AGENT } from '../config/constants';
11
- import { loadConfig, getActiveModel } from '../config/loader';
12
- import { getDefaultConfig } from '../services/api/llm';
9
+ import { getAgent } from '../agents/index.js';
10
+ import { DEFAULT_AGENT } from '../config/constants.js';
11
+ import { loadConfig, getActiveModel } from '../config/loader.js';
12
+ import { getDefaultConfig } from '../services/api/llm.js';
13
13
  /** 安全读取 JSON 文件 */
14
14
  function readJsonSafe(filePath) {
15
15
  try {
@@ -15,6 +15,10 @@ export interface QueryCallbacks {
15
15
  * @param ruleName 匹配的规则名
16
16
  */
17
17
  onConfirmDangerousCommand?: (command: string, reason: string, ruleName: string) => Promise<DangerConfirmResult>;
18
+ /** SubAgent 产生消息时透传到主线程 UI(由 dispatch_subagent 工具触发) */
19
+ onSubAgentMessage?: (msg: Message) => void;
20
+ /** SubAgent 更新已有消息时透传 */
21
+ onSubAgentUpdateMessage?: (id: string, updates: Partial<Message>) => void;
18
22
  }
19
23
  /**
20
24
  * 单轮 Agentic Loop:推理 → 工具调用 → 循环
@@ -2,12 +2,74 @@ import { v4 as uuid } from 'uuid';
2
2
  import { Worker } from 'worker_threads';
3
3
  import { fileURLToPath } from 'url';
4
4
  import path from 'path';
5
- import { findToolMerged as findTool } from '../tools/index';
6
- import { MAX_ITERATIONS } from '../config/constants';
7
- import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard';
5
+ import { findToolMerged as findTool } from '../tools/index.js';
6
+ import { MAX_ITERATIONS, CONTEXT_TOKEN_LIMIT } from '../config/constants.js';
7
+ import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard.js';
8
8
  // 兼容 ESM __dirname
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
+ // ===== Transcript 上下文压缩 =====
12
+ /**
13
+ * 粗略估算字符串的 token 数(按 4 字符/token 估算,中文按 2 字符/token)
14
+ * 仅用于判断是否需要压缩,不要求精确
15
+ */
16
+ function estimateTokens(text) {
17
+ // 中文字符占比高时每字约 1.5 token,英文约 0.25 token/char
18
+ const cjk = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
19
+ const rest = text.length - cjk;
20
+ return Math.ceil(cjk * 1.5 + rest * 0.25);
21
+ }
22
+ /**
23
+ * 对 transcript 中的 tool_result 做滑动压缩:
24
+ * - 保留最近 N 条 tool_result 完整内容
25
+ * - 更早的 tool_result 截断到 maxOldChars 字符
26
+ * - 确保总估算 token 不超过 CONTEXT_TOKEN_LIMIT
27
+ */
28
+ function compressTranscript(transcript) {
29
+ // 单条工具结果最大字符数
30
+ const MAX_TOOL_RESULT_CHARS = 3000;
31
+ // 旧条目压缩到的字符数
32
+ const MAX_OLD_TOOL_RESULT_CHARS = 800;
33
+ // 保留最近几条完整
34
+ const KEEP_RECENT = 2;
35
+ // 先对所有 tool_result 做单条截断
36
+ let result = transcript.map((msg) => {
37
+ if (msg.role !== 'tool_result')
38
+ return msg;
39
+ const content = msg.content;
40
+ if (content.length <= MAX_TOOL_RESULT_CHARS)
41
+ return msg;
42
+ return {
43
+ ...msg,
44
+ content: content.slice(0, MAX_TOOL_RESULT_CHARS) + `\n...[已截断,原始长度 ${content.length} 字符]`,
45
+ };
46
+ });
47
+ // 估算总 token,超限则压缩旧 tool_result
48
+ const totalTokens = estimateTokens(result.map((m) => {
49
+ if (typeof m.content === 'string')
50
+ return m.content;
51
+ return JSON.stringify(m.content);
52
+ }).join(''));
53
+ if (totalTokens <= CONTEXT_TOKEN_LIMIT)
54
+ return result;
55
+ // 找出所有 tool_result 的索引,保留最近 KEEP_RECENT 条,其余压缩
56
+ const toolResultIndices = result
57
+ .map((m, i) => (m.role === 'tool_result' ? i : -1))
58
+ .filter((i) => i >= 0);
59
+ const toCompress = toolResultIndices.slice(0, Math.max(0, toolResultIndices.length - KEEP_RECENT));
60
+ result = result.map((msg, i) => {
61
+ if (!toCompress.includes(i))
62
+ return msg;
63
+ const content = msg.content;
64
+ if (content.length <= MAX_OLD_TOOL_RESULT_CHARS)
65
+ return msg;
66
+ return {
67
+ ...msg,
68
+ content: content.slice(0, MAX_OLD_TOOL_RESULT_CHARS) + `\n...[已压缩]`,
69
+ };
70
+ });
71
+ return result;
72
+ }
11
73
  /**
12
74
  * 单轮 Agentic Loop:推理 → 工具调用 → 循环
13
75
  */
@@ -24,7 +86,7 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
24
86
  while (loopState.iteration < MAX_ITERATIONS && !abortSignal.aborted) {
25
87
  loopState.iteration++;
26
88
  callbacks.onLoopStateChange({ ...loopState });
27
- const result = await runOneIteration(localTranscript, _tools, service, callbacks, abortSignal);
89
+ const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal);
28
90
  // 构建 assistant transcript 块
29
91
  const assistantBlocks = [];
30
92
  if (result.text)
@@ -58,6 +120,10 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
58
120
  if (result.toolCalls.length > 1 && canRunInParallel(result.toolCalls)) {
59
121
  toolResults = await executeToolsInParallel(result.toolCalls, callbacks, abortSignal);
60
122
  }
123
+ else if (result.toolCalls.length > 1 && canRunInParallelDirect(result.toolCalls)) {
124
+ // dispatch_subagent 等需要 toolCallbacks 的工具:用 executeTool 并行执行
125
+ toolResults = await Promise.all(result.toolCalls.map((tc) => executeTool(tc, callbacks, abortSignal).then((r) => ({ tc, ...r }))));
126
+ }
61
127
  else {
62
128
  toolResults = [];
63
129
  for (const tc of result.toolCalls) {
@@ -207,6 +273,9 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
207
273
  // ===== 并行执行判断 =====
208
274
  /** 判断一组工具调用是否可以并行执行(无写-写冲突、无读-写冲突) */
209
275
  function canRunInParallel(calls) {
276
+ // run_agent / spawn_agent 需要 toolCallbacks 传递消息,不能走 runToolInWorker 路径
277
+ if (calls.some((c) => c.name === 'run_agent' || c.name === 'spawn_agent'))
278
+ return false;
210
279
  // 写操作工具集合
211
280
  const WRITE_TOOLS = new Set(['WriteFile', 'Bash']);
212
281
  // 读操作工具集合
@@ -228,6 +297,14 @@ function canRunInParallel(calls) {
228
297
  // 其余情况(如多个只读 skill)允许并行
229
298
  return true;
230
299
  }
300
+ /**
301
+ * 判断是否可以用 executeTool 直接并行(适用于 dispatch_subagent 等需要 toolCallbacks 的工具)
302
+ * 这些工具在主线程 Worker 里执行,可以正确传递 callbacks,但不能用 runToolInWorker
303
+ */
304
+ function canRunInParallelDirect(calls) {
305
+ // 全部是 run_agent / spawn_agent 时,可以用 executeTool 并行(各自启动独立 SubAgent Worker)
306
+ return calls.every((c) => c.name === 'run_agent' || c.name === 'spawn_agent');
307
+ }
231
308
  // ===== 并行工具执行(多 Worker 线程) =====
232
309
  /** 在独立 Worker 线程中执行单个工具,返回结果字符串 */
233
310
  function runToolInWorker(tc, abortSignal) {
@@ -430,7 +507,16 @@ async function executeTool(tc, callbacks, abortSignal) {
430
507
  }
431
508
  try {
432
509
  const start = Date.now();
433
- const result = await tool.execute(tc.input, abortSignal);
510
+ const result = await tool.execute(tc.input, abortSignal, {
511
+ onSubAgentMessage: (msg) => {
512
+ // SubAgent 消息只走 onSubAgentMessage,避免与主 Agent 消息流混淆
513
+ callbacks.onSubAgentMessage?.(msg);
514
+ },
515
+ onSubAgentUpdateMessage: (id, updates) => {
516
+ // SubAgent 更新只走 onSubAgentUpdateMessage,跳过主 Agent 的节流逻辑
517
+ callbacks.onSubAgentUpdateMessage?.(id, updates);
518
+ },
519
+ });
434
520
  // 对工具输出统一脱敏
435
521
  const safeResult = sanitizeOutput(result);
436
522
  // 工具执行期间被中断
@@ -1,5 +1,6 @@
1
1
  import { DangerConfirmResult } from './query.js';
2
2
  import { TranscriptMessage, Message, LoopState, Session } from '../types/index.js';
3
+ import { BusMessage } from './AgentMessageBus.js';
3
4
  export type WorkerInbound = {
4
5
  type: 'run';
5
6
  userInput: string;
@@ -10,6 +11,29 @@ export type WorkerInbound = {
10
11
  type: 'danger_confirm_result';
11
12
  requestId: string;
12
13
  choice: DangerConfirmResult;
14
+ } | {
15
+ type: 'bus_publish_ack';
16
+ requestId: string;
17
+ } | {
18
+ type: 'bus_subscribe_result';
19
+ requestId: string;
20
+ message: BusMessage | null;
21
+ } | {
22
+ type: 'bus_read_history_result';
23
+ requestId: string;
24
+ messages: BusMessage[];
25
+ } | {
26
+ type: 'bus_get_offset_result';
27
+ requestId: string;
28
+ offset: number;
29
+ } | {
30
+ type: 'bus_list_channels_result';
31
+ requestId: string;
32
+ channels: string[];
33
+ } | {
34
+ type: 'spawn_subagent_result';
35
+ requestId: string;
36
+ result: string;
13
37
  };
14
38
  export type WorkerOutbound = {
15
39
  type: 'message';
@@ -35,6 +59,44 @@ export type WorkerOutbound = {
35
59
  command: string;
36
60
  reason: string;
37
61
  ruleName: string;
62
+ } | {
63
+ type: 'subagent_message';
64
+ msg: Message;
65
+ } | {
66
+ type: 'subagent_update_message';
67
+ id: string;
68
+ updates: Partial<Message>;
69
+ } | {
70
+ type: 'bus_publish';
71
+ requestId: string;
72
+ from: string;
73
+ channel: string;
74
+ payload: string;
75
+ } | {
76
+ type: 'bus_subscribe';
77
+ requestId: string;
78
+ channel: string;
79
+ timeoutMs: number;
80
+ fromOffset?: number;
81
+ } | {
82
+ type: 'bus_read_history';
83
+ requestId: string;
84
+ channel: string;
85
+ limit?: number;
86
+ } | {
87
+ type: 'bus_get_offset';
88
+ requestId: string;
89
+ channel: string;
90
+ } | {
91
+ type: 'bus_list_channels';
92
+ requestId: string;
93
+ } | {
94
+ type: 'spawn_subagent';
95
+ requestId: string;
96
+ taskId: string;
97
+ instruction: string;
98
+ agentLabel: string;
99
+ allowedTools?: string[];
38
100
  } | {
39
101
  type: 'done';
40
102
  transcript: TranscriptMessage[];
@@ -8,6 +8,8 @@ import { getAllTools } from '../tools/index.js';
8
8
  import { LLMServiceImpl } from '../services/api/llm.js';
9
9
  import { MockService } from '../services/api/mock.js';
10
10
  import { loadConfig, getActiveModel } from '../config/loader.js';
11
+ import { pendingBusRequests } from './workerBusProxy.js';
12
+ import { pendingSpawnRequests } from './spawnRegistry.js';
11
13
  if (!parentPort)
12
14
  throw new Error('queryWorker must run inside worker_threads');
13
15
  // ===== 初始化 LLM 服务 =====
@@ -38,6 +40,37 @@ parentPort.on('message', async (msg) => {
38
40
  }
39
41
  return;
40
42
  }
43
+ // MessageBus IPC 回复分发 → 转发给 workerBusProxy 的 pending map
44
+ if (msg.type === 'bus_publish_ack' ||
45
+ msg.type === 'bus_subscribe_result' ||
46
+ msg.type === 'bus_read_history_result' ||
47
+ msg.type === 'bus_get_offset_result' ||
48
+ msg.type === 'bus_list_channels_result') {
49
+ const resolve = pendingBusRequests.get(msg.requestId);
50
+ if (resolve) {
51
+ pendingBusRequests.delete(msg.requestId);
52
+ if (msg.type === 'bus_publish_ack')
53
+ resolve(undefined);
54
+ else if (msg.type === 'bus_subscribe_result')
55
+ resolve(msg.message);
56
+ else if (msg.type === 'bus_read_history_result')
57
+ resolve(msg.messages);
58
+ else if (msg.type === 'bus_get_offset_result')
59
+ resolve(msg.offset);
60
+ else if (msg.type === 'bus_list_channels_result')
61
+ resolve(msg.channels);
62
+ }
63
+ return;
64
+ }
65
+ // spawn_subagent IPC 回复 → 转发给 pendingSpawnRequests
66
+ if (msg.type === 'spawn_subagent_result') {
67
+ const resolve = pendingSpawnRequests.get(msg.requestId);
68
+ if (resolve) {
69
+ pendingSpawnRequests.delete(msg.requestId);
70
+ resolve(msg.result);
71
+ }
72
+ return;
73
+ }
41
74
  if (msg.type === 'run') {
42
75
  abortSignal.aborted = false;
43
76
  const send = (out) => parentPort.postMessage(out);
@@ -54,6 +87,8 @@ parentPort.on('message', async (msg) => {
54
87
  send({ type: 'danger_confirm_request', requestId, command, reason, ruleName });
55
88
  });
56
89
  },
90
+ onSubAgentMessage: (msg) => send({ type: 'subagent_message', msg }),
91
+ onSubAgentUpdateMessage: (id, updates) => send({ type: 'subagent_update_message', id, updates }),
57
92
  };
58
93
  try {
59
94
  const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * spawnRegistry — spawn_subagent IPC 挂起请求注册表
3
+ *
4
+ * 独立模块,避免 spawnAgent.ts ↔ queryWorker.ts 循环依赖。
5
+ * queryWorker.ts 和 spawnAgent.ts 都可以安全 import 此模块。
6
+ */
7
+ /** 等待主线程完成 SubAgent 启动的挂起 Promise,key = requestId */
8
+ export declare const pendingSpawnRequests: Map<string, (result: string) => void>;
9
+ /** 增加运行中 SubAgent 计数 */
10
+ export declare function incrementActiveAgents(): void;
11
+ /** 减少运行中 SubAgent 计数 */
12
+ export declare function decrementActiveAgents(): void;
13
+ /** 获取当前运行中 SubAgent 数量 */
14
+ export declare function getActiveAgentCount(): number;
15
+ /** 订阅计数变更,返回取消订阅函数 */
16
+ export declare function subscribeAgentCount(listener: (count: number) => void): () => void;