Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
d321ebe
docs: design system integration guide, custom components guide, YouTu…
Mar 12, 2026
251bfee
Apply suggestions from code review
zeroasterisk Mar 12, 2026
2b64f9b
docs: address PR #824 review comments
Mar 12, 2026
50b3fd7
docs: add render_macros:false to prevent Jinja2 eval of Angular templ…
Mar 12, 2026
3838c78
feat(dojo): Add A2UI Dojo and Mock Scenarios
Mar 14, 2026
718ea73
feat(dojo): implement comprehensive visual design and layout polish f…
Mar 14, 2026
95600e3
fix(composer): remove edge runtime to fix Next.js build errors, prefe…
Mar 15, 2026
147afce
feat(dojo): Add scenario harvest, mobile layout, and UX evaluations
Mar 15, 2026
9989f0b
fix(composer): remove opennextjs-cloudflare dependency for Vercel dep…
Mar 15, 2026
d183d8c
fix(dojo): fix progress timeline and start renderers in empty state
Mar 15, 2026
df20f4d
feat(dojo): enable URL parameter deep linking for scenarios and timel…
Mar 15, 2026
f1ef63e
feat: wire up A2UIViewer to dojo scrubber stream via useA2UISurface hook
Mar 15, 2026
ceb44fb
feat(dojo): polish timeline scrubber, sync A2UI transcoder and adjust…
Mar 15, 2026
01d85f3
feat(dojo): fix A2UISurface hook compatibility for React renderer and…
Mar 15, 2026
6ab9f5a
feat(dojo): improve visual feedback for timeline and scrubber messages
Mar 15, 2026
27d8096
feat(dojo): integrate northstar-tour scenario, remove CopilotKit, and…
Mar 15, 2026
2c7de58
fix(dojo): use real v0.8 sample scenarios and fix renderer pipeline
Mar 16, 2026
649c780
fix(dojo): use @copilotkit/a2ui-renderer instead of local @a2ui/react
Mar 16, 2026
d4dcbd6
feat(dojo): single renderer pane, step summaries, hide Discord mock
Mar 16, 2026
1691d94
fix(dojo): remove broken v0.9 scenarios that crash renderer
Mar 16, 2026
c2a7a00
fix(dojo): use standard catalog for rizzcharts-chart scenario so it r…
Mar 16, 2026
4d53e30
fix(dojo): remove h-full overflow-hidden from renderer frame so conte…
Mar 16, 2026
3fe357c
fix(dojo): add missing confirmation-column component to restaurant-co…
Mar 16, 2026
8c8c654
dojo: hide mock browser chrome traffic light dots on mobile viewport
Mar 16, 2026
30b001e
fix(dojo): set copilotkit route to force-dynamic to fix 500 on page load
Mar 16, 2026
b0f2983
feat(dojo): URL state, config panel, nav link, curated scenarios
Mar 16, 2026
0d7952b
fix: use npm lockfile for Vercel compatibility
Mar 16, 2026
c350eb8
fix: gitignore pnpm-lock.yaml to prevent Vercel from using pnpm
Mar 16, 2026
c477f37
feat(dojo): streaming simulation, 3-tab left pane, remove header scen…
Mar 16, 2026
aff0f79
fix(dojo): correct JSONL streaming — chunk per message, not per line
Mar 16, 2026
19665c7
feat(dojo): add user interaction to restaurant-booking scenario
Mar 16, 2026
a1bf4ac
Use relative paths for agent output
nan-yu Mar 23, 2026
5fa3691
Enable streaming on the contact lookup agent
nan-yu Mar 19, 2026
2c85c70
feat(streaming): implement reactive client-side streaming support
nan-yu Mar 17, 2026
ef32659
Streaming support for restaurant
nan-yu Mar 18, 2026
b116d86
Stop yielding the accumulated final response
nan-yu Mar 25, 2026
6f268f2
Update record_scenario script to recording streaming chunks
nan-yu Mar 23, 2026
4cc44ed
Automate image asset serving for streaming preview
nan-yu Mar 24, 2026
241b95a
Skip copying if the image already exists in the destination
nan-yu Mar 24, 2026
20b9f87
Update recorded scenarios with streaming chunks
nan-yu Mar 24, 2026
f97f274
Disable non-streaming scenarios
nan-yu Mar 24, 2026
e8d9251
Fix CI failures
nan-yu Mar 25, 2026
4c8cb21
Add licenses to composer
nan-yu Mar 25, 2026
878d32f
chore: migrate package manager from npm to pnpm in composer tool
nan-yu Mar 25, 2026
a0b0020
feat: use @a2ui/markdown-it as optional peer dependency for v0.8 in L…
nan-yu Mar 26, 2026
df39743
refactor: remove redundant component re-yielding logic from streaming…
nan-yu Mar 26, 2026
d468b1d
Update streaming chunks
nan-yu Mar 26, 2026
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
40 changes: 39 additions & 1 deletion agent_sdks/python/src/a2ui/a2a.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
# limitations under the License.

