@code4bug/jarvis-agent 1.0.4 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +67 -0
  2. package/dist/agents/jarvis.md +11 -0
  3. package/dist/cli.js +13 -0
  4. package/dist/components/MessageItem.js +9 -5
  5. package/dist/components/StatusBar.d.ts +2 -1
  6. package/dist/components/StatusBar.js +5 -4
  7. package/dist/config/constants.d.ts +2 -0
  8. package/dist/config/constants.js +3 -1
  9. package/dist/config/userProfile.d.ts +4 -0
  10. package/dist/config/userProfile.js +25 -0
  11. package/dist/core/AgentMessageBus.d.ts +50 -0
  12. package/dist/core/AgentMessageBus.js +128 -0
  13. package/dist/core/AgentRegistry.d.ts +22 -0
  14. package/dist/core/AgentRegistry.js +16 -0
  15. package/dist/core/QueryEngine.d.ts +7 -0
  16. package/dist/core/QueryEngine.js +82 -30
  17. package/dist/core/SubAgentBridge.d.ts +20 -0
  18. package/dist/core/SubAgentBridge.js +208 -0
  19. package/dist/core/WorkerBridge.js +80 -0
  20. package/dist/core/busAccess.d.ts +9 -0
  21. package/dist/core/busAccess.js +32 -0
  22. package/dist/core/logger.d.ts +8 -0
  23. package/dist/core/logger.js +63 -0
  24. package/dist/core/query.d.ts +4 -0
  25. package/dist/core/query.js +169 -4
  26. package/dist/core/queryWorker.d.ts +62 -0
  27. package/dist/core/queryWorker.js +46 -0
  28. package/dist/core/spawnRegistry.d.ts +16 -0
  29. package/dist/core/spawnRegistry.js +32 -0
  30. package/dist/core/subAgentWorker.d.ts +89 -0
  31. package/dist/core/subAgentWorker.js +121 -0
  32. package/dist/core/workerBusProxy.d.ts +10 -0
  33. package/dist/core/workerBusProxy.js +57 -0
  34. package/dist/hooks/useSlashMenu.d.ts +2 -0
  35. package/dist/hooks/useSlashMenu.js +5 -1
  36. package/dist/hooks/useTokenDisplay.d.ts +1 -0
  37. package/dist/hooks/useTokenDisplay.js +5 -0
  38. package/dist/index.js +2 -0
  39. package/dist/screens/repl.js +48 -16
  40. package/dist/screens/slashCommands.js +2 -1
  41. package/dist/services/api/llm.d.ts +2 -0
  42. package/dist/services/api/llm.js +23 -1
  43. package/dist/services/userProfile.d.ts +1 -0
  44. package/dist/services/userProfile.js +127 -0
  45. package/dist/tools/index.d.ts +7 -1
  46. package/dist/tools/index.js +13 -2
  47. package/dist/tools/publishMessage.d.ts +8 -0
  48. package/dist/tools/publishMessage.js +41 -0
  49. package/dist/tools/readChannel.d.ts +8 -0
  50. package/dist/tools/readChannel.js +44 -0
  51. package/dist/tools/runAgent.d.ts +11 -0
  52. package/dist/tools/runAgent.js +111 -0
  53. package/dist/tools/runCommand.js +16 -0
  54. package/dist/tools/sendToAgent.d.ts +11 -0
  55. package/dist/tools/sendToAgent.js +35 -0
  56. package/dist/tools/spawnAgent.d.ts +6 -0
  57. package/dist/tools/spawnAgent.js +180 -0
  58. package/dist/tools/subscribeMessage.d.ts +8 -0
  59. package/dist/tools/subscribeMessage.js +59 -0
  60. package/dist/types/index.d.ts +49 -1
  61. package/package.json +1 -1
@@ -3,15 +3,82 @@ import { Worker } from 'worker_threads';
3
3
  import { fileURLToPath } from 'url';
4
4
  import path from 'path';
5
5
  import { findToolMerged as findTool } from '../tools/index.js';
