Coverage for merco/core/session.py: 90%
68 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-07 14:04 +0800
1"""会话管理"""
3import logging
4import uuid
5from datetime import datetime
7_logger = logging.getLogger("merco.session")
10class Session:
11 """单次对话会话 — 数据容器,对接 SessionStore 持久化"""
13 def __init__(self, session_id: str = None, title: str = "", store=None):
14 self.id = session_id or _new_id()
15 self.title = title
16 self.messages: list[dict] = []
17 self.metadata: dict = {}
18 self._store = store
19 self._dirty = False # 有未持久化的消息
21 # ── 消息 ──────────────────────────────────────────────
23 def add_message(self, role: str, content: str, **kwargs):
24 """添加消息。不立即写磁盘(由 agent 循环结束时统一 save)"""
25 r = kwargs.get("reasoning", "")
26 if r:
27 _logger.debug("add_message(%s): reasoning=%d chars, content=%d chars",
28 role, len(r), len(content))
29 msg = {"role": role, "content": content}
30 msg.update(kwargs)
31 self.messages.append(msg)
32 self._dirty = True
34 def get_history(self) -> list[dict]:
35 return self.messages.copy()
37 # ── 持久化 ────────────────────────────────────────────
39 def save(self):
40 """将未持久化的消息写入 SQLite。增量:DB 已有 N 条,只写 messages[N:]"""
41 if not self._store or not self._dirty:
42 return
44 self._store.create_session(self.id, self.title)
46 existing = self._store.count_messages(self.id)
47 for msg in self.messages[existing:]:
48 self._store.save_message(
49 session_id=self.id,
50 role=msg.get("role", ""),
51 content=msg.get("content", ""),
52 tool_call_id=msg.get("tool_call_id", ""),
53 tool_calls=msg.get("tool_calls"),
54 reasoning=msg.get("reasoning", ""),
55 )
56 self._dirty = False
58 def delete(self):
59 if self._store:
60 self._store.delete_session(self.id)
62 # ── 工厂 ──────────────────────────────────────────────
64 @classmethod
65 def load(cls, session_id: str, store) -> "Session | None":
66 """从 store 加载完整会话(含历史消息)"""
67 data = store.load_session(session_id)
68 if not data:
69 return None
71 s = cls(session_id=data["id"], title=data["title"], store=store)
72 s.messages = data["messages"]
73 s.metadata = data.get("metadata", {})
74 s._dirty = False
75 return s
77 @classmethod
78 def fork(cls, session_id: str, store, title: str = None) -> "Session | None":
79 """从 session_id 克隆新会话。返回新 Session 或 None。"""
80 try:
81 new_id = store.clone_session(session_id)
82 except ValueError:
83 return None
84 if title is not None:
85 store.set_title(new_id, title)
86 return cls.load(new_id, store)
88 @classmethod
89 def resume_or_create(cls, store, session_id: str = None) -> "Session":
90 """恢复指定会话,或自动恢复上次,或新建"""
91 if session_id:
92 s = cls.load(session_id, store)
93 if s:
94 return s
95 # 自动恢复上次会话
96 recent = store.list_sessions(limit=1)
97 if recent:
98 s = cls.load(recent[0]["id"], store)
99 if s:
100 return s
101 # 新建
102 s = cls(store=store)
103 store.create_session(s.id)
104 return s
107def _new_id() -> str:
108 return str(uuid.uuid4())[:8]