Summary
LangChainIntegration._llmobs_set_meta_tags_from_tool stores the tool invocation config dict directly in span metadata without sanitization:
if tool_inputs.get("config"):
metadata["tool_config"] = tool_inputs.get("config")
When the tool is executed by a LangGraph agent, this config is a RunnableConfig containing non-JSON-serializable objects, for example functools.partial (under configurable.__pregel_send) and AsyncCallbackManager (under callbacks). In agentless mode the JSON trace encoder then raises on flush and the entire trace payload is dropped:
TypeError: Object of type AsyncCallbackManager is not JSON serializable
File ".../ddtrace/internal/encoding.py", line 211, in put
encoded_trace = _json_dumps_bytes({"spans": [...]})
Depending on the graph wiring, the failing object can also be functools.partial. Every trace containing at least one LangGraph tool execution is lost, which makes LLM Observability silently blind for agent workloads in agentless setups.
Reproduction
ddtrace 4.10.1, Python 3.12, langgraph + langchain-core current releases
LLMObs.enable(agentless_enabled=True, ...) with the LangChain integration active
- Build any LangGraph
StateGraph with a ToolNode and invoke it so a tool runs
- On span flush, the writer raises the
TypeError above and drops the trace
Expected behavior
tool_config should go through the same safe_json sanitization that the integration already applies to tool arguments and I/O values in the neighbouring code paths.
Suggested fix
if tool_inputs.get("config"):
metadata["tool_config"] = json.loads(safe_json(tool_inputs.get("config")))
Workaround
Wrapping _llmobs_set_meta_tags_from_tool to sanitize tool_inputs["config"] via safe_json before delegating to the original restores trace delivery.
Summary
LangChainIntegration._llmobs_set_meta_tags_from_toolstores the tool invocation config dict directly in span metadata without sanitization:When the tool is executed by a LangGraph agent, this config is a
RunnableConfigcontaining non-JSON-serializable objects, for examplefunctools.partial(underconfigurable.__pregel_send) andAsyncCallbackManager(undercallbacks). In agentless mode the JSON trace encoder then raises on flush and the entire trace payload is dropped:Depending on the graph wiring, the failing object can also be
functools.partial. Every trace containing at least one LangGraph tool execution is lost, which makes LLM Observability silently blind for agent workloads in agentless setups.Reproduction
ddtrace4.10.1, Python 3.12,langgraph+langchain-corecurrent releasesLLMObs.enable(agentless_enabled=True, ...)with the LangChain integration activeStateGraphwith aToolNodeand invoke it so a tool runsTypeErrorabove and drops the traceExpected behavior
tool_configshould go through the samesafe_jsonsanitization that the integration already applies to tool arguments and I/O values in the neighbouring code paths.Suggested fix
Workaround
Wrapping
_llmobs_set_meta_tags_from_toolto sanitizetool_inputs["config"]viasafe_jsonbefore delegating to the original restores trace delivery.