Skip to content

fix(agent): optional require_tool_call runtime fabrication guard#1490

Open
kgarg2468 wants to merge 2 commits into
developfrom
fix/RR-1403-require-tool-call-guard
Open

fix(agent): optional require_tool_call runtime fabrication guard#1490
kgarg2468 wants to merge 2 commits into
developfrom
fix/RR-1403-require-tool-call-guard

Conversation

@kgarg2468

@kgarg2468 kgarg2468 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an opt-in, default-off require_tool_call guardrail: when enabled, an
    agent run that produces an answer without invoking at least one tool fails
    with a RocketRide.agent.guard.v1 error instead of delivering the ungrounded
    text. Fixes the gap where a weak planner could narrate a tool chain in prose
    (the "must call ≥1 tool" rule existed only as a prompt instruction, not code).
  • Enforced once in the shared AgentBase (tally on AgentContext, counted in
    call_tool, checked in run_agent), so it covers all five agent frameworks;
    crewai-manager and deepagent sub-contexts thread the parent's tally so
    delegated tool calls count. meta.tool_calls is now reported on every answer.
  • Exposes require_tool_call (boolean, off by default) on the six top-level
    agent service files, with README notes + regenerated schema tables.

Type

Fix (bug) — adds an opt-in runtime guardrail + config field; no behavior change
when the flag is off.

Testing

  • Tests added or updated
  • Tested locally
  • ./builder test passes — see note below

