myagent-ai 1.13.0 → 1.13.1

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.
@@ -454,7 +454,11 @@ class MainAgent(BaseAgent):
454
454
  )
455
455
  if db_history:
456
456
  conversation_history = [
457
- Message(role=entry.role, content=entry.content)
457
+ Message(
458
+ role=entry.role,
459
+ content=entry.content,
460
+ metadata={"time": (entry.created_at[:19] if entry.created_at else "")}
461
+ )
458
462
  for entry in db_history
459
463
  ]
460
464
  logger.info(f"[{task_id}] 从 DB 加载了 {len(conversation_history)} 条历史对话")
@@ -415,7 +415,12 @@ class ContextBuilder:
415
415
  if not content.strip():
416
416
  continue
417
417
  label = role_labels.get(role, role)
418
- filtered_msgs.append((label, content.strip()))
418
+ # 从 metadata 中提取时间(DB加载时已附带)
419
+ msg_time = ""
420
+ msg_meta = getattr(msg, "metadata", None)
421
+ if isinstance(msg_meta, dict):
422
+ msg_time = msg_meta.get("time", "")
423
+ filtered_msgs.append((label, content.strip(), msg_time))
419
424
 
420
425
  if not filtered_msgs:
421
426
  return "<resentdialog>\n(无对话历史)\n</resentdialog>"
@@ -439,8 +444,12 @@ class ContextBuilder:
439
444
  formatted_lines.append(prefix_text)
440
445
  formatted_lines.append("") # 空行分隔
441
446
 
442
- for label, content in recent_msgs:
443
- formatted_lines.append(f"[{label}] {_xml_escape(content)}")
447
+ for label, content, msg_time in recent_msgs:
448
+ # 临时合并时间信息到内容中给 LLM 参考
449
+ if msg_time:
450
+ formatted_lines.append(f"[{label}] [{msg_time}] {_xml_escape(content)}")
451
+ else:
452
+ formatted_lines.append(f"[{label}] {_xml_escape(content)}")
444
453
 
445
454
  dialog_text = "\n".join(formatted_lines)
446
455
 
@@ -482,7 +491,9 @@ class ContextBuilder:
482
491
  return ""
483
492
 
484
493
  summary_parts: List[str] = ["[历史对话摘要]"]
485
- for label, content in old_msgs:
494
+ for item in old_msgs:
495
+ label = item[0]
496
+ content = item[1]
486
497
  # 提取第一行或前100字符作为要点
487
498
  first_line = content.split("\n")[0].strip()
488
499
  if len(first_line) > 100:
package/memory/manager.py CHANGED
@@ -278,17 +278,13 @@ class MemoryManager:
278
278
  # ==========================================================================
279
279
 
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
283
  _now_str = _dt.now().strftime("%Y-%m-%d %H:%M:%S")
284
- # 对话类记忆(user/assistant/system/tool)自动加时间前缀
285
- if role and content and not content.startswith("["):
286
- timestamped_content = f"[{_now_str}] {content}"
287
- else:
288
- timestamped_content = truncate_str(content, 50000)
284
+ # 直接存储原始内容,不再注入时间前缀
289
285
  entry = MemoryEntry(
290
286
  session_id=session_id, category="session", role=role,
291
- content=timestamped_content, key=key,
287
+ content=truncate_str(content, 50000), key=key,
292
288
  importance=importance, metadata={"timestamp": _now_str, **(metadata or {})},
293
289
  )
294
290
  return self._insert(entry)
@@ -366,7 +362,7 @@ class MemoryManager:
366
362
  session_id: str,
367
363
  limit: int = 50,
368
364
  ) -> str:
369
- """获取对话历史文本(供 LLM 使用)"""
365
+ """获取对话历史文本(供 LLM 使用),临时合并时间信息"""
370
366
  entries = self.get_conversation(session_id, limit)
371
367
  lines = []
372
368
  for e in entries:
@@ -379,7 +375,12 @@ class MemoryManager:
379
375
  label = "系统"
380
376
  elif e.role == "tool":
381
377
  label = "工具"
382
- lines.append(f"[{label}] {e.content}")
378
+ # 从 created_at 提取时间,临时合并到内容中给 LLM
379
+ time_str = e.created_at[:19] if e.created_at and len(e.created_at) >= 19 else ""
380
+ if time_str:
381
+ lines.append(f"[{label}] [{time_str}] {e.content}")
382
+ else:
383
+ lines.append(f"[{label}] {e.content}")
383
384
  return "\n".join(lines)
