Description
In the LangGraph adapter, tools whose inputs are object-typed properties do not receive plain dictionaries: they receive instances of the pydantic models that the adapter generates from the JSON schema. Those instances then leak into the flow state, the final flow outputs and client-tool interrupt payloads.
Agent Spec tools are written against JSON values, and every other runtime (Wayflow, AutoGen, Agent Framework) passes dictionaries, so the same tool_registry callable behaves differently on LangGraph.
Reproduction
from pyagentspec.adapters.langgraph import AgentSpecLoader
from pyagentspec.flows.edges import ControlFlowEdge, DataFlowEdge
from pyagentspec.flows.flow import Flow
from pyagentspec.flows.nodes import EndNode, StartNode, ToolNode
from pyagentspec.property import Property
from pyagentspec.tools import ServerTool
request = Property(json_schema={
"title": "request", "type": "object",
"properties": {"customer_id": {"type": "string"}, "priority": {"type": "string", "default": "normal"}},
"required": ["customer_id"], "additionalProperties": False,
})
echo = ServerTool(name="echo", description="Echoes", inputs=[request], outputs=[request])
start = StartNode(name="start", inputs=[request])
node = ToolNode(name="echo_node", tool=echo)
end = EndNode(name="end", outputs=[request])
flow = Flow(
name="f", start_node=start, nodes=[start, node, end],
control_flow_connections=[ControlFlowEdge(name="a", from_node=start, to_node=node),
ControlFlowEdge(name="b", from_node=node, to_node=end)],
data_flow_connections=[
DataFlowEdge(name="i", source_node=start, source_output="request", destination_node=node, destination_input="request"),
DataFlowEdge(name="o", source_node=node, source_output="request", destination_node=end, destination_input="request"),
],
)
def echo_tool(request):
print(type(request)) # <class 'pydantic...request'>, not dict
return request
graph = AgentSpecLoader(tool_registry={"echo": echo_tool}).load_component(flow)
print(graph.invoke({"inputs": {"request": {"customer_id": "C-1"}}})["outputs"])
# {'request': request(customer_id='C-1', priority=None)} <- pydantic instance in the flow outputs
Wayflow (wayflowcore 26.3.0) passes a dict to the tool and returns a dict.
The same happens for RemoteTool (the templated request body renders the model's repr, e.g. customer_id='C-1', instead of the JSON value) and for ClientTool (the interrupt payload contains model instances).
Expected behavior
Tools receive plain JSON values (dictionaries, lists, scalars) for object-typed inputs, and flow outputs / interrupt payloads only contain JSON values.
Root cause
The adapter builds a pydantic args_schema for each tool from the Agent Spec input properties. LangChain's BaseTool._parse_input validates the call with that model and passes getattr(result, field) to the callable, so nested object fields arrive as model instances. Nothing converts them back before the callable runs or before values are written to the flow state.
Environment
- pyagentspec
main (26.4.0.dev0), langgraph 1.2.4, langchain-core 1.4.9, Python 3.12
Description
In the LangGraph adapter, tools whose inputs are object-typed properties do not receive plain dictionaries: they receive instances of the pydantic models that the adapter generates from the JSON schema. Those instances then leak into the flow state, the final flow outputs and client-tool interrupt payloads.
Agent Spec tools are written against JSON values, and every other runtime (Wayflow, AutoGen, Agent Framework) passes dictionaries, so the same
tool_registrycallable behaves differently on LangGraph.Reproduction
Wayflow (
wayflowcore26.3.0) passes adictto the tool and returns adict.The same happens for
RemoteTool(the templated request body renders the model'srepr, e.g.customer_id='C-1', instead of the JSON value) and forClientTool(the interrupt payload contains model instances).Expected behavior
Tools receive plain JSON values (dictionaries, lists, scalars) for object-typed inputs, and flow outputs / interrupt payloads only contain JSON values.
Root cause
The adapter builds a pydantic
args_schemafor each tool from the Agent Spec input properties. LangChain'sBaseTool._parse_inputvalidates the call with that model and passesgetattr(result, field)to the callable, so nested object fields arrive as model instances. Nothing converts them back before the callable runs or before values are written to the flow state.Environment
main(26.4.0.dev0), langgraph 1.2.4, langchain-core 1.4.9, Python 3.12