Skip to content

[pull] main from danny-avila:main - #155

Merged
pull[bot] merged 1 commit into
innFactory:mainfrom
danny-avila:main
Jul 31, 2026
Merged

[pull] main from danny-avila:main#155
pull[bot] merged 1 commit into
innFactory:mainfrom
danny-avila:main

Conversation

@pull

@pull pull Bot commented Jul 31, 2026

Copy link
Copy Markdown

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 : )

…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.
@pull pull Bot locked and limited conversation to collaborators Jul 31, 2026
@pull pull Bot added the ⤵️ pull label Jul 31, 2026
@pull
pull Bot merged commit 361d2e1 into innFactory:main Jul 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant