Files
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

122 lines
4.5 KiB
Python

"""File-backed run receipt store."""
from __future__ import annotations
import json
from pathlib import Path
from .models import RunRecord, SkillEffectRecord
class RunMemoryStore:
def __init__(self, root: str | Path) -> None:
self.root = Path(root)
self.root.mkdir(parents=True, exist_ok=True)
self.runs_path = self.root / "runs.jsonl"
self.effects_path = self.root / "skill-effects.jsonl"
def append_run_record(self, record: RunRecord) -> None:
self._append_jsonl(self.runs_path, record.to_dict())
def update_run_record(self, run_id: str, **updates: object) -> RunRecord | None:
records = self.list_runs()
updated: RunRecord | None = None
for index, record in enumerate(records):
if record.run_id != run_id:
continue
payload = record.to_dict()
payload.update(updates)
updated = RunRecord.from_dict(payload)
records[index] = updated
break
if updated is None:
return None
self.runs_path.parent.mkdir(parents=True, exist_ok=True)
self.runs_path.write_text(
"".join(
json.dumps(record.to_dict(), ensure_ascii=False, sort_keys=True) + "\n"
for record in records
),
encoding="utf-8",
)
return updated
def append_skill_effect(self, effect: SkillEffectRecord) -> None:
self._append_jsonl(self.effects_path, effect.to_dict())
def update_skill_effects_for_run(self, run_id: str, **updates: object) -> list[SkillEffectRecord]:
effects = [SkillEffectRecord.from_dict(item) for item in self._read_jsonl(self.effects_path)]
updated: list[SkillEffectRecord] = []
for index, effect in enumerate(effects):
if effect.run_id != run_id:
continue
payload = effect.to_dict()
payload.update(updates)
next_effect = SkillEffectRecord.from_dict(payload)
effects[index] = next_effect
updated.append(next_effect)
if not updated:
return []
self.effects_path.parent.mkdir(parents=True, exist_ok=True)
self.effects_path.write_text(
"".join(
json.dumps(effect.to_dict(), ensure_ascii=False, sort_keys=True) + "\n"
for effect in effects
),
encoding="utf-8",
)
return updated
def list_runs(self) -> list[RunRecord]:
return [RunRecord.from_dict(item) for item in self._read_jsonl(self.runs_path)]
def list_runs_by_skill(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[RunRecord]:
results: list[RunRecord] = []
for record in self.list_runs():
matched = False
for receipt in record.activated_skills:
if receipt.skill_name != skill_name:
continue
if version is not None and receipt.skill_version != version:
continue
matched = True
break
if matched:
results.append(record)
if limit is not None:
return results[-limit:]
return results
def list_skill_effects(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[SkillEffectRecord]:
results: list[SkillEffectRecord] = []
for payload in self._read_jsonl(self.effects_path):
effect = SkillEffectRecord.from_dict(payload)
if effect.skill_name != skill_name:
continue
if version is not None and effect.skill_version != version:
continue
results.append(effect)
if limit is not None:
return results[-limit:]
return results
@staticmethod
def _append_jsonl(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n")
@staticmethod
def _read_jsonl(path: Path) -> list[dict]:
if not path.exists():
return []
results: list[dict] = []
for line in path.read_text(encoding="utf-8").splitlines():
cleaned = line.strip()
if not cleaned:
continue
payload = json.loads(cleaned)
if isinstance(payload, dict):
results.append(payload)
return results