01. What Structured Outputs Are
Structured outputs constrain a language model to follow a provided schema. In LangChain, tools achieve this. Tools are callable functions with well-defined inputs and outputs. They get passed to a chat model. The model decides when to invoke a tool based on the conversation. It also decides what argument values to provide. But those arguments must match a strict schema. Type hints are required because they define the input schema. For more complex inputs, you can use Pydantic models. This solves a core problem. Downstream systems need type-safe data, not free-form text. Without structured outputs, the model might produce invalid or inconsistent arguments. With a schema, the output is reliable and predictable. The trade-off is that you must define the schema in advance. But this upfront work prevents errors later. The model understands exactly what fields are needed. So your application gets validated data every time. Tools also extend what agents can do. They fetch real-time data, execute code, or query external databases. But for each action, the model needs to supply the right inputs. Structured outputs ensure those inputs are exactly what the tool expects. That keeps your system running smoothly.
<!-- mem:begin -->Generate it: Structured outputs constrain a language model to follow a provided ______, so its arguments can't be free-form text. (cue: the contract the output must match; answer: schema)
Generate it: In LangChain, the thing you pass to a chat model so the model decides when to invoke it and what arguments to provide is a ____. (cue: a callable function with well-defined inputs and outputs; answer: tool)
Ask yourself: Downstream systems need type-safe data, not free-form text — so what would go wrong if the model returned arguments that didn't match the schema?
<!-- mem:end -->Recall check (try before reading the answer):
What do downstream systems need, and what happens without structured outputs? — __________________________________________________________________________ Answer: Downstream systems need type-safe data, not free-form text; without structured outputs, the model might produce invalid or inconsistent arguments.
Why are type hints required? — __________________________________________________________________________ Answer: Type hints are required because they define the input schema.
A tool with type hints defines the input schema for structured outputs.
from langchain.tools import tool
@tool
def search_database(query: str, limit: int = 10) -> str:
"""Search the customer database for records matching the query.
Args:
query: Search terms to look for
limit: Maximum number of results to return
"""
return f"Found {limit} results for '{query}'"
Imagine ordering at a restaurant where the waiter hands you a menu with set categories—appetizer, main, dessert—and each dish has a fixed list of ingredients. You can pick any dish, but you must fill in exactly the required fields (like “no onions” or “medium rare”). That’s what structured outputs do for a language model in LangChain. Instead of letting the model blurt out free-form text, tools act like that menu: they are callable functions with well-defined inputs and outputs. The model decides when to call a tool based on the conversation, but the arguments must match a strict schema. For instance, type hints are required because they define the input schema, and you can use Pydantic models for more complex inputs. This ensures downstream systems receive type-safe data, not messy prose. Without this constraint, the model might return “temperature 75” or “today’s weather is warm” – a downstream weather app would break trying to parse that. A beginner would feel the frustration of a waiter bringing the wrong dish because the order was vague, leading to confusion and extra work fixing what should have been straightforward.
The subsystem ensures the language model's output conforms to a strict schema by channeling all structured output through tools. Execution begins when the model decides to invoke a tool based on conversation context: it reads the tool’s name, description, and args_schema (derived from the function’s type hints or a Pydantic model like WeatherInput). The model must supply argument values that match that schema; type hints are required because they define the input schema. The tool then executes (e.g., search_database(query, limit)) and returns a ToolMessage. Optionally, if a tool is decorated with @tool(return_direct=True), the agent stops immediately and returns the tool’s output as the final response, bypassing any further LLM call. On error, middleware such as wrap_tool_call can intercept the ToolCallRequest to retry or return a custom error message.
The invariant the design preserves is that every tool invocation receives arguments that exactly match its input schema. The schema is enforced by LangChain’s conversion of type hints or Pydantic models into a JSON Schema that the model sees. This guarantees that downstream systems never receive free-form text where structured data is expected. The model cannot supply out-of‑schema types without causing a validation failure, which the agent’s middleware or the tool’s own logic would catch and report.
The key trade‑off is constraining the model’s creativity in exchange for type safety for downstream consumers. The obvious alternative is to let the model output raw text and parse it after the fact, which would require fragile regex or ad‑hoc heuristics. That alternative is rejected because it introduces a write-boundary problem: once free-form text enters a database or API, schema violations are hard to detect and fix. By forcing the model to follow the tool’s args_schema at the point of generation, the subsystem avoids the cost of downstream parsing errors, mis‑typed fields, and the resulting debugging effort.
A concrete failure mode occurs when the model tries to call search_database with a limit argument that is a string like "five" instead of an integer. The schema defined by the type hint limit: int = 10 rejects this at the point the tool is invoked. The operator would see a ToolMessage whose content is a validation error (e.g., "Input validation error: limit should be int") logged by the middleware, or the wrap_tool_call handler could surface a custom error. The signal is unambiguous: the tool node returns an error payload instead of a successful result, and the agent either retries or halts depending on the middleware configuration.
1. Missing Type Hints on Tool Arguments
- Trigger – A tool is defined with the
@tooldecorator but one or more parameters lack type hints. The source states: “Type hints are required as they define the tool's input schema.” - Guard – No explicit guard identified in the source. The requirement is stated but no validation or exception handler is shown.
- Posture – Fail-hard – The tool creation would likely raise a
TypeErroror schema-building failure, aborting the tool definition at import time. - Operator signal – A
TypeErrorsuch as"Argument 'query' has no type annotation"or a schema validation error when the tool is registered with the model. - Recovery – Manual fix: add a type hint to every parameter. No automatic retry.
2. Reserved Parameter Name Used (runtime or config)
- Trigger – A tool is defined with an argument named
configorruntimethat is not the injectedToolRuntimeorRunnableConfiginstance. The source warns: “Using these names will cause runtime errors.” - Guard – No guard identified in the source; it only documents the restriction.
- Posture – Fail-hard – The tool call will raise a runtime error (likely a
ValueErrorabout a reserved name or a schema conflict) and the agent run aborts. - Operator signal – A
ValueErrormessage such as"Parameter name 'runtime' is reserved"or an error traceback in the agent logs. - Recovery – Manual rename of the conflicting parameter. No automatic retry.
3. Attempting to Access runtime.execution_info on an Unsupported Version
- Trigger – Code calls
runtime.execution_infowhendeepagentsis below version 0.5.0 (orlanggraphbelow 1.1.5). The source notes the requirement in a<Note>: “Requiresdeepagents>=0.5.0(orlanggraph>=1.1.5).” - Guard – No guard is shown; the feature simply does not exist in older versions.
- Posture – Fail-hard – An
AttributeErroris raised becauseruntimedoes not have anexecution_infoattribute. - Operator signal –
AttributeError: 'ToolRuntime' object has no attribute 'execution_info'in the logs. - Recovery – Upgrade
deepagentsorlanggraphto the required version. No automatic retry.
4. Using server_info When Not Running on LangGraph Server
- Trigger – A tool calls
runtime.server_info.assistant_idor similar without checking forNone. The source documents: “server_infoisNonewhen the tool is not running on LangGraph Server (e.g., during local development or testing).” - Guard – The example shows the guard
if server is not None:before accessing attributes. - Posture – Fail-hard if the guard is omitted – an
AttributeErroronNoneobject would occur. Fail-soft if the guard is used – the code gracefully falls through without error. - Operator signal – Without guard:
AttributeError: 'NoneType' object has no attribute 'assistant_id'. With guard: silent absence (no log line, no metric). - Recovery – Without guard: manual fix to add a
Nonecheck. With guard: the tool proceeds normally, returning a fallback behavior (e.g., skipping server-dependent logic).
5. Pydantic Schema Validation Failure Inside a Tool
- Trigger – A tool’s input schema is defined with a Pydantic model (e.g., via
args_schemaor@toolwith a Pydantic field), and the model passes an argument that violates the schema (e.g., wrong type, missing required field). The source says “For more complex inputs, you can use Pydantic models.” - Guard – Pydantic’s built-in
ValidationErroris the guard (raised automatically by the model when parsing the tool call). - Posture – Fail-hard – The tool call is rejected before the function body runs; the agent run typically aborts or retries with a different model output (depending on the runtime).
- Operator signal – A
pydantic.ValidationErrorlog trace or a message with the error description (e.g.,"Field required"or"Input should be a valid integer"). - Recovery – The model may attempt to reformulate the tool call (some runtimes automatically retry), or the error is surfaced to the user for manual correction. The source does not specify a retry count or backoff.
6. Accidental Creation of a HeadlessTool Without Local Execution
- Trigger – A tool is instantiated in Python by calling
tool(...)with onlyname,description, andargs_schema(no function body). The source states: “If you calltool(...)in Python with onlyname,description, andargs_schema, LangChain returns aHeadlessTool. There is no.implement()API on the Python side.” - Guard – No guard is shown; the tool is silently returned as a
HeadlessTool. - Posture – Fail-soft (but confusing) – The tool exists and can be passed to a model, but when the model calls it, the run interrupts instead of executing locally. The system does not crash, but the expected local execution does not happen.
- Operator signal – An interrupt event (detectable via the
useStreamhook or aHeadlessToolinterrupt identifier). No error is raised, but the agent does not complete autonomously. - Recovery – The graph must be resumed manually or through a frontend that implements the tool. No automatic retry; the operator must inspect the payload and provide the result.