Skip to content

hooks

strands_teams.hooks

Generic HookProviders for agent guidance, quality control, and display.

Ported from the pre-split code_context_agent.agent.hooks with every domain literal replaced by :class:~strands_teams.usecase.UseCase-supplied data. The eight providers here know nothing about any specific domain: reasoning prompts, shell alternatives, fail-fast exemptions, artifact paths, and the scoring rubric all arrive through constructor parameters (fed by :func:create_all_hooks from a UseCase), so the framework never hardcodes a tool name or artifact scheme.

Uses the stable strands.hooks API (verified against strands-agents 1.47.0):

  • BeforeInvocationEvent / AfterInvocationEvent — invocation lifecycle. AfterInvocationEvent.resume is the single writable field (_can_write returns True only for "resume"); setting it to a non-None agent input triggers a full re-invocation cycle. This mechanism is unchanged from the port source, so :class:ArtifactQualityHook uses it directly (AC-3-3, AC-3-6).
  • BeforeToolCallEvent / AfterToolCallEvent — per-tool hooks.

Display coupling (T-C3 anti-goal)

The concrete display state lives in the consumer/display layer (a separate task). To avoid a hard dependency in the wrong direction, :class:ToolDisplayHook and :class:TeamDispatchHook accept any object satisfying the duck-typed :class:TeamDisplayState Protocol (start_team / complete_team / record_tool). The framework never imports the concrete state class.

TeamDisplayState

Bases: Protocol

Duck-typed contract the display hooks require of a display-state object.

The concrete state class lives in the consumer/display layer (a separate task). Depending on this Protocol rather than the concrete class keeps the framework free of a dependency on its own consumer.

start_team

start_team(team_id, mandate, agent_count)

Record that a team was dispatched.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def start_team(self, team_id: str, mandate: str, agent_count: int) -> None:
    """Record that a team was dispatched."""
    ...

complete_team

complete_team(team_id, *, status)

Record that a dispatched team finished.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def complete_team(self, team_id: str, *, status: str) -> None:
    """Record that a dispatched team finished."""
    ...

record_tool

record_tool(*, tool_id, tool_name, status, result)

Record a completed tool call for display.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def record_tool(self, *, tool_id: str, tool_name: str, status: str, result: str) -> None:
    """Record a completed tool call for display."""
    ...

FullModeToolError

FullModeToolError(tool_name, error_message)

Bases: RuntimeError

Raised by :class:FailFastHook when a critical tool returns an error.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, tool_name: str, error_message: str) -> None:  # noqa: D107
    self.tool_name = tool_name
    super().__init__(f"Tool '{tool_name}' failed: {error_message}")

OutputQualityHook

Bases: HookProvider

Logs a warning when a tool produces unusually large output.

register_hooks

register_hooks(registry, **kwargs)

Register the output-quality callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the output-quality callback."""
    registry.add_callback(AfterToolCallEvent, self._check_output_quality)

ConversationCompactionHook

Bases: HookProvider

Strips large toolUse/toolResult payloads from older conversation history.

After the model has reasoned about tool results, the raw payloads are no longer needed. Before each model invocation this hook replaces large tool payloads in older messages with stubs, keeping all reasoning turns and the most recent messages intact — preventing context overflow without ever truncating data the model has not yet seen.

register_hooks

register_hooks(registry, **kwargs)

Register the conversation-compaction callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the conversation-compaction callback."""
    registry.add_callback(BeforeInvocationEvent, self._compact_history)

ToolEfficiencyHook

ToolEfficiencyHook(alternatives)

Bases: HookProvider

Warns when shell is used for a task that has a dedicated tool.

The mapping of shell-command substring -> suggested alternative is supplied by the use case (alternatives), so the framework holds no domain literals.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, alternatives: Mapping[str, str]) -> None:  # noqa: D107
    self._alternatives = alternatives

register_hooks

register_hooks(registry, **kwargs)

Register the tool-efficiency callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the tool-efficiency callback."""
    registry.add_callback(BeforeToolCallEvent, self._check_tool_efficiency)

ReasoningCheckpointHook

ReasoningCheckpointHook(prompts)

Bases: HookProvider

Appends a reasoning-checkpoint prompt to results of mapped analysis tools.

Only tools present in the prompts map (supplied by the use case) get a checkpoint appended; every other tool's result passes through unchanged (AC-3-2). Forces the model to interpret combined signals rather than merely collecting tool outputs.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, prompts: Mapping[str, str]) -> None:  # noqa: D107
    self._prompts = prompts

register_hooks

register_hooks(registry, **kwargs)

Register the reasoning-checkpoint callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the reasoning-checkpoint callback."""
    registry.add_callback(AfterToolCallEvent, self._inject_reasoning_prompt)

