@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.
@@ -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
- return Promise.all(tasks);
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,
@@ -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
  }
@@ -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) {
@@ -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>;