Added packages/ai/tests/ai/common/agent/ (first AgentBase unit tests) plus
config cases in test_config_shapes.py:

  • Unit (11): call_tool counting, guard trip/pass/off, sub-context sharing,
    context hashability, string-safe bool coercion, error-path count.
  • Integration (8): drives the real RocketRide wave loop and the real
    LangChain create_agent loop
    — fabrication trips, grounding passes,
    memory.peek-only trips (local reads don't count), parallel tools all counted.
  • Config (14) + contracts (16) for all six service files. ruff lint +
    format clean.

Note: ./builder test wasn't run locally (no built C++ engine here). The
ai:test suite + nodes:test-contracts were verified under a Python 3.12
engine-stub harness plus ruff; the full ./builder test runs in CI.

Checklist

  • Commit messages follow conventional commits
  • No secrets or credentials included
  • Wiki updated (if applicable) — n/a
  • Breaking changes documented (if applicable) — none; default-off, additive meta.tool_calls key only

Linked Issue

Fixes #1403


Out of scope (mentioned in the issue, deliberately not built): single-shot composite tools; provider-echoed tool-call ids (the call_tool counter supersedes that need as a detection mechanism).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an optional Require tool call fabrication guard to multiple agent node types.
    • When enabled, runs must invoke at least one real tool before returning an answer, otherwise they fail with a guard error.
    • Response metadata now includes the number of tool calls for the run.
  • Bug Fixes
    • Tool calls made by sub-agents now count toward the parent run’s requirement.
    • Internal/local reads (e.g., memory peek) do not satisfy the requirement.
  • Documentation
    • Updated node schemas and READMEs to document require_tool_call behavior and defaults.

No runtime guard asserted that an agent actually invoked a tool, so a weak
planning model could narrate a multi-step tool chain in prose instead of
calling the tools — producing a plausible but ungrounded answer. The
"must call >=1 tool" rule existed only as a prompt instruction, never as code.

Enforce it once in the shared AgentBase so it covers all five agent
frameworks (langchain, llamaindex, rocketride, crewai, deepagent):

- AgentContext gains a run-scoped `invoked_tools` tally (a mutable field on
  the otherwise-frozen context; compare=False keeps the context hashable, and
  list.append is thread-safe under the GIL).
- call_tool — the single choke point every driver routes real tool calls
  through — records each invocation (memory.peek and other local reads bypass
  it and correctly do not count).
- run_agent raises ToolCallRequiredError -> a distinct
  RocketRide.agent.guard.v1 error answer when require_tool_call is on and no
  tool ran; meta.tool_calls now reports the count on success, guard, and error.
- The crewai-manager and deepagent sub-contexts thread the parent's tally so
  tools a sub-agent invokes count toward the parent run's guard.

Exposed as an off-by-default `require_tool_call` boolean on the six top-level
agent service files, read with parse_bool (a stringified "false" leaves it
off, rather than tripping the guard), with README notes + regenerated schema
tables.

Tests (packages/ai/tests/ai/common/agent/ + test_config_shapes.py): AgentBase
guard unit tests, config resolution (flat/nested/default shapes), and
end-to-end integration driving the real RocketRide wave loop and the real
LangChain create_agent loop — fabrication trips the guard, grounding passes,
memory.peek-only trips, and parallel tools in one wave are all counted.

Closes #1403

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added docs Documentation module:nodes Python pipeline nodes module:ai AI/ML modules labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an optional require_tool_call fabrication guard across shared agent runtime code, node schemas, README docs, and tests. Tool invocations are counted per run, propagated to sub-agents, and exposed through tool_calls metadata and guard-specific error handling.

Changes

require_tool_call fabrication guard

Layer / File(s) Summary
Core guard mechanics
packages/ai/src/ai/common/agent/__init__.py, packages/ai/src/ai/common/agent/_internal/host.py, packages/ai/src/ai/common/agent/agent.py
Adds AgentContext.invoked_tools, ToolCallRequiredError, require_tool_call config parsing, guard handling in run_agent, tool_calls metadata, and call_tool recording.
Sub-agent tool-tally propagation
nodes/src/nodes/agent_crewai/crewai_manager/manager.py, nodes/src/nodes/agent_deepagent/deepagent.py
Passes parent invoked_tools into sub-agent AgentContext so delegated tool calls count toward the parent guard.
Node schema updates
nodes/src/nodes/agent_crewai/services.agent.json, nodes/src/nodes/agent_crewai/services.manager.json, nodes/src/nodes/agent_deepagent/services.agent.json, nodes/src/nodes/agent_langchain/services.json, nodes/src/nodes/agent_llamaindex/services.json, nodes/src/nodes/agent_rocketride/services.json
Adds require_tool_call boolean field, preconfig default (false), and updates shape/property lists across all node schemas.
README documentation
nodes/src/nodes/agent_crewai/README.md, nodes/src/nodes/agent_deepagent/README.md, nodes/src/nodes/agent_langchain/README.md, nodes/src/nodes/agent_llamaindex/README.md, nodes/src/nodes/agent_rocketride/README.md
Documents the fabrication guard behavior, require_tool_call field, and guard-related answer payload fields.
Unit tests
packages/ai/tests/ai/common/agent/test_agent_base.py, packages/ai/tests/ai/common/test_config_shapes.py
Adds tests for tool-call recording, guard behavior, sub-context sharing, hashability, and config resolution.
Integration tests
packages/ai/tests/ai/common/agent/test_require_tool_call_integration.py
Adds end-to-end tests exercising the RocketRide and LangChain drivers with fake tools/host to validate guard behavior scenarios.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested reviewers: jmaionchi, Rod-Christensen, stepmikhaylov

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested opt-in runtime guard in shared AgentBase, propagates tool counts, and adds matching config/docs/tests for #1403.
Out of Scope Changes check ✅ Passed The changes stay focused on the fabrication guard across agents, schemas, docs, and tests with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an optional runtime fabrication guard via require_tool_call.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/RR-1403-require-tool-call-guard

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ai/src/ai/common/agent/agent.py (1)

216-291: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate meta payload construction across success/guard/error branches.

The success, guard-trip, and generic-error branches each rebuild the same meta scaffold (framework, agent_id, run_id, started_at, ended_at, task_id handling) with slightly different styles (inline conditional-merge vs. post-hoc assignment). Extracting a small helper (e.g. _base_meta(iInstance, run_id, started_at, ended_at, task_id)) would remove the duplication and prevent future field additions from silently missing one of the three branches.

♻️ Sketch of shared helper
def _base_meta(self, iInstance, run_id, started_at, ended_at, task_id):
    meta = {
        'framework': self.FRAMEWORK,
        'agent_id': self._agent_id(iInstance),
        'run_id': run_id,
        'started_at': started_at,
        'ended_at': ended_at,
    }
    if task_id:
        meta['task_id'] = task_id
    return meta

Each branch would then do meta = self._base_meta(...); meta['tool_calls'] = ....

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai/src/ai/common/agent/agent.py` around lines 216 - 291, The
success, guard-trip, and error paths in the agent run flow all reconstruct the
same meta fields, making the logic duplicated and easy to drift. Extract the
shared meta assembly in the agent base _run logic into a helper such as
_base_meta (or equivalent) that builds framework, agent_id, run_id, started_at,
ended_at, and optional task_id, then have each branch in agent.py reuse it and
set only branch-specific fields like tool_calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/agent_crewai/README.md`:
- Around line 106-117: Update the Fabrication guard docs in README.md to match
the runtime contract used by the guard failure path: the failure error code
should be described as RocketRide.agent.guard.v1 instead of
RocketRide.agent.guard. Keep the existing explanation of require_tool_call and
tool-invocation behavior, but revise the guard failure wording so it matches
what consumers receive from the agent guard implementation.

In `@nodes/src/nodes/agent_crewai/services.manager.json`:
- Around line 58-63: Clarify the require_tool_call setting in
services.manager.json so it explicitly covers delegated subagent tool usage, not
just direct tool calls by the CrewAI Manager. Update the description for the
require_tool_call field to note that tool calls made by delegated subagents are
counted toward the manager’s total via sub-context propagation, while keeping
the existing guard behavior and referencing the require_tool_call setting for
easy location.

In `@nodes/src/nodes/agent_deepagent/README.md`:
- Around line 110-118: Update the Fabrication guard documentation in the README
section that describes require_tool_call so the guard failure name matches the
runtime contract used by the agent guard logic. Replace the old
RocketRide.agent.guard reference with RocketRide.agent.guard.v1 in this section,
keeping the surrounding explanation about real tool invocations and default-off
behavior unchanged.

In `@nodes/src/nodes/agent_langchain/README.md`:
- Around line 96-104: The Fabrication guard documentation still uses the
unversioned `RocketRide.agent.guard` identifier, which is inconsistent with the
Answer payload docs and the current guard failure contract. Update the
`Fabrication guard` section in the agent_langchain README to use
`RocketRide.agent.guard.v1` everywhere the guard error is mentioned, keeping the
wording aligned with the existing `Answer payload` documentation and
`require_tool_call` behavior.

In `@nodes/src/nodes/agent_llamaindex/README.md`:
- Around line 94-102: Update the Fabrication guard documentation in the
agent_llamaindex README so the guard failure payload matches the runtime
contract used by this PR. In the Require tool call description, replace the old
RocketRide.agent.guard identifier with RocketRide.agent.guard.v1, and keep the
wording aligned with the actual failure behavior exposed to consumers.

In `@packages/ai/src/ai/common/agent/agent.py`:
- Around line 237-264: The ToolCallRequiredError guard-trip path builds
answer_payload without the framework raw trace, unlike the normal success path.
Update the except ToolCallRequiredError block in agent.py so the existing raw
value in scope is attached to the stack alongside the current
RocketRide.agent.guard.v1 entry, using the same RocketRide.agent.raw.v1
structure as the success flow. Keep the guard response behavior unchanged, but
ensure answer_payload.stack preserves both the guard reason and raw trace for
debugging.

---

Outside diff comments:
In `@packages/ai/src/ai/common/agent/agent.py`:
- Around line 216-291: The success, guard-trip, and error paths in the agent run
flow all reconstruct the same meta fields, making the logic duplicated and easy
to drift. Extract the shared meta assembly in the agent base _run logic into a
helper such as _base_meta (or equivalent) that builds framework, agent_id,
run_id, started_at, ended_at, and optional task_id, then have each branch in
agent.py reuse it and set only branch-specific fields like tool_calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 40d99bb2-376c-44a6-93a6-5a75a4873eaa

📥 Commits

Reviewing files that changed from the base of the PR and between 89f6129 and b75c312.

📒 Files selected for processing (20)
  • nodes/src/nodes/agent_crewai/README.md
  • nodes/src/nodes/agent_crewai/crewai_manager/manager.py
  • nodes/src/nodes/agent_crewai/services.agent.json
  • nodes/src/nodes/agent_crewai/services.manager.json
  • nodes/src/nodes/agent_deepagent/README.md
  • nodes/src/nodes/agent_deepagent/deepagent.py
  • nodes/src/nodes/agent_deepagent/services.agent.json
  • nodes/src/nodes/agent_langchain/README.md
  • nodes/src/nodes/agent_langchain/services.json
  • nodes/src/nodes/agent_llamaindex/README.md
  • nodes/src/nodes/agent_llamaindex/services.json
  • nodes/src/nodes/agent_rocketride/README.md
  • nodes/src/nodes/agent_rocketride/services.json
  • packages/ai/src/ai/common/agent/__init__.py
  • packages/ai/src/ai/common/agent/_internal/host.py
  • packages/ai/src/ai/common/agent/agent.py
  • packages/ai/tests/ai/common/agent/__init__.py
  • packages/ai/tests/ai/common/agent/test_agent_base.py
  • packages/ai/tests/ai/common/agent/test_require_tool_call_integration.py
  • packages/ai/tests/ai/common/test_config_shapes.py

Comment thread nodes/src/nodes/agent_crewai/README.md
Comment on lines +58 to +63
"require_tool_call": {
"type": "boolean",
"title": "Require tool call",
"default": false,
"description": "Require the agent to invoke at least one tool before answering. When on, a run that answers without calling any tool fails with a guard error. Use for determinism-critical pipelines where an ungrounded or narrated answer must never be delivered. Off by default."
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify guard scope for delegated tool calls.

The description is generic ("invoke at least one tool before answering"), but a CrewAI Manager typically delegates work to subagents rather than calling tools directly. Per the stack context, delegated subagent tool calls are tallied into the manager's parent count via sub-context propagation — worth a short note here so operators don't misread the guard as requiring the manager itself to call a tool.

✏️ Suggested wording tweak
-			"description": "Require the agent to invoke at least one tool before answering. When on, a run that answers without calling any tool fails with a guard error. Use for determinism-critical pipelines where an ungrounded or narrated answer must never be delivered. Off by default."
+			"description": "Require at least one tool call (including calls delegated to sub-agents) before answering. When on, a run that answers without any tool invocation fails with a guard error. Use for determinism-critical pipelines where an ungrounded or narrated answer must never be delivered. Off by default."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"require_tool_call": {
"type": "boolean",
"title": "Require tool call",
"default": false,
"description": "Require the agent to invoke at least one tool before answering. When on, a run that answers without calling any tool fails with a guard error. Use for determinism-critical pipelines where an ungrounded or narrated answer must never be delivered. Off by default."
},
"require_tool_call": {
"type": "boolean",
"title": "Require tool call",
"default": false,
"description": "Require at least one tool call (including calls delegated to sub-agents) before answering. When on, a run that answers without any tool invocation fails with a guard error. Use for determinism-critical pipelines where an ungrounded or narrated answer must never be delivered. Off by default."
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nodes/src/nodes/agent_crewai/services.manager.json` around lines 58 - 63,
Clarify the require_tool_call setting in services.manager.json so it explicitly
covers delegated subagent tool usage, not just direct tool calls by the CrewAI
Manager. Update the description for the require_tool_call field to note that
tool calls made by delegated subagents are counted toward the manager’s total
via sub-context propagation, while keeping the existing guard behavior and
referencing the require_tool_call setting for easy location.

Comment thread nodes/src/nodes/agent_deepagent/README.md
Comment thread nodes/src/nodes/agent_langchain/README.md
Comment thread nodes/src/nodes/agent_llamaindex/README.md
Comment thread packages/ai/src/ai/common/agent/agent.py
…swer

Addresses PR review feedback:
- The Fabrication-guard notes in all five agent READMEs referenced the
  unversioned `RocketRide.agent.guard`; the runtime emits
  `RocketRide.agent.guard.v1` (matching the existing `.raw.v1` / `.error.v1`
  identifiers). Use the versioned id consistently.
- The guard-trip answer now also carries the framework's raw output as a
  `RocketRide.agent.raw.v1` stack entry (like the success path), so an operator
  investigating a tripped guard can inspect exactly what was narrated. `raw` is
  bound before the try, so this is safe on every path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kgarg2468

Copy link
Copy Markdown
Collaborator Author

Thanks @coderabbitai — addressed in 39495821:

Fixed

  • Versioned guard code in docs (5 READMEs): the Fabrication-guard notes now use RocketRide.agent.guard.v1, matching the runtime payload and the existing .raw.v1 / .error.v1 identifiers.
  • Guard-trip answer retains the raw trace (agent.py): the guard payload now also carries the framework's raw output as a RocketRide.agent.raw.v1 stack entry (like the success path), so an operator investigating a tripped guard can see exactly what was narrated. raw is bound before the try, so it's safe on every path; added a test assertion.

Considered, keeping as-is

  • CrewAI Manager field description (services.manager.json): the require_tool_call description is intentionally identical across all six agent service files, and it's accurate at the run level — a tool invoked by any delegated sub-agent counts toward the manager's guard (the manager/deepagent sub-contexts share the parent's tally via invoked_tools=context.invoked_tools). The Fabrication-guard README note already frames this as "counts real tool invocations." Happy to add a manager-specific note if a maintainer prefers.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@kgarg2468 Thanks for the update — both fixes look good:

  • The versioned RocketRide.agent.guard.v1 codes across the READMEs now align with the .raw.v1/.error.v1 convention used elsewhere.
  • Retaining the raw trace as a RocketRide.agent.raw.v1 stack entry on the guard-trip path is a nice improvement for debuggability, and binding raw before the try is the right call to keep it safe on every path.

On the CrewAI Manager description: agreed, keeping it as-is makes sense given the shared invoked_tools tally design — a sub-agent's tool call legitimately counts toward the manager's guard, so a manager-specific caveat isn't strictly necessary. No need to change it.


✏️ Learnings added
Learnt from: kgarg2468
URL: https://github.com/rocketride-org/rocketride-server/pull/1490

Timestamp: 2026-07-07T18:45:50.702Z
Learning: In the `rocketride-server` repository, for the `require_tool_call` guardrail feature (packages/ai/src/ai/common/agent/agent.py and related sub-agent drivers like nodes/src/nodes/agent_crewai/crewai_manager/manager.py and nodes/src/nodes/agent_deepagent/deepagent.py), sub-agent `AgentContext` instances share the parent's `invoked_tools` list tally (passed via `invoked_tools=context.invoked_tools`). This means a tool invoked by any delegated sub-agent counts toward the parent/manager's `require_tool_call` guard check, which is why the `require_tool_call` field description is intentionally identical across all six agent service JSON files (services.agent.json / services.manager.json for crewai, deepagent, langchain, llamaindex, rocketride).

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@kgarg2468
kgarg2468 enabled auto-merge (squash) July 9, 2026 00:27

@joshuadarron joshuadarron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

Verdict: Approve. Default-off guard, single enforcement point, thorough tests, CI green.

Verified against the code (not just the diff)

  • Choke point holds: AgentBase.call_tool is the only place context.tools.invoke is reached (agent.py:457); all five drivers route real tool calls through it (crewai_base.py:370, deepagent.py:204, langchain.py:189, llamaindex.py:123, executor.py:462). No bypass path found.
  • Sub-context threading is per-run: both crewai-manager (manager.py:220) and deepagent (_collect_subagents, called from _run at deepagent.py:344) build sub-contexts inside the per-run path with the per-run parent list — no stale tally leaking across runs.
  • Hashability: invoked_tools uses field(default_factory=list, compare=False) on the frozen dataclass, and there's a regression test for it.
  • Coercion: parse_bool exists in ai.common.utils.config_utils and handles stringified "false"/"0"/"no"; bool("false") footgun avoided and tested.
  • Error path: context = None / raw = None bound before the try, so both except handlers are safe if _run raises early; guard answer retains the rejected framework output on the stack for diagnosis.
  • Counting before invoke() (so a raising tool still counts as a grounding attempt) is a reasonable, documented choice.

Non-blocking nit

  • test_config_shapes.py — the TestRequireToolCallResolution docstring says AgentBase reads the flag "exactly as bool(config.get('require_tool_call', False))", but __init__ uses parse_bool. Stale comment only; fine to fix in a follow-up.

CI: all builds, ruff, gitleaks, contracts green. Docs/schema/README generated tables all updated consistently across the six service files.

🤖 Generated with Claude Code

@asclearuc asclearuc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @kgarg2468 — this is a well-scoped, well-tested fix. Turning the "must call ≥1 tool" rule from a prompt instruction into a real runtime check closes a genuine fabrication gap, and doing it once in AgentBase.call_tool so all five frameworks are covered is the right call. Nice touches: compare=False to keep the frozen AgentContext hashable (with a regression test), parse_bool instead of bool() for string-safe coercion, and sharing the parent's tally into sub-contexts so delegated calls count.

Approving. Two small, non-blocking notes:

  1. When the guard trips, only content (the guard message) is written to the answers lane — the RocketRide.agent.guard.v1 stack entry that marks it as a guard trip lives only in the returned payload. A downstream node reading the lane can't distinguish a blocked run from a real answer. This matches the existing generic-error path, so it's consistent, not a regression — just flagging it as a conscious choice.
  2. The "thread-safe under the GIL" comment in AgentContext.invoked_tools is correct for 3.10–3.12 but would not hold on a future free-threaded (nogil) build. No change needed now.

Good to merge once you're happy with those.

@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@joshuadarron joshuadarron left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — require_tool_call fabrication guard ✅

Strong, well-reasoned change. The design (count at the one adapter every framework routes through, check at run-completion, opt-in and off by default) is the right shape, and the edge cases are handled with real care. I verified the load-bearing claims against the tree rather than taking them on faith:

Verified

  • The single choke point actually covers all five frameworks. Every driver routes real tool execution through AgentBase.call_tool: crewai (crewai_base.py:370), deepagent (deepagent.py:204), langchain (langchain.py:189), llamaindex (llamaindex.py:123), wave (executor.py:462). So counting in call_tool genuinely tallies every framework — enabling the flag on any of the 6 service files won't false-trip because its tools bypassed the counter.
  • AgentContext is @dataclass(frozen=True) (host.py:244), so the mutable invoked_tools list would make it unhashable if included in __hash__. field(default_factory=list, compare=False) is exactly the right fix, and adding it as a trailing defaulted field keeps every existing keyword construction valid. Nicely explained in the docstring and pinned by test_context_stays_hashable_with_the_tool_tally.
  • parse_bool exists (config_utils.py:49) and is used instead of bool() — this matters: bool("false") is True, which would silently arm the guard on every run when the flag arrives stringified. Good catch, and regression-tested.

Things done right

  • Count is recorded before invoke(), so a tool that raises still counts — an attempted call isn't fabrication. Correct semantics, tested.
  • except ToolCallRequiredError is ordered before except Exception (it subclasses RuntimeError), returns a distinct RocketRide.agent.guard.v1 answer, and retains the rejected framework output on the stack for diagnosis. meta.tool_calls is reported on success, guard, and error paths alike.
  • Sub-context threading (invoked_tools=context.invoked_tools) in the crewai manager and deepagent so delegated tool calls count toward the parent's guard — with test_independent_contexts_do_not_share_a_tally guarding against accidental cross-run sharing.
  • Thread-safe append documented and exercised by the parallel-tools integration test; memory.peek local reads correctly excluded (they never reach call_tool) and that's tested end-to-end.
  • Backward compatible: off by default, additive config + meta.tool_calls only.

Non-blocking

  • Integration coverage is Wave + LangChain only. crewai/llamaindex/deepagent counting is proven structurally (routing confirmed above) and by the sub-context unit test, but not driven end-to-end. Since the crewai-manager sub-context threading is the most intricate path, one integration smoke through the real manager loop would fully close the loop. Not a blocker given the routing is verified.
  • Agent with zero tools + require_tool_call on trips every run. Arguably correct (operator misconfig), but a config-time warning ("guard enabled but no tools connected") would be friendlier than a per-run guard error.
  • The require_tool_call rows land inside the ROCKETRIDE:GENERATED:PARAMS blocks; fine if produced by nodes:docs-generate (they match services.json verbatim, and the PR says the tables were regenerated) — just don't hand-maintain them.
  • The field isn't added to the crewai/deepagent services.subagent.json. Reasonable (subagents are delegated, not top-level runs), just flagging the asymmetry in case it was meant to be uniform.

Approving — correct, thorough, well-tested, and it fails loud in exactly the case it's meant to catch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation module:ai AI/ML modules module:nodes Python pipeline nodes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No runtime fabrication guard — agent may narrate a tool chain without calling it

3 participants