[{"data":1,"prerenderedAt":2641},["ShallowReactive",2],{"blog-ag-ui-part-2-generative-ui":3},{"post":4,"minutes":891,"surround":2637},{"id":5,"title":6,"body":7,"color":2615,"cover":2616,"coverAlt":2617,"crossPost":2618,"date":2619,"description":2620,"draft":2621,"extension":2622,"meta":2623,"navigation":2624,"path":2625,"seo":2626,"stem":2627,"tags":2628,"tldr":2632,"updated":2618,"__hash__":2636},"blogs\u002Fblogs\u002Fag-ui-part-2-generative-ui.md","AG-UI Part 2: Generative UI, Three Patterns and a Demo",{"type":8,"value":9,"toc":2600},"minimark",[10,19,27,32,35,42,46,54,61,70,144,154,158,165,168,220,248,252,255,260,269,288,303,311,315,318,321,656,659,1025,1031,1037,1043,1053,1057,1064,1071,1086,1089,1361,1367,1526,1536,1810,1813,1832,1895,1902,1921,1927,1931,1938,1941,1944,2057,2071,2176,2183,2189,2192,2196,2199,2205,2315,2318,2325,2332,2336,2339,2397,2401,2467,2470,2476,2480,2483,2491,2511,2530,2534,2537,2544,2548,2551,2596],[11,12,13,14,18],"p",{},"A chat that streams text is the simplest agent UI, but an agent that finds flights should not answer with a paragraph. It should show a flight card. ",[15,16,17],"strong",{},"Generative UI"," means parts of the interface are generated, selected or controlled by an agent at runtime. In this post I walk through the three patterns for doing that, show how each one travels over AG-UI, and build a dependency-free demo that runs all of them.",[11,20,21,26],{},[22,23,25],"a",{"href":24},"\u002Fblogs\u002Fag-ui-part-1-how-agents-talk-to-your-frontend","Part 1"," covered the protocol: one request in, a stream of typed events out. This part is about what you put in that stream.",[28,29,31],"h2",{"id":30},"the-idea-in-30-seconds","The idea in 30 seconds",[11,33,34],{},"Here is the short video version. Picture one slot in your app. The agent can fill it with a component you built, a UI it describes in JSON, or a whole widget it hands over. The rest of this post is about who owns the pixels in each case, and what the trade-offs are.",[36,37],"video-embed",{"id":38,"provider":39,"ratio":40,"title":41},"X3FCQS4tnjM","youtube","9\u002F16","Generative UI in 30 seconds: three ways an agent can fill a slot in your app",[28,43,45],{"id":44},"_1-what-generative-ui-is-and-the-one-trade-off-behind-it","1. What generative UI is, and the one trade-off behind it",[11,47,48,49,53],{},"Generative UI is the name for an agent influencing what the interface ",[50,51,52],"em",{},"is",", not only what it says: a card when it has results, a form when it needs your email.",[11,55,56,57,60],{},"The question that separates the patterns is ",[15,58,59],{},"who owns the pixels",". Every option below moves that line.",[11,62,63],{},[64,65],"img",{"alt":66,"height":67,"src":68,"width":69},"Three cards from left to right: controlled, declarative and open-ended, showing what the agent sends, what you own and which AG-UI events carry each. A shared state band runs under all three.",1400,"https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fpatterns-part-2.webp",2400,[71,72,73,92],"table",{},[74,75,76],"thead",{},[77,78,79,83,86,89],"tr",{},[80,81,82],"th",{},"Pattern",[80,84,85],{},"The agent sends",[80,87,88],{},"You own",[80,90,91],{},"Main cost",[93,94,95,112,128],"tbody",{},[77,96,97,103,106,109],{},[98,99,100],"td",{},[15,101,102],{},"Controlled",[98,104,105],{},"A tool name and data",[98,107,108],{},"The components, layout and styling",[98,110,111],{},"Frontend and backend are coupled; code grows with every new use case",[77,113,114,119,122,125],{},[98,115,116],{},[15,117,118],{},"Declarative",[98,120,121],{},"A JSON description of a UI",[98,123,124],{},"The component catalog and styling",[98,126,127],{},"You are limited to the catalog, and the structure can vary between runs",[77,129,130,135,138,141],{},[98,131,132],{},[15,133,134],{},"Open-ended",[98,136,137],{},"A full UI as HTML or an iframe",[98,139,140],{},"Only the box it lives in",[98,142,143],{},"Hard to style, security questions, mostly web-only",[11,145,146,147,153],{},"CopilotKit's team uses these names in their ",[22,148,152],{"href":149,"rel":150},"https:\u002F\u002Fgithub.com\u002FCopilotKit\u002Fgenerative-ui",[151],"nofollow","generative UI repo",". In an earlier webinar they called the first one \"static\". Same idea, older word.",[28,155,157],{"id":156},"_2-where-ag-ui-fits","2. Where AG-UI fits",[11,159,160,161,164],{},"None of these patterns is an AG-UI feature. AG-UI is the transport underneath them. In the stack from Part 1, A2UI and MCP Apps describe ",[50,162,163],{},"what"," UI to render, and AG-UI is how it reaches your app.",[11,166,167],{},"That means one agent can use all three in the same chat, and the events involved are ones you have already met:",[169,170,171,194,208],"ul",{},[172,173,174,175,178,179,183,184,183,187,183,190,193],"li",{},"Controlled UI rides on ",[15,176,177],{},"tool call events"," (",[180,181,182],"code",{},"TOOL_CALL_START",", ",[180,185,186],{},"ARGS",[180,188,189],{},"END",[180,191,192],{},"RESULT",").",[172,195,196,197,178,200,203,204,207],{},"Declarative UI rides on ",[15,198,199],{},"activity events",[180,201,202],{},"ACTIVITY_SNAPSHOT"," and ",[180,205,206],{},"ACTIVITY_DELTA","), which carry structured data that is not a chat message.",[172,209,210,211,178,214,203,217,193],{},"Shared state rides on ",[15,212,213],{},"state events",[180,215,216],{},"STATE_SNAPSHOT",[180,218,219],{},"STATE_DELTA",[11,221,222,223,226,227,232,233,236,237,240,241,244,245,247],{},"One honest caveat. The spec defines the activity events, but ",[50,224,225],{},"what you put in them"," is a convention between your agent and your client. For A2UI I followed ",[22,228,231],{"href":229,"rel":230},"https:\u002F\u002Fwww.assistant-ui.com\u002Fdocs\u002Ftools\u002Fa2ui",[151],"assistant-ui's A2UI integration",": ",[180,234,235],{},"activityType: \"a2ui-surface\""," for the surface, the operations in a ",[180,238,239],{},"content.a2ui_operations"," array, and user actions going back in ",[180,242,243],{},"forwardedProps.a2uiAction",". Its docs describe full snapshots, so streaming the form in as ",[180,246,206],{}," patches (below) is my own variation. For the open-ended widget the type name is my own invention. Check what your framework's middleware does before copying either.",[28,249,251],{"id":250},"_3-the-demo","3. The demo",[11,253,254],{},"Try it right here. Click a chip, and watch the event stream:",[256,257],"iframe-embed",{"src":258,"title":259},"https:\u002F\u002F75jwlk.csb.app\u002F","AG-UI generative UI demo: click a chip and watch the events arrive",[11,261,262,263,268],{},"Prefer a full tab? Open it on ",[22,264,267],{"href":265,"rel":266},"https:\u002F\u002Fcodesandbox.io\u002Fp\u002Fsandbox\u002F75jwlk",[151],"CodeSandbox",".",[11,270,271,272,275,276,279,280,282,283,268],{},"This embedded version is one ",[180,273,274],{},"index.html"," file with no server. The \"agent\" runs inside the page and hands the client the same events as a Server-Sent Events byte stream, so the client code parses exactly what it would get from a real endpoint. I also kept a version with a real Python server, for seeing the actual network hop. It is two files, ",[180,277,278],{},"server.py"," (about 260 lines, standard library only) and ",[180,281,274],{}," (about 290 lines), and it is in ",[22,284,287],{"href":285,"rel":286},"https:\u002F\u002Fgist.github.com\u002FMondal10\u002F793c83cf8928b31cbf5bd012f6745a51",[151],"this Gist",[11,289,290,291,294,295,298,299,302],{},"In every version the ",[50,292,293],{},"events"," are real AG-UI events and the ",[50,296,297],{},"agent"," is fake. Weather data is canned, and the router is a handful of ",[180,300,301],{},"if"," statements, so the demo is deterministic and needs no API key. What it proves is the wire format and the client-side design, not any model behaviour.",[11,304,305],{},[64,306],{"alt":307,"height":308,"src":309,"width":310},"The demo: a weather card, then a form the agent generated, and the live event stream on the right",820,"https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fdeclarative-form.webp",1100,[28,312,314],{"id":313},"_4-controlled-you-build-the-card-the-agent-picks-it","4. Controlled: you build the card, the agent picks it",[11,316,317],{},"The agent calls a tool. The page already has a component registered for that tool name. That is the whole pattern.",[11,319,320],{},"The server streams a tool call exactly as an LLM would, with the arguments arriving in fragments:",[322,323,328],"pre",{"className":324,"code":325,"language":326,"meta":327,"style":327},"language-python shiki shiki-themes tokyo-night","yield {\"type\": \"TOOL_CALL_START\", \"toolCallId\": call_id, \"toolCallName\": \"get_weather\"}\nfor i in range(0, len(args), 5):\n    yield {\"type\": \"TOOL_CALL_ARGS\", \"toolCallId\": call_id, \"delta\": args[i:i + 5]}\nyield {\"type\": \"TOOL_CALL_END\", \"toolCallId\": call_id}\nyield {\"type\": \"TOOL_CALL_RESULT\", \"messageId\": new_id(), \"toolCallId\": call_id,\n       \"role\": \"tool\", \"content\": json.dumps(result)}\n","python","",[180,329,330,399,447,517,553,608],{"__ignoreMap":327},[331,332,335,339,343,346,350,352,355,358,360,362,365,367,370,372,374,378,380,382,385,387,389,391,394,396],"span",{"class":333,"line":334},"line",1,[331,336,338],{"class":337},"sd1Qi","yield",[331,340,342],{"class":341},"sAklC"," {",[331,344,345],{"class":341},"\"",[331,347,349],{"class":348},"sPY7s","type",[331,351,345],{"class":341},[331,353,354],{"class":341},":",[331,356,357],{"class":341}," \"",[331,359,182],{"class":348},[331,361,345],{"class":341},[331,363,364],{"class":341},",",[331,366,357],{"class":341},[331,368,369],{"class":348},"toolCallId",[331,371,345],{"class":341},[331,373,354],{"class":341},[331,375,377],{"class":376},"sGX4V"," call_id",[331,379,364],{"class":341},[331,381,357],{"class":341},[331,383,384],{"class":348},"toolCallName",[331,386,345],{"class":341},[331,388,354],{"class":341},[331,390,357],{"class":341},[331,392,393],{"class":348},"get_weather",[331,395,345],{"class":341},[331,397,398],{"class":341},"}\n",[331,400,402,405,408,411,415,419,423,425,428,430,434,437,439,442,444],{"class":333,"line":401},2,[331,403,404],{"class":337},"for",[331,406,407],{"class":376}," i ",[331,409,410],{"class":337},"in",[331,412,414],{"class":413},"sySf4"," range",[331,416,418],{"class":417},"sgJMe","(",[331,420,422],{"class":421},"sOJ5S","0",[331,424,364],{"class":341},[331,426,427],{"class":413}," len",[331,429,418],{"class":417},[331,431,433],{"class":432},"sE3pS","args",[331,435,436],{"class":417},")",[331,438,364],{"class":341},[331,440,441],{"class":421}," 5",[331,443,436],{"class":417},[331,445,446],{"class":341},":\n",[331,448,450,453,455,457,459,461,463,465,468,470,472,474,476,478,480,482,484,486,489,491,493,496,499,502,504,507,510,512,515],{"class":333,"line":449},3,[331,451,452],{"class":337},"    yield",[331,454,342],{"class":341},[331,456,345],{"class":341},[331,458,349],{"class":348},[331,460,345],{"class":341},[331,462,354],{"class":341},[331,464,357],{"class":341},[331,466,467],{"class":348},"TOOL_CALL_ARGS",[331,469,345],{"class":341},[331,471,364],{"class":341},[331,473,357],{"class":341},[331,475,369],{"class":348},[331,477,345],{"class":341},[331,479,354],{"class":341},[331,481,377],{"class":376},[331,483,364],{"class":341},[331,485,357],{"class":341},[331,487,488],{"class":348},"delta",[331,490,345],{"class":341},[331,492,354],{"class":341},[331,494,495],{"class":376}," args",[331,497,498],{"class":417},"[",[331,500,501],{"class":376},"i",[331,503,354],{"class":341},[331,505,506],{"class":376},"i ",[331,508,509],{"class":341},"+",[331,511,441],{"class":421},[331,513,514],{"class":417},"]",[331,516,398],{"class":341},[331,518,520,522,524,526,528,530,532,534,537,539,541,543,545,547,549,551],{"class":333,"line":519},4,[331,521,338],{"class":337},[331,523,342],{"class":341},[331,525,345],{"class":341},[331,527,349],{"class":348},[331,529,345],{"class":341},[331,531,354],{"class":341},[331,533,357],{"class":341},[331,535,536],{"class":348},"TOOL_CALL_END",[331,538,345],{"class":341},[331,540,364],{"class":341},[331,542,357],{"class":341},[331,544,369],{"class":348},[331,546,345],{"class":341},[331,548,354],{"class":341},[331,550,377],{"class":376},[331,552,398],{"class":341},[331,554,556,558,560,562,564,566,568,570,573,575,577,579,582,584,586,590,593,595,597,599,601,603,605],{"class":333,"line":555},5,[331,557,338],{"class":337},[331,559,342],{"class":341},[331,561,345],{"class":341},[331,563,349],{"class":348},[331,565,345],{"class":341},[331,567,354],{"class":341},[331,569,357],{"class":341},[331,571,572],{"class":348},"TOOL_CALL_RESULT",[331,574,345],{"class":341},[331,576,364],{"class":341},[331,578,357],{"class":341},[331,580,581],{"class":348},"messageId",[331,583,345],{"class":341},[331,585,354],{"class":341},[331,587,589],{"class":588},"s3R4Z"," new_id",[331,591,592],{"class":417},"()",[331,594,364],{"class":341},[331,596,357],{"class":341},[331,598,369],{"class":348},[331,600,345],{"class":341},[331,602,354],{"class":341},[331,604,377],{"class":376},[331,606,607],{"class":341},",\n",[331,609,611,614,617,619,621,623,626,628,630,632,635,637,639,642,644,647,649,652,654],{"class":333,"line":610},6,[331,612,613],{"class":341},"       \"",[331,615,616],{"class":348},"role",[331,618,345],{"class":341},[331,620,354],{"class":341},[331,622,357],{"class":341},[331,624,625],{"class":348},"tool",[331,627,345],{"class":341},[331,629,364],{"class":341},[331,631,357],{"class":341},[331,633,634],{"class":348},"content",[331,636,345],{"class":341},[331,638,354],{"class":341},[331,640,641],{"class":376}," json",[331,643,268],{"class":341},[331,645,646],{"class":588},"dumps",[331,648,418],{"class":417},[331,650,651],{"class":432},"result",[331,653,436],{"class":417},[331,655,398],{"class":341},[11,657,658],{},"The client keeps a registry from tool name to component. While the arguments are still streaming it shows a loading state, and when the result lands it shows the card:",[322,660,664],{"className":661,"code":662,"language":663,"meta":327,"style":327},"language-js shiki shiki-themes tokyo-night","const toolRenderers = {\n  get_weather(t) {\n    if (t.status !== 'complete')\n      return el(\n        'div',\n        { class: 'card loading' },\n        'Calling get_weather ',\n        el('code', {}, t.raw),\n      );\n    const r = t.result;\n    return el(\n      'div',\n      { class: 'card' },\n      el('div', { class: 'big' }, `${r.tempC}°C`),\n      el('div', {}, `${r.city} · ${r.conditions}`),\n    );\n  },\n};\n","js",[180,665,666,681,696,726,738,750,773,785,818,827,846,856,868,889,950,1002,1010,1018],{"__ignoreMap":327},[331,667,668,672,675,678],{"class":333,"line":334},[331,669,671],{"class":670},"sN7LL","const",[331,673,674],{"class":337}," toolRenderers",[331,676,677],{"class":341}," =",[331,679,680],{"class":417}," {\n",[331,682,683,686,688,692,694],{"class":333,"line":401},[331,684,685],{"class":588},"  get_weather",[331,687,418],{"class":417},[331,689,691],{"class":690},"sT800","t",[331,693,436],{"class":417},[331,695,680],{"class":417},[331,697,698,701,703,705,707,711,714,717,720,723],{"class":333,"line":449},[331,699,700],{"class":337},"    if",[331,702,178],{"class":417},[331,704,691],{"class":432},[331,706,268],{"class":341},[331,708,710],{"class":709},"su0L4","status",[331,712,713],{"class":337}," !==",[331,715,716],{"class":341}," '",[331,718,719],{"class":348},"complete",[331,721,722],{"class":341},"'",[331,724,725],{"class":417},")\n",[331,727,728,732,735],{"class":333,"line":519},[331,729,731],{"class":730},"sEsAJ","      return",[331,733,734],{"class":588}," el",[331,736,737],{"class":417},"(\n",[331,739,740,743,746,748],{"class":333,"line":555},[331,741,742],{"class":341},"        '",[331,744,745],{"class":348},"div",[331,747,722],{"class":341},[331,749,607],{"class":341},[331,751,752,755,759,761,763,766,768,771],{"class":333,"line":610},[331,753,754],{"class":417},"        { ",[331,756,758],{"class":757},"syYvs","class",[331,760,354],{"class":341},[331,762,716],{"class":341},[331,764,765],{"class":348},"card loading",[331,767,722],{"class":341},[331,769,770],{"class":417}," }",[331,772,607],{"class":341},[331,774,776,778,781,783],{"class":333,"line":775},7,[331,777,742],{"class":341},[331,779,780],{"class":348},"Calling get_weather ",[331,782,722],{"class":341},[331,784,607],{"class":341},[331,786,788,791,793,795,797,799,801,804,806,809,811,814,816],{"class":333,"line":787},8,[331,789,790],{"class":588},"        el",[331,792,418],{"class":417},[331,794,722],{"class":341},[331,796,180],{"class":348},[331,798,722],{"class":341},[331,800,364],{"class":341},[331,802,803],{"class":417}," {}",[331,805,364],{"class":341},[331,807,808],{"class":432}," t",[331,810,268],{"class":341},[331,812,813],{"class":709},"raw",[331,815,436],{"class":417},[331,817,607],{"class":341},[331,819,821,824],{"class":333,"line":820},9,[331,822,823],{"class":417},"      )",[331,825,826],{"class":341},";\n",[331,828,830,833,836,838,840,842,844],{"class":333,"line":829},10,[331,831,832],{"class":670},"    const",[331,834,835],{"class":337}," r",[331,837,677],{"class":341},[331,839,808],{"class":432},[331,841,268],{"class":341},[331,843,651],{"class":709},[331,845,826],{"class":341},[331,847,849,852,854],{"class":333,"line":848},11,[331,850,851],{"class":730},"    return",[331,853,734],{"class":588},[331,855,737],{"class":417},[331,857,859,862,864,866],{"class":333,"line":858},12,[331,860,861],{"class":341},"      '",[331,863,745],{"class":348},[331,865,722],{"class":341},[331,867,607],{"class":341},[331,869,871,874,876,878,880,883,885,887],{"class":333,"line":870},13,[331,872,873],{"class":417},"      { ",[331,875,758],{"class":757},[331,877,354],{"class":341},[331,879,716],{"class":341},[331,881,882],{"class":348},"card",[331,884,722],{"class":341},[331,886,770],{"class":417},[331,888,607],{"class":341},[331,890,892,895,897,899,901,903,905,908,910,912,914,917,919,921,923,926,929,932,934,937,940,943,946,948],{"class":333,"line":891},14,[331,893,894],{"class":588},"      el",[331,896,418],{"class":417},[331,898,722],{"class":341},[331,900,745],{"class":348},[331,902,722],{"class":341},[331,904,364],{"class":341},[331,906,907],{"class":417}," { ",[331,909,758],{"class":757},[331,911,354],{"class":341},[331,913,716],{"class":341},[331,915,916],{"class":348},"big",[331,918,722],{"class":341},[331,920,770],{"class":417},[331,922,364],{"class":341},[331,924,925],{"class":341}," `",[331,927,928],{"class":709},"${",[331,930,931],{"class":432},"r",[331,933,268],{"class":341},[331,935,936],{"class":709},"tempC",[331,938,939],{"class":709},"}",[331,941,942],{"class":348},"°C",[331,944,945],{"class":341},"`",[331,947,436],{"class":417},[331,949,607],{"class":341},[331,951,953,955,957,959,961,963,965,967,969,971,973,975,977,980,982,985,987,989,991,994,996,998,1000],{"class":333,"line":952},15,[331,954,894],{"class":588},[331,956,418],{"class":417},[331,958,722],{"class":341},[331,960,745],{"class":348},[331,962,722],{"class":341},[331,964,364],{"class":341},[331,966,803],{"class":417},[331,968,364],{"class":341},[331,970,925],{"class":341},[331,972,928],{"class":709},[331,974,931],{"class":432},[331,976,268],{"class":341},[331,978,979],{"class":709},"city",[331,981,939],{"class":709},[331,983,984],{"class":348}," · ",[331,986,928],{"class":709},[331,988,931],{"class":432},[331,990,268],{"class":341},[331,992,993],{"class":709},"conditions",[331,995,939],{"class":709},[331,997,945],{"class":341},[331,999,436],{"class":417},[331,1001,607],{"class":341},[331,1003,1005,1008],{"class":333,"line":1004},16,[331,1006,1007],{"class":417},"    )",[331,1009,826],{"class":341},[331,1011,1013,1016],{"class":333,"line":1012},17,[331,1014,1015],{"class":417},"  }",[331,1017,607],{"class":341},[331,1019,1021,1023],{"class":333,"line":1020},18,[331,1022,939],{"class":417},[331,1024,826],{"class":341},[11,1026,1027],{},[64,1028],{"alt":1029,"height":308,"src":1030,"width":310},"The weather card, rendered by a component the page already had","https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fcontrolled.webp",[11,1032,1033,1036],{},[15,1034,1035],{},"Why you would pick it:"," the mental model is tiny (tool call in, component out), and your designers get full control. It is the right choice for the paths your users take every day.",[11,1038,1039,1042],{},[15,1040,1041],{},"The cost is coupling."," The tool name and its arguments are now a contract between the backend team that defines the tool and the frontend team that renders it. Change one side and the other breaks, like any API. And if the agent calls a tool nobody built a component for, the user sees nothing useful, so my renderer falls back to a placeholder for that case. Every new use case means another component.",[11,1044,1045,1046,1049,1050,268],{},"In CopilotKit this is the ",[180,1047,1048],{},"useFrontendTool"," hook, which the webinar called ",[180,1051,1052],{},"useRenderToolCall",[28,1054,1056],{"id":1055},"_5-declarative-the-agent-describes-it-you-render-it","5. Declarative: the agent describes it, you render it",[11,1058,1059,1060,1063],{},"This is the pattern I find the most interesting. The agent sends a ",[50,1061,1062],{},"description"," of a UI, and your frontend renders it with your own components. The agent decides the structure. You decide how every button and text field looks.",[11,1065,1066,1067,1070],{},"The standard that people mean here is ",[15,1068,1069],{},"A2UI",", a JSON spec from Google. Its current version is v0.9.1, and v1.0 is a release candidate as I write this. Official renderers exist for Flutter, Lit, Angular and, since v0.9, React.",[11,1072,1073,1074,1077,1078,1081,1082,1085],{},"Two details of the spec shaped my demo. Components arrive as a ",[15,1075,1076],{},"flat list with ids",", and a component with the id ",[180,1079,1080],{},"root"," anchors the tree. And renderers should show ",[15,1083,1084],{},"placeholders for children that have not arrived yet",", which is what lets a UI appear progressively while the agent is still generating it.",[11,1087,1088],{},"Here is the form request. The agent sends the root first, pointing at children that do not exist yet:",[322,1090,1094],{"className":1091,"code":1092,"language":1093,"meta":327,"style":327},"language-json shiki shiki-themes tokyo-night","{\n  \"updateComponents\": {\n    \"surfaceId\": \"apply-a2442a63\",\n    \"components\": [\n      {\n        \"id\": \"root\",\n        \"component\": \"Column\",\n        \"children\": [\"title\", \"hint\", \"name\", \"email\", \"submit\"]\n      },\n      {\n        \"id\": \"title\",\n        \"component\": \"Text\",\n        \"variant\": \"h3\",\n        \"text\": \"Apply for the Example Card\"\n      }\n    ]\n  }\n}\n","json",[180,1095,1096,1101,1115,1136,1150,1155,1175,1195,1255,1262,1266,1284,1303,1323,1342,1347,1352,1357],{"__ignoreMap":327},[331,1097,1098],{"class":333,"line":334},[331,1099,1100],{"class":417},"{\n",[331,1102,1103,1106,1109,1111,1113],{"class":333,"line":401},[331,1104,1105],{"class":341},"  \"",[331,1107,1108],{"class":588},"updateComponents",[331,1110,345],{"class":341},[331,1112,354],{"class":341},[331,1114,680],{"class":417},[331,1116,1117,1120,1123,1125,1127,1129,1132,1134],{"class":333,"line":449},[331,1118,1119],{"class":341},"    \"",[331,1121,1122],{"class":413},"surfaceId",[331,1124,345],{"class":341},[331,1126,354],{"class":341},[331,1128,357],{"class":341},[331,1130,1131],{"class":348},"apply-a2442a63",[331,1133,345],{"class":341},[331,1135,607],{"class":341},[331,1137,1138,1140,1143,1145,1147],{"class":333,"line":519},[331,1139,1119],{"class":341},[331,1141,1142],{"class":413},"components",[331,1144,345],{"class":341},[331,1146,354],{"class":341},[331,1148,1149],{"class":417}," [\n",[331,1151,1152],{"class":333,"line":555},[331,1153,1154],{"class":417},"      {\n",[331,1156,1157,1160,1163,1165,1167,1169,1171,1173],{"class":333,"line":610},[331,1158,1159],{"class":341},"        \"",[331,1161,1162],{"class":709},"id",[331,1164,345],{"class":341},[331,1166,354],{"class":341},[331,1168,357],{"class":341},[331,1170,1080],{"class":348},[331,1172,345],{"class":341},[331,1174,607],{"class":341},[331,1176,1177,1179,1182,1184,1186,1188,1191,1193],{"class":333,"line":775},[331,1178,1159],{"class":341},[331,1180,1181],{"class":709},"component",[331,1183,345],{"class":341},[331,1185,354],{"class":341},[331,1187,357],{"class":341},[331,1189,1190],{"class":348},"Column",[331,1192,345],{"class":341},[331,1194,607],{"class":341},[331,1196,1197,1199,1202,1204,1206,1209,1211,1214,1216,1218,1220,1223,1225,1227,1229,1232,1234,1236,1238,1241,1243,1245,1247,1250,1252],{"class":333,"line":787},[331,1198,1159],{"class":341},[331,1200,1201],{"class":709},"children",[331,1203,345],{"class":341},[331,1205,354],{"class":341},[331,1207,1208],{"class":417}," [",[331,1210,345],{"class":341},[331,1212,1213],{"class":348},"title",[331,1215,345],{"class":341},[331,1217,364],{"class":341},[331,1219,357],{"class":341},[331,1221,1222],{"class":348},"hint",[331,1224,345],{"class":341},[331,1226,364],{"class":341},[331,1228,357],{"class":341},[331,1230,1231],{"class":348},"name",[331,1233,345],{"class":341},[331,1235,364],{"class":341},[331,1237,357],{"class":341},[331,1239,1240],{"class":348},"email",[331,1242,345],{"class":341},[331,1244,364],{"class":341},[331,1246,357],{"class":341},[331,1248,1249],{"class":348},"submit",[331,1251,345],{"class":341},[331,1253,1254],{"class":417},"]\n",[331,1256,1257,1260],{"class":333,"line":820},[331,1258,1259],{"class":417},"      }",[331,1261,607],{"class":341},[331,1263,1264],{"class":333,"line":829},[331,1265,1154],{"class":417},[331,1267,1268,1270,1272,1274,1276,1278,1280,1282],{"class":333,"line":848},[331,1269,1159],{"class":341},[331,1271,1162],{"class":709},[331,1273,345],{"class":341},[331,1275,354],{"class":341},[331,1277,357],{"class":341},[331,1279,1213],{"class":348},[331,1281,345],{"class":341},[331,1283,607],{"class":341},[331,1285,1286,1288,1290,1292,1294,1296,1299,1301],{"class":333,"line":858},[331,1287,1159],{"class":341},[331,1289,1181],{"class":709},[331,1291,345],{"class":341},[331,1293,354],{"class":341},[331,1295,357],{"class":341},[331,1297,1298],{"class":348},"Text",[331,1300,345],{"class":341},[331,1302,607],{"class":341},[331,1304,1305,1307,1310,1312,1314,1316,1319,1321],{"class":333,"line":870},[331,1306,1159],{"class":341},[331,1308,1309],{"class":709},"variant",[331,1311,345],{"class":341},[331,1313,354],{"class":341},[331,1315,357],{"class":341},[331,1317,1318],{"class":348},"h3",[331,1320,345],{"class":341},[331,1322,607],{"class":341},[331,1324,1325,1327,1330,1332,1334,1336,1339],{"class":333,"line":891},[331,1326,1159],{"class":341},[331,1328,1329],{"class":709},"text",[331,1331,345],{"class":341},[331,1333,354],{"class":341},[331,1335,357],{"class":341},[331,1337,1338],{"class":348},"Apply for the Example Card",[331,1340,1341],{"class":341},"\"\n",[331,1343,1344],{"class":333,"line":952},[331,1345,1346],{"class":417},"      }\n",[331,1348,1349],{"class":333,"line":1004},[331,1350,1351],{"class":417},"    ]\n",[331,1353,1354],{"class":333,"line":1012},[331,1355,1356],{"class":417},"  }\n",[331,1358,1359],{"class":333,"line":1020},[331,1360,398],{"class":417},[11,1362,1363,1364,1366],{},"Each chunk travels inside an ",[180,1365,206],{}," as a JSON Patch, which is the same mechanism as state deltas from Part 1:",[322,1368,1370],{"className":324,"code":1369,"language":326,"meta":327,"style":327},"yield {\"type\": \"ACTIVITY_DELTA\", \"messageId\": mid, \"activityType\": \"a2ui-surface\",\n       \"patch\": [{\"op\": \"add\", \"path\": \"\u002Fa2ui_operations\u002F-\",\n                  \"value\": {\"updateComponents\": {\"surfaceId\": sid, \"components\": chunk}}}]}\n",[180,1371,1372,1425,1475],{"__ignoreMap":327},[331,1373,1374,1376,1378,1380,1382,1384,1386,1388,1390,1392,1394,1396,1398,1400,1402,1405,1407,1409,1412,1414,1416,1418,1421,1423],{"class":333,"line":334},[331,1375,338],{"class":337},[331,1377,342],{"class":341},[331,1379,345],{"class":341},[331,1381,349],{"class":348},[331,1383,345],{"class":341},[331,1385,354],{"class":341},[331,1387,357],{"class":341},[331,1389,206],{"class":348},[331,1391,345],{"class":341},[331,1393,364],{"class":341},[331,1395,357],{"class":341},[331,1397,581],{"class":348},[331,1399,345],{"class":341},[331,1401,354],{"class":341},[331,1403,1404],{"class":376}," mid",[331,1406,364],{"class":341},[331,1408,357],{"class":341},[331,1410,1411],{"class":348},"activityType",[331,1413,345],{"class":341},[331,1415,354],{"class":341},[331,1417,357],{"class":341},[331,1419,1420],{"class":348},"a2ui-surface",[331,1422,345],{"class":341},[331,1424,607],{"class":341},[331,1426,1427,1429,1432,1434,1436,1439,1441,1444,1446,1448,1450,1453,1455,1457,1459,1462,1464,1466,1468,1471,1473],{"class":333,"line":401},[331,1428,613],{"class":341},[331,1430,1431],{"class":348},"patch",[331,1433,345],{"class":341},[331,1435,354],{"class":341},[331,1437,1438],{"class":341}," [{",[331,1440,345],{"class":341},[331,1442,1443],{"class":348},"op",[331,1445,345],{"class":341},[331,1447,354],{"class":341},[331,1449,357],{"class":341},[331,1451,1452],{"class":348},"add",[331,1454,345],{"class":341},[331,1456,364],{"class":341},[331,1458,357],{"class":341},[331,1460,1461],{"class":348},"path",[331,1463,345],{"class":341},[331,1465,354],{"class":341},[331,1467,357],{"class":341},[331,1469,1470],{"class":348},"\u002Fa2ui_operations\u002F-",[331,1472,345],{"class":341},[331,1474,607],{"class":341},[331,1476,1477,1480,1483,1485,1487,1489,1491,1493,1495,1497,1499,1501,1503,1505,1507,1510,1512,1514,1516,1518,1520,1523],{"class":333,"line":449},[331,1478,1479],{"class":341},"                  \"",[331,1481,1482],{"class":348},"value",[331,1484,345],{"class":341},[331,1486,354],{"class":341},[331,1488,342],{"class":341},[331,1490,345],{"class":341},[331,1492,1108],{"class":348},[331,1494,345],{"class":341},[331,1496,354],{"class":341},[331,1498,342],{"class":341},[331,1500,345],{"class":341},[331,1502,1122],{"class":348},[331,1504,345],{"class":341},[331,1506,354],{"class":341},[331,1508,1509],{"class":376}," sid",[331,1511,364],{"class":341},[331,1513,357],{"class":341},[331,1515,1142],{"class":348},[331,1517,345],{"class":341},[331,1519,354],{"class":341},[331,1521,1522],{"class":376}," chunk",[331,1524,1525],{"class":341},"}}}]}\n",[11,1527,1528,1529,1532,1533,354],{},"The client rebuilds the surface from the operations and renders it. The ",[180,1530,1531],{},"switch"," is the important part, because ",[15,1534,1535],{},"it is your catalog",[322,1537,1539],{"className":661,"code":1538,"language":663,"meta":327,"style":327},"switch (c.component) {\n  case 'Column':\n    return el('div', { class: 'col' }, ...(c.children || []).map(node));\n  case 'Text':\n    return el('p', { class: c.variant || '' }, c.text);\n  case 'TextField': \u002F* ...input bound to a JSON pointer in the data model... *\u002F\n  case 'Button': \u002F* ...sends an action back to the agent... *\u002F\n  default:\n    return el(\n      'div',\n      { class: 'placeholder' },\n      `[${c.component} is not in this app's catalog]`,\n    );\n}\n",[180,1540,1541,1558,1571,1638,1650,1698,1715,1731,1738,1746,1756,1776,1800,1806],{"__ignoreMap":327},[331,1542,1543,1545,1547,1550,1552,1554,1556],{"class":333,"line":334},[331,1544,1531],{"class":337},[331,1546,178],{"class":417},[331,1548,1549],{"class":432},"c",[331,1551,268],{"class":341},[331,1553,1181],{"class":709},[331,1555,436],{"class":417},[331,1557,680],{"class":417},[331,1559,1560,1563,1565,1567,1569],{"class":333,"line":401},[331,1561,1562],{"class":337},"  case",[331,1564,716],{"class":341},[331,1566,1190],{"class":348},[331,1568,722],{"class":341},[331,1570,446],{"class":341},[331,1572,1573,1575,1577,1579,1581,1583,1585,1587,1589,1592,1594,1596,1599,1601,1603,1605,1609,1611,1613,1615,1617,1620,1623,1625,1628,1630,1633,1636],{"class":333,"line":449},[331,1574,851],{"class":730},[331,1576,734],{"class":588},[331,1578,418],{"class":417},[331,1580,722],{"class":341},[331,1582,745],{"class":348},[331,1584,722],{"class":341},[331,1586,364],{"class":341},[331,1588,342],{"class":417},[331,1590,1591],{"class":757}," class",[331,1593,354],{"class":341},[331,1595,716],{"class":341},[331,1597,1598],{"class":348},"col",[331,1600,722],{"class":341},[331,1602,770],{"class":417},[331,1604,364],{"class":341},[331,1606,1608],{"class":1607},"sipP7"," ...",[331,1610,418],{"class":417},[331,1612,1549],{"class":432},[331,1614,268],{"class":341},[331,1616,1201],{"class":709},[331,1618,1619],{"class":337}," ||",[331,1621,1622],{"class":417}," [])",[331,1624,268],{"class":341},[331,1626,1627],{"class":588},"map",[331,1629,418],{"class":417},[331,1631,1632],{"class":432},"node",[331,1634,1635],{"class":417},"))",[331,1637,826],{"class":341},[331,1639,1640,1642,1644,1646,1648],{"class":333,"line":519},[331,1641,1562],{"class":337},[331,1643,716],{"class":341},[331,1645,1298],{"class":348},[331,1647,722],{"class":341},[331,1649,446],{"class":341},[331,1651,1652,1654,1656,1658,1660,1662,1664,1666,1668,1670,1672,1675,1677,1679,1681,1684,1686,1688,1690,1692,1694,1696],{"class":333,"line":555},[331,1653,851],{"class":730},[331,1655,734],{"class":588},[331,1657,418],{"class":417},[331,1659,722],{"class":341},[331,1661,11],{"class":348},[331,1663,722],{"class":341},[331,1665,364],{"class":341},[331,1667,342],{"class":417},[331,1669,1591],{"class":757},[331,1671,354],{"class":341},[331,1673,1674],{"class":432}," c",[331,1676,268],{"class":341},[331,1678,1309],{"class":709},[331,1680,1619],{"class":337},[331,1682,1683],{"class":341}," ''",[331,1685,770],{"class":417},[331,1687,364],{"class":341},[331,1689,1674],{"class":432},[331,1691,268],{"class":341},[331,1693,1329],{"class":709},[331,1695,436],{"class":417},[331,1697,826],{"class":341},[331,1699,1700,1702,1704,1707,1709,1711],{"class":333,"line":610},[331,1701,1562],{"class":337},[331,1703,716],{"class":341},[331,1705,1706],{"class":348},"TextField",[331,1708,722],{"class":341},[331,1710,354],{"class":341},[331,1712,1714],{"class":1713},"sbD-w"," \u002F* ...input bound to a JSON pointer in the data model... *\u002F\n",[331,1716,1717,1719,1721,1724,1726,1728],{"class":333,"line":775},[331,1718,1562],{"class":337},[331,1720,716],{"class":341},[331,1722,1723],{"class":348},"Button",[331,1725,722],{"class":341},[331,1727,354],{"class":341},[331,1729,1730],{"class":1713}," \u002F* ...sends an action back to the agent... *\u002F\n",[331,1732,1733,1736],{"class":333,"line":787},[331,1734,1735],{"class":337},"  default",[331,1737,446],{"class":341},[331,1739,1740,1742,1744],{"class":333,"line":820},[331,1741,851],{"class":730},[331,1743,734],{"class":588},[331,1745,737],{"class":417},[331,1747,1748,1750,1752,1754],{"class":333,"line":829},[331,1749,861],{"class":341},[331,1751,745],{"class":348},[331,1753,722],{"class":341},[331,1755,607],{"class":341},[331,1757,1758,1761,1763,1765,1767,1770,1772,1774],{"class":333,"line":848},[331,1759,1760],{"class":417},"      {",[331,1762,1591],{"class":757},[331,1764,354],{"class":341},[331,1766,716],{"class":341},[331,1768,1769],{"class":348},"placeholder",[331,1771,722],{"class":341},[331,1773,770],{"class":417},[331,1775,607],{"class":341},[331,1777,1778,1781,1783,1785,1787,1789,1791,1793,1796,1798],{"class":333,"line":858},[331,1779,1780],{"class":341},"      `",[331,1782,498],{"class":348},[331,1784,928],{"class":709},[331,1786,1549],{"class":432},[331,1788,268],{"class":341},[331,1790,1181],{"class":709},[331,1792,939],{"class":709},[331,1794,1795],{"class":348}," is not in this app's catalog]",[331,1797,945],{"class":341},[331,1799,607],{"class":341},[331,1801,1802,1804],{"class":333,"line":870},[331,1803,1007],{"class":417},[331,1805,826],{"class":341},[331,1807,1808],{"class":333,"line":891},[331,1809,398],{"class":417},[11,1811,1812],{},"If the agent asks for a component you did not define, it gets a placeholder, not an error and not arbitrary markup. That is the trade you are making: less freedom for the agent, a safe and consistent UI for you.",[11,1814,1815,1818,1819,1822,1823,1826,1827,1829,1830,354],{},[15,1816,1817],{},"Going back to the agent."," When the user clicks the button, the page resolves the bound values and starts a ",[50,1820,1821],{},"new run"," with the action in ",[180,1824,1825],{},"forwardedProps",". There is no new chat message. The server answers by replacing the form with a confirmation, using an ",[180,1828,202],{}," on the same ",[180,1831,581],{},[322,1833,1835],{"className":661,"code":1834,"language":663,"meta":327,"style":327},"run({ action: { name: ev.name, surfaceId: s.id, context } });\n\u002F\u002F sent as: forwardedProps: { a2uiAction: { userAction: action } }\n",[180,1836,1837,1890],{"__ignoreMap":327},[331,1838,1839,1842,1845,1848,1850,1852,1855,1857,1860,1862,1864,1866,1869,1871,1874,1876,1878,1880,1883,1885,1888],{"class":333,"line":334},[331,1840,1841],{"class":588},"run",[331,1843,1844],{"class":417},"({",[331,1846,1847],{"class":757}," action",[331,1849,354],{"class":341},[331,1851,342],{"class":417},[331,1853,1854],{"class":757}," name",[331,1856,354],{"class":341},[331,1858,1859],{"class":432}," ev",[331,1861,268],{"class":341},[331,1863,1231],{"class":709},[331,1865,364],{"class":341},[331,1867,1868],{"class":757}," surfaceId",[331,1870,354],{"class":341},[331,1872,1873],{"class":432}," s",[331,1875,268],{"class":341},[331,1877,1162],{"class":709},[331,1879,364],{"class":341},[331,1881,1882],{"class":432}," context",[331,1884,770],{"class":417},[331,1886,1887],{"class":417}," })",[331,1889,826],{"class":341},[331,1891,1892],{"class":333,"line":401},[331,1893,1894],{"class":1713},"\u002F\u002F sent as: forwardedProps: { a2uiAction: { userAction: action } }\n",[11,1896,1897],{},[64,1898],{"alt":1899,"height":1900,"src":1901,"width":69},"Four stacked steps: run 1 streams the form as activity events, the app renders it from its own catalog, the click starts a new run with forwardedProps, and run 2 replaces the form with a confirmation.",1640,"https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fdeclarative-loop-part-2.webp",[11,1903,1904,1907,1908,1911,1912,183,1915,183,1917,1920],{},[15,1905,1906],{},"A note on fidelity."," My demo is ",[50,1909,1910],{},"A2UI-style",", not A2UI. I used the real operation names (",[180,1913,1914],{},"createSurface",[180,1916,1108],{},[180,1918,1919],{},"updateDataModel","), but my component fields are simplified and I did not validate against the spec's catalog schema. Treat it as a way to understand the shape. For real work, use an official renderer.",[11,1922,1923,1926],{},[15,1924,1925],{},"The pros and cons."," Frontend and backend are decoupled: the frontend team perfects each component once, and the backend team decides when to use them. It extends to any framework, because it is only JSON. The limits are real, too. You are constrained by the catalog, so a calculator with a custom display does not fit. And the structure can differ between runs. As far as I can tell, declarative UI is strongest today at forms and cards.",[28,1928,1930],{"id":1929},"_6-open-ended-the-agent-hands-you-a-whole-ui","6. Open-ended: the agent hands you a whole UI",[11,1932,1933,1934,1937],{},"At the far end, the agent (or a tool server) returns finished HTML, and your app is just the container. This is the idea behind ",[15,1935,1936],{},"MCP Apps",", the extension that lets an MCP server ship an interactive UI into a chat. It reached production-ready status on 26 January 2026, as the first official MCP extension. Claude, VS Code Insiders and Goose supported it that day, and ChatGPT followed later that week with some features missing.",[11,1939,1940],{},"The appeal is that you can embed someone else's whole application in your chat, and the UI can be anything. The costs are exactly what you would expect: it is hard to style, the same request can look different each time, and you are now running code you did not write.",[11,1942,1943],{},"My demo only shows the core idea. It is not an MCP Apps implementation, because real MCP Apps involve an MCP server and more machinery than I built. The server sends an HTML string and the page puts it in a sandboxed iframe:",[322,1945,1947],{"className":661,"code":1946,"language":663,"meta":327,"style":327},"const f = el('iframe', {\n  class: 'widget',\n  sandbox: 'allow-scripts',\n  title: a.content.title,\n});\nf.srcdoc = a.content.html;\n",[180,1948,1949,1973,1989,2005,2025,2032],{"__ignoreMap":327},[331,1950,1951,1953,1956,1958,1960,1962,1964,1967,1969,1971],{"class":333,"line":334},[331,1952,671],{"class":670},[331,1954,1955],{"class":337}," f",[331,1957,677],{"class":341},[331,1959,734],{"class":588},[331,1961,418],{"class":417},[331,1963,722],{"class":341},[331,1965,1966],{"class":348},"iframe",[331,1968,722],{"class":341},[331,1970,364],{"class":341},[331,1972,680],{"class":417},[331,1974,1975,1978,1980,1982,1985,1987],{"class":333,"line":401},[331,1976,1977],{"class":757},"  class",[331,1979,354],{"class":341},[331,1981,716],{"class":341},[331,1983,1984],{"class":348},"widget",[331,1986,722],{"class":341},[331,1988,607],{"class":341},[331,1990,1991,1994,1996,1998,2001,2003],{"class":333,"line":449},[331,1992,1993],{"class":757},"  sandbox",[331,1995,354],{"class":341},[331,1997,716],{"class":341},[331,1999,2000],{"class":348},"allow-scripts",[331,2002,722],{"class":341},[331,2004,607],{"class":341},[331,2006,2007,2010,2012,2015,2017,2019,2021,2023],{"class":333,"line":519},[331,2008,2009],{"class":757},"  title",[331,2011,354],{"class":341},[331,2013,2014],{"class":432}," a",[331,2016,268],{"class":341},[331,2018,634],{"class":432},[331,2020,268],{"class":341},[331,2022,1213],{"class":709},[331,2024,607],{"class":341},[331,2026,2027,2030],{"class":333,"line":555},[331,2028,2029],{"class":417},"})",[331,2031,826],{"class":341},[331,2033,2034,2037,2039,2042,2044,2046,2048,2050,2052,2055],{"class":333,"line":610},[331,2035,2036],{"class":432},"f",[331,2038,268],{"class":341},[331,2040,2041],{"class":709},"srcdoc",[331,2043,677],{"class":341},[331,2045,2014],{"class":432},[331,2047,268],{"class":341},[331,2049,634],{"class":432},[331,2051,268],{"class":341},[331,2053,2054],{"class":709},"html",[331,2056,826],{"class":341},[11,2058,2059,2060,2063,2064,2063,2067,2070],{},"The attribute that matters is ",[180,2061,2062],{},"sandbox=\"allow-scripts\""," ",[50,2065,2066],{},"without",[180,2068,2069],{},"allow-same-origin",". The widget can run its own JavaScript, but it cannot touch your page, your cookies or your storage. The widget in the demo proves it by trying:",[322,2072,2074],{"className":661,"code":2073,"language":663,"meta":327,"style":327},"try {\n  void window.parent.document.title;\n  m = 'can read the host page (bad)';\n} catch (e) {\n  m = 'host page: blocked (' + e.name + ')';\n}\n",[180,2075,2076,2083,2107,2123,2139,2172],{"__ignoreMap":327},[331,2077,2078,2081],{"class":333,"line":334},[331,2079,2080],{"class":337},"try",[331,2082,680],{"class":417},[331,2084,2085,2088,2091,2093,2096,2098,2101,2103,2105],{"class":333,"line":401},[331,2086,2087],{"class":341},"  void",[331,2089,2090],{"class":432}," window",[331,2092,268],{"class":341},[331,2094,2095],{"class":432},"parent",[331,2097,268],{"class":341},[331,2099,2100],{"class":432},"document",[331,2102,268],{"class":341},[331,2104,1213],{"class":709},[331,2106,826],{"class":341},[331,2108,2109,2112,2114,2116,2119,2121],{"class":333,"line":449},[331,2110,2111],{"class":432},"  m",[331,2113,677],{"class":341},[331,2115,716],{"class":341},[331,2117,2118],{"class":348},"can read the host page (bad)",[331,2120,722],{"class":341},[331,2122,826],{"class":341},[331,2124,2125,2127,2130,2132,2135,2137],{"class":333,"line":519},[331,2126,939],{"class":417},[331,2128,2129],{"class":337}," catch",[331,2131,178],{"class":417},[331,2133,2134],{"class":432},"e",[331,2136,436],{"class":417},[331,2138,680],{"class":417},[331,2140,2141,2143,2145,2147,2150,2152,2155,2158,2160,2162,2164,2166,2168,2170],{"class":333,"line":555},[331,2142,2111],{"class":432},[331,2144,677],{"class":341},[331,2146,716],{"class":341},[331,2148,2149],{"class":348},"host page: blocked (",[331,2151,722],{"class":341},[331,2153,2154],{"class":341}," +",[331,2156,2157],{"class":432}," e",[331,2159,268],{"class":341},[331,2161,1231],{"class":709},[331,2163,2154],{"class":341},[331,2165,716],{"class":341},[331,2167,436],{"class":348},[331,2169,722],{"class":341},[331,2171,826],{"class":341},[331,2173,2174],{"class":333,"line":610},[331,2175,398],{"class":417},[11,2177,2178,2179,2182],{},"In the browser it prints ",[180,2180,2181],{},"host page: blocked (SecurityError)",". The widget is a tiny counter, and the server picks a random background colour on every request, so two runs of \"show me a widget\" give two differently coloured widgets. That is the unpredictability in miniature.",[11,2184,2185],{},[64,2186],{"alt":2187,"height":308,"src":2188,"width":310},"A widget the agent wrote, running inside a sandboxed iframe","https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fopen-ended.webp",[11,2190,2191],{},"During the webinar, a participant pointed out that the MCP Apps spec goes further than a plain sandbox, with nested iframes. If you build on it, read the spec's security section rather than trusting my sketch.",[28,2193,2195],{"id":2194},"_7-shared-state-the-ui-can-write-back","7. Shared state: the UI can write back",[11,2197,2198],{},"So far the agent has pushed things at the user. The state events from Part 1 work in both directions.",[11,2200,2201,2202,2204],{},"When you ask the demo to \"add a note about pizza\", the server sends a ",[180,2203,219],{},". The page renders the note as an editable field on the right:",[322,2206,2208],{"className":324,"code":2207,"language":326,"meta":327,"style":327},"yield {\"type\": \"STATE_DELTA\",\n       \"delta\": [{\"op\": \"add\", \"path\": \"\u002Fnotes\u002F-\", \"value\": {\"id\": new_id(), \"text\": topic}}]}\n",[180,2209,2210,2232],{"__ignoreMap":327},[331,2211,2212,2214,2216,2218,2220,2222,2224,2226,2228,2230],{"class":333,"line":334},[331,2213,338],{"class":337},[331,2215,342],{"class":341},[331,2217,345],{"class":341},[331,2219,349],{"class":348},[331,2221,345],{"class":341},[331,2223,354],{"class":341},[331,2225,357],{"class":341},[331,2227,219],{"class":348},[331,2229,345],{"class":341},[331,2231,607],{"class":341},[331,2233,2234,2236,2238,2240,2242,2244,2246,2248,2250,2252,2254,2256,2258,2260,2262,2264,2266,2268,2270,2273,2275,2277,2279,2281,2283,2285,2287,2289,2291,2293,2295,2297,2299,2301,2303,2305,2307,2309,2312],{"class":333,"line":401},[331,2235,613],{"class":341},[331,2237,488],{"class":348},[331,2239,345],{"class":341},[331,2241,354],{"class":341},[331,2243,1438],{"class":341},[331,2245,345],{"class":341},[331,2247,1443],{"class":348},[331,2249,345],{"class":341},[331,2251,354],{"class":341},[331,2253,357],{"class":341},[331,2255,1452],{"class":348},[331,2257,345],{"class":341},[331,2259,364],{"class":341},[331,2261,357],{"class":341},[331,2263,1461],{"class":348},[331,2265,345],{"class":341},[331,2267,354],{"class":341},[331,2269,357],{"class":341},[331,2271,2272],{"class":348},"\u002Fnotes\u002F-",[331,2274,345],{"class":341},[331,2276,364],{"class":341},[331,2278,357],{"class":341},[331,2280,1482],{"class":348},[331,2282,345],{"class":341},[331,2284,354],{"class":341},[331,2286,342],{"class":341},[331,2288,345],{"class":341},[331,2290,1162],{"class":348},[331,2292,345],{"class":341},[331,2294,354],{"class":341},[331,2296,589],{"class":588},[331,2298,592],{"class":417},[331,2300,364],{"class":341},[331,2302,357],{"class":341},[331,2304,1329],{"class":348},[331,2306,345],{"class":341},[331,2308,354],{"class":341},[331,2310,2311],{"class":376}," topic",[331,2313,2314],{"class":341},"}}]}\n",[11,2316,2317],{},"Now edit the note in the page. It becomes \"pizza with extra cheese\". Ask \"what notes do I have\" and the agent reads the state that came in with the request, not its own memory:",[322,2319,2323],{"className":2320,"code":2322,"language":1329,"meta":327},[2321],"language-text","You have 1 note(s), exactly as they are in your state: 1) pizza with extra cheese\n",[180,2324,2322],{"__ignoreMap":327},[11,2326,2327,2328,2331],{},"That works because ",[180,2329,2330],{},"state"," is part of the run input, and the page sends its current copy on every run. The agent writes, the user edits, the agent sees the edit. It is similar to editing a document that an agent generated in Claude or ChatGPT and having the agent see your changes. As Part 1 warned, state round-trips through the client, so do not put secrets in it.",[28,2333,2335],{"id":2334},"_8-choosing-between-them","8. Choosing between them",[11,2337,2338],{},"They are not mutually exclusive. A good agent app will probably use all three:",[71,2340,2341,2354],{},[74,2342,2343],{},[77,2344,2345,2348,2351],{},[80,2346,2347],{},"If you want...",[80,2349,2350],{},"Use",[80,2352,2353],{},"Because",[93,2355,2356,2366,2376,2386],{},[77,2357,2358,2361,2363],{},[98,2359,2360],{},"Weather cards, charts and flows your users hit every day",[98,2362,102],{},[98,2364,2365],{},"Pixel-perfect and simple. Pay the coupling cost once.",[77,2367,2368,2371,2373],{},[98,2369,2370],{},"Forms and views that vary per request, owned by separate teams",[98,2372,118],{},[98,2374,2375],{},"The agent picks the structure, your design system draws it.",[77,2377,2378,2381,2383],{},[98,2379,2380],{},"Partner apps or one-off interactive widgets",[98,2382,134],{},[98,2384,2385],{},"The other side owns the UI, you contain it.",[77,2387,2388,2391,2394],{},[98,2389,2390],{},"An editable document or canvas shared with the agent",[98,2392,2393],{},"Shared state",[98,2395,2396],{},"Both sides read and write the same object.",[28,2398,2400],{"id":2399},"_9-gotchas","9. Gotchas",[169,2402,2403,2421,2431,2454],{},[172,2404,2405,2408,2409,2412,2413,2416,2417,2420],{},[15,2406,2407],{},"Everything the model writes is untrusted input."," I tested this with a hostile name in the form: ",[180,2410,2411],{},"\u003Cimg src=x onerror=\"window.__xss=1\">",". The agent put it into its confirmation text, and the page rendered it as literal text. No element was created and no script ran, because the renderer only ever uses ",[180,2414,2415],{},"textContent",", never ",[180,2418,2419],{},"innerHTML",". The AG-UI spec says the same: apps must not render streamed content as executable markup.",[172,2422,2423,2426,2427,2430],{},[15,2424,2425],{},"Allowlist your components."," In declarative UI the ",[180,2428,2429],{},"default:"," branch of your renderer is a security feature, not an afterthought.",[172,2432,2433,2436,2437,183,2440,183,2443,2446,2447,183,2449,183,2451,2453],{},[15,2434,2435],{},"Specs move."," A2UI renamed its messages between v0.8 (",[180,2438,2439],{},"surfaceUpdate",[180,2441,2442],{},"dataModelUpdate",[180,2444,2445],{},"beginRendering",") and v0.9.1 (",[180,2448,1914],{},[180,2450,1108],{},[180,2452,1919],{},"). Even CopilotKit's repo README showed the older names when I read it. Pin your versions.",[172,2455,2456,2459,2460,2462,2463,2466],{},[15,2457,2458],{},"A2UI is not AG-UI."," Easy to mix up. A2UI defines ",[50,2461,163],{}," the interface is, AG-UI is the ",[50,2464,2465],{},"wire"," that delivers it.",[11,2468,2469],{},"Here is the hostile input from the first gotcha, shown as plain text in the confirmation:",[11,2471,2472],{},[64,2473],{"alt":2474,"height":308,"src":2475,"width":310},"The confirmation message showing the hostile name as literal text, with no element created and no script run","https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fno-xss.webp",[28,2477,2479],{"id":2478},"_10-run-it-yourself","10. Run it yourself",[11,2481,2482],{},"The quickest way is the embed above. To run the server version on your machine you need Python 3 and a browser. There is nothing to install.",[11,2484,2485,2486,2490],{},"Download the two files from the ",[22,2487,2489],{"href":285,"rel":2488},[151],"Gist"," into one folder, then:",[322,2492,2496],{"className":2493,"code":2494,"language":2495,"meta":327,"style":327},"language-bash shiki shiki-themes tokyo-night","python3 server.py\n# open http:\u002F\u002Flocalhost:8001\n","bash",[180,2497,2498,2506],{"__ignoreMap":327},[331,2499,2500,2503],{"class":333,"line":334},[331,2501,2502],{"class":432},"python3",[331,2504,2505],{"class":348}," server.py\n",[331,2507,2508],{"class":333,"line":401},[331,2509,2510],{"class":1713},"# open http:\u002F\u002Flocalhost:8001\n",[11,2512,2513,2514,183,2517,183,2520,183,2523,183,2526,2529],{},"Click the chips: ",[50,2515,2516],{},"weather in Mumbai",[50,2518,2519],{},"give me an application form",[50,2521,2522],{},"add a note about pizza",[50,2524,2525],{},"what notes do I have",[50,2527,2528],{},"show me a widget",". Watch the event stream on the right.",[28,2531,2533],{"id":2532},"whats-next","What's next",[11,2535,2536],{},"My demo has no model in it, so the next step is the real thing: a real LLM generating the A2UI, rendered by an official renderer. That is where the \"validate, and send the errors back so the model can fix them\" loop matters. I also want to look at agent steering, where the user redirects an agent mid-run, since the interrupts from Part 1 are the building block for it.",[11,2538,2539,2540,2543],{},"If you take one thing from this post, make it the question ",[50,2541,2542],{},"who owns the pixels?"," Start with controlled UI for your everyday paths, reach for declarative when the structure must vary, and treat open-ended as the escape hatch you contain. AG-UI carries all three, so you do not have to pick one up front.",[28,2545,2547],{"id":2546},"credits-and-further-reading","Credits and further reading",[11,2549,2550],{},"The three-pattern framing comes from CopilotKit's generative UI repo and a webinar by their team. They also created AG-UI, so read their material with that in mind. The demo code is mine.",[169,2552,2553,2559,2571,2577,2584,2591],{},[172,2554,2555],{},[22,2556,2558],{"href":149,"rel":2557},[151],"CopilotKit generative UI repo",[172,2560,2561,203,2566],{},[22,2562,2565],{"href":2563,"rel":2564},"https:\u002F\u002Fa2ui.org\u002Fspecification\u002Fv1.0-a2ui\u002F",[151],"A2UI specification",[22,2567,2570],{"href":2568,"rel":2569},"https:\u002F\u002Fdevelopers.googleblog.com\u002Fa2ui-v0-9-generative-ui\u002F",[151],"A2UI v0.9 announcement",[172,2572,2573],{},[22,2574,2576],{"href":229,"rel":2575},[151],"A2UI over AG-UI (assistant-ui)",[172,2578,2579],{},[22,2580,2583],{"href":2581,"rel":2582},"https:\u002F\u002Falpic.ai\u002Fblog\u002Fmcp-apps-goes-official-claude-chatgpt-support",[151],"MCP Apps goes official (Alpic)",[172,2585,2586],{},[22,2587,2590],{"href":2588,"rel":2589},"https:\u002F\u002Fdocs.ag-ui.com\u002Fconcepts\u002Fevents",[151],"AG-UI docs: events",[172,2592,2593],{},[22,2594,2595],{"href":24},"Part 1: how agents talk to your frontend",[2597,2598,2599],"style",{},"html pre.shiki code .sd1Qi, html code.shiki .sd1Qi{--shiki-default:#BB9AF7}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sPY7s, html code.shiki .sPY7s{--shiki-default:#9ECE6A}html pre.shiki code .sGX4V, html code.shiki .sGX4V{--shiki-default:#A9B1D6}html pre.shiki code .sySf4, html code.shiki .sySf4{--shiki-default:#0DB9D7}html pre.shiki code .sgJMe, html code.shiki .sgJMe{--shiki-default:#9ABDF5}html pre.shiki code .sOJ5S, html code.shiki .sOJ5S{--shiki-default:#FF9E64}html pre.shiki code .sE3pS, html code.shiki .sE3pS{--shiki-default:#C0CAF5}html pre.shiki code .s3R4Z, html code.shiki .s3R4Z{--shiki-default:#7AA2F7}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sN7LL, html code.shiki .sN7LL{--shiki-default:#9D7CD8;--shiki-default-font-style:italic}html pre.shiki code .sT800, html code.shiki .sT800{--shiki-default:#E0AF68}html pre.shiki code .su0L4, html code.shiki .su0L4{--shiki-default:#7DCFFF}html pre.shiki code .sEsAJ, html code.shiki .sEsAJ{--shiki-default:#BB9AF7;--shiki-default-font-style:italic}html pre.shiki code .syYvs, html code.shiki .syYvs{--shiki-default:#73DACA}html pre.shiki code .sipP7, html code.shiki .sipP7{--shiki-default:#F7768E;--shiki-default-font-weight:bold}html pre.shiki code .sbD-w, html code.shiki .sbD-w{--shiki-default:#51597D;--shiki-default-font-style:italic}",{"title":327,"searchDepth":401,"depth":401,"links":2601},[2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2614],{"id":30,"depth":401,"text":31},{"id":44,"depth":401,"text":45},{"id":156,"depth":401,"text":157},{"id":250,"depth":401,"text":251},{"id":313,"depth":401,"text":314},{"id":1055,"depth":401,"text":1056},{"id":1929,"depth":401,"text":1930},{"id":2194,"depth":401,"text":2195},{"id":2334,"depth":401,"text":2335},{"id":2399,"depth":401,"text":2400},{"id":2478,"depth":401,"text":2479},{"id":2532,"depth":401,"text":2533},{"id":2546,"depth":401,"text":2547},"#ffaa00","https:\u002F\u002Fres.cloudinary.com\u002Fc552lpdm\u002Fimage\u002Fupload\u002Fag-ui-2-cover.webp","Dark cover reading \"Generative UI: the agent picks the interface\" beside an app panel containing a weather card, a form and a sandboxed widget.",null,"2026-10-29","Generative UI lets an agent choose the interface. Three patterns (controlled, A2UI-style, MCP Apps-style) and a runnable AG-UI demo with no LLM or libraries.",false,"md",{},true,"\u002Fblogs\u002Fag-ui-part-2-generative-ui",{"title":6,"description":2620},"blogs\u002Fag-ui-part-2-generative-ui",[2629,17,2630,2631],"AG-UI","AI Engineering","Frontend",[2633,2634,2635],"Generative UI comes in three patterns, ordered by how much control you keep: controlled, declarative and open-ended.","AG-UI does not choose between them. It is the pipe that carries any of them to your frontend, plus the shared state that keeps both sides in sync.","My demo runs all three plus shared state with no LLM and no libraries, and you can watch every event arrive.","GbmbLlgVFMea0GRkIb-GkyLYH3smpgQAJWfMiRtrgek",{"newer":2618,"older":2638},{"title":2639,"path":24,"stem":2640,"draft":2621,"children":-1},"AG-UI Part 1: How Agents Talk to Your Frontend","blogs\u002Fag-ui-part-1-how-agents-talk-to-your-frontend",1793264230252]