fix(langchain): record tool span input as structured data - #905
Closed
GeneralistDev wants to merge 1 commit into
Closed
fix(langchain): record tool span input as structured data#905GeneralistDev wants to merge 1 commit into
GeneralistDev wants to merge 1 commit into
Conversation
LangChain's BaseTool.run/arun call on_tool_start with the tool input twice: positionally as str(tool_input), and as the original dict under the `inputs` keyword. The handler stored the positional value, so dict inputs landed in $ai_input_state as a Python repr with single quotes, which no JSON parser can read. Prefer the `inputs` dict when LangChain supplies one, matching on_chain_start. Tools invoked with a plain string keep recording that string. Generated-By: PostHog Desktop Task-Id: 75cf9ec9-fd9e-49fa-af56-ac505bc09cbe
Contributor
posthog-python Compliance ReportDate: 2026-08-31 13:06:42 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
LangChain's
BaseTool.run/arunhand the tool input toon_tool_starttwice: positionally asstr(tool_input), and as the original dict under theinputskeyword (langchain_core/tools/base.pylines 838/846 and 949/957).CallbackHandler.on_tool_startstored the positional one, so a dict tool input landed in$ai_input_stateas a Python repr:Single quotes,
Trueinstead oftrue— no JSON parser reads that. In ClickHouseJSONExtract*returns empty against theinput_statecolumn, so anything consuming tool spans has to fall back to substring matching on the raw text, and any analysis that assumed valid JSON silently returns zero rows rather than failing loudly.on_chain_startalready stores the realinputsdict, so chain spans were fine and only tool spans were affected — which made the gap easy to miss.This prefers the structured
inputsdict when LangChain supplies one. Tools invoked with a plain string have noinputsand keep recording the string unchanged.💚 How did you test it?
Two unit tests driving
on_tool_start/on_tool_endthe wayBaseTool.arundoes — one asserting a dict input is recorded as a dict, one asserting a string input still records the string.uv run --extra test python -m pytest posthog/test/ai/langchain/test_callbacks.py— 77 passed, 7 failed. The same 7 fail on an unmodifiedmainin this environment (they needOPENAI_API_KEY/ANTHROPIC_API_KEYand network), so no regressions from this change.📝 Checklist
If releasing new changes
sampo addto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Found while investigating whether MCP and Max agents lean on raw SQL where a typed query runner would do. Measuring that needs the query text off the tool span, and every
JSONExtractagainstinput_statecame back empty for Max'sexecute_sqlspans while the MCP path's spans parsed fine — which is what led back to this handler.Root-causing went one layer at a time: ruled out the server-side truncation hook in the main repo (it only caps strings that are already strings, and uses
json.dumpson its one stringifying path), then the SDK'scaptureplumbing (passes properties through untouched), before landing on the positional-vs-keyword argument inon_tool_start. Thestr()itself happens inlangchain-coreand can't be changed here; taking theinputskwarg it already passes alongside is the fix on our side.Considered normalizing the value at query time instead and rejected it — that leaves every existing and future consumer parsing Python repr, and the correct value is already being handed to us.
Note this changes the shape of
$ai_input_statefor tool spans from a string to an object. That matches chain spans and is the intended fix, but anyone who built around the repr string will see the type change.Created with PostHog from a Slack thread