mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(quickjs): ensure top-level title on subagent response schemas (#4155)
## Problem
Agent-generated `response_schema` values passed through `task({
responseSchema })` in the QuickJS REPL bridge often omit a top-level
`title`. Structured-output backends that treat a JSON schema as a
function (e.g. the OpenAI function-calling path) require a top-level
`title` to use as the function name, otherwise they raise:
> To use a JSON schema as a function, it must have a top-level 'title'
key.
This surfaced as a `ValueError` from `js_eval` when a subagent was
dispatched with a schema like `{'type': 'object', 'properties': {'word':
{'type': 'string'}}, 'required': ['word']}`.
## Fix
In `langchain_quickjs/_subagent.py`, inject a default `title`
(`"subagent_response"`) into the response schema when one is missing or
blank, after validation and before the schema is wrapped in
`AutoStrategy`. This is the single point where every agent-generated
`response_schema` enters the pipeline.
- Schemas with an existing non-empty string `title` are returned
untouched.
- Blank / non-string titles (`""`, `" "`, `0`, `None`) are replaced.
- The caller's dict is never mutated (a copy is returned).
## Tests
Added unit tests in `tests/unit_tests/test_repl_middleware.py` covering
default injection, preservation of existing titles, replacement of
blank/invalid titles, and non-mutation of the input.
Full suite: 75 passed, ruff clean.
This commit is contained in:
@@ -70,6 +70,7 @@ async def call_subagent_task_tool(
|
||||
parse_json_output = response_schema is not None
|
||||
if response_schema is not None:
|
||||
_validate_response_schema(response_schema)
|
||||
response_schema = _ensure_schema_title(response_schema)
|
||||
runtime = _runtime_with_response_format(runtime, response_schema)
|
||||
|
||||
runtime = _runtime_with_tool_call_id(
|
||||
@@ -121,6 +122,23 @@ def _validate_response_schema(schema: dict[str, Any]) -> None:
|
||||
_check(schema, 0, [0])
|
||||
|
||||
|
||||
_DEFAULT_SCHEMA_TITLE = "subagent_response"
|
||||
|
||||
|
||||
def _ensure_schema_title(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Ensure the response schema carries a non-empty top-level ``title``.
|
||||
|
||||
Structured output backends that treat a JSON schema as a function (for
|
||||
example, the OpenAI function-calling path) require a top-level ``title`` to
|
||||
use as the function name. Agent-generated ``response_schema`` values often
|
||||
omit it, so inject a default when it is missing or blank.
|
||||
"""
|
||||
existing = schema.get("title")
|
||||
if isinstance(existing, str) and existing.strip():
|
||||
return schema
|
||||
return {**schema, "title": _DEFAULT_SCHEMA_TITLE}
|
||||
|
||||
|
||||
def _runtime_with_response_format(
|
||||
runtime: Any,
|
||||
response_schema: dict[str, Any],
|
||||
|
||||
@@ -28,7 +28,10 @@ from quickjs_rs import Runtime, ThreadWorker
|
||||
from langchain_quickjs import CodeInterpreterMiddleware
|
||||
from langchain_quickjs._format import format_outcome
|
||||
from langchain_quickjs._repl import _clear_exception_references, _Registry, _ThreadREPL
|
||||
from langchain_quickjs._subagent import _runtime_with_response_format
|
||||
from langchain_quickjs._subagent import (
|
||||
_ensure_schema_title,
|
||||
_runtime_with_response_format,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.callbacks import CallbackManagerForLLMRun
|
||||
@@ -888,6 +891,44 @@ def test_runtime_with_response_format_uses_configurable() -> None:
|
||||
assert strategy.schema == schema
|
||||
|
||||
|
||||
def test_ensure_schema_title_injects_default_when_missing() -> None:
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {"word": {"type": "string"}},
|
||||
"required": ["word"],
|
||||
}
|
||||
|
||||
updated = _ensure_schema_title(schema)
|
||||
|
||||
assert updated["title"] == "subagent_response"
|
||||
# Original schema is not mutated.
|
||||
assert "title" not in schema
|
||||
# Other keys are preserved unchanged.
|
||||
assert updated["properties"] == schema["properties"]
|
||||
assert updated["required"] == schema["required"]
|
||||
|
||||
|
||||
def test_ensure_schema_title_preserves_existing_non_empty_title() -> None:
|
||||
schema = {"title": "MyWord", "type": "object"}
|
||||
|
||||
updated = _ensure_schema_title(schema)
|
||||
|
||||
assert updated is schema
|
||||
assert updated["title"] == "MyWord"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"title",
|
||||
["", " ", 0, None],
|
||||
)
|
||||
def test_ensure_schema_title_replaces_blank_or_invalid_title(title: Any) -> None:
|
||||
schema = {"title": title, "type": "object"}
|
||||
|
||||
updated = _ensure_schema_title(schema)
|
||||
|
||||
assert updated["title"] == "subagent_response"
|
||||
|
||||
|
||||
class _StructuredSubagentModel(GenericFakeChatModel):
|
||||
"""Fake subagent model that calls the currently bound response-format tool."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user