FailFastHook

FailFastHook(exempt, exempt_prefixes=())

Bases: HookProvider

Raises :class:FullModeToolError when a non-exempt tool errors (full mode).

In full mode, most tool failures should halt the run rather than silently degrade. The exempt tool-name set and exempt name-prefixes both come from the use case (search tools may return no results; external MCP tools may be unavailable), so the framework holds no domain literals (AC-3-4).

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, exempt: frozenset[str], exempt_prefixes: tuple[str, ...] = ()) -> None:  # noqa: D107
    self._exempt = exempt
    self._exempt_prefixes = exempt_prefixes

register_hooks

register_hooks(registry, **kwargs)

Register the fail-fast callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the fail-fast callback."""
    registry.add_callback(AfterToolCallEvent, self._check_for_error)

ArtifactQualityHook

ArtifactQualityHook(
    layout,
    rubric,
    max_passes=2,
    resume_template=DEFAULT_RESUME_TEMPLATE,
    coverage_resume_template=DEFAULT_COVERAGE_RESUME_TEMPLATE,
)

Bases: HookProvider

Triggers revision passes on weak rubric scores OR uncovered acceptance criteria.

After the coordinator finishes an invocation this hook applies two gates (AC-4-1):

  1. Rubric. Scores every artifact under layout.artifacts_dir() (matched by layout.artifact_glob()) with the shared rubric.score. An artifact below the rubric's revision threshold is "weak".
  2. Acceptance-criteria coverage. Reads every dispatched team's acceptance_criteria from its contract.json (written by dispatch_team) and checks each criterion against the corpus of produced artifacts + team findings. A criterion whose significant tokens are mostly absent from that corpus is "uncovered".

If any artifact is weak OR any criterion is uncovered, the run needs revision and event.resume (verified writable on AfterInvocationEvent in strands 1.47.0) is set to re-invoke the coordinator. An uncovered criterion forces a revision regardless of the rubric total (AC-4-2) and the emitted resume prompt names the uncovered criteria (AC-4-3). When all criteria are covered and no artifact is weak, no resume is emitted (AC-4-4). When no acceptance criteria were supplied for the run, the hook falls back to unchanged rubric-only behavior (AC-4-5). The single max_passes budget caps both the rubric- and coverage-driven resumes combined (AC-4-6).

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(  # noqa: D107
    self,
    layout: TeamsLayout,
    rubric: NarrativeRubric,
    max_passes: int = 2,
    resume_template: str = DEFAULT_RESUME_TEMPLATE,
    coverage_resume_template: str = DEFAULT_COVERAGE_RESUME_TEMPLATE,
) -> None:
    self._layout = layout
    self._rubric = rubric
    self._max_passes = max_passes
    self._resume_template = resume_template
    self._coverage_resume_template = coverage_resume_template
    self._pass_count = 0

register_hooks

register_hooks(registry, **kwargs)

Register the artifact-quality callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the artifact-quality callback."""
    registry.add_callback(AfterInvocationEvent, self._check_artifact_quality)

ToolDisplayHook

ToolDisplayHook(state)

Bases: HookProvider

Tracks tool calls on a display state for TUI rendering.

Depends on the duck-typed :class:TeamDisplayState Protocol rather than a concrete state class so the framework does not import its own consumer.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, state: TeamDisplayState) -> None:  # noqa: D107
    self._state = state

register_hooks

register_hooks(registry, **kwargs)

Register the tool-display callbacks.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the tool-display callbacks."""
    registry.add_callback(BeforeToolCallEvent, self._on_tool_start)
    registry.add_callback(AfterToolCallEvent, self._on_tool_end)

TeamDispatchHook

TeamDispatchHook(state)

Bases: HookProvider

Tracks dispatch_team tool calls on a display state.

dispatch_team is a framework-level core tool (not a domain literal), so the name is fixed. Depends on the duck-typed :class:TeamDisplayState.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, state: TeamDisplayState) -> None:  # noqa: D107
    self._state = state

register_hooks

register_hooks(registry, **kwargs)

Register the team-dispatch callbacks.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the team-dispatch callbacks."""
    registry.add_callback(BeforeToolCallEvent, self._on_dispatch_start)
    registry.add_callback(AfterToolCallEvent, self._on_dispatch_end)

