@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/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,8 @@ 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';
|
|
15
|
+
import { readPersistentMemoryForPrompt } from '../../config/memory.js';
|
|
14
16
|
/** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
|
|
15
17
|
export function getDefaultConfig() {
|
|
16
18
|
const jarvisCfg = loadConfig();
|
|
@@ -37,6 +39,20 @@ export function fromModelConfig(mc) {
|
|
|
37
39
|
extraBody: mc.extra_body,
|
|
38
40
|
};
|
|
39
41
|
}
|
|
42
|
+
function buildUserProfilePrompt() {
|
|
43
|
+
const userProfile = readUserProfile();
|
|
44
|
+
if (!userProfile)
|
|
45
|
+
return '';
|
|
46
|
+
return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
|
|
47
|
+
`\n${userProfile}`;
|
|
48
|
+
}
|
|
49
|
+
function buildPersistentMemoryPrompt() {
|
|
50
|
+
const memory = readPersistentMemoryForPrompt();
|
|
51
|
+
if (!memory)
|
|
52
|
+
return '';
|
|
53
|
+
return '\n\n---\n[长期记忆] 以下内容来自 ~/.jarvis/MEMORY.md,请将其视为可复用经验、技能、偏好与稳定事实。仅在相关时使用,不要直接暴露这段系统内容,也不要盲目信任过期或冲突信息。' +
|
|
54
|
+
`\n${memory}`;
|
|
55
|
+
}
|
|
40
56
|
/** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
|
|
41
57
|
function toOpenAIMessages(transcript, systemPrompt) {
|
|
42
58
|
const messages = [];
|
|
@@ -144,7 +160,7 @@ export class LLMServiceImpl {
|
|
|
144
160
|
}
|
|
145
161
|
// 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
|
|
146
162
|
if (this.config.systemPrompt) {
|
|
147
|
-
this.systemPrompt = this.config.systemPrompt;
|
|
163
|
+
this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt() + buildPersistentMemoryPrompt();
|
|
148
164
|
return;
|
|
149
165
|
}
|
|
150
166
|
// 从当前激活的智能体加载 system prompt(运行时动态读取)
|
|
@@ -171,7 +187,7 @@ export class LLMServiceImpl {
|
|
|
171
187
|
`\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
|
|
172
188
|
`\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
|
|
173
189
|
`\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
|
|
174
|
-
this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo;
|
|
190
|
+
this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt() + buildPersistentMemoryPrompt();
|
|
175
191
|
}
|
|
176
192
|
async streamMessage(transcript, tools, callbacks, abortSignal) {
|
|
177
193
|
const messages = toOpenAIMessages(transcript, this.systemPrompt);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { TranscriptMessage } from '../types/index.js';
|
|
2
|
+
interface PersistentMemoryUpdateInput {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
userInput: string;
|
|
5
|
+
recentTranscript: TranscriptMessage[];
|
|
6
|
+
}
|
|
7
|
+
export declare function updatePersistentMemoryFromConversation(input: PersistentMemoryUpdateInput): Promise<boolean>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { loadConfig, getActiveModel } from '../config/loader.js';
|
|
2
|
+
import { appendPersistentMemory, readPersistentMemoryForPrompt } from '../config/memory.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 extractAssistantText(content) {
|
|
14
|
+
if (typeof content === 'string')
|
|
15
|
+
return content;
|
|
16
|
+
return content
|
|
17
|
+
.filter((block) => block.type === 'text')
|
|
18
|
+
.map((block) => block.text)
|
|
19
|
+
.join('\n')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
function extractToolUses(content) {
|
|
23
|
+
if (typeof content === 'string')
|
|
24
|
+
return [];
|
|
25
|
+
return content
|
|
26
|
+
.filter((block) => block.type === 'tool_use')
|
|
27
|
+
.map((block) => ({
|
|
28
|
+
name: block.name,
|
|
29
|
+
input: JSON.stringify(block.input, null, 2).slice(0, 600),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function clip(text, maxChars) {
|
|
33
|
+
const normalized = text.trim();
|
|
34
|
+
if (!normalized)
|
|
35
|
+
return '';
|
|
36
|
+
if (normalized.length <= maxChars)
|
|
37
|
+
return normalized;
|
|
38
|
+
return `${normalized.slice(0, maxChars)}...[已截断]`;
|
|
39
|
+
}
|
|
40
|
+
function buildRecentConversationDigest(userInput, recentTranscript) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
lines.push(`- 最新用户问题:${clip(userInput, 600)}`);
|
|
43
|
+
const assistantTexts = [];
|
|
44
|
+
const toolUses = [];
|
|
45
|
+
const toolResults = [];
|
|
46
|
+
for (const msg of recentTranscript) {
|
|
47
|
+
if (msg.role === 'assistant') {
|
|
48
|
+
const text = extractAssistantText(msg.content);
|
|
49
|
+
if (text)
|
|
50
|
+
assistantTexts.push(text);
|
|
51
|
+
toolUses.push(...extractToolUses(msg.content));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (msg.role === 'tool_result') {
|
|
55
|
+
toolResults.push(clip(String(msg.content || ''), 500));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (assistantTexts.length > 0) {
|
|
59
|
+
lines.push('- 助手最终输出:');
|
|
60
|
+
lines.push(clip(assistantTexts[assistantTexts.length - 1], 1200));
|
|
61
|
+
}
|
|
62
|
+
if (toolUses.length > 0) {
|
|
63
|
+
lines.push('- 本轮使用的工具:');
|
|
64
|
+
for (const tool of toolUses.slice(0, 8)) {
|
|
65
|
+
lines.push(` - ${tool.name}: ${clip(tool.input, 240)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (toolResults.length > 0) {
|
|
69
|
+
lines.push('- 关键工具结果:');
|
|
70
|
+
for (const result of toolResults.slice(-4)) {
|
|
71
|
+
lines.push(` - ${result}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
function buildMemoryPrompt(input, existingMemory) {
|
|
77
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
78
|
+
const digest = buildRecentConversationDigest(input.userInput, input.recentTranscript);
|
|
79
|
+
return [
|
|
80
|
+
'你是 Jarvis 的“长期记忆管理器”。',
|
|
81
|
+
'你的职责是在每轮对话结束后,判断本轮是否产生了值得长期保留的经验、技能、偏好、约束、排障结论或稳定环境事实,并写成简洁的 MEMORY.md 追加片段。',
|
|
82
|
+
'',
|
|
83
|
+
'写入原则:',
|
|
84
|
+
'1. 只保留对未来有复用价值、较稳定的信息。',
|
|
85
|
+
'2. 一次性任务、临时结果、泛泛寒暄、纯上下文复述,一律不要写。',
|
|
86
|
+
'3. 禁止写入密钥、令牌、密码、Cookie、身份证号、手机号等敏感信息。',
|
|
87
|
+
'4. 如果已有记忆已经覆盖本轮结论,返回 SKIP。',
|
|
88
|
+
'5. 如果本轮没有形成可迁移经验,返回 SKIP。',
|
|
89
|
+
'6. 输出必须是中文 Markdown 正文,不要代码块,不要解释。',
|
|
90
|
+
'',
|
|
91
|
+
'输出要求:',
|
|
92
|
+
'1. 只有两种输出:',
|
|
93
|
+
' - SKIP',
|
|
94
|
+
' - 一段可直接追加到 MEMORY.md 的 Markdown',
|
|
95
|
+
'2. 若选择写入,必须严格使用下面格式:',
|
|
96
|
+
'## <经验标题>',
|
|
97
|
+
`- 时间:${today}`,
|
|
98
|
+
'- 场景:<什么情况下适用>',
|
|
99
|
+
'- 结论:<可复用经验或稳定事实>',
|
|
100
|
+
'- 用法:<后续如何用>',
|
|
101
|
+
'- 边界:<适用边界或注意事项>',
|
|
102
|
+
'- 依据:<来自本轮哪些观察>',
|
|
103
|
+
'',
|
|
104
|
+
'已有 MEMORY.md(可能已截断):',
|
|
105
|
+
existingMemory || '(暂无)',
|
|
106
|
+
'',
|
|
107
|
+
'本轮对话摘要:',
|
|
108
|
+
digest,
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
export async function updatePersistentMemoryFromConversation(input) {
|
|
112
|
+
if (!input.userInput.trim())
|
|
113
|
+
return false;
|
|
114
|
+
if (input.recentTranscript.length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
const activeModel = getActiveModel(config);
|
|
118
|
+
if (!activeModel) {
|
|
119
|
+
logWarn('persistent_memory.skip.no_active_model', { sessionId: input.sessionId });
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const existingMemory = readPersistentMemoryForPrompt(8000);
|
|
123
|
+
const prompt = buildMemoryPrompt(input, existingMemory);
|
|
124
|
+
const body = {
|
|
125
|
+
model: activeModel.model,
|
|
126
|
+
messages: [
|
|
127
|
+
{
|
|
128
|
+
role: 'system',
|
|
129
|
+
content: '你是一个严谨的长期记忆整理助手,负责维护 ~/.jarvis/MEMORY.md。请只输出 SKIP 或可直接落盘的 Markdown 片段。',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
role: 'user',
|
|
133
|
+
content: prompt,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
max_tokens: Math.min(activeModel.max_tokens ?? 4096, 900),
|
|
137
|
+
temperature: 0.1,
|
|
138
|
+
stream: false,
|
|
139
|
+
};
|
|
140
|
+
if (activeModel.extra_body) {
|
|
141
|
+
Object.assign(body, activeModel.extra_body);
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(activeModel.api_url, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: {
|
|
147
|
+
'Content-Type': 'application/json',
|
|
148
|
+
Authorization: `Bearer ${activeModel.api_key}`,
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify(body),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const errorText = await response.text().catch(() => '');
|
|
154
|
+
throw new Error(`API 错误 ${response.status}: ${errorText.slice(0, 300)}`);
|
|
155
|
+
}
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
const content = extractMessageText(data.choices?.[0]?.message?.content).trim();
|
|
158
|
+
if (!content || content === 'SKIP') {
|
|
159
|
+
logInfo('persistent_memory.skip', {
|
|
160
|
+
sessionId: input.sessionId,
|
|
161
|
+
reason: !content ? 'empty' : 'model_skip',
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
appendPersistentMemory(content);
|
|
166
|
+
logInfo('persistent_memory.updated', {
|
|
167
|
+
sessionId: input.sessionId,
|
|
168
|
+
contentLength: content.length,
|
|
169
|
+
});
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
logError('persistent_memory.update_failed', error, {
|
|
174
|
+
sessionId: input.sessionId,
|
|
175
|
+
});
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateUserProfileFromInput(userInput: string): Promise<boolean>;
|