Skip to content

feat(hooks): add optional agent-hooks governance engine - #6710

Open
prayagupa wants to merge 8 commits into
crewAIInc:mainfrom
prayagupa:feat/agent-hooks-control-engine
Open

feat(hooks): add optional agent-hooks governance engine#6710
prayagupa wants to merge 8 commits into
crewAIInc:mainfrom
prayagupa:feat/agent-hooks-control-engine

Conversation

@prayagupa

Copy link
Copy Markdown

Summary

Adds agent-hooks as an optional, framework-neutral control engine for CrewAI. Policies, content filters, approval gates, and egress guards can be registered once and applied across execution, model, and tool lifecycle points.

  • Lazily loads agent-hooks-sdk through the agent-hooks extra: See: https://github.com/responsibleai/agent-hooks/blob/main/spec/AGENT-HOOKS-0.1.md
  • Appends the injected governor after CrewAI global and scoped hooks.
  • Applies transforms to the effective target and fails closed on denial, timeout, malformed context, or engine failure.
  • Uses per-run sessions and host-owned correlation IDs for auditable pre/post records.
  • Covers direct and executor model calls plus CrewAI and native-provider tool paths, with remaining provider-specific exclusions documented explicitly.

Architecture

flowchart LR
    subgraph Runtime["CrewAI runtime"]
        B["Crew and flow boundaries"]
        M["Model call paths"]
        T["Tool call paths"]
        H["CrewAI hook seams"]
        N["Global and scoped hooks"]
        G["Injected governor<br/>authoritative final hook"]
    end

    subgraph Engine["Optional agent-hooks control engine"]
        A["Point adapter<br/>schema-valid context and correlation"]
        L["Dedicated async emitter loop"]
        I["Registered interceptors<br/>policy, filter, approval, egress"]
        V{"Combined verdict"}
        R["Interception records<br/>buffer or record sink"]
    end

    B --> H
    M --> H
    T --> H
    H --> N --> G
    G --> A --> L --> I --> V
    V -->|allow| P["Proceed unchanged"]
    V -->|transform| X["Apply effective target"]
    V -->|deny, timeout, or error| D["Fail closed"]
    V --> R
Loading

How it works

  1. use_agent_hooks(...) lazily loads the optional SDK, configures the emitter and interceptors, and installs the engine as the process governor.
  2. CrewAI global and execution-scoped hooks run first. The governor adapter is appended last for supported lifecycle points.
  3. Each adapter validates and normalizes untrusted hook data, adds per-run session and correlation metadata, and submits the context to a dedicated async emitter loop.
  4. The combined verdict either allows the action, applies the effective transformed target, or fails closed. Pre-point denial prevents the action; post-point denial replaces the result with a blocked marker.
  5. Every decision produces an auditable record available from engine.records, take_records(), or a configured record sink.

Coverage

The engine maps CrewAI execution start/end, input/output, model pre/post, and tool pre/post points to the eight agent-hooks control points. CrewAI PRE_STEP and POST_STEP remain native. Direct provider-specific async calls and direct native-provider Pydantic responses are not yet uniformly governed and are documented as exclusions.

Validation

  • uv run mypy lib/ with and without agent-hooks-sdk installed
  • uv run ruff format --check and uv run ruff check --no-fix on changed sources
  • uv lock --check
  • uv run --package crewai --with "agent-hooks-sdk==0.1.0a3" pytest lib/crewai/tests -q

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an optional agent-hooks governance engine with fail-closed interception across crewAI execution, tool calls, and model calls. It adds correlation metadata, transformed-input cache ordering, lifecycle controls, bounded audit records, documentation, and comprehensive tests.

Changes

Agent Hooks Governance

