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:
Hunter Lovell
2026-06-23 04:26:35 -07:00
committed by GitHub
parent f63a7b56f9
commit 08f917eea7
2 changed files with 60 additions and 1 deletions
@@ -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."""