fix(agents): validate tool name against declared tools list before dispatch #64

Merged
CoreRasurae merged 1 commit from bugfix/m2-llmagent-tool-dispatch-validation into master 2026-06-30 11:28:31 +00:00
Member

Summary

Validates that the model-synthesized tool names in the LLM synthesis round match the declared tools list, preventing crashes or silent failures when the model hallucinates an undeclared tool name.

Fixes an additional bug in ToolAgent._execute_tool() where the containment check tool_name not in self.tools always failed because self.tools is a heterogeneous list of strings and dicts — a string tool name like "file_read" would never match. Fixed by extracting only string entries for the comparison.

Changes

File Change
src/cleveractors/agents/llm.py Build _declared_names set from self._lc_tools; reject undeclared tool calls with ToolMessage error
src/cleveractors/agents/tool.py Split string_tool_names from mixed list for correct dispatch check

Closes #63

## Summary Validates that the model-synthesized tool names in the LLM synthesis round match the declared tools list, preventing crashes or silent failures when the model hallucinates an undeclared tool name. Fixes an additional bug in `ToolAgent._execute_tool()` where the containment check `tool_name not in self.tools` always failed because `self.tools` is a heterogeneous list of strings and dicts — a string tool name like `"file_read"` would never match. Fixed by extracting only string entries for the comparison. ## Changes | File | Change | |------|--------| | `src/cleveractors/agents/llm.py` | Build `_declared_names` set from `self._lc_tools`; reject undeclared tool calls with `ToolMessage` error | | `src/cleveractors/agents/tool.py` | Split `string_tool_names` from mixed list for correct dispatch check | Closes #63
fix(agents): validate tool name against declared tools list before dispatch
Some checks failed
CI / lint (pull_request) Failing after 45s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 36s
CI / unit_tests (pull_request) Successful in 3m38s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 37s
CI / status-check (pull_request) Failing after 2s
bb3decbd96
ISSUES CLOSED: #63
Graa left a comment

Code Review — PR #64

Fixes look correct and well-scoped. A few notes:

llm.py — synthesis-round validation

Building _declared_names as a set from self._lc_tools is the right pattern: O(1) lookup, and gracefully handles the hallucinated-name case by returning a ToolMessage error rather than crashing. The t.get("function", {}).get("name", "") extraction matches the standard OpenAI/LangChain tool schema correctly.

Minor nit: _declared_names is built unconditionally, before the if isinstance(synth_tool_calls, list) and synth_tool_calls and _has_tools: gate. It runs even when there are no tool calls — not a bug, but a small waste. Could move it inside the if block. Low priority.

tool.py_execute_tool dispatch check

The fix (splitting string_tool_names from the heterogeneous self.tools list) is cleaner and more explicit than the original tool_name not in self.tools check. The original actually did work correctly in Python (a string == a dict is always False, so the and dict_tool_names fallback caught the dict-defined tools), but the refactored version makes the intent unambiguous. Good call.

Micro nit: string_tool_names is a list, so tool_name not in string_tool_names is O(n). For typical tool list sizes this is negligible — just flagging it in case the list ever grows large.

Blank-line additions in tool.py

Pure style — no functional change.

CI

All checks still pending at time of review. Nothing to block on from the code side.


Verdict: Logic is sound, both fixes are correct, CHANGELOG entries are accurate. Ready to merge once CI is green.

cc @rui.hu

## Code Review — PR #64 Fixes look correct and well-scoped. A few notes: ### `llm.py` — synthesis-round validation ✅ Building `_declared_names` as a set from `self._lc_tools` is the right pattern: O(1) lookup, and gracefully handles the hallucinated-name case by returning a `ToolMessage` error rather than crashing. The `t.get("function", {}).get("name", "")` extraction matches the standard OpenAI/LangChain tool schema correctly. **Minor nit:** `_declared_names` is built unconditionally, before the `if isinstance(synth_tool_calls, list) and synth_tool_calls and _has_tools:` gate. It runs even when there are no tool calls — not a bug, but a small waste. Could move it inside the `if` block. Low priority. ### `tool.py` — `_execute_tool` dispatch check ✅ The fix (splitting `string_tool_names` from the heterogeneous `self.tools` list) is cleaner and more explicit than the original `tool_name not in self.tools` check. The original actually did work correctly in Python (a string `==` a dict is always `False`, so the `and dict_tool_names` fallback caught the dict-defined tools), but the refactored version makes the intent unambiguous. Good call. **Micro nit:** `string_tool_names` is a `list`, so `tool_name not in string_tool_names` is O(n). For typical tool list sizes this is negligible — just flagging it in case the list ever grows large. ### Blank-line additions in `tool.py` Pure style — no functional change. ### CI All checks still pending at time of review. Nothing to block on from the code side. --- **Verdict:** Logic is sound, both fixes are correct, CHANGELOG entries are accurate. Ready to merge once CI is green. cc @rui.hu
hurui200320 left a comment

