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方法重新构建全文搜索索引
- 优化索引触发器和表的维护流程
This commit is contained in:
2026-05-14 09:43:48 +08:00
parent 8a12c30141
commit 30ab74ffb2
149 changed files with 12293 additions and 2812 deletions

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
@ -64,6 +65,7 @@ class AgentLoop:
self.profile = profile or AgentProfile()
self.loader = loader or EngineLoader()
self.loaded: EngineLoadResult | None = None
self.runtime_services: dict[str, Any] = {}
self._run_queue: asyncio.Queue[_DirectRunRequest | None] | None = None
self._running = False
self._stop_requested = False
@ -190,6 +192,7 @@ class AgentLoop:
user_id: str | None = None,
title: str | None = None,
execution_context: str | None = None,
skill_selection_context: str | None = None,
model: str | None = None,
provider_name: str | None = None,
api_key: str | None = None,
@ -202,6 +205,9 @@ class AgentLoop:
embedding_model: str | None = None,
max_tokens: int | None = None,
temperature: float | None = None,
thinking_enabled: bool | None = None,
include_skill_assembly: bool = True,
include_tools: bool = True,
max_tool_iterations: int | None = None,
provider_bundle: ProviderBundle | None = None,
parent_session_id: str | None = None,
@ -210,7 +216,7 @@ class AgentLoop:
attempt_index: int | None = None,
pinned_skill_names: list[str] | None = None,
pinned_skill_contexts: list[SkillContext] | None = None,
learning_candidate_enabled: bool = False,
allow_candidate_generation: bool = False,
) -> AgentRunResult:
"""跑通最小 direct run 主链。
@ -234,6 +240,7 @@ class AgentLoop:
user_id=user_id,
title=title,
execution_context=execution_context,
skill_selection_context=skill_selection_context,
model=model,
provider_name=provider_name,
api_key=api_key,
@ -246,6 +253,9 @@ class AgentLoop:
embedding_model=embedding_model,
max_tokens=max_tokens,
temperature=temperature,
thinking_enabled=thinking_enabled,
include_skill_assembly=include_skill_assembly,
include_tools=include_tools,
max_tool_iterations=max_tool_iterations,
provider_bundle=provider_bundle,
parent_session_id=parent_session_id,
@ -254,7 +264,7 @@ class AgentLoop:
attempt_index=attempt_index,
pinned_skill_names=pinned_skill_names,
pinned_skill_contexts=pinned_skill_contexts,
learning_candidate_enabled=learning_candidate_enabled,
allow_candidate_generation=allow_candidate_generation,
)
async def _process_direct_impl(
@ -266,6 +276,7 @@ class AgentLoop:
user_id: str | None = None,
title: str | None = None,
execution_context: str | None = None,
skill_selection_context: str | None = None,
model: str | None = None,
provider_name: str | None = None,
api_key: str | None = None,
@ -278,6 +289,9 @@ class AgentLoop:
embedding_model: str | None = None,
max_tokens: int | None = None,
temperature: float | None = None,
thinking_enabled: bool | None = None,
include_skill_assembly: bool = True,
include_tools: bool = True,
max_tool_iterations: int | None = None,
provider_bundle: ProviderBundle | None = None,
parent_session_id: str | None = None,
@ -286,7 +300,7 @@ class AgentLoop:
attempt_index: int | None = None,
pinned_skill_names: list[str] | None = None,
pinned_skill_contexts: list[SkillContext] | None = None,
learning_candidate_enabled: bool = False,
allow_candidate_generation: bool = False,
) -> AgentRunResult:
"""真正执行一轮 direct run 的内部实现。
@ -306,6 +320,10 @@ class AgentLoop:
skills_loader = self._require_loaded("skills_loader")
skill_assembler = self._require_loaded("skill_assembler")
skill_learning_service = self._require_loaded("skill_learning_service")
mcp_manager = getattr(loaded, "mcp_manager", None)
if mcp_manager is not None:
loaded.mcp_report = await mcp_manager.connect_all(tool_registry)
loaded.tools = [spec.name for spec in tool_registry.list_specs()]
config = loaded.config
configured_provider = config.resolve_provider_target(model=model, provider_name=provider_name)
@ -357,6 +375,9 @@ class AgentLoop:
"task_id": task_id,
"task_mode": task_mode,
"attempt_index": attempt_index,
"thinking_enabled": thinking_enabled,
"include_skill_assembly": include_skill_assembly,
"skill_selection_context_present": bool(skill_selection_context),
"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 []],
@ -396,19 +417,39 @@ class AgentLoop:
if bundle.auxiliary_runtime is not None
else bundle.main_runtime.model
)
assembled_skills = await skill_assembler.assemble(
task_description=task,
provider=skill_selector_provider,
model=skill_selector_model,
embedding_runtime=bundle.embedding_runtime,
)
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,
)
pinned_skills = [
*(pinned_skill_contexts or []),
*self._load_pinned_skill_contexts(skills_loader, pinned_skill_names or []),
]
if not include_skill_assembly or thinking_enabled is False:
activated_skills = self._merge_skill_contexts(pinned_skills, [])
else:
skill_query = skill_selection_context or task
assembled_skills = await skill_assembler.assemble(
task_description=skill_query,
provider=skill_selector_provider,
model=skill_selector_model,
embedding_runtime=bundle.embedding_runtime,
thinking_enabled=thinking_enabled,
)
for interaction in getattr(assembled_skills, "llm_interactions", []) or []:
session_manager.append_message(
resolved_session_id,
run_id=resolved_run_id,
role="system",
event_type="skill_assembler_llm_interaction_snapshotted",
event_payload=interaction,
content=json.dumps(interaction, ensure_ascii=False, default=str),
context_visible=False,
source=source,
title=title,
model=skill_selector_model,
user_id=user_id,
)
activated_skills = self._merge_skill_contexts(
pinned_skills,
assembled_skills.activated_skills,
)
skill_activation_messages = context_builder.build_skill_activation_messages(
activated_skills
)
@ -444,14 +485,19 @@ class AgentLoop:
user_id=user_id,
)
selected_tool_specs = await tool_assembler.assemble(
task_description=task,
registry=tool_registry,
skills_loader=skills_loader,
activated_skills=activated_skills,
embedding_runtime=bundle.embedding_runtime,
top_k=10,
)
if not include_tools:
selected_tool_specs = []
elif thinking_enabled is False:
selected_tool_specs = tool_registry.list_specs()
else:
selected_tool_specs = await tool_assembler.assemble(
task_description=task,
registry=tool_registry,
skills_loader=skills_loader,
activated_skills=activated_skills,
embedding_runtime=bundle.embedding_runtime,
top_k=10,
)
tool_schemas = tool_registry.export_selected_provider_schemas(selected_tool_specs)
session_manager.append_message(
resolved_session_id,
@ -486,6 +532,25 @@ class AgentLoop:
execution_context=execution_context,
)
context_result = context_builder.build_messages(build_input)
if skill_selection_context:
session_manager.append_message(
resolved_session_id,
run_id=resolved_run_id,
role="system",
event_type="skill_selection_context_snapshotted",
event_payload={
"skill_selection_context": skill_selection_context,
"task_id": task_id,
"task_mode": task_mode,
"attempt_index": attempt_index,
},
content=skill_selection_context,
context_visible=False,
source=source,
title=title,
model=resolved_model,
user_id=user_id,
)
session_manager.update_system_prompt(resolved_session_id, context_result.system_prompt)
session_manager.append_message(
resolved_session_id,
@ -528,6 +593,9 @@ class AgentLoop:
"memory_service": memory_service,
"memory_store": memory_service.get_store(),
"tool_registry": tool_registry,
"skills_loader": skills_loader,
"draft_service": getattr(loaded, "draft_service", None),
**self.runtime_services,
},
metadata={
"source": source,
@ -541,13 +609,45 @@ class AgentLoop:
final_model = bundle.main_runtime.model
while True:
response = await provider.chat(
messages=messages,
tools=tool_schemas,
chat_kwargs: dict[str, Any] = {
"messages": messages,
"tools": tool_schemas,
"model": final_model,
"max_tokens": resolved_max_tokens,
"temperature": resolved_temperature,
}
if thinking_enabled is not None:
chat_kwargs["thinking_enabled"] = thinking_enabled
session_manager.append_message(
resolved_session_id,
run_id=resolved_run_id,
role="system",
event_type="llm_request_snapshotted",
event_payload={
"iteration": iterations,
"provider_name": final_provider_name,
"model": final_model,
"messages": messages,
"tools": tool_schemas,
"max_tokens": resolved_max_tokens,
"temperature": resolved_temperature,
"thinking_enabled": thinking_enabled,
},
content=json.dumps(
{
"messages": messages,
"tools": tool_schemas,
},
ensure_ascii=False,
default=str,
),
context_visible=False,
source=source,
title=title,
model=final_model,
max_tokens=resolved_max_tokens,
temperature=resolved_temperature,
user_id=user_id,
)
response = await provider.chat(**chat_kwargs)
final_provider_name = response.provider_name or final_provider_name
final_model = response.model or final_model
final_usage = self._merge_usage(final_usage, response.usage or {})
@ -650,7 +750,7 @@ class AgentLoop:
model=final_model,
user_id=user_id,
)
self._record_skill_learning(
self._record_run_receipts(
skill_learning_service=skill_learning_service,
session_manager=session_manager,
session_id=resolved_session_id,
@ -663,7 +763,7 @@ class AgentLoop:
success=(final_finish_reason == "stop"),
task_id=task_id,
attempt_index=attempt_index,
generate_candidates=learning_candidate_enabled,
allow_candidate_generation=False,
)
return AgentRunResult(
session_id=resolved_session_id,
@ -703,7 +803,7 @@ class AgentLoop:
usage=final_usage,
task_id=task_id,
)
self._record_skill_learning(
self._record_run_receipts(
skill_learning_service=skill_learning_service,
session_manager=session_manager,
session_id=resolved_session_id,
@ -716,7 +816,7 @@ class AgentLoop:
success=False,
task_id=task_id,
attempt_index=attempt_index,
generate_candidates=learning_candidate_enabled,
allow_candidate_generation=False,
)
return result
@ -771,13 +871,16 @@ class AgentLoop:
def _serialize_tool_calls(tool_calls: list[Any]) -> list[dict[str, Any]]:
payload: list[dict[str, Any]] = []
for tool_call in tool_calls:
arguments = tool_call.arguments
if not isinstance(arguments, str):
arguments = json.dumps(arguments or {}, ensure_ascii=False, default=str)
payload.append(
{
"id": tool_call.id,
"type": "function",
"function": {
"name": tool_call.name,
"arguments": tool_call.arguments,
"arguments": arguments,
},
}
)
@ -877,7 +980,7 @@ class AgentLoop:
)
@staticmethod
def _record_skill_learning(
def _record_run_receipts(
*,
skill_learning_service: Any,
session_manager: Any,
@ -891,7 +994,7 @@ class AgentLoop:
success: bool,
task_id: str | None = None,
attempt_index: int | None = None,
generate_candidates: bool = False,
allow_candidate_generation: bool = False,
) -> None:
run_record = RunRecord(
run_id=run_id,
@ -921,7 +1024,7 @@ class AgentLoop:
try:
candidates = skill_learning_service.collect_run_receipts(
RunReceiptContext(run_record=run_record, effect_records=effect_records),
generate_candidates=generate_candidates,
generate_candidates=allow_candidate_generation,
)
except Exception as exc: # pragma: no cover - defensive hot-path guard
session_manager.append_message(
@ -948,7 +1051,7 @@ class AgentLoop:
"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,
"candidate_generation_allowed": allow_candidate_generation,
},
content=f"Recorded {len(effect_records)} skill effect record(s).",
context_visible=False,