6
- import { MAX_ITERATIONS } from '../config/constants.js';
6
+ import { MAX_ITERATIONS, CONTEXT_TOKEN_LIMIT } from '../config/constants.js';
7
7
  import { sanitizeOutput, validateCommand, authorizeCommand, authorizeRule } from './safeguard.js';
8
+ import { logError, logInfo, logWarn } from './logger.js';
8
9
  // 兼容 ESM __dirname
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
12
+ // ===== Transcript 上下文压缩 =====
13
+ /**
14
+ * 粗略估算字符串的 token 数(按 4 字符/token 估算,中文按 2 字符/token)
15
+ * 仅用于判断是否需要压缩,不要求精确
16
+ */
17
+ function estimateTokens(text) {
18
+ // 中文字符占比高时每字约 1.5 token,英文约 0.25 token/char
19
+ const cjk = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
20
+ const rest = text.length - cjk;
21
+ return Math.ceil(cjk * 1.5 + rest * 0.25);
22
+ }
23
+ /**
24
+ * 对 transcript 中的 tool_result 做滑动压缩:
25
+ * - 保留最近 N 条 tool_result 完整内容
26
+ * - 更早的 tool_result 截断到 maxOldChars 字符
27
+ * - 确保总估算 token 不超过 CONTEXT_TOKEN_LIMIT
28
+ */
29
+ function compressTranscript(transcript) {
30
+ // 单条工具结果最大字符数
31
+ const MAX_TOOL_RESULT_CHARS = 3000;
32
+ // 旧条目压缩到的字符数
33
+ const MAX_OLD_TOOL_RESULT_CHARS = 800;
34
+ // 保留最近几条完整
35
+ const KEEP_RECENT = 2;
36
+ // 先对所有 tool_result 做单条截断
37
+ let result = transcript.map((msg) => {
38
+ if (msg.role !== 'tool_result')
39
+ return msg;
40
+ const content = msg.content;
41
+ if (content.length <= MAX_TOOL_RESULT_CHARS)
42
+ return msg;
43
+ return {
44
+ ...msg,
45
+ content: content.slice(0, MAX_TOOL_RESULT_CHARS) + `\n...[已截断,原始长度 ${content.length} 字符]`,
46
+ };
47
+ });
48
+ // 估算总 token,超限则压缩旧 tool_result
49
+ const totalTokens = estimateTokens(result.map((m) => {
50
+ if (typeof m.content === 'string')
51
+ return m.content;
52
+ return JSON.stringify(m.content);
53
+ }).join(''));
54
+ if (totalTokens <= CONTEXT_TOKEN_LIMIT)
55
+ return result;
56
+ // 找出所有 tool_result 的索引,保留最近 KEEP_RECENT 条,其余压缩
57
+ const toolResultIndices = result
58
+ .map((m, i) => (m.role === 'tool_result' ? i : -1))
59
+ .filter((i) => i >= 0);
60
+ const toCompress = toolResultIndices.slice(0, Math.max(0, toolResultIndices.length - KEEP_RECENT));
61
+ result = result.map((msg, i) => {
62
+ if (!toCompress.includes(i))
63
+ return msg;
64
+ const content = msg.content;
65
+ if (content.length <= MAX_OLD_TOOL_RESULT_CHARS)
66
+ return msg;
67
+ return {
68
+ ...msg,
69
+ content: content.slice(0, MAX_OLD_TOOL_RESULT_CHARS) + `\n...[已压缩]`,
70
+ };
71
+ });
72
+ return result;
73
+ }
11
74
  /**
12
75
  * 单轮 Agentic Loop:推理 → 工具调用 → 循环
13
76
  */
