feat(sdk): support configurable subagent response format (#3882)

Adds SDK support for passing per-dispatch subagent response formats
through `RunnableConfig.configurable` instead of runtime context. This
gives bridge callers a stable way to request schema-backed task-tool
dispatch for raw `SubAgent` specs while keeping compiled/runnable-backed
subagents fixed at construction time.

The task tool now reads a private configurable response-format key,
compiles temporary raw `SubAgent` variants with that response format,
and rejects dynamic schemas for compiled subagents. The raw-spec
compilation path stays centralized in `create_sub_agent`, and tests
cover the dynamic response-format path, compiled-subagent rejection,
name/config propagation, and custom state-schema compilation.
This commit is contained in:
Hunter Lovell
2026-06-11 13:40:08 -07:00
committed by GitHub
parent 52bcc5f01f
commit b0e4d7aa8d
5 changed files with 373 additions and 129 deletions
+5 -1
View File
@@ -6,7 +6,11 @@ from deepagents.middleware.async_subagents import AsyncSubAgent, AsyncSubAgentMi
from deepagents.middleware.filesystem import FilesystemMiddleware, FilesystemPermission
from deepagents.middleware.memory import MemoryMiddleware
from deepagents.middleware.rubric import RubricMiddleware
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
from deepagents.middleware.subagents import (
CompiledSubAgent,
SubAgent,
SubAgentMiddleware,
)
from deepagents.profiles.harness.harness_profiles import (
GeneralPurposeSubagentProfile,
HarnessProfile,
@@ -64,7 +64,11 @@ from deepagents.middleware.rubric import (
RubricState,
)
from deepagents.middleware.skills import SkillsMiddleware
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
from deepagents.middleware.subagents import (
CompiledSubAgent,
SubAgent,
SubAgentMiddleware,
)
from deepagents.middleware.summarization import (
SummarizationMiddleware,
SummarizationToolMiddleware,
@@ -8,7 +8,13 @@ from typing import Any, NotRequired, TypedDict, cast
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig
from langchain.agents.middleware.types import AgentMiddleware, ContextT, ModelRequest, ModelResponse, ResponseT
from langchain.agents.middleware.types import (
AgentMiddleware,
ContextT,
ModelRequest,
ModelResponse,
ResponseT,
)
from langchain.agents.structured_output import ResponseFormat
from langchain.tools import BaseTool, ToolRuntime
from langchain_core.language_models import BaseChatModel
@@ -23,6 +29,9 @@ from deepagents.backends.protocol import BackendFactory, BackendProtocol
from deepagents.middleware._utils import append_to_system_message
from deepagents.middleware.filesystem import FilesystemPermission
SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY = "__deepagents_subagent_response_format"
"""Configurable key used by task-tool callers to request dynamic response format."""
class SubAgent(TypedDict):
"""Specification for an agent.
@@ -451,18 +460,20 @@ def create_sub_agent(
spec: SubAgent,
*,
state_schema: type | None = None,
response_format: ResponseFormat[Any] | type | dict[str, Any] | None = None,
) -> Runnable:
"""Create a runnable agent from a declarative `SubAgent` spec.
"""Create a runnable agent from a raw `SubAgent` spec.
This is the shared entrypoint for the `create_agent` path used by
declarative subagent specs. Pre-compiled `CompiledSubAgent` runnables are
already created by the caller and are handled separately by
`SubAgentMiddleware`.
raw subagent specs. Pre-compiled `CompiledSubAgent` runnables are already
created by the caller and are handled separately by `SubAgentMiddleware`.
Args:
spec: Subagent spec to compile. Must specify `model` and `tools`.
state_schema: Base graph state schema forwarded to `create_agent` for
the subagent.
response_format: Optional response format override for this compiled
subagent instance.
Returns:
Runnable agent ready for task-tool invocation.
@@ -486,12 +497,13 @@ def create_sub_agent(
if interrupt_on:
middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
selected_response_format = response_format if response_format is not None else spec.get("response_format")
create_agent_kwargs: dict[str, Any] = {
"system_prompt": spec["system_prompt"],
"tools": spec["tools"],
"middleware": middleware,
"name": spec["name"],
"response_format": spec.get("response_format"),
"response_format": selected_response_format,
}
if state_schema is not None:
create_agent_kwargs["state_schema"] = state_schema
@@ -499,28 +511,83 @@ def create_sub_agent(
return create_agent(model, **create_agent_kwargs)
def _get_subagent_response_format(
runtime: ToolRuntime,
) -> ResponseFormat[Any] | type | dict[str, Any] | None:
"""Return the response format carried in this task tool call's config."""
config = runtime.config
configurable = config.get("configurable") if isinstance(config, dict) else None
if not isinstance(configurable, dict):
return None
value = configurable.get(SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY)
if value is None:
return None
return value
def _build_task_tool( # noqa: C901, PLR0915
subagents: list[CompiledSubAgent],
subagents: Sequence[SubAgent | CompiledSubAgent],
task_description: str | None = None,
*,
private_state_keys: frozenset[str] = frozenset(),
state_schema: type | None = None,
) -> BaseTool:
"""Create a task tool from pre-built subagent graphs.
"""Create a task tool from subagent specs.
Args:
subagents: List of subagent specs containing name, description, and runnable.
subagents: List of raw or compiled subagent specs.
task_description: Custom description for the task tool. If `None`,
uses default template. Supports `{available_agents}` placeholder.
private_state_keys: State keys marked with `PrivateStateAttr` that
should be stripped from parent state before invoking subagents.
state_schema: Base graph state schema forwarded to raw subagent specs.
Returns:
A StructuredTool that can invoke subagents by type.
"""
# Build the graphs dict and descriptions from the unified spec list
subagent_graphs: dict[str, Runnable] = {spec["name"]: spec["runnable"] for spec in subagents}
subagent_description_str = "\n".join(f"- {s['name']}: {s['description']}" for s in subagents)
def _compile_spec(
spec: SubAgent | CompiledSubAgent,
*,
response_format: ResponseFormat[Any] | type | dict[str, Any] | None = None,
) -> CompiledSubAgent:
"""Compile one raw spec or configure one provided runnable."""
if "runnable" in spec:
if response_format is not None:
msg = f'response_schema cannot be used with compiled subagent "{spec["name"]}"; dynamic schemas require a raw SubAgent spec.'
raise ValueError(msg)
# Use with_config (not attribute mutation) so the original runnable is
# untouched and a shared instance can be registered under multiple names.
compiled = cast("CompiledSubAgent", spec)
runnable = compiled["runnable"].with_config(
{
"metadata": {"lc_agent_name": spec["name"]},
"run_name": spec["name"],
}
)
return {
"name": spec["name"],
"description": spec["description"],
"runnable": runnable,
}
return {
"name": spec["name"],
"description": spec["description"],
"runnable": create_sub_agent(
spec,
state_schema=state_schema,
response_format=response_format,
),
}
compiled_subagents = [_compile_spec(spec) for spec in subagents]
subagents_by_name = {spec["name"]: spec for spec in subagents}
# Build the graphs dict and descriptions from the unified spec list
subagent_graphs: dict[str, Runnable] = {spec["name"]: spec["runnable"] for spec in compiled_subagents}
subagent_description_str = "\n".join(f"- {s['name']}: {s['description']}" for s in compiled_subagents)
# Use custom description if provided, otherwise use default template
if task_description is None:
@@ -570,9 +637,28 @@ def _build_task_tool( # noqa: C901, PLR0915
}
)
def _validate_and_prepare_state(subagent_type: str, description: str, runtime: ToolRuntime) -> tuple[Runnable, dict]:
def _select_subagent(
subagent_type: str,
runtime: ToolRuntime,
) -> Runnable:
"""Return the runnable to use for this task invocation."""
response_format = _get_subagent_response_format(runtime)
if response_format is not None:
new_spec = _compile_spec(
subagents_by_name[subagent_type],
response_format=response_format,
)
return new_spec["runnable"]
return subagent_graphs[subagent_type]
def _validate_and_prepare_state(
subagent_type: str,
description: str,
runtime: ToolRuntime,
) -> tuple[Runnable, dict]:
"""Prepare state for invocation."""
subagent = subagent_graphs[subagent_type]
subagent = _select_subagent(subagent_type, runtime)
# Create a new state dict to avoid mutating the original
subagent_state = {k: v for k, v in runtime.state.items() if k not in _EXCLUDED_STATE_KEYS}
subagent_state = {k: v for k, v in subagent_state.items() if k not in private_state_keys}
@@ -590,7 +676,11 @@ def _build_task_tool( # noqa: C901, PLR0915
if not runtime.tool_call_id:
value_error_msg = "Tool call ID is required for subagent invocation"
raise ValueError(value_error_msg)
subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
subagent, subagent_state = _validate_and_prepare_state(
subagent_type,
description,
runtime,
)
# The parent's callbacks, tags and configurable reach the subagent
# automatically: langgraph's `ensure_config` seeds each run from the
# ambient parent config and (as of langgraph#7926) merges it per-key, so
@@ -614,7 +704,11 @@ def _build_task_tool( # noqa: C901, PLR0915
if not runtime.tool_call_id:
value_error_msg = "Tool call ID is required for subagent invocation"
raise ValueError(value_error_msg)
subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
subagent, subagent_state = _validate_and_prepare_state(
subagent_type,
description,
runtime,
)
# The parent's callbacks, tags and configurable reach the subagent
# automatically: langgraph's `ensure_config` seeds each run from the
# ambient parent config and (as of langgraph#7926) merges it per-key, so
@@ -662,8 +756,8 @@ class SubAgentMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
system_prompt: Instructions appended to main agent's system prompt
about how to use the task tool.
task_description: Custom description for the task tool.
state_schema: Base graph state schema forwarded to declarative
`SubAgent` specs when their runnables are compiled.
state_schema: Base graph state schema forwarded to raw `SubAgent`
specs when their runnables are compiled.
Leave unset to use `create_agent`'s default. `CompiledSubAgent`
entries are unaffected — callers own those runnables' schemas.
@@ -715,21 +809,20 @@ class SubAgentMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
self._private_state_keys = private_state_keys or frozenset()
self._task_description = task_description
self._state_schema = state_schema
subagent_specs = self._get_subagents()
self._subagent_specs = subagent_specs
self.subagent_names: frozenset[str] = frozenset(spec["name"] for spec in subagent_specs)
self.subagent_names: frozenset[str] = frozenset(spec["name"] for spec in subagents)
"""Declared subagent names. Public so streamers can discover them
without introspecting the `task` tool's closure."""
task_tool = _build_task_tool(
subagent_specs,
self._subagents,
task_description,
private_state_keys=self._private_state_keys,
state_schema=self._state_schema,
)
# Build system prompt with available agents
if system_prompt and subagent_specs:
agents_desc = "\n".join(f"- {s['name']}: {s['description']}" for s in subagent_specs)
if system_prompt and subagents:
agents_desc = "\n".join(f"- {s['name']}: {s['description']}" for s in subagents)
self.system_prompt = system_prompt + "\n\nAvailable subagent types:\n\n" + agents_desc
else:
self.system_prompt = system_prompt
@@ -745,44 +838,13 @@ class SubAgentMiddleware(AgentMiddleware[Any, ContextT, ResponseT]):
def private_state_keys(self, value: frozenset[str]) -> None:
self._private_state_keys = value
task_tool = _build_task_tool(
self._subagent_specs,
self._subagents,
task_description=self._task_description,
private_state_keys=value,
state_schema=self._state_schema,
)
self.tools = [task_tool]
def _get_subagents(self) -> list[CompiledSubAgent]:
"""Create runnable agents from specs.
Returns:
List of subagent specs with name, description, and runnable.
"""
specs: list[CompiledSubAgent] = []
for spec in self._subagents:
if "runnable" in spec:
# Use with_config (not attribute mutation) so the original runnable is
# untouched and a shared instance can be registered under multiple names.
compiled = cast("CompiledSubAgent", spec)
runnable = compiled["runnable"].with_config(
{
"metadata": {"lc_agent_name": compiled["name"]},
"run_name": compiled["name"],
}
)
specs.append({"name": compiled["name"], "description": compiled["description"], "runnable": runnable})
continue
specs.append(
{
"name": spec["name"],
"description": spec["description"],
"runnable": create_sub_agent(spec, state_schema=self._state_schema),
}
)
return specs
def wrap_model_call(
self,
request: ModelRequest[ContextT],
@@ -1,20 +1,29 @@
"""Unit tests for SubAgentMiddleware initialization and configuration."""
from typing import get_type_hints
import json
from typing import Any, get_type_hints
import pytest
from langchain.agents import create_agent
from langchain_core.messages import AIMessage
from langchain.agents.structured_output import AutoStrategy
from langchain.tools import ToolRuntime
from langchain_core.callbacks import CallbackManagerForLLMRun
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage
from langchain_core.outputs import ChatResult
from langchain_core.runnables import RunnableLambda
from langchain_core.tools import tool
from langgraph.graph import START, MessagesState, StateGraph
from deepagents.backends.state import StateBackend
from deepagents.middleware.subagents import (
GENERAL_PURPOSE_SUBAGENT,
SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY,
TASK_SYSTEM_PROMPT,
SubAgentMiddleware,
_build_task_tool,
create_sub_agent,
)
from tests.unit_tests.chat_model import GenericFakeChatModel
@tool
@@ -23,6 +32,47 @@ def get_weather(city: str) -> str:
return f"The weather in {city} is sunny."
class _DynamicStructuredOutputModel(GenericFakeChatModel):
"""Fake model that calls whichever structured-output tool is bound."""
def _generate(
self,
messages: list[BaseMessage],
stop: list[str] | None = None,
run_manager: CallbackManagerForLLMRun | None = None,
**kwargs: Any,
) -> ChatResult:
tool_name = getattr(self.tools[-1], "name", None)
if not isinstance(tool_name, str):
msg = "Expected a structured-output tool to be bound"
raise TypeError(msg)
self.messages = iter(
[
AIMessage(
content="",
tool_calls=[
{
"name": tool_name,
"args": {
"name": "Maya Thornton",
"age": 29,
"city": "Portland",
},
"id": "call_payload",
"type": "tool_call",
}
],
)
]
)
return super()._generate(
messages,
stop=stop,
run_manager=run_manager,
**kwargs,
)
class TestSubagentMiddlewareInit:
"""Tests for SubAgentMiddleware initialization that don't require LLM invocation."""
@@ -57,50 +107,128 @@ class TestSubagentMiddlewareInit:
assert "state_schema" in create_hints
assert "return" in create_hints
def test_create_sub_agent_compiles_declarative_spec(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""The public helper should own the declarative create_agent path."""
graph = self._make_echo_graph()
calls: dict[str, object] = {}
def test_create_sub_agent_compiles_declarative_spec(self) -> None:
"""The public helper should compile and invoke declarative specs."""
class CustomState(MessagesState):
pass
def fake_resolve_model(model: object) -> object:
calls["model_spec"] = model
return "resolved-model"
def fake_create_agent(model: object, **kwargs: object) -> object:
calls["model"] = model
calls["kwargs"] = kwargs
return graph
monkeypatch.setattr("deepagents._models.resolve_model", fake_resolve_model)
monkeypatch.setattr("deepagents.middleware.subagents.create_agent", fake_create_agent)
model = GenericFakeChatModel(messages=iter([AIMessage(content="done")]))
runnable = create_sub_agent(
{
"name": "worker",
"description": "Does work.",
"system_prompt": "Work on the task.",
"model": "test-model",
"model": model,
"tools": [get_weather],
"interrupt_on": {"get_weather": True},
},
state_schema=CustomState,
)
assert runnable is graph
assert calls["model_spec"] == "test-model"
assert calls["model"] == "resolved-model"
kwargs = calls["kwargs"]
assert isinstance(kwargs, dict)
assert kwargs["system_prompt"] == "Work on the task."
assert kwargs["tools"] == [get_weather]
assert kwargs["name"] == "worker"
assert kwargs["state_schema"] is CustomState
middleware = kwargs["middleware"]
assert isinstance(middleware, list)
assert middleware[-1].__class__.__name__ == "HumanInTheLoopMiddleware"
assert "HumanInTheLoopMiddleware.after_model" in runnable.nodes
result = runnable.invoke({"messages": [HumanMessage(content="Do work.")]})
assert result["messages"][-1].content == "done"
assert result["messages"][-1].name == "worker"
assert [getattr(tool, "name", None) for tool in model.tools] == ["get_weather"]
def test_task_tool_compiles_dynamic_response_format_for_declarative_subagent(self) -> None:
"""Dynamic schemas are present when declarative subagent variants compile."""
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"city": {"type": "string"},
},
"required": ["name", "age", "city"],
}
response_format = AutoStrategy(schema)
middleware = SubAgentMiddleware(
backend=StateBackend(),
subagents=[
{
"name": "worker",
"description": "Does work.",
"system_prompt": "Return structured data.",
"model": _DynamicStructuredOutputModel(messages=iter(())),
"tools": [],
}
],
system_prompt=None,
)
task_tool = middleware.tools[0]
runtime = ToolRuntime(
state={},
context={},
config={
"configurable": {
SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY: response_format,
}
},
stream_writer=lambda _chunk: None,
tools=[task_tool],
tool_call_id="call_worker",
store=None,
)
result = task_tool.func(
description="Make a person.",
subagent_type="worker",
runtime=runtime,
)
assert json.loads(result.update["messages"][0].content) == {
"name": "Maya Thornton",
"age": 29,
"city": "Portland",
}
def test_task_tool_rejects_response_format_for_compiled_subagent(self) -> None:
"""Dynamic schemas require a declarative spec to compile a variant."""
schema = {
"type": "object",
"properties": {"ok": {"type": "boolean"}},
"required": ["ok"],
}
response_format = AutoStrategy(schema)
runnable = RunnableLambda(lambda _state, _config: (_ for _ in ()).throw(AssertionError("compiled runnable should not be invoked")))
task_tool = _build_task_tool(
[
{
"name": "worker",
"description": "Does work.",
"runnable": runnable,
}
]
)
runtime = ToolRuntime(
state={},
context={},
config={
"configurable": {
SUBAGENT_RESPONSE_FORMAT_CONFIG_KEY: response_format,
}
},
stream_writer=lambda _chunk: None,
tools=[task_tool],
tool_call_id="call_worker",
store=None,
)
with pytest.raises(
ValueError,
match='response_schema cannot be used with compiled subagent "worker"',
):
task_tool.func(
description="Do work.",
subagent_type="worker",
runtime=runtime,
)
def test_subagent_middleware_with_custom_subagent(self) -> None:
"""Test SubAgentMiddleware initialization with a custom subagent."""
@@ -191,33 +319,58 @@ class TestSubagentMiddlewareInit:
builder.add_edge(START, "echo")
return builder.compile()
def _task_runtime(self, task_tool: object, tool_call_id: str) -> ToolRuntime:
return ToolRuntime(
state={},
context={},
config={"configurable": {}},
stream_writer=lambda _chunk: None,
tools=[task_tool],
tool_call_id=tool_call_id,
store=None,
)
def test_compiled_subagent_name_propagated_via_config(self) -> None:
"""CompiledSubAgent.name is forwarded into metadata.lc_agent_name and run_name."""
graph = self._make_echo_graph()
configs: list[dict[str, object]] = []
middleware = SubAgentMiddleware(
class _Runnable:
def with_config(self, config: dict[str, object]) -> "_Runnable":
configs.append(config)
return self
def invoke(
self,
state: dict[str, object],
config: object = None,
) -> dict[str, object]:
del state, config
return {"messages": [AIMessage(content="hello")]}
SubAgentMiddleware(
backend=StateBackend(),
subagents=[
{
"name": "my-subagent",
"description": "A custom subagent",
"runnable": graph,
"runnable": _Runnable(),
}
],
)
specs = middleware._get_subagents()
runnable = specs[0]["runnable"]
assert runnable.config is not None
assert runnable.config.get("metadata", {}).get("lc_agent_name") == "my-subagent"
assert runnable.config.get("run_name") == "my-subagent"
assert configs == [
{
"metadata": {"lc_agent_name": "my-subagent"},
"run_name": "my-subagent",
}
]
def test_compiled_subagent_does_not_mutate_original_runnable(self) -> None:
"""_get_subagents must not mutate the original runnable passed by the caller."""
"""Task-tool setup must not mutate the original runnable."""
graph = self._make_echo_graph()
original_config = getattr(graph, "config", None)
middleware = SubAgentMiddleware(
SubAgentMiddleware(
backend=StateBackend(),
subagents=[
{
@@ -228,13 +381,30 @@ class TestSubagentMiddlewareInit:
],
)
middleware._get_subagents()
assert graph.config == original_config, "Original runnable was mutated by _get_subagents(); use with_config instead of attribute assignment"
assert graph.config == original_config, "Original runnable was mutated; use with_config instead of attribute assignment"
def test_same_runnable_reused_across_multiple_subagents(self) -> None:
"""Same runnable registered under two different names must not cross-contaminate configs."""
graph = self._make_echo_graph()
class _Runnable:
def __init__(self, config: dict[str, object] | None = None) -> None:
self.config = config
def with_config(self, config: dict[str, object]) -> "_Runnable":
return _Runnable(config)
def invoke(
self,
state: dict[str, object],
config: object = None,
) -> dict[str, object]:
del state, config
if self.config is None:
msg = "Expected configured runnable clone"
raise AssertionError(msg)
return {"messages": [AIMessage(content=str(self.config["run_name"]))]}
graph = _Runnable()
middleware = SubAgentMiddleware(
backend=StateBackend(),
@@ -252,17 +422,20 @@ class TestSubagentMiddlewareInit:
],
)
specs = middleware._get_subagents()
assert len(specs) == 2
task_tool = middleware.tools[0]
alpha = task_tool.func(
description="Do alpha work.",
subagent_type="agent-alpha",
runtime=self._task_runtime(task_tool, "call_alpha"),
)
beta = task_tool.func(
description="Do beta work.",
subagent_type="agent-beta",
runtime=self._task_runtime(task_tool, "call_beta"),
)
alpha_runnable = specs[0]["runnable"]
beta_runnable = specs[1]["runnable"]
assert alpha_runnable.config.get("metadata", {}).get("lc_agent_name") == "agent-alpha"
assert beta_runnable.config.get("metadata", {}).get("lc_agent_name") == "agent-beta"
assert alpha_runnable.config.get("run_name") == "agent-alpha"
assert beta_runnable.config.get("run_name") == "agent-beta"
assert alpha_runnable is not beta_runnable
assert alpha.update["messages"][0].content == "agent-alpha"
assert beta.update["messages"][0].content == "agent-beta"
assert graph.config is None
def test_middleware_delegates_to_create_sub_agent(self, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -273,7 +446,13 @@ class TestSubagentMiddlewareInit:
class CustomState(MessagesState):
pass
def fake_create_sub_agent(spec: object, *, state_schema: type | None = None) -> object:
def fake_create_sub_agent(
spec: object,
*,
state_schema: type | None = None,
response_format: object = None,
) -> object:
del response_format
calls.append((spec, state_schema))
return graph
+9 -14
View File
@@ -16,7 +16,6 @@ from langchain_core.tools import BaseTool, StructuredTool
from deepagents._api.deprecation import LangChainDeprecationWarning
from deepagents._tools import _apply_tool_description_overrides, _tool_name
from deepagents._version import __version__
from deepagents.backends import StateBackend
from deepagents.graph import (
_REQUIRED_MIDDLEWARE_CLASSES,
_REQUIRED_MIDDLEWARE_NAMES,
@@ -28,7 +27,7 @@ from deepagents.graph import (
from deepagents.middleware._tool_exclusion import _ToolExclusionMiddleware
from deepagents.middleware.async_subagents import AsyncSubAgentMiddleware
from deepagents.middleware.filesystem import FilesystemMiddleware
from deepagents.middleware.subagents import SubAgentMiddleware
from deepagents.middleware.subagents import SubAgentMiddleware, create_sub_agent
from deepagents.middleware.summarization import _DeepAgentsSummarizationMiddleware
from deepagents.profiles import GeneralPurposeSubagentProfile, HarnessProfile, register_harness_profile
from deepagents.profiles.harness.harness_profiles import (
@@ -768,21 +767,17 @@ class TestStateSchema:
class MyState(DeepAgentState):
page_url: str
middleware = SubAgentMiddleware(
backend=StateBackend(),
subagents=[
{
"name": "researcher",
"description": "Research agent",
"system_prompt": "You are a researcher.",
"model": GenericFakeChatModel(messages=iter([AIMessage(content="ok")])),
"tools": [],
}
],
runnable = create_sub_agent(
{
"name": "researcher",
"description": "Research agent",
"system_prompt": "You are a researcher.",
"model": GenericFakeChatModel(messages=iter([AIMessage(content="ok")])),
"tools": [],
},
state_schema=MyState,
)
runnable = middleware._get_subagents()[0]["runnable"]
properties = runnable.get_input_jsonschema()["properties"]
assert "page_url" in properties