Repository navigation
Conversation
Through the server gateway, thread state messages arrive as dicts, and convert_to_messages moves their invalid_tool_calls into additional_kwargs. _unanswered_calls then missed a malformed call on the last AI message, so closing a parked HITL turn left it without a result while the verify step still passed. Read the invalid calls from the dict when the converted message has none. Part of EvoScientist#509.
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Find unanswered tool calls EvoScientist/backends.py, tests/test_hitl_loop.py, tests/test_parked_close.py |
The scan reads invalid tool calls from additional_kwargs alongside valid calls. Tests cover abandoned invalid calls and parked-state closure with valid, ungated, and malformed calls. |
Priority:
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix
Suggested reviewers: jfilipiuk, din0s
Merge Risk: 
Minimal · up to 496f9
-
No concrete merge-blocking risk remains for this change.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed -
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
din0s
left a comment
There was a problem hiding this comment.
Hi @Thanatos9404,
Thank you for the PR. I just have one comment below:
convert_to_messages already keeps a dict's invalid_tool_calls in additional_kwargs, so read them from the converted message instead of zipping the raw messages back in.
X-iZhang
left a comment
There was a problem hiding this comment.
Thanks for the fix, @Thanatos9404. One suggestion on the new close test, inline at tests/test_hitl_loop.py.
| async def test_close_parked_checkpoint_answers_invalid_calls_in_dict_state(): | ||
| """A server-backed close also answers the malformed call (#509).""" | ||
| from EvoScientist.backends import close_parked_checkpoint | ||
| from EvoScientist.gateway import GraphTarget | ||
|
|
||
| agent = FakeCheckpointAgent( | ||
| values={ | ||
| "messages": [ | ||
| { | ||
| "type": "ai", | ||
| "content": "", | ||
| "tool_calls": [ | ||
| { | ||
| "name": "execute", | ||
| "args": {"command": "echo hi"}, | ||
| "id": "exec-1", | ||
| "type": "tool_call", | ||
| } | ||
| ], | ||
| "invalid_tool_calls": [ | ||
| { | ||
| "name": "write_file", | ||
| "args": "{not json", | ||
| "id": "bad-1", | ||
| "error": "parse", | ||
| "type": "invalid_tool_call", | ||
| } | ||
| ], | ||
| } | ||
| ] | ||
| } | ||
| ) | ||
|
|
||
| await close_parked_checkpoint( | ||
| FakeGraphGateway(checkpoint=agent), | ||
| GraphTarget(local_graph=None), | ||
| "t-dicts", | ||
| reason="round_limit", | ||
| ) | ||
|
|
||
| patch = next(values for values, node in agent.updates if node == "tools")[ | ||
| "messages" | ||
| ] | ||
| assert [message.tool_call_id for message in patch] == ["exec-1", "bad-1"] | ||
| assert "was not executed" in patch[1].content |
There was a problem hiding this comment.
This test runs on FakeGraphGateway, so it never goes through LangGraphServerGateway.get_state_snapshot / _thread_state_as_snapshot, the patch is written back as message objects rather than the dicts the server stores, and the thread is parked at tools rather than before tools as an approval close is. tests/test_parked_close.py already drives the close through the real LangGraphServerGateway on dict state, parked before tools (_ParkedServerThreads, _parked_server_state(), _server_gateway()), and holds the other invalid-call close tests.
Suggestion: drop this test and add the case there, next to test_the_close_through_the_server_gateway_classifies_like_the_local_one:
async def test_the_close_through_the_server_gateway_answers_an_invalid_call():
"""A malformed call in the server's dict state is closed too (#509)."""
state = _parked_server_state()
state["values"]["messages"][-1]["invalid_tool_calls"] = [_invalid("bad-1")]
threads = _ParkedServerThreads(threads=[], states={"t-srv": state})
await close_parked_checkpoint(
_server_gateway(threads), GraphTarget(), "t-srv", reason="rejected"
)
assert _results(_texts(threads.state_updates[1][1]["messages"])) == {
"c1": ("asked", "rejected", True),
"w1": ("not executed", "rejected", False),
"bad-1": ("not executed", "rejected", False),
}
assert threads.states["t-srv"]["next"] == []There was a problem hiding this comment.
Makes sense, moved it in 496f9f7: the FakeGraphGateway test is gone and your test now sits in test_parked_close.py right after the classifies-like-the-local-one case. It fails with main's backends.py and passes on this branch.
Move the dict-state close test to test_parked_close.py so it runs through LangGraphServerGateway on the server's dict state, parked before tools.
Description
Part of #509 (the first half).
Through the server gateway, thread state messages arrive as dicts.
_unanswered_callsconverts them withconvert_to_messages, which keepstool_callsbut movesinvalid_tool_callsintoadditional_kwargs. So when a parked HITL turn was closed, a malformed call on the last AI message got no result, and the close still checked out as clean. It now reads the invalid calls from the dict when the converted message has none. Message objects are handled as before.Two new tests pass dict state through
abandoned_tool_messagesandclose_parked_checkpoint. Both fail onmainand pass here. The second half (cancelling the Rich CLI close) is left for a separate change.Type of change
Checklist
uv run ruff check .passesuv run pytestpasses (on Windows, the symlink-privilege and git-config tests fail the same way onmain)Summary by CodeRabbit