myagent-ai 1.15.82 → 1.15.84

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.
@@ -50,7 +50,7 @@ class MainAgent(BaseAgent):
50
50
  </toolstocal>
51
51
  </response>
52
52
  <task_plan>仅复杂任务使用任务计划,如"context"包含非空"task_plan",则更新它。否则,先评估任务复杂度,针对单次查询、简单问答、格式转换、单文件修改、简单计算等简单任务,若预计操作步骤不超过3步,则此处输出为空,不要创建任务列表;针对多文件修改、需要调研+实现+测试、涉及多个模块联动等复杂任务,如预计超过3步操作,则以Markdown列表格式制定新任务列表。格式:每项用 "- [ ] 任务描述" 或 "- [x] 已完成任务",含完成状态标记。</task_plan>
53
- <remember><type>填global或session,其中"global"为跨会话全局记忆,"session"为仅当前会话。</type><content>仅从最新用户输入,包括"userprint"或"usersays_correct"或工具调用结果,中提炼值得记忆的信息(如用户偏好、重要结论、错误经验等)。如本轮无新信息要记忆,则为空,type也为空。</content></remember>
53
+ <remember><type>填global或session,其中"global"为跨会话全局记忆,"session"为仅当前会话。</type><content>仅从最新用户输入,包括"userprint"或"usersays_correct"或工具调用结果,中提炼值得记忆的信息(如用户偏好、重要结论、错误经验、用户个人信息、对话要点、用户诉求、ai回复等)。因为对话默认不自动保存聊天记录,而是从记忆库搜索最相关的最新内容到"automemory"供决策,所以此次必须有所记忆,才能为后续多轮对话提供持续记忆基础。</content></remember>
54
54
  <recall>下一轮需要主动召回的记忆描述。填写需要从记忆库中检索的关键字或描述。如果不填写则为空。如果需要更多记忆支持当前任务,填写相关关键词(可包含时间参考,如"2025年1月的项目"),系统将在下一轮搜索top5相关记忆并通过"recall_memory"注入上下文。也可直接调用"recall_memory"工具即时搜索。</recall>
55
55
  <knowledge>从本轮对话或工具执行结果中提炼值得长期保存到知识库的专业知识、事实、经验法则、技术要点等,将被持久化存储,未来可通过 "get_knowledge"检索复用。如果本轮无需保存的知识,则为空。格式要求:简洁明确,每条知识一行,用换行分隔。</knowledge>
56
56
  <get_knowledge>下一轮执行时需要从知识库搜索获得的知识,填写检索关键词或描述。如context中已包含充足的knowledge内容,则为空。如需更多专业知识支撑,则填写相关搜索词。</get_knowledge>
@@ -593,6 +593,8 @@ class MainAgent(BaseAgent):
593
593
  )
594
594
 
595
595
  # Step 1: 构建 Context XML
596
+ # 获取 MemoryAgent 预加载的用户偏好/错误模式(如果有)
597
+ _memory_ctx_prompt = context.working_memory.get("memory_context_prompt", "")
596
598
  context_xml = self.context_builder.build_context(
597
599
  agent_name=agent_name,
598
600
  agent_description=agent_description,
@@ -604,6 +606,7 @@ class MainAgent(BaseAgent):
604
606
  agent_override_prompt=agent_override_prompt,
605
607
  get_knowledge=get_knowledge_content,
606
608
  recall=recall_content,
609
+ memory_context_prompt=_memory_ctx_prompt,
607
610
  )
608
611
 
609
612
  await self._emit_v2_event(
@@ -951,13 +954,26 @@ class MainAgent(BaseAgent):
951
954
  importance=0.7,
952
955
  )
953
956
  else:
954
- # === 会话记忆:直接存储 add_session ===
955
- self.memory.add_session(
957
+ # === 会话记忆:查重后存储(避免 "必须记忆" 导致大量重复) ===
958
+ _session_dup = self.memory.find_duplicate_memory(
959
+ content=parsed.remember,
956
960
  session_id=context.session_id,
957
961
  key="conversation_insight",
958
- content=parsed.remember,
959
- importance=0.6,
962
+ similarity_threshold=0.80,
963
+ category="session",
960
964
  )
965
+ if _session_dup:
966
+ logger.debug(
967
+ f"[{task_id}] 会话记忆查重: 跳过相似内容 "
968
+ f"(相似度>=0.80, 旧记忆ID={_session_dup.id})"
969
+ )
970
+ else:
971
+ self.memory.add_session(
972
+ session_id=context.session_id,
973
+ key="conversation_insight",
974
+ content=parsed.remember,
975
+ importance=0.6,
976
+ )
961
977
 
962
978
  await self._emit_v2_event(
963
979
  "v2_memory_saved",
@@ -124,6 +124,7 @@ class ContextBuilder:
124
124
  agent_override_prompt: Optional[str] = None,
125
125
  get_knowledge: str = "",
126
126
  recall: str = "",
127
+ memory_context_prompt: str = "",
127
128
  ) -> str:
128
129
  """
129
130
  构建完整的 <context> XML 字符串。
@@ -161,11 +162,10 @@ class ContextBuilder:
161
162
  sections: List[str] = [
162
163
  self._build_datetime(),
163
164
  self._build_whomi(agent_name, agent_description, agent_override_prompt),
164
- self._build_memory(query, session_id, recall),
165
+ self._build_memory(query, session_id, recall, memory_context_prompt),
165
166
  self._build_knowledge(kb_query),
166
- # [v1.15.6] 注释掉聊天历史注入 仅保留用户最新消息 + 工具回调,
167
- # 其他上下文由系统记忆(automemory/recall_memory)自行处理
168
- # self._build_recent_dialog(conversation_history, self.max_dialog_chars, session_id),
167
+ # 轻量近期对话兜底:最近 3 轮对话摘要,补充 automemory 搜索的盲区
168
+ self._build_recent_summary(session_id),
169
169
  self._build_user_input(user_typed_text, user_voice_text),
170
170
  self._build_task_plan(task_plan),
171
171
  self._build_tools(self.skill_registry),
@@ -242,7 +242,7 @@ class ContextBuilder:
242
242
  parts.append("</whomi>")
243
243
  return "\n".join(parts)
244
244
 
245
- def _build_memory(self, query: str, session_id: str, recall: str = "") -> str:
245
+ def _build_memory(self, query: str, session_id: str, recall: str = "", memory_context_prompt: str = "") -> str:
246
246
  """
247
247
  构建 <automemory> 和 <recall_memory> 段落 —— 双层记忆检索结果。
248
248
 
@@ -258,6 +258,7 @@ class ContextBuilder:
258
258
  query: 搜索查询文本(通常为最新用户消息)
259
259
  session_id: 会话 ID
260
260
  recall: LLM 上一轮输出的 <recall> 内容(关键字+时间描述)
261
+ memory_context_prompt: MemoryAgent 预加载的用户偏好/错误模式(直接注入)
261
262
 
262
263
  Returns:
263
264
  <automemory> + <recall_memory> XML 段落字符串
@@ -299,6 +300,9 @@ class ContextBuilder:
299
300
 
300
301
  if combined:
301
302
  auto_lines.append("<automemory>")
303
+ # 注入 MemoryAgent 预加载的用户偏好/错误模式
304
+ if memory_context_prompt and memory_context_prompt.strip():
305
+ auto_lines.append(_xml_escape(memory_context_prompt.strip()))
302
306
  for i, entry in enumerate(combined[:10], 1):
303
307
  content = entry.content.strip()
304
308
  if content:
@@ -438,6 +442,51 @@ class ContextBuilder:
438
442
  logger.warning(f"知识库 RAG 检索失败 ({kb_dir}): {e}")
439
443
  return ""
440
444
 
445
+ def _build_recent_summary(self, session_id: str) -> str:
446
+ """
447
+ 构建 <recent_summary> 段落 —— 最近几轮对话的轻量摘要。
448
+
449
+ 作为 automemory 搜索的兜底机制,确保 LLM 至少能看到最近几轮
450
+ 对话的基本脉络。只取最近 6 条(约 3 轮 user+assistant),
451
+ 每条截断到 200 字,总字符控制在 1500 以内。
452
+ """
453
+ if not self.memory_manager or not session_id:
454
+ return ""
455
+
456
+ try:
457
+ from core.utils import truncate_str
458
+ # 只取 user 和 assistant 角色,排除内部审计条目
459
+ entries = self.memory_manager.get_conversation(
460
+ session_id=session_id,
461
+ limit=6,
462
+ include_roles=["user", "assistant"],
463
+ )
464
+ if not entries:
465
+ return ""
466
+
467
+ role_labels = {"user": "用户", "assistant": "助手"}
468
+ lines = []
469
+ total_chars = 0
470
+ for entry in reversed(entries): # 最新的在前
471
+ content = (entry.content or "").strip()
472
+ if not content:
473
+ continue
474
+ label = role_labels.get(entry.role, entry.role)
475
+ # 截断过长内容
476
+ truncated = truncate_str(content, 200)
477
+ lines.append(f"{label}: {_xml_escape(truncated)}")
478
+ total_chars += len(truncated) + 10
479
+ if total_chars > 1500:
480
+ break
481
+
482
+ if not lines:
483
+ return ""
484
+
485
+ return "<recent_summary>\n" + "\n".join(lines) + "\n</recent_summary>"
486
+ except Exception as e:
487
+ logger.debug(f"recent_summary 构建失败: {e}")
488
+ return ""
489
+
441
490
  def _build_recent_dialog(
442
491
  self,
443
492
  conversation_history: List["Message"],
@@ -768,8 +817,9 @@ class ContextBuilder:
768
817
  replacement = f'<{tag}>\n(因 token 预算不足已裁剪)\n</{tag}>'
769
818
  return re.sub(pattern, replacement, xml, count=1, flags=re.DOTALL)
770
819
 
771
- # 按优先级从低到高裁剪
772
- for tag in ['knowledge', 'recall_memory', 'automemory']:
820
+ # 按优先级从低到高裁剪(记忆最优先保留,知识库和任务计划优先裁剪)
821
+ # recent_summary 是兜底机制,最先裁剪
822
+ for tag in ['recent_summary', 'knowledge', 'task_plan', 'recall_memory', 'automemory']:
773
823
  if estimated <= budget:
774
824
  break
775
825
  if f'<{tag}>' in context_xml:
package/memory/manager.py CHANGED
@@ -280,7 +280,8 @@ class MemoryManager:
280
280
  def add_session(self, session_id, role="", content="", key="", importance=0.5, metadata=None) -> str:
281
281
  """添加会话记忆。内容不包含时间前缀,时间仅存于 created_at 和 metadata。"""
282
282
  from datetime import datetime as _dt
283
- _now_str = _dt.now().strftime("%Y-%m-%d %H:%M:%S")
283
+ from core.utils import get_config_tz
284
+ _now_str = _dt.now(get_config_tz()).strftime("%Y-%m-%d %H:%M:%S")
284
285
  # 直接存储原始内容,不再注入时间前缀
285
286
  entry = MemoryEntry(
286
287
  session_id=session_id, category="session", role=role,
@@ -489,6 +490,7 @@ class MemoryManager:
489
490
  session_id: str = "",
490
491
  key: str = "",
491
492
  similarity_threshold: float = 0.85,
493
+ category: str = "global",
492
494
  ) -> Optional[MemoryEntry]:
493
495
  """
494
496
  查找与给定内容高度相似的已有记忆(查重)。
@@ -501,6 +503,7 @@ class MemoryManager:
501
503
  session_id: 会话 ID(为空则跨会话查重)
502
504
  key: 记忆分类键(为空则不限制分类)
503
505
  similarity_threshold: 相似度阈值,超过此值视为重复(默认 0.85)
506
+ category: 记忆类别(默认 "global",可设为 "session" 用于会话记忆查重)
504
507
 
505
508
  Returns:
506
509
  匹配的旧 MemoryEntry,或 None(无重复)
@@ -509,7 +512,7 @@ class MemoryManager:
509
512
  return None # 空内容不查重
510
513
 
511
514
  conn = self._get_conn()
512
- conditions = ["category = 'global'"]
515
+ conditions = [f"category = '{category}'"]
513
516
  params: list = []
514
517
 
515
518
  if session_id:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.15.82",
3
+ "version": "1.15.84",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
package/web/api_server.py CHANGED
@@ -884,7 +884,11 @@ class ApiServer:
884
884
 
885
885
  agent_path = data.get("agent_path", "default")
886
886
  raw_session_id = data.get("session_id", "web_default")
887
- session_id = f"{agent_path}_{raw_session_id}"
887
+ # Avoid double-prefixing: if the session_id already starts with agent_path_, use it directly
888
+ if raw_session_id.startswith(f"{agent_path}_"):
889
+ session_id = raw_session_id
890
+ else:
891
+ session_id = f"{agent_path}_{raw_session_id}"
888
892
  choice = data.get("choice", "queue") # "continue" (插入后继续) 或 "queue" (排队)
889
893
 
890
894
  # 检查会话是否正在运行
@@ -311,6 +311,11 @@ const StatePersistence = {
311
311
  if (savedRpSections) { Object.assign(rpSections, savedRpSections); }
312
312
  const savedNodes = StatePersistence.load('expandedNodes', ['default']);
313
313
  state.expandedNodes = new Set(savedNodes);
314
+ // 恢复上次活跃的会话 ID,供 loadSessions() 使用
315
+ var savedSessionId = StatePersistence.load('activeSessionId', null);
316
+ if (savedSessionId) {
317
+ state._pendingSessionRestore = savedSessionId;
318
+ }
314
319
  },
315
320
  /** 标记 setup 已完成(避免每次刷新弹出向导) */
316
321
  markSetupDone() { StatePersistence.save('setupDone', true); },
@@ -318,7 +323,7 @@ const StatePersistence = {
318
323
  };
319
324
 
320
325
  // ── Init ──
321
- function initChat() {
326
+ async function initChat() {
322
327
  // Initialize theme
323
328
  initTheme();
324
329
  // Restore persisted UI state
@@ -333,8 +338,6 @@ function initChat() {
333
338
  if (urlMode === 'chat' || urlMode === 'exec') {
334
339
  state.chatMode = urlMode;
335
340
  }
336
- // agent 参数先暂存,等 loadAgents() 完成后校验是否存在
337
- state._pendingUrlAgent = urlAgent;
338
341
 
339
342
  // Popout mode: hide sidebar, collapse agent panel, update title
340
343
  if (isPopout) {
@@ -347,9 +350,7 @@ function initChat() {
347
350
  if (agentPanel) agentPanel.classList.add('collapsed');
348
351
  const agentToggle = document.getElementById('agentToggle');
349
352
  if (agentToggle) agentToggle.style.display = 'none';
350
- // Update page title with agent name
351
- const agentObj = findAgentByPath(urlAgent);
352
- document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
353
+ // Update page title with agent name (after agents load)
353
354
  }
354
355
 
355
356
  // Apply restored state to DOM
@@ -357,7 +358,6 @@ function initChat() {
357
358
  if (panel) panel.classList.toggle('collapsed', !state.agentPanelOpen);
358
359
  if (state.chatMode === 'exec') setMode('exec');
359
360
  loadModels();
360
- loadAgents();
361
361
  loadStatus();
362
362
  loadChatVersion();
363
363
  fetchGroups();
@@ -380,49 +380,53 @@ function initChat() {
380
380
  loadTaskPlan();
381
381
  }
382
382
 
383
- // 如果 URL 指定了 agent 或 session,等 agent 列表加载后自动选中
384
- // 注意:loadSessions() 内部会检查 URL session 参数并自动恢复
383
+ // 页面卸载前保存 UI 状态(包括活跃 session
384
+ window.addEventListener('beforeunload', function() {
385
+ StatePersistence.saveUIState();
386
+ });
387
+
388
+ // ── 核心:先等待 loadAgents 完成(内含 loadSessions),再做 agent/session 选择 ──
389
+ // 这样避免了 setTimeout 竞态条件导致 sendMessage 和 selectAgent 互相干扰
390
+ await loadAgents();
391
+
392
+ // loadAgents 完成后,agent 列表和 sessions 已加载完毕
393
+ // 现在可以安全地做 agent/session 选择(不会再和 sendMessage 竞态)
394
+ if (isPopout) {
395
+ const agentObj = findAgentByPath(urlAgent);
396
+ document.title = (agentObj ? agentObj.name : urlAgent || 'MyAgent') + ' - MyAgent';
397
+ }
398
+
385
399
  if (urlAgent) {
386
- setTimeout(function() {
387
- // 校验 agent 是否真实存在,不存在则回退 default
388
- var resolved = urlAgent;
389
- if (!findAgentByPath(resolved)) {
390
- console.warn('URL agent not found, fallback to default:', resolved);
391
- resolved = 'default';
392
- }
400
+ // URL 指定了 agent,校验后切换
401
+ var resolved = urlAgent;
402
+ if (!findAgentByPath(resolved)) {
403
+ console.warn('URL agent not found, fallback to default:', resolved);
404
+ resolved = 'default';
405
+ }
406
+ if (resolved !== state.activeAgent) {
393
407
  selectAgent(resolved);
394
- }, 500);
408
+ }
395
409
  } else if (urlSession) {
396
410
  // 只有 session 没有 agent,尝试从 session ID 推断 agent
397
411
  const targetAgent = urlSession.split('_web_')[0] || 'default';
398
- setTimeout(function() {
399
- if (!findAgentByPath(targetAgent)) {
400
- console.warn('Session-inferred agent not found, fallback to default:', targetAgent);
401
- selectAgent('default');
402
- } else {
403
- selectAgent(targetAgent);
404
- }
405
- }, 500);
412
+ var target = targetAgent;
413
+ if (!findAgentByPath(target)) {
414
+ console.warn('Session-inferred agent not found, fallback to default:', target);
415
+ target = 'default';
416
+ }
417
+ if (target !== state.activeAgent) {
418
+ selectAgent(target);
419
+ }
420
+ // 如果 agent 已一致,loadSessions 内部已通过 URL session 参数自动恢复了
406
421
  } else {
407
- // URL 中没有 session 参数,尝试从 localStorage 恢复上次的会话
408
- var savedSessionId = StatePersistence.load('activeSessionId', null);
422
+ // URL 中没有 agent/session 参数,尝试从 localStorage 恢复上次的会话
409
423
  var savedSessionAgent = StatePersistence.load('activeSessionAgent', null);
410
- if (savedSessionId && savedSessionAgent) {
411
- // 确保 agent 一致,然后延迟等待 loadSessions() 完成后恢复
412
- state._pendingSessionRestore = savedSessionId;
413
- if (savedSessionAgent !== state.activeAgent) {
414
- setTimeout(function() {
415
- selectAgent(savedSessionAgent);
416
- }, 500);
417
- }
418
- // 如果 agent 已经一致,loadSessions() 内部会自动处理
424
+ if (savedSessionAgent && savedSessionAgent !== state.activeAgent) {
425
+ // agent 不一致,需要切换(loadSessions 内部已通过 _pendingSessionRestore 处理 session 恢复)
426
+ selectAgent(savedSessionAgent);
419
427
  }
428
+ // 如果 agent 一致,loadSessions() 内部已通过 _pendingSessionRestore 自动处理了
420
429
  }
421
-
422
- // 页面卸载前保存 UI 状态(包括活跃 session)
423
- window.addEventListener('beforeunload', function() {
424
- StatePersistence.saveUIState();
425
- });
426
430
  }
427
431
 
428
432
  // Run init: if DOMContentLoaded already fired (dynamic script load), run immediately
@@ -1382,6 +1382,7 @@ async function sendMessage(opts) {
1382
1382
  let allExecEvents = []; // All exec events (for summary panel at bottom)
1383
1383
  let msgIdx = state.messages.length;
1384
1384
  let sessionIdReceived = sessionId;
1385
+ let sessionIdOriginal = sessionId; // 保存前端原始 ID,用于后端返回不同 ID 时的回退查找
1385
1386
  let fullThought = '';
1386
1387
  let _isV2Mode = false; // Track whether V2 structured output events are received
1387
1388
  let _v2RawXml = ''; // In V2 mode, accumulate raw XML from text_delta separately
@@ -1857,8 +1858,11 @@ async function sendMessage(opts) {
1857
1858
  state.sessions.unshift(existing);
1858
1859
  } else {
1859
1860
  // 防御:后端返回的 sessionIdReceived 和前端的不一致时,
1860
- // 找到当前 activeSessionId 的条目,更新其 ID 而非创建新条目
1861
- var localIdx = state.sessions.findIndex(s => s.id === state.activeSessionId);
1861
+ // 优先用前端原始 ID 查找,再用 activeSessionId 查找
1862
+ var localIdx = state.sessions.findIndex(s => s.id === sessionIdOriginal);
1863
+ if (localIdx < 0) {
1864
+ localIdx = state.sessions.findIndex(s => s.id === state.activeSessionId);
1865
+ }
1862
1866
  if (localIdx >= 0) {
1863
1867
  var localEntry = state.sessions[localIdx];
1864
1868
  localEntry.id = sessionIdReceived;
@@ -1869,6 +1873,7 @@ async function sendMessage(opts) {
1869
1873
  state.sessions.unshift(localEntry);
1870
1874
  console.warn('[sendMessage] Session ID mismatch, updated local entry:', sessionIdReceived);
1871
1875
  } else {
1876
+ // 真正的新会话(本地列表中不存在)
1872
1877
  state.sessions.unshift({ id: sessionIdReceived, name: formatSessionName(sessionIdReceived), messages: 2, last: new Date().toISOString() });
1873
1878
  }
1874
1879
  }