From e3a7d8534cbe180a39003846377c507ac0652f34 Mon Sep 17 00:00:00 2001 From: abdelmoumen_mezhoud Date: Tue, 22 Sep 2026 14:54:41 +0400 Subject: [PATCH] fix(adapters/langgraph): honour the declared output format, not its shape A single string output is treated as the agent's free text and skips structured generation. That heuristic cannot tell two different things apart, because both produce the identical outputs list: text agent outputs: [{title: "output", type: "string"}] JSON, one string field outputs: [{title: "sentiment", type: "string"}] A caller that declares one JSON field therefore gets prose, with no tool registered and nothing in the request telling the model a schema was wanted. Adding a second field changes nothing about the model or the prompt, yet works, which is what makes the failure so hard to place. Gate on the caller's declared format when there is one. `metadata` carries `outputFormat` (`TEXT` or `JSON`); when it is absent the previous shape heuristic still applies, so callers that declare no format keep today's behaviour. Weak models stay protected: a single string output still satisfies is_single_string_output, so extract_outputs_from_invoke_result continues to fall back to the final message when structured generation returns nothing. The worst case is the prose that is returned today, never an empty output. Refs OA-1681 --- .../adapters/langgraph/_langgraphconverter.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py b/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py index 4524e094c..e468b654b 100644 --- a/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py +++ b/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py @@ -1458,12 +1458,25 @@ def _create_react_agent_with_given_info( output_model: Optional[type[BaseModel]] = None state_schema: Optional[Any] = None - # Build response (output) model (used for response_format). A single - # string output is taken from the agent's final message (see - # extract_outputs_from_invoke_result), so it needs no structured - # generation — mirrors LlmNodeExecutor and lets a string output work on - # models without structured-output support. - if outputs and not is_single_string_output(outputs): + # Build response (output) model (used for response_format). + # + # Gate on the DECLARED output format, not on the shape of `outputs`. + # AgentFabric gives every agent an output port, so a plain text agent and + # a JSON agent with one string field produce an identical `outputs` list + # and cannot be told apart by shape. Asking "did the caller declare JSON?" + # is the only reliable signal; the previous `is_single_string_output` + # heuristic silently answered in prose for one-field JSON schemas. + # + # A caller that declares no format keeps the old shape heuristic, so a + # plain pyagentspec user still gets structured output for multi-field + # outputs and free text for a lone string. + declared_format = str((agent.metadata or {}).get("outputFormat", "")).upper() + wants_structured = ( + declared_format == "JSON" + if declared_format in ("JSON", "TEXT") + else not is_single_string_output(outputs) + ) + if outputs and wants_structured: output_model = create_pydantic_model_from_properties("AgentOutputModel", outputs) if inputs: