Files
beaver_project/app-instance/backend/beaver/engine/session/manager.py
steven_li 30ab74ffb2 feat(engine): 添加MCP连接管理和工具集成功能
- 集成MCP连接管理器,支持MCP服务器连接
- 添加多种内置工具:ClarifyTool、CronTool、DelegateTool、ExecuteCodeTool、
  PatchFileTool、ProcessTool、SendMessageTool、SpawnTool、TerminalTool、
  TodoTool、WebFetchTool、WebSearchTool、WriteFileTool等
- 实现工具注册和装配功能
- 添加技能选择上下文参数
- 支持思考模式控制参数thinking_enabled

feat(coordinator): 重构任务执行计划器参数命名

- 将learning_candidate_enabled重命名为allow_candidate_generation
- 更新TeamGraphScheduler中的参数传递
- 修改LocalAgentRunner中的相关参数处理
- 更新README文档中的相应描述

refactor(context): 标准化工具调用参数格式

- 添加_json导入用于参数序列化
- 实现_provider_tool_calls方法标准化OpenAI兼容的工具调用载荷
- 修复工具调用中参数非字符串类型的序列化问题

refactor(session): 优化消息历史记录过滤逻辑

- 修改get_messages_as_conversation为基于运行状态过滤消息
- 排除未完成、失败或错误结束的运行记录
- 改进对话历史的可见性控制机制

fix(store): 修复FTS索引重建逻辑

- 添加异常处理防止FTS索引创建失败
- 实现_rebuild_fts_index方法重新构建全文搜索索引
- 优化索引触发器和表的维护流程
2026-05-14 09:43:48 +08:00

187 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Beaver session 子系统对 runtime 暴露的统一门面。"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from .models import MessageRecord
from .search import SessionSearchService
from .store import SessionStore
class SessionManager:
"""供 AgentLoop / services / MCP tools 使用的统一 session facade。"""
def __init__(self, workspace: str | Path, db_path: str | Path | None = None) -> None:
self.workspace = Path(workspace)
self.sessions_dir = self.workspace / "sessions"
self.sessions_dir.mkdir(parents=True, exist_ok=True)
self.db_path = Path(db_path) if db_path is not None else self.sessions_dir / "state.db"
self.store = SessionStore(self.db_path)
self.search = SessionSearchService(self.store)
def close(self) -> None:
self.store.close()
def ensure_session(
self,
session_id: str,
*,
source: str = "unknown",
model: str | None = None,
title: str | None = None,
user_id: str | None = None,
parent_session_id: str | None = None,
) -> str:
return self.store.ensure_session(
session_id,
source=source,
model=model,
title=title,
user_id=user_id,
parent_session_id=parent_session_id,
)
def get_session(self, session_id: str) -> dict[str, Any] | None:
record = self.store.get_session_record(session_id)
return record.to_dict() if record is not None else None
def get_or_create(
self,
session_id: str,
*,
source: str = "unknown",
model: str | None = None,
title: str | None = None,
user_id: str | None = None,
parent_session_id: str | None = None,
) -> dict[str, Any]:
self.ensure_session(
session_id,
source=source,
model=model,
title=title,
user_id=user_id,
parent_session_id=parent_session_id,
)
session = self.get_session(session_id)
if session is None:
raise RuntimeError(f"Failed to create session {session_id!r}")
return session
def append_message(self, session_id: str, **kwargs: Any) -> int:
return self.store.append_message(session_id, **kwargs)
def get_event_records(self, session_id: str) -> list[MessageRecord]:
"""返回当前 session 的完整事件流。
这里和 `get_messages_as_conversation()` 的区别很关键:
- `get_event_records()` 面向 runtime / replay / audit保留隐藏系统事件
- `get_messages_as_conversation()` 面向 prompt builder只暴露可进上下文的事件
第 6 阶段开始后session 已不再只是“聊天消息存储”,而是在逐步收敛成
“外部事件流 + 上层投影视图”。
"""
return self.store.get_event_records(session_id)
def get_run_event_records(self, session_id: str, run_id: str) -> list[MessageRecord]:
"""返回某一次 direct run / future bus run 对应的事件片段。"""
return self.store.get_run_event_records(session_id, run_id)
def update_latest_assistant_event_payload(
self,
session_id: str,
run_id: str,
updates: dict[str, Any],
) -> None:
"""把 run 级 UI 状态投影回最新 assistant 可见消息。"""
self.store.update_latest_assistant_event_payload(session_id, run_id, updates)
def set_run_context_visible(self, session_id: str, run_id: str, visible: bool) -> None:
self.store.set_run_context_visible(session_id, run_id, visible)
def list_run_ids(self, session_id: str) -> list[str]:
"""按出现顺序列出当前 session 的所有 run_id。"""
return self.store.list_run_ids(session_id)
def get_messages_as_conversation(self, session_id: str) -> list[dict[str, Any]]:
return self.store.get_messages_as_conversation(session_id)
def get_visible_history(self, session_id: str, max_messages: int = 500) -> list[dict[str, Any]]:
"""返回适合注入 prompt 的可见历史切片。
这里故意不直接暴露完整事件流,而是继续提供“模型可消费历史”这个投影视图:
1. 只包含 `context_visible=True` 的事件
2. 继续保留旧式窗口裁剪逻辑,避免当前主链行为突然变化
3. 让 `ContextBuilder` 明确消费的是“上游裁剪后的可见片段”
"""
records = self.get_event_records(session_id)
completed_run_ids = {
record.run_id
for record in records
if record.run_id and record.event_type == "run_completed"
}
failed_run_ids = {
record.run_id
for record in records
if record.run_id
and record.event_type == "run_completed"
and (
record.finish_reason == "error"
or (record.event_payload or {}).get("finish_reason") == "error"
)
}
history = []
for record in records:
if not record.context_visible or record.role == "system":
continue
if record.role == "tool":
continue
if record.role == "assistant" and record.tool_calls:
continue
if record.run_id and record.run_id not in completed_run_ids:
continue
if record.run_id and record.run_id in failed_run_ids:
continue
if record.role == "assistant" and record.finish_reason == "error":
continue
history.append(record.to_conversation_message())
sliced = history[-max_messages:]
for index, message in enumerate(sliced):
if message.get("role") == "user":
sliced = sliced[index:]
break
return sliced
def get_history(self, session_id: str, max_messages: int = 500) -> list[dict[str, Any]]:
"""兼容旧名称,实际返回可见历史切片。"""
return self.get_visible_history(session_id, max_messages=max_messages)
def update_system_prompt(self, session_id: str, system_prompt: str) -> None:
self.store.update_system_prompt(session_id, system_prompt)
def update_usage(self, session_id: str, **kwargs: Any) -> None:
self.store.update_usage(session_id, **kwargs)
def end_session(self, session_id: str, end_reason: str) -> None:
self.store.end_session(session_id, end_reason)
def reopen_session(self, session_id: str) -> None:
self.store.reopen_session(session_id)
def list_sessions_rich(self, **kwargs: Any) -> list[dict[str, Any]]:
return self.search.list_sessions_rich(**kwargs)
def search_messages(self, **kwargs: Any) -> list[dict[str, Any]]:
return self.search.search_messages(**kwargs)
def resolve_session_id(self, session_id_or_prefix: str) -> str | None:
return self.search.resolve_session_id(session_id_or_prefix)