Skip to content

feat(python/agent)!: make the Parser stateless; always validate; add ResponseStream - #3269

Open
nan-yu wants to merge 20 commits into
feat/python-agent-renderer-statefrom
feat/python-agent-response-stream
Open

nan-yu wants to merge 20 commits into
feat/python-agent-renderer-statefrom
feat/python-agent-response-stream

Conversation

@nan-yu

@nan-yu nan-yu commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #3262 and #3263. Stacked on #3268.

Important

Like #3268, this PR needs the core change in #3326, whose branch is merged in through #3268. The core part of the diff disappears once #3326 lands.

The Parser read whole responses, which needs no state, and streamed responses, which needs the buffers of one response, on the same object. It also validated inside compile, so translation and validation could not be separated, and some paths skipped validation while others repeated it.

Parser

  1. The parser holds no state. compile, decompile, wrap and unwrap only translate.
  2. parse_response(content, wrapped, state=) compiles every block, applies them to a RendererState inside a transaction, and checks the trees of the surfaces they touch once every block is applied. A component may name a child that a later block adds. Without a state, the response is checked against a new, empty one, as a chat turn that shows new UI.
  3. open_stream(wrapped=, state=) returns a ResponseStream for one response, with feed and close. Each message it emits is applied to the state, so the state matches what the client received even when the response fails part way. close emits text still held back as a possible tag, raises A2uiParseError if a block is still open, and checks the tree of an unwrapped stream.
  4. The direct JSON stream (direct_json/response_stream.py) checks each message against its state instead of calling validate_payload for every message on every chunk, and no longer keeps its own map of surfaces to catalogs. The block reading it shares with the parser moves to direct_json/blocks.py.
  5. Express, Elemental and Atom output is checked the same way as direct JSON output.

Reading each chunk once

A stream must take time in proportion to the response (blueprint, "Cost of a response"). The direct JSON stream used to scan the open block for the close tag from its start, read the whole open block again, and apply a growing message again whole, on every chunk. Now:

  1. BlockScanner resumes the close-tag scan where the previous chunk stopped.
  2. read_partial_message_items takes a PartialReadCache: a list or map that closed in an earlier read is taken from the cache, and an open list, the list of messages included, is read on from its last closed item. The reader ports the Dart reader from Fix performance and create conformance tests. #3314.
  3. A message that has closed is not read, applied or checked again. A growing updateComponents applies only the components it gained, plus its last component again while that one is healed before v1.0 (_check_added); an id repeated across those batches is rejected as a duplicate. A growing updateDataModel is emitted each time its value changes and applied again once it closes (_check_grown).
  4. When a block closes, the read goes on from the cache. Only a block that does not read as complete JSON is compiled whole, so that it raises as compile does (_final_messages).

