Skip to content

[FEATURE] Include full metadata in hook events #4015

Description

@kevindew

SDK Language

Python

Problem Statement

I want to monitor the results of Bedrock Guardrails in Hooks when I'm using Bedrock Converse API as a model, so I can change invocation/request state with information about which guardrail was triggered and/or whether non-blocking guardrails were triggered

Proposed Solution

Currently, when us the only way to know which guardrail is triggered is to set a callback and listen for a metadata event, which contains the full metadata from the agent invocation. For example:

{'event': {'metadata': {'usage': {'inputTokens': 67, 'outputTokens': 386, 'totalTokens': 453}, 'metrics': {'latencyMs': 6668}, 'trace': {'guardrail': {'inputAssessment': {'<id>': {'invocationMetrics': {'guardrailProcessingLatency': 132, 'usage': {'topicPolicyUnits': 0, 'contentPolicyUnits': 0, 'wordPolicyUnits': 0, 'sensitiveInformationPolicyUnits': 0, 'sensitiveInformationPolicyFreeUnits': 0, 'contextualGroundingPolicyUnits': 0}, 'guardrailCoverage': {'textCharacters': {'guarded': 77, 'total': 206}}}, 'appliedGuardrailDetails': {'guardrailId': '<id>', 'guardrailVersion': '4', 'guardrailArn': '<arn>', 'guardrailOrigin': ['REQUEST'], 'guardrailOwnership': 'SELF'}}}, 'outputAssessments': {'<id>': [{'topicPolicy': {'topics': [{'name': 'Personalised Financial Advice', 'type': 'DENY', 'action': 'NONE', 'detected': True}, {'name': 'Negative comments about government', 'type': 'DENY', 'action': 'NONE', 'detected': False}, {'name': 'Encouraging Skirting or Ignoring Laws or Regulations', 'type': 'DENY', 'action': 'NONE', 'detected': False}]}, 'contentPolicy': {'filters': [{'type': 'SEXUAL', 'confidence': 'NONE', 'filterStrength': 'HIGH', 'action': 'NONE', 'detected': False}, {'type': 'VIOLENCE', 'confidence': 'NONE', 'filterStrength': 'HIGH', 'action': 'NONE', 'detected': False}, {'type': 'HATE', 'confidence': 'NONE', 'filterStrength': 'HIGH', 'action': 'NONE', 'detected': False}, {'type': 'INSULTS', 'confidence': 'NONE', 'filterStrength': 'HIGH', 'action': 'NONE', 'detected': False}, {'type': 'MISCONDUCT', 'confidence': 'NONE', 'filterStrength': 'HIGH', 'action': 'NONE', 'detected': False}]}, 'sensitiveInformationPolicy': {'piiEntities': [{'match': '', 'type': 'AGE', 'action': 'NONE', 'detected': False}]}, 'invocationMetrics': {'guardrailProcessingLatency': 475, 'usage': {'topicPolicyUnits': 3, 'contentPolicyUnits': 3, 'wordPolicyUnits': 0, 'sensitiveInformationPolicyUnits': 3, 'sensitiveInformationPolicyFreeUnits': 0, 'contextualGroundingPolicyUnits': 0}, 'guardrailCoverage': {'textCharacters': {'guarded': 1074, 'total': 1074}}}, 'appliedGuardrailDetails': {'guardrailId': '<id>', 'guardrailVersion': '4', 'guardrailArn': '<arn>', 'guardrailOrigin': ['REQUEST'], 'guardrailOwnership': 'SELF'}}]}, 'actionReason': 'No action.'}}}}}

Which is triggered from:

# Yield metadata event
if "usage" in response or "metrics" in response or "trace" in response:
metadata: StreamEvent = {"metadata": {}}
if "usage" in response:
metadata["metadata"]["usage"] = response["usage"]
if "metrics" in response:
metadata["metadata"]["metrics"] = response["metrics"]
if "trace" in response:
metadata["metadata"]["trace"] = response["trace"]
yield metadata

At the point of triggering hooks the trace metadata is stripped so hooks do not have this information:

yield ModelStopReason(stop_reason=stop_reason, message=state["message"], usage=usage, metrics=metrics)
as just usage and metrics has been extracted leaving traces behind.

It is then not available in AfterModelCallEvent.ModelStopResponse message attribute, despite the type signature being suitable for arbitrary data:

class MessageMetadata(TypedDict, total=False):
"""Optional metadata attached to a message.
Not sent to model providers — explicitly stripped before model calls.
Persisted alongside the message in session storage.
Attributes:
usage: Token usage information from the model response.
metrics: Performance metrics from the model response.
custom: Arbitrary user/framework metadata (e.g. compression provenance).
"""
usage: Usage
metrics: Metrics
custom: dict[str, Any]

Use Case

This would be an approach to allow changing invocation state, or other activities, in hooks working with the cleaner interface a hook gives compared to events and easier ability to interact with the agent

Alternatives Solutions

The approach we're using to work around this is using global agent state or global variables, that we need to remember to reset and putting a callback on strands e.g:

def make_guardrail_callback(agent: Agent):
    def handler(**kwargs: Any) -> None:
        chunk = kwargs.get("event")
        if not chunk:
            return

        guardrail = chunk.get("metadata", {}).get("trace", {}).get("guardrail")
        if not guardrail:
            return

        assessments = extract_guardrail_assessments(guardrail)
        if not assessments:
            return

        agent.state.set("guardrail_triggered", True)
        existing = agent.state.get("assessments") or []
        existing.extend(assessments)
        agent.state.set("assessments", existing)

    return handler

Additional Context

No response

Activity

  1. added
    area-hooksFeatures or requests that might be implementable via hooks
    pythonPull requests that update python code
    on Aug 27, 2026
  2. lifelmy commented on Aug 31, 2026

    @lifelmy
    Contributor

    I opened a draft PR that preserves the trace portion of the model metadata event so it reaches the assistant message metadata and is available in AfterModelCallEvent.ModelStopResponse.message: #4086.

    Approach: capture trace in process_stream and attach it to the assistant message metadata (also added a trace field to MessageMetadata). The ModelStopReason tuple shape is left unchanged to avoid touching its many consumers, and the key is only added when the provider actually emits a trace. Added regression tests asserting the trace surfaces on the assistant message and that no trace key is added when absent.

    I use a coding assistant to help implement, and I review and take responsibility for the final change. Happy to adjust the shape/placement if maintainers prefer a different surface.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-hooksFeatures or requests that might be implementable via hooksarea-interventionsRelated to interventionsenhancementNew feature or requestpythonPull requests that update python code

    Fields

    Language

    Python

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions