mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-08-28 05:15:00 +00:00
913 lines
36 KiB
Python
913 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import copy
|
|
import os
|
|
import time
|
|
import uuid
|
|
from dataclasses import replace
|
|
from typing import Any, Mapping
|
|
|
|
from openspace.agents.agent_definitions import (
|
|
AgentDefinition,
|
|
AgentDefinitionRegistry,
|
|
AgentDefinitionsResult,
|
|
filter_agents_by_mcp_requirements,
|
|
)
|
|
from openspace.agents.agent_tool_utils import (
|
|
AGENT_TOOL_NAME,
|
|
AgentToolResult,
|
|
filter_denied_agents,
|
|
finalize_agent_result,
|
|
format_agent_line,
|
|
format_agent_tool_result,
|
|
resolve_agent_tools,
|
|
should_inject_agent_list_in_messages,
|
|
tool_matches_name,
|
|
)
|
|
from openspace.agents.built_in_agents import BASH_TOOL_NAME, FILE_READ_TOOL_NAME
|
|
from openspace.agents.task_manager import AgentTask, TaskManager, TaskType
|
|
from openspace.grounding.core.permissions.types import PermissionAllow
|
|
from openspace.grounding.core.tool.base import BaseTool
|
|
from openspace.grounding.core.types import BackendType, ToolResult, ToolStatus
|
|
from openspace.utils.logging import Logger
|
|
|
|
logger = Logger.get_logger(__name__)
|
|
|
|
|
|
class AgentTool(BaseTool):
|
|
"""Launch a subagent to handle a complex task.
|
|
|
|
Implementation notes: ``tools/AgentTool/AgentTool.tsx``. This OS port keeps the
|
|
core engine path in-process: select an ``AgentDefinition``, filter the
|
|
tool pool, run ``GroundingAgent.process()`` with ``pre_filtered_tools``,
|
|
and map the result back into a single tool result for the parent model.
|
|
"""
|
|
|
|
_name = AGENT_TOOL_NAME
|
|
_description = "Launch a new agent"
|
|
backend_type = BackendType.META
|
|
_is_read_only = True
|
|
_is_concurrency_safe = True
|
|
max_result_size_chars = 100_000
|
|
aliases = ["Task"]
|
|
search_hint = "delegate work to a subagent"
|
|
parameter_descriptions = {
|
|
"name": (
|
|
"Optional teammate name. When combined with team_name or an active "
|
|
"TeamCreate team, launches an in-process teammate."
|
|
),
|
|
"team_name": "Optional team name for teammate spawning.",
|
|
"description": "A short (3-5 word) description of the task.",
|
|
"prompt": "The task for the agent to perform.",
|
|
"subagent_type": (
|
|
"The type of specialized agent to use. If omitted, the "
|
|
"general-purpose agent is used."
|
|
),
|
|
"model": (
|
|
"Optional model override for this agent. Takes precedence over "
|
|
"the agent definition model. If omitted, inherits from the parent."
|
|
),
|
|
"run_in_background": (
|
|
"Set to true to run this agent in the background. A lightweight "
|
|
"task handle and output file are returned."
|
|
),
|
|
}
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
registry: AgentDefinitionRegistry | None = None,
|
|
parent_agent: Any | None = None,
|
|
grounding_client: Any | None = None,
|
|
llm_client: Any | None = None,
|
|
orchestrator: Any | None = None,
|
|
) -> None:
|
|
self._registry = registry or AgentDefinitionRegistry()
|
|
self._parent_agent = parent_agent
|
|
self._grounding_client = grounding_client
|
|
self._llm_client = llm_client
|
|
self._orchestrator = orchestrator
|
|
self._current_context: Any | None = None
|
|
super().__init__(verbose=False, handle_errors=False)
|
|
|
|
def set_context(self, context: Any) -> None:
|
|
self._current_context = context
|
|
|
|
def get_prompt(self, context: Any = None) -> str:
|
|
active_agents = self._active_agents_for_prompt(context)
|
|
list_via_attachment = should_inject_agent_list_in_messages()
|
|
if list_via_attachment:
|
|
agent_list = (
|
|
"Available agent types are listed in <system-reminder> "
|
|
"messages in the conversation."
|
|
)
|
|
else:
|
|
agent_list = (
|
|
"Available agent types and the tools they have access to:\n"
|
|
+ "\n".join(format_agent_line(agent) for agent in active_agents)
|
|
)
|
|
|
|
concurrency_note = (
|
|
"- Launch multiple agents concurrently whenever possible by using "
|
|
"a single assistant message with multiple Agent tool calls.\n"
|
|
)
|
|
return (
|
|
"Launch a new agent to handle complex, multi-step tasks autonomously.\n\n"
|
|
"The Agent tool launches specialized agents that work independently "
|
|
"and return a single result message to you.\n\n"
|
|
f"{agent_list}\n\n"
|
|
"When using the Agent tool, specify a subagent_type parameter to "
|
|
"select which agent type to use. If omitted, the general-purpose "
|
|
"agent is used.\n\n"
|
|
"When NOT to use the Agent tool:\n"
|
|
f"- If you want to read a specific file path, use {FILE_READ_TOOL_NAME}.\n"
|
|
"- If you are searching within a known file or a small set of files, "
|
|
f"use {FILE_READ_TOOL_NAME} or grep directly.\n"
|
|
"- Other tasks unrelated to the agent descriptions above.\n\n"
|
|
"Usage notes:\n"
|
|
"- Always include a short description (3-5 words) summarizing what "
|
|
"the agent will do.\n"
|
|
f"{concurrency_note}"
|
|
"- Use foreground execution when you need the result before your "
|
|
"next step. Use run_in_background=true only for independent work.\n"
|
|
"- The agent starts without the parent conversation; provide a "
|
|
"complete briefing with relevant paths, findings, and constraints.\n"
|
|
"- The agent result is not shown to the user automatically; summarize "
|
|
"it yourself when needed."
|
|
)
|
|
|
|
async def check_permissions(
|
|
self,
|
|
input: dict[str, Any],
|
|
context: Any = None,
|
|
):
|
|
return PermissionAllow(updated_input=input)
|
|
|
|
async def _arun(
|
|
self,
|
|
prompt: str,
|
|
description: str,
|
|
name: str | None = None,
|
|
team_name: str | None = None,
|
|
subagent_type: str | None = None,
|
|
model: str | None = None,
|
|
run_in_background: bool = False,
|
|
) -> ToolResult:
|
|
if not prompt or not str(prompt).strip():
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error="Agent prompt is required",
|
|
content="Error: Agent prompt is required",
|
|
)
|
|
|
|
context = self._current_context
|
|
if context is None:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error="AgentTool was not given a ToolUseContext",
|
|
content="Error: AgentTool was not given a ToolUseContext",
|
|
)
|
|
|
|
coordinator = getattr(context, "coordinator_mode", None) or getattr(
|
|
self._parent_agent,
|
|
"_coordinator_mode",
|
|
None,
|
|
)
|
|
coordinator_enabled = bool(getattr(context, "coordinator_mode_enabled", False))
|
|
requested_agent_type = subagent_type or "general-purpose"
|
|
if coordinator_enabled and requested_agent_type == "worker":
|
|
requested_agent_type = "general-purpose"
|
|
|
|
agent_result = _coerce_agent_definitions_result(
|
|
getattr(context, "agent_definitions", None),
|
|
self._registry,
|
|
)
|
|
selected = self._select_agent(
|
|
agent_result,
|
|
requested_agent_type,
|
|
context,
|
|
)
|
|
if isinstance(selected, ToolResult):
|
|
return selected
|
|
agent_def = selected
|
|
worker_name = name or description or agent_def.agent_type
|
|
if coordinator_enabled and coordinator is not None:
|
|
agent_def = coordinator.prepare_worker_agent_definition(
|
|
agent_def,
|
|
worker_name=worker_name,
|
|
)
|
|
|
|
is_async = bool(run_in_background or agent_def.background or coordinator_enabled)
|
|
available_tools = list(
|
|
getattr(context, "coordinator_worker_tools", None)
|
|
or getattr(context, "all_tools", None)
|
|
or getattr(context, "tools", [])
|
|
)
|
|
resolved = resolve_agent_tools(
|
|
agent_def,
|
|
available_tools,
|
|
is_async=is_async,
|
|
)
|
|
filtered_tools = resolved.resolved_tools
|
|
if resolved.allowed_agent_types:
|
|
# Nested Agent metadata is carried only for coordinator/team steps.
|
|
logger.debug(
|
|
"Agent %s resolved allowed nested agent types: %s",
|
|
agent_def.agent_type,
|
|
resolved.allowed_agent_types,
|
|
)
|
|
|
|
resolved_model = _resolve_agent_model(model, agent_def.model, context)
|
|
task_description = description or agent_def.description or agent_def.when_to_use
|
|
|
|
if (name or team_name) and not coordinator_enabled:
|
|
if not name:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error="name is required when using Agent as a teammate spawn.",
|
|
content="Error: name is required when using Agent as a teammate spawn.",
|
|
)
|
|
task_manager = _get_task_manager(context)
|
|
resolved_team = team_name or getattr(task_manager, "active_team_name", None)
|
|
if not resolved_team:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error=(
|
|
"team_name is required for teammate spawn. Use "
|
|
"TeamCreate first or pass team_name."
|
|
),
|
|
content=(
|
|
"Error: team_name is required for teammate spawn. Use "
|
|
"TeamCreate first or pass team_name."
|
|
),
|
|
)
|
|
orchestrator = self._orchestrator or getattr(
|
|
context,
|
|
"multi_agent_orchestrator",
|
|
None,
|
|
)
|
|
if orchestrator is None:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error="Agent teammate spawn requires MultiAgentOrchestrator.",
|
|
content="Error: Agent teammate spawn requires MultiAgentOrchestrator.",
|
|
)
|
|
try:
|
|
spawn_result = await orchestrator.spawn_teammate(
|
|
name=name,
|
|
prompt=prompt,
|
|
parent_context=context,
|
|
available_tools=list(
|
|
getattr(context, "coordinator_worker_tools", None)
|
|
or getattr(context, "all_tools", None)
|
|
or getattr(context, "tools", [])
|
|
),
|
|
team_name=str(resolved_team),
|
|
agent_type=agent_def.agent_type,
|
|
model=resolved_model,
|
|
description=task_description,
|
|
)
|
|
except Exception as exc:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error=str(exc),
|
|
content=f"Error: {exc}",
|
|
)
|
|
data = {"status": "teammate_spawned", **spawn_result.to_dict()}
|
|
return ToolResult(
|
|
status=ToolStatus.SUCCESS,
|
|
content=_format_teammate_spawn_result(data),
|
|
metadata={"tool": self.name, "agent_result": data},
|
|
)
|
|
|
|
if is_async:
|
|
task_manager = _get_task_manager(context)
|
|
if task_manager is None:
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error=(
|
|
"AgentTool requires a session-scoped TaskManager. "
|
|
"Initialize MultiAgentOrchestrator before launching "
|
|
"background agents."
|
|
),
|
|
content=(
|
|
"Error: AgentTool requires a session-scoped "
|
|
"TaskManager for background agents."
|
|
),
|
|
)
|
|
if coordinator_enabled:
|
|
worker_team_name = team_name or getattr(
|
|
task_manager,
|
|
"active_team_name",
|
|
None,
|
|
)
|
|
else:
|
|
worker_team_name = None
|
|
|
|
async def runner(task: AgentTask) -> AgentToolResult:
|
|
spawn_payload = {
|
|
"agent_id": task.agent_id,
|
|
"agent_type": agent_def.agent_type,
|
|
"team_name": worker_team_name,
|
|
"description": task_description,
|
|
"status": "running",
|
|
"background": True,
|
|
"task_id": task.id,
|
|
"parent_task_id": getattr(context, "task_id", None),
|
|
"session_id": getattr(context, "session_id", None),
|
|
}
|
|
await context.emit_event("agent_spawn", spawn_payload)
|
|
await context.emit_event(
|
|
"agent_event",
|
|
{
|
|
"session_id": getattr(context, "session_id", None),
|
|
"agent_id": task.agent_id,
|
|
"event": "agent_spawn",
|
|
"payload": spawn_payload,
|
|
},
|
|
)
|
|
return await run_agent(
|
|
agent_def=agent_def,
|
|
prompt=prompt,
|
|
filtered_tools=filtered_tools,
|
|
allowed_agent_types=resolved.allowed_agent_types,
|
|
parent_context=context,
|
|
parent_agent=self._parent_agent,
|
|
grounding_client=self._grounding_client,
|
|
llm_client=self._llm_client,
|
|
resolved_model=resolved_model,
|
|
agent_id=task.agent_id,
|
|
task_description=task_description,
|
|
is_async_agent=True,
|
|
abort_event=task.abort_event,
|
|
message_source=task.inbox,
|
|
)
|
|
|
|
task = await task_manager.register_async_agent(
|
|
runner=runner,
|
|
prompt=prompt,
|
|
description=task_description,
|
|
agent_type=agent_def.agent_type,
|
|
selected_agent=agent_def,
|
|
model=resolved_model,
|
|
parent_abort_event=getattr(context, "abort_event", None),
|
|
parent_task_id=getattr(context, "task_id", None),
|
|
parent_inbox=(
|
|
getattr(context, "coordinator_notification_queue", None)
|
|
if coordinator_enabled
|
|
else None
|
|
),
|
|
task_type=(
|
|
TaskType.COORDINATOR_WORKER
|
|
if coordinator_enabled
|
|
else TaskType.LOCAL_AGENT
|
|
),
|
|
team_name=worker_team_name,
|
|
)
|
|
can_read_output_file = any(
|
|
tool_matches_name(tool, FILE_READ_TOOL_NAME)
|
|
or tool_matches_name(tool, BASH_TOOL_NAME)
|
|
for tool in getattr(context, "tools", [])
|
|
)
|
|
data = {
|
|
"status": "async_launched",
|
|
"agent_id": task.agent_id,
|
|
"task_id": task.id,
|
|
"agent_type": agent_def.agent_type,
|
|
"description": task_description,
|
|
"prompt": prompt,
|
|
"output_file": task.output_file,
|
|
"can_read_output_file": can_read_output_file,
|
|
}
|
|
return ToolResult(
|
|
status=ToolStatus.SUCCESS,
|
|
content=format_agent_tool_result(data),
|
|
metadata={"tool": self.name, "agent_result": data},
|
|
)
|
|
|
|
agent_id = f"agent_{uuid.uuid4().hex[:12]}"
|
|
spawn_payload = {
|
|
"agent_id": agent_id,
|
|
"agent_type": agent_def.agent_type,
|
|
"description": task_description,
|
|
"status": "starting",
|
|
"background": False,
|
|
"task_id": agent_id,
|
|
"parent_task_id": getattr(context, "task_id", None),
|
|
"session_id": getattr(context, "session_id", None),
|
|
}
|
|
await context.emit_event("agent_spawn", spawn_payload)
|
|
await context.emit_event(
|
|
"agent_event",
|
|
{
|
|
"session_id": getattr(context, "session_id", None),
|
|
"agent_id": agent_id,
|
|
"event": "agent_spawn",
|
|
"payload": spawn_payload,
|
|
},
|
|
)
|
|
result = await run_agent(
|
|
agent_def=agent_def,
|
|
prompt=prompt,
|
|
filtered_tools=filtered_tools,
|
|
allowed_agent_types=resolved.allowed_agent_types,
|
|
parent_context=context,
|
|
parent_agent=self._parent_agent,
|
|
grounding_client=self._grounding_client,
|
|
llm_client=self._llm_client,
|
|
resolved_model=resolved_model,
|
|
agent_id=agent_id,
|
|
task_description=task_description,
|
|
)
|
|
return ToolResult(
|
|
status=ToolStatus.SUCCESS if result.status == "completed" else ToolStatus.ERROR,
|
|
content=format_agent_tool_result(result),
|
|
error=None if result.status == "completed" else format_agent_tool_result(result),
|
|
metadata={"tool": self.name, "agent_result": _agent_result_to_dict(result)},
|
|
)
|
|
|
|
def _active_agents_for_prompt(self, context: Any = None) -> list[AgentDefinition]:
|
|
agent_result = _coerce_agent_definitions_result(
|
|
getattr(context, "agent_definitions", None) if context is not None else None,
|
|
self._registry,
|
|
)
|
|
agents = list(agent_result.active_agents)
|
|
if context is not None:
|
|
agents = filter_agents_by_mcp_requirements(
|
|
agents,
|
|
_mcp_servers_from_context_tools(context),
|
|
)
|
|
agents = filter_denied_agents(
|
|
agents,
|
|
getattr(context, "permission_context", None),
|
|
)
|
|
allowed = getattr(agent_result, "allowed_agent_types", None)
|
|
if allowed:
|
|
allowed_set = set(allowed)
|
|
agents = [agent for agent in agents if agent.agent_type in allowed_set]
|
|
return agents
|
|
|
|
def _select_agent(
|
|
self,
|
|
agent_result: AgentDefinitionsResult,
|
|
agent_type: str,
|
|
context: Any,
|
|
) -> AgentDefinition | ToolResult:
|
|
all_agents = list(agent_result.active_agents)
|
|
allowed = set(agent_result.allowed_agent_types or [])
|
|
if allowed:
|
|
candidates = [agent for agent in all_agents if agent.agent_type in allowed]
|
|
else:
|
|
candidates = all_agents
|
|
|
|
mcp_servers = _mcp_servers_from_context_tools(context)
|
|
candidates = filter_agents_by_mcp_requirements(candidates, mcp_servers)
|
|
candidates = filter_denied_agents(
|
|
candidates,
|
|
getattr(context, "permission_context", None),
|
|
)
|
|
|
|
for agent in candidates:
|
|
if agent.agent_type == agent_type:
|
|
return agent
|
|
|
|
if any(agent.agent_type == agent_type for agent in all_agents):
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error=f"Agent type '{agent_type}' is not available in this context.",
|
|
content=f"Error: Agent type '{agent_type}' is not available in this context.",
|
|
)
|
|
available = ", ".join(agent.agent_type for agent in candidates) or "none"
|
|
return ToolResult(
|
|
status=ToolStatus.ERROR,
|
|
error=f"Agent type '{agent_type}' not found. Available agents: {available}",
|
|
content=f"Error: Agent type '{agent_type}' not found. Available agents: {available}",
|
|
)
|
|
|
|
|
|
async def run_agent(
|
|
*,
|
|
agent_def: AgentDefinition,
|
|
prompt: str,
|
|
filtered_tools: list[BaseTool],
|
|
parent_context: Any,
|
|
parent_agent: Any | None,
|
|
grounding_client: Any | None,
|
|
llm_client: Any | None,
|
|
resolved_model: str,
|
|
agent_id: str | None = None,
|
|
task_description: str | None = None,
|
|
allowed_agent_types: list[str] | None = None,
|
|
is_async_agent: bool = False,
|
|
child_context_modifier: Any | None = None,
|
|
abort_event: asyncio.Event | None = None,
|
|
message_source: Any | None = None,
|
|
) -> AgentToolResult:
|
|
"""Run a foreground subagent with the same GroundingAgent engine."""
|
|
|
|
from openspace.agents.grounding_agent import GroundingAgent
|
|
|
|
start = time.time()
|
|
agent_id = agent_id or f"agent_{uuid.uuid4().hex[:12]}"
|
|
child_llm = _clone_llm_client(llm_client, resolved_model)
|
|
|
|
system_prompt = agent_def.system_prompt(tool_use_context=parent_context)
|
|
if agent_def.critical_system_reminder:
|
|
system_prompt = f"{system_prompt}\n\n{agent_def.critical_system_reminder}"
|
|
|
|
child = GroundingAgent(
|
|
name=f"agent:{agent_def.agent_type}:{agent_id}",
|
|
backend_scope=agent_def.backend_scope,
|
|
llm_client=child_llm,
|
|
grounding_client=grounding_client,
|
|
recording_manager=getattr(parent_context, "recording_manager", None),
|
|
system_prompt=system_prompt,
|
|
max_iterations=agent_def.max_turns or getattr(parent_context, "max_iterations", 50) or 50,
|
|
tool_retrieval_llm=getattr(parent_agent, "_tool_retrieval_llm", None),
|
|
skill_selection_llm=getattr(parent_agent, "_skill_selection_llm", None),
|
|
)
|
|
child.set_runtime_event_sink(getattr(parent_context, "event_sink", None))
|
|
if hasattr(parent_agent, "_skill_registry"):
|
|
child.set_skill_registry(getattr(parent_agent, "_skill_registry", None))
|
|
elif getattr(parent_context, "skill_registry", None) is not None:
|
|
child.set_skill_registry(getattr(parent_context, "skill_registry", None))
|
|
child._skill_store = getattr(parent_context, "skill_store", None)
|
|
if parent_agent is not None:
|
|
child._skill_store = getattr(parent_agent, "_skill_store", None)
|
|
child.set_skill_protocol_settings(
|
|
listing_enabled=getattr(parent_agent, "_skill_listing_enabled", True),
|
|
discovery_enabled=getattr(parent_agent, "_skill_discovery_enabled", True),
|
|
discovery_max_results=getattr(parent_agent, "_skill_discovery_max_results", 5),
|
|
listing_budget_context_percent=getattr(
|
|
parent_agent,
|
|
"_skill_listing_budget_context_percent",
|
|
0.01,
|
|
),
|
|
listing_max_description_chars=getattr(
|
|
parent_agent,
|
|
"_skill_listing_max_description_chars",
|
|
250,
|
|
),
|
|
post_tool_query_builder_enabled=getattr(
|
|
parent_agent,
|
|
"_post_tool_query_builder_enabled",
|
|
False,
|
|
),
|
|
post_tool_query_builder_model=getattr(
|
|
parent_agent,
|
|
"_post_tool_query_builder_model",
|
|
None,
|
|
),
|
|
post_tool_query_builder_max_chars=getattr(
|
|
parent_agent,
|
|
"_post_tool_query_builder_max_chars",
|
|
4000,
|
|
),
|
|
)
|
|
|
|
child_is_async_agent = bool(
|
|
is_async_agent or getattr(parent_context, "is_async_agent", False)
|
|
)
|
|
parent_permission_context = getattr(parent_context, "permission_context", None)
|
|
parent_base_permission_context = (
|
|
getattr(parent_context, "base_permission_context", None)
|
|
or parent_permission_context
|
|
)
|
|
parent_permission_mode = str(
|
|
getattr(
|
|
parent_permission_context,
|
|
"mode",
|
|
getattr(parent_context, "permission_mode", "default"),
|
|
)
|
|
or "default"
|
|
)
|
|
protected_parent_modes = {"acceptEdits", "bypassPermissions", "auto"}
|
|
requested_permission_mode = agent_def.permission_mode
|
|
effective_permission_mode = (
|
|
parent_permission_mode
|
|
if parent_permission_mode in protected_parent_modes
|
|
else (requested_permission_mode or parent_permission_mode)
|
|
)
|
|
|
|
def _with_permission_mode(permission_context: Any | None) -> Any | None:
|
|
if permission_context is None:
|
|
return None
|
|
if getattr(permission_context, "mode", None) == effective_permission_mode:
|
|
return permission_context
|
|
with_mode = getattr(permission_context, "with_mode", None)
|
|
if callable(with_mode):
|
|
try:
|
|
return with_mode(effective_permission_mode)
|
|
except Exception:
|
|
return permission_context
|
|
return permission_context
|
|
|
|
child_context = {
|
|
"instruction": prompt,
|
|
"workspace_dir": getattr(parent_context, "cwd", "."),
|
|
"agent_id": agent_id,
|
|
"agent_type": agent_def.agent_type,
|
|
"task_id": agent_id,
|
|
"parent_task_id": (
|
|
getattr(parent_context, "task_id", None)
|
|
or getattr(parent_context, "parent_task_id", None)
|
|
),
|
|
"session_id": getattr(parent_context, "session_id", None),
|
|
"session_dir": getattr(parent_context, "session_dir", None),
|
|
"memory_mode": getattr(parent_context, "memory_mode", "direct"),
|
|
"pre_filtered_tools": list(filtered_tools),
|
|
"all_tools": list(filtered_tools),
|
|
"abort_event": abort_event or getattr(parent_context, "abort_event", None),
|
|
"message_source": message_source,
|
|
"read_file_state": copy.copy(getattr(parent_context, "read_file_state", {}) or {}),
|
|
"todo_state": getattr(parent_context, "todo_state", None),
|
|
"nested_memory_triggers": set(),
|
|
"loaded_nested_memory_paths": set(),
|
|
"dynamic_skill_path_triggers": set(),
|
|
"sent_dynamic_skill_keys": set(
|
|
getattr(parent_context, "sent_dynamic_skill_keys", set()) or set()
|
|
),
|
|
"path_activated_skill_names": set(
|
|
getattr(parent_context, "path_activated_skill_names", set()) or set()
|
|
),
|
|
"sent_skill_names_by_agent": {
|
|
str(agent): set(names or ())
|
|
for agent, names in (
|
|
getattr(parent_context, "sent_skill_names_by_agent", {}) or {}
|
|
).items()
|
|
},
|
|
"discovered_skill_names": set(
|
|
getattr(parent_context, "discovered_skill_names", set()) or set()
|
|
),
|
|
"invoked_skills_by_agent": {
|
|
str(agent): list(records or [])
|
|
for agent, records in (
|
|
getattr(parent_context, "invoked_skills_by_agent", {}) or {}
|
|
).items()
|
|
},
|
|
"skill_listing_suppressed_once": bool(
|
|
getattr(parent_context, "skill_listing_suppressed_once", False)
|
|
),
|
|
"active_skill_scopes": _clone_active_skill_scopes_for_child(
|
|
getattr(parent_context, "active_skill_scopes", {}) or {}
|
|
),
|
|
"skill_model_override": getattr(parent_context, "skill_model_override", None),
|
|
"skill_effort_override": getattr(parent_context, "skill_effort_override", None),
|
|
"initial_tool_use_context_modifier": child_context_modifier,
|
|
"permission_engine": getattr(parent_context, "permission_engine", None),
|
|
"permission_mode": effective_permission_mode,
|
|
"permission_context": _with_permission_mode(parent_permission_context),
|
|
"base_permission_context": _with_permission_mode(parent_base_permission_context),
|
|
"event_sink": getattr(parent_context, "event_sink", None),
|
|
"recording_manager": getattr(parent_context, "recording_manager", None),
|
|
"quality_manager": getattr(parent_context, "quality_manager", None),
|
|
"cost_tracker": getattr(parent_context, "cost_tracker", None),
|
|
"hook_registry": getattr(parent_context, "hook_registry", None),
|
|
"tui_available": bool(getattr(parent_context, "tui_available", False))
|
|
and not child_is_async_agent,
|
|
"is_async_agent": child_is_async_agent,
|
|
"max_iterations": agent_def.max_turns or getattr(parent_context, "max_iterations", 50) or 50,
|
|
"task_description": task_description or prompt,
|
|
"task_manager": getattr(parent_context, "task_manager", None),
|
|
"multi_agent_orchestrator": getattr(parent_context, "multi_agent_orchestrator", None),
|
|
"coordinator_mode": getattr(parent_context, "coordinator_mode", None),
|
|
"coordinator_mode_enabled": bool(
|
|
getattr(parent_context, "coordinator_mode_enabled", False)
|
|
),
|
|
"coordinator_notification_queue": getattr(
|
|
parent_context,
|
|
"coordinator_notification_queue",
|
|
None,
|
|
),
|
|
"coordinator_worker_tools": list(
|
|
getattr(parent_context, "coordinator_worker_tools", ()) or ()
|
|
),
|
|
"agent_definitions": getattr(parent_context, "agent_definitions", None),
|
|
"allowed_agent_types": allowed_agent_types or agent_def.allowed_agent_types,
|
|
"session_storage": getattr(parent_context, "session_storage", None),
|
|
"tool_results_dir": getattr(parent_context, "tool_results_dir", None),
|
|
"file_history": getattr(parent_context, "file_history", None),
|
|
}
|
|
|
|
try:
|
|
process_result = await child.process(child_context)
|
|
try:
|
|
from openspace.skill_engine.protocol import restore_skill_state_from_messages
|
|
|
|
restore_skill_state_from_messages(
|
|
process_result.get("messages") or [],
|
|
parent_context,
|
|
)
|
|
except Exception:
|
|
pass
|
|
status = str(process_result.get("status") or "")
|
|
if status not in {"success", "completed"}:
|
|
messages = process_result.get("messages") or []
|
|
if messages:
|
|
try:
|
|
partial = finalize_agent_result(
|
|
messages=messages,
|
|
agent_id=agent_id,
|
|
agent_type=agent_def.agent_type,
|
|
prompt=prompt,
|
|
start_time=start,
|
|
)
|
|
partial.status = "completed" if status == "success" else "error"
|
|
return partial
|
|
except Exception:
|
|
pass
|
|
return AgentToolResult(
|
|
agent_id=agent_id,
|
|
agent_type=agent_def.agent_type,
|
|
content=[{"type": "text", "text": str(process_result.get("response") or process_result.get("error") or status)}],
|
|
total_tool_use_count=len(process_result.get("tool_executions") or []),
|
|
total_duration_ms=max(0, int((time.time() - start) * 1000)),
|
|
total_tokens=0,
|
|
usage={},
|
|
status="error",
|
|
prompt=prompt,
|
|
)
|
|
|
|
return finalize_agent_result(
|
|
messages=process_result.get("messages") or [],
|
|
agent_id=agent_id,
|
|
agent_type=agent_def.agent_type,
|
|
prompt=prompt,
|
|
start_time=start,
|
|
)
|
|
except asyncio.CancelledError:
|
|
return AgentToolResult(
|
|
agent_id=agent_id,
|
|
agent_type=agent_def.agent_type,
|
|
content=[{"type": "text", "text": "Subagent stopped."}],
|
|
total_tool_use_count=0,
|
|
total_duration_ms=max(0, int((time.time() - start) * 1000)),
|
|
total_tokens=0,
|
|
usage={},
|
|
status="stopped",
|
|
prompt=prompt,
|
|
)
|
|
except Exception as exc:
|
|
return AgentToolResult(
|
|
agent_id=agent_id,
|
|
agent_type=agent_def.agent_type,
|
|
content=[{"type": "text", "text": str(exc)}],
|
|
total_tool_use_count=0,
|
|
total_duration_ms=max(0, int((time.time() - start) * 1000)),
|
|
total_tokens=0,
|
|
usage={},
|
|
status="error",
|
|
prompt=prompt,
|
|
)
|
|
finally:
|
|
_cleanup_agent_todos(parent_context, agent_id)
|
|
|
|
|
|
def _clone_active_skill_scopes_for_child(scopes: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""Copy parent skill scopes without sharing hook registrations.
|
|
|
|
Child agents reuse the same HookRegistry object as the parent. If they hold
|
|
the parent's scope objects directly, child cleanup unregisters parent hooks.
|
|
"""
|
|
|
|
cloned: dict[str, Any] = {}
|
|
for key, scope in scopes.items():
|
|
try:
|
|
cloned[str(key)] = replace(scope, hook_registrations=[])
|
|
except Exception:
|
|
cloned[str(key)] = copy.copy(scope)
|
|
try:
|
|
cloned[str(key)].hook_registrations = []
|
|
except Exception:
|
|
pass
|
|
return cloned
|
|
|
|
|
|
def _cleanup_agent_todos(parent_context: Any, agent_id: str) -> None:
|
|
"""Clean up subagent-specific todo entries."""
|
|
|
|
todo_state = getattr(parent_context, "todo_state", None)
|
|
if isinstance(todo_state, dict):
|
|
todo_state.pop(agent_id, None)
|
|
|
|
|
|
def build_agent_tool(
|
|
*,
|
|
registry: AgentDefinitionRegistry,
|
|
parent_agent: Any,
|
|
grounding_client: Any,
|
|
llm_client: Any,
|
|
orchestrator: Any | None = None,
|
|
) -> AgentTool:
|
|
tool = AgentTool(
|
|
registry=registry,
|
|
parent_agent=parent_agent,
|
|
grounding_client=grounding_client,
|
|
llm_client=llm_client,
|
|
orchestrator=orchestrator,
|
|
)
|
|
tool.bind_runtime_info(
|
|
backend=BackendType.META,
|
|
session_name="agent",
|
|
)
|
|
return tool
|
|
|
|
|
|
def _get_task_manager(context: Any) -> TaskManager | None:
|
|
manager = getattr(context, "task_manager", None)
|
|
if isinstance(manager, TaskManager):
|
|
return manager
|
|
return None
|
|
|
|
|
|
def _coerce_agent_definitions_result(
|
|
value: Any,
|
|
registry: AgentDefinitionRegistry,
|
|
) -> AgentDefinitionsResult:
|
|
if isinstance(value, AgentDefinitionsResult):
|
|
return value
|
|
allowed = getattr(value, "allowed_agent_types", None) if value is not None else None
|
|
return registry.result(allowed_agent_types=allowed)
|
|
|
|
|
|
def _resolve_agent_model(
|
|
model_arg: str | None,
|
|
agent_model: str | None,
|
|
context: Any,
|
|
) -> str:
|
|
parent_model = str(getattr(context, "model", None) or "")
|
|
for candidate in (model_arg, agent_model, parent_model):
|
|
if not candidate or str(candidate) == "inherit":
|
|
continue
|
|
return _resolve_model_alias(str(candidate), parent_model)
|
|
return parent_model or "unknown"
|
|
|
|
|
|
def _resolve_model_alias(model: str, parent_model: str) -> str:
|
|
alias = model.strip().lower()
|
|
if alias not in {"sonnet", "opus", "haiku"}:
|
|
return model
|
|
env_name = f"OPENSPACE_AGENT_MODEL_{alias.upper()}"
|
|
configured = os.environ.get(env_name)
|
|
if configured and configured.strip():
|
|
return configured.strip()
|
|
return parent_model or model
|
|
|
|
|
|
def _clone_llm_client(llm_client: Any, model: str) -> Any:
|
|
if llm_client is None:
|
|
return None
|
|
try:
|
|
clone = copy.copy(llm_client)
|
|
clone.model = model
|
|
return clone
|
|
except Exception:
|
|
return llm_client
|
|
|
|
|
|
def _mcp_servers_from_context_tools(context: Any) -> list[str]:
|
|
servers: set[str] = set()
|
|
for tool in getattr(context, "all_tools", None) or getattr(context, "tools", []):
|
|
name = getattr(tool, "name", "")
|
|
if name.startswith("mcp__"):
|
|
parts = name.split("__")
|
|
if len(parts) >= 3 and parts[1]:
|
|
servers.add(parts[1])
|
|
return sorted(servers)
|
|
|
|
|
|
def _agent_result_to_dict(result: AgentToolResult | Mapping[str, Any]) -> dict[str, Any]:
|
|
if isinstance(result, AgentToolResult):
|
|
return {
|
|
"status": result.status,
|
|
"agent_id": result.agent_id,
|
|
"agent_type": result.agent_type,
|
|
"content": result.content,
|
|
"total_tool_use_count": result.total_tool_use_count,
|
|
"total_duration_ms": result.total_duration_ms,
|
|
"total_tokens": result.total_tokens,
|
|
"usage": result.usage,
|
|
"prompt": result.prompt,
|
|
}
|
|
return dict(result)
|
|
|
|
|
|
def _format_teammate_spawn_result(data: Mapping[str, Any]) -> str:
|
|
return (
|
|
"Teammate spawned successfully.\n"
|
|
f"name: {data.get('name')}\n"
|
|
f"team_name: {data.get('team_name')}\n"
|
|
f"agent_id: {data.get('agent_id')}\n"
|
|
f"task_id: {data.get('task_id')}\n"
|
|
"Use SendMessage(to_agent/name/task_id, message) to send instructions, "
|
|
"TaskGet(task_id=...) to read output, TaskList to inspect tasks, and "
|
|
"TaskStop(task_id=...) to stop it."
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"AgentTool",
|
|
"build_agent_tool",
|
|
"run_agent",
|
|
]
|