- 集成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方法重新构建全文搜索索引 - 优化索引触发器和表的维护流程
187 lines
7.0 KiB
Python
187 lines
7.0 KiB
Python
"""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)
|