bb9bed82e1
ModelRole collapses to "primary": every role (main, tool selector, memory
agents, subagents, summarizer) resolves to the snapshot's frozen primary
model, per design 6.1/8.3 — users typically configure a single usable LLM,
so compile-time auxiliary bindings were bypassing run snapshots and
mis-attributing usage. Legacy auxiliary keys in stored snapshots, registry
JSON, and thread metadata are tolerated on read and dropped.
BREAKING CHANGE: ThreadModelSelection no longer carries an auxiliary ref;
snapshot selection_hash is computed over {primary, reasoning_effort} only;
ConfigurableModelMiddleware(role="auxiliary") is rejected.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
150 lines
6.4 KiB
Python
150 lines
6.4 KiB
Python
"""Factory for building deployable sub-agent graphs from yaml definitions.
|
|
|
|
Lives in ``EvoScientist/subagents/`` next to the canonical yaml entries
|
|
because the factory is "build a graph from a sub-agent name" — a generic
|
|
construction utility, not a deployment concern. Any deployment surface
|
|
(``EvoScientist/langgraph_dev/``, future ``langgraph_platform/``, custom
|
|
servers) can call ``build_async_subagent_graph(name)`` to materialize the
|
|
runnable graph.
|
|
|
|
Reuses the main EvoScientist agent's backend and middleware so the deployed
|
|
sub-agent has full capability parity with its in-process synchronous
|
|
counterpart: same workspace files, same ``/skills/`` and ``/memories/``
|
|
routes, same error-handling and context-overflow middleware. The chat model
|
|
is resolved from the active model registry's default primary and re-resolved
|
|
per run from the run snapshot by ``ConfigurableModelMiddleware``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Any
|
|
|
|
|
|
def build_async_subagent_graph(name: str) -> Any:
|
|
"""Build a deployable graph for the ``name`` sub-agent defined in yaml.
|
|
|
|
Args:
|
|
name: The sub-agent's key in one of the ``EvoScientist/subagents/*.yaml``
|
|
files (e.g. ``"writing-agent"``).
|
|
|
|
Returns:
|
|
A compiled ``langgraph`` graph ready for registration in ``langgraph.json``.
|
|
|
|
Raises:
|
|
ValueError: If ``name`` is not defined under ``EvoScientist/subagents/``.
|
|
"""
|
|
# Lazy imports — the factory is invoked at langgraph dev startup time, so
|
|
# all heavy modules (deepagents, llm, MCP) are pulled in here rather than
|
|
# at package import.
|
|
from deepagents import create_deep_agent
|
|
|
|
from EvoScientist.config import apply_config_to_env, get_effective_config
|
|
from EvoScientist.EvoScientist import (
|
|
SUBAGENTS_CONFIG,
|
|
_ensure_general_purpose_subagent,
|
|
_get_default_backend,
|
|
_get_default_middleware,
|
|
_inject_subagent_middleware,
|
|
)
|
|
from EvoScientist.model_registry.runtime import get_snapshot_runtime
|
|
from EvoScientist.tools import skill_manager, tavily_search, think_tool
|
|
from EvoScientist.utils import load_subagents
|
|
|
|
# Surface API keys as env vars so downstream SDKs (openai, anthropic, …)
|
|
# find them on subprocess invocations from langgraph dev.
|
|
cfg = get_effective_config()
|
|
apply_config_to_env(cfg)
|
|
|
|
# Mirror the tool registry constructed in EvoScientist._build_base_kwargs.
|
|
tool_registry = {"think_tool": think_tool, "skill_manager": skill_manager}
|
|
if os.environ.get("TAVILY_API_KEY"):
|
|
tool_registry["tavily_search"] = tavily_search
|
|
|
|
# Use the official loader so resolved tools, prompt_refs, and skills are
|
|
# all wired the same way as the in-process sync version.
|
|
specs = load_subagents(
|
|
SUBAGENTS_CONFIG,
|
|
tool_registry=tool_registry,
|
|
)
|
|
spec = next((s for s in specs if s.get("name") == name), None)
|
|
if spec is None:
|
|
raise ValueError(
|
|
f"Sub-agent {name!r} not found in {SUBAGENTS_CONFIG}. "
|
|
f"Available: {[s.get('name') for s in specs]}"
|
|
)
|
|
|
|
# Load MCP tools routed to THIS agent via ``expose_to: <name>`` in
|
|
# ``mcp.yaml``. Use the cached helper so multiple ``build_async_subagent_graph``
|
|
# calls in the same langgraph dev subprocess (one per registered async graph)
|
|
# share a single MCP connection set per server instead of re-spawning.
|
|
from EvoScientist.EvoScientist import _load_mcp_tools_cached
|
|
|
|
mcp_tools_by_agent = _load_mcp_tools_cached()
|
|
agent_mcp_tools = mcp_tools_by_agent.get(name, [])
|
|
|
|
# NOTE on HITL: async sub-agents intentionally do NOT set ``interrupt_on``,
|
|
# even though the deployed main agent does. They run as standalone graphs
|
|
# on the langgraph dev subprocess; the parent (CLI main agent) only sees a
|
|
# ``task_id`` from ``start_async_task`` and has no UI path to surface a
|
|
# paused-on-interrupt child to the user. Setting ``interrupt_on`` here
|
|
# would hang the sub-agent on its first ``execute`` call with no one to
|
|
# approve. The user-visible HITL boundary is the parent's
|
|
# ``start_async_task`` decision; restrict the child's reach by limiting
|
|
# ``tools`` in ``subagents/<name>.yaml`` instead.
|
|
#
|
|
# ``for_async_subagent=True`` propagates the same reasoning to the
|
|
# middleware list — specifically, it suppresses ``AskUserMiddleware``,
|
|
# which uses ``interrupt()`` for the same purpose (waiting on a user
|
|
# reply) and would deadlock an async sub-agent for the same reason.
|
|
#
|
|
# Memory middleware is included so async sub-agents get the same profile
|
|
# context and `/memories/profile/...` file guidance as the main agent.
|
|
#
|
|
# The compile-time model binding is resolved from the active registry's
|
|
# default primary — never from config.yaml free strings. Per-run calls
|
|
# are re-resolved from the run snapshot by ConfigurableModelMiddleware.
|
|
#
|
|
# Bootstrap registry: no default exists yet, so no compile-time model
|
|
# can be built. The graph must still materialize — one failing factory
|
|
# must not take down the whole langgraph dev service — so we bind a
|
|
# placeholder that raises MODEL_REGISTRY_NOT_READY on the first model
|
|
# call. Every run fails with that clear structured error until a
|
|
# primary model is configured and enabled; no 32K/implicit fallback.
|
|
from EvoScientist.model_registry.errors import (
|
|
MODEL_REGISTRY_NOT_READY,
|
|
ModelRegistryError,
|
|
)
|
|
from EvoScientist.model_registry.placeholder import RegistryNotReadyChatModel
|
|
|
|
runtime = get_snapshot_runtime()
|
|
snapshot_role = "primary"
|
|
try:
|
|
model = runtime.build_default_role_model(snapshot_role)
|
|
except ModelRegistryError as exc:
|
|
if exc.code != MODEL_REGISTRY_NOT_READY:
|
|
raise
|
|
model = RegistryNotReadyChatModel(detail=str(exc))
|
|
|
|
subagents = []
|
|
_ensure_general_purpose_subagent(subagents)
|
|
_inject_subagent_middleware(subagents, chat_model=model)
|
|
|
|
middleware = _get_default_middleware(
|
|
for_async_subagent=True,
|
|
memory_source_agent=name,
|
|
chat_model=model,
|
|
snapshot_role=snapshot_role,
|
|
)
|
|
|
|
return create_deep_agent(
|
|
name=name,
|
|
model=model,
|
|
system_prompt=spec.get("system_prompt", ""),
|
|
tools=spec.get("tools", []) + agent_mcp_tools,
|
|
skills=spec.get("skills"),
|
|
backend=_get_default_backend(),
|
|
middleware=middleware,
|
|
subagents=subagents,
|
|
).with_config({"recursion_limit": cfg.recursion_limit})
|