[pull] main from danny-avila:main - #155
Merged
Merged
Conversation
…ll_id attribution) (#366) * 🛡️ fix: Synthesize Tool Results for Invalid Tool Calls A streamed tool call whose accumulated args never collapse into a JSON object is filed by @langchain/core under invalid_tool_calls and never enters tool_calls, yet its tool_use block still rides the AI message content the provider receives. ToolNode previously skipped these calls entirely, leaving a tool_use with no tool_result; the next model call was then rejected (Anthropic 400 INVALID_TOOL_RESULTS). Fatal on HITL resume, where the paused AI message is replayed from the checkpoint, and equally reachable in ordinary multi-tool turns. ToolNode.run() now synthesizes an error ToolMessage for every id-bearing invalid_tool_calls entry (same already-answered and server-tool filters as tool_calls), appended adjacent to the real tool results in all non-Send exits, and resolves the streamed tool-call card best-effort after the batch settles so interrupt/resume flows emit the completion exactly once. * 🎯 feat: Surface tool_call_id on ask_user_question Interrupts askUserQuestion() accepts an optional toolCallId (tool bodies read it from config.toolCall.id, which LangChain stamps when a tool is invoked with a ToolCall) and surfaces it as tool_call_id on the interrupt payload. Hosts can then attribute the question, and later the answer, to the exact tool-call content part instead of guessing by position, which mislabels cards when a model emits several ask calls in one turn. Optional and additive: existing callers and payload consumers are unaffected. * 🛡️ fix: Route Invalid-Only Turns and Promote Answered Invalid Calls Codex round 1 on the invalid_tool_calls synthesis: 1. toolsCondition returned END when tool_calls was empty, so a turn whose ONLY calls were malformed never entered ToolNode — the dangling tool_use was committed with no result. Route id-bearing invalid-only turns to the tool node (restricted to the zero-valid-calls case so externally-invoked batches are not re-executed). 2. Provider converters that rebuild the call side of the wire from tool_calls (OpenAI Completions tool_calls, OpenAI Responses function_call items, Gemini/Bedrock function-call parts) drop invalid calls, which would leave the synthesized result referencing a call the provider never sees — the inverted rejection. ToolNode now also emits a REPLACEMENT copy of the AI message (messagesStateReducer upserts by id) with the answered invalid calls promoted into well-formed tool_calls entries (args {}), keeping the call and result sides agreeing for every provider at one seam. Skipped when the message carries no id (the reducer would append). Tests: graph-level invalid-only routing through the real agent→toolsCondition→toolNode cycle; promotion assertions on the direct batch; no-promotion-without-id; and a round-trip through the real _convertMessagesToOpenAIResponsesParams asserting function_call / function_call_output pairing for the promoted shape. * 🧼 fix: Sanitize Promoted tool_use Content Blocks CI (live anthropic summarization) surfaced the remaining provider surface: Anthropic formats array-content AI messages from the blocks verbatim, and a call whose streamed input_json never parsed leaves the block's input as the raw accumulated STRING. The promoted replacement message fixed tool_calls but replayed that block as-is, so the API rejected the follow-up with 'tool_use.input: Input should be an object' (reachable exactly when a live model emits a malformed call — the invalid-only continuation this branch newly enables). sanitizeInvalidToolUseBlocks normalizes the promoted calls' tool_use blocks to input {} (mirroring the promoted args); valid siblings and non-tool_use blocks pass through untouched, string content is returned as-is. * 🛡️ fix: Server-Mix Routing, Array-Input Scope, Invalid-Call Traces Codex round 2 on the invalid_tool_calls handling: 1. toolsCondition now routes when every valid call is provider-server- executed (srvtoolu_) and attributable invalid calls exist — covers the Anthropic server-call + malformed-client-call mix, where handleAnthropicSearchResults (the only invokedToolIds writer) marks the server call invoked and both prior branches declined. ToolNode's batch filter excludes srvtoolu_ calls, so nothing re-executes; a valid non-server call still declines conservatively. 2. Invalid-call handling (synthesized results + the replacement AI message) is scoped to the messages-state input form: a BaseMessage[] caller receives a plain output list it appends to its own history, where the replacement would duplicate the assistant turn. Array callers keep the untouched status quo. 3. langfuseTraceShaping counts id-bearing invalid_tool_calls in the tool-dispatch span input, so an invalid-only dispatch no longer falls back to the full serialized graph state and a mixed dispatch no longer omits the malformed call. * 🧩 fix: Align Routing, Gating, and Traces on One Attribution Predicate Codex round 3 consistency gaps between the routing condition, run()'s gating, and the trace shaper: 1. toolsCondition's invalid-call branch now mirrors ToolNode's own gating exactly — array-state graphs and id-less messages decline routing, since the node would no-op there and bounce the dangle back to the model. 2. Synthesized results and the replacement AI message are all-or- nothing: when the replacement cannot upsert (array input, id-less message), the results are suppressed too — emitting them alone would strand an output whose call the provider converters never reconstruct. 3. langfuseTraceShaping applies the same attribution predicate as execution (id-bearing, non-empty, non-server), so the dispatch span never claims calls the node deliberately skips. 4. Test-side dynamic import of the Responses converter replaced with a static import (repo rule). * 🧹 fix: Static Test Import + Nameless Invalid Calls in Traces Codex round 4: 1. The tool_call_id round-trip test imports askUserQuestion statically (repo rule; the file's pre-existing dynamic imports are untouched — out of this PR's scope). 2. The trace shaper applies ToolNode's 'unknown' name fallback to nameless attributable invalid calls, so a call that still gets a synthesized result also counts in the dispatch span input. * 🔗 fix: Promotion Consistency for Names, Traces, and Handoffs Codex round 5: 1. sanitizeInvalidToolUseBlocks also normalizes a promoted block's missing/empty name with the same 'unknown' fallback the promoted tool_calls entry uses — the Anthropic formatter treats a raw block as represented by its id, so a nameless block would fail provider validation on its own. 2. The trace shaper mirrors canPromoteInvalidCalls fully: invalid calls count only for messages-state span inputs (a bare-array state means ToolNode skipped invalid handling) with an id-bearing assistant message. 3. Handoff Commands carry the promotion: a handoff tool snapshots update.messages from the PRE-promotion state with a filtered same-id AI message copy, and commands apply after sibling reducer updates — the stale copy overwrote the replacement and a Send child's state omitted the synthesized results. patchCommandUpdateForPromotedInvalidCalls rewrites the same-id copy (sanitized content, promoted entries, leftover invalid_tool_calls; the update's own tool_calls narrowing preserved) and appends the missing results. * 🧪 test: Graph-Level Handoff Regression for Malformed Siblings Drives the REAL multi-agent handoff machinery (edges → transfer tools → Command/Send fan-out) with a scripted model whose sibling call streams non-object args, covering both the single-handoff same-id overwrite and the parallel Send state omission. validateToolHistory asserts the call/result pairing on EVERY model invocation including the children's; explicit assertions cover the promoted tool_calls and the synthesized result in final state. Both tests fail with the Command patching disabled and without the promotion. * 🏷️ fix: Normalize Empty Invalid-Call Names at Every Surface Codex round 6: the nullish name fallback preserved '' — an attributable invalid call with an empty-string name produced a nameless promoted tool_calls entry, a nameless sanitized block, and a nameless synthesized result, which providers reject before consuming the pairing. One shared normalizeInvalidCallName ('' treated like undefined → 'unknown') now backs the synthesized result, the promoted entry, the block sanitizer, the handoff patch, and the trace shaper. Test extended with the empty-name variant across all surfaces. * 🚿 fix: Strip Transfer Blocks on Reception + Answered Calls from Traces Codex round 7: 1. processHandoffReception kept the retained AI message's content verbatim while stripping transfer calls and results — with the promoted invalid sibling now holding the message in state, an Anthropic child replayed the stripped transfer's tool_use block (and a parallel sibling's block, whose result never reaches this recipient) as unmatched calls. filterTransferToolUseBlocks drops transfer blocks by gathered id AND by transfer name alongside the tool-call filtering; non-transfer blocks and string content pass through. 2. The trace shaper mirrors ToolNode's !toolMessageIds.has(id) filter: invalid calls already answered by a ToolMessage in the serialized state no longer count as pending work in the tool-dispatch span. * 🧭 fix: Round-Trip invalid_tool_calls in Sessions + Invariant Map Proactive sweep along the review's recurring defect class (parallel tool-call representations drifting): audited every AI-message copy-construction site and call-representation consumer in src/. One live gap found: the durable-session serializer kept tool_calls but dropped invalid_tool_calls while the serialized content (with any raw malformed tool_use blocks) survives — a restore stranded those blocks without the entries ToolNode repairs the pairing from. Now round-trips them; test restores a serialized message and drives it through ToolNode. Also records the invariant map at the attribution seam: the predicate and the list of surfaces that must stay in agreement (gating, routing, sanitizer, handoff patch, reception, trace shaping, session serialization), so the next surface added extends the list instead of rediscovering it in review.
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )