Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8858f9d
support redactedContent
mazyu36 May 28, 2025
4a19f45
test
mazyu36 May 28, 2025
530e2ce
fix
mazyu36 Jun 4, 2025
3af994e
Merge remote-tracking branch 'upstream/main' into feature/redactedCon…
mazyu36 Jun 4, 2025
3310114
add callback test
mazyu36 Jun 4, 2025
797c643
Merge remote-tracking branch 'strands/main' into feature/redactedContent
awsarron Jun 10, 2025
4eec5f1
format
awsarron Jun 16, 2025
1d462e7
remove unused ignore type
awsarron Jun 16, 2025
ed10796
Merge branch 'main' into feature/redactedContent
awsarron Jun 16, 2025
b75fa27
Merge branch 'main' into feature/redactedContent
afarntrog Sep 11, 2025
5a43f9e
feat: add RedactedContentStreamEvent for proper redacted content hand…
afarntrog Sep 11, 2025
f7d161c
tests
afarntrog Sep 11, 2025
8eb5abe
update RedactedContentStreamEvent to make reasoning optional with def…
afarntrog Sep 11, 2025
206b829
add redactedContent to state only when present
afarntrog Sep 12, 2025
f11b134
update event type
afarntrog Sep 12, 2025
c9c419e
Merge branch 'main' into feature/redactedContent
afarntrog Sep 12, 2025
0110789
update event type
afarntrog Sep 12, 2025
46002eb
use reasoningRedactedContent bec we will remove the reasoning param i…
afarntrog Sep 12, 2025
a1f4ea4
use reasoningRedactedContent bec we will remove the reasoning param i…
afarntrog Sep 12, 2025
d50b1d4
test updates and typed event param removed as discussed
afarntrog Sep 15, 2025
9aaf4fa
add test agent events for redacted reasoning content
afarntrog Sep 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/strands/event_loop/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ModelStreamEvent,
ReasoningSignatureStreamEvent,
ReasoningTextStreamEvent,
RedactedContentStreamEvent,
TextStreamEvent,
ToolUseStreamEvent,
TypedEvent,
Expand Down Expand Up @@ -170,6 +171,13 @@ def handle_content_block_delta(
delta=delta_content,
)

elif redacted_content := delta_content["reasoningContent"].get("redactedContent"):
state.setdefault("redactedContent", b"")
state["redactedContent"] += redacted_content
typed_event = RedactedContentStreamEvent(
redacted_content=redacted_content, delta=delta_content, reasoning=True
)

return state, typed_event


Expand All @@ -188,6 +196,7 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
text = state["text"]
reasoning_text = state["reasoningText"]
citations_content = state["citationsContent"]
redacted_content = state.get("redactedContent")

if current_tool_use:
if "input" not in current_tool_use:
Expand Down Expand Up @@ -231,6 +240,9 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:

content.append(content_block)
state["reasoningText"] = ""
elif redacted_content:
content.append({"reasoningContent": {"redactedContent": redacted_content}})
state["redactedContent"] = b""

return state

Expand Down Expand Up @@ -290,6 +302,7 @@ async def process_stream(chunks: AsyncIterable[StreamEvent]) -> AsyncGenerator[T
"current_tool_use": {},
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
}
state["content"] = state["message"]["content"]

Expand Down
8 changes: 8 additions & 0 deletions src/strands/types/_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ def __init__(self, delta: ContentBlockDelta, reasoning_text: str | None) -> None
super().__init__({"reasoningText": reasoning_text, "delta": delta, "reasoning": True})


class RedactedContentStreamEvent(ModelStreamEvent):
"""Event emitted during redacted content streaming."""

def __init__(self, delta: ContentBlockDelta, redacted_content: bytes | None, reasoning: bool = False) -> None:
"""Initialize with delta and redacted content."""
super().__init__({"redactedContent": redacted_content, "delta": delta, "reasoning": reasoning})


class ReasoningSignatureStreamEvent(ModelStreamEvent):
"""Event emitted during reasoning signature streaming."""

