diff --git a/blueprints/README.md b/blueprints/README.md index 4128c91..4a1967c 100644 --- a/blueprints/README.md +++ b/blueprints/README.md @@ -29,6 +29,7 @@ Use stable `snake_case` capability and module names. Codebase paths mirror the i | UI actions and rendering | [Action binding](modules/action_binding.blueprint.md) and [Lit adapter](features/reusable_lit_adapter.blueprint.md) | | Protected media | [Authorized playback](features/authorized_playback.blueprint.md) | | Project setup and host submission | [Application setup](features/application_setup.blueprint.md) and [host publication](features/host_publication.blueprint.md) | +| A React starter, the tic-tac-toe guide and messages from a view to the conversation | [Getting-started tracks](features/getting_started_tracks.blueprint.md) and [view conversation](features/view_conversation.blueprint.md) (both draft) | Each design's Status section separates its approved scope, implemented slices and outstanding acceptance. Host publication has working ChatGPT and Claude preparation commands; a fixture-verified local resumable-handoff record is also implemented, and none of these tools establishes publication. For working setup instructions, use [Getting started](../docs/getting-started.md). diff --git a/blueprints/features/application_setup.blueprint.md b/blueprints/features/application_setup.blueprint.md index 01e4c84..4cf2ba5 100644 --- a/blueprints/features/application_setup.blueprint.md +++ b/blueprints/features/application_setup.blueprint.md @@ -14,6 +14,8 @@ date_added: 2026-10-16 Hapi's direct-registration lifecycle amendment, [`6ac09eb2e7790179fc7e01ec561462e795b90469`](https://github.com/MachineWisdomAI/saga-a2ui/blob/6ac09eb2e7790179fc7e01ec561462e795b90469/blueprints/features/application_setup.blueprint.md), is implemented at `MachineWisdomAI/saga-a2ui@00e23b080493432250c9ae84c90385f5e5acc4f9` ([PR #215](https://github.com/MachineWisdomAI/saga-a2ui/pull/215)); [independent review](https://github.com/MachineWisdomAI/saga-a2ui/pull/215#pullrequestreview-5605193916) covered `072b3c2b1478cdbbb4c551a1fd2fc380e2e5cd6a`. +Successor (draft, not approved): [getting-started tracks](getting_started_tracks.blueprint.md) proposes to amend two parts of this design. It would add a qualified React/Hono starter pair (`templates/react-hono`, offered as `--a2ui-stack react-mcp --server hono`), and split the setup guide into a common guide plus Lit and React build tracks. Until it is approved, this design stays current, including its statement that no React pair is offered. + The seven SDK packages are published at [`0.7.7-rc.1`](https://github.com/MachineWisdomAI/saga-a2ui/releases/tag/v0.7.7-rc.1). Templates, the source CLI, demo and tour are development tooling, not npm packages or hosted services. Qualification of these outputs is local/installed evidence; it does not establish new OAuth, production or assistant-host acceptance. [Design and delivery history](#design-and-delivery-history) preserves the exact earlier scope and revisions. ## Problem and current behavior diff --git a/blueprints/features/getting_started_tracks.blueprint.md b/blueprints/features/getting_started_tracks.blueprint.md new file mode 100644 index 0000000..6bdfe15 --- /dev/null +++ b/blueprints/features/getting_started_tracks.blueprint.md @@ -0,0 +1,298 @@ +--- +feature_name: getting_started_tracks +status: draft +module_blueprints: [action_binding] +dependencies: [application_setup, view_conversation, view_composition, reusable_mcp_connector] +date_added: 2026-10-27 +--- + +# Getting started: a React starter and a tic-tac-toe guide + +## Status + +- Design: draft awaiting human approval. +- Implementation: not started. +- This would partially amend [application setup](application_setup.blueprint.md) in two places: a qualified React/Hono starter pair, and the split of the setup guide into a short common guide plus two build tracks. The approved application-setup contract stays in force for everything else, including the five Lit pairs, the demo, the CLI's safety behaviour and the generated existing-backend recipes. +- It would also make `view` optional on [reusable MCP connector](reusable_mcp_connector.blueprint.md) operation declarations, keeping every execution guarantee ([operations without a view](#operations-without-a-view)). + + + +## Problem and current behavior + +A developer who looks at saga a2ui for ten minutes should come away knowing what they could build, and how. Today they meet package structure, protocol terms and contributor workflow before they see an app. + +| What they read | What they meet first | +| --- | --- | +| The [setup guide](../../docs/getting-started.md) | Published packages, candidate tarballs, HTTP defaults and evidence boundaries, all before a running app | +| The [action guide](../../docs/actions.md) | The notes application, which is thorough but long | + +React has a published stack and a [complete application example](../../docs/react-consumers.md#follow-a-complete-react-application), but no starter. The CLI's [supported-pair table](../../scripts/saga.ts) offers only `lit-mcp`, and its tests assert that `react-mcp` is refused. + +Nothing shows an assistant taking part in an app. Every current example is driven either by a view control or by a single tool call. + +The brand expansion is changing from "Safe Agentic Apps" to "Simple Agentic Apps". The framework is still **saga a2ui**, and **saga** once context is clear. An app built with it is still a **Saga**. + +## Proposed behavior + +```diff + docs/getting-started.md install, init, run, attach, troubleshoot +- + Lit composition, actions, React caveats, evidence ++docs/build-notes-with-lit.md the Lit composition material, leading into actions.md ++docs/build-tic-tac-toe-with-react.md a ten-minute guide that opens with the finished game +``` + +The new path for a React developer: + +```sh +npx tsx scripts/saga.ts init --a2ui-stack react-mcp --server hono ../my-saga +``` + +The command creates a clean React starter on Hono from `templates/react-hono`. It installs nothing and runs nothing. The tic-tac-toe guide starts from that output. + +**One game, from the guide.** + +1. The person asks the assistant for a game of tic-tac-toe. The assistant calls `new_game`, and the host shows the board. +2. The person clicks a square. The board calls `play_move` for X. The server checks the move and stores it, and the board shows it and marks the assistant's turn. +3. If the game is still in progress, the board appends a short move message to the conversation, using [view conversation](view_conversation.blueprint.md). For example: "Your move in tic-tac-toe game ``. The board is in your context; if it isn't, read it with get_game. Then play O with play_move." + - The accepted game travels as the message's `context`, which the assistant receives and the conversation does not show. + - The message names the game and never restates the board. + - The assistant takes its turn. It reads `get_game` only if the context is missing, then calls `play_move` for O. +4. The board was already waiting for the assistant's move. It reads the game with `get_game`, shows O's square and gives the turn back to the person. +5. If X's move ends the game, the board instead appends the result as the person, such as "I won our tic-tac-toe game." or "Our tic-tac-toe game ended in a draw.", and starts no wait. New game starts a separate game, and nothing from the old game can change it. + +![The person's click and the assistant's tool call reach the same play_move operation; the board learns each move through its own server calls and hands the turn over by appending a message.](../../diagrams/tic-tac-toe-turn-loop.svg) + +[Editable Mermaid](../../diagrams/tic-tac-toe-turn-loop.mmd) · [PNG](../../diagrams/tic-tac-toe-turn-loop.png) + +Both players' moves go through the same server operation. They reach the board differently, because the host routes a result by who called: + +- **The person's move.** The view makes the call, so the result comes back to the calling action's success hook. +- **The assistant's move.** The model makes the call. A host delivers a model call's result to a card it mounts for that call, and MCP Apps sends one tool input and result per mounted view, so the board that is already open is not guaranteed to receive it. The board therefore reads the game itself while it waits. + +`play_move` opens no card of its own, so the assistant's move does not add a second board to the conversation. + +## Responsibilities and interfaces + +### React/Hono starter (`templates/react-hono`) + +**Same as `templates/lit-hono`.** The template keeps the clean-starter contract: public status, a protected account operation whose verifier refuses every credential, the bound "Clear the note" local action and no fixtures. + +- **Server.** Its `server/` is the `lit-hono` text, with the same Hono 4.13.13, `@hono/node-server` 1.19.17 and `@modelcontextprotocol/hono` 2.0.2 pins. + - The one exception is the view-artifact check in `operations.ts`, which looks for ` + +## Problem and current behavior + +A rendered view can reach its application's server but cannot reach the conversation around it. Some apps need both. In a game played with the assistant, the person clicks a square and the server accepts the move, but the assistant does not learn that it is its turn. + +Both stacks start a view with `startA2uiView`. Each gives the application's `actions` callback a `ViewHost`: + +| Stack | `ViewHost` today | +| --- | --- | +| `@saga-a2ui/react-mcp-stack` | `server`: the server transport ([`start-a2ui-view.tsx`](../../packages/react-mcp-stack/src/start-a2ui-view.tsx)) | +| `@saga-a2ui/lit-mcp-stack` | `server`, and `links` for validated link opening ([`start-a2ui-view.ts`](../../packages/lit-mcp-stack/src/start-a2ui-view.ts)) | + +Neither kind of action can reach the conversation: + +- **Local action.** It runs in the view and changes only view state. +- **Server action.** Its `call` reaches the application's server through `App.callServerTool`, which the host proxies. + +The reply to a view-initiated call returns to the calling action. MCP Apps hosts do not add it to the conversation. The assistant also acts only when given a turn, so a view that changes shared state leaves the assistant unaware until the person types into the chat. + +The MCP Apps bridge the stacks depend on (`@modelcontextprotocol/ext-apps` 2.0.3) already supports this. `App.sendMessage` sends `ui/message`, a message with role `"user"` that the host adds to the conversation, and the host advertises it through its `message` capability. `App.updateModelContext` sends `ui/update-model-context`: information that the host passes to the model with the next user message, advertised as `updateModelContext`. The stacks construct `App` privately, and the consumer boundary forbids applications from importing the bridge (`scripts/consumer-boundary.ts`). So an application has no supported way to add a message. + +## Proposed behavior + +```diff + actions: (host) => { + const play = Action.server('play-square', host.server) + .input((event) => ({square: Number(event.read('index'))})) + .run(({params, call}) => call('play_move', {...game.current(), ...params, player: 'X'})) + .onSuccess(({result, current}) => { + if (!current()) return; + const game = board.present(result); +- // The assistant does not know the person moved. ++ const shown = {gameId: game.gameId, moveNumber: game.moveNumber}; ++ const handOver = ++ game.status === 'playing' ++ ? host.conversation.appendMessage(TEXT.yourMove(game.gameId), { ++ // best effort: the move message tells the assistant to read get_game if the board is missing ++ context: {text: TEXT.boardForAssistant(game), data: {game}, bestEffort: true}, ++ }) ++ : host.conversation.appendMessage(TEXT.result(game)); // reports the result, asks for no move ++ void handOver.then((result) => { ++ if (!board.isShowing(shown)) return; // a newer game or move is on screen ++ if (result.outcome !== 'appended') board.say(TEXT.tellTheAssistant); ++ }); + }) + .build(); +``` + +**The flow:** + +1. The person clicks a square. The server accepts the move, and the action presents the new board. +2. **If the game is still in progress**, the action appends a short move message: "Your move in tic-tac-toe game ``. The board is in your context; if it isn't, read it with get_game. Then play O with play_move." The accepted game travels as the message's `context`, which the assistant receives but the conversation does not show. The message names the game; it never restates the board, because the server owns the state. +3. **If X's move ended the game**, the action instead appends the result, as the person: "I won our tic-tac-toe game." or "Our tic-tac-toe game ended in a draw." It asks for no move. Every draw ends on X's move, because X moves first on nine squares. An O win arrives through the assistant's own call and needs no message. +4. The host shows the message as if the person had typed it, and the assistant takes its turn. +5. If nothing was appended, the result says so, and the application tells the person what to type. + +**Why the action rechecks with its own identity.** The hook starts the append and does not wait for it. A host may wait for the person to confirm the message, and a success hook has a five-second budget ([action defaults](../../docs/actions.md)). The action's `current()` is true only while the runtime waits for this hook ([action guide](../../docs/actions.md)). It is already false when the append settles. So the application compares its own `{gameId, moveNumber}` with what the board shows. A late failure for an old game or move never appears on a newer one. + +The capability follows the shape of host links. A delivery-neutral contract lives in `@saga-a2ui/core`, the MCP Apps implementation lives in `@saga-a2ui/mcp/view`, and each stack places an instance on `ViewHost`. + +![The application's action calls appendMessage, optionally with context, on the core contract; the MCP Apps implementation sends ui/update-model-context and then ui/message when the host supports them; a future A2A delivery would send one message/send with the context as a data part.](../../diagrams/view-conversation.svg) + +[Editable Mermaid](../../diagrams/view-conversation.mmd) · [PNG](../../diagrams/view-conversation.png) + +## Responsibilities and interfaces + +**Contract (proposed, `@saga-a2ui/core/conversation`).** + +```ts +export type RequestOutcome = 'denied' | 'unavailable' | 'failed'; + +export type AppendResult = + | {readonly outcome: 'appended'; readonly context?: RequestOutcome} // context set only when best effort skipped it + | {readonly outcome: RequestOutcome; readonly stage: 'context' | 'message'}; // nothing appended + +export interface Conversation { + /** + * Adds a message to the conversation as the person, which gives the assistant its turn. + * `context` is information for that turn that the conversation does not show. By default the + * message is sent only if the context was accepted; `bestEffort: true` sends it regardless. + */ + appendMessage( + text: string, + options?: { + readonly role?: 'user'; + readonly context?: { + readonly text: string; + readonly data?: Record; + readonly bestEffort?: boolean; // default false + }; + }, + ): Promise; +} + +export const NO_CONVERSATION: Conversation; // every call resolves {outcome: 'unavailable', stage: ...} +``` + +| Outcome | Meaning | +| --- | --- | +| `appended` | The host accepted the message. It does not show that the assistant replied, or what it said. | +| `denied` | The host refused the request, for example because the person declined or a policy blocked it. | +| `unavailable` | The host advertises no capability for the request, or the view has ended. That request was not sent. | +| `failed` | The request could not complete, for example because the connection was lost or it timed out. | + +`stage` says which request stopped the append. `context` on an appended result says why a best-effort context was skipped. + +**Context blocks the message by default.** When `context` is present and cannot be delivered, because it is unsupported, refused, failed or the view ended, the message is not sent. The result is `{outcome, stage: 'context'}`, so the application knows nothing reached the conversation. + +With `bestEffort: true`, the application opts in to sending the message anyway. The result is `{outcome: 'appended', context: }`. An application should opt in only when the assistant has another way to get the information. The tic-tac-toe game opts in because its move message names `get_game`. + +**One verb, not two.** Context is a parameter of the message, not a separate request. +- An MCP Apps host "will typically defer sending the context to the model until the next user message… Only the last update is sent" (`ext-apps` 2.0.3 `App.updateModelContext`). +- So context without a following message has no effect the application can rely on. +- A2A carries both in one `message/send`. + +The verbs describe the conversation, not one delivery protocol: + +| Delivery | `appendMessage` sends | Status | +| --- | --- | --- | +| MCP Apps | With `context`: `ui/update-model-context` first, `text` as a content block and `data` as `structuredContent`, only when `getHostCapabilities().updateModelContext` is present. Then `ui/message` with role `"user"` and one text content block, only when `getHostCapabilities().message` is present. | Proposed | +| A2A | One `message/send` with a `Message` of role `"user"` in the view's current `contextId`. `context` would become a data part on that message. | Not proposed; the [A2A delivery issue](https://github.com/MachineWisdomAI/saga-a2ui/issues/198) owns it | + +**Trust.** Context comes from the view. It is a snapshot that helps the assistant, not authority. The server still validates every move, including its `expectedMoveNumber`. + +`role` defaults to `'user'` and is the only accepted value. MCP Apps permits only that role. A2A's other role, `'agent'`, belongs to the agent, and the type widens only if a delivery needs it. + +Capabilities are read at call time, as host links do, so the instance can exist before the bridge has connected. + +**MCP Apps implementation (`@saga-a2ui/mcp/view`).** `conversationFromApp(app, signal)` sits beside `hostLinksFromApp`. It sends the context request, then the message, never in parallel. Once the view's lifetime signal has aborted, it resolves `unavailable`, including while a request is outstanding, so an ended view hears no late outcome. + +**Stacks.** `ViewHost` gains a readonly `conversation: Conversation` in both stacks, built from the same `App` and lifetime signal as the existing members. React and Lit get it in the same release. + +**Application.** It owns everything about the conversation: + +- whether to append, what the text says, and what context goes with it; +- whether a context failure may be skipped (`bestEffort`); +- what the person sees when nothing was appended, shown only while the same state is on screen; +- never treating `appended` as the assistant having acted. + +Its server stays authoritative over application state. An appended message is a prompt, not a command, and it authorizes nothing. + +## Failure and recovery + +| Trigger | Observable result and recovery | +| --- | --- | +| The host does not advertise `message` | `{outcome: 'unavailable', stage: 'message'}`. Nothing is sent. The application shows its own instruction, such as "Tell the assistant it's their move". | +| The context cannot be delivered (unsupported, refused or failed) | By default, `{outcome, stage: 'context'}`: the message is not sent, and the application shows its instruction. With `bestEffort`, the message is sent and the result is `{outcome: 'appended', context}`. | +| The host refuses the message | `denied` at the message stage; the application shows the same instruction. No retry, because a repeated message would ask the assistant twice. | +| The request rejects or times out | `failed`; whether the host showed the message is unknown. The application shows the instruction and does not retry automatically. | +| The view ends while a request is outstanding | `unavailable`. No later request is sent. | +| The outcome settles after the application moved on, for example to a new game | The application's own identity check finds different state on screen and ignores the outcome. | +| The assistant does not respond, or responds without acting | Outside this capability. The application keeps its own state consistent and offers the person a way to continue. | + +## Verification outcomes + +- **Message only.** + - A fixture that advertises `message` and accepts the request records one `ui/message` with role `user` and the exact text, and the outcome is `appended`. + - No context request is made. +- **With context.** The fixture records `ui/update-model-context` with `text` as content and `data` as `structuredContent`, and then `ui/message`, in that order. +- **Context fails, default.** The context capability is withheld, refused (`isError`) or rejected. + - Nothing reaches `ui/message`. + - The result is `{outcome: 'unavailable' | 'denied' | 'failed', stage: 'context'}`. +- **Context fails, best effort.** The same three cases. + - Only `ui/message` is recorded. + - The result is `{outcome: 'appended', context: }`. +- **Message refused or failing.** An `isError` answer resolves `denied` and a rejected request resolves `failed`, both at the message stage. A missing `message` capability resolves `unavailable` at the message stage. +- **Ended view.** Aborting the lifetime before or during either request resolves `unavailable`, sends nothing further and ignores the late answer. +- **Parity.** The same cases pass through `ViewHost.conversation` from both stacks, in Chromium against the local MCP Apps host fixture (`apps/lit-host/e2e/host-fixture`). +- **Exports and boundary.** Each package's `exports.test.ts` lists the new entries, and consumer code reaches the capability without importing the bridge. + +These are local checks. Whether Claude, ChatGPT or Alexa+ show the message, and how their assistants respond, needs host runs. The design claims neither. + +## Implementation sequence + +1. Add the contract to `@saga-a2ui/core` and the MCP Apps implementation with its unit tests. +2. Add `ViewHost.conversation` to both stacks. Add fixture handlers that record context updates and messages in order, and that can withhold, refuse or fail each one. Add the browser cases. +3. Document it in the [React integration guide](../../docs/react-consumers.md) and the development reference. + +This release bumps the package versions. The [getting-started tracks](getting_started_tracks.blueprint.md) design uses it for the tic-tac-toe example. + +## Open decisions + +None in the design. Host behaviour is missing verification, not an open design choice. + +## References + +- [View composition](view_composition.blueprint.md) for the stacks and `startA2uiView`. +- [Action binding](../modules/action_binding.blueprint.md) for actions, hooks and currentness. +- `@saga-a2ui/core/host-links` and `@saga-a2ui/mcp/view` `hostLinksFromApp`: the pattern this follows. +- MCP Apps `App.sendMessage`, `App.updateModelContext` and the host `message` and `updateModelContext` capabilities in `@modelcontextprotocol/ext-apps` 2.0.3. A2A `message/send`, `Message.role` and data parts. diff --git a/diagrams/tic-tac-toe-turn-loop.mmd b/diagrams/tic-tac-toe-turn-loop.mmd new file mode 100644 index 0000000..618671d --- /dev/null +++ b/diagrams/tic-tac-toe-turn-loop.mmd @@ -0,0 +1,38 @@ +sequenceDiagram + actor P as Person + participant B as Board card (React view) + participant H as Host (Claude, ChatGPT, ...) + participant A as Assistant + participant S as Game server (Hono) + P->>B: clicks row 1, column 2 + B->>H: play_move(X, expectedMoveNumber 0) + H->>S: play_move + S-->>H: game, move 1 + H-->>B: result to the calling action (onSuccess) + B->>B: show X, squares disabled + alt game in progress, O next + B->>H: appendMessage("Your move in game ...", context: game) + Note over B,H: ui/update-model-context, then ui/message + H->>A: the person's message, with the game as context + opt context missing + A->>H: get_game + H->>S: get_game + S-->>H: game, move 1 + H-->>A: result + end + A->>H: play_move(O, expectedMoveNumber 1) + H->>S: play_move + S-->>H: game, move 2 + H-->>A: result (no card opened) + loop bounded wait, about every 1.5 s + B->>H: get_game + H->>S: get_game + S-->>H: game, move 2 + H-->>B: result to the waiting board + end + B->>B: show O, squares enabled for X + else X won, or the board is full + B->>H: appendMessage("I won our tic-tac-toe game.") + H->>A: the person's message (no move asked) + end + Note over B,S: A late append result is ignored unless the board still shows the same game and move.
A stale card or repeated request sends an old expectedMoveNumber, which the server refuses with no effect. diff --git a/diagrams/tic-tac-toe-turn-loop.png b/diagrams/tic-tac-toe-turn-loop.png new file mode 100644 index 0000000..c3b6f9b --- /dev/null +++ b/diagrams/tic-tac-toe-turn-loop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5e8de69d1a31e28cd84eb34cda9b3b623dc2bb6efeac6c89e8a525668945424 +size 282075 diff --git a/diagrams/tic-tac-toe-turn-loop.svg b/diagrams/tic-tac-toe-turn-loop.svg new file mode 100644 index 0000000..dd03675 --- /dev/null +++ b/diagrams/tic-tac-toe-turn-loop.svg @@ -0,0 +1 @@ +Game server (Hono)AssistantHost (Claude, ChatGPT, ...)Board card (React view)Game server (Hono)AssistantHost (Claude, ChatGPT, ...)Board card (React view)ui/update-model-context, then ui/messageopt[context missing]loop[bounded wait, about every 1.5 s]alt[game in progress, O next][X won, or the board is full]A late append result is ignored unless the board still shows the same game and move.A stale card or repeated request sends an old expectedMoveNumber, which the server refuses with no effect.Personclicks row 1, column 2play_move(X, expectedMoveNumber 0)play_movegame, move 1result to the calling action (onSuccess)show X, squares disabledappendMessage("Your move in game ...", context: game)the person's message, with the game as contextget_gameget_gamegame, move 1resultplay_move(O, expectedMoveNumber 1)play_movegame, move 2result (no card opened)get_gameget_gamegame, move 2result to the waiting boardshow O, squares enabled for XappendMessage("I won our tic-tac-toe game.")the person's message (no move asked)Person diff --git a/diagrams/view-conversation.mmd b/diagrams/view-conversation.mmd new file mode 100644 index 0000000..cd068e9 --- /dev/null +++ b/diagrams/view-conversation.mmd @@ -0,0 +1,11 @@ +graph LR + APP["Application action
(onSuccess hook)"] + ST["ViewHost.conversation
React and Lit stacks"] + C["Conversation contract
@saga-a2ui/core/conversation
appendMessage(text, role?, context?)"] + M["MCP Apps implementation
@saga-a2ui/mcp/view"] + CTX["ui/update-model-context
if context, when supported
(failure blocks the message
unless bestEffort)"] + UI["ui/message, role user
when the host advertises message"] + A2A["A2A delivery (not proposed)
one message/send, role user,
context as a data part"] + APP --> ST --> C + C --> M --> CTX --> UI + C -.-> A2A diff --git a/diagrams/view-conversation.png b/diagrams/view-conversation.png new file mode 100644 index 0000000..7995e64 --- /dev/null +++ b/diagrams/view-conversation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f8f83c8e3a4e3e31352692a9dad5b8aa58da64bb7d8b6c07463dc3f0e81a340 +size 99585 diff --git a/diagrams/view-conversation.svg b/diagrams/view-conversation.svg new file mode 100644 index 0000000..951cdd2 --- /dev/null +++ b/diagrams/view-conversation.svg @@ -0,0 +1 @@ +

Application action
(onSuccess hook)

ViewHost.conversation
React and Lit stacks

Conversation contract
@saga-a2ui/core/conversation
appendMessage(text, role?, context?)

MCP Apps implementation
@saga-a2ui/mcp/view

ui/update-model-context
if context, when supported
(failure blocks the message
unless bestEffort)

ui/message, role user
when the host advertises message

A2A delivery (not proposed)
one message/send, role user,
context as a data part