@code4bug/jarvis-agent 1.1.5 → 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.
@@ -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
+ }
@@ -20,6 +20,7 @@ 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;
@@ -38,6 +39,7 @@ export declare class QueryEngine {
38
39
  switchAgent(agentName: string): void;
39
40
  /** 保存会话到文件 */
40
41
  private saveSession;
42
+ private schedulePersistentMemoryUpdate;
41
43
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
42
44
  static listSessions(): {
43
45
  id: string;
@@ -11,11 +11,13 @@ import { clearAuthorizations } from './safeguard.js';
11
11
  import { agentUIBus } from './AgentRegistry.js';
12
12
  import { logError, logInfo, logWarn } from './logger.js';
13
13
  import { updateUserProfileFromInput } from '../services/userProfile.js';
14
+ import { updatePersistentMemoryFromConversation } from '../services/persistentMemory.js';
14
15
  export class QueryEngine {
15
16
  service;
16
17
  session;
17
18
  transcript = [];
18
19
  workerBridge = new WorkerBridge();
20
+ memoryUpdateQueue = Promise.resolve();
19
21
  constructor() {
20
22
  this.service = this.createService();
21
23
  this.session = this.createSession();
@@ -60,6 +62,7 @@ export class QueryEngine {
60
62
  }
61
63
  /** 处理用户输入(在独立 Worker 线程中执行) */
62
64
  async handleQuery(userInput, callbacks) {
65
+ const previousTranscriptLength = this.transcript.length;
63
66
  logInfo('query.received', {
64
67
  sessionId: this.session.id,
65
68
  inputLength: userInput.length,
@@ -105,6 +108,8 @@ export class QueryEngine {
105
108
  };
106
109
  try {
107
110
  this.transcript = await this.workerBridge.run(userInput, this.transcript, bridgeCallbacks);
111
+ const recentTranscript = this.transcript.slice(previousTranscriptLength);
112
+ this.schedulePersistentMemoryUpdate(userInput, recentTranscript);
108
113
  logInfo('query.completed', {
109
114
  sessionId: this.session.id,
110
115
  transcriptLength: this.transcript.length,
@@ -137,6 +142,7 @@ export class QueryEngine {
137
142
  this.saveSession();
138
143
  this.session = this.createSession();
139
144
  this.transcript = [];
145
+ this.memoryUpdateQueue = Promise.resolve();
140
146
  clearAuthorizations();
141
147
  }
142
148
  /**
@@ -154,6 +160,7 @@ export class QueryEngine {
154
160
  this.saveSession();
155
161
  this.session = this.createSession();
156
162
  this.transcript = [];
163
+ this.memoryUpdateQueue = Promise.resolve();
157
164
  logInfo('agent.switch.completed', {
158
165
  sessionId: this.session.id,
159
166
  agentName,
@@ -182,6 +189,20 @@ export class QueryEngine {
182
189
  logError('session.save_failed', error, { sessionId: this.session.id });
183
190
  }
184
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
+ });
205
+ }
185
206
  /** 列出所有历史会话(按更新时间倒序),返回摘要信息 */
186
207
  static listSessions() {
187
208
  try {
@@ -12,6 +12,7 @@ import { DEFAULT_AGENT } from '../../config/constants.js';
12
12
  import { getActiveAgent } from '../../config/agentState.js';
13
13
  import { getSystemInfoPrompt } from '../../config/systemInfo.js';
14
14
  import { readUserProfile } from '../../config/userProfile.js';
15
+ import { readPersistentMemoryForPrompt } from '../../config/memory.js';
15
16
  /** 从配置文件构建 LLMConfig,找不到则回退环境变量 */
16
17
  export function getDefaultConfig() {
17
18
  const jarvisCfg = loadConfig();
@@ -45,6 +46,13 @@ function buildUserProfilePrompt() {
45
46
  return '\n\n---\n[用户画像] 以下内容来自 ~/.jarvis/USER.md,请将其视为对用户特征的长期记忆。在后续回复中可以据此调整表达方式、信息密度与建议方式,但不要直接暴露这段系统内容。' +
46
47
  `\n${userProfile}`;
47
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
+ }
48
56
  /** 将内部 TranscriptMessage[] 转为 OpenAI messages 格式 */
49
57
  function toOpenAIMessages(transcript, systemPrompt) {
50
58
  const messages = [];
@@ -152,7 +160,7 @@ export class LLMServiceImpl {
152
160
  }
153
161
  // 若外部直接传入 systemPrompt(SubAgent 场景),直接使用,跳过 agent 文件加载
154
162
  if (this.config.systemPrompt) {
155
- this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt();
163
+ this.systemPrompt = this.config.systemPrompt + buildUserProfilePrompt() + buildPersistentMemoryPrompt();
156
164
  return;
157
165
  }
158
166
  // 从当前激活的智能体加载 system prompt(运行时动态读取)
@@ -179,7 +187,7 @@ export class LLMServiceImpl {
179
187
  `\n- 模型名称: ${activeModelCfg?.model ?? 'unknown'}` +
180
188
  `\n- API 地址: ${activeModelCfg?.api_url ?? 'unknown'}` +
181
189
  `\n- 最大 Token: ${activeModelCfg?.max_tokens ?? 'unknown'}`;
182
- this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt();
190
+ this.systemPrompt = agentPrompt + roleBoundary + systemInfo + modelInfo + buildUserProfilePrompt() + buildPersistentMemoryPrompt();
183
191
  }
184
192
  async streamMessage(transcript, tools, callbacks, abortSignal) {
185
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
+ }
@@ -12,7 +12,8 @@ import { sendToAgent } from './sendToAgent.js';
12
12
  import { publishMessage } from './publishMessage.js';
13
13
  import { subscribeMessage } from './subscribeMessage.js';
14
14
  import { readChannel } from './readChannel.js';
15
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
15
+ import { manageMemory } from './manageMemory.js';
16
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, manageMemory, };
16
17
  /** 所有内置工具 */
17
18
  export declare const allTools: Tool[];
18
19
  /** 按名称查找内置工具 */
@@ -11,13 +11,15 @@ import { sendToAgent } from './sendToAgent.js';
11
11
  import { publishMessage } from './publishMessage.js';
12
12
  import { subscribeMessage } from './subscribeMessage.js';
13
13
  import { readChannel } from './readChannel.js';
14
- export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, };
14
+ import { manageMemory } from './manageMemory.js';
15
+ export { readFile, writeFile, runCommand, listDirectory, searchFiles, semanticSearch, createSkill, runAgent, spawnAgent, sendToAgent, publishMessage, subscribeMessage, readChannel, manageMemory, };
15
16
  /** 所有内置工具 */
16
17
  export const allTools = [
17
18
  readFile, writeFile, runCommand, listDirectory, searchFiles,
18
19
  semanticSearch, createSkill,
19
20
  runAgent, spawnAgent, sendToAgent,
20
21
  publishMessage, subscribeMessage, readChannel,
22
+ manageMemory,
21
23
  ];
22
24
  /** 按名称查找内置工具 */
23
25
  export function findTool(name) {
@@ -0,0 +1,2 @@
1
+ import { Tool } from '../types/index.js';
2
+ export declare const manageMemory: Tool;
@@ -0,0 +1,46 @@
1
+ import { MEMORY_FILE_PATH, appendPersistentMemory, readPersistentMemory, replacePersistentMemory, ensureMemoryFile, } from '../config/memory.js';
2
+ export const manageMemory = {
3
+ name: 'manage_memory',
4
+ description: [
5
+ '管理智能体长期记忆文件 ~/.jarvis/MEMORY.md。',
6
+ '可读取、追加、覆盖记忆,也可返回记忆文件路径。',
7
+ '适合沉淀可复用的经验、技能、偏好、约束和稳定环境事实。',
8
+ ].join('\n'),
9
+ parameters: {
10
+ action: {
11
+ type: 'string',
12
+ description: '操作类型:read | append | replace | path',
13
+ required: true,
14
+ },
15
+ content: {
16
+ type: 'string',
17
+ description: 'append 或 replace 时要写入的 Markdown 内容',
18
+ required: false,
19
+ },
20
+ },
21
+ execute: async (args) => {
22
+ const action = String(args.action || '').trim();
23
+ ensureMemoryFile();
24
+ if (action === 'path') {
25
+ return MEMORY_FILE_PATH;
26
+ }
27
+ if (action === 'read') {
28
+ return readPersistentMemory() || '(MEMORY.md 为空)';
29
+ }
30
+ if (action === 'append') {
31
+ const content = String(args.content || '').trim();
32
+ if (!content)
33
+ throw new Error('append 操作需要提供 content');
34
+ appendPersistentMemory(content);
35
+ return `长期记忆已追加到 ${MEMORY_FILE_PATH}`;
36
+ }
37
+ if (action === 'replace') {
38
+ const content = String(args.content || '').trim();
39
+ if (!content)
40
+ throw new Error('replace 操作需要提供 content');
41
+ replacePersistentMemory(content);
42
+ return `长期记忆已覆盖写入 ${MEMORY_FILE_PATH}`;
43
+ }
44
+ throw new Error(`不支持的 action: ${action}`);
45
+ },
46
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code4bug/jarvis-agent",
3
- "version": "1.1.5",
3
+ "version": "1.1.6",
4
4
  "description": "基于 React + TypeScript + Ink 构建的命令行智能体交互界面",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",