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:
@ -1,2 +1,34 @@
|
||||
"""Multi-agent coordination layer."""
|
||||
|
||||
from .models import (
|
||||
AgentDescriptor,
|
||||
DelegationEnvelope,
|
||||
ExecutionGraph,
|
||||
ExecutionNode,
|
||||
NodeRunResult,
|
||||
TeamRunResult,
|
||||
)
|
||||
|
||||
|
||||
def __getattr__(name: str):
|
||||
if name == "LocalAgentRunner":
|
||||
from .local import LocalAgentRunner
|
||||
|
||||
return LocalAgentRunner
|
||||
if name == "TeamGraphScheduler":
|
||||
from .execution import TeamGraphScheduler
|
||||
|
||||
return TeamGraphScheduler
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AgentDescriptor",
|
||||
"DelegationEnvelope",
|
||||
"ExecutionGraph",
|
||||
"ExecutionNode",
|
||||
"LocalAgentRunner",
|
||||
"NodeRunResult",
|
||||
"TeamGraphScheduler",
|
||||
"TeamRunResult",
|
||||
]
|
||||
|
||||
@ -1,2 +1,5 @@
|
||||
"""Execution control, retry, and aggregation."""
|
||||
|
||||
from .scheduler import TeamGraphScheduler
|
||||
|
||||
__all__ = ["TeamGraphScheduler"]
|
||||
|
||||
256
app-instance/backend/beaver/coordinator/execution/scheduler.py
Normal file
256
app-instance/backend/beaver/coordinator/execution/scheduler.py
Normal file
@ -0,0 +1,256 @@
|
||||
"""Minimal scheduler for Beaver-native team execution graphs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from beaver.engine.providers import ProviderBundle
|
||||
|
||||
from ..local import LocalAgentRunner
|
||||
from ..models import DelegationEnvelope, ExecutionGraph, ExecutionNode, NodeRunResult, TeamRunResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver.engine.context import SkillContext
|
||||
|
||||
|
||||
class TeamGraphScheduler:
|
||||
"""Execute sequence, parallel, and DAG team graphs."""
|
||||
|
||||
def __init__(self, runner: LocalAgentRunner) -> None:
|
||||
self.runner = runner
|
||||
|
||||
async def run(
|
||||
self,
|
||||
graph: ExecutionGraph,
|
||||
*,
|
||||
parent_task_id: str | None,
|
||||
parent_session_id: str,
|
||||
parent_run_id: str | None = None,
|
||||
provider_bundle: ProviderBundle | None = None,
|
||||
provider_bundle_factory: Callable[[ExecutionNode], ProviderBundle | None] | None = None,
|
||||
inherited_pinned_skills: list[str] | None = None,
|
||||
inherited_pinned_skill_contexts: list["SkillContext"] | None = None,
|
||||
learning_candidate_enabled: bool = False,
|
||||
) -> TeamRunResult:
|
||||
graph.validate()
|
||||
if provider_bundle is not None and len(graph.nodes) > 1:
|
||||
raise ValueError("provider_bundle can only be used for single-node team graphs; use provider_bundle_factory")
|
||||
inherited = list(inherited_pinned_skills or [])
|
||||
inherited_contexts = list(inherited_pinned_skill_contexts or [])
|
||||
if graph.strategy == "sequence":
|
||||
results = await self._run_sequence(
|
||||
graph.nodes,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
provider_bundle=provider_bundle,
|
||||
provider_bundle_factory=provider_bundle_factory,
|
||||
inherited_pinned_skills=inherited,
|
||||
inherited_pinned_skill_contexts=inherited_contexts,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
)
|
||||
elif graph.strategy == "parallel":
|
||||
results = await self._run_parallel(
|
||||
graph.nodes,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
provider_bundle=provider_bundle,
|
||||
provider_bundle_factory=provider_bundle_factory,
|
||||
inherited_pinned_skills=inherited,
|
||||
inherited_pinned_skill_contexts=inherited_contexts,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
)
|
||||
else:
|
||||
results = await self._run_dag(
|
||||
graph.nodes,
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
provider_bundle=provider_bundle,
|
||||
provider_bundle_factory=provider_bundle_factory,
|
||||
inherited_pinned_skills=inherited,
|
||||
inherited_pinned_skill_contexts=inherited_contexts,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
)
|
||||
return self._summarize(results, task_id=parent_task_id)
|
||||
|
||||
async def _run_sequence(
|
||||
self,
|
||||
nodes: list[ExecutionNode],
|
||||
**kwargs,
|
||||
) -> list[NodeRunResult]:
|
||||
results: list[NodeRunResult] = []
|
||||
for node in nodes:
|
||||
if any(not item.success for item in results):
|
||||
results.append(self._blocked(node, results))
|
||||
continue
|
||||
dependency_outputs = {item.node_id: item.output_text for item in results if item.success}
|
||||
results.append(await self._run_node(node, dependency_outputs=dependency_outputs, **kwargs))
|
||||
return results
|
||||
|
||||
async def _run_parallel(
|
||||
self,
|
||||
nodes: list[ExecutionNode],
|
||||
**kwargs,
|
||||
) -> list[NodeRunResult]:
|
||||
return list(await asyncio.gather(*(self._run_node(node, dependency_outputs={}, **kwargs) for node in nodes)))
|
||||
|
||||
async def _run_dag(
|
||||
self,
|
||||
nodes: list[ExecutionNode],
|
||||
**kwargs,
|
||||
) -> list[NodeRunResult]:
|
||||
pending = {node.node_id: node for node in nodes}
|
||||
completed: dict[str, NodeRunResult] = {}
|
||||
ordered: list[NodeRunResult] = []
|
||||
|
||||
while pending:
|
||||
blocked_ids = {
|
||||
node_id
|
||||
for node_id, node in pending.items()
|
||||
if any(dep in completed and not completed[dep].success for dep in node.depends_on)
|
||||
}
|
||||
for node_id in sorted(blocked_ids):
|
||||
node = pending.pop(node_id)
|
||||
result = self._blocked(node, list(completed.values()))
|
||||
completed[node_id] = result
|
||||
ordered.append(result)
|
||||
|
||||
ready = [
|
||||
node
|
||||
for node in pending.values()
|
||||
if all(dep in completed and completed[dep].success for dep in node.depends_on)
|
||||
]
|
||||
if not ready:
|
||||
if pending:
|
||||
unresolved = ", ".join(sorted(pending))
|
||||
raise ValueError(f"ExecutionGraph has cyclic or unresolved dependencies: {unresolved}")
|
||||
break
|
||||
|
||||
batch = await asyncio.gather(
|
||||
*(
|
||||
self._run_node(
|
||||
node,
|
||||
dependency_outputs={
|
||||
dep: completed[dep].output_text
|
||||
for dep in node.depends_on
|
||||
if dep in completed
|
||||
},
|
||||
**kwargs,
|
||||
)
|
||||
for node in ready
|
||||
)
|
||||
)
|
||||
for result in batch:
|
||||
pending.pop(result.node_id, None)
|
||||
completed[result.node_id] = result
|
||||
ordered.append(result)
|
||||
|
||||
return ordered
|
||||
|
||||
async def _run_node(
|
||||
self,
|
||||
node: ExecutionNode,
|
||||
*,
|
||||
parent_task_id: str | None,
|
||||
parent_session_id: str,
|
||||
parent_run_id: str | None,
|
||||
provider_bundle: ProviderBundle | None,
|
||||
provider_bundle_factory: Callable[[ExecutionNode], ProviderBundle | None] | None,
|
||||
inherited_pinned_skills: list[str],
|
||||
inherited_pinned_skill_contexts: list["SkillContext"],
|
||||
learning_candidate_enabled: bool,
|
||||
dependency_outputs: dict[str, str],
|
||||
) -> NodeRunResult:
|
||||
try:
|
||||
pinned = self._merge_pinned(inherited_pinned_skills, node.inherited_pinned_skills)
|
||||
pinned_contexts = self._merge_skill_contexts(
|
||||
inherited_pinned_skill_contexts,
|
||||
node.inherited_pinned_skill_contexts,
|
||||
)
|
||||
envelope = DelegationEnvelope(
|
||||
parent_task_id=parent_task_id,
|
||||
parent_session_id=parent_session_id,
|
||||
parent_run_id=parent_run_id,
|
||||
agent=node.agent,
|
||||
task=node.task,
|
||||
inherited_pinned_skills=pinned,
|
||||
inherited_pinned_skill_contexts=pinned_contexts,
|
||||
constraints=list(node.constraints),
|
||||
expected_output=node.expected_output,
|
||||
node_id=node.node_id,
|
||||
dependency_outputs=dict(dependency_outputs),
|
||||
)
|
||||
node_provider_bundle = provider_bundle_factory(node) if provider_bundle_factory is not None else provider_bundle
|
||||
return await self.runner.run(
|
||||
envelope,
|
||||
provider_bundle=node_provider_bundle,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as exc:
|
||||
return NodeRunResult(
|
||||
node_id=node.node_id,
|
||||
success=False,
|
||||
output_text="",
|
||||
finish_reason="error",
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _merge_pinned(parent: list[str], local: list[str]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for name in [*parent, *local]:
|
||||
if name and name not in result:
|
||||
result.append(name)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _merge_skill_contexts(parent: list["SkillContext"], local: list["SkillContext"]) -> list["SkillContext"]:
|
||||
result: list["SkillContext"] = []
|
||||
seen: set[str] = set()
|
||||
for skill in [*parent, *local]:
|
||||
name = getattr(skill, "name", "")
|
||||
if not name or name in seen:
|
||||
continue
|
||||
seen.add(name)
|
||||
result.append(skill)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _blocked(node: ExecutionNode, prior_results: list[NodeRunResult]) -> NodeRunResult:
|
||||
failed = [item.node_id for item in prior_results if not item.success]
|
||||
detail = ", ".join(failed) or "unknown dependency"
|
||||
return NodeRunResult(
|
||||
node_id=node.node_id,
|
||||
success=False,
|
||||
output_text="",
|
||||
finish_reason="blocked",
|
||||
error=f"Blocked by failed dependency: {detail}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _summarize(results: list[NodeRunResult], *, task_id: str | None) -> TeamRunResult:
|
||||
success = all(item.success for item in results)
|
||||
successful_outputs = [item.output_text.strip() for item in results if item.success and item.output_text.strip()]
|
||||
summary_parts = list(successful_outputs)
|
||||
failed = [item for item in results if not item.success]
|
||||
if failed:
|
||||
failure_lines = [
|
||||
f"- {item.node_id}: {item.error or item.finish_reason}"
|
||||
for item in failed
|
||||
]
|
||||
summary_parts.append("Failed nodes:\n" + "\n".join(failure_lines))
|
||||
summary = "\n\n".join(summary_parts)
|
||||
return TeamRunResult(
|
||||
success=success,
|
||||
summary=summary,
|
||||
node_results=results,
|
||||
run_ids=[item.run_id for item in results if item.run_id],
|
||||
session_ids=[item.session_id for item in results if item.session_id],
|
||||
task_id=task_id,
|
||||
)
|
||||
92
app-instance/backend/beaver/coordinator/local.py
Normal file
92
app-instance/backend/beaver/coordinator/local.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Local delegated-agent runner built on the shared AgentLoop."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from beaver.engine import AgentLoop
|
||||
from beaver.engine.providers import ProviderBundle
|
||||
|
||||
from .models import DelegationEnvelope, NodeRunResult
|
||||
|
||||
|
||||
class LocalAgentRunner:
|
||||
"""Run delegated agents through the same AgentLoop implementation."""
|
||||
|
||||
def __init__(self, loop: AgentLoop) -> None:
|
||||
self.loop = loop
|
||||
|
||||
async def run(
|
||||
self,
|
||||
envelope: DelegationEnvelope,
|
||||
*,
|
||||
provider_bundle: ProviderBundle | None = None,
|
||||
learning_candidate_enabled: bool = False,
|
||||
) -> NodeRunResult:
|
||||
if provider_bundle is not None and (envelope.agent.model or envelope.agent.provider_name):
|
||||
raise ValueError(
|
||||
"provider_bundle cannot be combined with AgentDescriptor.model/provider_name; "
|
||||
"build a node-specific provider bundle instead."
|
||||
)
|
||||
child_session_id = self._child_session_id(envelope)
|
||||
runner = self.loop.submit_direct if self.loop.is_running else self.loop.process_direct
|
||||
result = await runner(
|
||||
envelope.task,
|
||||
session_id=child_session_id,
|
||||
parent_session_id=envelope.parent_session_id,
|
||||
source=f"team:{envelope.agent.name}",
|
||||
title=envelope.agent.role or envelope.agent.name,
|
||||
execution_context=self._execution_context(envelope),
|
||||
model=envelope.agent.model,
|
||||
provider_name=envelope.agent.provider_name,
|
||||
provider_bundle=provider_bundle,
|
||||
task_id=envelope.parent_task_id,
|
||||
task_mode=bool(envelope.parent_task_id),
|
||||
pinned_skill_names=envelope.inherited_pinned_skills,
|
||||
pinned_skill_contexts=envelope.inherited_pinned_skill_contexts,
|
||||
learning_candidate_enabled=learning_candidate_enabled,
|
||||
)
|
||||
success = result.finish_reason == "stop"
|
||||
return NodeRunResult(
|
||||
node_id=envelope.node_id or envelope.agent.name,
|
||||
success=success,
|
||||
output_text=result.output_text,
|
||||
run_id=result.run_id,
|
||||
session_id=result.session_id,
|
||||
finish_reason=result.finish_reason,
|
||||
error=None if success else (result.output_text or result.finish_reason),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _child_session_id(envelope: DelegationEnvelope) -> str:
|
||||
node = envelope.node_id or envelope.agent.name or "node"
|
||||
return f"{envelope.parent_session_id}:team:{node}:{uuid4().hex[:8]}"
|
||||
|
||||
@staticmethod
|
||||
def _execution_context(envelope: DelegationEnvelope) -> str:
|
||||
sections: list[str] = []
|
||||
if envelope.parent_task_id:
|
||||
sections.append(f"Parent task ID: {envelope.parent_task_id}")
|
||||
if envelope.parent_run_id:
|
||||
sections.append(f"Parent run ID: {envelope.parent_run_id}")
|
||||
sections.append("Delegated worker: generic task sub-agent. Follow active pinned skills as the primary guidance.")
|
||||
if envelope.agent.system_prompt:
|
||||
sections.append(f"Additional delegated instructions:\n{envelope.agent.system_prompt}")
|
||||
if envelope.constraints:
|
||||
sections.append("Constraints:\n" + "\n".join(f"- {item}" for item in envelope.constraints))
|
||||
if envelope.expected_output:
|
||||
sections.append(f"Expected output:\n{envelope.expected_output}")
|
||||
if envelope.dependency_outputs:
|
||||
rendered = "\n\n".join(
|
||||
f"Dependency {node_id} output:\n{output}"
|
||||
for node_id, output in envelope.dependency_outputs.items()
|
||||
)
|
||||
sections.append("Dependency outputs:\n" + rendered)
|
||||
if envelope.inherited_pinned_skills:
|
||||
sections.append("Pinned inherited skills:\n" + "\n".join(f"- {item}" for item in envelope.inherited_pinned_skills))
|
||||
if envelope.inherited_pinned_skill_contexts:
|
||||
sections.append(
|
||||
"Ephemeral pinned skill drafts:\n"
|
||||
+ "\n".join(f"- {item.name} ({item.version})" for item in envelope.inherited_pinned_skill_contexts)
|
||||
)
|
||||
return "\n\n".join(sections)
|
||||
151
app-instance/backend/beaver/coordinator/models.py
Normal file
151
app-instance/backend/beaver/coordinator/models.py
Normal file
@ -0,0 +1,151 @@
|
||||
"""Core models for Beaver team coordination."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver.engine.context import SkillContext
|
||||
|
||||
|
||||
TeamStrategy = Literal[
|
||||
"sequence",
|
||||
"parallel",
|
||||
"dag",
|
||||
"moa",
|
||||
"hierarchy",
|
||||
"heavy",
|
||||
"group_chat",
|
||||
"forest",
|
||||
"maker",
|
||||
"router",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentDescriptor:
|
||||
"""Runtime identity for a delegated local agent."""
|
||||
|
||||
name: str
|
||||
role: str = ""
|
||||
system_prompt: str = ""
|
||||
model: str | None = None
|
||||
provider_name: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DelegationEnvelope:
|
||||
"""All context passed from a parent agent run to one delegated run."""
|
||||
|
||||
parent_task_id: str | None
|
||||
parent_session_id: str
|
||||
parent_run_id: str | None
|
||||
agent: AgentDescriptor
|
||||
task: str
|
||||
inherited_pinned_skills: list[str] = field(default_factory=list)
|
||||
inherited_pinned_skill_contexts: list["SkillContext"] = field(default_factory=list)
|
||||
constraints: list[str] = field(default_factory=list)
|
||||
expected_output: str | None = None
|
||||
node_id: str | None = None
|
||||
dependency_outputs: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionNode:
|
||||
"""One node in a team execution graph."""
|
||||
|
||||
node_id: str
|
||||
task: str
|
||||
agent: AgentDescriptor
|
||||
depends_on: list[str] = field(default_factory=list)
|
||||
inherited_pinned_skills: list[str] = field(default_factory=list)
|
||||
inherited_pinned_skill_contexts: list["SkillContext"] = field(default_factory=list)
|
||||
constraints: list[str] = field(default_factory=list)
|
||||
expected_output: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ExecutionGraph:
|
||||
"""A lightweight team graph built from Beaver-native execution nodes."""
|
||||
|
||||
strategy: TeamStrategy
|
||||
nodes: list[ExecutionNode]
|
||||
|
||||
def validate(self) -> None:
|
||||
if self.strategy not in {"sequence", "parallel", "dag"}:
|
||||
raise NotImplementedError(f"Team strategy {self.strategy!r} is reserved but not implemented in v1")
|
||||
if not self.nodes:
|
||||
raise ValueError("ExecutionGraph requires at least one node")
|
||||
node_ids = [node.node_id for node in self.nodes]
|
||||
if len(node_ids) != len(set(node_ids)):
|
||||
raise ValueError("ExecutionGraph node_id values must be unique")
|
||||
known = set(node_ids)
|
||||
for node in self.nodes:
|
||||
missing = [item for item in node.depends_on if item not in known]
|
||||
if missing:
|
||||
raise ValueError(f"ExecutionNode {node.node_id!r} depends on unknown node(s): {missing}")
|
||||
visiting: set[str] = set()
|
||||
visited: set[str] = set()
|
||||
deps = {node.node_id: list(node.depends_on) for node in self.nodes}
|
||||
|
||||
def visit(node_id: str) -> None:
|
||||
if node_id in visited:
|
||||
return
|
||||
if node_id in visiting:
|
||||
raise ValueError(f"ExecutionGraph has cyclic or unresolved dependencies involving {node_id!r}")
|
||||
visiting.add(node_id)
|
||||
for dep in deps[node_id]:
|
||||
visit(dep)
|
||||
visiting.remove(node_id)
|
||||
visited.add(node_id)
|
||||
|
||||
for node_id in node_ids:
|
||||
visit(node_id)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class NodeRunResult:
|
||||
"""Normalized result for one team node."""
|
||||
|
||||
node_id: str
|
||||
success: bool
|
||||
output_text: str
|
||||
run_id: str | None = None
|
||||
session_id: str | None = None
|
||||
finish_reason: str = "stop"
|
||||
error: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"node_id": self.node_id,
|
||||
"success": self.success,
|
||||
"output_text": self.output_text,
|
||||
"run_id": self.run_id,
|
||||
"session_id": self.session_id,
|
||||
"finish_reason": self.finish_reason,
|
||||
"error": self.error,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TeamRunResult:
|
||||
"""Normalized result returned by a Beaver team run."""
|
||||
|
||||
success: bool
|
||||
summary: str
|
||||
node_results: list[NodeRunResult] = field(default_factory=list)
|
||||
run_ids: list[str] = field(default_factory=list)
|
||||
session_ids: list[str] = field(default_factory=list)
|
||||
task_id: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
"summary": self.summary,
|
||||
"node_results": [item.to_dict() for item in self.node_results],
|
||||
"run_ids": list(self.run_ids),
|
||||
"session_ids": list(self.session_ids),
|
||||
"task_id": self.task_id,
|
||||
}
|
||||
@ -1,2 +1,14 @@
|
||||
"""Agent registry and descriptors."""
|
||||
"""Workspace specialist agent registry."""
|
||||
|
||||
from .models import AgentMatch, RegisteredAgent, TargetResolutionReport
|
||||
from .resolver import TargetResolver
|
||||
from .store import AgentRegistry
|
||||
|
||||
__all__ = [
|
||||
"AgentMatch",
|
||||
"AgentRegistry",
|
||||
"RegisteredAgent",
|
||||
"TargetResolutionReport",
|
||||
"TargetResolver",
|
||||
]
|
||||
|
||||
184
app-instance/backend/beaver/coordinator/registry/models.py
Normal file
184
app-instance/backend/beaver/coordinator/registry/models.py
Normal file
@ -0,0 +1,184 @@
|
||||
"""Workspace agent registry models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Literal
|
||||
|
||||
from beaver.coordinator.models import AgentDescriptor
|
||||
|
||||
|
||||
AgentRegistryStatus = Literal["active", "disabled"]
|
||||
AgentRegistrySource = Literal["builtin", "workspace", "learned"]
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RegisteredAgent:
|
||||
agent_id: str
|
||||
name: str
|
||||
display_name: str
|
||||
role: str
|
||||
description: str
|
||||
system_prompt: str
|
||||
capabilities: list[str] = field(default_factory=list)
|
||||
skill_names: list[str] = field(default_factory=list)
|
||||
tool_hints: list[str] = field(default_factory=list)
|
||||
model: str | None = None
|
||||
provider_name: str | None = None
|
||||
tags: list[str] = field(default_factory=list)
|
||||
priority: int = 0
|
||||
status: AgentRegistryStatus = "active"
|
||||
source: AgentRegistrySource = "workspace"
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
created_at: str = field(default_factory=lambda: _utc_now())
|
||||
updated_at: str = field(default_factory=lambda: _utc_now())
|
||||
|
||||
def to_descriptor(self) -> AgentDescriptor:
|
||||
return AgentDescriptor(
|
||||
name=self.name,
|
||||
role=self.role,
|
||||
system_prompt=self.system_prompt,
|
||||
model=self.model,
|
||||
provider_name=self.provider_name,
|
||||
metadata={
|
||||
**self.metadata,
|
||||
"agent_id": self.agent_id,
|
||||
"display_name": self.display_name,
|
||||
"description": self.description,
|
||||
"capabilities": list(self.capabilities),
|
||||
"skill_names": list(self.skill_names),
|
||||
"tool_hints": list(self.tool_hints),
|
||||
"tags": list(self.tags),
|
||||
"source": self.source,
|
||||
"resolution": "registered",
|
||||
},
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"name": self.name,
|
||||
"display_name": self.display_name,
|
||||
"role": self.role,
|
||||
"description": self.description,
|
||||
"system_prompt": self.system_prompt,
|
||||
"capabilities": list(self.capabilities),
|
||||
"skill_names": list(self.skill_names),
|
||||
"tool_hints": list(self.tool_hints),
|
||||
"model": self.model,
|
||||
"provider_name": self.provider_name,
|
||||
"tags": list(self.tags),
|
||||
"priority": self.priority,
|
||||
"status": self.status,
|
||||
"source": self.source,
|
||||
"metadata": dict(self.metadata),
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "RegisteredAgent":
|
||||
now = _utc_now()
|
||||
agent_id = str(payload.get("agent_id") or payload.get("id") or payload.get("name") or "").strip()
|
||||
if not agent_id:
|
||||
raise ValueError("RegisteredAgent requires agent_id")
|
||||
name = str(payload.get("name") or agent_id).strip()
|
||||
return cls(
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
display_name=str(payload.get("display_name") or payload.get("displayName") or name).strip(),
|
||||
role=str(payload.get("role") or "").strip(),
|
||||
description=str(payload.get("description") or "").strip(),
|
||||
system_prompt=str(payload.get("system_prompt") or payload.get("systemPrompt") or "").strip(),
|
||||
capabilities=_string_list(payload.get("capabilities")),
|
||||
skill_names=_string_list(payload.get("skill_names") or payload.get("skillNames")),
|
||||
tool_hints=_string_list(payload.get("tool_hints") or payload.get("toolHints")),
|
||||
model=_optional_str(payload.get("model")),
|
||||
provider_name=_optional_str(payload.get("provider_name") or payload.get("providerName")),
|
||||
tags=_string_list(payload.get("tags")),
|
||||
priority=int(payload.get("priority", 0) or 0),
|
||||
status="disabled" if str(payload.get("status") or "active") == "disabled" else "active",
|
||||
source=_source(payload.get("source")),
|
||||
metadata=dict(payload.get("metadata") or {}),
|
||||
created_at=str(payload.get("created_at") or payload.get("createdAt") or now),
|
||||
updated_at=str(payload.get("updated_at") or payload.get("updatedAt") or now),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AgentMatch:
|
||||
agent_id: str
|
||||
score: float
|
||||
reasons: list[str]
|
||||
matched_capabilities: list[str]
|
||||
resolved_descriptor: AgentDescriptor
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"agent_id": self.agent_id,
|
||||
"score": self.score,
|
||||
"reasons": list(self.reasons),
|
||||
"matched_capabilities": list(self.matched_capabilities),
|
||||
"resolved_descriptor": {
|
||||
"name": self.resolved_descriptor.name,
|
||||
"role": self.resolved_descriptor.role,
|
||||
"model": self.resolved_descriptor.model,
|
||||
"provider_name": self.resolved_descriptor.provider_name,
|
||||
"metadata": dict(self.resolved_descriptor.metadata),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TargetResolutionReport:
|
||||
node_id: str
|
||||
requested_role: str
|
||||
requested_capabilities: list[str]
|
||||
selected_agent_id: str | None
|
||||
fallback_used: bool
|
||||
score: float
|
||||
reason: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"node_id": self.node_id,
|
||||
"requested_role": self.requested_role,
|
||||
"requested_capabilities": list(self.requested_capabilities),
|
||||
"selected_agent_id": self.selected_agent_id,
|
||||
"fallback_used": self.fallback_used,
|
||||
"score": self.score,
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
|
||||
def _utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _optional_str(value: Any) -> str | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _string_list(value: Any) -> list[str]:
|
||||
if not isinstance(value, list):
|
||||
if isinstance(value, str):
|
||||
value = [item.strip() for item in value.split(",")]
|
||||
else:
|
||||
return []
|
||||
result: list[str] = []
|
||||
for item in value:
|
||||
text = str(item).strip()
|
||||
if text and text not in result:
|
||||
result.append(text)
|
||||
return result
|
||||
|
||||
|
||||
def _source(value: Any) -> AgentRegistrySource:
|
||||
text = str(value or "workspace").strip()
|
||||
if text in {"builtin", "workspace", "learned"}:
|
||||
return text # type: ignore[return-value]
|
||||
return "workspace"
|
||||
208
app-instance/backend/beaver/coordinator/registry/resolver.py
Normal file
208
app-instance/backend/beaver/coordinator/registry/resolver.py
Normal file
@ -0,0 +1,208 @@
|
||||
"""Resolve planner node requirements to registered specialist agents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
|
||||
|
||||
from .models import AgentMatch, RegisteredAgent, TargetResolutionReport
|
||||
from .store import AgentRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from beaver.tasks.models import TaskRecord
|
||||
|
||||
|
||||
class TargetResolver:
|
||||
def __init__(self, registry: AgentRegistry) -> None:
|
||||
self.registry = registry
|
||||
|
||||
def resolve_graph(
|
||||
self,
|
||||
graph: ExecutionGraph,
|
||||
*,
|
||||
task: "TaskRecord",
|
||||
user_message: str,
|
||||
attempt_index: int,
|
||||
) -> tuple[ExecutionGraph, list[TargetResolutionReport]]:
|
||||
reports: list[TargetResolutionReport] = []
|
||||
resolved_nodes: list[ExecutionNode] = []
|
||||
for node in graph.nodes:
|
||||
descriptor, report = self.resolve_node(
|
||||
node,
|
||||
task=task,
|
||||
user_message=user_message,
|
||||
attempt_index=attempt_index,
|
||||
)
|
||||
resolved_nodes.append(replace(node, agent=descriptor))
|
||||
reports.append(report)
|
||||
return ExecutionGraph(strategy=graph.strategy, nodes=resolved_nodes), reports
|
||||
|
||||
def resolve_node(
|
||||
self,
|
||||
node: ExecutionNode,
|
||||
*,
|
||||
task: "TaskRecord",
|
||||
user_message: str,
|
||||
attempt_index: int,
|
||||
) -> tuple[AgentDescriptor, TargetResolutionReport]:
|
||||
requested_role = (node.agent.role or node.agent.name or node.node_id).strip()
|
||||
requested_capabilities = [
|
||||
str(item).strip()
|
||||
for item in node.agent.metadata.get("requested_capabilities", [])
|
||||
if str(item).strip()
|
||||
]
|
||||
requested_tags = [
|
||||
str(item).strip()
|
||||
for item in node.agent.metadata.get("requested_tags", [])
|
||||
if str(item).strip()
|
||||
]
|
||||
pinned_skills = list(node.inherited_pinned_skills)
|
||||
match = self.best_match(
|
||||
requested_role=requested_role,
|
||||
requested_capabilities=requested_capabilities,
|
||||
requested_tags=requested_tags,
|
||||
pinned_skills=pinned_skills,
|
||||
task_text=" ".join([task.goal, task.description, user_message, node.task]),
|
||||
)
|
||||
if match is not None and match.score > 0:
|
||||
descriptor = match.resolved_descriptor
|
||||
descriptor.metadata.update(
|
||||
{
|
||||
"node_id": node.node_id,
|
||||
"attempt_index": attempt_index,
|
||||
"requested_role": requested_role,
|
||||
"requested_capabilities": requested_capabilities,
|
||||
}
|
||||
)
|
||||
return descriptor, TargetResolutionReport(
|
||||
node_id=node.node_id,
|
||||
requested_role=requested_role,
|
||||
requested_capabilities=requested_capabilities,
|
||||
selected_agent_id=match.agent_id,
|
||||
fallback_used=False,
|
||||
score=match.score,
|
||||
reason="; ".join(match.reasons),
|
||||
)
|
||||
fallback = AgentDescriptor(
|
||||
name=node.agent.name or node.node_id,
|
||||
role=node.agent.role,
|
||||
system_prompt=node.agent.system_prompt,
|
||||
model=node.agent.model,
|
||||
provider_name=node.agent.provider_name,
|
||||
metadata={
|
||||
**node.agent.metadata,
|
||||
"node_id": node.node_id,
|
||||
"attempt_index": attempt_index,
|
||||
"requested_role": requested_role,
|
||||
"requested_capabilities": requested_capabilities,
|
||||
"resolution": "fallback_ephemeral",
|
||||
},
|
||||
)
|
||||
return fallback, TargetResolutionReport(
|
||||
node_id=node.node_id,
|
||||
requested_role=requested_role,
|
||||
requested_capabilities=requested_capabilities,
|
||||
selected_agent_id=None,
|
||||
fallback_used=True,
|
||||
score=0.0,
|
||||
reason="no active registered specialist matched planner requirements",
|
||||
)
|
||||
|
||||
def best_match(
|
||||
self,
|
||||
*,
|
||||
requested_role: str,
|
||||
requested_capabilities: list[str],
|
||||
requested_tags: list[str],
|
||||
pinned_skills: list[str],
|
||||
task_text: str,
|
||||
) -> AgentMatch | None:
|
||||
matches = [
|
||||
self._score_agent(
|
||||
agent,
|
||||
requested_role=requested_role,
|
||||
requested_capabilities=requested_capabilities,
|
||||
requested_tags=requested_tags,
|
||||
pinned_skills=pinned_skills,
|
||||
task_text=task_text,
|
||||
)
|
||||
for agent in self.registry.list_active_agents()
|
||||
]
|
||||
matches = [match for match in matches if match.score > 0]
|
||||
if not matches:
|
||||
return None
|
||||
matches.sort(key=lambda item: (item.score, item.resolved_descriptor.metadata.get("priority", 0)), reverse=True)
|
||||
return matches[0]
|
||||
|
||||
def _score_agent(
|
||||
self,
|
||||
agent: RegisteredAgent,
|
||||
*,
|
||||
requested_role: str,
|
||||
requested_capabilities: list[str],
|
||||
requested_tags: list[str],
|
||||
pinned_skills: list[str],
|
||||
task_text: str,
|
||||
) -> AgentMatch:
|
||||
score = 0.0
|
||||
reasons: list[str] = []
|
||||
requested_role_terms = _terms(requested_role)
|
||||
capability_terms = _terms(" ".join(requested_capabilities))
|
||||
tag_terms = _terms(" ".join(requested_tags))
|
||||
skill_terms = _terms(" ".join(pinned_skills))
|
||||
task_terms = _terms(task_text)
|
||||
agent_role_terms = _terms(agent.role + " " + agent.name + " " + agent.display_name)
|
||||
agent_capability_terms = _terms(" ".join(agent.capabilities))
|
||||
agent_tag_terms = _terms(" ".join(agent.tags))
|
||||
agent_skill_terms = _terms(" ".join(agent.skill_names))
|
||||
agent_all_terms = (
|
||||
agent_role_terms
|
||||
| agent_capability_terms
|
||||
| agent_tag_terms
|
||||
| agent_skill_terms
|
||||
| _terms(agent.description)
|
||||
)
|
||||
|
||||
role_hits = requested_role_terms & agent_role_terms
|
||||
if role_hits:
|
||||
score += 60 + 5 * len(role_hits)
|
||||
reasons.append(f"role matched: {', '.join(sorted(role_hits))}")
|
||||
|
||||
capability_hits = capability_terms & agent_capability_terms
|
||||
if capability_hits:
|
||||
score += 30 + 5 * len(capability_hits)
|
||||
reasons.append(f"capabilities matched: {', '.join(sorted(capability_hits))}")
|
||||
|
||||
tag_hits = tag_terms & agent_tag_terms
|
||||
if tag_hits:
|
||||
score += 10 + 3 * len(tag_hits)
|
||||
reasons.append(f"tags matched: {', '.join(sorted(tag_hits))}")
|
||||
|
||||
skill_hits = skill_terms & agent_skill_terms
|
||||
if skill_hits:
|
||||
score += 25 + 5 * len(skill_hits)
|
||||
reasons.append(f"skills matched: {', '.join(sorted(skill_hits))}")
|
||||
|
||||
task_hits = task_terms & agent_all_terms
|
||||
if task_hits:
|
||||
score += min(20, len(task_hits) * 2)
|
||||
reasons.append("task text matched registry profile")
|
||||
|
||||
score += agent.priority / 100.0
|
||||
descriptor = agent.to_descriptor()
|
||||
descriptor.metadata["priority"] = agent.priority
|
||||
return AgentMatch(
|
||||
agent_id=agent.agent_id,
|
||||
score=round(score, 3),
|
||||
reasons=reasons or ["priority fallback"],
|
||||
matched_capabilities=sorted(capability_hits),
|
||||
resolved_descriptor=descriptor,
|
||||
)
|
||||
|
||||
|
||||
def _terms(value: Any) -> set[str]:
|
||||
text = str(value or "")
|
||||
normalized = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
|
||||
return {part for part in normalized.split() if part}
|
||||
185
app-instance/backend/beaver/coordinator/registry/store.py
Normal file
185
app-instance/backend/beaver/coordinator/registry/store.py
Normal file
@ -0,0 +1,185 @@
|
||||
"""File-backed workspace agent registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .models import RegisteredAgent
|
||||
|
||||
|
||||
class AgentRegistry:
|
||||
def __init__(self, workspace: str | Path) -> None:
|
||||
self.workspace = Path(workspace)
|
||||
self.path = self.workspace / "agents" / "registry.json"
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not self.path.exists():
|
||||
self._write_agents(_builtin_agents())
|
||||
|
||||
def list_agents(self, *, include_disabled: bool = True) -> list[RegisteredAgent]:
|
||||
agents = self._read_agents()
|
||||
if include_disabled:
|
||||
return agents
|
||||
return [agent for agent in agents if agent.status == "active"]
|
||||
|
||||
def list_active_agents(self) -> list[RegisteredAgent]:
|
||||
return self.list_agents(include_disabled=False)
|
||||
|
||||
def get_agent(self, agent_id: str) -> RegisteredAgent | None:
|
||||
needle = agent_id.strip()
|
||||
for agent in self.list_agents():
|
||||
if agent.agent_id == needle:
|
||||
return agent
|
||||
return None
|
||||
|
||||
def upsert_agent(self, payload: dict[str, Any] | RegisteredAgent) -> RegisteredAgent:
|
||||
agent = payload if isinstance(payload, RegisteredAgent) else RegisteredAgent.from_dict(payload)
|
||||
agents = self.list_agents()
|
||||
for index, existing in enumerate(agents):
|
||||
if existing.agent_id == agent.agent_id:
|
||||
if existing.source == "builtin" and agent.source == "workspace":
|
||||
agent.source = "builtin"
|
||||
agent.created_at = existing.created_at
|
||||
agents[index] = agent
|
||||
self._write_agents(agents)
|
||||
return agent
|
||||
agents.append(agent)
|
||||
self._write_agents(agents)
|
||||
return agent
|
||||
|
||||
def disable_agent(self, agent_id: str) -> RegisteredAgent:
|
||||
agents = self.list_agents()
|
||||
for index, agent in enumerate(agents):
|
||||
if agent.agent_id != agent_id:
|
||||
continue
|
||||
agent.status = "disabled"
|
||||
agents[index] = agent
|
||||
self._write_agents(agents)
|
||||
return agent
|
||||
raise ValueError(f"Unknown agent_id: {agent_id}")
|
||||
|
||||
def search(
|
||||
self,
|
||||
*,
|
||||
role: str = "",
|
||||
capabilities: list[str] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
skills: list[str] | None = None,
|
||||
) -> list[RegisteredAgent]:
|
||||
role_terms = _terms(role)
|
||||
capability_terms = set(_terms(" ".join(capabilities or [])))
|
||||
tag_terms = set(_terms(" ".join(tags or [])))
|
||||
skill_terms = set(_terms(" ".join(skills or [])))
|
||||
matches: list[RegisteredAgent] = []
|
||||
for agent in self.list_active_agents():
|
||||
haystack = set(
|
||||
_terms(
|
||||
" ".join(
|
||||
[
|
||||
agent.agent_id,
|
||||
agent.name,
|
||||
agent.display_name,
|
||||
agent.role,
|
||||
agent.description,
|
||||
" ".join(agent.capabilities),
|
||||
" ".join(agent.tags),
|
||||
" ".join(agent.skill_names),
|
||||
]
|
||||
)
|
||||
)
|
||||
)
|
||||
if role_terms and not role_terms.intersection(haystack):
|
||||
continue
|
||||
if capability_terms and not capability_terms.intersection(haystack):
|
||||
continue
|
||||
if tag_terms and not tag_terms.intersection(haystack):
|
||||
continue
|
||||
if skill_terms and not skill_terms.intersection(haystack):
|
||||
continue
|
||||
matches.append(agent)
|
||||
return matches
|
||||
|
||||
def _read_agents(self) -> list[RegisteredAgent]:
|
||||
if not self.path.exists():
|
||||
return []
|
||||
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
raw_agents = payload.get("agents") if isinstance(payload, dict) else payload
|
||||
if not isinstance(raw_agents, list):
|
||||
return []
|
||||
return [RegisteredAgent.from_dict(item) for item in raw_agents if isinstance(item, dict)]
|
||||
|
||||
def _write_agents(self, agents: list[RegisteredAgent]) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {"version": 1, "agents": [agent.to_dict() for agent in agents]}
|
||||
self.path.write_text(json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _terms(text: str) -> set[str]:
|
||||
normalized = "".join(ch.lower() if ch.isalnum() else " " for ch in text)
|
||||
return {part for part in normalized.split() if part}
|
||||
|
||||
|
||||
def _builtin_agents() -> list[RegisteredAgent]:
|
||||
return [
|
||||
RegisteredAgent(
|
||||
agent_id="researcher",
|
||||
name="researcher",
|
||||
display_name="Researcher",
|
||||
role="research",
|
||||
description="Finds facts, references, constraints, and implementation options.",
|
||||
system_prompt="You are a research specialist. Gather concise evidence and tradeoffs for the parent task.",
|
||||
capabilities=["research", "analysis", "source review", "requirements"],
|
||||
tags=["planning", "research"],
|
||||
priority=50,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="implementer",
|
||||
name="implementer",
|
||||
display_name="Implementer",
|
||||
role="implementation",
|
||||
description="Builds scoped implementation slices and proposes concrete changes.",
|
||||
system_prompt="You are an implementation specialist. Produce practical, scoped implementation output.",
|
||||
capabilities=["implementation", "coding", "refactor", "integration"],
|
||||
tags=["coding", "build"],
|
||||
priority=45,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="reviewer",
|
||||
name="reviewer",
|
||||
display_name="Reviewer",
|
||||
role="review",
|
||||
description="Reviews plans, code, outputs, and risks before final synthesis.",
|
||||
system_prompt="You are a review specialist. Focus on defects, missing requirements, and risks.",
|
||||
capabilities=["review", "quality", "risk", "verification"],
|
||||
tags=["review", "quality"],
|
||||
priority=45,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="tester",
|
||||
name="tester",
|
||||
display_name="Tester",
|
||||
role="testing",
|
||||
description="Designs and executes verification checks for task outputs.",
|
||||
system_prompt="You are a testing specialist. Identify focused checks and report pass/fail evidence.",
|
||||
capabilities=["testing", "verification", "regression", "qa"],
|
||||
tags=["test", "quality"],
|
||||
priority=40,
|
||||
source="builtin",
|
||||
),
|
||||
RegisteredAgent(
|
||||
agent_id="documenter",
|
||||
name="documenter",
|
||||
display_name="Documenter",
|
||||
role="documentation",
|
||||
description="Writes and reconciles user-facing and internal documentation updates.",
|
||||
system_prompt="You are a documentation specialist. Produce concise docs aligned with the implementation.",
|
||||
capabilities=["documentation", "explanation", "migration notes", "release notes"],
|
||||
tags=["docs", "communication"],
|
||||
priority=35,
|
||||
source="builtin",
|
||||
),
|
||||
]
|
||||
@ -1,2 +1,19 @@
|
||||
"""Team models and orchestration objects."""
|
||||
|
||||
from ..models import (
|
||||
AgentDescriptor,
|
||||
DelegationEnvelope,
|
||||
ExecutionGraph,
|
||||
ExecutionNode,
|
||||
NodeRunResult,
|
||||
TeamRunResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgentDescriptor",
|
||||
"DelegationEnvelope",
|
||||
"ExecutionGraph",
|
||||
"ExecutionNode",
|
||||
"NodeRunResult",
|
||||
"TeamRunResult",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user