Layer / File(s) Summary
Engine foundation and public integration
lib/crewai/pyproject.toml, lib/crewai/src/crewai/hooks/*
Adds the optional dependency, lazy exports, governor dispatch APIs, asynchronous emission handling, session tracking, fail-closed decisions, and bounded audit records.
Hook context and block contracts
lib/crewai/src/crewai/hooks/tool_hooks.py, lib/crewai/src/crewai/hooks/llm_hooks.py
Adds tool-call identifiers, error/block flags, model request identifiers, and terminal post-model block provenance.
Tool-call governance and execution state
lib/crewai/src/crewai/agents/..., lib/crewai/src/crewai/llm.py, lib/crewai/src/crewai/llms/base_llm.py, lib/crewai/src/crewai/tools/..., lib/crewai/src/crewai/utilities/...
Routes tool execution through hooks, delays cache reads until transformed inputs are available, correlates calls, and tracks failed or blocked execution.
Model-call governance and correlation
lib/crewai/src/crewai/llm.py, lib/crewai/src/crewai/llms/base_llm.py, lib/crewai/src/crewai/utilities/agent_utils.py
Propagates request identifiers, governs structured model responses, enforces post-model blocks, and applies fail-closed response validation.
Documentation and governance validation
docs/..., lib/crewai/tests/...
Documents installation, interception semantics, records, and integration behavior while testing lifecycle, session, tool, model, caching, and failure-state changes.

Suggested reviewers: vinibrsl

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the addition of the optional agent-hooks governance engine.
Description check ✅ Passed The description directly explains the agent-hooks governance engine, its integration, behavior, coverage, and validation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 438dc75b8d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/crewai/src/crewai/utilities/agent_utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
lib/crewai/src/crewai/utilities/agent_utils.py (1)

2045-2054: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restrict strict Pydantic reparsing to governed post-model hooks.

This block runs for every registered after_llm_call hook chain, not only the optional agent-hooks governance adapter. A generic hook returning a modified string will now hard-fail on pydantic response_model output, changing the general after_llm_call hook contract. Scope this behavior to governance hooks or document/emit the breaking contract clearly for all hook consumers.

🤖 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 `@lib/crewai/src/crewai/utilities/agent_utils.py` around lines 2045 - 2054,
Restrict the strict Pydantic reparsing in the hook-modified response path to the
governed post-model hooks handled by the agent-hooks governance adapter, rather
than every after_llm_call hook chain. Preserve the existing generic hook
contract by bypassing this model_validate_json validation for non-governance
hooks, using the surrounding hook identification or governance adapter symbol to
distinguish the paths.
🧹 Nitpick comments (1)
lib/crewai/tests/tools/test_tool_usage.py (1)

278-294: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider an autouse fixture for global hook cleanup.

Both tests hand-roll clear_* + try/finally, and each clears only the hook type it registers, so a hook leaked by another module (or by a failure between register_* and try) still bleeds in. A small autouse fixture clearing both registries before and after each test would make this suite order-independent.

Also applies to: 326-340

🤖 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 `@lib/crewai/tests/tools/test_tool_usage.py` around lines 278 - 294, Replace
the duplicated manual cleanup around the synchronous and asynchronous tool
execution tests with an autouse fixture that clears both before-tool-call and
after-tool-call hook registries before and after every test. Remove the local
clear/try/finally blocks while preserving the existing test execution paths.
🤖 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 `@docs/edge/en/learn/agent-hooks.mdx`:
- Line 84: Update docs/edge/en/learn/agent-hooks.mdx lines 84-84 to call
AgentHooksEngine.records as engine.records() when printing the auditable
records; update lines 173-175 to document engine.records() as the non-draining
accessor.

In `@lib/crewai/src/crewai/agents/crew_agent_executor.py`:
- Around line 1023-1035: Update the after-hook governance context in
_execute_single_native_tool_call and execute_single_native_tool_call so is_error
also becomes true for unresolved, non-cached tool calls where output_tool is
None. Apply this in both lib/crewai/src/crewai/agents/crew_agent_executor.py
lines 1023-1035 and lib/crewai/src/crewai/utilities/agent_utils.py lines
1730-1742, preserving each method’s existing error flags.

In `@lib/crewai/src/crewai/experimental/agent_executor.py`:
- Around line 2056-2058: Update the post-tool governance record’s is_error
calculation near call_id and was_blocked to also treat the output_tool is
None/tool-not-found path as an error. Preserve the existing hook_blocked,
max_usage_reached, and error_event_emitted conditions while ensuring a missing
tool cannot be recorded as successful.

In `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py`:
- Around line 610-641: Update _active_sessions bookkeeping in
_current_session_id, _begin_session, and _finish_session to retain and validate
the owner object rather than relying solely on id(owner). Use weak references or
an equivalent identity check so garbage-collected owners cannot cause recycled
addresses to inherit stale sessions or builders, while preserving current
session stack behavior.

In `@lib/crewai/src/crewai/llm.py`:
- Around line 1768-1771: Ensure argument parsing and dictionary validation in
the tool-call flow completes before the region guarded by the before/after hook
lifecycle, or track whether `run_before_tool_call_hooks` actually ran and only
invoke the corresponding after-hooks when it did. Update both affected paths
around `tool_call.function.arguments` so parse failures cannot emit an orphan
`post_tool_call` for the call_id.

In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 1877-1896: Replace the string-type check on aborted.source in the
HookAborted handler with a dedicated sentinel marker owned by AgentHooksEngine,
and recognize only that marker as an agent-hooks abort. Preserve the existing
generic message and False return for all other sources, including custom string
sources, while retaining reason printing and ValueError propagation for the
sentinel case.

---

Outside diff comments:
In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 2045-2054: Restrict the strict Pydantic reparsing in the
hook-modified response path to the governed post-model hooks handled by the
agent-hooks governance adapter, rather than every after_llm_call hook chain.
Preserve the existing generic hook contract by bypassing this
model_validate_json validation for non-governance hooks, using the surrounding
hook identification or governance adapter symbol to distinguish the paths.

---

Nitpick comments:
In `@lib/crewai/tests/tools/test_tool_usage.py`:
- Around line 278-294: Replace the duplicated manual cleanup around the
synchronous and asynchronous tool execution tests with an autouse fixture that
clears both before-tool-call and after-tool-call hook registries before and
after every test. Remove the local clear/try/finally blocks while preserving the
existing test execution paths.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d2337f25-444a-4f06-99ea-e1685872d7ca

📥 Commits

Reviewing files that changed from the base of the PR and between f15844b and 438dc75.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • docs/docs.json
  • docs/edge/en/learn/agent-hooks.mdx
  • lib/crewai/pyproject.toml
  • lib/crewai/src/crewai/agents/crew_agent_executor.py
  • lib/crewai/src/crewai/experimental/agent_executor.py
  • lib/crewai/src/crewai/hooks/__init__.py
  • lib/crewai/src/crewai/hooks/agent_hooks_engine.py
  • lib/crewai/src/crewai/hooks/dispatch.py
  • lib/crewai/src/crewai/hooks/llm_hooks.py
  • lib/crewai/src/crewai/hooks/tool_hooks.py
  • lib/crewai/src/crewai/llm.py
  • lib/crewai/src/crewai/llms/base_llm.py
  • lib/crewai/src/crewai/tools/tool_usage.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/src/crewai/utilities/tool_utils.py
  • lib/crewai/tests/agents/test_native_tool_calling.py
  • lib/crewai/tests/hooks/test_agent_hooks_engine.py
  • lib/crewai/tests/tools/test_tool_usage.py
  • lib/crewai/tests/utilities/test_agent_utils.py

Comment thread docs/edge/en/learn/agent-hooks.mdx
Comment thread lib/crewai/src/crewai/agents/crew_agent_executor.py
Comment thread lib/crewai/src/crewai/experimental/agent_executor.py
Comment thread lib/crewai/src/crewai/hooks/agent_hooks_engine.py Outdated
Comment thread lib/crewai/src/crewai/llm.py
Comment thread lib/crewai/src/crewai/utilities/agent_utils.py
@prayagupa
prayagupa force-pushed the feat/agent-hooks-control-engine branch from 438dc75 to 113a2bc Compare July 30, 2026 19:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (3)
lib/crewai/src/crewai/llms/base_llm.py (1)

1107-1134: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale docstring example after dropping the str gate.

The example still shows if from_agent is None and isinstance(result, str):, but the method now accepts and governs any response type. Update it so providers don't reintroduce the gate that would skip POST_MODEL_CALL governance for structured responses.

📝 Proposed docstring fix
         Example:
             >>> # In a native provider's call() method:
-            >>> if from_agent is None and isinstance(result, str):
-            ...     result = self._invoke_after_llm_call_hooks(
-            ...         messages, result, from_agent
-            ...     )
+            >>> result = self._invoke_after_llm_call_hooks(messages, result, from_agent)
🤖 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 `@lib/crewai/src/crewai/llms/base_llm.py` around lines 1107 - 1134, The
docstring example for _invoke_after_llm_call_hooks still conditionally invokes
the hook only for string responses. Update the example to gate solely on
from_agent being None and invoke the method for any response type, preserving
structured-response governance.

Source: Coding guidelines

lib/crewai/src/crewai/utilities/agent_utils.py (1)

1913-1922: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new request_id parameter.

Both Args: blocks now omit request_id, which is the correlation identifier the whole governance audit trail keys on.

📝 Proposed docstring additions
     Args:
         executor_context: The executor context to setup the hooks for.
         printer: Printer instance for error logging.
+        request_id: Correlation identifier shared by this model call's pre/post hooks.
         verbose: Whether to print output.
     Args:
         executor_context: The executor context to setup the hooks for.
         answer: The LLM response (string or Pydantic model).
         printer: Printer instance for error logging.
+        request_id: Correlation identifier shared by this model call's pre/post hooks.
         verbose: Whether to print output.

As per coding guidelines: "Document public APIs and complex logic in Python code."

Also applies to: 2005-2015

🤖 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 `@lib/crewai/src/crewai/utilities/agent_utils.py` around lines 1913 - 1922,
Update the docstrings for the affected hook setup/invocation methods around the
executor context to include the new request_id parameter in their Args sections,
describing it as the correlation identifier used by the governance audit trail.
Apply this consistently to both documented methods, including the one around
lines 2005–2015.

Source: Coding guidelines

lib/crewai/src/crewai/llm.py (1)

1843-1885: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve ToolExecutionFailedError in the direct tool-call path.

available_functions can contain wrappers that call handle_tool_failure(...), which raises via ToolFailurePolicy.RAISE. The current except Exception as e converts that deliberate stop into a governed result/None; re-raise ToolExecutionFailedError unchanged alongside HookAborted.

🤖 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 `@lib/crewai/src/crewai/llm.py` around lines 1843 - 1885, Update the direct
tool-call exception handling to re-raise ToolExecutionFailedError unchanged
alongside HookAborted before the general except Exception branch. Keep the
existing governed-result and event-emission behavior for other tool execution
exceptions.

Source: Learnings

🧹 Nitpick comments (3)
lib/crewai/tests/hooks/test_agent_hooks_engine.py (1)

1757-1777: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the barrier waits so a failing thread can't hang the other one.

threading.Barrier.wait() without a timeout blocks forever if the sibling thread dies before reaching the barrier; the join(timeout=2.0) assertions then fail while a non-daemon thread leaks for the rest of the session.

♻️ Proposed change
         def run(label: str) -> None:
             session_id = engine._begin_session(crew=owner)
-            sessions_started.wait()
+            sessions_started.wait(timeout=5.0)
             observed[label] = (
                 session_id,
                 engine._current_session_id(crew=owner),
             )
-            sessions_observed.wait()
+            sessions_observed.wait(timeout=5.0)
             engine._finish_session(crew=owner)
 
-        first = threading.Thread(target=run, args=("first",))
-        second = threading.Thread(target=run, args=("second",))
+        first = threading.Thread(target=run, args=("first",), daemon=True)
+        second = threading.Thread(target=run, args=("second",), daemon=True)
🤖 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 `@lib/crewai/tests/hooks/test_agent_hooks_engine.py` around lines 1757 - 1777,
Bound both sessions_started.wait() and sessions_observed.wait() in the run
helper with a finite timeout so a missing sibling releases the test instead of
blocking indefinitely. Preserve the existing thread joins and session cleanup
behavior around the run function.
lib/crewai/src/crewai/hooks/agent_hooks_engine.py (1)

397-427: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Tool-call normalization is duplicated verbatim across the engine and the executor seam. Both sites walk a provider tool-call list, call extract_tool_call_info, json.loads string args, and wrap non-dict parses as {"raw": ...} / {"value": ...}. Since policy decisions depend on this exact shape, the two copies must never drift — extract one helper (e.g. normalize_tool_calls_for_governance in agent_utils) and call it from both.

  • lib/crewai/src/crewai/hooks/agent_hooks_engine.py#L397-L427: replace the inline loop in _model_response_parts with a call to the shared helper (it already imports from agent_utils here).
  • lib/crewai/src/crewai/utilities/agent_utils.py#L2039-L2061: move this loop into the shared helper and call it from _setup_after_llm_call_hooks.
🤖 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 `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py` around lines 397 - 427,
Extract the duplicated tool-call normalization into a shared agent_utils helper,
such as normalize_tool_calls_for_governance, preserving the existing
extract_tool_call_info, JSON parsing, and argument-wrapping behavior. In
lib/crewai/src/crewai/hooks/agent_hooks_engine.py lines 397-427, replace the
inline loop in _model_response_parts with the helper call; in
lib/crewai/src/crewai/utilities/agent_utils.py lines 2039-2061, move the
normalization loop into the helper and have _setup_after_llm_call_hooks call it.
lib/crewai/src/crewai/llms/base_llm.py (1)

740-797: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

The governed tool-hook scaffolding is near-identical in two tool-execution seams. Both generate a call_id, track before_hook_started / post_hook_attempted, build matching before/after ToolCallHookContext objects, re-raise HookAborted, and run an error-path after-hook with the same governed_result != error_msg return rule. Any future contract change has to be applied twice, and a miss silently weakens governance on one path.

  • lib/crewai/src/crewai/llms/base_llm.py#L740-L797: extract the shared before/after-hook wrapper (e.g. a BaseLLM._governed_tool_invocation helper or a small context manager) and use it here.
  • lib/crewai/src/crewai/llm.py#L1761-L1822: reuse that helper in _handle_tool_call instead of the parallel copy, keeping only the LiteLLM-specific argument parsing and event emission local.
🤖 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 `@lib/crewai/src/crewai/llms/base_llm.py` around lines 740 - 797, The governed
tool-hook logic is duplicated across two execution paths. In
lib/crewai/src/crewai/llms/base_llm.py:740-797, extract the shared
before/after-hook wrapper into a reusable BaseLLM._governed_tool_invocation
helper or context manager, preserving call ID tracking, HookAborted propagation,
context construction, and error-path behavior; in
lib/crewai/src/crewai/llm.py:1761-1822, update _handle_tool_call to reuse that
helper while keeping LiteLLM argument parsing and event emission local.
🤖 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 `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py`:
- Around line 1071-1088: Wrap the POST_MODEL_CALL call to
mark_post_model_blocked inside its own try/except Exception within the
adapter-error handling in guarded, logging any marking failure with
logger.exception. Ensure exceptions from mark_post_model_blocked do not escape
the fail-closed path, so the blocked-result return remains authoritative.

In `@lib/crewai/tests/hooks/test_agent_hooks_engine.py`:
- Around line 404-465: Update the timeout assertions in
test_timeout_cancels_and_releases_admission and
test_cancellation_resistant_timeout_keeps_admission_bounded to expect
concurrent.futures.TimeoutError (or a file-level alias), matching
_EmitterLoop.run’s FutureTimeoutError behavior on Python 3.10 while preserving
the existing admission assertion.

---

Outside diff comments:
In `@lib/crewai/src/crewai/llm.py`:
- Around line 1843-1885: Update the direct tool-call exception handling to
re-raise ToolExecutionFailedError unchanged alongside HookAborted before the
general except Exception branch. Keep the existing governed-result and
event-emission behavior for other tool execution exceptions.

In `@lib/crewai/src/crewai/llms/base_llm.py`:
- Around line 1107-1134: The docstring example for _invoke_after_llm_call_hooks
still conditionally invokes the hook only for string responses. Update the
example to gate solely on from_agent being None and invoke the method for any
response type, preserving structured-response governance.

In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 1913-1922: Update the docstrings for the affected hook
setup/invocation methods around the executor context to include the new
request_id parameter in their Args sections, describing it as the correlation
identifier used by the governance audit trail. Apply this consistently to both
documented methods, including the one around lines 2005–2015.

---

Nitpick comments:
In `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py`:
- Around line 397-427: Extract the duplicated tool-call normalization into a
shared agent_utils helper, such as normalize_tool_calls_for_governance,
preserving the existing extract_tool_call_info, JSON parsing, and
argument-wrapping behavior. In lib/crewai/src/crewai/hooks/agent_hooks_engine.py
lines 397-427, replace the inline loop in _model_response_parts with the helper
call; in lib/crewai/src/crewai/utilities/agent_utils.py lines 2039-2061, move
the normalization loop into the helper and have _setup_after_llm_call_hooks call
it.

In `@lib/crewai/src/crewai/llms/base_llm.py`:
- Around line 740-797: The governed tool-hook logic is duplicated across two
execution paths. In lib/crewai/src/crewai/llms/base_llm.py:740-797, extract the
shared before/after-hook wrapper into a reusable
BaseLLM._governed_tool_invocation helper or context manager, preserving call ID
tracking, HookAborted propagation, context construction, and error-path
behavior; in lib/crewai/src/crewai/llm.py:1761-1822, update _handle_tool_call to
reuse that helper while keeping LiteLLM argument parsing and event emission
local.

In `@lib/crewai/tests/hooks/test_agent_hooks_engine.py`:
- Around line 1757-1777: Bound both sessions_started.wait() and
sessions_observed.wait() in the run helper with a finite timeout so a missing
sibling releases the test instead of blocking indefinitely. Preserve the
existing thread joins and session cleanup behavior around the run function.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ca52160-6f72-4549-bc6c-45e93cd91b26

📥 Commits

Reviewing files that changed from the base of the PR and between 113a2bc and 4bffea7.

📒 Files selected for processing (11)
  • docs/edge/en/learn/agent-hooks.mdx
  • lib/crewai/src/crewai/agents/crew_agent_executor.py
  • lib/crewai/src/crewai/hooks/agent_hooks_engine.py
  • lib/crewai/src/crewai/hooks/dispatch.py
  • lib/crewai/src/crewai/hooks/llm_hooks.py
  • lib/crewai/src/crewai/llm.py
  • lib/crewai/src/crewai/llms/base_llm.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/tests/agents/test_native_tool_calling.py
  • lib/crewai/tests/hooks/test_agent_hooks_engine.py
  • lib/crewai/tests/hooks/test_llm_hooks.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/edge/en/learn/agent-hooks.mdx
  • lib/crewai/src/crewai/hooks/dispatch.py

Comment thread lib/crewai/src/crewai/hooks/agent_hooks_engine.py Outdated
Comment thread lib/crewai/tests/hooks/test_agent_hooks_engine.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (2)
lib/crewai/src/crewai/utilities/agent_utils.py (1)

2039-2079: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve transformations for native tool-call lists.

For list responses, structured_response is a newly created dictionary. If the governed hook mutates it in place, the identity check still succeeds and the original answer list is returned, silently discarding the transformation. If the hook replaces the response, this returns the normalized dictionary instead of the provider-native list, potentially bypassing downstream native-tool handling. Reconcile the governed response back into the native list shape, or update the downstream contract, and test both mutation styles.

🤖 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 `@lib/crewai/src/crewai/utilities/agent_utils.py` around lines 2039 - 2079,
Update the native tool-call list handling around structured_response and the
final return so POST_MODEL_CALL hook transformations are preserved without
changing the provider-native list contract. Reconcile both in-place mutations
and replaced hook_context.response values back into the native answer list
shape, rather than returning the unchanged answer or normalized dictionary
incorrectly. Add coverage for both hook mutation styles.
lib/crewai/src/crewai/hooks/agent_hooks_engine.py (1)

1270-1277: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

_execution_end leaks the session when self._builder(...) raises.

Unlike _execution_start (Line 1255), which builds inside the try so _finish_session always runs on failure, _execution_end calls self._builder(...) (Line 1272) before the try. If it raises — e.g. _current_session_id's "multiple active agent-hooks sessions lack an execution-local session identifier" RuntimeError, or any other builder-construction failure — _finish_session is skipped entirely, leaving a stale entry in _active_sessions/_builders for an owner that is still alive. That stale entry can make the ambiguity check misfire on the next run of the same crew/flow, compounding failures over time.

🐛 Proposed fix
     def _execution_end(self, ctx: ExecutionEndContext) -> None:
         """Adapt ``EXECUTION_END`` -> ``agent_shutdown``: records the run's end."""
-        builder = self._builder(crew=ctx.crew, flow=ctx.flow)
         reason = "completed" if ctx.status == "completed" else "error"
         try:
+            builder = self._builder(crew=ctx.crew, flow=ctx.flow)
             self._decide(builder.agent_shutdown(reason=reason))
         finally:
             self._finish_session(crew=ctx.crew, flow=ctx.flow)
🤖 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 `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py` around lines 1270 - 1277,
Move the self._builder(crew=ctx.crew, flow=ctx.flow) call inside the try block
in _execution_end, keeping _finish_session(crew=ctx.crew, flow=ctx.flow) in the
finally block so session cleanup runs even when builder construction raises.
Preserve the existing reason selection and agent_shutdown decision flow.
🧹 Nitpick comments (3)
lib/crewai/tests/hooks/test_agent_hooks_engine.py (1)

2017-2027: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert which records survived to actually cover oldest-first eviction.

The test varies call_id but never checks eviction order, so a LIFO/random eviction bug would still pass.

♻️ Strengthen the assertion
             assert len(engine.records) == 2
             assert engine.records_dropped == 1
+            # oldest-first eviction: "call-1" is the record that was dropped
+            assert [record.interception_point.value for record in engine.records] == [
+                "pre_tool_call",
+                "pre_tool_call",
+            ]
             assert DEFAULT_MAX_RECORDS > 2

Prefer asserting the retained call_ids directly if the record exposes them (e.g. via its context/correlation fields).

🤖 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 `@lib/crewai/tests/hooks/test_agent_hooks_engine.py` around lines 2017 - 2027,
Strengthen test_record_buffer_evicts_oldest_and_counts_drops by asserting the
retained records’ call_id values, using each record’s exposed context or
correlation field. Verify call-2 and call-3 remain after inserting call-1,
call-2, and call-3, while preserving the existing length and records_dropped
assertions.
lib/crewai/src/crewai/utilities/agent_utils.py (1)

2000-2004: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new request_id parameter.

The docstring documents the other parameters but omits request_id, which is part of the model-hook correlation contract. Add it to Args and describe its optional, host-owned semantics.

As per coding guidelines, Python code should document public APIs and complex logic.

🤖 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 `@lib/crewai/src/crewai/utilities/agent_utils.py` around lines 2000 - 2004,
Update the docstring for the function containing the `answer`, `printer`, and
`request_id` parameters to add `request_id` under `Args`; describe it as an
optional correlation identifier owned and supplied by the host for model-hook
correlation, while preserving the existing documentation for the other
parameters.

Source: Coding guidelines

lib/crewai/src/crewai/hooks/dispatch.py (1)

293-323: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the new final_hook parameter.

final_hook was added to the signature (Line 300) but the Args docstring section (Lines 308-315) doesn't mention it, and the deferred-abort/finalization semantics (a regular-hook abort is deferred until the final hook runs) are non-obvious from the code alone.

As per coding guidelines, "Document public APIs and complex logic in Python code."

📝 Proposed docstring addition
         reducer: Maps each hook's return value onto ``ctx``. Defaults to
             :func:`_default_reducer` (payload replacement). May raise
             :class:`HookAborted`.
         verbose: Whether to print swallowed-hook-error warnings.
+        final_hook: Optional hook run after ``hooks``, even if ``hooks`` is
+            empty or an earlier hook in ``hooks`` aborts. If a regular hook
+            aborts, that abort is deferred until ``final_hook`` completes and
+            is then re-raised (unless ``final_hook`` itself raises first).
🤖 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 `@lib/crewai/src/crewai/hooks/dispatch.py` around lines 293 - 323, Update the
run_hooks docstring to document the final_hook argument and explain that
regular-hook HookAborted failures are deferred until final_hook executes, after
which the abort is propagated. Keep the existing parameter and exception
documentation intact.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@lib/crewai/src/crewai/hooks/agent_hooks_engine.py`:
- Around line 1270-1277: Move the self._builder(crew=ctx.crew, flow=ctx.flow)
call inside the try block in _execution_end, keeping
_finish_session(crew=ctx.crew, flow=ctx.flow) in the finally block so session
cleanup runs even when builder construction raises. Preserve the existing reason
selection and agent_shutdown decision flow.

In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 2039-2079: Update the native tool-call list handling around
structured_response and the final return so POST_MODEL_CALL hook transformations
are preserved without changing the provider-native list contract. Reconcile both
in-place mutations and replaced hook_context.response values back into the
native answer list shape, rather than returning the unchanged answer or
normalized dictionary incorrectly. Add coverage for both hook mutation styles.

---

Nitpick comments:
In `@lib/crewai/src/crewai/hooks/dispatch.py`:
- Around line 293-323: Update the run_hooks docstring to document the final_hook
argument and explain that regular-hook HookAborted failures are deferred until
final_hook executes, after which the abort is propagated. Keep the existing
parameter and exception documentation intact.

In `@lib/crewai/src/crewai/utilities/agent_utils.py`:
- Around line 2000-2004: Update the docstring for the function containing the
`answer`, `printer`, and `request_id` parameters to add `request_id` under
`Args`; describe it as an optional correlation identifier owned and supplied by
the host for model-hook correlation, while preserving the existing documentation
for the other parameters.

In `@lib/crewai/tests/hooks/test_agent_hooks_engine.py`:
- Around line 2017-2027: Strengthen
test_record_buffer_evicts_oldest_and_counts_drops by asserting the retained
records’ call_id values, using each record’s exposed context or correlation
field. Verify call-2 and call-3 remain after inserting call-1, call-2, and
call-3, while preserving the existing length and records_dropped assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d5906841-8628-4135-ac46-8bc7129c9aaf

📥 Commits

Reviewing files that changed from the base of the PR and between 4bffea7 and 2f59512.

📒 Files selected for processing (5)
  • lib/crewai/src/crewai/hooks/agent_hooks_engine.py
  • lib/crewai/src/crewai/hooks/dispatch.py
  • lib/crewai/src/crewai/utilities/agent_utils.py
  • lib/crewai/tests/hooks/test_agent_hooks_engine.py
  • lib/crewai/tests/hooks/test_llm_hooks.py

Delegate crewAI's dispatch lifecycle points to the framework-neutral
agent-hooks contract via a dependency-injected governor, so policy
engines, content filters, and egress guards written once as agent-hooks
Interceptors govern the tool, model, and execution-boundary seams.

- hooks/agent_hooks_engine.py: AgentHooksEngine + use_agent_hooks(),
  lazy optional import, fail-closed emission on a dedicated event loop
- hooks/dispatch.py: set/clear/get_governor + governed_hook() injection
- utilities/agent_utils.py: govern the executor pre/post model-call seam
- hooks/__init__.py: lazy engine exports; docs + tests
…aths

The executor model-call seam now appends the same governor via
governed_hook(), so PRE_MODEL_CALL / POST_MODEL_CALL are governed on both
the dispatch and agent-executor (sync/async) paths. Flip the coverage
table to governed, replace the stale partial-coverage warning with an
accurate note, and clarify the injection in 'How it works'.
Declare `agent-hooks` as an optional-dependency extra scoped to Linux x86_64 (the only platform with a published wheel) so CI's `uv sync --all-extras` installs it and the governance tests run instead of skipping. Locks agent-hooks-sdk==0.1.0a3 and updates the install docs to `pip install "crewai[agent-hooks]"`.
…ation

- Break reference cycles in _json_safe with a <cycle> marker so a cyclic tool input cannot raise RecursionError before the decision (fail-closed).
- Wrap every per-point adapter so a context-build error fails closed (raise / blocked-result) instead of escaping dispatch and proceeding ungoverned.
- Stamp the acting agent on each emission and derive a stable pre/post tool-call id so the two contexts correlate.
- Surface model tool_calls/finish_reason at post_model_call.
- Drain pending emissions on _EmitterLoop.close() to release a blocked thread. Add regression tests.
Carry host-owned correlation IDs through model and tool seams, fail closed on invalid transforms, govern async/native/structured response paths, and make session lifecycle records schema-conformant.

Pin the optional SDK and add regression coverage for the review findings.

Signed-off-by: Prayag Upadhyay <prayag.upd@gmail.com>
Prevent a terminal governance decision from silently degrading into a
weaker tool-calling path, and keep block provenance host-owned so
untrusted adapter output cannot forge or mask a policy denial.

- Introduce PostModelCallBlockedError as a terminal executor result so a
  governed post-model block cannot downgrade into native or text
  tool-calling fallback on either the sync or async loop.
- Record block provenance in host-owned trusted context via
  mark_post_model_blocked / raise_if_post_model_blocked, keyed on the
  hook context in a WeakKeyDictionary; adapter errors fail closed as
  blocks instead of returning an SDK record.
- Bound the agent-hooks emitter loop with an admission queue and a
  close()/join lifecycle, and track per-owner sessions with weakref
  cleanup plus a dropped-records counter.
- Set post-hook error status when a native tool call cannot be resolved.
- Add tests for fail-closed no-fallback behavior and unresolved-tool
  error status.

Signed-off-by: Prayag Upadhyay <prayag.upd@gmail.com>
- Run the agent-hooks governor's EXECUTION_END as a final hook so shutdown
  and audit finalization still fire when a prior hook aborts; the pending
  HookAborted is re-raised afterward.
- Log host/adapter emission failures with logger.error, not logger.exception,
  so fail-closed logs stay payload-free (bounded correlation ids only).
- Guard mark_post_model_blocked so a provenance-recording failure still
  returns the blocked result instead of raising.
- Restore the original model on a non-governed post-model reparse failure
  with a warning instead of raising ValueError; governed denies still fail
  closed via raise_if_post_model_blocked.
- Add tests covering each path.

Signed-off-by: Prayag Upadhyay <prayag.upd@gmail.com>
Build inside the try so _finish_session runs on failure, matching
_execution_start. Add a regression test; document final_hook and request_id.

Signed-off-by: Prayag Upadhyay <prayag.upd@gmail.com>
@prayagupa
prayagupa force-pushed the feat/agent-hooks-control-engine branch from 229e681 to 6610681 Compare July 31, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant