@code4bug/jarvis-agent 1.1.4 → 1.1.6
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/memory.d.ts +7 -0
- package/dist/config/memory.js +55 -0
- 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 +3 -0
- package/dist/core/QueryEngine.js +92 -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 +18 -2
- package/dist/services/persistentMemory.d.ts +8 -0
- package/dist/services/persistentMemory.js +178 -0
- package/dist/services/userProfile.d.ts +1 -0
- package/dist/services/userProfile.js +127 -0
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.js +3 -1
- package/dist/tools/manageMemory.d.ts +2 -0
- package/dist/tools/manageMemory.js +46 -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,7 @@
|
|
|
1
|
+
export declare const MEMORY_FILE_PATH: string;
|
|
2
|
+
export declare function ensureMemoryHomeDir(): string;
|
|
3
|
+
export declare function ensureMemoryFile(): string;
|
|
4
|
+
export declare function readPersistentMemory(): string;
|
|
5
|
+
export declare function readPersistentMemoryForPrompt(maxChars?: number): string;
|
|
6
|
+
export declare function appendPersistentMemory(content: string): void;
|
|
7
|
+
export declare function replacePersistentMemory(content: string): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export const MEMORY_FILE_PATH = path.join(os.homedir(), '.jarvis', 'MEMORY.md');
|
|
5
|
+
const MEMORY_FILE_HEADER = [
|
|
6
|
+
'# Jarvis 长期记忆',
|
|
7
|
+
'',
|
|
8
|
+
'> 这里沉淀可复用的经验、技能、偏好、约束与稳定环境事实。',
|
|
9
|
+
'> 避免写入一次性闲聊、临时输出、密钥或其它敏感信息。',
|
|
10
|
+
'',
|
|
11
|
+
].join('\n');
|
|
12
|
+
export function ensureMemoryHomeDir() {
|
|
13
|
+
const dir = path.dirname(MEMORY_FILE_PATH);
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
export function ensureMemoryFile() {
|
|
20
|
+
ensureMemoryHomeDir();
|
|
21
|
+
if (!fs.existsSync(MEMORY_FILE_PATH)) {
|
|
22
|
+
fs.writeFileSync(MEMORY_FILE_PATH, MEMORY_FILE_HEADER, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
return MEMORY_FILE_PATH;
|
|
25
|
+
}
|
|
26
|
+
export function readPersistentMemory() {
|
|
27
|
+
try {
|
|
28
|
+
ensureMemoryFile();
|
|
29
|
+
return fs.readFileSync(MEMORY_FILE_PATH, 'utf-8').trim();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function readPersistentMemoryForPrompt(maxChars = 12000) {
|
|
36
|
+
const content = readPersistentMemory();
|
|
37
|
+
if (!content)
|
|
38
|
+
return '';
|
|
39
|
+
if (content.length <= maxChars)
|
|
40
|
+
return content;
|
|
41
|
+
return `...[已截断,仅保留最近 ${maxChars} 字符]\n${content.slice(-maxChars)}`;
|
|
42
|
+
}
|
|
43
|
+
export function appendPersistentMemory(content) {
|
|
44
|
+
const normalized = content.trim();
|
|
45
|
+
if (!normalized)
|
|
46
|
+
return;
|
|
47
|
+
ensureMemoryFile();
|
|
48
|
+
const current = fs.readFileSync(MEMORY_FILE_PATH, 'utf-8');
|
|
49
|
+
const suffix = current.endsWith('\n\n') ? '' : (current.endsWith('\n') ? '\n' : '\n\n');
|
|
50
|
+
fs.appendFileSync(MEMORY_FILE_PATH, `${suffix}${normalized}\n`, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
export function replacePersistentMemory(content) {
|
|
53
|
+
ensureMemoryFile();
|
|
54
|
+
fs.writeFileSync(MEMORY_FILE_PATH, `${content.trimEnd()}\n`, 'utf-8');
|
|
55
|
+
}
|
|
@@ -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))
|
|
@@ -20,11 +20,13 @@ export declare class QueryEngine {
|
|
|
20
20
|
private session;
|
|
21
21
|
private transcript;
|
|
22
22
|
private workerBridge;
|
|
23
|
+
private memoryUpdateQueue;
|
|
23
24
|
constructor();
|
|
24
25
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
25
26
|
registerUIBus(onMessage: (msg: Message) => void, onUpdateMessage: (id: string, updates: Partial<Message>) => void): void;
|
|
26
27
|
private createSession;
|
|
27
28
|
private ensureSessionDir;
|
|
29
|
+
private createService;
|
|
28
30
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
29
31
|
handleQuery(userInput: string, callbacks: EngineCallbacks): Promise<void>;
|
|
30
32
|
/** 终止当前任务(通知 Worker 中断) */
|
|
@@ -37,6 +39,7 @@ export declare class QueryEngine {
|
|
|
37
39
|
switchAgent(agentName: string): void;
|
|
38
40
|
/** 保存会话到文件 */
|
|
39
41
|
private saveSession;
|
|
42
|
+
private schedulePersistentMemoryUpdate;
|
|
40
43
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
41
44
|
static listSessions(): {
|
|
42
45
|
id: string;
|
package/dist/core/QueryEngine.js
CHANGED
|
@@ -9,32 +9,28 @@ 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';
|
|
14
|
+
import { updatePersistentMemoryFromConversation } from '../services/persistentMemory.js';
|
|
12
15
|
export class QueryEngine {
|
|
13
16
|
service;
|
|
14
17
|
session;
|
|
15
18
|
transcript = [];
|
|
16
19
|
workerBridge = new WorkerBridge();
|
|
20
|
+
memoryUpdateQueue = Promise.resolve();
|
|
17
21
|
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
|
-
}
|
|
22
|
+
this.service = this.createService();
|
|
32
23
|
this.session = this.createSession();
|
|
33
24
|
this.ensureSessionDir();
|
|
25
|
+
logInfo('engine.created', {
|
|
26
|
+
sessionId: this.session.id,
|
|
27
|
+
service: this.service.constructor.name,
|
|
28
|
+
});
|
|
34
29
|
}
|
|
35
30
|
/** 注册持久 UI 回调,供后台 spawn_agent 子 Agent 推送消息 */
|
|
36
31
|
registerUIBus(onMessage, onUpdateMessage) {
|
|
37
32
|
agentUIBus.register(onMessage, onUpdateMessage);
|
|
33
|
+
logInfo('engine.ui_bus_registered', { sessionId: this.session.id });
|
|
38
34
|
}
|
|
39
35
|
createSession() {
|
|
40
36
|
return {
|
|
@@ -51,8 +47,27 @@ export class QueryEngine {
|
|
|
51
47
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
52
48
|
}
|
|
53
49
|
}
|
|
50
|
+
createService() {
|
|
51
|
+
const config = loadConfig();
|
|
52
|
+
const activeModel = getActiveModel(config);
|
|
53
|
+
if (activeModel) {
|
|
54
|
+
try {
|
|
55
|
+
return new LLMServiceImpl();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return new MockService();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return new MockService();
|
|
62
|
+
}
|
|
54
63
|
/** 处理用户输入(在独立 Worker 线程中执行) */
|
|
55
64
|
async handleQuery(userInput, callbacks) {
|
|
65
|
+
const previousTranscriptLength = this.transcript.length;
|
|
66
|
+
logInfo('query.received', {
|
|
67
|
+
sessionId: this.session.id,
|
|
68
|
+
inputLength: userInput.length,
|
|
69
|
+
preview: userInput.slice(0, 200),
|
|
70
|
+
});
|
|
56
71
|
const userMsg = {
|
|
57
72
|
id: uuid(),
|
|
58
73
|
type: 'user',
|
|
@@ -62,6 +77,12 @@ export class QueryEngine {
|
|
|
62
77
|
};
|
|
63
78
|
callbacks.onMessage(userMsg);
|
|
64
79
|
this.session.messages.push(userMsg);
|
|
80
|
+
if (this.transcript.length === 0) {
|
|
81
|
+
const updated = await updateUserProfileFromInput(userInput);
|
|
82
|
+
if (updated) {
|
|
83
|
+
this.service = this.createService();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
// 将回调包装后传给 WorkerBridge,Worker 事件会映射回这里
|
|
66
87
|
const bridgeCallbacks = {
|
|
67
88
|
onMessage: (msg) => {
|
|
@@ -87,8 +108,16 @@ export class QueryEngine {
|
|
|
87
108
|
};
|
|
88
109
|
try {
|
|
89
110
|
this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
|
|
111
|
+
const recentTranscript = this.transcript.slice(previousTranscriptLength);
|
|
112
|
+
this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
|
|
113
|
+
logInfo('query.completed', {
|
|
114
|
+
sessionId: this.session.id,
|
|
115
|
+
transcriptLength: this.transcript.length,
|
|
116
|
+
totalTokens: this.session.totalTokens,
|
|
117
|
+
});
|
|
90
118
|
}
|
|
91
119
|
catch (err) {
|
|
120
|
+
logError('query.failed', err, { sessionId: this.session.id });
|
|
92
121
|
const errMsg = {
|
|
93
122
|
id: uuid(),
|
|
94
123
|
type: 'error',
|
|
@@ -104,38 +133,39 @@ export class QueryEngine {
|
|
|
104
133
|
}
|
|
105
134
|
/** 终止当前任务(通知 Worker 中断) */
|
|
106
135
|
abort() {
|
|
136
|
+
logWarn('query.abort_requested', { sessionId: this.session.id });
|
|
107
137
|
this.workerBridge.abort();
|
|
108
138
|
}
|
|
109
139
|
/** 重置会话 */
|
|
110
140
|
reset() {
|
|
141
|
+
logInfo('session.reset', { sessionId: this.session.id });
|
|
111
142
|
this.saveSession();
|
|
112
143
|
this.session = this.createSession();
|
|
113
144
|
this.transcript = [];
|
|
145
|
+
this.memoryUpdateQueue = Promise.resolve();
|
|
114
146
|
clearAuthorizations();
|
|
115
147
|
}
|
|
116
148
|
/**
|
|
117
149
|
* 切换智能体:持久化选择 + 重建 LLM service + 重置会话
|
|
118
150
|
*/
|
|
119
151
|
switchAgent(agentName) {
|
|
152
|
+
logInfo('agent.switch.start', {
|
|
153
|
+
fromSessionId: this.session.id,
|
|
154
|
+
agentName,
|
|
155
|
+
});
|
|
120
156
|
setActiveAgent(agentName);
|
|
121
157
|
// 重建 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
|
-
}
|
|
158
|
+
this.service = this.createService();
|
|
135
159
|
// 重置会话上下文
|
|
136
160
|
this.saveSession();
|
|
137
161
|
this.session = this.createSession();
|
|
138
162
|
this.transcript = [];
|
|
163
|
+
this.memoryUpdateQueue = Promise.resolve();
|
|
164
|
+
logInfo('agent.switch.completed', {
|
|
165
|
+
sessionId: this.session.id,
|
|
166
|
+
agentName,
|
|
167
|
+
service: this.service.constructor.name,
|
|
168
|
+
});
|
|
139
169
|
}
|
|
140
170
|
/** 保存会话到文件 */
|
|
141
171
|
saveSession() {
|
|
@@ -149,8 +179,29 @@ export class QueryEngine {
|
|
|
149
179
|
}
|
|
150
180
|
const filePath = path.join(SESSIONS_DIR, `${this.session.id}.json`);
|
|
151
181
|
fs.writeFileSync(filePath, JSON.stringify(this.session, null, 2), 'utf-8');
|
|
182
|
+
logInfo('session.saved', {
|
|
183
|
+
sessionId: this.session.id,
|
|
184
|
+
filePath,
|
|
185
|
+
messageCount: this.session.messages.length,
|
|
186
|
+
});
|
|
152
187
|
}
|
|
153
|
-
catch {
|
|
188
|
+
catch (error) {
|
|
189
|
+
logError('session.save_failed', error, { sessionId: this.session.id });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
schedulePersistentMemoryUpdate(userInput, recentTranscript) {
|
|
193
|
+
if (!userInput.trim() || recentTranscript.length === 0)
|
|
194
|
+
return;
|
|
195
|
+
const sessionId = this.session.id;
|
|
196
|
+
this.memoryUpdateQueue = this.memoryUpdateQueue
|
|
197
|
+
.catch(() => { })
|
|
198
|
+
.then(async () => {
|
|
199
|
+
await updatePersistentMemoryFromConversation({
|
|
200
|
+
sessionId,
|
|
201
|
+
userInput,
|
|
202
|
+
recentTranscript,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
154
205
|
}
|
|
155
206
|
/** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
|
|
156
207
|
static listSessions() {
|
|
@@ -221,9 +272,15 @@ export class QueryEngine {
|
|
|
221
272
|
});
|
|
222
273
|
}
|
|
223
274
|
}
|
|
275
|
+
logInfo('session.loaded', {
|
|
276
|
+
sessionId,
|
|
277
|
+
messageCount: cleanedMessages.length,
|
|
278
|
+
transcriptLength: this.transcript.length,
|
|
279
|
+
});
|
|
224
280
|
return { session: this.session, messages: cleanedMessages };
|
|
225
281
|
}
|
|
226
|
-
catch {
|
|
282
|
+
catch (error) {
|
|
283
|
+
logError('session.load_failed', error, { sessionId });
|
|
227
284
|
return null;
|
|
228
285
|
}
|
|
229
286
|
}
|
|
@@ -250,9 +307,14 @@ export class QueryEngine {
|
|
|
250
307
|
}
|
|
251
308
|
catch { /* 跳过删除失败的文件 */ }
|
|
252
309
|
}
|
|
310
|
+
logInfo('session.clear_others', {
|
|
311
|
+
sessionId: this.session.id,
|
|
312
|
+
removedCount: count,
|
|
313
|
+
});
|
|
253
314
|
return count;
|
|
254
315
|
}
|
|
255
|
-
catch {
|
|
316
|
+
catch (error) {
|
|
317
|
+
logError('session.clear_others_failed', error, { sessionId: this.session.id });
|
|
256
318
|
return 0;
|
|
257
319
|
}
|
|
258
320
|
}
|
|
@@ -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
|
+
}
|