Only updateComponents and updateDataModel (v0.8's surfaceUpdate and dataModelUpdate) are emitted while they grow. Any other message, callRendererFunction included, now waits for its object to close, and the messages after it wait with it, as the blueprint's DirectJsonResponseStream describes. Before, only createSurface, beginRendering and deleteSurface waited.

Streaming 40-character chunks with a v1.0 basic catalog, best of three:

payload n=100 n=200 n=400 400/100
one message of n components, before 667 ms 2595 ms 10264 ms 15.4
after 42 ms 85 ms 193 ms 4.6
n one-component messages, before 895 ms 3582 ms 14312 ms 16.0
after 45 ms 97 ms 208 ms 4.7

The "before" numbers are this branch with #3268 and #3326 merged, so they measure the stream alone.

DirectJsonParser.compile now returns AgentToRendererMessage models rather than decoded mappings, as the blueprint's compile and the other formats do. A message that matches no protocol version's message schema raises A2uiValidationError there. Every caller in the stack already accepted models.

Removed

  1. Parser.parse_chunk and validates_output, and the formats' parser property (use create_parser()).
  2. validate_payload(..., surface_catalog_ids=...).
  3. In a2ui-a2a, parse_stream_chunk_to_parts and the deprecated parse_response_to_parts. stream_response_to_parts takes a ResponseStream, and the new to_a2a_parts converts parsed parts. These move with the parser because they called parse_chunk.

Behaviour changes

  1. An update to a surface the response does not create is now rejected, by A2uiRequestProcessor.parse_response and A2uiPartConverter too. A canvas agent passes the RendererState it keeps; the next PR lets the processor hold it. validate_payload still assumes such a surface exists.
  2. Express, Elemental and Atom output that only passed because those formats skipped some checks is rejected: inline child ids that collide with assigned variables, check lists on components that declare none, a function call given to a property with a static type, and Elemental output whose top-level element has no id="root".

Conformance and evaluation

  1. The shared conformance cases describe no renderer history. A streaming or parse_response case is read as one chat turn against a renderer that holds no surfaces, so a case that updates a surface creates it in its input: streaming cases start with a block that creates the surface, and the parse_response case creates it in the same block. The Python runner drives streaming cases through open_stream, feed and close.
  2. Compile and decompile cases check a block on its own, the way validate_payload does, so a surface the block only updates is assumed to exist. The Python runner compiles the block and calls validate_payload.
  3. Two streaming cases now expect an update to a surface the renderer doesn't hold to be held back rather than emitted.
  4. The two v0.8 data model streaming cases begin rendering their surface in a first block, as the v0.9 cases create theirs, since a v0.8 response that never sends beginRendering is now rejected (feat(python/agent): add RendererState; build validate_payload on it #3268).
  5. The agent harness runs the timing suites agent/direct_json/scaling.yaml and agent/express/scaling.yaml in test_scaling_conformance.py, marked performance, under the timing harness from perf(python/core): check updateComponents incrementally under deferred graph checks #3326. test_scaling_cases_have_handlers runs in the default run. A payload that updates a surface it does not create (the v0.9 healed case) streams into a state that holds that surface.
  6. The Python evaluation bridge parses once, instead of parsing and then running a second strict MessageProcessor.

The Dart runner passes the updated cases as it is today. The current TypeScript stream processor now passes seven streaming cases it listed as known failures, so they come off its list.

The samples under samples/ still call parse_chunk and parse_response_to_parts; the sample PRs at the top of this stack move them over.

Testing

  1. New test_response_stream.py and test_renderer_state_parsing.py cover chat and canvas turns, rollback of a failed response, streams that fail part way, close, and a block that names a child a later block adds. test_parse_chunk.py is replaced by them. New python/a2ui_a2a/tests/test_parts.py.
  2. uv run pytest python/a2ui_agent/tests python/a2ui_a2a/tests python/a2ui_adk/tests samples/agent/adk/tests eval/tests: all pass except the TypeScript evaluation bridge tests, which fail the same way on main in my checkout (@a2ui/agent not resolvable from node_modules).
  3. uv run mypy python/a2ui_agent/src python/a2ui_a2a/src: no issues.
  4. TypeScript yarn test and Dart dart test still pass with the updated conformance YAML.
  5. A new test streams a fenced block one character at a time.
  6. New test_response_stream_reads.py checks that each closed message is applied once, that a growing message applies only what it gains, that a healed component is applied again only while it grows, that a data model update is applied when it first reads and once it closes, that a function call waits for its object to close, that a repeated id in a growing message is rejected, and that a close tag inside a string does not end the block however the response is cut.
  7. uv run pytest python/ samples/agent/adk/tests: 2855 passed. uv run pytest -m performance python/a2ui_core/tests python/a2ui_agent/tests: 12 passed, including the 8 agent scaling cases. The old stream's ratios in the table above are over their limit of 8.
  8. test_streaming_delete_surface.py, which main added against parse_chunk, is ported to open_stream.

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request refactors the A2UI parsing and streaming architecture to be stateless, introducing ResponseStream and DirectJsonResponseStream for incremental chunk-by-chunk processing and utilizing RendererState for validation. It also removes several deprecated streaming APIs across the a2ui_a2a and a2ui_adk packages. Feedback on the changes highlights a bug in clean_start where single backticks are incorrectly handled compared to triple backticks, and suggests a performance optimization in possible_tag_start to avoid O(N^2) string slicing by leveraging the pos parameter in re.match without the redundant ^ anchor.

Comment thread python/a2ui_agent/src/a2ui/inference_formats/direct_json/blocks.py Outdated
Comment thread python/a2ui_agent/src/a2ui/inference_formats/direct_json/blocks.py Outdated
Comment thread python/a2ui_agent/src/a2ui/inference_formats/direct_json/blocks.py
…ResponseStream

The `Parser` read whole responses, which needs no state, and streamed
responses, which needs the buffers of one response, on the same object.
It also validated inside `compile`, so translation and validation could
not be separated, and some paths skipped validation while others
repeated it.

1. The parser holds no state. `compile`, `decompile`, `wrap` and
   `unwrap` only translate. `parse_response(content, wrapped, state=)`
   compiles every block, applies them to a `RendererState` inside a
   transaction, and checks the trees of the surfaces they touch once
   every block is applied. Without a state, the response is checked
   against a new, empty one, as a chat turn that shows new UI.
2. `open_stream(wrapped=, state=)` returns a `ResponseStream` for one
   response, with `feed` and `close`. Each message it emits is applied to
   the state, so the state matches what the client received even when
   the response fails part way. `close` emits text still held back as a
   possible tag, raises if a block is still open, and checks the tree of
   an unwrapped stream.
3. The direct JSON stream checks each message against its state instead
   of calling `validate_payload` for every message on every chunk, and no
   longer keeps its own map of surfaces to catalogs. `validate_payload`
   loses `surface_catalog_ids`.
4. Express, Elemental and Atom output is checked the same way as direct
   JSON output.
5. `parse_chunk`, `validates_output` and the formats' `parser` property
   are removed.
6. `a2ui.a2a`: `stream_response_to_parts` takes a `ResponseStream`, and
   `to_a2a_parts` converts parsed parts. `parse_stream_chunk_to_parts` and
   the deprecated `parse_response_to_parts` are removed.
7. The shared conformance cases gain `args.history`, the messages the
   renderer received before a case starts, and the streaming cases
   expect an update to a surface the renderer doesn't hold to be held
   back. The evaluation bridge parses once, without a second check.

Part of #3262 and #3263.

Signed-off-by: Nan Yu <nanyu@google.com>
The cases no longer describe what a renderer received before they start.

1. Streaming cases that update a surface they don't create start with a
   block that creates it, since a stream is read as one chat turn
   against a renderer that holds no surfaces.
2. The parse_response case creates its surface in the same block.
3. Compile and decompile cases check a block on its own, the way
   `validate_payload` does, so a surface the block only updates is
   assumed to exist and needs no history. The Python runner compiles the
   block and calls `validate_payload` instead of parsing it against a
   seeded `RendererState`.

Signed-off-by: Nan Yu <nanyu@google.com>
… tags in linear time

1. `clean_start` strips an opening fence only when the block starts with
   three backticks, as `clean` does, and returns nothing while only the
   start of a fence has arrived. A block that starts with one backtick is
   no longer cut short while streaming and then read differently when it
   closes.
2. `possible_tag_start` and `scan_block` match the start of a tag at its
   position instead of copying the rest of the text for every `<`.

Signed-off-by: Nan Yu <nanyu@google.com>
Seven streaming cases now create their surface in a block of their own
instead of relying on `args.history`, which the TypeScript runner
ignored. The current stream processor passes them, so they come off the
known-failure list, and KNOWN_GAPS.md counts 40 failing cases instead of
47.

Signed-off-by: Nan Yu <nanyu@google.com>
A chunk could apply messages, for example by closing a block, and then
fail later in the same chunk. feed raised without returning their
parts, but the state kept them. feed now runs in a transaction, so the
state holds only what the stream emitted.
@nan-yu
nan-yu force-pushed the feat/python-agent-response-stream branch from bbadfc0 to 2f6c405 Compare October 16, 2026 23:07
Comment thread python/a2ui_agent/src/a2ui/parser/parser.py
Comment thread python/a2ui_agent/src/a2ui/inference_formats/direct_json/response_stream.py Outdated
…t-response-stream

# Conflicts:
#	python/a2ui_agent/src/a2ui/utils/renderer_state.py

@gspencergoog gspencergoog left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR makes the Python Parser stateless, moves catalog and topology checks out of compile into parse_response and the new open_stream/ResponseStream backed by a RendererState, and updates the a2ui.a2a helpers, the ADK converter, the eval bridge, the shared conformance suites and the tests. The streaming state machine and the transactional whole-response path hold up under review: a failed chunk rolls back to what was emitted, createSurface/deleteSurface are held until their object closes, and the open review threads from @ditman are addressed at 38c643f.

Requesting changes for one reason: the two rewritten "update before create" cases in response_streaming.yaml change the shared contract, and the Dart runner does not implement the hold-back, so dart_a2ui_agent (stable) fails on this PR and would stay red after the rebase onto main (the body says the Dart runner passes the updated cases). The sample breakage the body acknowledges is confirmed to be fixed by #3272 for the two workspace samples; the remaining notes below are about the gap between the two merges and one question on truncated blocks in canvas mode.

Broader Context & Non-Diff Observations

The following observations relate to surrounding or referenced code outside the active diff hunks:

  • samples/agent/adk/restaurant_finder/agent.py:304 — Workspace samples import a removed symbol until #3272 lands

    samples/agent/adk/restaurant_finder/agent.py:42,304 and samples/agent/adk/custom-components-example/agent.py:42,528 import the removed parse_response_to_parts and pass a Parser to stream_response_to_parts, so they fail at import, and streaming would raise AttributeError, from the moment this PR merges until #3272 merges. ci-samples was skipped on this PR, so CI does not show it. The gemini_enterprise samples depend on published releases and are unaffected.

🤖 Agent-generated

Comment thread conformance/agent/direct_json/response_streaming.yaml
Comment thread python/a2ui_agent/tests/parser/test_renderer_state_parsing.py
Comment thread python/a2ui_agent/src/a2ui/parser/parser.py

@ditman ditman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

More nits. I think I've seen the update to the samples coming later in the stack, so that's probably not too bad!

Comment thread python/a2ui_agent/src/a2ui/parser/parser.py
Comment thread python/a2ui_a2a/CHANGELOG.md
Comment thread python/a2ui_agent/CHANGELOG.md
Comment thread python/a2ui_agent/src/a2ui/utils/renderer_state.py
Comment thread python/a2ui_a2a/src/a2ui/a2a/parts.py Outdated
Comment thread python/a2ui_adk/CHANGELOG.md
…' into feat/python-agent-response-stream

Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	python/a2ui_agent/CHANGELOG.md
- RendererState.mark_checked stops holding a surface's changed components
  against later checks. A stream that fails after emitting messages, and a
  block cut off at the end of a parse_response, mark their surfaces
  checked, so a later turn isn't required to make components the renderer
  already holds reachable. Dangling references and a missing root are
  still reported.
- When parse_response's tree check fails, partial_results holds only the
  text, since the check spans every block; document that partial_results
  isn't applied to the state.
- parse_content_to_parts takes a keyword-only state for canvas agents.
- Fix the macros README to call create_parser().compile.
- Drop unused imports from test_renderer_state_parsing.py.

Signed-off-by: Nan Yu <nanyu@google.com>
…' into feat/python-agent-response-stream

Signed-off-by: Nan Yu <nanyu@google.com>
…s known gaps

The Dart stream emits an update for a surface the response hasn't created
yet, where response_streaming.yaml now holds it back. Skip both cases until
the Dart ResponseStream implements the hold-back.

Signed-off-by: Nan Yu <nanyu@google.com>
…ecked

Finishing a block in a chunk clears the stream's touched surfaces, but if
the chunk then fails in a second block, the rollback restores that block's
earlier-emitted messages unchecked. feed now marks the surfaces touched
before the chunk.

Signed-off-by: Nan Yu <nanyu@google.com>
…t-response-stream

Port test_streaming_delete_surface.py (added on main) to open_stream, and begin rendering the v0.8 surfaces in the ADK converter tests and the two v0.8 streaming conformance cases, since a v0.8 response that never sends beginRendering is now rejected.

Signed-off-by: Nan Yu <nanyu@google.com>
…seStream

The stream no longer scans the open block from its start and reads it whole
on every chunk:

- BlockScanner resumes the close-tag scan where the previous chunk stopped.
- read_partial_message_items takes a PartialReadCache: a list or map that
  closed is taken from the cache, and an open list (the list of messages
  included) is read on from its last closed item.
- A message that has closed is not read, applied or checked again. A growing
  updateComponents applies only the components it gained (and the last one
  again while it is healed before v1.0); a repeated id across those batches
  is rejected. A growing updateDataModel is emitted as its value changes and
  applied again once it closes.
- Only updateComponents and updateDataModel (surfaceUpdate and
  dataModelUpdate in v0.8) are emitted while they grow. Any other message,
  callRendererFunction included, waits for its object to close.
- At block close the read goes on from the cache. Only a block that does not
  read as complete JSON is compiled whole, so that it raises as compile does.

Streaming one message of n components in 40-character chunks drops from
10.3 s to 0.19 s at n=400 (ratio 400/100 from 15.4 to 4.6).

DirectJsonParser.compile now returns AgentToRendererMessage models, as the
blueprint and the other formats' compile do.

The agent conformance harness runs agent/direct_json/scaling.yaml and
agent/express/scaling.yaml under the timing harness (marked performance);
test_scaling_cases_have_handlers runs in the default run.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu removed this pull request from stack #3275 October 20, 2026 17:42
@nan-yu
nan-yu added this pull request to stack #3340 October 20, 2026 17:47
nan-yu added a commit that referenced this pull request Oct 20, 2026
…load on it

RendererState wraps one web_core MessageProcessor, created once with the
linked catalogs, with dangling references, a missing root and orphans
allowed while messages are applied.

- apply(payload) checks and applies messages atomically, and transaction(fn)
  extends that to everything fn applies. Transactions nest. A callback that
  returns a promise is rejected at compile time and at run time.
- checkTopology(surfaceIds) checks the root and references of each surface,
  and that every component added or replaced since the last check is
  reachable.
- Rollback uses an undo log rather than copying a surface. Each message
  records what it replaced: the components an updateComponents adds or
  replaces (with a copy of the properties of one updated in place), the
  value an updateDataModel overwrites, and a deleted surface, which is
  restored by replaying it with each component's own catalogId. The state's
  bookkeeping is logged too. A transaction is a position in the log, so a
  failed inner transaction undoes only what it applied.
- validatePayload runs on a short-lived subclass that assumes a surface the
  payload only updates exists, and accepts one message on its own.

The conformance cases under conformance/agent/ are updated to the shared
cases this SDK and the Python SDK (#3269) run: args.history is dropped, and
the streaming cases that now pass are no longer known gaps.

Applying 400 one-component messages to one surface, each in a transaction,
takes 2.2 times as long as 100 (9.1 with surface copies).

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…ach chunk once

The parser holds no state. parseResponse and openStream check a response
against a RendererState, a new one per call unless the caller passes one
(or binds one to A2uiRequestProcessor) for a canvas conversation.
A2uiRequestProcessor gains createState and acceptsState, which compares the
catalogs' ids and release lines.

DirectJsonResponseStream reads each chunk once:

1. BlockScanner resumes the close-tag scan where the last chunk left it.
2. readPartialMessages reads through a PartialReadCache, ported from the
   Dart reader: a list or map that closed is kept by offset, and a list of
   messages goes on after the last message that closed.
3. The stream keeps the closed messages and applies each once. Only
   updateComponents and updateDataModel are emitted while they grow; any
   other message, callRendererFunction included, waits for its object to
   close. A growing updateComponents is applied for the components it
   gained, with ids repeated across chunks rejected. A growing
   updateDataModel is checked for the lists and maps its value gained, and
   applied again once it closes.
4. tests/timing/scaling_test.mjs runs conformance/agent/direct_json/
   scaling.yaml and conformance/agent/express/scaling.yaml with
   `yarn test:timing`, using web_core's timing helper, and the ci-timing job
   runs it for @a2ui/agent.

Streaming one updateComponents of 400 components in 40-character chunks
drops from 7.0 s to 0.13 s (ratio 400/100 from 16.0 to 3.9).

The Node restaurant finder sample and the eval TypeScript bridge use the new
API.

The conformance cases under conformance/agent/ are updated to the shared
cases this SDK and the Python SDK (#3269) run: args.history is dropped, and
the streaming cases this stream now passes leave the known failures.
conformance/README.md says the TypeScript agent SDK runs the agent timing
suites with `yarn test:timing`.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…d RendererState to A2uiRequestProcessor

- A parser holds no state. Parser.parseChunk is removed: openStream returns
  a ResponseStream (DirectJsonResponseStream for direct JSON) whose feed and
  close read one response against a RendererState, applying each message as
  it is emitted and checking the trees a block touched when it closes. A
  failed stream keeps what it emitted and marks it checked
  (RendererState.markChecked, undone with a failed transaction like any
  other change).
- Parser.compile only translates; parseResponse checks a response as a whole
  against a RendererState. A2uiRequestProcessor binds a RendererState and
  gains acceptsState.
- DirectJsonResponseStream reads each chunk once: the close-tag scan and the
  read of the open block resume from the previous chunk (#3314's
  PartialReadCache), a closed message is applied once, a growing
  updateComponents applies only the components it gained (and the last one
  again if it grew) and rejects ids repeated across its chunks, and a
  growing updateDataModel is checked for the lists and maps its value gained
  and applied again once it closes. Only updateComponents and
  updateDataModel are emitted while they grow.
- The shared conformance cases for the stateless parser and stream, as in
  #3269: compile cases assume the surfaces a block only updates, response
  parser cases create the surfaces they update, and the streaming cases hold
  back an update to a surface the response has not created. The Dart known
  gaps for those cases are removed.
- The scaling conformance harness streams through openStream into a state
  that holds the surfaces a payload updates first, and times compile as
  parseResponse(payload, wrapped: false), as the Python harness does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…until #3269

#3269 changes test_stream_update_before_create_surface_fails_v09 and
test_stream_data_model_before_create_surface_fails_v09 to hold back a
message for a surface that does not exist yet. The Python and Dart SDKs on
main still emit it, so carrying the change here failed their CI. The cases
stay as main has them, and the TypeScript harness lists them as known
failures until #3269 lands.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…ases

The shared conformance cases this PR carries from #3269 failed CI for the
SDKs on main:

- test_stream_update_before_create_surface_fails_v09 and
  test_stream_data_model_before_create_surface_fails_v09 now expect a
  message for a surface that does not exist yet to be held back, which the
  Python SDK on main does not do. They stay as main has them until #3269
  lands, and the Dart harness skips them until then, since this stream
  already holds the message back.
- With the updated cases, seven TypeScript streaming cases pass, so they
  leave the TypeScript harness's known failures, as #3279 does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…d RendererState to A2uiRequestProcessor

- A parser holds no state. Parser.parseChunk is removed: openStream returns
  a ResponseStream (DirectJsonResponseStream for direct JSON) whose feed and
  close read one response against a RendererState, applying each message as
  it is emitted and checking the trees a block touched when it closes. A
  failed stream keeps what it emitted and marks it checked
  (RendererState.markChecked, undone with a failed transaction like any
  other change).
- Parser.compile only translates; parseResponse checks a response as a whole
  against a RendererState. A2uiRequestProcessor binds a RendererState and
  gains acceptsState.
- DirectJsonResponseStream reads each chunk once: the close-tag scan and the
  read of the open block resume from the previous chunk (#3314's
  PartialReadCache), a closed message is applied once, a growing
  updateComponents applies only the components it gained (and the last one
  again if it grew) and rejects ids repeated across its chunks, and a
  growing updateDataModel is checked for the lists and maps its value gained
  and applied again once it closes. Only updateComponents and
  updateDataModel are emitted while they grow.
- The shared conformance cases for the stateless parser and stream, as in
  #3269: compile cases assume the surfaces a block only updates, response
  parser cases create the surfaces they update, and the streaming cases hold
  back an update to a surface the response has not created. The Dart known
  gaps for those cases are removed.
- The scaling conformance harness streams through openStream into a state
  that holds the surfaces a payload updates first, and times compile as
  parseResponse(payload, wrapped: false), as the Python harness does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…ases

The shared conformance cases this PR carries from #3269 failed CI for the
SDKs on main:

- test_stream_update_before_create_surface_fails_v09 and
  test_stream_data_model_before_create_surface_fails_v09 now expect a
  message for a surface that does not exist yet to be held back, which the
  Python SDK on main does not do. They stay as main has them until #3269
  lands, and the Dart harness skips them until then, since this stream
  already holds the message back.
- With the updated cases, seven TypeScript streaming cases pass, so they
  leave the TypeScript harness's known failures, as #3279 does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…d RendererState to A2uiRequestProcessor

- A parser holds no state. Parser.parseChunk is removed: openStream returns
  a ResponseStream (DirectJsonResponseStream for direct JSON) whose feed and
  close read one response against a RendererState, applying each message as
  it is emitted and checking the trees a block touched when it closes. A
  failed stream keeps what it emitted and marks it checked
  (RendererState.markChecked, undone with a failed transaction like any
  other change).
- Parser.compile only translates; parseResponse checks a response as a whole
  against a RendererState. A2uiRequestProcessor binds a RendererState and
  gains acceptsState.
- DirectJsonResponseStream reads each chunk once: the close-tag scan and the
  read of the open block resume from the previous chunk (#3314's
  PartialReadCache), a closed message is applied once, a growing
  updateComponents applies only the components it gained (and the last one
  again if it grew) and rejects ids repeated across its chunks, and a
  growing updateDataModel is checked for the lists and maps its value gained
  and applied again once it closes. Only updateComponents and
  updateDataModel are emitted while they grow.
- The shared conformance cases for the stateless parser and stream, as in
  #3269: compile cases assume the surfaces a block only updates, response
  parser cases create the surfaces they update, and the streaming cases hold
  back an update to a surface the response has not created. The Dart known
  gaps for those cases are removed.
- The scaling conformance harness streams through openStream into a state
  that holds the surfaces a payload updates first, and times compile as
  parseResponse(payload, wrapped: false), as the Python harness does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…ases

The shared conformance cases this PR carries from #3269 failed CI for the
SDKs on main:

- test_stream_update_before_create_surface_fails_v09 and
  test_stream_data_model_before_create_surface_fails_v09 now expect a
  message for a surface that does not exist yet to be held back, which the
  Python SDK on main does not do. They stay as main has them until #3269
  lands, and the Dart harness skips them until then, since this stream
  already holds the message back.
- With the updated cases, seven TypeScript streaming cases pass, so they
  leave the TypeScript harness's known failures, as #3279 does.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Oct 20, 2026
…d RendererState to A2uiRequestProcessor (#3277)

* feat(dart/agent)!: make the Parser stateless; add ResponseStream; bind RendererState to A2uiRequestProcessor

- A parser holds no state. Parser.parseChunk is removed: openStream returns
  a ResponseStream (DirectJsonResponseStream for direct JSON) whose feed and
  close read one response against a RendererState, applying each message as
  it is emitted and checking the trees a block touched when it closes. A
  failed stream keeps what it emitted and marks it checked
  (RendererState.markChecked, undone with a failed transaction like any
  other change).
- Parser.compile only translates; parseResponse checks a response as a whole
  against a RendererState. A2uiRequestProcessor binds a RendererState and
  gains acceptsState.
- DirectJsonResponseStream reads each chunk once: the close-tag scan and the
  read of the open block resume from the previous chunk (#3314's
  PartialReadCache), a closed message is applied once, a growing
  updateComponents applies only the components it gained (and the last one
  again if it grew) and rejects ids repeated across its chunks, and a
  growing updateDataModel is checked for the lists and maps its value gained
  and applied again once it closes. Only updateComponents and
  updateDataModel are emitted while they grow.
- The shared conformance cases for the stateless parser and stream, as in
  #3269: compile cases assume the surfaces a block only updates, response
  parser cases create the surfaces they update, and the streaming cases hold
  back an update to a surface the response has not created. The Dart known
  gaps for those cases are removed.
- The scaling conformance harness streams through openStream into a state
  that holds the surfaces a payload updates first, and times compile as
  parseResponse(payload, wrapped: false), as the Python harness does.

Signed-off-by: Nan Yu <nanyu@google.com>

* test(conformance): keep main's SDKs green with the shared streaming cases

The shared conformance cases this PR carries from #3269 failed CI for the
SDKs on main:

- test_stream_update_before_create_surface_fails_v09 and
  test_stream_data_model_before_create_surface_fails_v09 now expect a
  message for a surface that does not exist yet to be held back, which the
  Python SDK on main does not do. They stay as main has them until #3269
  lands, and the Dart harness skips them until then, since this stream
  already holds the message back.
- With the updated cases, seven TypeScript streaming cases pass, so they
  leave the TypeScript harness's known failures, as #3279 does.

Signed-off-by: Nan Yu <nanyu@google.com>

---------

Signed-off-by: Nan Yu <nanyu@google.com>
…t-response-stream

Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	conformance/README.md
#	python/a2ui_agent/tests/utils/test_renderer_state.py
…t-response-stream

Signed-off-by: Nan Yu <nanyu@google.com>

# Conflicts:
#	dart/a2ui_agent/test/conformance/direct_json_conformance_test.dart
…t-response-stream

Signed-off-by: Nan Yu <nanyu@google.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants