feat(coordinator): 添加团队节点默认最大工具迭代次数配置

添加 DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS 配置项以控制团队节点的最大工具迭代次数,
并修改 LocalAgentRunner 中的逻辑来使用此默认值当 envelope 中未指定时。

fix(runtime): 修复团队节点运行成功判断逻辑

更新运行成功判断条件,将 finish_reason 为 "max_tool_iterations_finalized" 的情况
视为运行失败,并添加对原始工具调用输出的检测,避免将其误判为成功完成。

feat(mcp): 添加团队工作流MCP工具类别支持

增加新的本地MCP工具类别 "team_workflow" 及其对应的工具创建功能,
为团队工作流提供本地工具支持。

refactor(engine): 调整AgentLoop最大工具迭代次数设置

将 AgentProfile 中的默认 max_tool_iterations 从 30 增加到 100,
同时移除 TaskExecutionPlanner 构造函数中的重复参数传递。

perf(mcp): 优化MCP连接管理避免重复连接

添加 mcp_connected 标志来跟踪MCP连接状态,确保 connect_all 只执行一次,
提高性能并避免不必要的重复连接。

refactor(skills): 移除技能团队模板相关功能

移除与技能团队模板相关的代码,包括解析、存储和处理逻辑,
简化技能记录结构和加载流程。

feat(process): 增强会话过程投影器功能

添加技能激活快照事件处理,改进团队运行完成消息显示,
并增强技能激活事件的时间戳记录功能。

refactor(tasks): 简化任务尝试编排器团队执行逻辑

移除团队执行相关代码,将所有任务统一按单步执行处理,
简化任务编排器的复杂度并提升执行效率。

fix(evidence): 修复节点证据评估中需求验证逻辑

更新节点证据评估逻辑,跳过自然语言证据需求的确定性验证,
只执行机器可读的需求验证,避免因自然语言需求导致的节点失败。
This commit is contained in:
2026-06-26 16:36:29 +08:00
parent 53b13e8eac
commit 520a21a027
360 changed files with 13271 additions and 1848 deletions

View File

