Files
beaver_project/app-instance/backend/beaver/engine/session/manager.py
steven_li 8a12c30141 feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证
(通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。

实现Agent Team v1协调器,支持sequence/parallel/dag执行策略,
sub-agent复用主AgentLoop,每个run使用独立memory snapshot。

建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期,
通过Task验证通过且用户满意才生成学习候选。

重构目录结构,移除third_party依赖,建立统一engine内核,
所有agent共享运行时基础组件。

更新ContextBuilder清理provider消息字段,增强SkillContext版本管理,
集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
2026-05-08 17:14:14 +08:00

157 lines
5.9 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` 明确消费的是“上游裁剪后的可见片段”
"""
history = self.get_messages_as_conversation(session_id)
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)