Skip to content

Commit 0947b43

Browse files
feat(adk): migrate to REASONING events and add encrypted value support (#1411)
* feat(adk): migrate from deprecated THINKING events to REASONING events and add encrypted value support Migrates the ADK middleware from deprecated THINKING_* events to the newer REASONING_* event types, and adds support for thought signatures via REASONING_ENCRYPTED_VALUE events. This brings the ADK integration in line with the Claude Agent SDK and LangGraph integrations. Changes: - Replace ThinkingStart/End and ThinkingTextMessage* events with ReasoningStart/End and ReasoningMessage* events - Extract thought_signature from Google GenAI SDK Part objects and emit as REASONING_ENCRYPTED_VALUE events (base64-encoded) - Update all internal state tracking variables (_is_thinking -> _is_reasoning, etc.) - Update comprehensive tests and integration tests to expect REASONING events - Add new tests for thought signature / encrypted value handling Closes #1406 https://claude.ai/code/session_01Tn74iGdv36gg9zys3yQaAz * test(adk): add e2e tests for reasoning message_id consistency, role, encrypted values, and stream ordering Adds 5 new integration tests (requiring GOOGLE_API_KEY) that validate: - All reasoning events in a block share the same message_id - REASONING_MESSAGE_START carries role="reasoning" - REASONING_ENCRYPTED_VALUE events contain valid base64 thought signatures with correct entity_id and subtype - Reasoning stream is fully closed before text message events begin https://claude.ai/code/session_01Tn74iGdv36gg9zys3yQaAz * fix(adk): fix e2e tests for multi-block reasoning and require reasoning events - Reasoning events are now required (not optional) since gemini-2.5-flash with include_thoughts=True always produces them - Fix message_id consistency test to validate per-block rather than globally, since streaming can produce multiple reasoning blocks when thought/text parts interleave across partial events - Replace stream-ordering test with well-formedness check that validates each block is properly opened/closed (START/END balanced, no overlap) - Add helper _get_reasoning_blocks() to extract START-to-END blocks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(adk): add reasoning chat example and update changelog for #1406 - Add agentic_chat_reasoning example agent using Gemini 2.5 Flash with include_thoughts=True, registered at /adk-reasoning-chat - Update CHANGELOG.md unreleased section to document all changes: migration from THINKING to REASONING events, encrypted value support, and the new example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d70978d commit 0947b43

7 files changed

Lines changed: 531 additions & 230 deletions

File tree

integrations/adk-middleware/python/CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **BREAKING**: Migrate from deprecated `THINKING_*` events to `REASONING_*` events (#1406)
13+
- `THINKING_START` / `THINKING_END``REASONING_START` / `REASONING_END`
14+
- `THINKING_TEXT_MESSAGE_START` / `CONTENT` / `END``REASONING_MESSAGE_START` / `CONTENT` / `END`
15+
- All reasoning events now carry a `message_id` for client-side correlation and `role="reasoning"` on message start
16+
- Internal state variables renamed accordingly (`_is_thinking``_is_reasoning`, etc.)
17+
- Aligns the ADK middleware with the Claude Agent SDK and LangGraph integrations, which already use `REASONING_*` events
18+
1019
### Added
1120

21+
- **NEW**: `REASONING_ENCRYPTED_VALUE` support for Gemini thought signatures (#1406)
22+
- Extracts `thought_signature` (opaque bytes) from Google GenAI SDK `Part` objects when present
23+
- Emits `REASONING_ENCRYPTED_VALUE` events with `subtype="message"` and base64-encoded signature
24+
- Enables encrypted reasoning / zero-data-retention workflows with Gemini models
25+
26+
- **NEW**: Reasoning chat example (`examples/server/api/agentic_chat_reasoning.py`)
27+
- Demonstrates `REASONING_*` event emission using Gemini 2.5 Flash with `include_thoughts=True`
28+
- Registered at `/adk-reasoning-chat` in the example server
29+
1230
- **NEW**: Support for multimodal input types (`ImageInputContent`, `AudioInputContent`, `VideoInputContent`, `DocumentInputContent`) (#1405)
1331
- Replaces reliance on the deprecated `BinaryInputContent` with the newer modality-specific types defined in the AG-UI protocol
1432
- `InputContentDataSource` (inline base64) converts to `types.Part(inline_data=types.Blob(...))`, same as before

integrations/adk-middleware/python/examples/server/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
from .api import (
2121
agentic_chat_app,
22+
agentic_chat_reasoning_app,
2223
agentic_generative_ui_app,
2324
tool_based_generative_ui_app,
2425
human_in_the_loop_app,
@@ -37,6 +38,7 @@
3738
app.include_router(shared_state_app.router, prefix='/adk-shared-state-agent', tags=['Shared State'])
3839
app.include_router(backend_tool_rendering_app.router, prefix='/backend_tool_rendering', tags=['Backend Tool Rendering'])
3940
app.include_router(predictive_state_updates_app.router, prefix='/adk-predictive-state-agent', tags=['Predictive State Updates'])
41+
app.include_router(agentic_chat_reasoning_app.router, prefix='/adk-reasoning-chat', tags=['Agentic Chat Reasoning'])
4042

4143

4244
@app.get("/")
@@ -51,6 +53,7 @@ async def root():
5153
"shared_state": "/adk-shared-state-agent",
5254
"backend_tool_rendering": "/backend_tool_rendering",
5355
"predictive_state_updates": "/adk-predictive-state-agent",
56+
"agentic_chat_reasoning": "/adk-reasoning-chat",
5457
"docs": "/docs"
5558
}
5659
}
@@ -91,6 +94,7 @@ def main():
9194
print(f" • Human in the Loop: http://localhost:{port}/adk-human-in-loop-agent")
9295
print(f" • Shared State: http://localhost:{port}/adk-shared-state-agent")
9396
print(f" • Predictive State Updates: http://localhost:{port}/adk-predictive-state-agent")
97+
print(f" • Agentic Chat Reasoning: http://localhost:{port}/adk-reasoning-chat")
9498
print(f" • API docs: http://localhost:{port}/docs")
9599
uvicorn.run(app, host="0.0.0.0", port=port)
96100

integrations/adk-middleware/python/examples/server/api/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from .shared_state import app as shared_state_app
88
from .predictive_state_updates import app as predictive_state_updates_app
99
from .backend_tool_rendering import app as backend_tool_rendering_app
10+
from .agentic_chat_reasoning import app as agentic_chat_reasoning_app
1011

1112
__all__ = [
1213
"agentic_chat_app",
14+
"agentic_chat_reasoning_app",
1315
"agentic_generative_ui_app",
1416
"tool_based_generative_ui_app",
1517
"human_in_the_loop_app",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Agentic Chat with Reasoning (Thinking) feature.
2+
3+
Demonstrates REASONING_* events emitted when Gemini's include_thoughts
4+
is enabled, including encrypted thought signatures.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from fastapi import FastAPI
10+
from ag_ui_adk import ADKAgent, AGUIToolset, add_adk_fastapi_endpoint
11+
from google.adk.agents import LlmAgent
12+
from google.adk.planners import BuiltInPlanner
13+
from google.genai import types
14+
15+
# Create a reasoning-enabled ADK agent using Gemini 2.5 Flash
16+
reasoning_agent = LlmAgent(
17+
name="reasoning_assistant",
18+
model="gemini-2.5-flash",
19+
instruction="""You are a helpful assistant that thinks carefully before responding.
20+
Work through problems step by step in your reasoning.
21+
""",
22+
planner=BuiltInPlanner(
23+
thinking_config=types.ThinkingConfig(
24+
include_thoughts=True
25+
)
26+
),
27+
tools=[
28+
AGUIToolset(),
29+
],
30+
)
31+
32+
# Create ADK middleware agent instance
33+
chat_agent = ADKAgent(
34+
adk_agent=reasoning_agent,
35+
app_name="demo_app",
36+
user_id="demo_user",
37+
session_timeout_seconds=3600,
38+
use_in_memory_services=True,
39+
)
40+
41+
# Create FastAPI app
42+
app = FastAPI(title="ADK Middleware Reasoning Chat")
43+
44+
# Add the ADK endpoint
45+
add_adk_fastapi_endpoint(app, chat_agent, path="/")

integrations/adk-middleware/python/src/ag_ui_adk/event_translator.py

Lines changed: 95 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
ToolCallResultEvent, StateSnapshotEvent, StateDeltaEvent,
1717
CustomEvent, Message, UserMessage, AssistantMessage, ToolMessage, ReasoningMessage,
1818
ToolCall, FunctionCall,
19-
ThinkingStartEvent, ThinkingEndEvent,
20-
ThinkingTextMessageStartEvent, ThinkingTextMessageContentEvent, ThinkingTextMessageEndEvent,
19+
ReasoningStartEvent, ReasoningEndEvent,
20+
ReasoningMessageStartEvent, ReasoningMessageContentEvent, ReasoningMessageEndEvent,
21+
ReasoningEncryptedValueEvent,
2122
)
2223
import json
2324
from google.adk.events import Event as ADKEvent
@@ -51,7 +52,7 @@ def _check_thought_support() -> bool:
5152
_HAS_THOUGHT_SUPPORT = hasattr(types.Part, 'thought')
5253

5354
if _HAS_THOUGHT_SUPPORT:
54-
logger.info("Thought support detected in google-genai SDK; thoughts will be emitted as THINKING events")
55+
logger.info("Thought support detected in google-genai SDK; thoughts will be emitted as REASONING events")
5556
else:
5657
logger.info("Thought support not available in google-genai SDK; thoughts will be treated as regular text")
5758
except Exception as e:
@@ -216,10 +217,11 @@ def __init__(
216217
# in parallel (e.g. 5 concurrent create_item calls).
217218
self.lro_emitted_ids_by_name: Dict[str, List[str]] = {}
218219

219-
# Track thinking message streaming state (for thought parts)
220-
self._is_thinking: bool = False # Whether we're currently in a thinking block
221-
self._is_streaming_thinking: bool = False # Whether we're streaming thinking content
222-
self._current_thinking_text: str = "" # Accumulates thinking text for the active stream
220+
# Track reasoning message streaming state (for thought parts)
221+
self._is_reasoning: bool = False # Whether we're currently in a reasoning block
222+
self._is_streaming_reasoning: bool = False # Whether we're streaming reasoning content
223+
self._current_reasoning_text: str = "" # Accumulates reasoning text for the active stream
224+
self._current_reasoning_message_id: Optional[str] = None # Current reasoning message ID
223225

224226
# Predictive state configuration
225227
self._predict_state_mappings = normalize_predict_state(predict_state)
@@ -482,6 +484,7 @@ async def _translate_text_content(
482484
# Extract text from all parts, separating thought parts from regular text
483485
text_parts = []
484486
thought_parts = []
487+
thought_signatures: List[Optional[bytes]] = []
485488
has_thought_support = _check_thought_support()
486489

487490
# The check for adk_event.content.parts happens in the main translate method
@@ -499,18 +502,21 @@ async def _translate_text_content(
499502

500503
if is_thought:
501504
thought_parts.append(part.text)
505+
# Capture thought_signature if available (opaque bytes for encrypted reasoning)
506+
sig = getattr(part, 'thought_signature', None)
507+
thought_signatures.append(sig)
502508
else:
503509
text_parts.append(part.text)
504510

505-
# Handle thought parts first (emit THINKING events)
511+
# Handle thought parts first (emit REASONING events)
506512
if thought_parts:
507-
async for event in self._translate_thinking_content(thought_parts):
513+
async for event in self._translate_reasoning_content(thought_parts, thought_signatures):
508514
yield event
509515

510516
# If no text AND it's not a final response, we can safely skip.
511517
# Otherwise, we must continue to process the final_response signal.
512518
if not text_parts and not is_final_response:
513-
# If we only had thought parts and this is not final, close any active thinking
519+
# If we only had thought parts and this is not final, close any active reasoning
514520
# but don't return yet if we need to handle final response
515521
return
516522

@@ -523,7 +529,7 @@ async def _translate_text_content(
523529
# This is the final, complete message event.
524530

525531
# Close any active thinking stream first
526-
async for event in self._close_thinking_stream():
532+
async for event in self._close_reasoning_stream():
527533
yield event
528534

529535
# Case 1: A text stream is actively running. We must close it.
@@ -606,7 +612,7 @@ async def _translate_text_content(
606612
if not self._is_streaming:
607613
# Close any active thinking stream before starting regular text
608614
# (transition from thinking to response)
609-
async for event in self._close_thinking_stream():
615+
async for event in self._close_reasoning_stream():
610616
yield event
611617

612618
# Start of new message - emit START event
@@ -660,20 +666,25 @@ async def _translate_text_content(
660666
self._is_streaming = False
661667
logger.info("🏁 Streaming completed, state reset")
662668

663-
async def _translate_thinking_content(
669+
async def _translate_reasoning_content(
664670
self,
665-
thought_parts: List[str]
671+
thought_parts: List[str],
672+
thought_signatures: Optional[List[Optional[bytes]]] = None,
666673
) -> AsyncGenerator[BaseEvent, None]:
667-
"""Translate thought parts to AG-UI THINKING events.
674+
"""Translate thought parts to AG-UI REASONING events.
668675
669-
This method emits THINKING_START, THINKING_TEXT_MESSAGE_START/CONTENT/END,
670-
and tracks thinking state for proper stream management.
676+
This method emits REASONING_START, REASONING_MESSAGE_START/CONTENT/END,
677+
and tracks reasoning state for proper stream management. When thought_signatures
678+
are present, emits REASONING_ENCRYPTED_VALUE events for each signature.
671679
672680
Args:
673681
thought_parts: List of thought text strings to emit
682+
thought_signatures: Optional list of opaque signatures (bytes) for each
683+
thought part, used for encrypted reasoning (e.g., Gemini thought signatures).
674684
675685
Yields:
676-
Thinking events (THINKING_START, THINKING_TEXT_MESSAGE_START/CONTENT/END)
686+
Reasoning events (REASONING_START, REASONING_MESSAGE_START/CONTENT/END,
687+
REASONING_ENCRYPTED_VALUE)
677688
"""
678689
if not thought_parts:
679690
return
@@ -682,55 +693,78 @@ async def _translate_thinking_content(
682693
if not combined_thought:
683694
return
684695

685-
# Start thinking block if not already in one
686-
if not self._is_thinking:
687-
self._is_thinking = True
688-
yield ThinkingStartEvent(
689-
type=EventType.THINKING_START,
690-
title="Model Thinking"
696+
# Start reasoning block if not already in one
697+
if not self._is_reasoning:
698+
self._is_reasoning = True
699+
self._current_reasoning_message_id = str(uuid.uuid4())
700+
yield ReasoningStartEvent(
701+
type=EventType.REASONING_START,
702+
message_id=self._current_reasoning_message_id,
691703
)
692-
logger.debug("🧠 Started thinking block")
693-
694-
# Start thinking text message if not already streaming
695-
if not self._is_streaming_thinking:
696-
self._is_streaming_thinking = True
697-
self._current_thinking_text = ""
698-
yield ThinkingTextMessageStartEvent(
699-
type=EventType.THINKING_TEXT_MESSAGE_START
704+
logger.debug("🧠 Started reasoning block")
705+
706+
# Start reasoning message if not already streaming
707+
if not self._is_streaming_reasoning:
708+
self._is_streaming_reasoning = True
709+
self._current_reasoning_text = ""
710+
if not self._current_reasoning_message_id:
711+
self._current_reasoning_message_id = str(uuid.uuid4())
712+
yield ReasoningMessageStartEvent(
713+
type=EventType.REASONING_MESSAGE_START,
714+
message_id=self._current_reasoning_message_id,
715+
role="reasoning",
700716
)
701-
logger.debug("🧠 Started thinking text message")
702-
703-
# Emit thinking content
704-
self._current_thinking_text += combined_thought
705-
yield ThinkingTextMessageContentEvent(
706-
type=EventType.THINKING_TEXT_MESSAGE_CONTENT,
707-
delta=combined_thought
717+
logger.debug("🧠 Started reasoning message")
718+
719+
# Emit reasoning content
720+
self._current_reasoning_text += combined_thought
721+
yield ReasoningMessageContentEvent(
722+
type=EventType.REASONING_MESSAGE_CONTENT,
723+
message_id=self._current_reasoning_message_id,
724+
delta=combined_thought,
708725
)
709-
logger.debug(f"🧠 Emitted thinking content: {len(combined_thought)} chars")
726+
logger.debug(f"🧠 Emitted reasoning content: {len(combined_thought)} chars")
727+
728+
# Emit encrypted value events for thought signatures
729+
if thought_signatures and self._current_reasoning_message_id:
730+
import base64
731+
for sig in thought_signatures:
732+
if sig is not None:
733+
encrypted_value = base64.b64encode(sig).decode("ascii") if isinstance(sig, (bytes, bytearray)) else str(sig)
734+
yield ReasoningEncryptedValueEvent(
735+
type=EventType.REASONING_ENCRYPTED_VALUE,
736+
subtype="message",
737+
entity_id=self._current_reasoning_message_id,
738+
encrypted_value=encrypted_value,
739+
)
740+
logger.debug("🧠 Emitted reasoning encrypted value (thought signature)")
710741

711-
async def _close_thinking_stream(self) -> AsyncGenerator[BaseEvent, None]:
712-
"""Close any active thinking stream.
742+
async def _close_reasoning_stream(self) -> AsyncGenerator[BaseEvent, None]:
743+
"""Close any active reasoning stream.
713744
714-
This should be called when transitioning from thinking to regular output,
745+
This should be called when transitioning from reasoning to regular output,
715746
or when the response is finalized.
716747
717748
Yields:
718-
THINKING_TEXT_MESSAGE_END and THINKING_END events if needed
749+
REASONING_MESSAGE_END and REASONING_END events if needed
719750
"""
720-
if self._is_streaming_thinking:
721-
yield ThinkingTextMessageEndEvent(
722-
type=EventType.THINKING_TEXT_MESSAGE_END
751+
if self._is_streaming_reasoning:
752+
yield ReasoningMessageEndEvent(
753+
type=EventType.REASONING_MESSAGE_END,
754+
message_id=self._current_reasoning_message_id or "",
723755
)
724-
self._is_streaming_thinking = False
725-
self._current_thinking_text = ""
726-
logger.debug("🧠 Closed thinking text message")
727-
728-
if self._is_thinking:
729-
yield ThinkingEndEvent(
730-
type=EventType.THINKING_END
756+
self._is_streaming_reasoning = False
757+
self._current_reasoning_text = ""
758+
logger.debug("🧠 Closed reasoning message")
759+
760+
if self._is_reasoning:
761+
yield ReasoningEndEvent(
762+
type=EventType.REASONING_END,
763+
message_id=self._current_reasoning_message_id or "",
731764
)
732-
self._is_thinking = False
733-
logger.debug("🧠 Closed thinking block")
765+
self._is_reasoning = False
766+
self._current_reasoning_message_id = None
767+
logger.debug("🧠 Closed reasoning block")
734768

735769
async def translate_lro_function_calls(self,adk_event: ADKEvent)-> AsyncGenerator[BaseEvent, None]:
736770
"""Translate long running function calls from ADK event to AG-UI tool call events.
@@ -1164,10 +1198,11 @@ def reset(self):
11641198
self._emitted_confirm_for_tools.clear()
11651199
self._predictive_state_tool_call_ids.clear()
11661200
self._deferred_confirm_events.clear()
1167-
# Reset thinking state
1168-
self._is_thinking = False
1169-
self._is_streaming_thinking = False
1170-
self._current_thinking_text = ""
1201+
# Reset reasoning state
1202+
self._is_reasoning = False
1203+
self._is_streaming_reasoning = False
1204+
self._current_reasoning_text = ""
1205+
self._current_reasoning_message_id = None
11711206
# Reset streaming FC args state
11721207
self._active_streaming_fc_id = None
11731208
self._active_streaming_fc_name = None

0 commit comments

Comments
 (0)