@ -5,12 +5,11 @@ from __future__ import annotations
from time import perf_counter
from typing import Any, Callable
from beaver.coordinator.models import ExecutionNode, TeamRunResult
from beaver.engine import AgentRunResult
from beaver.engine.context import SkillContext
from beaver.prompts.main_agent import normalize_main_agent_prompt_locale
from .evidence import EvidenceBuilder, RunEvidence, TaskEvidencePacket, render_task_evidence
from .evidence import EvidenceBuilder, TaskEvidencePacket, render_task_evidence
from .models import TaskRecord
from .planner import TaskExecutionPlan
@ -46,7 +45,7 @@ class TaskAttemptOrchestrator:
output_language_instruction = self._output_language_instruction(prompt_locale)
provider_bundle = kwargs.get("provider_bundle") or self.make_provider_bundle_for_task(self.loaded, kwargs)
kwargs = dict(kwargs)
team_provider_bundle_factory = kwargs.pop("team_provider_bundle_factory", None)
kwargs.pop("team_provider_bundle_factory", None)
kwargs["provider_bundle"] = provider_bundle
attempt_index = int(task.metadata.get("latest_attempt_index") or 0) + 1
@ -87,75 +86,17 @@ class TaskAttemptOrchestrator:
**plan.to_event_payload(),
},
)
team_summaries: list[str] = []
team_execution_context = ""
team_result: TeamRunResult | None = None
if plan.is_team:
team_result, team_error = await self._run_team_for_task(
plan,
task=task,
parent_session_id=kwargs["session_id"],
provider_bundle_factory=team_provider_bundle_factory
or self._build_team_provider_bundle_factory(kwargs),
plan = TaskExecutionPlan.single(
"legacy_planner_team_ignored",
planner_adaptation=plan.planner_adaptation,
)
if team_result is not None:
team_summaries = [self._team_summary_for_validation(team_result)]
team_packet = TaskEvidencePacket(
task_id=task.task_id,
attempt_index=attempt_index,
main_run=None,
team_runs=self._team_run_evidence(team_result),
team_node_results=list(team_result.node_results),
final_output="",
)
team_execution_context = self._join_context(
self._team_execution_context(plan, team_result),
"Rendered team evidence:\n" + render_task_evidence(team_packet),
)
self._append_task_observation(
session_manager,
task.session_id,
event_type="task_team_run_completed" if team_result.success else "task_team_run_failed",
payload={
"task_id": task.task_id,
"attempt_index": attempt_index,
"plan_mode": plan.mode,
"strategy": plan.graph.strategy if plan.graph else None,
"node_ids": [node.node_id for node in plan.graph.nodes] if plan.graph else [],
"team_run_ids": team_result.run_ids,
"team_success": team_result.success,
"node_results": self._team_node_results_for_event(plan, team_result),
"reason": plan.reason,
"error": None if team_result.success else "one or more team nodes failed",
},
)
else:
team_summaries = [f"Team execution failed: {team_error}"]
team_execution_context = self._failed_team_execution_context(plan, team_error or "unknown error")
self._append_task_observation(
session_manager,
task.session_id,
event_type="task_team_run_failed",
payload={
"task_id": task.task_id,
"attempt_index": attempt_index,
"plan_mode": plan.mode,
"strategy": plan.graph.strategy if plan.graph else None,
"node_ids": [node.node_id for node in plan.graph.nodes] if plan.graph else [],
"team_run_ids": [],
"team_success": False,
"reason": plan.reason,
"error": team_error,
},
)
outcome_context, incomplete_prefix, outcome_metadata = self._team_synthesis_outcome(
plan,
team_result,
prompt_locale=prompt_locale,
)
if plan.is_team:
team_execution_context = self._join_context(outcome_context, team_execution_context)
outcome_metadata = {
"task_outcome": "single",
"incomplete_node_ids": [],
"node_statuses": {},
"evidence_gaps": {},
}
attempt_kwargs = dict(kwargs)
attempt_kwargs.update(
@ -171,22 +112,15 @@ class TaskAttemptOrchestrator:
attempt_kwargs["execution_context"] = self._join_context(
base_execution_context,
output_language_instruction,
team_execution_context,
)
if plan.is_team and team_execution_context:
attempt_kwargs["include_tools"] = False
attempt_kwargs["max_tool_iterations"] = 0
attempt_kwargs["skill_selection_context"] = self._build_skill_selection_context(
task=task,
user_message=message,
attempt_index=attempt_index,
plan=plan,
team_summaries=team_summaries,
)
result = await runner(message, **attempt_kwargs)
if outcome_metadata["task_outcome"] == "incomplete":
result.output_text = self._apply_incomplete_prefix(result.output_text, incomplete_prefix)
self._append_task_observation(
session_manager,
task.session_id,
@ -210,7 +144,6 @@ class TaskAttemptOrchestrator:
task=task,
attempt_index=attempt_index,
result=result,
team_result=team_result,
)
evidence_text = render_task_evidence(evidence_packet)
evidence_debug = {
@ -256,31 +189,6 @@ class TaskAttemptOrchestrator:
result.validation_result = None
return result
async def _run_team_for_task(
self,
plan: TaskExecutionPlan,
*,
task: TaskRecord,
parent_session_id: str,
provider_bundle_factory: Any,
) -> tuple[TeamRunResult | None, str | None]:
if plan.graph is None:
return None, "team plan did not include an execution graph"
try:
from beaver.services.team_service import TeamService
result = await TeamService(self.create_loop()).run_team(
plan.graph,
parent_task_id=task.task_id,
parent_session_id=parent_session_id,
parent_run_id=None,
provider_bundle_factory=provider_bundle_factory,
allow_candidate_generation=False,
)
return result, None
except Exception as exc:
return None, str(exc)
async def _assemble_task_attempt_skills(
self,
*,
@ -396,7 +304,6 @@ class TaskAttemptOrchestrator:
user_message: str,
attempt_index: int,
plan: TaskExecutionPlan | None = None,
team_summaries: list[str] | None = None,
) -> str:
phase = f"attempt_{attempt_index}"
if task.feedback and task.feedback[-1].get("acceptance_type") == "revise":
@ -445,8 +352,6 @@ class TaskAttemptOrchestrator:
)
)
sections.append("Execution plan:\n" + "\n".join(plan_lines))
if team_summaries:
sections.append("Team execution summaries:\n" + "\n\n".join(team_summaries)[:2400])
sections.append(
"Skill selection instruction:\n"
"Prefer reusing previously activated skills when they still match the Task. "
@ -476,140 +381,6 @@ class TaskAttemptOrchestrator:
def _join_context(*parts: str | None) -> str:
return "\n\n".join(part.strip() for part in parts if part and part.strip())
@staticmethod
def _team_summary_for_validation(result: TeamRunResult) -> str:
lines = [
f"success={result.success}",
f"task_id={result.task_id or ''}",
"summary:",
result.summary,
"nodes:",
]
for node in result.node_results:
lines.append(
f"- {node.node_id}: success={node.success} finish_reason={node.finish_reason} "
f"error={node.error or ''} output={node.output_text[:500]}"
)
return "\n".join(lines)
@staticmethod
def _team_node_results_for_event(plan: TaskExecutionPlan, result: TeamRunResult) -> list[dict[str, Any]]:
nodes = {node.node_id: node for node in plan.graph.nodes} if plan.graph else {}
payloads: list[dict[str, Any]] = []
for item in result.node_results:
payload = item.to_dict()
node = nodes.get(item.node_id)
if node is not None:
payload["selected_skill_names"] = list(node.inherited_pinned_skills)
payload["ephemeral_skill_names"] = [
skill.name for skill in node.inherited_pinned_skill_contexts
]
payload["skill_query"] = node.agent.metadata.get("skill_query")
payload["ephemeral_guidance_id"] = node.agent.metadata.get("ephemeral_guidance_id")
payload["ephemeral_guidance_name"] = node.agent.metadata.get("ephemeral_guidance_name")
payload["ephemeral_used"] = bool(node.inherited_pinned_skill_contexts)
payloads.append(payload)
return payloads
@staticmethod
def _team_run_evidence(result: TeamRunResult | None) -> list[RunEvidence]:
if result is None:
return []
return [node.evidence for node in result.node_results if node.evidence is not None]
@staticmethod
def _team_synthesis_outcome(
plan: TaskExecutionPlan,
result: TeamRunResult | None,
*,
prompt_locale: str | None = None,
) -> tuple[str, str, dict[str, Any]]:
if not plan.is_team or plan.graph is None:
metadata = {
"task_outcome": "single",
"incomplete_node_ids": [],
"node_statuses": {},
"evidence_gaps": {},
}
return "Task outcome: single", "", metadata
result_by_node = {
item.node_id: item
for item in (result.node_results if result is not None else [])
}
node_statuses: dict[str, str] = {}
evidence_gaps: dict[str, list[str]] = {}
incomplete_node_ids: list[str] = []
detail_lines: list[str] = []
successful_lines: list[str] = []
for node in plan.graph.nodes:
node_result = result_by_node.get(node.node_id)
status = node_result.completion_status if node_result is not None else "not_run"
node_statuses[node.node_id] = status
gaps = list(node_result.evidence_gaps) if node_result is not None else []
if gaps:
evidence_gaps[node.node_id] = gaps
if node.required_for_completion and status != "succeeded":
incomplete_node_ids.append(node.node_id)
detail_lines.append(
f"- {node.node_id}: status={status}, "
f"finish_reason={node_result.finish_reason if node_result is not None else 'not_run'}, "
f"error={(node_result.error or '') if node_result is not None else 'node did not run'}, "
f"evidence_gaps={gaps}"
)
elif node_result is not None and status == "succeeded":
successful_lines.append(f"- {node.node_id}: {node_result.output_text[:1000]}")
task_outcome = "incomplete" if incomplete_node_ids else "complete"
metadata = {
"task_outcome": task_outcome,
"incomplete_node_ids": incomplete_node_ids,
"node_statuses": node_statuses,
"evidence_gaps": evidence_gaps,
}
context_parts = [
f"Task outcome: {task_outcome}",
"Incomplete node IDs: " + (", ".join(incomplete_node_ids) or "none"),
]
if detail_lines:
context_parts.append("Incomplete required node details:\n" + "\n".join(detail_lines))
if successful_lines:
context_parts.append("Available successful node evidence:\n" + "\n".join(successful_lines))
if task_outcome == "incomplete":
context_parts.append(
"Synthesis requirement: produce a partial report from available evidence and explicitly state "
"that the task is incomplete, partially completed, or missing required evidence."
)
prefix = TaskAttemptOrchestrator._incomplete_prefix(prompt_locale) if incomplete_node_ids else ""
return "\n\n".join(context_parts), prefix, metadata
@staticmethod
def _incomplete_prefix(prompt_locale: str | None) -> str:
locale = normalize_main_agent_prompt_locale(prompt_locale)
if locale == "en":
return "Task incomplete: some required steps failed or lack required evidence. The report below uses available results only.\n\n"
if locale == "zh-Hant":
return "任務未完成:部分必要步驟失敗或缺少必要證據。以下內容僅基於現有結果。\n\n"
return "任务未完成:部分必要步骤失败或缺少必要证据。以下内容仅基于现有结果。\n\n"
@staticmethod
def _apply_incomplete_prefix(output_text: str, prefix: str) -> str:
normalized = output_text.lower()
notices = (
"任务未完成",
"任務未完成",
"部分完成",
"缺少证据",
"缺少證據",
"task incomplete",
"incomplete task",
"partially complete",
"missing evidence",
)
if any(notice in normalized for notice in notices):
return output_text
return prefix + output_text.lstrip()
def _build_task_evidence_packet(
self,
*,
@ -617,7 +388,6 @@ class TaskAttemptOrchestrator:
task: TaskRecord,
attempt_index: int,
result: AgentRunResult,
team_result: TeamRunResult | None,
) -> TaskEvidencePacket:
main_run = EvidenceBuilder(session_manager).build_run_evidence(
result.session_id,
@ -629,67 +399,7 @@ class TaskAttemptOrchestrator:
task_id=task.task_id,
attempt_index=attempt_index,
main_run=main_run,
team_runs=self._team_run_evidence(team_result),
team_node_results=list(team_result.node_results) if team_result is not None else [],
team_runs=[],
team_node_results=[],
final_output=result.output_text,
)
@staticmethod
def _team_execution_context(plan: TaskExecutionPlan, result: TeamRunResult) -> str:
node_lines = [
(
f"- {node.node_id}: success={node.success}, finish_reason={node.finish_reason}, "
f"run_id={node.run_id or ''}, error={node.error or ''}\n{node.output_text}"
)
for node in result.node_results
]
return "\n\n".join(
item
for item in [
"Task team execution result:",
f"Planner reason: {plan.reason}",
f"Strategy: {plan.graph.strategy if plan.graph else ''}",
f"Team success: {result.success}",
f"Team summary:\n{result.summary}",
"Node results:\n" + "\n\n".join(node_lines),
(
"Final synthesis instruction:\n" + plan.final_synthesis_instruction
if plan.final_synthesis_instruction
else None
),
(
"Use successful team outputs as internal evidence. If one or more nodes failed, "
"do not blindly repeat failed tool calls. Produce a user-visible fallback answer "
"with available evidence and clearly state any missing or uncertain data."
),
]
if item
)
@staticmethod
def _failed_team_execution_context(plan: TaskExecutionPlan, error: str) -> str:
return "\n\n".join(
[
"Task team execution failed before final synthesis.",
f"Planner reason: {plan.reason}",
f"Strategy: {plan.graph.strategy if plan.graph else ''}",
f"Error: {error}",
(
"Proceed as the main agent. Do not blindly repeat failed tool calls; "
"produce a user-visible fallback answer with available evidence and clearly "
"state any missing or uncertain data."
),
]
)
def _build_team_provider_bundle_factory(self, kwargs: dict[str, Any]) -> Any:
def factory(node: ExecutionNode) -> Any:
node_kwargs = dict(kwargs)
node_kwargs.pop("provider_bundle", None)
if node.agent.model:
node_kwargs["model"] = node.agent.model
if node.agent.provider_name:
node_kwargs["provider_name"] = node.agent.provider_name
return self.make_provider_bundle_for_task(self.loaded, node_kwargs)
return factory