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实现技能解析机制。
This commit is contained in:
2026-05-08 17:14:14 +08:00
parent 5ba5c7e4c1
commit 8a12c30141
93 changed files with 16724 additions and 1247 deletions

View File

@ -42,6 +42,10 @@ class SkillContext:
name: str
content: str
version: str = "legacy"
content_hash: str = ""
activation_reason: str = "selected"
tool_hints: list[str] = field(default_factory=list)
@dataclass(slots=True)
@ -197,7 +201,7 @@ class ContextBuilder:
# 如果上游 history 已经混入 system 消息,这里要主动跳过,避免双 system。
if message.get("role") == "system":
continue
messages.append(dict(message))
messages.append(self._provider_history_message(message))
if build_input.current_user_input is not None:
messages.append(
@ -212,6 +216,16 @@ class ContextBuilder:
messages=messages,
)
@staticmethod
def _provider_history_message(message: dict[str, Any]) -> dict[str, Any]:
"""Keep persisted UI/audit fields out of provider message payloads."""
allowed = {"role", "content", "tool_calls", "tool_call_id", "name"}
clean = {key: value for key, value in message.items() if key in allowed}
if "name" not in clean and message.get("tool_name"):
clean["name"] = message.get("tool_name")
return clean
def add_tool_result(
self,
messages: list[dict[str, Any]],
@ -322,7 +336,7 @@ class ContextBuilder:
{
"role": "user",
"content": (
f'[SYSTEM: The "{skill.name}" skill is active for this run. '
f'[SYSTEM: The "{skill.name}" skill (version {skill.version}) is active for this run. '
"Follow its instructions as active guidance unless the user overrides them.]\n\n"
f"{content}"
),

View File

@ -7,11 +7,23 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Callable
from beaver.coordinator.registry import AgentRegistry
from beaver.engine.context import ContextBuilder
from beaver.engine.session import SessionManager
from beaver.foundation.config import BeaverConfig, load_config
from beaver.memory.curated.store import MemoryStore
from beaver.memory.runs import RunMemoryStore
from beaver.memory.skills import SkillLearningStore
from beaver.services.memory_service import MemoryService
from beaver.skills.drafts import DraftService
from beaver.skills.learning import EvidenceSelector, SkillDraftSynthesizer, SkillLearningPipelineService, SkillLearningService
from beaver.skills.learning.safety import SkillDraftSafetyChecker
from beaver.skills.learning.eval import SkillDraftEvaluator
from beaver.skills.publisher import SkillPublisher
from beaver.skills.reviews import ReviewService
from beaver.skills.specs import SkillSpecStore
from beaver.tasks import TaskExecutionPlanner, TaskService, ValidationService
from beaver.tasks.skill_resolver import TaskSkillResolver
from beaver.skills import SkillAssembler, SkillsLoader
from beaver.tools import ObjectBackedTool, ToolAssembler, ToolExecutor, ToolRegistry
from beaver.tools.builtins import (
@ -45,12 +57,25 @@ class EngineLoadResult:
session_manager: SessionManager | None = None
curated_memory_store: MemoryStore | None = None
memory_service: MemoryService | None = None
run_memory_store: RunMemoryStore | None = None
skill_learning_store: SkillLearningStore | None = None
tool_registry: ToolRegistry | None = None
tool_assembler: ToolAssembler | None = None
tool_executor: ToolExecutor | None = None
context_builder: ContextBuilder | None = None
skills_loader: SkillsLoader | None = None
skill_assembler: SkillAssembler | None = None
skill_spec_store: SkillSpecStore | None = None
draft_service: DraftService | None = None
review_service: ReviewService | None = None
skill_publisher: SkillPublisher | None = None
skill_learning_service: SkillLearningService | None = None
skill_learning_pipeline: SkillLearningPipelineService | None = None
agent_registry: AgentRegistry | None = None
task_skill_resolver: TaskSkillResolver | None = None
task_service: TaskService | None = None
task_execution_planner: TaskExecutionPlanner | None = None
validation_service: ValidationService | None = None
closeables: list[tuple[str, Callable[[], None]]] = field(default_factory=list, repr=False)
closed: bool = False
@ -106,11 +131,24 @@ class EngineLoader:
session_manager: SessionManager | None = None,
curated_memory_store: MemoryStore | None = None,
memory_service: MemoryService | None = None,
run_memory_store: RunMemoryStore | None = None,
skill_learning_store: SkillLearningStore | None = None,
tool_registry: ToolRegistry | None = None,
tool_assembler: ToolAssembler | None = None,
context_builder: ContextBuilder | None = None,
skills_loader: SkillsLoader | None = None,
skill_assembler: SkillAssembler | None = None,
skill_spec_store: SkillSpecStore | None = None,
draft_service: DraftService | None = None,
review_service: ReviewService | None = None,
skill_publisher: SkillPublisher | None = None,
skill_learning_service: SkillLearningService | None = None,
skill_learning_pipeline: SkillLearningPipelineService | None = None,
agent_registry: AgentRegistry | None = None,
task_skill_resolver: TaskSkillResolver | None = None,
task_service: TaskService | None = None,
task_execution_planner: TaskExecutionPlanner | None = None,
validation_service: ValidationService | None = None,
) -> None:
self.config = config or load_config(workspace=workspace, config_path=config_path)
configured_workspace = self.config.agents_defaults.workspace
@ -119,11 +157,24 @@ class EngineLoader:
self._session_manager = session_manager
self._curated_memory_store = curated_memory_store
self._memory_service = memory_service
self._run_memory_store = run_memory_store
self._skill_learning_store = skill_learning_store
self._tool_registry = tool_registry
self._tool_assembler = tool_assembler
self._context_builder = context_builder
self._skills_loader = skills_loader
self._skill_assembler = skill_assembler
self._skill_spec_store = skill_spec_store
self._draft_service = draft_service
self._review_service = review_service
self._skill_publisher = skill_publisher
self._skill_learning_service = skill_learning_service
self._skill_learning_pipeline = skill_learning_pipeline
self._agent_registry = agent_registry
self._task_skill_resolver = task_skill_resolver
self._task_service = task_service
self._task_execution_planner = task_execution_planner
self._validation_service = validation_service
def load(self) -> EngineLoadResult:
"""装配当前主链需要的最小 runtime 对象。"""
@ -135,9 +186,12 @@ class EngineLoader:
curated_memory_store = self._curated_memory_store or MemoryStore(curated_root)
memory_service = self._memory_service or MemoryService(curated_root, store=curated_memory_store)
memory_service.initialize()
run_memory_store = self._run_memory_store or RunMemoryStore(workspace / "memory" / "runs")
skill_learning_store = self._skill_learning_store or SkillLearningStore(workspace / "memory" / "skills")
tool_registry = self._tool_registry or ToolRegistry()
skills_loader = self._skills_loader or SkillsLoader(workspace)
skill_spec_store = self._skill_spec_store or SkillSpecStore(workspace)
skills_loader = self._skills_loader or SkillsLoader(workspace, skill_store=skill_spec_store)
if self._tool_registry is None:
# 这里先注册最小工具集,满足主链的 tool loop。
tool_registry.register_many(
@ -156,6 +210,36 @@ class EngineLoader:
tool_assembler = self._tool_assembler or ToolAssembler()
tool_executor = ToolExecutor(tool_registry)
skill_assembler = self._skill_assembler or SkillAssembler(skills_loader)
draft_service = self._draft_service or DraftService(skill_spec_store)
review_service = self._review_service or ReviewService(skill_spec_store)
skill_publisher = self._skill_publisher or SkillPublisher(skill_spec_store)
evidence_selector = EvidenceSelector(run_memory_store, session_manager=session_manager)
skill_learning_service = self._skill_learning_service or SkillLearningService(
run_store=run_memory_store,
learning_store=skill_learning_store,
draft_service=draft_service,
evidence_selector=evidence_selector,
synthesizer=SkillDraftSynthesizer(),
)
skill_learning_pipeline = self._skill_learning_pipeline or SkillLearningPipelineService(
learning_store=skill_learning_store,
learning_service=skill_learning_service,
draft_service=draft_service,
review_service=review_service,
publisher=skill_publisher,
safety_checker=SkillDraftSafetyChecker(
allowed_tool_names={spec.name for spec in tool_registry.list_specs()}
),
evaluator=SkillDraftEvaluator(run_memory_store),
)
agent_registry = self._agent_registry or AgentRegistry(workspace)
task_skill_resolver = self._task_skill_resolver or TaskSkillResolver(
skills_loader=skills_loader,
draft_service=draft_service,
)
task_service = self._task_service or TaskService(workspace / "tasks")
task_execution_planner = self._task_execution_planner or TaskExecutionPlanner(task_skill_resolver=task_skill_resolver)
validation_service = self._validation_service or ValidationService()
result = EngineLoadResult(
workspace=workspace,
@ -167,12 +251,25 @@ class EngineLoader:
session_manager=session_manager,
curated_memory_store=memory_service.get_store(),
memory_service=memory_service,
run_memory_store=run_memory_store,
skill_learning_store=skill_learning_store,
tool_registry=tool_registry,
tool_assembler=tool_assembler,
tool_executor=tool_executor,
context_builder=context_builder,
skills_loader=skills_loader,
skill_assembler=skill_assembler,
skill_spec_store=skill_spec_store,
draft_service=draft_service,
review_service=review_service,
skill_publisher=skill_publisher,
skill_learning_service=skill_learning_service,
skill_learning_pipeline=skill_learning_pipeline,
agent_registry=agent_registry,
task_skill_resolver=task_skill_resolver,
task_service=task_service,
task_execution_planner=task_execution_planner,
validation_service=validation_service,
)
if self._session_manager is None:
result.register_closeable("session_manager", session_manager.close)

View File

@ -4,10 +4,15 @@ from __future__ import annotations
import asyncio
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from beaver.engine.context import ContextBuildInput, SessionContext
from beaver.engine.context import ContextBuildInput, SessionContext, SkillContext
from beaver.memory.runs import RunRecord, SkillEffectRecord
from beaver.skills.learning import RunReceiptContext
from beaver.skills.catalog.utils import strip_frontmatter
from beaver.skills.specs import SkillActivationReceipt
from beaver.engine.providers import ProviderBundle, make_provider_bundle
from beaver.tools import ToolContext
@ -38,6 +43,9 @@ class AgentRunResult:
provider_name: str | None = None
model: str | None = None
usage: dict[str, Any] = field(default_factory=dict)
task_id: str | None = None
task_status: str | None = None
validation_result: dict[str, Any] | None = None
@dataclass(slots=True)
@ -196,6 +204,13 @@ class AgentLoop:
temperature: float | None = None,
max_tool_iterations: int | None = None,
provider_bundle: ProviderBundle | None = None,
parent_session_id: str | None = None,
task_id: str | None = None,
task_mode: bool = False,
attempt_index: int | None = None,
pinned_skill_names: list[str] | None = None,
pinned_skill_contexts: list[SkillContext] | None = None,
learning_candidate_enabled: bool = False,
) -> AgentRunResult:
"""跑通最小 direct run 主链。
@ -233,6 +248,13 @@ class AgentLoop:
temperature=temperature,
max_tool_iterations=max_tool_iterations,
provider_bundle=provider_bundle,
parent_session_id=parent_session_id,
task_id=task_id,
task_mode=task_mode,
attempt_index=attempt_index,
pinned_skill_names=pinned_skill_names,
pinned_skill_contexts=pinned_skill_contexts,
learning_candidate_enabled=learning_candidate_enabled,
)
async def _process_direct_impl(
@ -258,6 +280,13 @@ class AgentLoop:
temperature: float | None = None,
max_tool_iterations: int | None = None,
provider_bundle: ProviderBundle | None = None,
parent_session_id: str | None = None,
task_id: str | None = None,
task_mode: bool = False,
attempt_index: int | None = None,
pinned_skill_names: list[str] | None = None,
pinned_skill_contexts: list[SkillContext] | None = None,
learning_candidate_enabled: bool = False,
) -> AgentRunResult:
"""真正执行一轮 direct run 的内部实现。
@ -276,6 +305,7 @@ class AgentLoop:
tool_executor = self._require_loaded("tool_executor")
skills_loader = self._require_loaded("skills_loader")
skill_assembler = self._require_loaded("skill_assembler")
skill_learning_service = self._require_loaded("skill_learning_service")
config = loaded.config
configured_provider = config.resolve_provider_target(model=model, provider_name=provider_name)
@ -296,16 +326,24 @@ class AgentLoop:
self.profile.max_tool_iterations if max_tool_iterations is None else max_tool_iterations
)
# 每次新运行开始前都通过 MemoryService 刷新 live state。
# 这样 memory policy 会收口在 service而不是散在 loop 里
memory_service.reload_for_new_run()
# 每个 run 都捕获自己的 frozen snapshot不能依赖 MemoryService
# 上的共享 `_snapshot`,否则 parallel team runs 会互相覆盖
memory_snapshot = memory_service.capture_snapshot_for_run()
if parent_session_id:
session_manager.ensure_session(
parent_session_id,
source="unknown",
model=resolved_model,
user_id=user_id,
)
session_manager.ensure_session(
resolved_session_id,
source=source,
model=resolved_model,
title=title,
user_id=user_id,
parent_session_id=parent_session_id,
)
session_manager.append_message(
resolved_session_id,
@ -316,6 +354,12 @@ class AgentLoop:
"source": source,
"model": resolved_model,
"agent_name": self.profile.name,
"task_id": task_id,
"task_mode": task_mode,
"attempt_index": attempt_index,
"parent_session_id": parent_session_id,
"pinned_skill_names": list(pinned_skill_names or []),
"pinned_skill_context_names": [skill.name for skill in pinned_skill_contexts or []],
},
content=task,
context_visible=False,
@ -330,6 +374,8 @@ class AgentLoop:
final_usage: dict[str, Any] = {}
final_provider_name: str | None = resolved_provider_name
final_model: str | None = resolved_model
run_started_at = self._utc_now()
activated_receipts: list[SkillActivationReceipt] = []
try:
bundle = provider_bundle or make_provider_bundle(
model=resolved_model,
@ -356,17 +402,38 @@ class AgentLoop:
model=skill_selector_model,
embedding_runtime=bundle.embedding_runtime,
)
skill_activation_messages = context_builder.build_skill_activation_messages(
assembled_skills.activated_skills
activated_skills = self._merge_skill_contexts(
[
*(pinned_skill_contexts or []),
*self._load_pinned_skill_contexts(skills_loader, pinned_skill_names or []),
],
assembled_skills.activated_skills,
)
skill_activation_messages = context_builder.build_skill_activation_messages(
activated_skills
)
activated_receipts = [
SkillActivationReceipt(
run_id=resolved_run_id,
session_id=resolved_session_id,
skill_name=skill.name,
skill_version=skill.version,
content_hash=skill.content_hash,
activated_at=self._utc_now(),
activation_reason=skill.activation_reason,
tool_hints=list(skill.tool_hints),
)
for skill in activated_skills
]
if skill_activation_messages:
if skill_activation_messages or activated_receipts:
session_manager.append_message(
resolved_session_id,
run_id=resolved_run_id,
role="system",
event_type="skill_activation_snapshotted",
event_payload={
"receipts": [receipt.to_dict() for receipt in activated_receipts],
"activation_messages": skill_activation_messages,
},
content="\n\n".join(message["content"] for message in skill_activation_messages) or None,
@ -381,7 +448,7 @@ class AgentLoop:
task_description=task,
registry=tool_registry,
skills_loader=skills_loader,
activated_skills=assembled_skills.activated_skills,
activated_skills=activated_skills,
embedding_runtime=bundle.embedding_runtime,
top_k=10,
)
@ -407,13 +474,14 @@ class AgentLoop:
base_system_prompt=self.profile.system_prompt,
history=session_manager.get_history(resolved_session_id),
current_user_input=task,
memory_snapshot=memory_service.get_snapshot(),
activated_skills=assembled_skills.activated_skills,
memory_snapshot=memory_snapshot,
activated_skills=activated_skills,
session_context=SessionContext(
session_id=resolved_session_id,
source=source,
model=resolved_model,
user_id=user_id,
parent_session_id=parent_session_id,
),
execution_context=execution_context,
)
@ -491,6 +559,7 @@ class AgentLoop:
run_id=resolved_run_id,
role="assistant",
event_type="assistant_message_added",
event_payload={"task_id": task_id} if task_id else None,
content=response.content,
tool_calls=assistant_tool_calls or None,
finish_reason=response.finish_reason,
@ -520,6 +589,7 @@ class AgentLoop:
run_id=resolved_run_id,
role="assistant",
event_type="assistant_message_added",
event_payload={"task_id": task_id} if task_id else None,
content=final_text,
finish_reason=final_finish_reason,
source=source,
@ -568,6 +638,9 @@ class AgentLoop:
event_payload={
"finish_reason": final_finish_reason,
"tool_iterations": iterations,
"task_id": task_id,
"task_mode": task_mode,
"attempt_index": attempt_index,
},
content=final_text,
finish_reason=final_finish_reason,
@ -577,6 +650,21 @@ class AgentLoop:
model=final_model,
user_id=user_id,
)
self._record_skill_learning(
skill_learning_service=skill_learning_service,
session_manager=session_manager,
session_id=resolved_session_id,
run_id=resolved_run_id,
task=task,
run_started_at=run_started_at,
run_ended_at=self._utc_now(),
finish_reason=final_finish_reason,
activated_receipts=activated_receipts,
success=(final_finish_reason == "stop"),
task_id=task_id,
attempt_index=attempt_index,
generate_candidates=learning_candidate_enabled,
)
return AgentRunResult(
session_id=resolved_session_id,
run_id=resolved_run_id,
@ -586,6 +674,7 @@ class AgentLoop:
provider_name=final_provider_name,
model=final_model,
usage=final_usage,
task_id=task_id,
)
except Exception as exc:
if not user_message_recorded:
@ -600,7 +689,7 @@ class AgentLoop:
model=resolved_model,
user_id=user_id,
)
return self._build_error_result(
result = self._build_error_result(
session_manager=session_manager,
session_id=resolved_session_id,
run_id=resolved_run_id,
@ -612,7 +701,24 @@ class AgentLoop:
tool_iterations=iterations,
provider_name=final_provider_name,
usage=final_usage,
task_id=task_id,
)
self._record_skill_learning(
skill_learning_service=skill_learning_service,
session_manager=session_manager,
session_id=resolved_session_id,
run_id=resolved_run_id,
task=task,
run_started_at=run_started_at,
run_ended_at=self._utc_now(),
finish_reason="error",
activated_receipts=activated_receipts,
success=False,
task_id=task_id,
attempt_index=attempt_index,
generate_candidates=learning_candidate_enabled,
)
return result
def _require_loaded(self, field_name: str) -> Any:
loaded = self.boot()
@ -621,6 +727,46 @@ class AgentLoop:
raise RuntimeError(f"Engine loader did not provide required dependency {field_name!r}")
return value
@staticmethod
def _load_pinned_skill_contexts(skills_loader: Any, skill_names: list[str]) -> list[SkillContext]:
contexts: list[SkillContext] = []
seen: set[str] = set()
for name in skill_names:
normalized = str(name).strip()
if not normalized or normalized in seen:
continue
seen.add(normalized)
record = skills_loader.get_skill_record(normalized)
raw_content = skills_loader.load_published_skill(normalized)
content = strip_frontmatter(raw_content).strip() if raw_content else ""
if record is None or not content:
raise ValueError(f"Pinned skill {normalized!r} is not available for delegated execution")
contexts.append(
SkillContext(
name=normalized,
content=content,
version=record.version,
content_hash=record.content_hash or "",
activation_reason="pinned_delegation",
tool_hints=list(record.tool_hints),
)
)
return contexts
@staticmethod
def _merge_skill_contexts(
pinned_skills: list[SkillContext],
open_skills: list[SkillContext],
) -> list[SkillContext]:
result: list[SkillContext] = []
seen: set[str] = set()
for skill in [*pinned_skills, *open_skills]:
if skill.name in seen:
continue
seen.add(skill.name)
result.append(skill)
return result
@staticmethod
def _serialize_tool_calls(tool_calls: list[Any]) -> list[dict[str, Any]]:
payload: list[dict[str, Any]] = []
@ -683,6 +829,7 @@ class AgentLoop:
tool_iterations: int,
provider_name: str | None,
usage: dict[str, Any],
task_id: str | None = None,
) -> AgentRunResult:
"""把主链中的未处理异常收口成可追踪的 assistant error turn。"""
@ -691,6 +838,7 @@ class AgentLoop:
run_id=run_id,
role="assistant",
event_type="assistant_message_added",
event_payload={"task_id": task_id} if task_id else None,
content=message,
finish_reason="error",
source=source,
@ -706,6 +854,7 @@ class AgentLoop:
event_payload={
"tool_iterations": tool_iterations,
"provider_name": provider_name,
"task_id": task_id,
},
content=message,
finish_reason="error",
@ -724,4 +873,87 @@ class AgentLoop:
provider_name=provider_name,
model=model,
usage=usage,
task_id=task_id,
)
@staticmethod
def _record_skill_learning(
*,
skill_learning_service: Any,
session_manager: Any,
session_id: str,
run_id: str,
task: str,
run_started_at: str,
run_ended_at: str,
finish_reason: str,
activated_receipts: list[SkillActivationReceipt],
success: bool,
task_id: str | None = None,
attempt_index: int | None = None,
generate_candidates: bool = False,
) -> None:
run_record = RunRecord(
run_id=run_id,
session_id=session_id,
task_id=task_id,
attempt_index=attempt_index,
task_text=task,
started_at=run_started_at,
ended_at=run_ended_at,
success=success,
finish_reason=finish_reason,
feedback={},
activated_skills=list(activated_receipts),
)
effect_records = [
SkillEffectRecord(
run_id=run_id,
skill_name=receipt.skill_name,
skill_version=receipt.skill_version,
success=success,
feedback_score=None,
notes=finish_reason,
created_at=run_ended_at,
)
for receipt in activated_receipts
]
try:
candidates = skill_learning_service.collect_run_receipts(
RunReceiptContext(run_record=run_record, effect_records=effect_records),
generate_candidates=generate_candidates,
)
except Exception as exc: # pragma: no cover - defensive hot-path guard
session_manager.append_message(
session_id,
run_id=run_id,
role="system",
event_type="skill_effects_snapshot_failed",
event_payload={
"run_record": run_record.to_dict(),
"skill_effects": [item.to_dict() for item in effect_records],
"error": str(exc),
},
content=f"Skill learning receipt recording failed: {exc}",
context_visible=False,
)
return
session_manager.append_message(
session_id,
run_id=run_id,
role="system",
event_type="skill_effects_snapshotted",
event_payload={
"run_record": run_record.to_dict(),
"skill_effects": [item.to_dict() for item in effect_records],
"learning_candidates": [candidate.to_dict() for candidate in candidates],
"learning_candidate_enabled": generate_candidates,
},
content=f"Recorded {len(effect_records)} skill effect record(s).",
context_visible=False,
)
@staticmethod
def _utc_now() -> str:
return datetime.now(timezone.utc).isoformat()