Expand Down
184 changes: 97 additions & 87 deletions tests/strands/event_loop/test_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ def test_handle_content_block_start(chunk: ContentBlockStartEvent, exp_tool_use)
{"signature": "val"},
{"reasoning_signature": "val", "reasoning": True},
),
# Reasoning - redactedContent - New
pytest.param(
{"delta": {"reasoningContent": {"redactedContent": b"encoded"}}},
{},
{"redactedContent": b"encoded"},
{"redactedContent": b"encoded", "reasoning": True},
),
# Reasoning - redactedContent - Existing
pytest.param(
{"delta": {"reasoningContent": {"redactedContent": b"data"}}},
{"redactedContent": b"encoded_"},
{"redactedContent": b"encoded_data"},
{"redactedContent": b"data", "reasoning": True},
),
# Reasoning - Empty
(
{"delta": {"reasoningContent": {}}},
Expand Down Expand Up @@ -167,13 +181,15 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [{"toolUse": {"toolUseId": "123", "name": "test", "input": {"key": "value"}}}],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
),
# Tool Use - Missing input
Expand All @@ -184,13 +200,15 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [{"toolUse": {"toolUseId": "123", "name": "test", "input": {}}}],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
),
# Text
Expand All @@ -201,13 +219,15 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "test",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [{"text": "test"}],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
),
# Citations
Expand All @@ -218,13 +238,15 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "",
"reasoningText": "",
"citationsContent": [{"citations": [{"text": "test", "source": "test"}]}],
"redactedContent": b"",
},
{
"content": [],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [{"citations": [{"text": "test", "source": "test"}]}],
"redactedContent": b"",
},
),
# Reasoning
Expand All @@ -236,6 +258,7 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"reasoningText": "test",
"signature": "123",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [{"reasoningContent": {"reasoningText": {"text": "test", "signature": "123"}}}],
Expand All @@ -244,6 +267,7 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"reasoningText": "",
"signature": "123",
"citationsContent": [],
"redactedContent": b"",
},
),
# Reasoning without signature
Expand All @@ -254,13 +278,34 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "",
"reasoningText": "test",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [{"reasoningContent": {"reasoningText": {"text": "test"}}}],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
),
# redactedContent
(
{
"content": [],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"redactedContent": b"encoded_data",
"citationsContent": [],
},
{
"content": [{"reasoningContent": {"redactedContent": b"encoded_data"}}],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"redactedContent": b"",
"citationsContent": [],
},
),
# Empty
Expand All @@ -271,13 +316,15 @@ def test_handle_content_block_delta(event: ContentBlockDeltaEvent, state, exp_up
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
{
"content": [],
"current_tool_use": {},
"text": "",
"reasoningText": "",
"citationsContent": [],
"redactedContent": b"",
},
),
],
Expand Down Expand Up @@ -449,25 +496,61 @@ def test_extract_usage_metrics_with_cache_tokens():
},
],
),
],
)
@pytest.mark.asyncio
async def test_process_stream(response, exp_events, agenerator, alist):
stream = strands.event_loop.streaming.process_stream(agenerator(response))

tru_events = await alist(stream)
assert tru_events == exp_events

# Ensure that we're getting typed events coming out of process_stream
non_typed_events = [event for event in tru_events if not isinstance(event, TypedEvent)]
assert non_typed_events == []


@pytest.mark.parametrize(
"response",
[
# Redacted Message
(
[
{"messageStart": {"role": "assistant"}},
{
"contentBlockStart": {"start": {}},
},
{
"contentBlockDelta": {"delta": {"text": "Hello!"}},
},
{"contentBlockStop": {}},
{
"messageStop": {"stopReason": "guardrail_intervened"},
},
{
"redactContent": {
"redactUserContentMessage": "REDACTED",
"redactAssistantContentMessage": "REDACTED.",
}
},
{
"metadata": {
"usage": {"inputTokens": 1, "outputTokens": 1, "totalTokens": 1},
"metrics": {"latencyMs": 1},
}
},
],
pytest.param(
[
{"messageStart": {"role": "assistant"}},
{
"contentBlockStart": {"start": {}},
},
{
"contentBlockDelta": {"delta": {"text": "Hello!"}},
"contentBlockDelta": {"delta": {"reasoningContent": {"redactedContent": b"encoded_data"}}},
},
{"contentBlockStop": {}},
{
"messageStop": {"stopReason": "guardrail_intervened"},
},
{
"redactContent": {
"redactUserContentMessage": "REDACTED",
"redactAssistantContentMessage": "REDACTED.",
}
"messageStop": {"stopReason": "end_turn"},
},
{
"metadata": {
Expand All @@ -476,91 +559,18 @@ def test_extract_usage_metrics_with_cache_tokens():
}
},
],
[
{
"event": {
"messageStart": {
"role": "assistant",
},
},
},
{
"event": {
"contentBlockStart": {
"start": {},
},
},
},
{
"event": {
"contentBlockDelta": {
"delta": {
"text": "Hello!",
},
},
},
},
{
"data": "Hello!",
"delta": {
"text": "Hello!",
},
},
{
"event": {
"contentBlockStop": {},
},
},
{
"event": {
"messageStop": {
"stopReason": "guardrail_intervened",
},
},
},
{
"event": {
"redactContent": {
"redactAssistantContentMessage": "REDACTED.",
"redactUserContentMessage": "REDACTED",
},
},
},
{
"event": {
"metadata": {
"metrics": {
"latencyMs": 1,
},
"usage": {
"inputTokens": 1,
"outputTokens": 1,
"totalTokens": 1,
},
},
},
},
{
"stop": (
"guardrail_intervened",
{
"role": "assistant",
"content": [{"text": "REDACTED."}],
},
{"inputTokens": 1, "outputTokens": 1, "totalTokens": 1},
{"latencyMs": 1},
),
},
],
marks=pytest.mark.skip(reason="Implementation has undefined callback_handler"),
),
],
)
@pytest.mark.asyncio
async def test_process_stream(response, exp_events, agenerator, alist):
async def test_process_stream_redacted(response, agenerator, alist):
stream = strands.event_loop.streaming.process_stream(agenerator(response))

tru_events = await alist(stream)
assert tru_events == exp_events

# Verify the structure matches expected redacted content behavior
assert len(tru_events) > 0

# Ensure that we're getting typed events coming out of process_stream
non_typed_events = [event for event in tru_events if not isinstance(event, TypedEvent)]
Expand Down
Loading
Loading