Repository navigation
Conversation
…streaming An alternative to #3283. Takes its blueprint text for RendererState, ResponseStream and the stateless Parser, and changes how rollback and streaming are specified so that parsing and streaming a response take time in proportion to the response: 1. RendererState undoes a change at the cost of what changed: each applied message records what it replaced, and a deleted surface is replayed through the processor rather than copied. A state must not copy a surface on each apply or transaction. 2. ResponseStream reads each chunk for what it adds, and a chunk that raises returns nothing. 3. A new "Cost of a response" section names the timing suites. 4. The DirectJsonResponseStream paragraph states which messages are emitted while they grow and what happens to a rejected one. 5. The core blueprint lists what core supplies for undoing, and the incremental updateComponents check. Co-authored-by: Nan Yu <nanyu@google.com>
There was a problem hiding this comment.
Code Review
This pull request updates the a2ui_agent and a2ui_core blueprints to introduce a stateful streaming and validation architecture, replacing the stateless parse_chunk with a RendererState and ResponseStream model. This allows outbound messages to be validated incrementally against the session's current surface state, with full topology checks deferred until the response is complete. The review feedback suggests minor corrections to the blueprints: removing an inaccurate reference to a delete method on DataModel, correcting the terminology 'v0.8 catalogs' to 'v0.8 capability resolution', and updating the streaming example code to properly iterate over and send individual ResponsePart elements.
| An agent does not drive `MessageProcessor` directly. The [Agent SDK](a2ui_agent.blueprint.md)'s `RendererState` owns one, and adds what checking outbound messages needs on top of it: atomic application with rollback, a tree check once a response is complete, and a lifetime the agent chooses (one state per chat turn, or one per canvas conversation). Core supplies the pieces it builds on: | ||
|
|
||
| 1. `processMessages` checks a message whole before it changes a surface, so a message it rejects leaves the surface as it was. | ||
| 2. `SurfaceComponentsModel.get` and `removeComponent`, and `DataModel.get`, `set` and `delete`, through which a state reverts what an applied message replaced. A deleted surface is restored by replaying its components and data model through `processMessages`, which also restores the children its components find through other catalogs. A state does not copy surfaces. |
There was a problem hiding this comment.
The DataModel class interface defined in this blueprint (around line 1310) does not include a delete method. Instead, deletion of keys is handled by calling set with undefined or null (as described in the JSON Pointer implementation rules). Consider removing the reference to delete here to align with the actual API definition.
| the model reads about. | ||
| - From v0.9 the capabilities are keyed by protocol version (`{"v0.9": {...}}`). The | ||
| v0.8 specification sends the entry itself (`{"supportedCatalogIds": [...]}`), so | ||
| v0.8 catalogs read that shape as well as the `"v0.8"` key. Capabilities count as a |
| # Streaming instead of step 4: each part is sent as soon as it is checked. | ||
| stream = processor.open_stream() | ||
| for chunk in myagent.stream_llm(processor.prompt_snippet, request_context): | ||
| send(stream.feed(chunk)) |
There was a problem hiding this comment.
Since stream.feed(chunk) and stream.close() return a collection of ResponsePart elements, passing their return values directly to send (e.g., send(stream.feed(chunk))) might be incorrect if send expects a single ResponsePart. It would be more robust to iterate over the returned collection of parts and send each part individually.
References
- Avoid using language-specific syntax (such as Python's keyword-only argument
*or type hints likelist[ResponsePart]) in language-agnostic blueprint specifications to prevent confusion.
|
Thanks, @polina-c. The measurements are convincing, and I agree with the direction: undo instead of copying, and a stated cost for a response. I'd like to fold this PR into #3283 because #3283 holds the review and the references. Your ten inline comments are on #3283, and the SDK PRs (#3268–#3279) link to it. Addressing them in one place keeps the threads and the history together. I'll consolidate changes in this PR to #3283 and address your review comments there. |
…streaming From #3322 by Polina Cherkasova (commit 26b4cc9), applied on top of #3283. It changes how rollback and streaming are specified so that parsing and streaming a response take time in proportion to the response: 1. RendererState undoes a change at the cost of what changed: each applied message records what it replaced, and a deleted surface is replayed through the processor rather than copied. A state must not copy a surface on each apply or transaction. 2. ResponseStream reads each chunk for what it adds, and a chunk that raises returns nothing. 3. A new "Cost of a response" section names the timing suites. 4. The DirectJsonResponseStream paragraph states which messages are emitted while they grow and what happens to a rejected one. 5. The core blueprint lists what core supplies for undoing, and the incremental updateComponents check. Co-authored-by: Polina Cherkasova <polina.c@live.com> Co-authored-by: Nan Yu <nanyu@google.com> Signed-off-by: Nan Yu <nanyu@google.com>
This is an alternative to #3283. It takes #3283's blueprint text as of d26cf5ac0 (the
RendererState, theResponseStreamand the statelessParser) and changes how it specifies rollback and streaming. With those changes, an SDK that follows the blueprint keeps passing the timing suites added in #3314. The rest of #3283 is unchanged here.The problem in #3283
#3283 says that
applyis atomic (L505-L518) and that "a rollback restores copies of the surfaces that the frame changed" (L541). A stream applies each message as it is emitted (L564). A state that follows all three copies a surface for every message it applies. A response of n messages to one surface then takes time that grows with the square of n. The stream paragraph (L1103) also leaves open whether a chunk is read and checked for what it adds or together with everything before it.The SDK PRs for #3283 do both:
applychanges it (_save), and everyapplyopens one._save,save). They also read each chunk inside a transaction (Python, TypeScript), so every chunk copies the surfaces it touches.Measured
The numbers come from a port of #3276 and #3277 onto main's Dart agent, run against the timing suites. The port is in two variants:
Each number is the median time at 800 items divided by the median time at 200. A linear cost gives about 4, a quadratic one about 16, and a case fails above 8.
Parsing 800 messages of one component each whole, against 200:
Streaming 800 components that heal as they arrive, to a surface an earlier turn created, against 200:
Streaming a surface of 800 components, against 200:
Streaming 800 messages of one component each, against 200:
The time to stream a surface of 400 components, divided by the time to parse it whole:
Changes from #3283
RendererStateundoes a change at the cost of what changed, not the size of the surface:applyor each transaction.applyandtransactiondocstrings say so.ResponseStreamreads each chunk for what it adds.feedsays that a chunk that raises returns nothing and that what it applied is undone, which is what the SDKs already do.A new section, "Cost of a response", states what keeps parsing and streaming linear and names the suites that check it:
applyandtransactioncost what they change.updateComponentsagainst what it changes.check_topologywalks a whole surface.The
DirectJsonResponseStreamparagraph now says which messages are emitted while they grow and what happens to a message the state rejects, and it requires reading each chunk once.The agent conformance table lists
<format>/scaling.yaml.The core blueprint lists what core supplies for undoing:
processMessages.SurfaceComponentsModel.getandremoveComponent.DataModel.get,setanddelete.It drops
addSurfaceanddeleteSurfaceas the way to restore a surface. It also says that a processor under a deferringValidationConfigchecks anupdateComponentswithout walking the whole surface. Its conformance section namesmessage_processor_scaling.yaml.Testing
python3 blueprints/validate_blueprints.pypasses.a2ui_agenttests and both timing suites,dart test -P performanceina2ui_agentanda2ui_core.🤖 Agent-generated