@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.
- package/README.md +67 -0
- package/dist/agents/jarvis.md +11 -0
- package/dist/cli.js +13 -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/config/constants.d.ts +2 -0
- package/dist/config/constants.js +3 -1
- package/dist/config/userProfile.d.ts +4 -0
- package/dist/config/userProfile.js +25 -0
- package/dist/core/AgentMessageBus.d.ts +50 -0
- package/dist/core/AgentMessageBus.js +128 -0
- package/dist/core/AgentRegistry.d.ts +22 -0
- package/dist/core/AgentRegistry.js +16 -0
- package/dist/core/QueryEngine.d.ts +7 -0
- package/dist/core/QueryEngine.js +82 -30
- package/dist/core/SubAgentBridge.d.ts +20 -0
- package/dist/core/SubAgentBridge.js +208 -0
- package/dist/core/WorkerBridge.js +80 -0
- package/dist/core/busAccess.d.ts +9 -0
- package/dist/core/busAccess.js +32 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +63 -0
- package/dist/core/query.d.ts +4 -0
- package/dist/core/query.js +169 -4
- package/dist/core/queryWorker.d.ts +62 -0
- package/dist/core/queryWorker.js +46 -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 +121 -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/index.js +2 -0
- package/dist/screens/repl.js +48 -16
- package/dist/screens/slashCommands.js +2 -1
- package/dist/services/api/llm.d.ts +2 -0
- package/dist/services/api/llm.js +23 -1
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +127 -0
- 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/runCommand.js +16 -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 +180 -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
package/dist/core/query.js
CHANGED
|
@@ -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
|
-
|
|
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[];
|
package/dist/core/queryWorker.js
CHANGED
|
@@ -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
|
+
};
|