@code4bug/jarvis-agent 1.1.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 +4 -0
- package/dist/cli.js +13 -0
- 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 +0 -13
- package/dist/core/AgentMessageBus.js +21 -0
- package/dist/core/QueryEngine.d.ts +1 -0
- package/dist/core/QueryEngine.js +71 -30
- package/dist/core/SubAgentBridge.js +17 -0
- package/dist/core/WorkerBridge.js +12 -0
- package/dist/core/logger.d.ts +8 -0
- package/dist/core/logger.js +63 -0
- package/dist/core/query.js +80 -1
- package/dist/core/queryWorker.js +11 -0
- package/dist/core/subAgentWorker.js +14 -0
- package/dist/index.js +2 -0
- package/dist/screens/repl.js +14 -0
- package/dist/services/api/llm.js +10 -2
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +127 -0
- package/dist/tools/runCommand.js +16 -0
- package/dist/tools/spawnAgent.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { APP_VERSION } from './config/constants.js';
|
|
3
|
+
import { ensureLoggerReady, logError, logInfo } from './core/logger.js';
|
|
3
4
|
import { startJarvis } from './index.js';
|
|
4
5
|
const arg = process.argv[2];
|
|
6
|
+
ensureLoggerReady();
|
|
7
|
+
logInfo('cli.launch', {
|
|
8
|
+
argv: process.argv.slice(2),
|
|
9
|
+
version: APP_VERSION,
|
|
10
|
+
});
|
|
5
11
|
if (arg === '--version' || arg === '-v' || arg === 'version') {
|
|
12
|
+
logInfo('cli.version', { version: APP_VERSION });
|
|
6
13
|
console.log(APP_VERSION);
|
|
7
14
|
process.exit(0);
|
|
8
15
|
}
|
|
16
|
+
process.on('uncaughtException', (error) => {
|
|
17
|
+
logError('process.uncaught_exception', error);
|
|
18
|
+
});
|
|
19
|
+
process.on('unhandledRejection', (reason) => {
|
|
20
|
+
logError('process.unhandled_rejection', reason);
|
|
21
|
+
});
|
|
9
22
|
startJarvis();
|
|
@@ -3,7 +3,9 @@ export declare const APP_VERSION: string;
|
|
|
3
3
|
export declare const PROJECT_NAME: string;
|
|
4
4
|
/** Agentic Loop 最大迭代次数 */
|
|
5
5
|
export declare const MAX_ITERATIONS = 50;
|
|
6
|
+
export declare const JARVIS_HOME_DIR: string;
|
|
6
7
|
export declare const SESSIONS_DIR: string;
|
|
8
|
+
export declare const LOGS_DIR: string;
|
|
7
9
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
8
10
|
export declare const HIDE_WELCOME_AFTER_INPUT = false;
|
|
9
11
|
export declare const MODEL_NAME: string;
|
package/dist/config/constants.js
CHANGED
|
@@ -20,7 +20,9 @@ export const PROJECT_NAME = path.basename(process.cwd());
|
|
|
20
20
|
export const MAX_ITERATIONS = 50;
|
|
21
21
|
/** 会话存储目录(~/.jarvis/sessions/) */
|
|
22
22
|
import os from 'os';
|
|
23
|
-
export const
|
|
23
|
+
export const JARVIS_HOME_DIR = path.join(os.homedir(), '.jarvis');
|
|
24
|
+
export const SESSIONS_DIR = path.join(JARVIS_HOME_DIR, 'sessions');
|
|
25
|
+
export const LOGS_DIR = path.join(JARVIS_HOME_DIR, 'logs');
|
|
24
26
|
/** 输入后是否隐藏 WelcomeHeader,默认 false(不隐藏) */
|
|
25
27
|
export const HIDE_WELCOME_AFTER_INPUT = false;
|
|
26
28
|
/** 从配置文件获取当前模型名称 */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export const USER_PROFILE_PATH = path.join(os.homedir(), '.jarvis', 'USER.md');
|
|
5
|
+
export function ensureJarvisHomeDir() {
|
|
6
|
+
const dir = path.dirname(USER_PROFILE_PATH);
|
|
7
|
+
if (!fs.existsSync(dir)) {
|
|
8
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
export function readUserProfile() {
|
|
13
|
+
try {
|
|
14
|
+
if (!fs.existsSync(USER_PROFILE_PATH))
|
|
15
|
+
return '';
|
|
16
|
+
return fs.readFileSync(USER_PROFILE_PATH, 'utf-8').trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function writeUserProfile(content) {
|
|
23
|
+
ensureJarvisHomeDir();
|
|
24
|
+
fs.writeFileSync(USER_PROFILE_PATH, content.trimEnd() + '\n', 'utf-8');
|
|
25
|
+
}
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentMessageBus — SubAgent 间通讯总线(进程内单例)
|
|
3
|
-
*
|
|
4
|
-
* 提供发布/订阅机制,允许:
|
|
5
|
-
* - SubAgent 向命名频道发布消息
|
|
6
|
-
* - SubAgent 订阅频道,等待其他 Agent 发布的消息
|
|
7
|
-
* - 主 Agent 观察所有频道历史
|
|
8
|
-
*
|
|
9
|
-
* 线程安全说明:
|
|
10
|
-
* Node.js Worker 线程之间不共享内存,因此 MessageBus 运行在
|
|
11
|
-
* 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
|
|
12
|
-
* SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
|
|
13
|
-
*/
|
|
14
1
|
export interface BusMessage {
|
|
15
2
|
/** 发布者 Agent 标识 */
|
|
16
3
|
from: string;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* 主线程(queryWorker)中,SubAgent Worker 通过 IPC 消息与其交互。
|
|
12
12
|
* SubAgentBridge 负责在主线程侧代理 publish/subscribe 请求。
|
|
13
13
|
*/
|
|
14
|
+
import { logInfo } from './logger.js';
|
|
14
15
|
class AgentMessageBus {
|
|
15
16
|
/** 频道历史消息,key = channel */
|
|
16
17
|
history = new Map();
|
|
@@ -24,6 +25,11 @@ class AgentMessageBus {
|
|
|
24
25
|
*/
|
|
25
26
|
publish(from, channel, payload) {
|
|
26
27
|
const msg = { from, channel, payload, timestamp: Date.now() };
|
|
28
|
+
logInfo('bus.publish', {
|
|
29
|
+
from,
|
|
30
|
+
channel,
|
|
31
|
+
payloadLength: payload.length,
|
|
32
|
+
});
|
|
27
33
|
// 存入历史
|
|
28
34
|
if (!this.history.has(channel))
|
|
29
35
|
this.history.set(channel, []);
|
|
@@ -48,10 +54,19 @@ class AgentMessageBus {
|
|
|
48
54
|
* @param fromOffset 从第几条开始消费(0-based),不传则只等新消息
|
|
49
55
|
*/
|
|
50
56
|
subscribe(channel, timeoutMs = 30_000, fromOffset) {
|
|
57
|
+
logInfo('bus.subscribe', {
|
|
58
|
+
channel,
|
|
59
|
+
timeoutMs,
|
|
60
|
+
fromOffset,
|
|
61
|
+
});
|
|
51
62
|
// 如果指定了 offset 且历史中已有该位置之后的消息,立即返回
|
|
52
63
|
if (fromOffset !== undefined) {
|
|
53
64
|
const history = this.history.get(channel) ?? [];
|
|
54
65
|
if (fromOffset < history.length) {
|
|
66
|
+
logInfo('bus.subscribe.hit_history', {
|
|
67
|
+
channel,
|
|
68
|
+
fromOffset,
|
|
69
|
+
});
|
|
55
70
|
return Promise.resolve(history[fromOffset]);
|
|
56
71
|
}
|
|
57
72
|
}
|
|
@@ -62,10 +77,16 @@ class AgentMessageBus {
|
|
|
62
77
|
if (list) {
|
|
63
78
|
this.waiters.set(channel, list.filter((w) => w.resolve !== resolve));
|
|
64
79
|
}
|
|
80
|
+
logInfo('bus.subscribe.timeout', { channel, timeoutMs, fromOffset });
|
|
65
81
|
resolve(null);
|
|
66
82
|
}, timeoutMs);
|
|
67
83
|
const wrappedResolve = (msg) => {
|
|
68
84
|
clearTimeout(timer);
|
|
85
|
+
logInfo('bus.subscribe.received', {
|
|
86
|
+
channel,
|
|
87
|
+
fromOffset,
|
|
88
|
+
from: msg.from,
|
|
89
|
+
});
|
|
69
90
|
resolve(msg);
|
|
70
91
|
};
|
|
71
92
|
if (!this.waiters.has(channel))
|
|
@@ -25,6 +25,7 @@ export declare class QueryEngine {
|
|
|
25
25
|
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
26
26
|
private createSession;
|
|
27
27
|
private ensureSessionDir;
|
|
28
|
+
private createService;
|
|
28
29
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
29
30
|
handleQuery(userInput: string, callbacks: EngineCallbacks): Promise<void>;
|
|
30
31
|
/** 终止当前任务(通知 Worker 中断) */
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -9,32 +9,26 @@ import { SESSIONS_DIR } from '../config/constants.js';
|
|
|
9
9
|
import { setActiveAgent } from '../config/agentState.js';
|
|
10
10
|
import { clearAuthorizations } from './safeguard.js';
|
|
11
11
|
import { agentUIBus } from './AgentRegistry.js';
|
|
12
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
13
|
+
import { updateUserProfileFromInput } from '../services/userProfile.js';
|
|
12
14
|
export class QueryEngine {
|
|
13
15
|
service;
|
|
14
16
|
session;
|
|
15
17
|
transcript = [];
|
|
16
18
|
workerBridge = new WorkerBridge();
|
|
17
19
|
constructor() {
|
|
18
|
-
|
|
19
|
-
const config = loadConfig();
|
|
20
|
-
const activeModel = getActiveModel(config);
|
|
21
|
-
if (activeModel) {
|
|
22
|
-
try {
|
|
23
|
-
this.service = new LLMServiceImpl();
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
this.service = new MockService();
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
this.service = new MockService();
|
|
31
|
-
}
|
|
20
|
+
this.service = this.createService();
|
|
32
21
|
this.session = this.createSession();
|
|
33
22
|
this.ensureSessionDir();
|
|
23
|
+
logInfo('engine.created', {
|
|
24
|
+
sessionId: this.session.id,
|
|
25
|
+
service: this.service.constructor.name,
|
|
26
|
+
});
|
|
34
27
|
}
|
|
35
28
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
36
29
|
registerUIBus(onMessage, onUpdateMessage) {
|
|
37
30
|
agentUIBus.register(onMessage, onUpdateMessage);
|
|
31
|
+
logInfo('engine.ui_bus_registered', { sessionId: this.session.id });
|
|
38
32
|
}
|
|
39
33
|
createSession() {
|
|
40
34
|
return {
|
|
@@ -51,8 +45,26 @@ export class QueryEngine {
|
|
|
51
45
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
52
46
|
}
|
|
53
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
|
+
}
|
|
54
61
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
55
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
|
+
});
|
|
56
68
|
const userMsg = {
|
|
57
69
|
id: uuid(),
|
|
58
70
|
type: 'user',
|
|
@@ -62,6 +74,12 @@ export class QueryEngine {
|
|
|
62
74
|
};
|
|
63
75
|
callbacks.onMessage(userMsg);
|
|
64
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
|
+
}
|
|
65
83
|
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
66
84
|
const bridgeCallbacks = {
|
|
67
85
|
onMessage: (msg) => {
|
|
@@ -87,8 +105,14 @@ export class QueryEngine {
|
|
|
87
105
|
};
|
|
88
106
|
try {
|
|
89
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
|
+
});
|
|
90
113
|
}
|
|
91
114
|
catch (err) {
|
|
115
|
+
logError('query.failed', err, { sessionId: this.session.id });
|
|
92
116
|
const errMsg = {
|
|
93
117
|
id: uuid(),
|
|
94
118
|
type: 'error',
|
|
@@ -104,10 +128,12 @@ export class QueryEngine {
|
|
|
104
128
|
}
|
|
105
129
|
/** 终止当前任务(通知 Worker 中断) */
|
|
106
130
|
abort() {
|
|
131
|
+
logWarn('query.abort_requested', { sessionId: this.session.id });
|
|
107
132
|
this.workerBridge.abort();
|
|
108
133
|
}
|
|
109
134
|
/** 重置会话 */
|
|
110
135
|
reset() {
|
|
136
|
+
logInfo('session.reset', { sessionId: this.session.id });
|
|
111
137
|
this.saveSession();
|
|
112
138
|
this.session = this.createSession();
|
|
113
139
|
this.transcript = [];
|
|
@@ -117,25 +143,22 @@ export class QueryEngine {
|
|
|
117
143
|
* 切换智能体:持久化选择 + 重建 LLM service + 重置会话
|
|
118
144
|
*/
|
|
119
145
|
switchAgent(agentName) {
|
|
146
|
+
logInfo('agent.switch.start', {
|
|
147
|
+
fromSessionId: this.session.id,
|
|
148
|
+
agentName,
|
|
149
|
+
});
|
|
120
150
|
setActiveAgent(agentName);
|
|
121
151
|
// 重建 LLM service 以加载新 agent 的 system prompt
|
|
122
|
-
|
|
123
|
-
const activeModel = getActiveModel(config);
|
|
124
|
-
if (activeModel) {
|
|
125
|
-
try {
|
|
126
|
-
this.service = new LLMServiceImpl();
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
this.service = new MockService();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
this.service = new MockService();
|
|
134
|
-
}
|
|
152
|
+
this.service = this.createService();
|
|
135
153
|
// 重置会话上下文
|
|
136
154
|
this.saveSession();
|
|
137
155
|
this.session = this.createSession();
|
|
138
156
|
this.transcript = [];
|
|
157
|
+
logInfo('agent.switch.completed', {
|
|
158
|
+
sessionId: this.session.id,
|
|
159
|
+
agentName,
|
|
160
|
+
service: this.service.constructor.name,
|
|
161
|
+
});
|
|
139
162
|
}
|
|
140
163
|
/** 保存会话到文件 */
|
|
141
164
|
saveSession() {
|
|
@@ -149,8 +172,15 @@ export class QueryEngine {
|
|
|
149
172
|
}
|
|
150
173
|
const filePath = path.join(SESSIONS_DIR, `${this.session.id}.json`);
|
|
151
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 });
|
|
152
183
|
}
|
|
153
|
-
catch { /* 静默失败 */ }
|
|
154
184
|
}
|
|
155
185
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
156
186
|
static listSessions() {
|
|
@@ -221,9 +251,15 @@ export class QueryEngine {
|
|
|
221
251
|
});
|
|
222
252
|
}
|
|
223
253
|
}
|
|
254
|
+
logInfo('session.loaded', {
|
|
255
|
+
sessionId,
|
|
256
|
+
messageCount: cleanedMessages.length,
|
|
257
|
+
transcriptLength: this.transcript.length,
|
|
258
|
+
});
|
|
224
259
|
return { session: this.session, messages: cleanedMessages };
|
|
225
260
|
}
|
|
226
|
-
catch {
|
|
261
|
+
catch (error) {
|
|
262
|
+
logError('session.load_failed', error, { sessionId });
|
|
227
263
|
return null;
|
|
228
264
|
}
|
|
229
265
|
}
|
|
@@ -250,9 +286,14 @@ export class QueryEngine {
|
|
|
250
286
|
}
|
|
251
287
|
catch { /* 跳过删除失败的文件 */ }
|
|
252
288
|
}
|
|
289
|
+
logInfo('session.clear_others', {
|
|
290
|
+
sessionId: this.session.id,
|
|
291
|
+
removedCount: count,
|
|
292
|
+
});
|
|
253
293
|
return count;
|
|
254
294
|
}
|
|
255
|
-
catch {
|
|
295
|
+
catch (error) {
|
|
296
|
+
logError('session.clear_others_failed', error, { sessionId: this.session.id });
|
|
256
297
|
return 0;
|
|
257
298
|
}
|
|
258
299
|
}
|
|
@@ -12,6 +12,7 @@ import { Worker } from 'worker_threads';
|
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { agentMessageBus } from './AgentMessageBus.js';
|
|
15
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
17
18
|
/** 创建 SubAgent Worker(兼容 tsx 开发模式与编译后 .js) */
|
|
@@ -42,6 +43,10 @@ export class SubAgentBridge {
|
|
|
42
43
|
return new Promise((resolve, reject) => {
|
|
43
44
|
const worker = createSubAgentWorker();
|
|
44
45
|
this.worker = worker;
|
|
46
|
+
logInfo('subagent_bridge.run.start', {
|
|
47
|
+
taskId: task.taskId,
|
|
48
|
+
allowedTools: task.allowedTools,
|
|
49
|
+
});
|
|
45
50
|
// 收集 SubAgent 产生的所有消息,用于汇总结果
|
|
46
51
|
const collectedMessages = [];
|
|
47
52
|
let finalTranscript = [];
|
|
@@ -125,6 +130,11 @@ export class SubAgentBridge {
|
|
|
125
130
|
finalTranscript = msg.transcript;
|
|
126
131
|
this.worker = null;
|
|
127
132
|
worker.terminate();
|
|
133
|
+
logInfo('subagent_bridge.run.done', {
|
|
134
|
+
taskId: msg.taskId,
|
|
135
|
+
transcriptLength: msg.transcript.length,
|
|
136
|
+
messageCount: collectedMessages.length,
|
|
137
|
+
});
|
|
128
138
|
// 从 transcript 中提取最终输出文本
|
|
129
139
|
resolve({
|
|
130
140
|
taskId: msg.taskId,
|
|
@@ -137,6 +147,10 @@ export class SubAgentBridge {
|
|
|
137
147
|
case 'error':
|
|
138
148
|
this.worker = null;
|
|
139
149
|
worker.terminate();
|
|
150
|
+
logError('subagent_bridge.run.failed', msg.message, {
|
|
151
|
+
taskId: msg.taskId,
|
|
152
|
+
messageCount: collectedMessages.length,
|
|
153
|
+
});
|
|
140
154
|
resolve({
|
|
141
155
|
taskId: msg.taskId,
|
|
142
156
|
status: 'error',
|
|
@@ -150,12 +164,14 @@ export class SubAgentBridge {
|
|
|
150
164
|
});
|
|
151
165
|
worker.on('error', (err) => {
|
|
152
166
|
this.worker = null;
|
|
167
|
+
logError('subagent_bridge.worker_error', err, { taskId: task.taskId });
|
|
153
168
|
reject(err);
|
|
154
169
|
});
|
|
155
170
|
worker.on('exit', (code) => {
|
|
156
171
|
if (this.worker) {
|
|
157
172
|
// Worker 退出但未发送 done/error,说明异常终止
|
|
158
173
|
this.worker = null;
|
|
174
|
+
logError('subagent_bridge.worker_exit_abnormal', undefined, { taskId: task.taskId, code });
|
|
159
175
|
reject(new Error(`SubAgent Worker 意外退出,code=${code}`));
|
|
160
176
|
}
|
|
161
177
|
});
|
|
@@ -166,6 +182,7 @@ export class SubAgentBridge {
|
|
|
166
182
|
/** 中断 SubAgent 执行 */
|
|
167
183
|
abort() {
|
|
168
184
|
if (this.worker) {
|
|
185
|
+
logWarn('subagent_bridge.abort_forwarded');
|
|
169
186
|
const msg = { type: 'abort' };
|
|
170
187
|
this.worker.postMessage(msg);
|
|
171
188
|
}
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from 'url';
|
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { agentMessageBus } from './AgentMessageBus.js';
|
|
9
9
|
import { spawnSubAgentInMainThread } from '../tools/spawnAgent.js';
|
|
10
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
10
11
|
// 兼容 ESM __dirname
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
@@ -41,6 +42,10 @@ export class WorkerBridge {
|
|
|
41
42
|
const workerTsPath = path.join(__dirname, 'queryWorker.ts');
|
|
42
43
|
const worker = createWorker(workerTsPath);
|
|
43
44
|
this.worker = worker;
|
|
45
|
+
logInfo('worker_bridge.run.start', {
|
|
46
|
+
inputLength: userInput.length,
|
|
47
|
+
transcriptLength: transcript.length,
|
|
48
|
+
});
|
|
44
49
|
worker.on('message', async (msg) => {
|
|
45
50
|
switch (msg.type) {
|
|
46
51
|
case 'message':
|
|
@@ -143,22 +148,28 @@ export class WorkerBridge {
|
|
|
143
148
|
case 'done':
|
|
144
149
|
this.worker = null;
|
|
145
150
|
worker.terminate();
|
|
151
|
+
logInfo('worker_bridge.run.done', {
|
|
152
|
+
transcriptLength: msg.transcript.length,
|
|
153
|
+
});
|
|
146
154
|
resolve(msg.transcript);
|
|
147
155
|
break;
|
|
148
156
|
case 'error':
|
|
149
157
|
this.worker = null;
|
|
150
158
|
worker.terminate();
|
|
159
|
+
logError('worker_bridge.run.error', msg.message);
|
|
151
160
|
reject(new Error(msg.message));
|
|
152
161
|
break;
|
|
153
162
|
}
|
|
154
163
|
});
|
|
155
164
|
worker.on('error', (err) => {
|
|
156
165
|
this.worker = null;
|
|
166
|
+
logError('worker_bridge.worker_error', err);
|
|
157
167
|
reject(err);
|
|
158
168
|
});
|
|
159
169
|
worker.on('exit', (code) => {
|
|
160
170
|
if (code !== 0 && this.worker) {
|
|
161
171
|
this.worker = null;
|
|
172
|
+
logError('worker_bridge.worker_exit_abnormal', undefined, { code });
|
|
162
173
|
reject(new Error(`Worker 异常退出,code=${code}`));
|
|
163
174
|
}
|
|
164
175
|
});
|
|
@@ -170,6 +181,7 @@ export class WorkerBridge {
|
|
|
170
181
|
/** 向 Worker 发送中断信号 */
|
|
171
182
|
abort() {
|
|
172
183
|
if (this.worker) {
|
|
184
|
+
logWarn('worker_bridge.abort_forwarded');
|
|
173
185
|
const msg = { type: 'abort' };
|
|
174
186
|
this.worker.postMessage(msg);
|
|
175
187
|
}
|
|
@@ -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.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
import { findToolMerged as findTool } from '../tools/index.js';
|
|
6
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);
|
|
@@ -74,6 +75,10 @@ function compressTranscript(transcript) {
|
|
|
74
75
|
* 单轮 Agentic Loop:推理 → 工具调用 → 循环
|
|
75
76
|
*/
|
|
76
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
|
+
});
|
|
77
82
|
const localTranscript = [...transcript];
|
|
78
83
|
localTranscript.push({ role: 'user', content: userInput });
|
|
79
84
|
const loopState = {
|
|
@@ -85,8 +90,19 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
85
90
|
callbacks.onLoopStateChange({ ...loopState });
|
|
86
91
|
while (loopState.iteration < MAX_ITERATIONS && !abortSignal.aborted) {
|
|
87
92
|
loopState.iteration++;
|
|
93
|
+
logInfo('agent_loop.iteration.start', {
|
|
94
|
+
iteration: loopState.iteration,
|
|
95
|
+
transcriptLength: localTranscript.length,
|
|
96
|
+
});
|
|
88
97
|
callbacks.onLoopStateChange({ ...loopState });
|
|
89
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
|
+
});
|
|
90
106
|
// 构建 assistant transcript 块
|
|
91
107
|
const assistantBlocks = [];
|
|
92
108
|
if (result.text)
|
|
@@ -102,6 +118,10 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
102
118
|
break;
|
|
103
119
|
// 中断发生在推理阶段
|
|
104
120
|
if (abortSignal.aborted) {
|
|
121
|
+
logWarn('agent_loop.aborted_before_tool_execution', {
|
|
122
|
+
iteration: loopState.iteration,
|
|
123
|
+
toolCallCount: result.toolCalls.length,
|
|
124
|
+
});
|
|
105
125
|
for (const tc of result.toolCalls) {
|
|
106
126
|
const skippedResult = `[用户中断] 工具 ${tc.name} 未执行(用户按下 ESC 中断)`;
|
|
107
127
|
localTranscript.push({ role: 'tool_result', toolUseId: tc.id, content: skippedResult });
|
|
@@ -145,11 +165,20 @@ export async function executeQuery(userInput, transcript, _tools, service, callb
|
|
|
145
165
|
loopState.aborted = abortSignal.aborted;
|
|
146
166
|
callbacks.onLoopStateChange({ ...loopState });
|
|
147
167
|
if (abortSignal.aborted) {
|
|
168
|
+
logWarn('agent_loop.aborted', {
|
|
169
|
+
finalIteration: loopState.iteration,
|
|
170
|
+
transcriptLength: localTranscript.length,
|
|
171
|
+
});
|
|
148
172
|
localTranscript.push({
|
|
149
173
|
role: 'user',
|
|
150
174
|
content: '[系统提示] 用户中断了上一轮回复(按下 ESC)。上一条助手消息可能不完整,请在后续回复中注意这一点。',
|
|
151
175
|
});
|
|
152
176
|
}
|
|
177
|
+
logInfo('agent_loop.done', {
|
|
178
|
+
finalIteration: loopState.iteration,
|
|
179
|
+
aborted: abortSignal.aborted,
|
|
180
|
+
transcriptLength: localTranscript.length,
|
|
181
|
+
});
|
|
153
182
|
return localTranscript;
|
|
154
183
|
}
|
|
155
184
|
/** 执行一次 LLM 调用 */
|
|
@@ -161,6 +190,10 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
161
190
|
let tokenCount = 0;
|
|
162
191
|
let firstTokenTime = null;
|
|
163
192
|
const thinkingId = uuid();
|
|
193
|
+
logInfo('llm.iteration.requested', {
|
|
194
|
+
transcriptLength: transcript.length,
|
|
195
|
+
toolCount: tools.length,
|
|
196
|
+
});
|
|
164
197
|
callbacks.onMessage({
|
|
165
198
|
id: thinkingId,
|
|
166
199
|
type: 'thinking',
|
|
@@ -268,6 +301,15 @@ async function runOneIteration(transcript, tools, service, callbacks, abortSigna
|
|
|
268
301
|
});
|
|
269
302
|
}
|
|
270
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
|
+
});
|
|
271
313
|
return { text: effectiveText, toolCalls, duration, tokenCount, firstTokenLatency, tokensPerSecond };
|
|
272
314
|
}
|
|
273
315
|
// ===== 并行执行判断 =====
|
|
@@ -358,17 +400,27 @@ export async function runToolDirect(tc, abortSignal) {
|
|
|
358
400
|
if (!tool)
|
|
359
401
|
return `错误: 未知工具 ${tc.name}`;
|
|
360
402
|
try {
|
|
403
|
+
logInfo('tool.direct.start', { toolName: tc.name, toolArgs: tc.input });
|
|
361
404
|
const result = await tool.execute(tc.input, abortSignal);
|
|
362
405
|
const { sanitizeOutput } = await import('./safeguard.js');
|
|
406
|
+
logInfo('tool.direct.done', {
|
|
407
|
+
toolName: tc.name,
|
|
408
|
+
resultLength: String(result).length,
|
|
409
|
+
});
|
|
363
410
|
return sanitizeOutput(result);
|
|
364
411
|
}
|
|
365
412
|
catch (err) {
|
|
413
|
+
logError('tool.direct.failed', err, { toolName: tc.name, toolArgs: tc.input });
|
|
366
414
|
return `错误: ${err.message || '工具执行失败'}`;
|
|
367
415
|
}
|
|
368
416
|
}
|
|
369
417
|
/** 并行执行多个工具,每个工具在独立 Worker 线程中运行,实时更新 UI */
|
|
370
418
|
async function executeToolsInParallel(calls, callbacks, abortSignal) {
|
|
371
419
|
const groupId = uuid();
|
|
420
|
+
logInfo('tool.parallel_group.start', {
|
|
421
|
+
groupId,
|
|
422
|
+
toolNames: calls.map((call) => call.name),
|
|
423
|
+
});
|
|
372
424
|
// 为每个工具预先创建 pending 消息节点(TUI 立即渲染占位)
|
|
373
425
|
const msgIds = calls.map((tc) => {
|
|
374
426
|
const msgId = uuid();
|
|
@@ -433,11 +485,18 @@ async function executeToolsInParallel(calls, callbacks, abortSignal) {
|
|
|
433
485
|
}
|
|
434
486
|
catch (err) {
|
|
435
487
|
const errMsg = err.message || '工具执行失败';
|
|
488
|
+
logError('tool.parallel.failed', err, { groupId, toolName: tc.name, toolArgs: tc.input });
|
|
436
489
|
callbacks.onUpdateMessage(msgId, { status: 'error', content: errMsg, toolResult: errMsg });
|
|
437
490
|
return { tc, content: `错误: ${errMsg}`, isError: false };
|
|
438
491
|
}
|
|
439
492
|
});
|
|
440
|
-
|
|
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;
|
|
441
500
|
}
|
|
442
501
|
/** 执行工具并返回结果 */
|
|
443
502
|
async function executeTool(tc, callbacks, abortSignal) {
|
|
@@ -462,6 +521,11 @@ async function executeTool(tc, callbacks, abortSignal) {
|
|
|
462
521
|
toolName: tc.name,
|
|
463
522
|
toolArgs: tc.input,
|
|
464
523
|
});
|
|
524
|
+
logInfo('tool.execute.start', {
|
|
525
|
+
toolName: tc.name,
|
|
526
|
+
toolArgs: tc.input,
|
|
527
|
+
toolExecId,
|
|
528
|
+
});
|
|
465
529
|
// ===== 安全围栏:Bash 命令拦截 + 交互式确认 =====
|
|
466
530
|
if (tc.name === 'Bash' && tc.input.command) {
|
|
467
531
|
const command = tc.input.command;
|
|
@@ -470,6 +534,7 @@ async function executeTool(tc, callbacks, abortSignal) {
|
|
|
470
534
|
if (!check.canOverride) {
|
|
471
535
|
// critical 级别:直接禁止
|
|
472
536
|
const errMsg = `${check.reason}\n🚫 该命令已被永久禁止,无法通过授权绕过。\n命令: ${command}`;
|
|
537
|
+
logWarn('tool.execute.blocked', { toolName: tc.name, command, reason: check.reason });
|
|
473
538
|
callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg, toolResult: errMsg });
|
|
474
539
|
return { content: `错误: ${errMsg}`, isError: true };
|
|
475
540
|
}
|
|
@@ -480,6 +545,7 @@ async function executeTool(tc, callbacks, abortSignal) {
|
|
|
480
545
|
const userChoice = await callbacks.onConfirmDangerousCommand(command, reason, ruleName);
|
|
481
546
|
if (userChoice === 'cancel') {
|
|
482
547
|
const cancelMsg = `⛔ 用户取消执行危险命令: ${command}`;
|
|
548
|
+
logWarn('tool.execute.cancelled_by_user', { toolName: tc.name, command, reason });
|
|
483
549
|
callbacks.onUpdateMessage(toolExecId, { status: 'error', content: cancelMsg, toolResult: cancelMsg });
|
|
484
550
|
return { content: cancelMsg, isError: true };
|
|
485
551
|
}
|
|
@@ -502,6 +568,7 @@ async function executeTool(tc, callbacks, abortSignal) {
|
|
|
502
568
|
const tool = findTool(tc.name);
|
|
503
569
|
if (!tool) {
|
|
504
570
|
const errMsg = `未知工具: ${tc.name}`;
|
|
571
|
+
logWarn('tool.execute.unknown', { toolName: tc.name });
|
|
505
572
|
callbacks.onUpdateMessage(toolExecId, { status: 'error', content: errMsg });
|
|
506
573
|
return { content: errMsg, isError: true };
|
|
507
574
|
}
|
|
@@ -539,10 +606,22 @@ async function executeTool(tc, callbacks, abortSignal) {
|
|
|
539
606
|
duration: Date.now() - start,
|
|
540
607
|
...(wasAborted ? { abortHint: '命令已中断(ESC)' } : {}),
|
|
541
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
|
+
});
|
|
542
616
|
return { content: safeResult, isError: false };
|
|
543
617
|
}
|
|
544
618
|
catch (err) {
|
|
545
619
|
const errMsg = err.message || '工具执行失败';
|
|
620
|
+
logError('tool.execute.failed', err, {
|
|
621
|
+
toolName: tc.name,
|
|
622
|
+
toolArgs: tc.input,
|
|
623
|
+
toolExecId,
|
|
624
|
+
});
|
|
546
625
|
callbacks.onUpdateMessage(toolExecId, {
|
|
547
626
|
status: 'error',
|
|
548
627
|
content: errMsg,
|
package/dist/core/queryWorker.js
CHANGED
|
@@ -10,8 +10,10 @@ import { MockService } from '../services/api/mock.js';
|
|
|
10
10
|
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
11
11
|
import { pendingBusRequests } from './workerBusProxy.js';
|
|
12
12
|
import { pendingSpawnRequests } from './spawnRegistry.js';
|
|
13
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
13
14
|
if (!parentPort)
|
|
14
15
|
throw new Error('queryWorker must run inside worker_threads');
|
|
16
|
+
logInfo('query_worker.ready');
|
|
15
17
|
// ===== 初始化 LLM 服务 =====
|
|
16
18
|
const config = loadConfig();
|
|
17
19
|
const activeModel = getActiveModel(config);
|
|
@@ -30,6 +32,7 @@ const pendingConfirms = new Map();
|
|
|
30
32
|
parentPort.on('message', async (msg) => {
|
|
31
33
|
if (msg.type === 'abort') {
|
|
32
34
|
abortSignal.aborted = true;
|
|
35
|
+
logWarn('query_worker.abort_received');
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
if (msg.type === 'danger_confirm_result') {
|
|
@@ -73,6 +76,10 @@ parentPort.on('message', async (msg) => {
|
|
|
73
76
|
}
|
|
74
77
|
if (msg.type === 'run') {
|
|
75
78
|
abortSignal.aborted = false;
|
|
79
|
+
logInfo('query_worker.run.start', {
|
|
80
|
+
inputLength: msg.userInput.length,
|
|
81
|
+
transcriptLength: msg.transcript.length,
|
|
82
|
+
});
|
|
76
83
|
const send = (out) => parentPort.postMessage(out);
|
|
77
84
|
const callbacks = {
|
|
78
85
|
onMessage: (m) => send({ type: 'message', msg: m }),
|
|
@@ -92,9 +99,13 @@ parentPort.on('message', async (msg) => {
|
|
|
92
99
|
};
|
|
93
100
|
try {
|
|
94
101
|
const newTranscript = await executeQuery(msg.userInput, msg.transcript, getAllTools(), service, callbacks, abortSignal);
|
|
102
|
+
logInfo('query_worker.run.done', {
|
|
103
|
+
transcriptLength: newTranscript.length,
|
|
104
|
+
});
|
|
95
105
|
send({ type: 'done', transcript: newTranscript });
|
|
96
106
|
}
|
|
97
107
|
catch (err) {
|
|
108
|
+
logError('query_worker.run.failed', err);
|
|
98
109
|
send({ type: 'error', message: err.message ?? '未知错误' });
|
|
99
110
|
}
|
|
100
111
|
}
|
|
@@ -15,8 +15,10 @@ import { LLMServiceImpl, fromModelConfig } from '../services/api/llm.js';
|
|
|
15
15
|
import { MockService } from '../services/api/mock.js';
|
|
16
16
|
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
17
17
|
import { pendingBusRequests } from './workerBusProxy.js';
|
|
18
|
+
import { logError, logInfo, logWarn } from './logger.js';
|
|
18
19
|
if (!parentPort)
|
|
19
20
|
throw new Error('subAgentWorker must run inside worker_threads');
|
|
21
|
+
logInfo('subagent_worker.ready');
|
|
20
22
|
// ===== 初始化 LLM 服务 =====
|
|
21
23
|
const config = loadConfig();
|
|
22
24
|
const activeModel = getActiveModel(config);
|
|
@@ -43,6 +45,7 @@ const pendingConfirms = new Map();
|
|
|
43
45
|
parentPort.on('message', async (msg) => {
|
|
44
46
|
if (msg.type === 'abort') {
|
|
45
47
|
abortSignal.aborted = true;
|
|
48
|
+
logWarn('subagent_worker.abort_received');
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
if (msg.type === 'danger_confirm_result') {
|
|
@@ -79,6 +82,12 @@ parentPort.on('message', async (msg) => {
|
|
|
79
82
|
abortSignal.aborted = false;
|
|
80
83
|
const { task } = msg;
|
|
81
84
|
const { taskId, instruction, allowedTools, contextTranscript, role, systemPrompt } = task;
|
|
85
|
+
logInfo('subagent_worker.run.start', {
|
|
86
|
+
taskId,
|
|
87
|
+
inputLength: instruction.length,
|
|
88
|
+
allowedTools,
|
|
89
|
+
transcriptLength: contextTranscript?.length ?? 0,
|
|
90
|
+
});
|
|
82
91
|
const send = (out) => parentPort.postMessage(out);
|
|
83
92
|
const service = buildSubAgentService(role, systemPrompt);
|
|
84
93
|
const tools = getAllTools().filter((t) => !allowedTools || allowedTools.length === 0 || allowedTools.includes(t.name));
|
|
@@ -98,9 +107,14 @@ parentPort.on('message', async (msg) => {
|
|
|
98
107
|
};
|
|
99
108
|
try {
|
|
100
109
|
const newTranscript = await executeQuery(instruction, contextTranscript ?? [], tools, service, callbacks, abortSignal);
|
|
110
|
+
logInfo('subagent_worker.run.done', {
|
|
111
|
+
taskId,
|
|
112
|
+
transcriptLength: newTranscript.length,
|
|
113
|
+
});
|
|
101
114
|
send({ type: 'done', taskId, transcript: newTranscript });
|
|
102
115
|
}
|
|
103
116
|
catch (err) {
|
|
117
|
+
logError('subagent_worker.run.failed', err, { taskId });
|
|
104
118
|
send({ type: 'error', taskId, message: err.message ?? '未知错误' });
|
|
105
119
|
}
|
|
106
120
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from 'ink';
|
|
3
|
+
import { logInfo } from './core/logger.js';
|
|
3
4
|
import REPL from './screens/repl.js';
|
|
4
5
|
export function startJarvis() {
|
|
6
|
+
logInfo('app.render.start');
|
|
5
7
|
render(_jsx(REPL, {}), { exitOnCtrlC: false });
|
|
6
8
|
}
|
package/dist/screens/repl.js
CHANGED
|
@@ -21,6 +21,7 @@ import { QueryEngine } from '../core/QueryEngine.js';
|
|
|
21
21
|
import { HIDE_WELCOME_AFTER_INPUT } from '../config/constants.js';
|
|
22
22
|
import { generateAgentHint } from '../core/hint.js';
|
|
23
23
|
import { subscribeAgentCount, getActiveAgentCount } from '../core/spawnRegistry.js';
|
|
24
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
24
25
|
export default function REPL() {
|
|
25
26
|
const { exit } = useApp();
|
|
26
27
|
const width = useTerminalWidth();
|
|
@@ -45,6 +46,7 @@ export default function REPL() {
|
|
|
45
46
|
const { displayTokens, tokenCountRef, startTokenTimer, stopTokenTimer, updateTokenCount, syncTokenDisplay, resetTokens, } = useTokenDisplay();
|
|
46
47
|
// ===== 新会话逻辑 =====
|
|
47
48
|
const handleNewSession = useCallback(() => {
|
|
49
|
+
logInfo('ui.new_session');
|
|
48
50
|
if (engineRef.current) {
|
|
49
51
|
engineRef.current.reset();
|
|
50
52
|
sessionRef.current = engineRef.current.getSession();
|
|
@@ -74,8 +76,13 @@ export default function REPL() {
|
|
|
74
76
|
// 注册持久 UI 回调,供 spawn_agent 后台子 Agent 跨轮次推送消息
|
|
75
77
|
engineRef.current.registerUIBus((msg) => setMessages((prev) => [...prev, msg]), (id, updates) => setMessages((prev) => prev.map((m) => (m.id === id ? { ...m, ...updates } : m))));
|
|
76
78
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
79
|
+
logError('ui.hint.init_failed', err);
|
|
77
80
|
console.error('[hint] 初始化提示失败:', err);
|
|
78
81
|
});
|
|
82
|
+
logInfo('ui.repl.mounted');
|
|
83
|
+
return () => {
|
|
84
|
+
logInfo('ui.repl.unmounted');
|
|
85
|
+
};
|
|
79
86
|
}, []);
|
|
80
87
|
// 订阅后台 SubAgent 计数变化
|
|
81
88
|
useEffect(() => {
|
|
@@ -141,6 +148,10 @@ export default function REPL() {
|
|
|
141
148
|
const trimmed = value.trim();
|
|
142
149
|
if (!trimmed || isProcessing || !engineRef.current)
|
|
143
150
|
return;
|
|
151
|
+
logInfo('ui.submit', {
|
|
152
|
+
inputLength: trimmed.length,
|
|
153
|
+
isSlashCommand: trimmed.startsWith('/'),
|
|
154
|
+
});
|
|
144
155
|
if (trimmed.startsWith('/')) {
|
|
145
156
|
const parts = trimmed.slice(1).split(/\s+/);
|
|
146
157
|
const cmdName = parts[0].toLowerCase();
|
|
@@ -359,12 +370,15 @@ export default function REPL() {
|
|
|
359
370
|
setShowWelcome(true);
|
|
360
371
|
resetTokens();
|
|
361
372
|
generateAgentHint().then((hint) => setPlaceholder(hint)).catch((err) => {
|
|
373
|
+
logError('ui.hint.reset_failed', err);
|
|
362
374
|
console.error('[hint] 重新生成提示失败:', err);
|
|
363
375
|
});
|
|
376
|
+
logInfo('ui.clear_screen_reset');
|
|
364
377
|
return;
|
|
365
378
|
}
|
|
366
379
|
if (key.escape) {
|
|
367
380
|
if (isProcessing && engineRef.current) {
|
|
381
|
+
logWarn('ui.abort_by_escape');
|
|
368
382
|
engineRef.current.abort();
|
|
369
383
|
}
|
|
370
384
|
else if (input.length > 0) {
|
package/dist/services/api/llm.js
CHANGED
|
@@ -11,6 +11,7 @@ import { getAgent } from '../../agents/index.js';
|
|
|
11
11
|
import { DEFAULT_AGENT } from '../../config/constants.js';
|
|
12
12
|
import { getActiveAgent } from '../../config/agentState.js';
|
|
13
13
|
import { getSystemInfoPrompt } from '../../config/systemInfo.js';
|
|
14
|
+
import { readUserProfile } from '../../config/userProfile.js';
|
|
14
15
|
/** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
|
|
15
16
|
export function getDefaultConfig() {
|
|
16
17
|
const jarvisCfg = loadConfig();
|
|
@@ -37,6 +38,13 @@ export function fromModelConfig(mc) {
|
|
|
37
38
|
extraBody: mc.extra_body,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
function buildUserProfilePrompt() {
|
|
42
|
+
const userProfile = readUserProfile();
|
|
43
|
+
if (!userProfile)
|
|
44
|
+
return '';
|
|
45
|
+
return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
|
|
46
|
+
`\n${userProfile}`;
|
|
47
|
+
}
|
|
40
48
|
/** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
|
|
41
49
|
function toOpenAIMessages(transcript, systemPrompt) {
|
|
42
50
|
const messages = [];
|
|
@@ -144,7 +152,7 @@ export class LLMServiceImpl {
|
|
|
144
152
|
}
|
|
145
153
|
// 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
|
|
146
154
|
if (this.config.systemPrompt) {
|
|
147
|
-
this.systemPrompt = this.config.systemPrompt;
|
|
155
|
+
this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt();
|
|
148
156
|
return;
|
|
149
157
|
}
|
|
150
158
|
// 从当前激活的智能体加载 system prompt(运行时动态读取)
|
|
@@ -171,7 +179,7 @@ export class LLMServiceImpl {
|
|
|
171
179
|
`\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
|
|
172
180
|
`\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
|
|
173
181
|
`\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
|
|
174
|
-
this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo;
|
|
182
|
+
this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt();
|
|
175
183
|
}
|
|
176
184
|
async streamMessage(transcript, tools, callbacks, abortSignal) {
|
|
177
185
|
const messages = toOpenAIMessages(transcript, this.systemPrompt);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateUserProfileFromInput(userInput: string): Promise<boolean>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
2
|
+
import { readUserProfile, writeUserProfile } from '../config/userProfile.js';
|
|
3
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
4
|
+
function extractMessageText(content) {
|
|
5
|
+
if (typeof content === 'string')
|
|
6
|
+
return content;
|
|
7
|
+
if (!Array.isArray(content))
|
|
8
|
+
return '';
|
|
9
|
+
return content
|
|
10
|
+
.map((item) => (item?.type === 'text' ? item.text ?? '' : ''))
|
|
11
|
+
.join('');
|
|
12
|
+
}
|
|
13
|
+
function buildProfilePrompt(userInput, existingProfile) {
|
|
14
|
+
return [
|
|
15
|
+
'请基于“已有用户画像”和“最新用户输入”,整理一份新的 USER.md。',
|
|
16
|
+
'',
|
|
17
|
+
'目标:让后续智能体在系统提示词中读取后,能够更了解用户。',
|
|
18
|
+
'',
|
|
19
|
+
'必须遵守:',
|
|
20
|
+
'1. 只保留对后续交互有帮助的信息。',
|
|
21
|
+
'2. 有明确依据的内容写成确定描述;只有弱信号时写成“可能/倾向于”。',
|
|
22
|
+
'3. 没有依据时明确写“未知”,不要编造职业、年龄、经历。',
|
|
23
|
+
'4. 输出必须是中文 Markdown,不要输出代码块围栏,不要解释你的推理过程。',
|
|
24
|
+
'5. 画像应简洁稳定,避免复述用户原话。',
|
|
25
|
+
'',
|
|
26
|
+
'请严格按下面结构输出:',
|
|
27
|
+
'# 用户画像',
|
|
28
|
+
'## 基本信息',
|
|
29
|
+
'- 职业:',
|
|
30
|
+
'- 年龄阶段:',
|
|
31
|
+
'- 所在地区:',
|
|
32
|
+
'- 语言偏好:',
|
|
33
|
+
'',
|
|
34
|
+
'## 思维与沟通',
|
|
35
|
+
'- 思维习惯:',
|
|
36
|
+
'- 沟通风格:',
|
|
37
|
+
'- 决策偏好:',
|
|
38
|
+
'',
|
|
39
|
+
'## 能力与背景',
|
|
40
|
+
'- 技术背景:',
|
|
41
|
+
'- 专业领域:',
|
|
42
|
+
'- 熟悉工具:',
|
|
43
|
+
'',
|
|
44
|
+
'## 当前关注点',
|
|
45
|
+
'- 长期目标:',
|
|
46
|
+
'- 近期任务倾向:',
|
|
47
|
+
'- 约束与偏好:',
|
|
48
|
+
'',
|
|
49
|
+
'## 交互建议',
|
|
50
|
+
'- 回答策略:',
|
|
51
|
+
'- 需要避免:',
|
|
52
|
+
'',
|
|
53
|
+
'## 置信说明',
|
|
54
|
+
'- 高置信信息:',
|
|
55
|
+
'- 低置信推断:',
|
|
56
|
+
'- 明显未知:',
|
|
57
|
+
'',
|
|
58
|
+
'---',
|
|
59
|
+
'已有用户画像:',
|
|
60
|
+
existingProfile || '(暂无)',
|
|
61
|
+
'',
|
|
62
|
+
'最新用户输入:',
|
|
63
|
+
userInput,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
export async function updateUserProfileFromInput(userInput) {
|
|
67
|
+
const normalizedInput = userInput.trim();
|
|
68
|
+
if (!normalizedInput)
|
|
69
|
+
return false;
|
|
70
|
+
const config = loadConfig();
|
|
71
|
+
const activeModel = getActiveModel(config);
|
|
72
|
+
if (!activeModel) {
|
|
73
|
+
logWarn('user_profile.skip.no_active_model');
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const prompt = buildProfilePrompt(normalizedInput, readUserProfile());
|
|
77
|
+
const body = {
|
|
78
|
+
model: activeModel.model,
|
|
79
|
+
messages: [
|
|
80
|
+
{
|
|
81
|
+
role: 'system',
|
|
82
|
+
content: '你是一个严谨的用户画像整理助手,负责维护 ~/.jarvis/USER.md。输出必须是可直接写入文件的 Markdown 正文。',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: prompt,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
max_tokens: Math.min(activeModel.max_tokens ?? 4096, 1200),
|
|
90
|
+
temperature: 0.2,
|
|
91
|
+
stream: false,
|
|
92
|
+
};
|
|
93
|
+
if (activeModel.extra_body) {
|
|
94
|
+
Object.assign(body, activeModel.extra_body);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const response = await fetch(activeModel.api_url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
Authorization: `Bearer ${activeModel.api_key}`,
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorText = await response.text().catch(() => '');
|
|
107
|
+
throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
|
|
108
|
+
}
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
|
|
111
|
+
if (!content) {
|
|
112
|
+
throw new Error('用户画像生成结果为空');
|
|
113
|
+
}
|
|
114
|
+
writeUserProfile(content);
|
|
115
|
+
logInfo('user_profile.updated', {
|
|
116
|
+
inputLength: normalizedInput.length,
|
|
117
|
+
outputLength: content.length,
|
|
118
|
+
});
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
logError('user_profile.update_failed', error, {
|
|
123
|
+
inputLength: normalizedInput.length,
|
|
124
|
+
});
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/tools/runCommand.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { exec } from 'child_process';
|
|
2
2
|
import { sanitizeOutput } from '../core/safeguard.js';
|
|
3
|
+
import { logError, logInfo, logWarn } from '../core/logger.js';
|
|
3
4
|
/**
|
|
4
5
|
* 异步执行命令,支持通过 abortSignal 中断子进程
|
|
5
6
|
*/
|
|
6
7
|
function execAsync(command, options, abortSignal) {
|
|
7
8
|
return new Promise((resolve, reject) => {
|
|
9
|
+
logInfo('bash.exec.start', { command });
|
|
8
10
|
const child = exec(command, options, (error, stdout, stderr) => {
|
|
9
11
|
// 清理轮询
|
|
10
12
|
if (pollTimer !== null)
|
|
@@ -12,6 +14,10 @@ function execAsync(command, options, abortSignal) {
|
|
|
12
14
|
// 被中断时直接返回已有输出,不视为错误
|
|
13
15
|
if (abortSignal?.aborted) {
|
|
14
16
|
const partial = sanitizeOutput(String(stdout ?? '').trim());
|
|
17
|
+
logWarn('bash.exec.aborted', {
|
|
18
|
+
command,
|
|
19
|
+
stdoutLength: String(stdout ?? '').length,
|
|
20
|
+
});
|
|
15
21
|
resolve(partial ? `(命令被中断)\n${partial}` : '(命令被中断)');
|
|
16
22
|
return;
|
|
17
23
|
}
|
|
@@ -25,9 +31,18 @@ function execAsync(command, options, abortSignal) {
|
|
|
25
31
|
parts.push(`[exit code] ${error.code}`);
|
|
26
32
|
if (parts.length === 0)
|
|
27
33
|
parts.push(error.message);
|
|
34
|
+
logError('bash.exec.failed', error, {
|
|
35
|
+
command,
|
|
36
|
+
stdoutLength: String(stdout ?? '').length,
|
|
37
|
+
stderrLength: String(stderr ?? '').length,
|
|
38
|
+
});
|
|
28
39
|
reject(new Error(`命令执行失败:\n${parts.join('\n')}`));
|
|
29
40
|
return;
|
|
30
41
|
}
|
|
42
|
+
logInfo('bash.exec.done', {
|
|
43
|
+
command,
|
|
44
|
+
stdoutLength: String(stdout ?? '').length,
|
|
45
|
+
});
|
|
31
46
|
resolve(sanitizeOutput(String(stdout).trim()) || '(命令执行完成,无输出)');
|
|
32
47
|
});
|
|
33
48
|
// 轮询 abortSignal,检测到中断时 kill 子进程
|
|
@@ -38,6 +53,7 @@ function execAsync(command, options, abortSignal) {
|
|
|
38
53
|
if (pollTimer !== null)
|
|
39
54
|
clearInterval(pollTimer);
|
|
40
55
|
pollTimer = null;
|
|
56
|
+
logWarn('bash.exec.kill_requested', { command, pid: child.pid });
|
|
41
57
|
// 先尝试 SIGTERM,给进程优雅退出的机会
|
|
42
58
|
try {
|
|
43
59
|
child.kill('SIGTERM');
|
package/dist/tools/spawnAgent.js
CHANGED
|
@@ -15,6 +15,7 @@ import { agentUIBus } from '../core/AgentRegistry.js';
|
|
|
15
15
|
import { agentMessageBus } from '../core/AgentMessageBus.js';
|
|
16
16
|
import { getBus } from '../core/busAccess.js';
|
|
17
17
|
import { pendingSpawnRequests, incrementActiveAgents, decrementActiveAgents } from '../core/spawnRegistry.js';
|
|
18
|
+
import { logError, logInfo } from '../core/logger.js';
|
|
18
19
|
/** 运行中的 task_id 集合(主线程侧,防止重复启动) */
|
|
19
20
|
const runningAgents = new Set();
|
|
20
21
|
function resolveAgentLabel(taskId, role) {
|
|
@@ -31,12 +32,18 @@ function resolveAgentLabel(taskId, role) {
|
|
|
31
32
|
*/
|
|
32
33
|
export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allowedTools) {
|
|
33
34
|
if (runningAgents.has(taskId)) {
|
|
35
|
+
logInfo('spawn_agent.duplicate', { taskId, agentLabel });
|
|
34
36
|
return `子 Agent [${taskId}] 已在运行中,请使用 send_to_agent 向其发送消息,或使用不同的 task_id 启动新实例。`;
|
|
35
37
|
}
|
|
36
38
|
const replyChannel = `agent-reply:${taskId}`;
|
|
37
39
|
const bridge = new SubAgentBridge();
|
|
38
40
|
runningAgents.add(taskId);
|
|
39
41
|
incrementActiveAgents();
|
|
42
|
+
logInfo('spawn_agent.started', {
|
|
43
|
+
taskId,
|
|
44
|
+
agentLabel,
|
|
45
|
+
allowedTools,
|
|
46
|
+
});
|
|
40
47
|
bridge.run({ taskId, instruction, allowedTools }, {
|
|
41
48
|
onMessage: (_tid, msg) => {
|
|
42
49
|
const tagged = { ...msg, subAgentId: agentLabel };
|
|
@@ -51,10 +58,20 @@ export function spawnSubAgentInMainThread(taskId, instruction, agentLabel, allow
|
|
|
51
58
|
}).then((result) => {
|
|
52
59
|
runningAgents.delete(taskId);
|
|
53
60
|
decrementActiveAgents();
|
|
61
|
+
logInfo('spawn_agent.done', {
|
|
62
|
+
taskId,
|
|
63
|
+
agentLabel,
|
|
64
|
+
status: result.status,
|
|
65
|
+
outputLength: result.output.length,
|
|
66
|
+
});
|
|
54
67
|
agentMessageBus.publish(taskId, replyChannel, `[AGENT_DONE] ${result.output || '子 Agent 已完成'}`);
|
|
55
68
|
}).catch((err) => {
|
|
56
69
|
runningAgents.delete(taskId);
|
|
57
70
|
decrementActiveAgents();
|
|
71
|
+
logError('spawn_agent.failed', err, {
|
|
72
|
+
taskId,
|
|
73
|
+
agentLabel,
|
|
74
|
+
});
|
|
58
75
|
agentMessageBus.publish(taskId, replyChannel, `[AGENT_ERROR] ${err.message}`);
|
|
59
76
|
});
|
|
60
77
|
const inboxChannel = `agent-inbox:${taskId}`;
|