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.
- package/agents/main_agent.py +5 -1
- package/core/context_builder.py +15 -4
- package/memory/manager.py +11 -11
- package/package.json +1 -1
- package/web/ui/chat/chat.css +0 -1
- package/web/ui/chat/chat_main.js +21 -21
- package/web/ui/chat/flow_engine.js +93 -27
package/agents/main_agent.py
CHANGED
|
@@ -454,7 +454,11 @@ class MainAgent(BaseAgent):
|
|
|
454
454
|
)
|
|
455
455
|
if db_history:
|
|
456
456
|
conversation_history = [
|
|
457
|
-
Message(
|
|
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)} 条历史对话")
|
package/core/context_builder.py
CHANGED
|
@@ -415,7 +415,12 @@ class ContextBuilder:
|
|
|
415
415
|
if not content.strip():
|
|
416
416
|
continue
|
|
417
417
|
label = role_labels.get(role, role)
|
|
418
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
package/web/ui/chat/chat.css
CHANGED
|
@@ -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) ──
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
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
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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) {
|