384
385
 
385
386
  def clear_conversation(self, session_id) -> int:
@@ -469,11 +470,10 @@ class MemoryManager:
469
470
  """添加全局记忆(跨会话可检索)"""
470
471
  from datetime import datetime
471
472
  now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
472
- timestamped_content = f"[{now_str}] {truncate_str(content, 50000)}"
473
473
  ts_summary = summary or truncate_str(content, 200)
474
474
  entry = MemoryEntry(
475
475
  session_id=session_id, category="global", key=key,
476
- content=timestamped_content, summary=f"[{now_str}] {ts_summary}",
476
+ content=truncate_str(content, 50000), summary=f"[{now_str}] {ts_summary}",
477
477
  importance=importance, metadata={"timestamp": now_str, **(metadata or {})},
478
478
  )
479
479
  return self._insert(entry)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {
@@ -2005,7 +2005,6 @@ body.popout-mode .main{margin-left:0 !important;border-left:none !important}
2005
2005
  body.popout-mode .agent-panel{display:none !important}
2006
2006
  body.popout-mode .main-header{padding-left:12px}
2007
2007
  body.popout-mode #popoutBtn{display:none !important}
2008
- body.popout-mode #debugToggleBtn{display:none !important}
2009
2008
 
2010
2009
  /* ══════════════════════════════════════════════════════
2011
2010
  ── Mobile Responsive (≤768px) ──
@@ -2547,32 +2547,27 @@ function formatTime(timeStr) {
2547
2547
  }
2548
2548
  }
2549
2549
 
2550
+ // ── User scroll lock: when user manually scrolls up, stop auto-scrolling ──
2551
+ var _userScrollLocked = false;
2552
+
2550
2553
  function scrollToBottom(force) {
2551
2554
  const c = document.getElementById('messagesContainer');
2552
2555
  if (!c) return;
2553
- // During streaming: pin the active assistant message to the top of the chat window
2554
- // so the user can see the full response content below
2555
- const isStreaming = state.isGenerating;
2556
- if (isStreaming && !force) {
2557
- const activeRow = c.querySelector('.message-row.assistant.streaming, .message-row.assistant:last-of-type');
2558
- if (activeRow) {
2559
- requestAnimationFrame(() => {
2560
- const rowTop = activeRow.offsetTop;
2561
- // Scroll so the assistant row sits at the very top of the visible area
2562
- c.scrollTop = rowTop;
2563
- updateScrollToBottomBtn(c.scrollHeight - c.scrollTop - c.clientHeight);
2564
- });
2565
- return;
2566
- }
2567
- }
2568
2556
  requestAnimationFrame(() => {
2569
- // Smart scroll: only auto-scroll if user is near bottom (within 120px)
2570
- // or if force is true
2571
2557
  const distFromBottom = c.scrollHeight - c.scrollTop - c.clientHeight;
2558
+ // If user has manually scrolled away, don't auto-scroll (unless forced)
2559
+ if (!force && _userScrollLocked) {
2560
+ updateScrollToBottomBtn(distFromBottom);
2561
+ return;
2562
+ }
2563
+ // Smart scroll: only auto-scroll if near bottom (within 120px) or forced
2572
2564
  if (force || distFromBottom < 120) {
2573
2565
  c.scrollTop = c.scrollHeight;
2566
+ _userScrollLocked = false;
2567
+ } else {
2568
+ // User is far from bottom — lock auto-scroll
2569
+ _userScrollLocked = true;
2574
2570
  }
2575
- // Update scroll-to-bottom button visibility
2576
2571
  updateScrollToBottomBtn(distFromBottom);
2577
2572
  });
2578
2573
  }
@@ -2603,7 +2598,7 @@ function initScrollToBottomBtn() {
2603
2598
  btn.id = 'scrollToBottomBtn';
2604
2599
  btn.className = 'scroll-to-bottom-btn';
2605
2600
  btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12l7 7 7-7"/></svg>';
2606
- btn.onclick = function() { scrollToBottom(true); };
2601
+ btn.onclick = function() { _userScrollLocked = false; scrollToBottom(true); };
2607
2602
  // Insert into the main area (parent of messagesContainer)
2608
2603
  const mainArea = container.parentElement;
2609
2604
  if (mainArea) {
@@ -2611,9 +2606,14 @@ function initScrollToBottomBtn() {
2611
2606
  mainArea.appendChild(btn);
2612
2607
  }
2613
2608
 
2614
- // Listen for manual scroll to show/hide button
2609
+ // Listen for manual scroll to show/hide button and detect user scroll-away
2615
2610
  container.addEventListener('scroll', function() {
2616
- updateScrollToBottomBtn();
2611
+ const dist = container.scrollHeight - container.scrollTop - container.clientHeight;
2612
+ // Lock auto-scroll when user scrolls more than 120px away from bottom
2613
+ if (dist > 120) {
2614
+ _userScrollLocked = true;
2615
+ }
2616
+ updateScrollToBottomBtn(dist);
2617
2617
  }, { passive: true });
2618
2618
  }
2619
2619
 
@@ -855,6 +855,41 @@ function toggleExecEventsPanel(header) {
855
855
  // ── Inline Exec Event (Timeline Card) ──
856
856
  // ══════════════════════════════════════════════════════
857
857
 
858
+ // Update an existing V2 tool card in the DOM (replace spinner with result icon)
859
+ function _updateToolCardInDOM(msgIdx, partIdx) {
860
+ var container = document.getElementById('messagesInner');
861
+ if (!container) return;
862
+ // Find the target message row
863
+ var allRows = container.querySelectorAll('.message-row');
864
+ var targetRow = null;
865
+ var rowCount = 0;
866
+ for (var ri = 0; ri < allRows.length; ri++) {
867
+ if (rowCount === msgIdx) { targetRow = allRows[ri]; break; }
868
+ rowCount++;
869
+ }
870
+ if (!targetRow) return;
871
+ // Find the timeline inside the message
872
+ var timeline = targetRow.querySelector('.msg-timeline');
873
+ if (!timeline) return;
874
+ // Find all V2 tool event cards in the timeline
875
+ var toolCards = timeline.querySelectorAll('.v2-tool-event');
876
+ // partIdx is the index in msgParts — count only v2_tool parts to find the right card
877
+ var msg = state.messages[msgIdx];
878
+ if (!msg || !msg.parts) return;
879
+ var toolPartCount = 0;
880
+ var targetCard = null;
881
+ for (var ti = 0; ti < msg.parts.length && ti <= partIdx; ti++) {
882
+ if (msg.parts[ti].type === 'v2_tool') {
883
+ if (ti === partIdx) { targetCard = toolCards[toolPartCount]; break; }
884
+ toolPartCount++;
885
+ }
886
+ }
887
+ if (!targetCard) return;
888
+ // Re-render the card with updated data
889
+ var updatedHtml = renderInlineExecEvent(msg.parts[partIdx], msgIdx);
890
+ targetCard.outerHTML = updatedHtml;
891
+ }
892
+
858
893
  function renderInlineExecEvent(data, msgIdx) {
859
894
  // V2 Tool Event handling (called with full part: {type:'v2_tool', data:{...}})
860
895
  if (data.type === 'v2_tool') {
@@ -1349,6 +1384,7 @@ async function sendMessage() {
1349
1384
  fullThought = '';
1350
1385
  state.messages.push({ role: 'assistant', content: '', thought: '', parts: [], time: new Date().toISOString(), streaming: true });
1351
1386
  renderMessages();
1387
+ _userScrollLocked = false;
1352
1388
  scrollToBottom(true); // Force scroll for new message
1353
1389
  } else if (evt.type === 'clear_text') {
1354
1390
  // Clear intermediate text from previous agent loop iterations
@@ -1437,36 +1473,66 @@ async function sendMessage() {
1437
1473
  state.messages[msgIdx].exec_events = [...allExecEvents];
1438
1474
  throttledStreamUpdate(msgIdx);
1439
1475
  } else if (evt.type === 'v2_tool_result') {
1440
- // Tool execution completed
1441
- // Stop the tool timer
1442
- if (evt.tool && evt.tool.toolname) {
1443
- // Find matching timer by checking all active timers
1444
- for (var tId in _toolTimers) {
1445
- stopToolTimer(tId);
1446
- }
1447
- }
1448
- // evt.tool contains: {beforecalltext, toolname, ...}
1449
- // evt.result contains: {success, output, error, ...}
1476
+ // Tool execution completed — find and UPDATE the matching start card
1450
1477
  var _r = evt.result || {};
1451
1478
  var _t = evt.tool || {};
1452
- var resultEvent = {
1453
- type: 'v2_tool',
1454
- data: {
1455
- id: 'v2tool_' + Date.now() + '_' + allExecEvents.length,
1456
- type: 'tool_result',
1457
- title: (_t.toolname || '工具') + ' 执行完成',
1458
- tool_name: _t.toolname,
1459
- success: !!_r.success,
1460
- summary: (_r.output || _r.error || '').substring(0, 500),
1461
- result: _r,
1462
- callback: _t.callback
1479
+ var _toolName = _t.toolname || '';
1480
+ // Find the matching tool_start part in msgParts by tool_name
1481
+ var _matchedIdx = -1;
1482
+ for (var _pi = msgParts.length - 1; _pi >= 0; _pi--) {
1483
+ if (msgParts[_pi].type === 'v2_tool' && msgParts[_pi].data.tool_name === _toolName && msgParts[_pi].data.status === 'running') {
1484
+ _matchedIdx = _pi;
1485
+ break;
1463
1486
  }
1464
- };
1465
- msgParts.push(resultEvent);
1466
- allExecEvents.push(resultEvent.data);
1467
- state.messages[msgIdx].parts = [...msgParts];
1468
- state.messages[msgIdx].exec_events = [...allExecEvents];
1469
- throttledStreamUpdate(msgIdx);
1487
+ }
1488
+ if (_matchedIdx >= 0) {
1489
+ // Update the existing start card in-place
1490
+ var _startData = msgParts[_matchedIdx].data;
1491
+ // Stop timer using the start card's original ID
1492
+ stopToolTimer(_startData.id);
1493
+ // Update the part data to reflect completion
1494
+ _startData.status = 'done';
1495
+ _startData.type = 'tool_result';
1496
+ _startData.success = !!_r.success;
1497
+ _startData.summary = (_r.output || _r.error || '').substring(0, 500);
1498
+ _startData.result = _r;
1499
+ _startData.title = (_t.toolname || '工具') + ' 执行完成';
1500
+ // Update in allExecEvents too
1501
+ for (var _ei = allExecEvents.length - 1; _ei >= 0; _ei--) {
1502
+ if (allExecEvents[_ei].id === _startData.id) {
1503
+ Object.assign(allExecEvents[_ei], _startData);
1504
+ break;
1505
+ }
1506
+ }
1507
+ // Force re-render this specific card in the DOM
1508
+ _updateToolCardInDOM(msgIdx, _matchedIdx);
1509
+ state.messages[msgIdx].parts = [...msgParts];
1510
+ state.messages[msgIdx].exec_events = [...allExecEvents];
1511
+ } else {
1512
+ // Fallback: no matching start found, push as new part
1513
+ var _fallbackTimerIds = Object.keys(_toolTimers);
1514
+ for (var _fi = 0; _fi < _fallbackTimerIds.length; _fi++) {
1515
+ stopToolTimer(_fallbackTimerIds[_fi]);
1516
+ }
1517
+ var resultEvent = {
1518
+ type: 'v2_tool',
1519
+ data: {
1520
+ id: 'v2tool_' + Date.now() + '_' + allExecEvents.length,
1521
+ type: 'tool_result',
1522
+ title: (_t.toolname || '工具') + ' 执行完成',
1523
+ tool_name: _t.toolname,
1524
+ success: !!_r.success,
1525
+ summary: (_r.output || _r.error || '').substring(0, 500),
1526
+ result: _r,
1527
+ callback: _t.callback
1528
+ }
1529
+ };
1530
+ msgParts.push(resultEvent);
1531
+ allExecEvents.push(resultEvent.data);
1532
+ state.messages[msgIdx].parts = [...msgParts];
1533
+ state.messages[msgIdx].exec_events = [...allExecEvents];
1534
+ throttledStreamUpdate(msgIdx);
1535
+ }
1470
1536
  } else if (evt.type === 'v2_task_plan') {
1471
1537
  // Updated task plan from V2 output
1472
1538
  if (evt.plan) {