View File

@ -91,6 +91,19 @@ class SessionManager:
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。"""

View File

@ -75,6 +75,19 @@ class MessageRecord:
"role": self.role,
"content": self.content,
}
if self.run_id:
payload["run_id"] = self.run_id
if self.event_payload:
if self.event_payload.get("task_id"):
payload["task_id"] = self.event_payload.get("task_id")
if self.event_payload.get("task_status"):
payload["task_status"] = self.event_payload.get("task_status")
if self.event_payload.get("validation_status"):
payload["validation_status"] = self.event_payload.get("validation_status")
if self.event_payload.get("feedback_state"):
payload["feedback_state"] = self.event_payload.get("feedback_state")
if self.event_payload.get("feedback_error"):
payload["feedback_error"] = self.event_payload.get("feedback_error")
if self.tool_name:
payload["tool_name"] = self.tool_name
if self.tool_calls:

View File

@ -432,6 +432,71 @@ class SessionStore:
)
return [MessageRecord.from_row(row) for row in rows]
def update_latest_assistant_event_payload(
self,
session_id: str,
run_id: str,
updates: dict[str, Any],
) -> None:
"""Merge payload fields into the latest visible assistant message for a run."""
if not updates:
return
def _do(conn: sqlite3.Connection) -> None:
row = conn.execute(
"""
SELECT id, event_payload
FROM messages
WHERE session_id = ?
AND run_id = ?
AND role = 'assistant'
AND event_type = 'assistant_message_added'
AND context_visible = 1
ORDER BY timestamp DESC, id DESC
LIMIT 1
""",
(session_id, run_id),
).fetchone()
if row is None:
return
payload: dict[str, Any] = {}
if row["event_payload"]:
try:
parsed = json.loads(row["event_payload"])
if isinstance(parsed, dict):
payload = parsed
except json.JSONDecodeError:
payload = {}
payload.update(updates)
conn.execute(
"""
UPDATE messages
SET event_payload = ?
WHERE id = ?
""",
(json.dumps(payload, ensure_ascii=False, sort_keys=True), row["id"]),
)
self._execute_write(_do)
def set_run_context_visible(self, session_id: str, run_id: str, visible: bool) -> None:
"""Set context visibility for all currently visible events in one run."""
def _do(conn: sqlite3.Connection) -> None:
conn.execute(
"""
UPDATE messages
SET context_visible = ?
WHERE session_id = ?
AND run_id = ?
AND context_visible != ?
""",
(1 if visible else 0, session_id, run_id, 1 if visible else 0),
)
self._execute_write(_do)
def get_messages_as_conversation(self, session_id: str) -> list[dict[str, Any]]:
messages: list[dict[str, Any]] = []
for record in self.get_event_records(session_id):