JsonLogHook

JsonLogHook()

Bases: HookProvider

Emits one structured JSON log line per tool start/end for --quiet mode.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self) -> None:  # noqa: D107
    self._json_logger = logger.bind(output="json")

register_hooks

register_hooks(registry, **kwargs)

Register the JSON-log callbacks.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the JSON-log callbacks."""
    registry.add_callback(BeforeToolCallEvent, self._on_tool_start)
    registry.add_callback(AfterToolCallEvent, self._on_tool_end)

PolicyGateDenied

PolicyGateDenied(tool_name, verdict)

Bases: RuntimeError

Raised by :class:PolicyGateHook to block a denied consequential tool call (AC-6-4).

Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(self, tool_name: str, verdict: Any) -> None:  # noqa: D107
    self.tool_name = tool_name
    self.verdict = verdict
    gate = getattr(verdict, "failed_gate", None)
    reason = getattr(verdict, "reason", "")
    super().__init__(f"Policy gate '{gate}' blocked tool '{tool_name}': {reason}")

PolicyGateHook

PolicyGateHook(
    engine,
    consequential_tools,
    *,
    actor_of=None,
    input_mapper=None,
)

Bases: HookProvider

Structurally blocks a denied consequential tool call before it runs (AC-6-4, §14.7).

On BeforeToolCallEvent for a tool whose name is in consequential_tools, the hook builds a :class:~strands_teams.models.TransitionRequest from the tool input and the current actor, calls :meth:~strands_teams.policy.PolicyEngine.authorize_transition (reading grants, approvals, findings, and revision hashes from the event's invocation_state by reference — AC-3-7), and raises :class:PolicyGateDenied if the verdict is allowed=False — so the action never executes without a valid revision-bound approval and a clear blocking-finding slate (AC-6-2, AC-6-3). Non-consequential tools pass straight through; the engine is present but, on a run that requests no consequential transitions, is never invoked to deny (AC-7-5).

The transition kind, target revision, and presented tokens are read from the tool input under conventional keys (transition_kind/kind, target_revision_id, presented_tokens, consequential); a use case that names them differently supplies an input_mapper callable.

Wire the gate to a policy engine and the set of consequential tools.

Parameters:

Name Type Description Default
engine Any

The :class:~strands_teams.policy.PolicyEngine that issues verdicts.

required
consequential_tools frozenset[str]

Tool names whose calls are gated.

required
actor_of Any

Optional callable mapping invocation_state to the actor id.

None
input_mapper Any

Optional callable (tool_name, args, invocation_state) -> TransitionRequest for use cases that name inputs differently.

None
Source code in packages/strands-teams/src/strands_teams/hooks.py
def __init__(
    self,
    engine: Any,
    consequential_tools: frozenset[str],
    *,
    actor_of: Any = None,
    input_mapper: Any = None,
) -> None:
    """Wire the gate to a policy engine and the set of consequential tools.

    Args:
        engine: The :class:`~strands_teams.policy.PolicyEngine` that issues verdicts.
        consequential_tools: Tool names whose calls are gated.
        actor_of: Optional callable mapping ``invocation_state`` to the actor id.
        input_mapper: Optional callable ``(tool_name, args, invocation_state)`` ->
            ``TransitionRequest`` for use cases that name inputs differently.
    """
    self._engine = engine
    self._consequential = consequential_tools
    self._actor_of = actor_of
    self._input_mapper = input_mapper

register_hooks

register_hooks(registry, **kwargs)

Register the pre-tool authorization callback.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def register_hooks(self, registry: HookRegistry, **kwargs: Any) -> None:
    """Register the pre-tool authorization callback."""
    registry.add_callback(BeforeToolCallEvent, self._authorize)

create_all_hooks

create_all_hooks(
    use_case,
    *,
    full_mode=False,
    state=None,
    quiet=False,
    policy_engine=None,
    consequential_tools=None,
)

Assemble the hook providers for a run from a UseCase (AC-3-5).

Always includes: :class:ConversationCompactionHook, :class:OutputQualityHook, :class:ToolEfficiencyHook, :class:ReasoningCheckpointHook, and :class:ArtifactQualityHook. Adds :class:FailFastHook iff full_mode. For display: :class:JsonLogHook iff quiet; otherwise :class:ToolDisplayHook + :class:TeamDispatchHook iff state is provided.

The reasoning prompts, fail-fast exempt set, artifact layout, and rubric come from the use-case contract members (reasoning_prompts, fail_fast_exempt, layout, rubric). Two knobs are NOT part of the UseCase contract and are read optionally via getattr so a use case may supply them without the framework requiring them: shell_alternatives (mapping, default empty) and fail_fast_exempt_prefixes (tuple, default empty).

Parameters:

Name Type Description Default
use_case Any

The use case supplying reasoning prompts, exemptions, layout, and rubric.

required
full_mode bool

If True, include :class:FailFastHook.

False
state TeamDisplayState | None

A display state (duck-typed :class:TeamDisplayState). Ignored when quiet is True.

None
quiet bool

If True, use :class:JsonLogHook instead of the display hooks.

False
policy_engine Any

Optional :class:~strands_teams.policy.PolicyEngine; when given together with consequential_tools, a :class:PolicyGateHook is added to block denied consequential tool calls before they run.

None
consequential_tools frozenset[str] | None

Tool names gated by the policy engine (AC-6-4).

None

Returns:

Type Description
list[HookProvider]

A flat list of :class:HookProvider instances to register on the agent.

Source code in packages/strands-teams/src/strands_teams/hooks.py
def create_all_hooks(
    use_case: Any,
    *,
    full_mode: bool = False,
    state: TeamDisplayState | None = None,
    quiet: bool = False,
    policy_engine: Any = None,
    consequential_tools: frozenset[str] | None = None,
) -> list[HookProvider]:
    """Assemble the hook providers for a run from a ``UseCase`` (AC-3-5).

    Always includes: :class:`ConversationCompactionHook`, :class:`OutputQualityHook`,
    :class:`ToolEfficiencyHook`, :class:`ReasoningCheckpointHook`, and
    :class:`ArtifactQualityHook`. Adds :class:`FailFastHook` iff ``full_mode``.
    For display: :class:`JsonLogHook` iff ``quiet``; otherwise
    :class:`ToolDisplayHook` + :class:`TeamDispatchHook` iff ``state`` is provided.

    The reasoning prompts, fail-fast exempt set, artifact layout, and rubric come
    from the use-case contract members (``reasoning_prompts``, ``fail_fast_exempt``,
    ``layout``, ``rubric``). Two knobs are NOT part of the ``UseCase`` contract and
    are read optionally via ``getattr`` so a use case may supply them without the
    framework requiring them: ``shell_alternatives`` (mapping, default empty) and
    ``fail_fast_exempt_prefixes`` (tuple, default empty).

    Args:
        use_case: The use case supplying reasoning prompts, exemptions, layout,
            and rubric.
        full_mode: If True, include :class:`FailFastHook`.
        state: A display state (duck-typed :class:`TeamDisplayState`). Ignored
            when ``quiet`` is True.
        quiet: If True, use :class:`JsonLogHook` instead of the display hooks.
        policy_engine: Optional :class:`~strands_teams.policy.PolicyEngine`; when
            given together with ``consequential_tools``, a :class:`PolicyGateHook`
            is added to block denied consequential tool calls before they run.
        consequential_tools: Tool names gated by the policy engine (AC-6-4).

    Returns:
        A flat list of :class:`HookProvider` instances to register on the agent.
    """
    shell_alternatives: Mapping[str, str] = getattr(use_case, "shell_alternatives", {})
    exempt_prefixes: tuple[str, ...] = tuple(getattr(use_case, "fail_fast_exempt_prefixes", ()))

    hooks: list[HookProvider] = [
        ConversationCompactionHook(),
        OutputQualityHook(),
        ToolEfficiencyHook(shell_alternatives),
        ReasoningCheckpointHook(use_case.reasoning_prompts),
        ArtifactQualityHook(use_case.layout, use_case.rubric),
    ]
    if full_mode:
        hooks.append(FailFastHook(use_case.fail_fast_exempt, exempt_prefixes))

    if policy_engine is not None and consequential_tools:
        hooks.append(PolicyGateHook(policy_engine, consequential_tools))

    if quiet:
        hooks.append(JsonLogHook())
    elif state is not None:
        hooks.append(ToolDisplayHook(state))
        hooks.append(TeamDispatchHook(state))

    return hooks