14
77
  export async function executeQuery(userInput, transcript, _tools, service, callbacks, abortSignal) {
78
+ logInfo('agent_loop.start', {
79
+ inputLength: userInput.length,
80
+ initialTranscriptLength: transcript.length,
81
+ });
15
82
  const localTranscript = [...transcript];
16
83
  localTranscript.push({ role: 'user', content: userInput });
17
84
  const loopState = {
@@ -23,8 +90,19 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
23
90
  callbacks.onLoopStateChange({ ...loopState });
24
91
  while (loopState.iteration < MAX_ITERATIONS && !abortSignal.aborted) {
25
92
  loopState.iteration++;
93
+ logInfo('agent_loop.iteration.start', {
94
+ iteration: loopState.iteration,
95
+ transcriptLength: localTranscript.length,
96
+ });
26
97
  callbacks.onLoopStateChange({ ...loopState });
27
- const result = await runOneIteration(localTranscript, _tools, service, callbacks, abortSignal);
98
+ const result = await runOneIteration(compressTranscript(localTranscript), _tools, service, callbacks, abortSignal);
99
+ logInfo('agent_loop.iteration.result', {
100
+ iteration: loopState.iteration,
101
+ textLength: result.text.length,
102
+ toolCallCount: result.toolCalls.length,
103
+ duration: result.duration,
104
+ tokenCount: result.tokenCount,
105
+ });
28
106
  // 构建 assistant transcript 块
29
107
  const assistantBlocks = [];
30
108
  if (result.text)
@@ -40,6 +118,10 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
40
118
  break;
41
119
  // 中断发生在推理阶段
42
120
  if (abortSignal.aborted) {
121
+ logWarn('agent_loop.aborted_before_tool_execution', {
122
+ iteration: loopState.iteration,
123
+ toolCallCount: result.toolCalls.length,
124
+ });
43
125
  for (const tc of result.toolCalls) {
44
126
  const skippedResult = `[用户中断] 工具 ${tc.name} 未执行(用户按下 ESC 中断)`;
45
127
  localTranscript.push({ role: 'tool_result', toolUseId: tc.id, content: skippedResult });
@@ -58,6 +140,10 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
58
140
  if (result.toolCalls.length > 1 && canRunInParallel(result.toolCalls)) {
59
141
  toolResults = await executeToolsInParallel(result.toolCalls, callbacks, abortSignal);
60
142
  }
143
+ else if (result.toolCalls.length > 1 && canRunInParallelDirect(result.toolCalls)) {
144
+ // dispatch_subagent 等需要 toolCallbacks 的工具:用 executeTool 并行执行
145
+ toolResults = await Promise.all(result.toolCalls.map((tc) => executeTool(tc, callbacks, abortSignal).then((r) => ({ tc, ...r }))));
146
+ }
61
147
  else {
62
148
  toolResults = [];
63
149
  for (const tc of result.toolCalls) {
@@ -79,11 +165,20 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
79
165
  loopState.aborted = abortSignal.aborted;
80
166
  callbacks.onLoopStateChange({ ...loopState });
81
167
  if (abortSignal.aborted) {
168
+ logWarn('agent_loop.aborted', {
169
+ finalIteration: loopState.iteration,
170
+ transcriptLength: localTranscript.length,
171
+ });
82
172
  localTranscript.push({
83
173
  role: 'user',
84
174
  content: '[系统提示] 用户中断了上一轮回复(按下 ESC)。上一条助手消息可能不完整,请在后续回复中注意这一点。',
85
175
  });
86
176
  }
177
+ logInfo('agent_loop.done', {
178
+ finalIteration: loopState.iteration,
179
+ aborted: abortSignal.aborted,
180
+ transcriptLength: localTranscript.length,
181
+ });
87
182
  return localTranscript;
88
183
  }
89
184
  /** 执行一次 LLM 调用 */
@@ -95,6 +190,10 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
95
190
  let tokenCount = 0;
96
191
  let firstTokenTime = null;
97
192
  const thinkingId = uuid();
193
+ logInfo('llm.iteration.requested', {
194
+ transcriptLength: transcript.length,
195
+ toolCount: tools.length,
196
+ });
98
197
  callbacks.onMessage({
99
198
  id: thinkingId,
100
199
  type: 'thinking',
@@ -202,11 +301,23 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
202
301
  });
203
302
  }
204
303
  const effectiveText = accumulatedText || (accumulatedThinking && toolCalls.length === 0 ? accumulatedThinking : '');
304
+ logInfo('llm.iteration.completed', {
305
+ duration,
306
+ tokenCount,
307
+ firstTokenLatency,
308
+ tokensPerSecond,
309
+ textLength: effectiveText.length,
310
+ thinkingLength: accumulatedThinking.length,
311
+ toolCallCount: toolCalls.length,
312
+ });
205
313
  return { text: effectiveText, toolCalls, duration, tokenCount, firstTokenLatency, tokensPerSecond };
206
314
  }
207
315
  // ===== 并行执行判断 =====
208
316
  /** 判断一组工具调用是否可以并行执行(无写-写冲突、无读-写冲突) */
209
317
  function canRunInParallel(calls) {
318
+ // run_agent / spawn_agent 需要 toolCallbacks 传递消息,不能走 runToolInWorker 路径
319
+ if (calls.some((c) => c.name === 'run_agent' || c.name === 'spawn_agent'))
320
+ return false;
210
321
  // 写操作工具集合
211
322
  const WRITE_TOOLS = new Set(['WriteFile', 'Bash']);
212
323
  // 读操作工具集合
@@ -228,6 +339,14 @@ function canRunInParallel(calls) {
228
339
  // 其余情况(如多个只读 skill)允许并行
229
340
  return true;
230
341
  }
342
+ /**
343
+ * 判断是否可以用 executeTool 直接并行(适用于 dispatch_subagent 等需要 toolCallbacks 的工具)
344
+ * 这些工具在主线程 Worker 里执行,可以正确传递 callbacks,但不能用 runToolInWorker
345
+ */
346
+ function canRunInParallelDirect(calls) {
347
+ // 全部是 run_agent / spawn_agent 时,可以用 executeTool 并行(各自启动独立 SubAgent Worker)
348
+ return calls.every((c) => c.name === 'run_agent' || c.name === 'spawn_agent');
349
+ }
231
350
  // ===== 并行工具执行(多 Worker 线程) =====
232
351
  /** 在独立 Worker 线程中执行单个工具,返回结果字符串 */
233
352
  function runToolInWorker(tc, abortSignal) {
@@ -281,17 +400,27 @@ export async function runToolDirect(tc, abortSignal) {
281
400
  if (!tool)
282
401
  return `错误: 未知工具 ${tc.name}`;
283
402
  try {
403
+ logInfo('tool.direct.start', { toolName: tc.name, toolArgs: tc.input });
284
404
  const result = await tool.execute(tc.input, abortSignal);
285
405
  const { sanitizeOutput } = await import('./safeguard.js');
406
+ logInfo('tool.direct.done', {
407
+ toolName: tc.name,
408
+ resultLength: String(result).length,
409
+ });
286
410
  return sanitizeOutput(result);
287
411
  }
288
412
  catch (err) {
413
+ logError('tool.direct.failed', err, { toolName: tc.name, toolArgs: tc.input });
289
414
  return `错误: ${err.message || '工具执行失败'}`;
290
415
  }
291
416
  }
292
417
  /** 并行执行多个工具,每个工具在独立 Worker 线程中运行,实时更新 UI */
293
418
  async function executeToolsInParallel(calls, callbacks, abortSignal) {
294
419
  const groupId = uuid();
420
+ logInfo('tool.parallel_group.start', {
421
+ groupId,
422
+ toolNames: calls.map((call) => call.name),
423
+ });
295
424
  // 为每个工具预先创建 pending 消息节点(TUI 立即渲染占位)
296
425
  const msgIds = calls.map((tc) => {
297
426
  const msgId = uuid();
@@ -356,11 +485,18 @@ async function executeToolsInParallel(calls, callbacks, abortSignal) {
356
485
  }
357
486
  catch (err) {
358
487
  const errMsg = err.message || '工具执行失败';
488
+ logError('tool.parallel.failed', err, { groupId, toolName: tc.name, toolArgs: tc.input });
359
489
  callbacks.onUpdateMessage(msgId, { status: 'error', content: errMsg, toolResult: errMsg });
360
490
  return { tc, content: `错误: ${errMsg}`, isError: false };
361
491
  }
362
492
  });
363
- return Promise.all(tasks);
493
+ const results = await Promise.all(tasks);
494
+ logInfo('tool.parallel_group.done', {
495
+ groupId,
496
+ toolNames: calls.map((call) => call.name),
497
+ errorCount: results.filter((item) => item.isError).length,
498
+ });
499
+ return results;
364
500
  }
365
501
  /** 执行工具并返回结果 */
366
502
  async function executeTool(tc, callbacks, abortSignal) {
@@ -385,6 +521,11 @@ async function executeTool(tc, callbacks, abortSignal) {
385
521
  toolName: tc.name,
386
522
  toolArgs: tc.input,
387
523
  });
524
+ logInfo('tool.execute.start', {
525
+ toolName: tc.name,
526
+ toolArgs: tc.input,
527
+ toolExecId,
528
+ });
388
529
  // ===== 安全围栏:Bash 命令拦截 + 交互式确认 =====
389
530
  if (tc.name === 'Bash' && tc.input.command) {
390
531
  const command = tc.input.command;
@@ -393,6 +534,7 @@ async function executeTool(tc, callbacks, abortSignal) {
393
534
  if (!check.canOverride) {
394
535
  // critical 级别:直接禁止
395
536
  const errMsg = `${check.reason}\n🚫 该命令已被永久禁止,无法通过授权绕过。\n命令: ${command}`;
537
+ logWarn('tool.execute.blocked', { toolName: tc.name, command, reason: check.reason });
396
538
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg, toolResult: errMsg });
397
539
  return { content: `错误: ${errMsg}`, isError: true };
398
540
  }
@@ -403,6 +545,7 @@ async function executeTool(tc, callbacks, abortSignal) {
403
545
  const userChoice = await callbacks.onConfirmDangerousCommand(command, reason, ruleName);
404
546
  if (userChoice === 'cancel') {
405
547
  const cancelMsg = `⛔ 用户取消执行危险命令: ${command}`;
548
+ logWarn('tool.execute.cancelled_by_user', { toolName: tc.name, command, reason });
406
549
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: cancelMsg, toolResult: cancelMsg });
407
550
  return { content: cancelMsg, isError: true };
408
551
  }
@@ -425,12 +568,22 @@ async function executeTool(tc, callbacks, abortSignal) {
425
568
  const tool = findTool(tc.name);
426
569
  if (!tool) {
427
570
  const errMsg = `未知工具: ${tc.name}`;
571
+ logWarn('tool.execute.unknown', { toolName: tc.name });
428
572
  callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg });
429
573
  return { content: errMsg, isError: true };
430
574
  }
431
575
  try {
432
576
  const start = Date.now();
433
- const result = await tool.execute(tc.input, abortSignal);
577
+ const result = await tool.execute(tc.input, abortSignal, {
578
+ onSubAgentMessage: (msg) => {
579
+ // SubAgent 消息只走 onSubAgentMessage,避免与主 Agent 消息流混淆
580
+ callbacks.onSubAgentMessage?.(msg);
581
+ },
582
+ onSubAgentUpdateMessage: (id, updates) => {
583
+ // SubAgent 更新只走 onSubAgentUpdateMessage,跳过主 Agent 的节流逻辑
584
+ callbacks.onSubAgentUpdateMessage?.(id, updates);
585
+ },
586
+ });
434
587
  // 对工具输出统一脱敏
435
588
  const safeResult = sanitizeOutput(result);
436
589
  // 工具执行期间被中断
@@ -453,10 +606,22 @@ async function executeTool(tc, callbacks, abortSignal) {
453
606
  duration: Date.now() - start,
454
607
  ...(wasAborted ? { abortHint: '命令已中断(ESC)' } : {}),
455
608
  });
609
+ logInfo('tool.execute.done', {
610
+ toolName: tc.name,
611
+ toolExecId,
612
+ duration: Date.now() - start,
613
+ aborted: Boolean(wasAborted),
614
+ resultLength: safeResult.length,
615
+ });
456
616
  return { content: safeResult, isError: false };
457
617
  }
458
618
  catch (err) {
459
619
  const errMsg = err.message || '工具执行失败';
620
+ logError('tool.execute.failed', err, {
621
+ toolName: tc.name,
622
+ toolArgs: tc.input,
623
+ toolExecId,
624
+ });
460
625
  callbacks.onUpdateMessage(toolExecId, {
461
626
  status: 'error',
462
627
  content: errMsg,
@@ -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,8 +8,12 @@ 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';
13
+ import { logError, logInfo, logWarn } from './logger.js';
11
14
  if (!parentPort)
12
15
  throw new Error('queryWorker must run inside worker_threads');
16
+ logInfo('query_worker.ready');
13
17
  // ===== 初始化 LLM 服务 =====
14
18
  const config = loadConfig();
15
19
  const activeModel = getActiveModel(config);
@@ -28,6 +32,7 @@ const pendingConfirms = new Map();
28
32
  parentPort.on('message', async (msg) => {
29
33
  if (msg.type === 'abort') {
30
34
  abortSignal.aborted = true;
35
+ logWarn('query_worker.abort_received');
31
36
  return;
32
37
  }
33
38
  if (msg.type === 'danger_confirm_result') {
@@ -38,8 +43,43 @@ parentPort.on('message', async (msg) => {
38
43
  }
39
44
  return;
40
45
  }
46
+ // MessageBus IPC 回复分发 → 转发给 workerBusProxy 的 pending map
47
+ if (msg.type === 'bus_publish_ack' ||
48
+ msg.type === 'bus_subscribe_result' ||
49
+ msg.type === 'bus_read_history_result' ||
50
+ msg.type === 'bus_get_offset_result' ||
51
+ msg.type === 'bus_list_channels_result') {
52
+ const resolve = pendingBusRequests.get(msg.requestId);
53
+ if (resolve) {
54
+ pendingBusRequests.delete(msg.requestId);
55
+ if (msg.type === 'bus_publish_ack')
56
+ resolve(undefined);
57
+ else if (msg.type === 'bus_subscribe_result')
58
+ resolve(msg.message);
59
+ else if (msg.type === 'bus_read_history_result')
60
+ resolve(msg.messages);
61
+ else if (msg.type === 'bus_get_offset_result')
62
+ resolve(msg.offset);
63
+ else if (msg.type === 'bus_list_channels_result')
64
+ resolve(msg.channels);
65
+ }
66
+ return;
67
+ }
68
+ // spawn_subagent IPC 回复 → 转发给 pendingSpawnRequests
69
+ if (msg.type === 'spawn_subagent_result') {
70
+ const resolve = pendingSpawnRequests.get(msg.requestId);
71
+ if (resolve) {
72
+ pendingSpawnRequests.delete(msg.requestId);
73
+ resolve(msg.result);
74
+ }
75
+ return;
76
+ }
41
77
  if (msg.type === 'run') {
42
78
  abortSignal.aborted = false;
79
+ logInfo('query_worker.run.start', {
80
+ inputLength: msg.userInput.length,
81
+ transcriptLength: msg.transcript.length,
82
+ });
43
83
  const send = (out) => parentPort.postMessage(out);
44
84
  const callbacks = {
45
85
  onMessage: (m) => send({ type: 'message', msg: m }),
@@ -54,12 +94,18 @@ parentPort.on('message', async (msg) => {
54
94
  send({ type: 'danger_confirm_request', requestId, command, reason, ruleName });
55
95
  });
56
96
  },
97
+ onSubAgentMessage: (msg) => send({ type: 'subagent_message', msg }),
98
+ onSubAgentUpdateMessage: (id, updates) => send({ type: 'subagent_update_message', id, updates }),
57
99
  };
58
100
  try {
59
101
  const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
102
+ logInfo('query_worker.run.done', {
103
+ transcriptLength: newTranscript.length,
104
+ });
60
105
  send({ type: 'done', transcript: newTranscript });
61
106
  }
62
107
  catch (err) {
108
+ logError('query_worker.run.failed', err);
63
109
  send({ type: 'error', message: err.message ?? '未知错误' });
64
110
  }
65
111
  }
@@ -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;
@@ -0,0 +1,32 @@
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 const pendingSpawnRequests = new Map();
9
+ // ===== 运行中 SubAgent 计数器 =====
10
+ /** 当前运行中的 SubAgent 数量 */
11
+ let _activeAgentCount = 0;
12
+ /** 计数变更订阅者列表 */
13
+ const _countListeners = new Set();
14
+ /** 增加运行中 SubAgent 计数 */
15
+ export function incrementActiveAgents() {
16
+ _activeAgentCount++;
17
+ _countListeners.forEach((fn) => fn(_activeAgentCount));
18
+ }
19
+ /** 减少运行中 SubAgent 计数 */
20
+ export function decrementActiveAgents() {
21
+ _activeAgentCount = Math.max(0, _activeAgentCount - 1);
22
+ _countListeners.forEach((fn) => fn(_activeAgentCount));
23
+ }
24
+ /** 获取当前运行中 SubAgent 数量 */
25
+ export function getActiveAgentCount() {
26
+ return _activeAgentCount;
27
+ }
28
+ /** 订阅计数变更,返回取消订阅函数 */
29
+ export function subscribeAgentCount(listener) {
30
+ _countListeners.add(listener);
31
+ return () => _countListeners.delete(listener);
32
+ }
@@ -0,0 +1,89 @@
1
+ import { DangerConfirmResult } from './query.js';
2
+ import { TranscriptMessage, Message, LoopState, SubAgentTask } from '../types/index.js';
3
+ import { BusMessage } from './AgentMessageBus.js';
4
+ export type SubAgentInbound = {
5
+ type: 'run';
6
+ task: SubAgentTask;
7
+ } | {
8
+ type: 'abort';
9
+ } | {
10
+ type: 'danger_confirm_result';
11
+ requestId: string;
12
+ choice: DangerConfirmResult;
13
+ } | {
14
+ type: 'bus_publish_ack';
15
+ requestId: string;
16
+ } | {
17
+ type: 'bus_subscribe_result';
18
+ requestId: string;
19
+ message: BusMessage | null;
20
+ } | {
21
+ type: 'bus_read_history_result';
22
+ requestId: string;
23
+ messages: BusMessage[];
24
+ } | {
25
+ type: 'bus_get_offset_result';
26
+ requestId: string;
27
+ offset: number;
28
+ } | {
29
+ type: 'bus_list_channels_result';
30
+ requestId: string;
31
+ channels: string[];
32
+ };
33
+ export type SubAgentOutbound = {
34
+ type: 'message';
35
+ taskId: string;
36
+ msg: Message;
37
+ } | {
38
+ type: 'update_message';
39
+ taskId: string;
40
+ id: string;
41
+ updates: Partial<Message>;
42
+ } | {
43
+ type: 'stream_text';
44
+ taskId: string;
45
+ text: string;
46
+ } | {
47
+ type: 'loop_state';
48
+ taskId: string;
49
+ state: LoopState;
50
+ } | {
51
+ type: 'danger_confirm_request';
52
+ taskId: string;
53
+ requestId: string;
54
+ command: string;
55
+ reason: string;
56
+ ruleName: string;
57
+ } | {
58
+ type: 'bus_publish';
59
+ requestId: string;
60
+ from: string;
61
+ channel: string;
62
+ payload: string;
63
+ } | {
64
+ type: 'bus_subscribe';
65
+ requestId: string;
66
+ channel: string;
67
+ timeoutMs: number;
68
+ fromOffset?: number;
69
+ } | {
70
+ type: 'bus_read_history';
71
+ requestId: string;
72
+ channel: string;
73
+ limit?: number;
74
+ } | {
75
+ type: 'bus_get_offset';
76
+ requestId: string;
77
+ channel: string;
78
+ } | {
79
+ type: 'bus_list_channels';
80
+ requestId: string;
81
+ } | {
82
+ type: 'done';
83
+ taskId: string;
84
+ transcript: TranscriptMessage[];
85
+ } | {
86
+ type: 'error';
87
+ taskId: string;
88
+ message: string;
89
+ };