PR Review: !64 (Ticket #63)

Verdict: Approve

The synthesis-branch guard is implemented correctly, and the tool.py refactor is a reasonable clarity improvement. The PR author has explained (in a comment on ticket #63) why they did not extend the guard to the other two tool-dispatch branches: the _execute_tool safeguard already rejects undeclared tool names, and the surrounding except (ExecutionError, ConfigurationError) block in llm.py already converts that into a ToolMessage for the LLM. The new _declared_names check is therefore a UX refinement for clearer LLM feedback, applied where it matters most. This reasoning is technically sound and should be reflected back into the ticket (see Summary).

Critical Issues

None.

Major Issues

None.

Minor Issues

  1. PR description overstates the tool.py change as a bug fix.
    • File: src/cleveractors/agents/tool.py, lines 256–259
    • Problem: The PR description says: "the containment check tool_name not in self.tools always failed because self.tools is a heterogeneous list of strings and dicts — a string tool name like "file_read" would never match." This claim is not quite accurate — the original check tool_name not in self.tools and tool_name not in dict_tool_names did function correctly, because Python's in operator returns False for any string-vs-dict equality, causing the dict_tool_names fallback to handle dict-typed tools. The author's own ticket comment ("it just needs to be adjusted to...") implicitly acknowledges this — they propose simplifying to if tool_name not in dict_tool_names:, but the actual PR keeps both checks. Graa's review on the PR also notes the original code worked.
    • Recommendation: The refactor is fine as a clarity/safety improvement, but the commit message and PR description should describe it as a clarity refactor (or a defensive split), not a bug fix. Otherwise a future reader may misdiagnose the original code as broken.

Nits

  1. _declared_names built outside its gate in llm.py (src/cleveractors/agents/llm.py:1058-1060): the set comprehension runs even when there are no synth_tool_calls. Cost is trivial; could be moved inside the if isinstance(synth_tool_calls, list) and synth_tool_calls and _has_tools: gate for symmetry with the rest of the block. (Already flagged by Graa.)
  2. string_tool_names is a list, not a set (src/cleveractors/agents/tool.py:256): tool_name not in string_tool_names is O(n). Negligible at typical tool counts (≤10); a set would also make the two checks symmetric in style.
  3. Blank-line additions in tool.py (lines 94, 200): cosmetic only.

Summary

The synthesis-branch fix is correct: it builds _declared_names from self._lc_tools using the OpenAI/LangChain tool-schema convention (t.get("function", {}).get("name", "")), appends a ToolMessage with the same error wording the ticket suggests, and uses continue to keep the loop alive so the LLM can recover. The tool.py refactor splits the heterogeneous self.tools list into string_tool_names and dict_tool_names, making the dispatch check explicit.

The PR does not strictly satisfy ticket AC #3 ("the guard is applied consistently in all three tool-dispatch branches"), but the PR author has provided a sound technical justification in the ticket comments for limiting the new check to the synthesis branch: the existing ToolAgent._execute_tool safeguard + the surrounding except ExecutionError handler already prevent undeclared tool execution and deliver error feedback to the LLM in the other two branches. The new check is therefore a UX refinement for clearer LLM feedback, applied where it provides the most value (the synthesis round, where the model is being explicitly prompted to recover).

Recommended follow-up (not a blocker): Reconcile the ticket with the implementation. Either:

  • Update ticket #63 to reflect the narrower scope (e.g., add a note that the safeguard in tool.py already prevents the bug in the other branches, and the new check is a UX refinement), or
  • Open a follow-up ticket to apply the new _declared_names guard in the main loop and budget-exhausted branches for error-message consistency.

The existing review from Graa on the PR approves the fix and concurs on the nits above.

## PR Review: !64 (Ticket #63) ### Verdict: Approve The synthesis-branch guard is implemented correctly, and the `tool.py` refactor is a reasonable clarity improvement. The PR author has explained (in a comment on ticket #63) why they did not extend the guard to the other two tool-dispatch branches: the `_execute_tool` safeguard already rejects undeclared tool names, and the surrounding `except (ExecutionError, ConfigurationError)` block in `llm.py` already converts that into a `ToolMessage` for the LLM. The new `_declared_names` check is therefore a UX refinement for clearer LLM feedback, applied where it matters most. This reasoning is technically sound and should be reflected back into the ticket (see Summary). ### Critical Issues None. ### Major Issues None. ### Minor Issues 1. **PR description overstates the `tool.py` change as a bug fix.** - **File:** `src/cleveractors/agents/tool.py`, lines 256–259 - **Problem:** The PR description says: *"the containment check `tool_name not in self.tools` always failed because `self.tools` is a heterogeneous list of strings and dicts — a string tool name like `"file_read"` would never match."* This claim is not quite accurate — the original check `tool_name not in self.tools and tool_name not in dict_tool_names` did function correctly, because Python's `in` operator returns `False` for any string-vs-dict equality, causing the `dict_tool_names` fallback to handle dict-typed tools. The author's own ticket comment ("it just needs to be adjusted to...") implicitly acknowledges this — they propose simplifying to `if tool_name not in dict_tool_names:`, but the actual PR keeps both checks. Graa's review on the PR also notes the original code worked. - **Recommendation:** The refactor is fine as a clarity/safety improvement, but the commit message and PR description should describe it as a clarity refactor (or a defensive split), not a bug fix. Otherwise a future reader may misdiagnose the original code as broken. ### Nits 1. **`_declared_names` built outside its gate in `llm.py`** (`src/cleveractors/agents/llm.py:1058-1060`): the set comprehension runs even when there are no `synth_tool_calls`. Cost is trivial; could be moved inside the `if isinstance(synth_tool_calls, list) and synth_tool_calls and _has_tools:` gate for symmetry with the rest of the block. (Already flagged by Graa.) 2. **`string_tool_names` is a `list`, not a `set`** (`src/cleveractors/agents/tool.py:256`): `tool_name not in string_tool_names` is O(n). Negligible at typical tool counts (≤10); a set would also make the two checks symmetric in style. 3. **Blank-line additions in `tool.py`** (lines 94, 200): cosmetic only. ### Summary The synthesis-branch fix is correct: it builds `_declared_names` from `self._lc_tools` using the OpenAI/LangChain tool-schema convention (`t.get("function", {}).get("name", "")`), appends a `ToolMessage` with the same error wording the ticket suggests, and uses `continue` to keep the loop alive so the LLM can recover. The `tool.py` refactor splits the heterogeneous `self.tools` list into `string_tool_names` and `dict_tool_names`, making the dispatch check explicit. The PR does not strictly satisfy ticket AC #3 ("the guard is applied consistently in all three tool-dispatch branches"), but the PR author has provided a sound technical justification in the ticket comments for limiting the new check to the synthesis branch: the existing `ToolAgent._execute_tool` safeguard + the surrounding `except ExecutionError` handler already prevent undeclared tool execution and deliver error feedback to the LLM in the other two branches. The new check is therefore a UX refinement for clearer LLM feedback, applied where it provides the most value (the synthesis round, where the model is being explicitly prompted to recover). **Recommended follow-up (not a blocker):** Reconcile the ticket with the implementation. Either: - Update ticket #63 to reflect the narrower scope (e.g., add a note that the safeguard in `tool.py` already prevents the bug in the other branches, and the new check is a UX refinement), **or** - Open a follow-up ticket to apply the new `_declared_names` guard in the main loop and budget-exhausted branches for error-message consistency. The existing review from Graa on the PR approves the fix and concurs on the nits above.
CoreRasurae force-pushed bugfix/m2-llmagent-tool-dispatch-validation from bb3decbd96
Some checks failed
CI / lint (pull_request) Failing after 45s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 51s
CI / quality (pull_request) Successful in 36s
CI / unit_tests (pull_request) Successful in 3m38s
CI / coverage (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 1m5s
CI / build (pull_request) Successful in 37s
CI / status-check (pull_request) Failing after 2s
to 4642d49a86
All checks were successful
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 49s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m6s
CI / integration_tests (pull_request) Successful in 1m8s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 4s
2026-06-30 09:26:23 +00:00
Compare
CoreRasurae force-pushed bugfix/m2-llmagent-tool-dispatch-validation from 4642d49a86
All checks were successful
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 49s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m6s
CI / integration_tests (pull_request) Successful in 1m8s
CI / build (pull_request) Successful in 34s
CI / coverage (pull_request) Successful in 3m7s
CI / status-check (pull_request) Successful in 4s
to bcce5dc42a
All checks were successful
CI / lint (pull_request) Successful in 43s
CI / typecheck (pull_request) Successful in 47s
CI / security (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 31s
CI / unit_tests (pull_request) Successful in 3m1s
CI / integration_tests (pull_request) Successful in 1m1s
CI / build (pull_request) Successful in 31s
CI / coverage (pull_request) Successful in 3m2s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 31s
CI / typecheck (push) Successful in 46s
CI / security (push) Successful in 47s
CI / quality (push) Successful in 30s
CI / unit_tests (push) Successful in 3m9s
CI / integration_tests (push) Successful in 1m7s
CI / build (push) Successful in 37s
CI / coverage (push) Successful in 3m11s
CI / status-check (push) Successful in 3s
2026-06-30 11:06:15 +00:00
Compare
CoreRasurae deleted branch bugfix/m2-llmagent-tool-dispatch-validation 2026-06-30 11:28:31 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
cleveragents/cleveractors-core!64
No description provided.