@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/QueryEngine.js
CHANGED
|
@@ -8,28 +8,27 @@ import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
|
8
8
|
import { SESSIONS_DIR } from '../config/constants.js';
|
|
9
9
|
import { setActiveAgent } from '../config/agentState.js';
|
|
10
10
|
import { clearAuthorizations } from './safeguard.js';
|
|
11
|
+
import { agentUIBus } from './AgentRegistry.js';
|
|
12
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
13
|
+
import { updateUserProfileFromInput } from '../services/userProfile.js';
|
|
11
14
|
export class QueryEngine {
|
|
12
15
|
service;
|
|
13
16
|
session;
|
|
14
17
|
transcript = [];
|
|
15
18
|
workerBridge = new WorkerBridge();
|
|
16
19
|
constructor() {
|
|
17
|
-
|
|
18
|
-
const config = loadConfig();
|
|
19
|
-
const activeModel = getActiveModel(config);
|
|
20
|
-
if (activeModel) {
|
|
21
|
-
try {
|
|
22
|
-
this.service = new LLMServiceImpl();
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
this.service = new MockService();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
this.service = new MockService();
|
|
30
|
-
}
|
|
20
|
+
this.service = this.createService();
|
|
31
21
|
this.session = this.createSession();
|
|
32
22
|
this.ensureSessionDir();
|
|
23
|
+
logInfo('engine.created', {
|
|
24
|
+
sessionId: this.session.id,
|
|
25
|
+
service: this.service.constructor.name,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
29
|
+
registerUIBus(onMessage, onUpdateMessage) {
|
|
30
|
+
agentUIBus.register(onMessage, onUpdateMessage);
|
|
31
|
+
logInfo('engine.ui_bus_registered', { sessionId: this.session.id });
|
|
33
32
|
}
|
|
34
33
|
createSession() {
|
|
35
34
|
return {
|
|
@@ -46,8 +45,26 @@ export class QueryEngine {
|
|
|
46
45
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
47
46
|
}
|
|
48
47
|
}
|
|
48
|
+
createService() {
|
|
49
|
+
const config = loadConfig();
|
|
50
|
+
const activeModel = getActiveModel(config);
|
|
51
|
+
if (activeModel) {
|
|
52
|
+
try {
|
|
53
|
+
return new LLMServiceImpl();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return new MockService();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return new MockService();
|
|
60
|
+
}
|
|
49
61
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
50
62
|
async handleQuery(userInput, callbacks) {
|
|
63
|
+
logInfo('query.received', {
|
|
64
|
+
sessionId: this.session.id,
|
|
65
|
+
inputLength: userInput.length,
|
|
66
|
+
preview: userInput.slice(0, 200),
|
|
67
|
+
});
|
|
51
68
|
const userMsg = {
|
|
52
69
|
id: uuid(),
|
|
53
70
|
type: 'user',
|
|
@@ -57,6 +74,12 @@ export class QueryEngine {
|
|
|
57
74
|
};
|
|
58
75
|
callbacks.onMessage(userMsg);
|
|
59
76
|
this.session.messages.push(userMsg);
|
|
77
|
+
if (this.transcript.length === 0) {
|
|
78
|
+
const updated = await updateUserProfileFromInput(userInput);
|
|
79
|
+
if (updated) {
|
|
80
|
+
this.service = this.createService();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
60
83
|
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
61
84
|
const bridgeCallbacks = {
|
|
62
85
|
onMessage: (msg) => {
|
|
@@ -73,11 +96,23 @@ export class QueryEngine {
|
|
|
73
96
|
onLoopStateChange: callbacks.onLoopStateChange,
|
|
74
97
|
onSessionUpdate: callbacks.onSessionUpdate,
|
|
75
98
|
onConfirmDangerousCommand: callbacks.onConfirmDangerousCommand,
|
|
99
|
+
onSubAgentMessage: (msg) => {
|
|
100
|
+
// SubAgent 消息也存入会话,方便持久化
|
|
101
|
+
this.session.messages.push(msg);
|
|
102
|
+
callbacks.onSubAgentMessage?.(msg);
|
|
103
|
+
},
|
|
104
|
+
onSubAgentUpdateMessage: callbacks.onSubAgentUpdateMessage,
|
|
76
105
|
};
|
|
77
106
|
try {
|
|
78
107
|
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
108
|
+
logInfo('query.completed', {
|
|
109
|
+
sessionId: this.session.id,
|
|
110
|
+
transcriptLength: this.transcript.length,
|
|
111
|
+
totalTokens: this.session.totalTokens,
|
|
112
|
+
});
|
|
79
113
|
}
|
|
80
114
|
catch (err) {
|
|
115
|
+
logError('query.failed', err, { sessionId: this.session.id });
|
|
81
116
|
const errMsg = {
|
|
82
117
|
id: uuid(),
|
|
83
118
|
type: 'error',
|
|
@@ -93,10 +128,12 @@ export class QueryEngine {
|
|
|
93
128
|
}
|
|
94
129
|
/** 终止当前任务(通知 Worker 中断) */
|
|
95
130
|
abort() {
|
|
131
|
+
logWarn('query.abort_requested', { sessionId: this.session.id });
|
|
96
132
|
this.workerBridge.abort();
|
|
97
133
|
}
|
|
98
134
|
/** 重置会话 */
|
|
99
135
|
reset() {
|
|
136
|
+
logInfo('session.reset', { sessionId: this.session.id });
|
|
100
137
|
this.saveSession();
|
|
101
138
|
this.session = this.createSession();
|
|
102
139
|
this.transcript = [];
|
|
@@ -106,25 +143,22 @@ export class QueryEngine {
|
|
|
106
143
|
* 切换智能体:持久化选择 + 重建 LLM service + 重置会话
|
|
107
144
|
*/
|
|
108
145
|
switchAgent(agentName) {
|
|
146
|
+
logInfo('agent.switch.start', {
|
|
147
|
+
fromSessionId: this.session.id,
|
|
148
|
+
agentName,
|
|
149
|
+
});
|
|
109
150
|
setActiveAgent(agentName);
|
|
110
151
|
// 重建 LLM service 以加载新 agent 的 system prompt
|
|
111
|
-
|
|
112
|
-
const activeModel = getActiveModel(config);
|
|
113
|
-
if (activeModel) {
|
|
114
|
-
try {
|
|
115
|
-
this.service = new LLMServiceImpl();
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
this.service = new MockService();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
this.service = new MockService();
|
|
123
|
-
}
|
|
152
|
+
this.service = this.createService();
|
|
124
153
|
// 重置会话上下文
|
|
125
154
|
this.saveSession();
|
|
126
155
|
this.session = this.createSession();
|
|
127
156
|
this.transcript = [];
|
|
157
|
+
logInfo('agent.switch.completed', {
|
|
158
|
+
sessionId: this.session.id,
|
|
159
|
+
agentName,
|
|
160
|
+
service: this.service.constructor.name,
|
|
161
|
+
});
|
|
128
162
|
}
|
|
129
163
|
/** 保存会话到文件 */
|
|
130
164
|
saveSession() {
|
|
@@ -138,8 +172,15 @@ export class QueryEngine {
|
|
|
138
172
|
}
|
|
139
173
|
const filePath = path.join(SESSIONS_DIR, `${this.session.id}.json`);
|
|
140
174
|
fs.writeFileSync(filePath, JSON.stringify(this.session, null, 2), 'utf-8');
|
|
175
|
+
logInfo('session.saved', {
|
|
176
|
+
sessionId: this.session.id,
|
|
177
|
+
filePath,
|
|
178
|
+
messageCount: this.session.messages.length,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
logError('session.save_failed', error, { sessionId: this.session.id });
|
|
141
183
|
}
|
|
142
|
-
catch { /* 静默失败 */ }
|
|
143
184
|
}
|
|
144
185
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
145
186
|
static listSessions() {
|
|
@@ -210,9 +251,15 @@ export class QueryEngine {
|
|
|
210
251
|
});
|
|
211
252
|
}
|
|
212
253
|
}
|
|
254
|
+
logInfo('session.loaded', {
|
|
255
|
+
sessionId,
|
|
256
|
+
messageCount: cleanedMessages.length,
|
|
257
|
+
transcriptLength: this.transcript.length,
|
|
258
|
+
});
|
|
213
259
|
return { session: this.session, messages: cleanedMessages };
|
|
214
260
|
}
|
|
215
|
-
catch {
|
|
261
|
+
catch (error) {
|
|
262
|
+
logError('session.load_failed', error, { sessionId });
|
|
216
263
|
return null;
|
|
217
264
|
}
|
|
218
265
|
}
|
|
@@ -239,9 +286,14 @@ export class QueryEngine {
|
|
|
239
286
|
}
|
|
240
287
|
catch { /* 跳过删除失败的文件 */ }
|
|
241
288
|
}
|
|
289
|
+
logInfo('session.clear_others', {
|
|
290
|
+
sessionId: this.session.id,
|
|
291
|
+
removedCount: count,
|
|
292
|
+
});
|
|
242
293
|
return count;
|
|
243
294
|
}
|
|
244
|
-
catch {
|
|
295
|
+
catch (error) {
|
|
296
|
+
logError('session.clear_others_failed', error, { sessionId: this.session.id });
|
|
245
297
|
return 0;
|
|
246
298
|
}
|
|
247
299
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Message, LoopState, SubAgentTask, SubAgentResult } from '../types/index.js';
|
|
2
|
+
import { DangerConfirmResult } from './query.js';
|
|
3
|
+
export interface SubAgentBridgeCallbacks {
|
|
4
|
+
onMessage: (taskId: string, msg: Message) => void;
|
|
5
|
+
onUpdateMessage: (taskId: string, id: string, updates: Partial<Message>) => void;
|
|
6
|
+
onStreamText: (taskId: string, text: string) => void;
|
|
7
|
+
onLoopStateChange: (taskId: string, state: LoopState) => void;
|
|
8
|
+
/** 危险命令确认,委托给主线程 UI */
|
|
9
|
+
onConfirmDangerousCommand?: (taskId: string, command: string, reason: string, ruleName: string) => Promise<DangerConfirmResult>;
|
|
10
|
+
}
|
|
11
|
+
export declare class SubAgentBridge {
|
|
12
|
+
private worker;
|
|
13
|
+
/**
|
|
14
|
+
* 在独立 Worker 线程中执行 SubAgent 任务
|
|
15
|
+
* @returns SubAgentResult(包含最终输出、消息列表、transcript)
|
|
16
|
+
*/
|
|
17
|
+
run(task: SubAgentTask, callbacks: SubAgentBridgeCallbacks): Promise<SubAgentResult>;
|
|
18
|
+
/** 中断 SubAgent 执行 */
|
|
19
|
+
abort(): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
/** 创建 SubAgent Worker(兼容 tsx 开发模式与编译后 .js) */
|
|
19
|
+
function createSubAgentWorker() {
|
|
20
|
+
const workerTsPath = path.join(__dirname, 'subAgentWorker.ts');
|
|
21
|
+
const isTsx = __filename.endsWith('.ts');
|
|
22
|
+
if (isTsx) {
|
|
23
|
+
const inlineScript = `
|
|
24
|
+
import { tsImport } from 'tsx/esm/api';
|
|
25
|
+
import { workerData } from 'worker_threads';
|
|
26
|
+
import { pathToFileURL } from 'url';
|
|
27
|
+
await tsImport(workerData.__workerFile, pathToFileURL(workerData.__workerFile).href);
|
|
28
|
+
`;
|
|
29
|
+
return new Worker(inlineScript, {
|
|
30
|
+
eval: true,
|
|
31
|
+
workerData: { __workerFile: workerTsPath },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return new Worker(workerTsPath.replace(/\.ts$/, '.js'));
|
|
35
|
+
}
|
|
36
|
+
export class SubAgentBridge {
|
|
37
|
+
worker = null;
|
|
38
|
+
/**
|
|
39
|
+
* 在独立 Worker 线程中执行 SubAgent 任务
|
|
40
|
+
* @returns SubAgentResult(包含最终输出、消息列表、transcript)
|
|
41
|
+
*/
|
|
42
|
+
run(task, callbacks) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const worker = createSubAgentWorker();
|
|
45
|
+
this.worker = worker;
|
|
46
|
+
logInfo('subagent_bridge.run.start', {
|
|
47
|
+
taskId: task.taskId,
|
|
48
|
+
allowedTools: task.allowedTools,
|
|
49
|
+
});
|
|
50
|
+
// 收集 SubAgent 产生的所有消息,用于汇总结果
|
|
51
|
+
const collectedMessages = [];
|
|
52
|
+
let finalTranscript = [];
|
|
53
|
+
worker.on('message', async (msg) => {
|
|
54
|
+
switch (msg.type) {
|
|
55
|
+
case 'message':
|
|
56
|
+
collectedMessages.push(msg.msg);
|
|
57
|
+
callbacks.onMessage(msg.taskId, msg.msg);
|
|
58
|
+
break;
|
|
59
|
+
case 'update_message':
|
|
60
|
+
callbacks.onUpdateMessage(msg.taskId, msg.id, msg.updates);
|
|
61
|
+
break;
|
|
62
|
+
case 'stream_text':
|
|
63
|
+
callbacks.onStreamText(msg.taskId, msg.text);
|
|
64
|
+
break;
|
|
65
|
+
case 'loop_state':
|
|
66
|
+
callbacks.onLoopStateChange(msg.taskId, msg.state);
|
|
67
|
+
break;
|
|
68
|
+
case 'danger_confirm_request': {
|
|
69
|
+
const choice = callbacks.onConfirmDangerousCommand
|
|
70
|
+
? await callbacks.onConfirmDangerousCommand(msg.taskId, msg.command, msg.reason, msg.ruleName)
|
|
71
|
+
: 'cancel';
|
|
72
|
+
const reply = {
|
|
73
|
+
type: 'danger_confirm_result',
|
|
74
|
+
requestId: msg.requestId,
|
|
75
|
+
choice,
|
|
76
|
+
};
|
|
77
|
+
worker.postMessage(reply);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
// ===== MessageBus IPC 代理 =====
|
|
81
|
+
case 'bus_publish': {
|
|
82
|
+
agentMessageBus.publish(msg.from, msg.channel, msg.payload);
|
|
83
|
+
const ack = { type: 'bus_publish_ack', requestId: msg.requestId };
|
|
84
|
+
worker.postMessage(ack);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'bus_subscribe': {
|
|
88
|
+
// 在主线程侧等待消息,结果回传 Worker
|
|
89
|
+
agentMessageBus.subscribe(msg.channel, msg.timeoutMs, msg.fromOffset).then((busMsg) => {
|
|
90
|
+
const reply = {
|
|
91
|
+
type: 'bus_subscribe_result',
|
|
92
|
+
requestId: msg.requestId,
|
|
93
|
+
message: busMsg,
|
|
94
|
+
};
|
|
95
|
+
worker.postMessage(reply);
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case 'bus_read_history': {
|
|
100
|
+
const history = agentMessageBus.getHistory(msg.channel, msg.limit);
|
|
101
|
+
const reply = {
|
|
102
|
+
type: 'bus_read_history_result',
|
|
103
|
+
requestId: msg.requestId,
|
|
104
|
+
messages: history,
|
|
105
|
+
};
|
|
106
|
+
worker.postMessage(reply);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'bus_get_offset': {
|
|
110
|
+
const offset = agentMessageBus.getOffset(msg.channel);
|
|
111
|
+
const reply = {
|
|
112
|
+
type: 'bus_get_offset_result',
|
|
113
|
+
requestId: msg.requestId,
|
|
114
|
+
offset,
|
|
115
|
+
};
|
|
116
|
+
worker.postMessage(reply);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
case 'bus_list_channels': {
|
|
120
|
+
const channels = agentMessageBus.listChannels();
|
|
121
|
+
const reply = {
|
|
122
|
+
type: 'bus_list_channels_result',
|
|
123
|
+
requestId: msg.requestId,
|
|
124
|
+
channels,
|
|
125
|
+
};
|
|
126
|
+
worker.postMessage(reply);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'done':
|
|
130
|
+
finalTranscript = msg.transcript;
|
|
131
|
+
this.worker = null;
|
|
132
|
+
worker.terminate();
|
|
133
|
+
logInfo('subagent_bridge.run.done', {
|
|
134
|
+
taskId: msg.taskId,
|
|
135
|
+
transcriptLength: msg.transcript.length,
|
|
136
|
+
messageCount: collectedMessages.length,
|
|
137
|
+
});
|
|
138
|
+
// 从 transcript 中提取最终输出文本
|
|
139
|
+
resolve({
|
|
140
|
+
taskId: msg.taskId,
|
|
141
|
+
status: 'done',
|
|
142
|
+
output: extractFinalOutput(msg.transcript),
|
|
143
|
+
messages: collectedMessages,
|
|
144
|
+
transcript: finalTranscript,
|
|
145
|
+
});
|
|
146
|
+
break;
|
|
147
|
+
case 'error':
|
|
148
|
+
this.worker = null;
|
|
149
|
+
worker.terminate();
|
|
150
|
+
logError('subagent_bridge.run.failed', msg.message, {
|
|
151
|
+
taskId: msg.taskId,
|
|
152
|
+
messageCount: collectedMessages.length,
|
|
153
|
+
});
|
|
154
|
+
resolve({
|
|
155
|
+
taskId: msg.taskId,
|
|
156
|
+
status: 'error',
|
|
157
|
+
output: '',
|
|
158
|
+
messages: collectedMessages,
|
|
159
|
+
transcript: finalTranscript,
|
|
160
|
+
error: msg.message,
|
|
161
|
+
});
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
worker.on('error', (err) => {
|
|
166
|
+
this.worker = null;
|
|
167
|
+
logError('subagent_bridge.worker_error', err, { taskId: task.taskId });
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
worker.on('exit', (code) => {
|
|
171
|
+
if (this.worker) {
|
|
172
|
+
// Worker 退出但未发送 done/error,说明异常终止
|
|
173
|
+
this.worker = null;
|
|
174
|
+
logError('subagent_bridge.worker_exit_abnormal', undefined, { taskId: task.taskId, code });
|
|
175
|
+
reject(new Error(`SubAgent Worker 意外退出,code=${code}`));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const runMsg = { type: 'run', task };
|
|
179
|
+
worker.postMessage(runMsg);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** 中断 SubAgent 执行 */
|
|
183
|
+
abort() {
|
|
184
|
+
if (this.worker) {
|
|
185
|
+
logWarn('subagent_bridge.abort_forwarded');
|
|
186
|
+
const msg = { type: 'abort' };
|
|
187
|
+
this.worker.postMessage(msg);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 从 transcript 中提取最后一条 assistant 文本作为最终输出 */
|
|
192
|
+
function extractFinalOutput(transcript) {
|
|
193
|
+
for (let i = transcript.length - 1; i >= 0; i--) {
|
|
194
|
+
const msg = transcript[i];
|
|
195
|
+
if (msg.role === 'assistant') {
|
|
196
|
+
if (typeof msg.content === 'string')
|
|
197
|
+
return msg.content;
|
|
198
|
+
// ContentBlock[] — 取最后一个 text block
|
|
199
|
+
const blocks = msg.content;
|
|
200
|
+
for (let j = blocks.length - 1; j >= 0; j--) {
|
|
201
|
+
if (blocks[j].type === 'text' && blocks[j].text) {
|
|
202
|
+
return blocks[j].text;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
@@ -5,6 +5,9 @@
|
|
|
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';
|
|
10
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
8
11
|
// 兼容 ESM __dirname
|
|
9
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -39,6 +42,10 @@ export class WorkerBridge {
|
|
|
39
42
|
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
40
43
|
const worker = createWorker(workerTsPath);
|
|
41
44
|
this.worker = worker;
|
|
45
|
+
logInfo('worker_bridge.run.start', {
|
|
46
|
+
inputLength: userInput.length,
|
|
47
|
+
transcriptLength: transcript.length,
|
|
48
|
+
});
|
|
42
49
|
worker.on('message', async (msg) => {
|
|
43
50
|
switch (msg.type) {
|
|
44
51
|
case 'message':
|
|
@@ -72,25 +79,97 @@ export class WorkerBridge {
|
|
|
72
79
|
worker.postMessage(reply);
|
|
73
80
|
break;
|
|
74
81
|
}
|
|
82
|
+
case 'subagent_message':
|
|
83
|
+
// SubAgent 消息:推送到 UI(已携带 subAgentId)
|
|
84
|
+
callbacks.onSubAgentMessage?.(msg.msg);
|
|
85
|
+
break;
|
|
86
|
+
case 'subagent_update_message':
|
|
87
|
+
callbacks.onSubAgentUpdateMessage?.(msg.id, msg.updates);
|
|
88
|
+
break;
|
|
89
|
+
// ===== MessageBus IPC 代理(queryWorker → 主线程) =====
|
|
90
|
+
case 'bus_publish': {
|
|
91
|
+
agentMessageBus.publish(msg.from, msg.channel, msg.payload);
|
|
92
|
+
const ack = { type: 'bus_publish_ack', requestId: msg.requestId };
|
|
93
|
+
worker.postMessage(ack);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case 'bus_subscribe': {
|
|
97
|
+
agentMessageBus.subscribe(msg.channel, msg.timeoutMs, msg.fromOffset).then((busMsg) => {
|
|
98
|
+
const reply = {
|
|
99
|
+
type: 'bus_subscribe_result',
|
|
100
|
+
requestId: msg.requestId,
|
|
101
|
+
message: busMsg,
|
|
102
|
+
};
|
|
103
|
+
worker.postMessage(reply);
|
|
104
|
+
});
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case 'bus_read_history': {
|
|
108
|
+
const history = agentMessageBus.getHistory(msg.channel, msg.limit);
|
|
109
|
+
const reply = {
|
|
110
|
+
type: 'bus_read_history_result',
|
|
111
|
+
requestId: msg.requestId,
|
|
112
|
+
messages: history,
|
|
113
|
+
};
|
|
114
|
+
worker.postMessage(reply);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case 'bus_get_offset': {
|
|
118
|
+
const offset = agentMessageBus.getOffset(msg.channel);
|
|
119
|
+
const reply = {
|
|
120
|
+
type: 'bus_get_offset_result',
|
|
121
|
+
requestId: msg.requestId,
|
|
122
|
+
offset,
|
|
123
|
+
};
|
|
124
|
+
worker.postMessage(reply);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case 'bus_list_channels': {
|
|
128
|
+
const channels = agentMessageBus.listChannels();
|
|
129
|
+
const reply = {
|
|
130
|
+
type: 'bus_list_channels_result',
|
|
131
|
+
requestId: msg.requestId,
|
|
132
|
+
channels,
|
|
133
|
+
};
|
|
134
|
+
worker.postMessage(reply);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
// ===== spawn_subagent IPC(在主线程创建 SubAgentBridge)=====
|
|
138
|
+
case 'spawn_subagent': {
|
|
139
|
+
const result = spawnSubAgentInMainThread(msg.taskId, msg.instruction, msg.agentLabel, msg.allowedTools);
|
|
140
|
+
const reply = {
|
|
141
|
+
type: 'spawn_subagent_result',
|
|
142
|
+
requestId: msg.requestId,
|
|
143
|
+
result,
|
|
144
|
+
};
|
|
145
|
+
worker.postMessage(reply);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
75
148
|
case 'done':
|
|
76
149
|
this.worker = null;
|
|
77
150
|
worker.terminate();
|
|
151
|
+
logInfo('worker_bridge.run.done', {
|
|
152
|
+
transcriptLength: msg.transcript.length,
|
|
153
|
+
});
|
|
78
154
|
resolve(msg.transcript);
|
|
79
155
|
break;
|
|
80
156
|
case 'error':
|
|
81
157
|
this.worker = null;
|
|
82
158
|
worker.terminate();
|
|
159
|
+
logError('worker_bridge.run.error', msg.message);
|
|
83
160
|
reject(new Error(msg.message));
|
|
84
161
|
break;
|
|
85
162
|
}
|
|
86
163
|
});
|
|
87
164
|
worker.on('error', (err) => {
|
|
88
165
|
this.worker = null;
|
|
166
|
+
logError('worker_bridge.worker_error', err);
|
|
89
167
|
reject(err);
|
|
90
168
|
});
|
|
91
169
|
worker.on('exit', (code) => {
|
|
92
170
|
if (code !== 0 && this.worker) {
|
|
93
171
|
this.worker = null;
|
|
172
|
+
logError('worker_bridge.worker_exit_abnormal', undefined, { code });
|
|
94
173
|
reject(new Error(`Worker 异常退出,code=${code}`));
|
|
95
174
|
}
|
|
96
175
|
});
|
|
@@ -102,6 +181,7 @@ export class WorkerBridge {
|
|
|
102
181
|
/** 向 Worker 发送中断信号 */
|
|
103
182
|
abort() {
|
|
104
183
|
if (this.worker) {
|
|
184
|
+
logWarn('worker_bridge.abort_forwarded');
|
|
105
185
|
const msg = { type: 'abort' };
|
|
106
186
|
this.worker.postMessage(msg);
|
|
107
187
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type LogLevel = 'INFO' | 'WARN' | 'ERROR';
|
|
2
|
+
export interface LogPayload {
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare function ensureLoggerReady(): string;
|
|
6
|
+
export declare function logInfo(event: string, payload?: LogPayload): void;
|
|
7
|
+
export declare function logWarn(event: string, payload?: LogPayload): void;
|
|
8
|
+
export declare function logError(event: string, error?: unknown, payload?: LogPayload): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { isMainThread, threadId } from 'worker_threads';
|
|
5
|
+
function resolveLogsDir() {
|
|
6
|
+
return path.join(os.homedir(), '.jarvis', 'logs');
|
|
7
|
+
}
|
|
8
|
+
function ensureLogsDir() {
|
|
9
|
+
const logsDir = resolveLogsDir();
|
|
10
|
+
if (!fs.existsSync(logsDir)) {
|
|
11
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
return logsDir;
|
|
14
|
+
}
|
|
15
|
+
function getLogFilePath(date = new Date()) {
|
|
16
|
+
const logsDir = ensureLogsDir();
|
|
17
|
+
const day = date.toISOString().slice(0, 10);
|
|
18
|
+
return path.join(logsDir, `${day}.log`);
|
|
19
|
+
}
|
|
20
|
+
function normalizeError(error) {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
return {
|
|
23
|
+
name: error.name,
|
|
24
|
+
message: error.message,
|
|
25
|
+
stack: error.stack,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return { message: String(error) };
|
|
29
|
+
}
|
|
30
|
+
function writeLog(level, event, payload = {}) {
|
|
31
|
+
try {
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const line = JSON.stringify({
|
|
34
|
+
timestamp: now.toISOString(),
|
|
35
|
+
level,
|
|
36
|
+
event,
|
|
37
|
+
pid: process.pid,
|
|
38
|
+
threadId,
|
|
39
|
+
workerType: isMainThread ? 'main' : 'worker',
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
...payload,
|
|
42
|
+
});
|
|
43
|
+
fs.appendFileSync(getLogFilePath(now), `${line}\n`, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// 日志系统自身不能影响主流程
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function ensureLoggerReady() {
|
|
50
|
+
return ensureLogsDir();
|
|
51
|
+
}
|
|
52
|
+
export function logInfo(event, payload) {
|
|
53
|
+
writeLog('INFO', event, payload);
|
|
54
|
+
}
|
|
55
|
+
export function logWarn(event, payload) {
|
|
56
|
+
writeLog('WARN', event, payload);
|
|
57
|
+
}
|
|
58
|
+
export function logError(event, error, payload = {}) {
|
|
59
|
+
writeLog('ERROR', event, {
|
|
60
|
+
...payload,
|
|
61
|
+
...(error !== undefined ? { error: normalizeError(error) } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
package/dist/core/query.d.ts
CHANGED
|
@@ -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:推理 → 工具调用 → 循环
|