search_node (the actively-run Python backend, see
langgraph-agent-convention) reads the incoming
tool call at search.py:66 (ai_message.tool_calls[0]["args"]["queries"]) and the model's
extraction response at search.py:130 (ai_message_response.tool_calls[0]["args"]["resources"])
with no guard that a tool call exists. The extraction call uses
tool_choice="ExtractResources" (search.py:109), but forced tool choice is not honored
identically across the four providers this app offers (openai / anthropic / google_genai / grok
— see model-selection-flow); a safety refusal, an
empty-result completion, or a provider that returns text instead throws IndexError and the
whole graph run aborts with no user-facing result.
Compiles and runs fine on the happy path, so no gate catches it — this is exactly the silent
correctness gap the search flow (research-chat-flow)
depends on. chat_node already handles the no-tool-call case (falls through to goto="__end__",
chat.py:146); search_node should be similarly defensive. Mirror the fix in the TS port
(agent-typescript-parity).
Resolution (2026-07-23, plan search-guard)
Both indexing sites are now guarded in both ports; an empty/missing tool_calls response no
longer crashes the node — it appends a user-facing log entry (the app's established
state["logs"] convention, matching chat_node) and returns early.
The dangling-tool_call resolution in Guard 2 is load-bearing: the first Python attempt returned
without appending that ToolMessage, leaving the original Search tool_call unresolved — the
next chat_node model turn would 400 ("tool_calls must be followed by tool result messages").
prism caught it, it was fixed and re-validated PASS; the TS port applied the resolving
ToolMessage from the start and also passed prism validation.
Re-verified 2026-07-30: both guards are present in current source (search.py:67, :139).
This row's citations were re-pointed at the guards in the same pass — they previously named
the pre-fix indexing lines (:66, :130), which the edit shifted onto a blank line and a
bracket. verify-citations skips status: fixed rows, so that rot was invisible to the gate;
citing the fix rather than the removed defect keeps a tombstone checkable.