import logging
from typing import Any, Optional, List
from typing import Any, Optional, List, AsyncIterable, TYPE_CHECKING

if TYPE_CHECKING:
from a2ui.core.parser.streaming import A2uiStreamParser
from a2a.server.agent_execution import RequestContext
from a2a.types import (
AgentExtension,
Expand Down Expand Up @@ -247,3 +249,39 @@ def try_activate_a2ui_extension(
return selected_uri.replace(f"{A2UI_EXTENSION_BASE_URI}/v", "")

return None


async def stream_response_to_parts(
parser: "A2uiStreamParser",
token_stream: AsyncIterable[str],
) -> AsyncIterable[Part]:
"""Helper to parse a stream of LLM tokens into A2A Parts incrementally.

Args:
parser: A2uiStreamParser instance to process the stream.
token_stream: An async iterable of strings (tokens).

Yields:
A2A Part objects as they are discovered in the stream.
"""
async for token in token_stream:
logger.info("-----------------------------")
logger.info(f"--- AGENT: Received token:\n{token}")
response_parts = parser.process_chunk(token)
logger.info(
f"--- AGENT: Response parts:\n{[part.a2ui_json for part in response_parts]}\n"
)
logger.info("-----------------------------")
Comment on lines +268 to +274
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The logger.info statements within the async for token in token_stream loop can be very verbose and might impact performance, especially when processing a large number of tokens. Consider changing these to logger.debug or removing them for production environments. This issue is also present in samples/agent/adk/contact_lookup/agent.py (lines 268-274) and samples/agent/adk/restaurant_finder/agent.py (lines 262-268).

Suggested change
logger.info("-----------------------------")
logger.info(f"--- AGENT: Received token:\n{token}")
response_parts = parser.process_chunk(token)
logger.info(
f"--- AGENT: Response parts:\n{[part.a2ui_json for part in response_parts]}\n"
)
logger.info("-----------------------------")
logger.debug("-----------------------------")
logger.debug(f"--- AGENT: Received token:\n{token}")
response_parts = parser.process_chunk(token)
logger.debug(
f"--- AGENT: Response parts:\n{[part.a2ui_json for part in response_parts]}\n"
)
logger.debug("-----------------------------")


for part in response_parts:
if part.text:
yield Part(root=TextPart(text=part.text))

if part.a2ui_json:
json_data = part.a2ui_json

if isinstance(json_data, list):
for message in json_data:
yield create_a2ui_part(message)
else:
yield create_a2ui_part(json_data)
15 changes: 2 additions & 13 deletions agent_sdks/python/src/a2ui/core/parser/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ def __init__(self, catalog: "A2uiCatalog" = None):
# Set of unique component IDs yielded per surface
self._yielded_ids: Dict[str, Set[str]] = {} # surfaceId -> set of cids
self._yielded_contents: Dict[Any, str] = {} # (surfaceId, cid) -> hash of content
self._dirty_components: Set[str] = (
set()
) # Set of component IDs that need re-yielding
self._components_with_placeholders: Set[str] = (
set()
) # cid set that were yielded as placeholders
Expand Down Expand Up @@ -695,9 +692,8 @@ def _sniff_partial_data_model(self, messages: List[ResponsePart]) -> None:
delta_msg = {MSG_TYPE_DATA_MODEL_UPDATE: delta_msg_payload}
self._yield_messages([delta_msg], messages, strict_integrity=False)
self._yielded_data_model.update(contents_dict)
# Update internal model to trigger re-yielding of resolved placeholders
# Update internal model for path resolution
self.update_data_model(dm_obj, messages)
self.yield_reachable(messages, check_root=False)

# v0.9: updateDataModel
elif MSG_TYPE_UPDATE_DATA_MODEL in obj:
Expand Down Expand Up @@ -889,9 +885,6 @@ def update_data_model(

self._data_model.update(contents)
updated_keys = set(contents.keys())
for cid, paths in self._comp_paths.items():
if paths & updated_keys:
self._dirty_components.add(cid)

def _handle_complete_object(
self,
Expand Down Expand Up @@ -1078,9 +1071,7 @@ def yield_reachable(
return

should_yield = False
if (available_reachable - yielded_for_surface) or (
available_reachable & self._dirty_components
):
if available_reachable - yielded_for_surface:
should_yield = True
else:
# Check if any yielded component's content has changed for this surface
Expand Down Expand Up @@ -1154,8 +1145,6 @@ def yield_reachable(
cid = comp["id"]
self._yielded_contents[(surface_id, cid)] = json.dumps(comp, sort_keys=True)
self._yielded_placeholders[cid] = self._get_placeholders(comp)
if cid in self._dirty_components:
self._dirty_components.remove(cid)

except ValueError as e:
if "Circular reference detected" in str(e):
Expand Down
12 changes: 8 additions & 4 deletions agent_sdks/python/src/a2ui/core/schema/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@

DEFAULT_WORKFLOW_RULES = f"""
The generated response MUST follow these rules:
1. The response can contain one or more A2UI JSON blocks.
2. Each A2UI JSON block MUST be wrapped in `{A2UI_OPEN_TAG}` and `{A2UI_CLOSE_TAG}` tags.
3. Between or around these blocks, you can provide conversational text.
4. The JSON part MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA.
- The response can contain one or more A2UI JSON blocks.
- Each A2UI JSON block MUST be wrapped in `{A2UI_OPEN_TAG}` and `{A2UI_CLOSE_TAG}` tags.
- Between or around these blocks, you can provide conversational text.
- The JSON part MUST be a single, raw JSON object (usually a list of A2UI messages) and MUST validate against the provided A2UI JSON SCHEMA.
- Top-Down Component Ordering: Within the `components` list of a message:
- The 'root' component MUST be the FIRST element.
- Parent components MUST appear before their child components.
This specific ordering allows the streaming parser to yield and render the UI incrementally as it arrives.
"""
148 changes: 0 additions & 148 deletions agent_sdks/python/tests/core/parser/test_streaming_v08.py
Original file line number Diff line number Diff line change
Expand Up @@ -1091,19 +1091,6 @@ def test_data_model_after_components(mock_catalog):
}],
}
},
{
"surfaceUpdate": {
"surfaceId": "s1",
"components": [{
"id": "root",
"component": {
"Text": {
"text": {"path": "/name"},
}
},
}],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand Down Expand Up @@ -1518,37 +1505,6 @@ def test_multiple_re_yielding_scenarios(mock_catalog):
"contents": [{"key": "p1", "valueString": "v1"}],
}
},
{
"surfaceUpdate": {
"surfaceId": "s1",
"components": [
{
"id": "c1",
"component": {
"Text": {
"text": {"path": "/p1"},
}
},
},
{
"id": "c2",
"component": {
"Text": {
"text": {"path": "/p2"},
}
},
},
{
"id": "root",
"component": {
"Container": {
"children": ["c1", "c2"],
}
},
},
],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand All @@ -1564,37 +1520,6 @@ def test_multiple_re_yielding_scenarios(mock_catalog):
"contents": [{"key": "p2", "valueString": "v2"}],
}
},
{
"surfaceUpdate": {
"surfaceId": "s1",
"components": [
{
"id": "c1",
"component": {
"Text": {
"text": {"path": "/p1"},
}
},
},
{
"id": "c2",
"component": {
"Text": {
"text": {"path": "/p2"},
}
},
},
{
"id": "root",
"component": {
"Container": {
"children": ["c1", "c2"],
}
},
},
],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand Down Expand Up @@ -1650,38 +1575,13 @@ def test_incremental_data_model_streaming(mock_catalog):
' "valueMap": ['
)
# The parser yields the data model early once it sniffs the start of it
# AND it triggers a re-yield of reachable components that depend on 'items'
expected = [
{
"dataModelUpdate": {
"contents": [{"key": "items", "valueMap": []}],
"surfaceId": "default",
}
},
{
"surfaceUpdate": {
"surfaceId": "default",
"components": [
{
"id": "item-list",
"component": {
"List": {
"children": {
"template": {
"componentId": "template-name",
"dataBinding": "/items",
}
}
}
},
},
{
"id": "template-name",
"component": {"Text": {"text": {"path": "/name"}}},
},
],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand All @@ -1697,30 +1597,6 @@ def test_incremental_data_model_streaming(mock_catalog):
}],
}
},
{
"surfaceUpdate": {
"surfaceId": "default",
"components": [
{
"id": "item-list",
"component": {
"List": {
"children": {
"template": {
"componentId": "template-name",
"dataBinding": "/items",
}
}
}
},
},
{
"id": "template-name",
"component": {"Text": {"text": {"path": "/name"}}},
},
],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand All @@ -1739,30 +1615,6 @@ def test_incremental_data_model_streaming(mock_catalog):
}],
}
},
{
"surfaceUpdate": {
"surfaceId": "default",
"components": [
{
"id": "item-list",
"component": {
"List": {
"children": {
"template": {
"componentId": "template-name",
"dataBinding": "/items",
}
}
}
},
},
{
"id": "template-name",
"component": {"Text": {"text": {"path": "/name"}}},
},
],
}
},
]
assertResponseContainsMessages(response_parts, expected)

Expand Down
Loading
Loading