<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Mete Atamel on Medium]]></title>
        <description><![CDATA[Stories by Mete Atamel on Medium]]></description>
        <link>https://medium.com/@meteatamel?source=rss-6444b44155fa------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*0D3MqBpzg9laGKlwvZJ7Uw.jpeg</url>
            <title>Stories by Mete Atamel on Medium</title>
            <link>https://medium.com/@meteatamel?source=rss-6444b44155fa------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 06:23:19 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@meteatamel/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[A2UI Express and Swappable Inference Formats]]></title>
            <link>https://medium.com/google-cloud/a2ui-express-and-swappable-inference-formats-08c00cec882f?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/08c00cec882f</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[generative-ai-use-cases]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Tue, 10 Nov 2026 08:22:03 GMT</pubDate>
            <atom:updated>2026-11-10T08:22:03.947Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gOvJWTpt3vBxllwF.png" /></figure><p>In my <a href="https://atamel.dev/posts/2026/03-17_agent_protocols_mcp_a2a_a2ui_agui/">Agent Protocols — MCP, A2A, A2UI, AG-UI</a> and <a href="https://atamel.dev/posts/2026/03-30_a2ui_with_adk/">Agent to UI Protocol (A2UI) with Agent Development Kit (ADK)</a> posts, I introduced <a href="https://a2ui.org/">A2UI</a> — Google’s declarative generative UI protocol that lets AI agents return rich, interactive user interfaces across web, mobile, and desktop using JSON messages (createSurface, updateComponents, updateDataModel, and deleteSurface).</p><p>While standard JSON is an ideal wire protocol for client renderers (Lit, Angular, React, Flutter), having an LLM generate deeply nested wire JSON token-by-token is verbose, token-heavy, and adds latency. Recently, the A2UI team introduced <a href="https://developers.googleblog.com/fast-reliable-a2ui/">Swappable Inference Formats</a> and made <strong>A2UI Express</strong> the new default format in the A2UI Agent SDKs — cutting output tokens by ~3x–4x, shrinking system prompts by more than half, and speeding up UI generation without requiring any changes to client renderers.</p><p>In this post, I’ll briefly introduce <strong>A2UI Express</strong> and walk through how to use it, based on the interactive explorer sample in my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2ui/a2ui-express">genai-beyond-basics repository</a>.</p><h3>What is A2UI Express?</h3><p>The key insight behind <a href="https://a2ui.org/concepts/inference-formats/">Swappable Inference Formats</a> is decoupling the <strong>inference format</strong> (what the LLM writes) from the <strong>wire format</strong> (the standard A2UI JSON that client renderers consume):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KY1eBdjOGGXPcqHT.png" /></figure><p>Using the official <a href="https://pypi.org/project/a2ui-agent-sdk/">A2UI Python Agent SDK</a> (a2ui-agent-sdk, along with <a href="https://www.npmjs.com/package/@a2ui/agent">@a2ui/agent</a> in TypeScript and a2ui_agent in Dart), you can swap the inference format factory on the server while keeping the wire protocol 100% identical:</p><ul><li><strong>Direct JSON (</strong><strong>DirectJsonFormatFactory)</strong>: The original &lt;a2ui-json&gt; format where the LLM generates full A2UI Wire JSON directly.</li><li><strong>A2UI Express (</strong><strong>ExpressFormatFactory, Default)</strong>: A compact, Python-like positional DSL enclosed in &lt;a2ui&gt;...&lt;/a2ui&gt; tags (root = Card(main_col), _ for omitted optional arguments, $/path = value for data-model bindings, and ?required(...) for validation checks). It cuts system prompt size by <strong>~52–55%</strong>, reduces output tokens by <strong>~3x–5.5x</strong>, and generates valid UIs reliably even with model thinking turned off (thinking_budget=0).</li><li><strong>Atom (</strong><strong>AtomFormatFactory, Experimental)</strong>: A Lisp S-expression DSL ((Card (Column :id &quot;main_col&quot; ...))).</li><li><strong>Elemental (</strong><strong>ElementalFormatFactory, Experimental)</strong>: An HTML Custom Elements DSL (&lt;ui-card id=&quot;root&quot;&gt;&lt;ui-column id=&quot;main_col&quot;&gt;...).</li></ul><h3>A2UI Express &amp; Swappable Inference Formats Explorer</h3><p>To see how this works in practice and compare the formats side-by-side, I built an interactive <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2ui/a2ui-express">A2UI Express &amp; Swappable Inference Formats Explorer</a> using a2ui-agent-sdk, google-genai, FastAPI, and the official <a href="https://www.npmjs.com/package/@a2ui/lit">@a2ui/lit</a> v0.9 Web Component renderer.</p><p>You can run the explorer locally with a single uv command:</p><pre>uv run --with a2ui-agent-sdk --with google-genai --with fastapi --with uvicorn python app.py</pre><p>Open http://127.0.0.1:8080 in your browser. At the top of the page, you can switch between 3 built-in UI examples (<em>Restaurant Reservation Form</em>, <em>Login Form with Validation</em>, and <em>Notification Permission Card</em>), toggle which format to compare against Direct JSON (<em>A2UI Express</em>, <em>Atom</em>, or <em>Elemental</em>), or enter a custom prompt and click <strong>⚡ Generate Live with gemini-3.8-flash (thinking_budget=0)</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4icsX_XK5gOQ8Tbk.png" /></figure><p>Notice the summary metrics for the <em>Restaurant Reservation Form</em> example: <strong>A2UI Express</strong> uses <strong>4.0x fewer output tokens</strong> (262 vs. 1,054 tokens), runs <strong>4.0x faster</strong>, and shrinks the system prompt footprint by <strong>55%</strong> (6,059 vs. 13,424 input tokens) by replacing full JSON Schemas with compact function signatures.</p><p>Let’s look at each part of the explorer and the code under the hood.</p><h4>Python Agent SDK Code (a2ui-agent-sdk)</h4><p>On the server, we configure A2uiGenerator with our component catalog (BasicCatalog(ProtocolVersion.V0_9)), optional few-shot examples, and our chosen inference_format_factory:</p><pre>from a2ui.core.basic_catalog import BasicCatalog<br>from a2ui.core.schema import ProtocolVersion<br>from a2ui.inference_formats import ExpressFormatFactory  # or DirectJsonFormatFactory<br>from a2ui.processor import A2uiGenerator, CatalogConfig<br>​<br>catalog = BasicCatalog(ProtocolVersion.V0_9)<br>generator = A2uiGenerator(<br>    [CatalogConfig(catalog)],<br>    examples=[example_models],<br>    inference_format_factory=ExpressFormatFactory(),  # Default (or AtomFormatFactory / ElementalFormatFactory)<br>)<br>​<br>processor = generator.create_processor(<br>    {&quot;v0.9&quot;: {&quot;supportedCatalogIds&quot;: [catalog.catalog_id]}}<br>)</pre><p>As shown in the explorer, the Python SDK setup is virtually identical between <strong>Direct JSON</strong> on the left and <strong>A2UI Express</strong> on the right — the only difference is passing DirectJsonFormatFactory() vs. ExpressFormatFactory():</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2SqDh-vbsJvoOhC4.png" /></figure><h4>What the LLM Generates (&lt;a2ui-json&gt; vs. &lt;a2ui&gt;)</h4><p>Next, we inject processor.prompt_snippet into Gemini&#39;s system_instruction and ask the model to generate the UI with thinking_budget=0:</p><pre>from google import genai<br>from google.genai import types<br>​<br>client = genai.Client()<br>​<br>response = client.models.generate_content(<br>    model=&quot;gemini-3.8-flash&quot;,<br>    contents=&quot;Create a reservation form for Xi&#39;an Famous Foods with Name, Guests, Time, and Confirm button.&quot;,<br>    config=types.GenerateContentConfig(<br>        system_instruction=f&quot;You are an A2UI assistant.\n\n{processor.prompt_snippet}&quot;,<br>        thinking_config=types.ThinkingConfig(thinking_budget=0),<br>    ),<br>)</pre><p>In the next section of the explorer, we can compare what the LLM actually has to generate token-by-token:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9rqom-tV4trOXH2H.png" /></figure><p>On the left, <strong>Direct JSON</strong> requires <strong>168 lines (</strong><strong>3,796 characters / </strong><strong>1,054 tokens)</strong> of deeply nested JSON keys (&quot;id&quot;, &quot;component&quot;, &quot;path&quot;). On the right, <strong>A2UI Express</strong> expresses the exact same reservation form—including surface initialization, data-model bindings ($/reservation/...), layout hierarchy, a required validation check (?required(...)), and button event actions—in just <strong>20 lines (</strong><strong>945 characters / </strong><strong>262 tokens)</strong>:</p><pre>&lt;a2ui&gt;<br>surface(&quot;booking&quot;, sendDataModel=true)<br>$/reservation/name = &quot;Alice&quot;<br>$/reservation/guests = &quot;3&quot;<br>$/reservation/time = &quot;7:30 PM&quot;<br>$/reservation/notes = &quot;No peanuts&quot;<br>root = Card(main_col)<br>main_col = Column([title, subtitle, divider, name_field, row_fields, notes_field, actions])<br>title = Text(&quot;Confirm Reservation&quot;, &quot;h2&quot;)<br>subtitle = Text(&quot;Xi&#39;an Famous Foods • Friday Evening&quot;, &quot;caption&quot;)<br>divider = Divider()<br>name_field = TextField(&quot;Guest Name&quot;, $/reservation/name, [?required(&quot;Guest name is required&quot;)])<br>row_fields = Row([guests_field, time_field])<br>guests_field = TextField(&quot;Party Size&quot;, $/reservation/guests)<br>time_field = TextField(&quot;Time&quot;, $/reservation/time)<br>notes_field = TextField(&quot;Dietary Notes&quot;, $/reservation/notes)<br>actions = Row([confirm_btn, cancel_btn], &quot;end&quot;)<br>confirm_btn = Button(Text(&quot;Confirm Booking&quot;), &quot;primary&quot;, Event(&quot;confirm&quot;, {reservation: $/reservation}))<br>cancel_btn = Button(Text(&quot;Cancel&quot;), _, Event(&quot;cancel&quot;, {}))<br>&lt;/a2ui&gt;</pre><h4>What the Renderer Gets (Validated A2UI v0.9 Wire JSON)</h4><p>Once the model responds, the server calls processor.parse_response(response.text) and converts the parsed A2uiPart models into standard wire message dictionaries via to_message_dicts():</p><pre>from a2ui.inference_formats import to_message_dicts<br>from a2ui.parser import A2uiPart<br>​<br>parts = processor.parse_response(response.text)<br>wire_messages = [<br>    msg<br>    for part in parts<br>    if isinstance(part, A2uiPart)<br>    for msg in to_message_dicts(part.a2ui)<br>]</pre><p>Comparing the output of processor.parse_response() across both columns in the explorer:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GhhWhLclnZH9Aoha.png" /></figure><p>While the left column validates the &lt;a2ui-json&gt; payload and the right column compiles the &lt;a2ui&gt; Express DSL, both produce the <strong>exact same </strong><strong>166-line (</strong><strong>3,766-character) A2UI v0.9 Wire JSON</strong> (createSurface, updateComponents, and updateDataModel).</p><h4>Rendered UI (@a2ui/lit v0.9)</h4><p>Finally, the server sends wire_messages to the browser, where @a2ui/web_core&#39;s MessageProcessor and @a2ui/lit&#39;s &lt;a2ui-surface&gt; Web Component render the interactive UI:</p><pre>import &#39;@a2ui/lit/v0_9&#39;; // Registers &lt;a2ui-surface&gt;<br>import { basicCatalog } from &#39;@a2ui/lit/v0_9&#39;;<br>import { MessageProcessor } from &#39;@a2ui/web_core/v0_9&#39;;<br>​<br>const surfaceEl = document.querySelector(&#39;a2ui-surface&#39;);<br>​<br>const processor = new MessageProcessor([basicCatalog], (action) =&gt; {<br>  console.log(&#39;User triggered A2UI action:&#39;, action);<br>});<br>​<br>processor.onSurfaceCreated((surface) =&gt; {<br>  surfaceEl.surface = surface;<br>});<br>​<br>processor.processMessages(wireMessages);</pre><p>Client renderers never see &lt;a2ui&gt; Express, Atom, or Elemental syntax—they only consume standard A2UI v0.9 Wire JSON. When you edit the form fields and click <strong>Confirm Booking</strong> in the explorer, the &lt;a2ui-surface&gt; component automatically dispatches the confirm action event along with the bound $/reservation data model context:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Jn8DWrLG0NdjelXo.png" /></figure><h3>Wrapping up</h3><p>Decoupling the LLM inference format from the wire protocol is a great example of optimizing for both sides of an agentic system: <strong>A2UI Express</strong> gives the LLM a compact, token-efficient DSL to generate UIs up to 4x faster, while compiling down to the exact same validated A2UI JSON that client renderers already speak.</p><p>For more details, check out the following resources:</p><ul><li><a href="https://developers.googleblog.com/fast-reliable-a2ui/">Blog: Fast, reliable A2UI, with swappable inference formats</a></li><li><a href="https://a2ui.org/concepts/inference-formats/">Docs: Swappable Inference Formats</a></li><li><a href="https://github.com/google/A2UI/blob/main/specification/proposals/express/a2ui_express.md">Spec: A2UI Express Technical Specification</a></li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2ui/a2ui-express">Sample: A2UI Express &amp; Swappable Inference Formats Explorer</a></li><li><a href="https://speakerdeck.com/meteatamel/agent-to-ui-protocol-a2ui">Presentation: Agent to UI Protocol (A2UI)</a></li></ul><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/11-10_a2ui_express_inference_formats/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=08c00cec882f" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/a2ui-express-and-swappable-inference-formats-08c00cec882f">A2UI Express and Swappable Inference Formats</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agentic Commerce Protocols — UCP and AP2]]></title>
            <link>https://medium.com/google-cloud/agentic-commerce-protocols-ucp-and-ap2-fc9f7c289e31?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/fc9f7c289e31</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[ucp]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[ap2]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Wed, 04 Nov 2026 10:13:48 GMT</pubDate>
            <atom:updated>2026-11-04T10:13:48.563Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*GAfvYgDnPhB4f1rG.png" /></figure><p>In my <a href="https://atamel.dev/posts/2026/03-17_agent_protocols_mcp_a2a_a2ui_agui/">Agent Protocols — MCP, A2A, A2UI, AG-UI</a> post back in March, I provided a high-level overview of the main agent protocols for tool use (MCP), agent-to-agent communication (A2A), and agent-to-UI interactions (AG-UI and A2UI).</p><p>Recently, I had a chance to explore two newer protocols focused on <strong>agentic commerce</strong> and <strong>payments</strong>: the <a href="https://ucp.dev/">Universal Commerce Protocol (UCP)</a> and the <a href="https://ap2-protocol.org/">Agent Payments Protocol (AP2)</a>. Traditional e-commerce and payment flows assume a human is directly browsing a website and clicking “Buy” on a trusted checkout screen. When an AI agent shops and pays on your behalf, both the commerce workflow and the cryptographic proof of user authorization need a standard language.</p><p>In this post, I’ll share an update on the agent protocol landscape by introducing <strong>UCP</strong> and <strong>AP2</strong>, explaining how they work together, and walking through the new Python and ADK samples I added to my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols">genai-beyond-basics repository</a>.</p><h3>TLDR</h3><p>With UCP and AP2 added to the mix, here’s how the six agent protocols in my repository fit together:</p><ul><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/mcp"><strong>Model Context Protocol (MCP)</strong></a>: An open protocol, from Anthropic, that standardizes how models and agents access external tools and resources</li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a"><strong>Agent2Agent Protocol (A2A)</strong></a>: An open protocol, from Google, that standardizes how agents running on diverse frameworks and platforms communicate</li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ag-ui"><strong>Agent-User Interaction Protocol (AG-UI)</strong></a>: An open protocol, from CopilotKit, that standardizes how agents communicate with user interfaces</li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2ui"><strong>Agent to UI Protocol (A2UI)</strong></a>: A generative UI protocol, from Google, that enables AI agents to generate rich, interactive user interfaces across web, mobile, and desktop</li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ucp"><strong>Universal Commerce Protocol (UCP)</strong></a>: An open standard, co-developed by Google, Shopify, and industry partners, that standardizes merchant discovery, capability negotiation, and checkout sessions for agentic commerce</li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ap2"><strong>Agent Payments Protocol (AP2)</strong></a>: An open protocol, from Google, that secures agent-initiated payments using cryptographically signed mandates and verifiable credentials</li></ul><p>Let’s look into <strong>UCP</strong> and <strong>AP2</strong> in more detail.</p><h3>Universal Commerce Protocol (UCP)</h3><p><strong>Universal Commerce Protocol (UCP)</strong> is an open standard, co-developed by Google, Shopify, and industry partners (including Target, Walmart, Wayfair, Etsy, and Stripe), that defines functional primitives for agentic commerce. It allows AI agents, consumer platforms, businesses (who remain the Merchant of Record), credential providers, and payment service providers to interoperate without bespoke one-off integrations.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JnQ8775SucGEVN7H.png" /></figure><h4>Architecture, Discovery, and Capability Negotiation</h4><p>UCP is structured around four layers:</p><ul><li><strong>Services</strong>: Domain groupings such as dev.ucp.shopping, dev.ucp.common, and dev.ucp.lodging.</li><li><strong>Capabilities</strong>: Versioned features (“verbs”) within a service, such as dev.ucp.shopping.checkout, dev.ucp.shopping.cart, and dev.ucp.shopping.catalog.search.</li><li><strong>Extensions</strong>: Optional schemas that augment a parent capability via extends (e.g., dev.ucp.shopping.fulfillment, dev.ucp.shopping.discount, and dev.ucp.common.payment.ap2_mandate).</li><li><strong>Transports</strong>: Transport-agnostic bindings supporting <strong>REST</strong> (OpenAPI 3.1), <strong>MCP</strong> (OpenRPC), <strong>A2A</strong> (Agent Card), and <strong>Embedded</strong> (OpenRPC).</li></ul><p>Every UCP merchant publishes an unauthenticated JSON profile at GET /.well-known/ucp advertising its protocol version, capabilities, payment handlers (com.google.pay, dev.shopify.shop_pay), and JWK signing keys (ES256 as the mandatory baseline, plus optional Ed25519 for Web Bot Auth interop). Permissionless request authentication across HTTP transports uses <strong>HTTP Message Signatures (RFC 9421)</strong> resolved directly against the public keys in each party&#39;s profile. When an agent calls a merchant, UCP uses a <strong>server-selects</strong> negotiation model:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/923/0*bVkL0VKhlfFzRBBi.png" /></figure><p>The merchant computes the intersection between its declared capabilities and the agent’s profile, selects the latest mutually supported YYYY-MM-DD version, and prunes any orphaned extensions whose parent capability isn&#39;t active.</p><p>Once a checkout session starts, dev.ucp.shopping.checkout progresses through six explicit states so an AI agent always knows whether it can finish the purchase autonomously or must hand off to the user:</p><ul><li>incomplete: Missing required details (such as buyer email or shipping method).</li><li>requires_escalation: Requires direct buyer input via a continue_url handoff (e.g., custom configuration or 3DS challenge).</li><li>ready_for_complete: All line items, totals, and fulfillment details are finalized; ready for payment submission.</li><li>complete_in_progress: Checkout completion is currently executing (new PUT updates are rejected while in progress).</li><li>completed: Payment processed and OrderConfirmation (order.id, order.permalink_url) generated.</li><li>canceled: Checkout session was canceled (POST /checkout-sessions/{id}/cancel) or expired.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/811/0*8sIOL2qLkl1_loFt.png" /></figure><blockquote><strong><em>ℹ️ Note:</em></strong><em> All monetary amounts in UCP (and AP2) are expressed in </em><strong><em>minor currency units (cents)</em></strong><em> — for example, </em><em>2500 for </em><em>$25.00, and negative integers like </em><em>-400 for a </em><em>-$4.00 discount.</em></blockquote><h4>UCP Samples: Python SDK &amp; Google ADK</h4><p>In my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ucp">UCP samples</a> folder, I created two samples using the official <a href="https://github.com/Universal-Commerce-Protocol/python-sdk">UCP Python SDK</a> (ucp-sdk):</p><p>1. <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ucp/hello-world"><strong>UCP Hello World (Python SDK &amp; FastAPI)</strong></a>: A self-contained FastAPI merchant server (<a href="https://github.com/meteatamel/genai-beyond-basics/blob/main/samples/protocols/ucp/hello-world/server.py">server.py</a>) and client script (<a href="https://github.com/meteatamel/genai-beyond-basics/blob/main/samples/protocols/ucp/hello-world/client.py">client.py</a>) that validates payloads against BusinessSchema and Checkout models from ucp_sdk. Because client.py mounts server.py in-process via httpx.ASGITransport by default, you can run the entire 4-step UCP lifecycle in a single command:</p><pre>uv run --with ucp-sdk --with fastapi --with httpx python client.py</pre><pre>=== 1. UCP Discovery (GET /.well-known/ucp) ===<br>Protocol Version:  2026-08-25<br>Services:          [&#39;dev.ucp.shopping&#39;]<br>Capabilities:      [&#39;dev.ucp.shopping.checkout&#39;, &#39;dev.ucp.shopping.fulfillment&#39;, &#39;dev.ucp.shopping.discount&#39;]<br>Payment Handlers:  [&#39;com.google.pay&#39;, &#39;dev.shopify.shop_pay&#39;]<br>Signing Key (kid): merchant_key_2026<br> <br>=== 2. Create Checkout Session (POST /checkout-sessions) ===<br>Checkout ID:       chk_9c35e04f<br>Checkout Status:   incomplete<br>Negotiated Caps:   [&#39;dev.ucp.shopping.checkout&#39;, &#39;dev.ucp.shopping.fulfillment&#39;, &#39;dev.ucp.shopping.discount&#39;]<br>  - 1x Red Roses Bouquet (sku_roses): $25.00<br>  - 1x Ceramic Vase (sku_vase): $15.00<br> <br>=== 3. Update Checkout: Fulfillment &amp; Discount (PUT /checkout-sessions/chk_9c35e04f) ===<br>Checkout Status:   ready_for_complete<br>Totals Breakdown:<br>  - Subtotal                : $40.00<br>  - 10% Promo (SAVE10)      : -$4.00<br>  - Shipping (Express Delivery): $5.00<br>  - Total                   : $41.00<br> <br>=== 4. Complete Checkout (POST /checkout-sessions/chk_9c35e04f/complete) ===<br>Checkout Status:   completed<br>Order Created:     Order #ORD_EF9FCC12 (id=ord_ef9fcc12)<br>Order Permalink:   https://flowershop.example.com/orders/ord_ef9fcc12</pre><p>2. <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ucp/adk"><strong>UCP Shopping Agent with Google ADK</strong></a>: An interactive <a href="https://google.github.io/adk-docs/">Agent Development Kit (ADK)</a> agent (<a href="https://github.com/meteatamel/genai-beyond-basics/blob/main/samples/protocols/ucp/adk/shopping_agent/agent.py">shopping_agent/agent.py</a>) equipped with UCP discovery and checkout tools:</p><pre>root_agent = Agent(<br>    name=&quot;ucp_shopping_agent&quot;,<br>    model=&quot;gemini-2.5-flash&quot;,<br>    description=&quot;AI Shopping Agent that discovers merchants and completes purchases using the Universal Commerce Protocol (UCP).&quot;,<br>    instruction=...,<br>    tools=[<br>        discover_merchant,<br>        create_checkout_session,<br>        update_checkout_session,<br>        complete_checkout_session,<br>    ],<br>)</pre><p>Start server.py in one terminal and adk web in another, and you can ask the agent what the merchant supports and have it negotiate extensions, apply promo codes, and complete an order conversationally.</p><h3>Agent Payments Protocol (AP2)</h3><p>While UCP standardizes the commerce session, a big question remains: <em>How does a merchant or payment processor know that an AI agent actually has the user’s permission to spend their money — and didn’t hallucinate the item or price?</em></p><p>That’s where the <strong>Agent Payments Protocol (AP2)</strong> comes in. Originated by Google, AP2 is an open security and authorization protocol that replaces “trusting the LLM’s text summary” with <strong>Verifiable Digital Credentials (VDCs)</strong> — specifically tamper-evident <strong>SD-JWTs (Selective Disclosure JWTs)</strong> and <strong>KB-SD-JWT (Key-Binding SD-JWT) delegation chains</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EG6Ru5RlaTqvebRQ.png" /></figure><h4>Trust Boundary, Mandates (v0.2), and Delegation Chains</h4><p>A key rule in the <a href="https://github.com/google-agentic-commerce/AP2/blob/main/docs/ap2/specification.md">AP2 v0.2 specification</a> is the strict separation between <strong>Agentic</strong> (LLM-driven) and <strong>Non-Agentic</strong> (deterministic) roles:</p><ul><li>The <strong>Shopping Agent</strong> is expected to be agentic.</li><li>The <strong>Trusted Surface</strong> (where the user approves and signs mandates) <strong>MUST</strong> be non-agentic.</li><li>Even if a <strong>Merchant</strong>, <strong>Credential Provider</strong>, or <strong>Payment Processor</strong> exposes an agentic interface, <strong>all mandate validation and constraint verification MUST execute in deterministic code</strong>.</li></ul><p>In early AP2 overviews (v0.1), mandates were described conceptually as <em>Intent</em>, <em>Cart</em>, and <em>Payment</em> mandates. In <strong>AP2 v0.2</strong> (and the official ap2.sdk Python package), this is formalized into two linked mandate families — <strong>Checkout Mandates</strong> (securing <em>what</em> is purchased) and <strong>Payment Mandates</strong> (securing <em>how</em> it is paid) — each with an <strong>Open</strong> and <strong>Closed</strong> state, supporting two modes:</p><ol><li><strong>Human Present (</strong><strong>Direct Flow)</strong>: The user reviews the assembled cart on a Trusted Surface and directly signs the <strong>Closed Checkout Mandate</strong> (mandate.checkout.1) and <strong>Closed Payment Mandate</strong> (mandate.payment.1), bound to the merchant-signed UCP checkout_jwt via checkout_hash.</li><li><strong>Human Not Present (</strong><strong>Autonomous Flow)</strong>: The user pre-authorizes an <strong>Open Checkout Mandate</strong> (mandate.checkout.open.1) and <strong>Open Payment Mandate</strong> (mandate.payment.open.1) with deterministic constraints (e.g., allowed merchants, acceptable SKUs, and a maximum budget) and binds the Shopping Agent&#39;s public key in the cnf claim. Later, when the condition is met (such as a price drop), the Shopping Agent signs the <strong>Closed Mandates</strong> with its own private key and chains them onto the user&#39;s open mandates (open_sd_jwt~~closed_kb_sd_jwt~):</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*iSKpG0_HgM9-rCY3ek9A6A.png" /></figure><p>AP2 supports two trust models for issuing the root Mandate on the <strong>Trusted Surface</strong> (<strong>User Credential Model</strong> via OpenID4VP and the browser Digital Credentials API, or <strong>Trusted Agent Provider Model</strong> signed by the agent provider’s key) and enforces two normative presentation rules on the Shopping Agent: <strong>Selective Disclosure Minimization</strong> (revealing only the disclosures required to evaluate the Closed Mandate) and <strong>Sequential Presentation</strong> (preventing concurrent reuse of an Open Mandate across multiple checkouts without a prior rejection receipt).</p><h4>AP2 Sample: Mandate Delegation &amp; Guardrails in Python</h4><p>The official <a href="https://github.com/google-agentic-commerce/AP2">google-agentic-commerce/AP2</a> repository includes full multi-server A2A and MCP scenarios (using Cards or x402 stablecoins), which I documented in my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ap2">AP2 README</a>.</p><p>However, spinning up four separate servers just to see how AP2 mandates work under the hood can be heavy. So I also built a self-contained <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ap2/hello-world">AP2 Hello World (</a><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/ap2/hello-world">mandate_demo.py)</a> sample using the official ap2.sdk package that runs an entire <strong>Autonomous (Human Not Present)</strong> flow in a single script with no API keys required.</p><p>Let’s take a quick look at what’s happening under the hood in <a href="https://github.com/meteatamel/genai-beyond-basics/blob/main/samples/protocols/ap2/hello-world/mandate_demo.py">mandate_demo.py</a>. First, the user (on a Trusted Surface) signs an OpenCheckoutMandate and an OpenPaymentMandate capping the spend at $150.00 (15000 cents), linking to the open checkout mandate hash via PaymentReference, and binding the agent&#39;s public key via cnf:</p><pre>open_checkout_hash = compute_sha256_b64url(open_checkout_token)<br> <br>open_payment_token = client.create(<br>    payloads=[<br>        OpenPaymentMandate(<br>            constraints=[<br>                AmountRange(currency=&quot;USD&quot;, min=0, max=15000),<br>                AllowedPayees(allowed=[merchant]),<br>                PaymentReference(conditional_transaction_id=open_checkout_hash),<br>            ],<br>            cnf={&quot;jwk&quot;: agent_pub_dict},<br>            iat=now,<br>            exp=now + 3600,<br>        )<br>    ],<br>    issuer_key=user_jwk,<br>)</pre><p>When the merchant returns a signed UCP Checkout JWT for $120.00, the Shopping Agent autonomously signs and chains the closed mandates using MandateClient.present(), and the Merchant and Payment Processor deterministically verify the chain (CheckoutMandateChain and PaymentMandateChain):</p><pre>payment_chain_token = client.present(<br>    holder_key=agent_jwk,<br>    mandate_token=open_payment_token,<br>    payloads=[closed_payment],<br>    aud=&quot;credential-provider&quot;,<br>    nonce=&quot;nonce_pay_98765&quot;,<br>)<br> <br>payment_payloads = client.verify(<br>    token=payment_chain_token,<br>    key_or_provider=lambda _tok: user_pub_jwk,<br>    expected_aud=&quot;credential-provider&quot;,<br>    expected_nonce=&quot;nonce_pay_98765&quot;,<br>)<br>payment_chain = PaymentMandateChain.parse(payment_payloads)<br>payment_violations = payment_chain.verify(<br>    expected_transaction_id=checkout_hash,<br>    expected_open_checkout_hash=open_checkout_hash,<br>)</pre><p>You can run the sample with a single uv command:</p><pre>uv run --with &quot;ap2 @ git+https://github.com/google-agentic-commerce/AP2.git&quot; python mandate_demo.py</pre><p>Notice in step 6 how deterministic verification immediately catches and rejects an agent that tries to spend $250.00 against the user&#39;s $150.00 open mandate:</p><pre>=== 1. Generating Keys for User (Trusted Surface), Agent, and Merchant ===<br> <br>=== 2. User Signs Open Mandates on Trusted Surface (SD-JWT) ===<br>Intent: &#39;Buy white running shoes (SKU-SHOE-WHITE) from RunFast Store for at most $150.00&#39;<br>Open Checkout Mandate (SD-JWT): eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0IiwgImtpZCI6I...<br>Open Payment Mandate  (SD-JWT): eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0IiwgImtpZCI6I...<br> <br>=== 3. Merchant Creates Signed UCP Checkout ($120.00) ===<br>Merchant Checkout JWT Hash: MBOOU7B8tmrp1fickmNKbaLJ74iQRtvSnO2HQ01ILU0<br> <br>=== 4. Shopping Agent Signs Closed Mandates (KB-SD-JWT Delegation Chain) ===<br>Agent appended closed KB-SD-JWT hop to both open mandates.<br> <br>=== 5. Deterministic Verification &amp; Receipt Issuance ===<br>[Merchant] Checkout Mandate Chain verified! Constraints satisfied (0 violations).<br>[Payment Processor] Payment Mandate Chain verified! Amount $120.00 &lt;= $150.00 max.<br>[Shopping Agent] Checkout Receipt verified: {&#39;verified&#39;: True}<br>[Shopping Agent] Payment Receipt verified:  {&#39;verified&#39;: True}<br> <br>=== 6. Guardrail Demo: Agent Tries to Exceed Budget ($250.00 &gt; $150.00) ===<br>[Payment Processor] Rejected over-budget payment! Violations: [&#39;Amount 25000 exceeds maximum 15000&#39;]</pre><h3>UCP vs. AP2: How They Work Together</h3><p>Rather than competing, <strong>UCP</strong> and <strong>AP2</strong> are complementary layers in the agentic commerce stack:</p><ul><li><strong>UCP is the commerce layer</strong>: It standardizes how agents discover stores (/.well-known/ucp), negotiate capabilities, calculate shipping and discounts, and manage checkout sessions.</li><li><strong>AP2 is the cryptographic trust and payment authorization layer</strong>: When UCP’s dev.ucp.common.payment.ap2_mandate extension is negotiated, the session is <strong>Security Locked</strong> (preventing any downgrade to an unsigned checkout). The merchant signs the canonicalized (RFC 8785 JCS) Checkout payload and embeds a detached JWS at checkout.ap2.merchant_authorization, and on POST /checkout-sessions/{id}/complete, the agent submits cryptographically signed AP2 CheckoutMandate (ap2.checkout_mandate) and PaymentMandate (payment.instruments[*].credential.token) credentials bound to that checkout.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KpBUt3n4gKvL9HFd.png" /></figure><h3>Wrapping up</h3><p>As AI agents move from answering questions to taking real-world actions like shopping and paying, <strong>UCP</strong> provides the standardized commerce primitives while <strong>AP2</strong> provides the cryptographic guardrails to prove user intent.</p><p>Here are some resources to learn more:</p><ul><li><a href="https://ucp.dev/">Docs: Universal Commerce Protocol (ucp.dev)</a></li><li><a href="https://ap2-protocol.org/">Docs: Agent Payments Protocol (ap2-protocol.org)</a></li><li><a href="https://ucp.dev/latest/documentation/ucp-and-ap2/">Docs: UCP and AP2 Integration</a></li><li><a href="https://developers.googleblog.com/under-the-hood-universal-commerce-protocol-ucp/">Blog: Under the Hood: Universal Commerce Protocol (UCP)</a></li><li><a href="https://cloud.google.com/blog/products/ai-machine-learning/announcing-agents-to-payments-ap2-protocol">Blog: Announcing Agent Payments Protocol (AP2)</a></li><li><a href="https://developers.googleblog.com/developers-guide-to-ai-agent-protocols/">Blog: Developer’s Guide to AI Agent Protocols</a></li><li><a href="https://codelabs.developers.google.com/next26/adk-agent-commerce">Codelab: Secure Agent Commerce with AP2 and UCP</a></li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols">Sample: My UCP and AP2 Samples in genai-beyond-basics</a></li></ul><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/11-04_agentic_commerce_protocols_ucp_ap2/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=fc9f7c289e31" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/agentic-commerce-protocols-ucp-and-ap2-fc9f7c289e31">Agentic Commerce Protocols — UCP and AP2</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Antigravity Plugin Marketplace in Action: Auditing My Talk Slides with the Google Slides Plugin]]></title>
            <link>https://medium.com/google-cloud/antigravity-plugin-marketplace-in-action-auditing-my-talk-slides-with-the-google-slides-plugin-e0bbaf78c0c3?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/e0bbaf78c0c3</guid>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[generative-ai-use-cases]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 10:26:55 GMT</pubDate>
            <atom:updated>2026-10-30T10:26:55.624Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*njP0NFA_SL65P6Zt.png" /></figure><p>Back in August, I wrote <a href="https://atamel.dev/posts/2026/08-18_where_agy_plugins/">Where does Antigravity look for Plugins?</a> to explain how plugins bundle skills, rules, subagents, hooks, MCP servers, and sidecars into a single unit. At the time, installing a plugin meant pointing Antigravity to a GitHub repository or a local directory path.</p><p>This week, the team launched the <a href="https://antigravity.google/blog/plugin-marketplace-in-antigravity">Antigravity Plugin Marketplace</a>: a curated catalog of first-party Google plugins and third-party partner plugins built directly into the Antigravity UI and CLI.</p><p>Instead of just listing what is in the marketplace, I wanted to share a real workflow that saved me hours this week: using the <strong>Google Slides</strong> plugin to audit and update my 80+ slide <a href="https://speakerdeck.com/meteatamel/getting-started-with-google-antigravity">Getting Started with Google Antigravity</a> presentation against the latest documentation.</p><h3>What is the Antigravity Plugin Marketplace?</h3><p>The <strong>Antigravity Plugin Marketplace</strong> makes discovering, installing, and authenticating plugins a one-click experience across both the Antigravity App/IDE and the agy CLI:</p><ul><li><strong>In the Antigravity App / IDE:</strong> Open the Agent side panel, click ... at the top right, navigate toCustomizations -&gt; Plugins, and switch to the <strong>Marketplace</strong> tab.</li><li><strong>In the Antigravity CLI (</strong><strong>agy):</strong> Run the /plugin slash command and switch to the <strong>Discover</strong> tab.</li></ul><p>Here is what the <strong>Marketplace</strong> tab looks like under Customizations in the Antigravity UI:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*SSkZubeT2_lME_DR.png" /></figure><p>Plugins are organized into curated shelves:</p><ul><li><strong>Google Workspace:</strong> Google Docs, Google Sheets, Google Slides, Google Drive, and Google Calendar.</li><li><strong>Build with Google:</strong> Gemini API, Chrome DevTools, Android CLI, and Google Cloud Developer.</li><li><strong>Design, Creative &amp; Media:</strong> Canva, Figma, and Mobbin.</li><li><strong>Productivity &amp; Project Management:</strong> Atlassian Teamwork Graph CLI, Dropbox, and Linear.</li><li><strong>Cloud Hosting &amp; Deployment:</strong> Netlify, Railway, and Vercel.</li><li><strong>Developer Tools &amp; Business:</strong> Auth0, Base44, Browserbase, Bigin, Databricks, and Intercom.</li></ul><p>Better yet, the marketplace handles authentication and configuration in place. For plugins that require an external account-like Google Workspace-you click <strong>Sign in</strong> directly on the plugin card, complete the OAuth flow in your browser, and Antigravity manages the credentials automatically. You can even connect multiple accounts (for example, work and personal) and switch between them.</p><h3>Installing the Google Slides Plugin</h3><p>If you give technical talks about fast-moving AI tools, you know the pain of keeping slide decks up to date. Antigravity ships multiple releases every week, which means screenshots, model names, CLI flags, and feature lists in my <strong>Getting Started with Google Antigravity</strong> deck start drifting out of date almost immediately.</p><p>Previously, auditing a Google Slides presentation meant exporting a PDF or copying slide text manually, asking an LLM what was outdated, and then manually clicking through 80+ slides in the browser to apply the fixes.</p><p>With the <strong>Google Slides</strong> plugin from the <strong>Google Workspace</strong> shelf, Antigravity gets:</p><ol><li>A dedicated gslides skill that teaches the agent how to read, design, and mutate presentations safely.</li><li>A sandboxed gslides CLI separated into gslides readonly (for reading slides, inspecting page elements, and exporting PNG thumbnails) andgslides mutate (for updating text, inserting slides, and replacing images).</li><li>Managed Google OAuth authentication so it can access your presentation directly via its Google Slides URL.</li></ol><p>After installing the <strong>Google Slides</strong> plugin (and the <strong>Google Drive</strong> plugin) from the marketplace and signing in with my Google account, I was ready to put it to the test.</p><h3>Auditing My Antigravity Slides Against the Docs</h3><p>I pasted the Google Slides link to my presentation into Antigravity and gave it a simple prompt:</p><pre>Can you check my Getting Started with Google Antigravity slides<br>(https://docs.google.com/presentation/d/&lt;presentation-id&gt;/edit)<br>against the latest Antigravity documentation and let me know if<br>there are any errors, outdated slides, or missing new features?</pre><p>Antigravity used gslides readonly read to inspect all 81 slides in my deck, cross-referenced every slide against its built-in documentation skills ( antigravity-guide and agy-customizations), and came back with a structured audit report broken down into three categories:</p><ol><li><strong>Wrong, outdated, or inconsistent slides</strong></li><li><strong>Missing major features introduced in recent releases</strong></li><li><strong>Outdated screenshots and sketchnote diagrams</strong></li></ol><p>Once I reviewed the report and gave the green light, Antigravity updated the live presentation directly. Here is what it caught and fixed.</p><p>First, Antigravity used gslides mutate replace-text and gslides mutate batch to fix factual drift and typos across existing slides without disturbing the slide formatting.</p><p>For example, it updated my permission presets slide to include all four current presets ( Request Review, Default, Auto (Preview), and Turbo), refreshed the IDE and subagent lists ( Eclipse support and the image-generator subagent), and updated the Skills and Rules slides with the references/ directory and all four rule trigger types ( always-on, glob, manual, and model-triggered). It even caught a doubled-quote Python syntax typo in an SDK code snippet and noticed that speaker notes on one slide had been accidentally copy-pasted from an earlier slide.</p><h3>2. Adding Missing Features as New Slides</h3><p>Next, Antigravity pointed out six major capabilities that had shipped since I first created the deck: <strong>Context &amp; Attachments</strong> (@ mentions, diff line quoting, and multimedia attachments), <strong>Slash Commands</strong> (/plan, /grill-me,/goal, /boost, /teamwork, /btw), <strong>Teamwork</strong> (peer-to-peer subagent teams), <strong>Generative UI</strong> (inline interactive HTML/JS widgets), the <strong>Plugin Marketplace</strong>, and <strong>Enterprise Identity</strong> (BYO GCP project and SSO/WIF).</p><p>When I asked it to add slides for them, it inspected the exact typography ( Poppins headings, Roboto body, Roboto Mono code blocks, and #1976D2 accents) of my existing deck, inserted six new slides in the right sections, and ran gslides readonly export-thumbnail to visually verify every new slide for text overflow or alignment issues.</p><p>Here is the new <strong>Plugin Marketplace</strong> slide that Antigravity inserted into the presentation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*K2wZk-OrySFdnWN_.png" /></figure><p>The most impressive part of the workflow happened when we got to the image-only slides. During its initial audit, Antigravity noticed that even on slides with no editable text boxes, the embedded screenshots and hand-drawn sketchnotes had outdated details.</p><p>I asked:</p><pre>I like the updates, can you also update the images in the image-only<br>slides with the suggestions you had?</pre><p>Antigravity extracted the images from those slides, spawned image-generator subagents in parallel to surgically edit each image while preserving its exact visual style, uploaded the updated images to Google Drive, and swapped them into the presentation.</p><p>Across those image-only slides, the subagents updated the dark-mode model selector screenshots to show <strong>Gemini 4 Argon Medium</strong>, refreshed the permission format table screenshot, updated my <strong>Hooks</strong> sketchnote diagram from <em>&quot;5 Critical Event Triggers&quot;</em> to <strong>&quot;6 Critical Event Triggers&quot;</strong> (adding Session Start to the flow), and updated my <strong>Plugins</strong> sketchnote diagram so <strong>Sidecars</strong> are included inside the AGY PLUGIN bundle instead of crossed out.</p><p>Here is the updated <strong>Hooks</strong> slide showing all 6 event triggers in the original hand-drawn sketchnote style:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RYVdF0zVu0QUuHZZ.png" /></figure><p>And here is the updated <strong>Plugins</strong> slide with <strong>Sidecars</strong> added into the plugin bundle diagram:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*4doWzwQNrLCV-4v6.png" /></figure><h3>Wrapping up</h3><p>What used to be a tedious multi-hour chore of cross-checking release notes and manually editing slides turned into a single conversation. Having first-party Google Workspace plugins available directly in the <strong>Antigravity Plugin Marketplace</strong>-combined with documentation skills and image-generator subagents-makes it effortless to connect Antigravity to the docs, spreadsheets, and presentations where your day-to-day work actually lives.</p><p>To learn more, check out these resources:</p><ul><li><a href="https://antigravity.google/blog/plugin-marketplace-in-antigravity">Blog: Plugin Marketplace in Antigravity</a></li><li><a href="https://antigravity.google/docs/plugins">Docs: Antigravity Plugins</a></li><li><a href="https://speakerdeck.com/meteatamel/getting-started-with-google-antigravity">Presentation: Getting Started with Google Antigravity</a></li></ul><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/10-30_agy_plugin_marketplace_slides/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e0bbaf78c0c3" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/antigravity-plugin-marketplace-in-action-auditing-my-talk-slides-with-the-google-slides-plugin-e0bbaf78c0c3">Antigravity Plugin Marketplace in Action: Auditing My Talk Slides with the Google Slides Plugin</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Testing and Serving A2A Agents from the Terminal with A2A CLI]]></title>
            <link>https://medium.com/google-cloud/testing-and-serving-a2a-agents-from-the-terminal-with-a2a-cli-f2a24c26f68c?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/f2a24c26f68c</guid>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[generative-ai]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Mon, 26 Oct 2026 09:23:02 GMT</pubDate>
            <atom:updated>2026-10-26T09:23:02.574Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/905/0*zikSpSgbl4Y5L0eh.gif" /></figure><p>In my <a href="https://atamel.dev/posts/2026/03-17_agent_protocols_mcp_a2a_a2ui_agui/">Agent Protocols — MCP, A2A, A2UI, AG-UI</a> post, I provided a high-level overview of the main agent protocols, including the <a href="https://a2a-protocol.org/latest/">Agent2Agent (A2A) Protocol</a>. When building an A2A server, you need an easy way to test that its agent card is published properly and that it responds to messages as expected.</p><p>Traditionally, you either had to write a custom client script using the A2A SDK or clone and run the web-based <a href="https://github.com/a2aproject/a2a-inspector">A2A Inspector</a>. Recently, the A2A project released the official <a href="https://github.com/a2aproject/a2a-cli">A2A CLI</a> ( a2a) tool, which makes testing A2A agents as simple as running a single terminal command. Even better, it includes a built-in server mode to spin up test agents or wrap existing scripts as A2A servers.</p><p>In this post, I’ll introduce <strong>A2A CLI</strong> and show how to use it with the updated A2A v1.0 samples in my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a">genai-beyond-basics repository</a>.</p><h3>What is A2A CLI?</h3><p><strong>A2A CLI</strong> ( a2a) is the official command-line client and server tool for the A2A protocol (built on the A2A v1.0 specification):</p><ul><li><strong>Terminal Client (</strong><strong>a2a card get, </strong><strong>a2a send, </strong><strong>a2a task)</strong>: Discover agent cards, send messages, stream real-time task updates (--stream), and output JSON/JSONL (-o json, -o jsonl) for shell pipelines.</li><li><strong>Built-in Server (</strong><strong>a2a server)</strong>: Spin up a zero-code echo agent (--echo), wrap any script or executable into an A2A agent (--exec), or run a debugging proxy in front of an upstream agent (--proxy).</li><li><strong>AI Coding Assistant Skill (</strong><strong>a2a skill)</strong>: Outputs a bundled SKILL.md that teaches AI coding assistants (like Antigravity, Gemini CLI, or Claude Code) how to discover and delegate tasks to remote A2A agents.</li></ul><p>On macOS or Linux, you can install it via Homebrew:</p><pre>brew tap a2aproject/a2a-cli https://github.com/a2aproject/a2a-cli brew install a2a</pre><p>Or install it from source using Go:</p><pre>go install github.com/a2aproject/a2a-cli@latest<br>mv &quot;$(command -v a2a-cli)&quot; &quot;$(dirname &quot;$(command -v a2a-cli)&quot;)/a2a&quot;</pre><h3>Testing Existing A2A Agents with A2A CLI</h3><p>Let’s first see how <strong>A2A CLI</strong> simplifies testing the existing A2A servers in my repository (which I also recently updated to the latest A2A v1.0 SDKs).</p><h4>Echo Agent (Python &amp; C#)</h4><p>In my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a/echo-agent">echo-agent</a> sample, I have a barebones Echo Agent implemented in both Python ( a2a-sdk) and C# ( A2A .NET SDK) running on port 5209.</p><p>Start the Python Echo Agent server:</p><pre>python __main__.py</pre><p>Or start the C# Echo Agent server:</p><pre>cd A2AAgent<br>dotnet run</pre><p>Previously, you had to run a2a_client.py (or the C# A2AClient project) or spin up <strong>A2A Inspector</strong> in the browser just to verify the server. With <strong>A2A CLI</strong>, you can inspect the agent card directly from your terminal:</p><pre>a2a card get http://localhost:5209</pre><p>You should see the agent’s metadata, supported interfaces, and skills:</p><pre>Name:         Echo Agent<br>Description:  An agent that will echo every message it receives.<br>Version:      1.0.0<br>Interfaces:<br>  JSONRPC      http://localhost:5209<br>Streaming:    true<br>Skills:<br>  echo                 Echo tool</pre><p>Next, send a message to the agent:</p><pre>a2a send -a http://localhost:5209 &quot;Hello from the A2A CLI&quot;<br><br>[agent] Echo: Hello from the A2A CLI</pre><h3>ADK Currency Agent</h3><p>You can also use <strong>A2A CLI</strong> to test <a href="https://google.github.io/adk-docs/a2a/">Agent Development Kit (ADK)</a> agents exposed over A2A. In my <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a/adk/currency">adk/currency</a> sample, a currency_agent uses a tool ( convert_currency) to fetch live exchange rates from the Frankfurter API and exposes itself as an A2A server via ADK&#39;s to_a2a() utility:</p><pre>root_agent = Agent(<br>    name=&quot;currency_agent&quot;,<br>    model=&quot;gemini-3.5-flash&quot;,<br>    description=&quot;Agent to convert from one currency to another.&quot;,<br>    instruction=instruction_prompt,<br>    tools=[convert_currency],<br>)<br><br># Expose the agent over A2A protocol locally<br>a2a_app = to_a2a(root_agent, host=&quot;localhost&quot;, port=int(os.getenv(&quot;PORT&quot;, &quot;8080&quot;)))</pre><p>Start the ADK A2A server locally:</p><pre>uvicorn agent:a2a_app --host localhost --port 8080</pre><p>Instead of needing a second ADK agent (like travel_helper) just to test currency_agent, you can query its agent card and send a conversion request straight from the CLI:</p><pre>a2a card get http://localhost:8080<br><br>Name:         currency_agent<br>Description:  Agent to convert from one currency to another.<br>Version:      0.0.1<br>Interfaces:<br>  JSONRPC      http://localhost:8080<br>Streaming:    true<br>Skills:<br>  currency_agent       model<br>  currency_agent-convert_currency convert_currency</pre><p>Send a currency conversion prompt:</p><pre>a2a send -a http://localhost:8080 &quot;How much is 1 GBP in EUR?&quot;</pre><p>Notice how <strong>A2A CLI</strong> displays the full A2A task lifecycle, including the resulting artifact and the tool call history:</p><pre>Task:     598a1dce-f628-45cc-bbe0-305a639ffc02<br>Context:  8e770de5-beb6-4a48-8c43-9e0736b696bf<br>Status:   completed (2026-10-19T15:29:30Z)<br>Artifacts:<br>  [93ef7de9-fb90-4b19-a4e3-aadc07c057ab] 1 British Pounds (GBP) = 1.1876 Euros (EUR)<br>History:<br>  [user] How much is 1 GBP in EUR?<br>  [agent] {&quot;args&quot;:{&quot;from_currency&quot;:&quot;GBP&quot;,&quot;to_currency&quot;:&quot;EUR&quot;},&quot;id&quot;:&quot;call_2821308&quot;,&quot;name&quot;:&quot;convert_currency&quot;}<br>  [agent] {&quot;id&quot;:&quot;call_2821308&quot;,&quot;name&quot;:&quot;convert_currency&quot;,&quot;response&quot;:{&quot;result&quot;:1.1876}}<br>  [agent] 1 British Pounds (GBP) = 1.1876 Euros (EUR)</pre><h3>Beyond Testing: a2a server Mode</h3><p>While <strong>A2A CLI</strong> is great as a client, its a2a server command is equally useful for testing and prototyping. You can find all of these examples in the new <a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a/a2a-cli">a2a-cli</a> folder in my repo.</p><h4>1. Zero-Code Echo Server (--echo)</h4><p>If you are building an A2A client and just need a quick A2A endpoint to test against, you don’t need to write any Python or C# code at all. Run a2a server --echo:</p><pre>a2a server --echo --port 8080 --name &quot;Echo Agent&quot; --description &quot;An agent that echoes messages back.&quot;</pre><p>Then test it with a2a send:</p><pre>a2a send -a http://localhost:8080 &quot;Hello from A2A CLI&quot;</pre><h3>2. Turn Any Script into an A2A Agent (--exec)</h3><p>With --exec, a2a server wraps any command or script as an A2A-compliant agent. Incoming message text is piped to stdin, and stdout becomes the response artifact.</p><p>For example, here’s a plain Python script (<a href="https://github.com/meteatamel/genai-beyond-basics/blob/main/samples/protocols/a2a/a2a-cli/word_processor.py">word_processor.py</a>) that knows nothing about A2A:</p><pre>import sys<br>import time<br><br>def main():<br>    text = sys.stdin.read().strip()<br>    words = text.split()<br>    for i, word in enumerate(words, 1):<br>        print(f&quot;[{i}/{len(words)}] {word.upper()}&quot;, flush=True)<br>        time.sleep(0.3)<br><br>if __name__ == &quot;__main__&quot;:<br>    main()</pre><p>Expose it as a streaming A2A server by passing --exec and splitting chunks on newlines with --chunk=$&#39;\n&#39;:</p><pre>a2a server --exec &quot;python3 -u word_processor.py&quot; \<br>  --chunk=$&#39;\n&#39; \<br>  --port 8080 \<br>  --name &quot;Word Processor Agent&quot; \<br>  --description &quot;Streams words in uppercase.&quot;</pre><p>Now call it with --stream to watch the ArtifactUpdateEvent chunks arrive in real time:</p><pre>a2a send -a http://localhost:8080 --stream &quot;hello agent world&quot;<br><br>Task:     01a154c9-bf34-7bb0-b949-f632a913ca5e<br>Context:  01a154c9-bf34-7cb2-b457-904e4dd2ed6e<br>Status:   submitted<br>History:<br>  [user] hello agent world<br>[status] working<br>[artifact] [1/3] HELLO<br>[artifact+] [2/3] AGENT<br>[artifact+] [3/3] WORLD<br>[status] completed</pre><h3>3. Debugging Traffic with Proxy Mode (--proxy)</h3><p>Finally, if you want to inspect traffic between two A2A agents or inject headers, you can put a2a server --proxy in front of any upstream A2A server:</p><pre>a2a server --proxy -a http://localhost:8080 --port 9090</pre><p>Any requests sent to http://localhost:9090 will be logged to the terminal and forwarded to the upstream agent on port 8080.</p><h3>Wrapping up</h3><p><strong>A2A CLI</strong> is a must-have utility in your belt if you’re working with the A2A protocol — whether you’re quickly sanity-checking an agent card, debugging task streams in the terminal, or wrapping a local script into an A2A server.</p><p>For more details, check out the following resources:</p><ul><li><a href="https://github.com/a2aproject/a2a-cli/blob/main/internal/README.md">Docs: A2A CLI Command Reference</a></li><li><a href="https://github.com/meteatamel/genai-beyond-basics/tree/main/samples/protocols/a2a">Sample: A2A Samples (Python, C#, ADK, and A2A CLI)</a></li><li><a href="https://speakerdeck.com/meteatamel/agent2agent-a2a-protocol">Presentation: Agent2Agent (A2A) Protocol</a></li></ul><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/10-26_testing_a2a_agents_with_a2a_cli/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f2a24c26f68c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/testing-and-serving-a2a-agents-from-the-terminal-with-a2a-cli-f2a24c26f68c">Testing and Serving A2A Agents from the Terminal with A2A CLI</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Google Chat Unread Messages Brief with Workspace Studio and Gemini]]></title>
            <link>https://medium.com/google-cloud/google-chat-unread-messages-brief-with-workspace-studio-and-gemini-1a201932eac5?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a201932eac5</guid>
            <category><![CDATA[generative-ai-use-cases]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 10:15:12 GMT</pubDate>
            <atom:updated>2026-10-20T10:15:12.924Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*UErwtW5IDfVmD8rm.png" /></figure><p>Coming to work every morning often means facing a wall of unread Google Chat messages from the night before across colleagues in different time zones. Skimming through every space and thread to catch up takes time.</p><p>To make my mornings easier, I built an automated <strong>Unread Messages Brief</strong> directly inside Google Chat-powered by <a href="https://studio.workspace.google.com">Google Workspace Studio</a>, <strong>Gemini</strong>, and a <strong>Google Apps Script Custom Step</strong> (with zero Google Cloud Console setup required). Everything was built with the help of <strong>Gemini 4</strong> and <strong>Antigravity</strong>, which made figuring out the Google Chat read-state API nuances and wiring up the Workspace Studio custom step effortless.</p><p>In this post, I’ll share how it works, the design decisions behind it, and how you can set it up for yourself using my <a href="https://github.com/meteatamel/google-chat-unread-brief">google-chat-unread-brief repository</a>.</p><h3>Why not just use Google Chat’s “Ask Gemini” tab?</h3><p>At this point, you might be wondering: <strong>Can’t you just use the built-in “Ask Gemini” tab in Google Chat?</strong></p><p>I had the same question and gave it a try, but ran into two main limitations:</p><ol><li><strong>Too much noise from background threads</strong>: When you ask the built-in <strong>Ask Gemini</strong> tab to summarize your unread messages, it includes <em>all</em> unread messages across every background thread-even threads you aren’t following. I wanted a brief that filters strictly for<strong>top-level unread messages</strong> and<strong>messages from threads I actively follow</strong>.</li><li><strong>No scheduled summaries</strong>: There is no way to schedule an automatic daily summary in the<strong>Ask Gemini</strong> tab so that it’s waiting for you when you start your day.</li></ol><h3>What does Unread Messages Brief do?</h3><p>With <strong>Google Workspace Studio</strong> and a small Google Apps Script custom step, the workflow gives you:</p><ul><li><strong>Daily 7:45 AM Scheduled Brief</strong>: Automatically summarizes your unread messages from the last 24 hours (top-level messages and threads you follow,<strong>without marking them as read</strong>) and posts a clean bulleted brief with clickable space links into your privateUnread Messages Brief space each weekday morning.</li><li><strong>On-Demand Trigger (</strong><strong>/briefnow)</strong>: Type /briefnow in yourUnread Messages Brief space anytime during the day to generate a fresh summary on demand.</li></ul><p>Let’s look at how to set it up step by step.</p><h3>Step 1: Create your private space in Google Chat</h3><p>First, create a dedicated space in Google Chat where your briefs will be posted:</p><ol><li>Open <a href="https://chat.google.com">Google Chat</a>.</li><li>Name the space <strong>Unread Messages Brief</strong> (keep it private with just yourself).</li></ol><h3>Step 2: Set up &amp; install the Apps Script Custom Step</h3><p>While <strong>Google Workspace Studio</strong> has built-in steps for Google Chat and Gemini, we need a custom step in Google Apps Script to query the Google Chat API’s read state ( getSpaceReadState and getThreadReadState) and fetch only the unread top-level messages and followed threads without marking them as read.</p><blockquote><em>ℹ️ Note</em></blockquote><blockquote><em>Why isn’t there a “Mark all unread messages as read” command? While reading unread state ( </em><em>spaces.list, </em><em>messages.list, </em><em>getSpaceReadState, </em><em>getThreadReadState) works out of the box with Apps Script&#39;s default project, the Google Chat API blocks write calls ( </em><em>updateSpaceReadState) unless you create, link, and configure a standard Google Cloud project in the Cloud Console. Keeping it read-only avoids any Google Cloud project setup.</em></blockquote><p>Here’s how to set up and install the custom step:</p><ol><li>Go to <a href="https://script.google.com">script.google.com</a> and click <strong>New project</strong> (name it Unread Messages Brief).</li></ol><p>2. Enable the manifest file:</p><ul><li>Click <strong>Project Settings</strong> (⚙️ icon on the left sidebar).</li><li>Check the box <strong>“Show ‘appsscript.json’ manifest file in editor”</strong>.</li></ul><p>3. Copy the files from my <a href="https://github.com/meteatamel/google-chat-unread-brief">google-chat-unread-brief</a> repository into the editor:</p><ul><li>Click <strong>Editor</strong> (&lt; &gt; icon on the left sidebar).</li><li>Open appsscript.json and replace its contents with<a href="https://github.com/meteatamel/google-chat-unread-brief/blob/main/appsscript.json">appsscript.json</a>.</li><li>Open Code.gs and replace its contents with .</li><li>Click <strong>Save</strong> (💾).</li></ul><p>4. <strong>Authorize the Script in Apps Script</strong>:</p><ul><li>In the top toolbar dropdown (next to <strong>Run</strong> / <strong>Debug</strong>), selectonExecuteFetchUnread and click <strong>Run</strong>.</li><li>Approve the Google OAuth permissions prompt when asked.</li></ul><p>5. <strong>Install Test Deployment for Workspace Studio</strong>:</p><ul><li>Click <strong>Deploy</strong> (top right) → <strong>Test deployments</strong>.</li><li>Next to <strong>Application(s): Google Workspace Add-on</strong>, click <strong>Install</strong>.</li><li>Click <strong>Done</strong>.</li></ul><p>Let’s take a quick look at what’s happening under the hood in both files.</p><p>First, appsscript.json declares the Google Chat Advanced Service ( Chat v1), read-only OAuth scopes, and exposes a custom workflow action ( Fetch Unread Chat Messages) to <strong>Google Workspace Studio</strong> with two inputs ( excludeSpaceName and maxLookbackHours) and one output ( unreadMessagesText):</p><pre>&quot;flows&quot;: {<br>  &quot;workflowElements&quot;: [<br>    {<br>      &quot;id&quot;: &quot;fetch_unread_chat_messages&quot;,<br>      &quot;state&quot;: &quot;ACTIVE&quot;,<br>      &quot;name&quot;: &quot;Fetch Unread Chat Messages&quot;,<br>      &quot;description&quot;: &quot;Fetches unread Google Chat messages across your spaces with space links, without marking them as read.&quot;,<br>      &quot;workflowAction&quot;: {<br>        &quot;inputs&quot;: [<br>          {<br>            &quot;id&quot;: &quot;excludeSpaceName&quot;,<br>            &quot;description&quot;: &quot;Name or Space ID of your Unread Messages Brief space to exclude from the summary (optional)&quot;,<br>            &quot;cardinality&quot;: &quot;SINGLE&quot;,<br>            &quot;dataType&quot;: { &quot;basicType&quot;: &quot;STRING&quot; }<br>          },<br>          {<br>            &quot;id&quot;: &quot;maxLookbackHours&quot;,<br>            &quot;description&quot;: &quot;Maximum hours to look back for unread messages (e.g. 24)&quot;,<br>            &quot;cardinality&quot;: &quot;SINGLE&quot;,<br>            &quot;dataType&quot;: { &quot;basicType&quot;: &quot;INTEGER&quot; }<br>          }<br>        ],<br>        &quot;outputs&quot;: [<br>          {<br>            &quot;id&quot;: &quot;unreadMessagesText&quot;,<br>            &quot;description&quot;: &quot;Formatted unread messages and space links ready for Gemini summarization&quot;,<br>            &quot;cardinality&quot;: &quot;SINGLE&quot;,<br>            &quot;dataType&quot;: { &quot;basicType&quot;: &quot;STRING&quot; }<br>          }<br>        ],<br>        &quot;onConfigFunction&quot;: &quot;onConfigFetchUnread&quot;,<br>        &quot;onExecuteFunction&quot;: &quot;onExecuteFetchUnread&quot;<br>      }<br>    }<br>  ]<br>}</pre><p>Second, Code.gs implements the configuration UI ( onConfigFetchUnread), the execution handler ( onExecuteFetchUnread), and the core fetchUnreadChatData function. For each active space within the lookback window, it checks your space-level lastReadTime and then inspects each thread&#39;s lastReadTime so that only unread top-level messages and replies in threads you follow are included:</p><pre>// Get the user&#39;s space-level read state<br>const readState = Chat.Users.Spaces.getSpaceReadState(<br>  `users/me/${space.name}/spaceReadState`<br>);<br>const spaceLastReadMs = new Date(readState.lastReadTime).getTime();<br>const lastActiveMs = new Date(space.lastActiveTime).getTime();<br><br>// Skip if the space is already read<br>if (lastActiveMs &lt;= spaceLastReadMs) continue;<br><br>// Fetch candidate messages newer than spaceLastReadTime (within lookback window)<br>const msgResponse = Chat.Spaces.Messages.list(space.name, {<br>  filter: `createTime &gt; &quot;${effectiveSinceIso}&quot;`,<br>  pageSize: 40<br>});<br><br>// Filter to ONLY top-level messages + replies in new or followed threads<br>for (const m of candidateMessages) {<br>  const trs = Chat.Users.Spaces.Threads.getThreadReadState(<br>    `users/me/${m.thread.name}/threadReadState`<br>  );<br>  const threadLastReadMs = (trs &amp;&amp; trs.lastReadTime)<br>    ? new Date(trs.lastReadTime).getTime()<br>    : null;<br><br>  const isTopLevel = !m.threadReply;<br>  const isReplyToNewUnreadThread = newTopLevelThreads.has(m.thread.name);<br>  const isFollowedThread = threadLastReadMs !== null &amp;&amp; threadLastReadMs &gt; spaceLastReadMs;<br><br>  if (isTopLevel || isReplyToNewUnreadThread || isFollowedThread) {<br>    // Format sender, message text, and space link for Gemini<br>  }<br>}</pre><p>Finally, onExecuteFetchUnread packages the formatted messages and space links into the unreadMessagesText output variable using AddOnsResponseService so the next step in the flow can pass it directly to Gemini.</p><h3>Step 3: Build the on-demand flow in Google Workspace Studio</h3><p>Next, open <strong>Google Workspace Studio</strong> (refresh the page if it was already open so your newly installed <strong>Unread Messages Brief</strong> custom step appears).</p><blockquote><em>💡 Tip</em></blockquote><blockquote><strong><em>Google Workspace Studio</em></strong><em> displays each output variable using its </em><em>description from </em><em>appsscript.json (for example, </em><em>unreadMessagesText appears in the picker under</em><strong><em>Fetch Unread Chat Messages →</em></strong><strong><em>Formatted unread messages and space links ready...</em></strong><em>).</em></blockquote><p>Create a new flow named Unread Messages Brief with the following steps:</p><ol><li><strong>Starter (</strong><strong>Chat → When I get a chat message)</strong>:</li></ol><ul><li><strong>Conversation type</strong>: Choose <strong>Spaces &amp; named group DMs</strong>.</li><li><strong>Spaces</strong>: Select <strong>Unread Messages Brief</strong>.</li><li>Enter /briefnow in the <strong>&quot;Message has&quot;</strong> field and choose <strong>&quot;Match text exactly&quot;</strong>.</li></ul><p>2. <strong>Step 2 (</strong><strong>Chat → Post in a space - Progress Indicator)</strong>:</p><p>3. <strong>Step 3 (</strong><strong>Custom Step → Fetch Unread Chat Messages)</strong>:</p><ul><li>Click <strong>+ Add step</strong> → scroll to <strong>Unread Messages Brief</strong> → select <strong>Fetch Unread Chat Messages</strong>.</li><li>Set <strong>Exclude Space Name or ID</strong> to Unread Messages Brief.</li><li>Set <strong>Max Lookback Window (Hours)</strong> to 24.</li></ul><p><strong>4. Step 4 (</strong><strong>Ask Gemini - Summarize)</strong>:</p><ul><li>Add an <strong>Ask Gemini</strong> step with the following prompt:</li></ul><pre>Provide a high-level summary of my unread Google Chat messages below, formatted as a bulleted list grouped by Space.<br>- If the input says NO_UNREAD_MESSAGES, just reply: &quot;🎉 You&#39;re all caught up! No unread Google Chat messages.&quot;<br>- Format each Space as a bullet point starting with the Space name in bold (* **Space Name**:) followed by an overall summary whose length is proportional to the discussion (keep it brief for 1–2 messages, and more detailed for longer discussions).<br>- Focus on the topic and outcome rather than listing who said what. Only mention a person&#39;s name (in bold, e.g. **Aaron**) when attribution is genuinely useful—such as a specific announcement, question, or action item—and omit names entirely for general group discussions or quick updates.<br>- At the end of each Space&#39;s bullet item, include a clickable chat icon linked to its exact Link from the input: [💬](https://chat.google.com/room/...).<br>- Do not add any concluding remarks or trailing blank lines after the last bullet item.<br><br>Unread Messages Data:</pre><ul><li>On the last line under Unread Messages Data:, click and select<strong>Step 3: Fetch Unread Chat Messages → </strong><strong>Formatted unread messages and space links ready...</strong>.</li></ul><p>5. <strong>Step 5 (</strong><strong>Chat → Post in a space)</strong>:</p><p>6. Click <strong>Turn on</strong>.</p><p>Here’s what the complete on-demand flow looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/733/0*bGalDlO91ezT1lVW.png" /></figure><h3>Step 4: Test the flow &amp; schedule the daily 7:45 AM brief</h3><p>To test the flow, mark any conversation in Google Chat as unread, head over to your <strong>Unread Messages Brief</strong> space, and send /briefnow.</p><p>Within seconds, you’ll see the progress indicator followed by your <strong>☕ Unread Messages Brief</strong> with bold space names, high-level topic summaries, and a clickable 💬 link at the end of each item that jumps straight to that space-while all your unread badges in the sidebar remain intact:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*J5k6nikI81SxDPPw.jpeg" /></figure><p>Once your /briefnow flow is working, you can easily create the daily morning schedule by making a copy of the flow (since <strong>Google Workspace Studio</strong> only allows one Starter per flow):</p><ol><li>In your <strong>My flows</strong> list, click next to Unread Messages Brief and select <strong>Make a copy</strong>.</li><li>Rename it to Unread Messages Brief - Scheduled.</li><li>Change the <strong>Starter</strong> at the top to <strong>Schedule</strong> → <strong>Every weekday at 7:45 AM</strong> (and delete the ⏳ progress step in Step 2 so it only posts the finished brief), then click<strong>Turn on</strong>.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/729/0*HzbpNHgx-f2-3vHo.png" /></figure><h3>Wrapping up</h3><p>In this post, we built an automated <strong>Unread Messages Brief</strong> for Google Chat that delivers both a scheduled morning summary and an on-demand/briefnow summary without marking your messages as read.</p><p>For the full code and step-by-step instructions, check out the repository on GitHub:</p><ul><li><a href="https://github.com/meteatamel/google-chat-unread-brief">Sample: google-chat-unread-brief</a></li></ul><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/10-20_google_chat_unread_brief/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a201932eac5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/google-chat-unread-messages-brief-with-workspace-studio-and-gemini-1a201932eac5">Google Chat Unread Messages Brief with Workspace Studio and Gemini</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting Up an Antigravity Workspace for Multiple Conversations]]></title>
            <link>https://medium.com/google-cloud/setting-up-an-antigravity-workspace-for-multiple-conversations-30931bc6c3f7?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/30931bc6c3f7</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[generative-ai-use-cases]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Fri, 16 Oct 2026 11:47:05 GMT</pubDate>
            <atom:updated>2026-10-16T11:47:05.963Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RR0GiYmtmZb60K5I.png" /><figcaption><em>Setting Up an Antigravity Workspace for Multiple Conversations</em></figcaption></figure><p>Beyond coding projects, I use Antigravity (AGY) all the time for personal tasks and random day-to-day questions-like trip planning, tax research, quick scripts, or brainstorming. Rather than creating a separate project folder for every little topic, I keep a single project called llm-conversations for all my miscellaneous chats and tasks.</p><p>However, using a single project for unrelated conversations comes with an annoying problem: context bleed and clutter. By default, every conversation shares the same root folder, so the agent sees files from unrelated chats, litters the root directory with random markdown files and scripts, and leaves behind orphaned files even after you delete a conversation in the UI.</p><p>To solve this, I combined Antigravity <strong>Rules</strong> and <strong>Hooks</strong> to build a multi-conversation workspace template where every chat gets its own isolated subfolder that stays automatically synced with the Antigravity UI. In this post, I’ll walk through how it works and how you can set it up using my <a href="https://github.com/meteatamel/llm-conversations-template">llm-conversations-template repository</a>.</p><h3>What does the Multi-Conversation Workspace do?</h3><p>With a workspace rule and a lightweight PreInvocation hook, this setup gives you:</p><ul><li><strong>1 Conversation = 1 Subfolder</strong>: Every new chat automatically works insideconversations/&lt;chat-title&gt;/ (for example, conversations/cyprus-trip/ orconversations/tax-planning/).</li><li><strong>Zero Context Bleed</strong>: The agent is scoped strictly to the active conversation’s subfolder for reading, searching, editing, and running commands. The project root stays clean withREADME.md as the only root file.</li><li><strong>UI-Synced Lifecycle</strong>: Rename or delete a chat in the Antigravity sidebar and its subfolder is automatically renamed or cleaned up on disk on the next turn.</li><li><strong>Chat Announcements &amp; On-Demand Git Sync</strong>: Whenever a subfolder is created, renamed, or deleted, the agent announces it at the top of its response, and changes are only committed/pushed to GitHub when you explicitly ask.</li></ul><p>Let’s look at how all the pieces fit together.</p><h3>Repository Structure</h3><p>Here is the structure of the workspace in <strong>llm-conversations-template</strong>:</p><pre>llm-conversations/<br>├── .agents/<br>│   ├── hooks.json                          # Configures the PreInvocation hook<br>│   ├── rules/<br>│   │   └── conversation-isolation.md       # Workspace isolation &amp; Git sync rules for the agent<br>│   └── scripts/<br>│       └── cleanup-conversations.py        # Syncs folder renames &amp; deletes orphaned folders<br>├── .conversations.json                     # Local Conversation ID -&gt; subfolder map (git-ignored)<br>├── .gitignore<br>├── README.md                               # Project documentation (only file in root)<br>└── conversations/<br>└── &lt;conversation-subfolder&gt;/           # Isolated directory per conversation</pre><p>There are two main components driving this workflow under .agents/: an always_on <strong>Workspace Rule</strong> and a <strong>PreInvocation Hook</strong>.</p><h3>Step 1: Per-conversation isolation with a Workspace Rule</h3><p>First, <a href="https://github.com/meteatamel/llm-conversations-template/blob/main/.agents/rules/conversation-isolation.md">.agents/rules/conversation-isolation.md</a> defines an always_on rule that instructs the agent how to manage subfolders and stay isolated inside the current conversation&#39;s directory.</p><p>Every Antigravity conversation has a unique Conversation ID injected into the agent&#39;s context (under&lt;user_information&gt;). The rule instructs the agent to maintain a local mapping between each Conversation ID and its assigned kebab-case subfolder in .conversations.json at the workspace root:</p><pre>{<br>  &quot;32392475-06f0-4146-835d-66100755ec3e&quot;: &quot;cyprus-trip&quot;,<br>  &quot;903266d9-0539-412e-baad-97f5b1a3c744&quot;: &quot;tax-planning&quot;<br>}</pre><blockquote><em>ℹ️ Note</em></blockquote><blockquote><em>.conversations.json is added to </em><em>.gitignore because conversation IDs are specific to your local Antigravity installation, even if you back up your </em><em>conversations/ folder to a private GitHub repository.</em></blockquote><p>Here is the core isolation logic from .agents/rules/conversation-isolation.md:</p><pre>---<br>trigger: always_on<br>description: Per-conversation subfolder isolation and on-demand GitHub sync rules for this workspace.<br>---<br><br># Per-Conversation Subfolder Isolation Rules<br><br>This workspace hosts multiple independent conversations on different topics.<br>Every conversation MUST stay isolated within its own subfolder inside<br>`&lt;workspace-root&gt;/conversations/`.<br><br>## 1. Subfolder Creation &amp; Naming<br>- Each conversation has a unique `Conversation ID` (provided in `&lt;user_information&gt;`).<br>- Maintain a mapping between `Conversation ID` and its subfolder name in<br>  `.conversations.json` at the workspace root (format: `{&quot;&lt;conversation-id&gt;&quot;: &quot;&lt;subfolder-name&gt;&quot;}`).<br>- When a conversation begins:<br>  1. Check `.conversations.json` in the workspace root to see if the current<br>     `Conversation ID` already has an assigned subfolder.<br>  2. If not, use the suggested subfolder name from the `PreInvocation` hook<br>     message (if provided), or derive a concise, descriptive `kebab-case`<br>     subfolder name that matches the conversation topic/name (e.g.,<br>     `cyprus-trip`, `tax-planning`), create<br>     `&lt;workspace-root&gt;/conversations/&lt;subfolder-name&gt;`, and record<br>     `&lt;subfolder-name&gt;` in `.conversations.json`.<br><br>## 2. Strict Read/Write Isolation<br>- **Read Scope**: ONLY read, search (`grep_search`, `find_by_name`), or inspect<br>  files inside the current conversation&#39;s assigned subfolder<br>  `&lt;workspace-root&gt;/conversations/&lt;subfolder-name&gt;/` (plus `.conversations.json`<br>  at the workspace root for setup). Do NOT read files in the workspace root or<br>  in other conversation subfolders unless the user explicitly instructs you to<br>  access a specific path outside your subfolder.<br>- **Write &amp; Command Scope**: Create, edit, and save all files inside<br>  `&lt;workspace-root&gt;/conversations/&lt;subfolder-name&gt;/`, and run shell commands<br>  with `&lt;workspace-root&gt;/conversations/&lt;subfolder-name&gt;` as the working<br>  directory (`Cwd`) whenever possible.</pre><p>The rule also enforces two quality-of-life behaviors:</p><p><strong>Visible Folder Announcements</strong>: Whenever a conversation subfolder is created, renamed, or deleted, the agent announces it right at the top of its response so you always know what happened on disk:</p><ul><li>📁 <strong>Folder created:</strong> conversations/&lt;subfolder-name&gt;/</li><li>📁 <strong>Folder renamed:</strong> conversations/&lt;old-name&gt;/ → conversations/&lt;new-name&gt;/</li><li>🗑️ <strong>Folder deleted:</strong> conversations/&lt;subfolder-name&gt;/ <em>(conversation deleted)</em></li></ul><p><strong>On-Demand GitHub Sync</strong>: The agent never commits or pushes automatically. Whenever you reach a milestone or want to back up your conversations, you can simply say<em>”sync”</em> or <em>“push changes”</em> in chat.</p><h3>Step 2: Automatic UI syncing with a PreInvocation Hook</h3><p>A workspace rule alone can isolate new conversations, but what happens when you <strong>rename</strong> a conversation in the Antigravity sidebar, or <strong>delete</strong> an old conversation from the UI? The agent inside an active chat doesn’t automatically know what you renamed or deleted in the sidebar unless something checks the Antigravity state on disk.</p><p>That’s where <a href="https://github.com/meteatamel/llm-conversations-template/blob/main/.agents/hooks.json">.agents/hooks.json</a> comes in. It registers a PreInvocation hook that runs<a href="https://github.com/meteatamel/llm-conversations-template/blob/main/.agents/scripts/cleanup-conversations.py">.agents/scripts/cleanup-conversations.py</a> automatically before each agent turn:</p><p>At a high level, cleanup-conversations.py keeps the workspace folders in sync with the Antigravity UI in three ways:</p><ul><li><strong>Creating Subfolders</strong>: For a new conversation, the hook reads the current conversation title from Antigravity’s local SQLite database (conversation_summaries.db) and suggests a matchingconversations/&lt;kebab-case-name&gt; folder to the agent.</li><li><strong>Renaming Subfolders</strong>: If you rename a conversation in the Antigravity sidebar, the hook detects the title change on the next turn, renamesconversations/&lt;old-name&gt;/ → conversations/&lt;new-name&gt;/ on disk, updates.conversations.json, and instructs the agent to announce the rename in chat.</li><li><strong>Deleting Orphaned Subfolders</strong>: If you delete a conversation in the Antigravity UI, its directory in~/.gemini/antigravity/brain/&lt;conversation-id&gt; is removed. On the next turn of any conversation, the hook notices that the brain directory no longer exists, deletes the orphanedconversations/&lt;subfolder-name&gt;/ directory from the workspace, cleans up.conversations.json, and instructs the agent to announce the deletion in chat.</li></ul><h3>Using the template for your own conversations</h3><p>To set up your own multi-conversation workspace:</p><ol><li>Create a new <strong>private</strong> repository on GitHub from the<a href="https://github.com/meteatamel/llm-conversations-template">llm-conversations-template</a> template (or clone it and pointorigin to your own private repository).</li><li>Remove conversations/ from .gitignore in your private repository if you want your conversation folders to be backed up when you ask the agent to sync (it is git-ignored by default in the public template so test conversations aren&#39;t committed accidentally).</li><li>Open the repository folder in Antigravity and start a conversation-your first topic folder will be created automatically under conversations/ and announced at the top of the agent&#39;s reply.</li></ol><h3>Wrapping up</h3><p>Combining a workspace rule with a PreInvocation hook turns a single Antigravity project into a clean, self-maintaining workspace for all your day-to-day conversations-without context bleed or leftover folder clutter.</p><p>For the full configuration and script, check out the template repository on GitHub: <a href="https://github.com/meteatamel/llm-conversations-template">llm-conversations-template</a></p><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/10-16_multi_conversation_agy_workspace/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=30931bc6c3f7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/setting-up-an-antigravity-workspace-for-multiple-conversations-30931bc6c3f7">Setting Up an Antigravity Workspace for Multiple Conversations</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[6 Years of Tech Debt, 1 Prompt: Modernizing a Legacy App with Gemini 4]]></title>
            <link>https://medium.com/google-cloud/6-years-of-tech-debt-1-prompt-modernizing-a-legacy-app-with-gemini-4-a4d2fa2ff318?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/a4d2fa2ff318</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[gemini-4-argon]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[machine-learning]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 16:21:05 GMT</pubDate>
            <atom:updated>2026-10-13T16:21:05.553Z</atom:updated>
            <content:encoded><![CDATA[<p>Back in early 2020, I wrote a <a href="https://atamel.dev/posts/2020/01-31_an-app-modernization-story---part-1--prototype/">4-part blog series</a> on how a friend and I modernized <a href="https://github.com/meteatamel/amathus">Amathus</a>, a Cyprus news aggregator app, using Google Cloud’s serverless technologies.</p><p>By the end of that series, we had decomposed our legacy Windows/.NET Framework monolith into a clean serverless microservices architecture on Google Cloud Run with a C#/.NET Core backend ( Amathus.Reader, Amathus.Converter, and Amathus.Web connected via Cloud Scheduler, Cloud Storage, Pub/Sub, and Firestore) and a cross-platform Flutter mobile frontend ( AmathusClient):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*UBxUW8X4ESkirETZ.png" /></figure><p>And then, as often happens with side projects, life got busy. After a <a href="https://github.com/meteatamel/amathus/commit/cc2ff599fc884c2595b60be493cf74d70cc4d8b4">final commit on November 21, 2020</a>, the repo sat untouched for nearly 6 years.</p><p>Recently, I wanted to see how <strong>Gemini 4</strong> (running in Antigravity with the google-cloud-developer plugin) would handle bringing this 6-year-old project back to life.</p><p>Not only did Gemini 4 upgrade the entire backend and frontend and deploy everything to Google Cloud with a <strong>single prompt</strong>, it also helped me transform the app with a flurry of new features in just a few hours.</p><p>Here is the story of how it went.</p><h3>Assessing 6 years of tech debt</h3><p>Before touching any code, I pointed Gemini 4 at the repository and asked it to assess how outdated the stack was.</p><p><strong>Prompt:</strong></p><pre>Here&#39;s an app I built a while back with a microservices architecture on the <br>backend and a Flutter frontend. Take a look and let me know the versions of <br>C#/.NET and Flutter used and how outdated they are.Gemini came back with <br>a breakdown of what 6 years in software looks like.</pre><p>Gemini came back with a breakdown of what 6 years in software looks like.</p><p><strong>Gemini’s response:</strong></p><pre>### 1. Backend: C# / .NET (Amathus)<br>• Framework Version: .NET Core 3.1 (netcoreapp3.1)<br>• Configured across all 5 projects: Amathus.Common.csproj, Amathus.Web.csproj,<br>Amathus.Reader.csproj, Amathus.Converter.csproj, and Amathus.FuncTests.csproj.<br>• Docker images use mcr.microsoft.com/dotnet/core/sdk:3.1-alpine and<br>mcr.microsoft.com/dotnet/core/aspnet:3.1.<br>• C# Language Version: C# 8.0 (the default for netcoreapp3.1, using the older<br>Program.Main + Startup.cs hosting model in Program.CreateHostBuilder).<br><br>#### How outdated is it?<br>• ~7 major versions behind: .NET Core 3.1 was released in December 2019 and reached<br>End of Life (EOL) in December 2022 (nearly 4 years ago).<br>• Current version: .NET 10.0 (LTS) with C# 14 (you currently have .NET 10.0.301,<br>.NET 9.0.202, and .NET 8.0.403 installed locally).<br>• Key ecosystem gaps:<br>• The Docker base image repositories (mcr.microsoft.com/dotnet/core/...) were<br>deprecated and moved to mcr.microsoft.com/dotnet/sdk and<br>mcr.microsoft.com/dotnet/aspnet starting in .NET 5.0.<br>• Key NuGet dependencies in Amathus.Common.csproj are from 2019–2020<br>(Google.Cloud.Firestore 1.1.0 vs 3.x+, Google.Cloud.Storage.V1 2.5.0 vs 4.x+,<br>Newtonsoft.Json 12.0.3 vs 13.x or built-in System.Text.Json).<br><br>### 2. Frontend: Flutter &amp; Dart (AmathusClient)<br>• Dart SDK Constraint: &gt;=2.7.0 &lt;3.0.0 in pubspec.yaml<br>• Flutter Version Era: Flutter 1.20 – 1.22 (late 2020, pre-Flutter 2.0)<br><br>#### How outdated is it?<br>• ~6 years / 2 major versions behind: Dart 2.7–2.10 and Flutter 1.x date back to<br>2020, before Dart Sound Null Safety (introduced in Dart 2.12 / Flutter 2.0 in<br>March 2021) and Dart 3 (May 2023).<br>• Current version: Flutter 3.47.6 and Dart 3.13.5 (installed on your machine).<br>• Major breaking changes between then and now:<br>1. No Null Safety: Because the SDK constraint in pubspec.yaml has an upper bound<br>of &lt;3.0.0, Dart 3+ refuses to compile the code without migrating to sound null<br>safety (&gt;=3.0.0).<br>2. Removed Flutter Theme &amp; Widget APIs:<br>• ThemeData(accentColor: Colors.amber) (removed in Flutter 3.1; replaced by ColorScheme).<br>• FlatButton (removed in Flutter 3.0; replaced by TextButton).<br>3. Discontinued / Legacy Packages in pubspec.yaml:<br>• admob_flutter: ^1.0.1 — discontinued in favor of google_mobile_ads.<br>• share: ^0.6.5+4 — discontinued in favor of share_plus.<br>4. Android Build Tooling:<br>• build.gradle uses Kotlin 1.3.50, Android Gradle Plugin 3.5.0, and the<br>shut-down jcenter() repository.</pre><p>Normally, bridging a 6-year gap across both .NET and Flutter versions, fixing broken RSS feeds and redeploying a multi-service cloud architecture is a multi-weekend project. But this time was different.</p><h3>From staged approvals to Turbo mode</h3><p>My initial goal was simple: upgrade all the backend microservices and the Flutter frontend to the latest versions, get them running locally, and deploy them to Google Cloud.</p><p>At first, I took the cautious route. I asked Gemini to come up with a staged plan and let me approve and test each stage as the services were upgraded.</p><p>However, within 10 minutes, I realized that I was simply rubber-stamping and approving everything Gemini proposed anyway. So I quit the session, started over in <strong>Turbo mode</strong> in Antigravity and gave Gemini 4 a single prompt to do the entire job autonomously.</p><p><strong>Prompt:</strong></p><pre>Please make a plan to upgrade both the backend and the frontend to the latest versions.<br>Save the plan for me to take a look but don&#39;t wait for my approval.<br><br>Follow this process during your upgrade:<br><br>- Upgrade the .NET backend to the latest, make sure it runs locally.<br>- During the upgrade, make sure the news feeds are still active. If some of the<br>news sources do not work, try to find the new links and if you can&#39;t, just<br>remove the obsolete news sources.<br>- Deploy the .NET backend to Google Cloud project. Use events-atamel as my main<br>project for this. Test all microservices and update scripts to deploy/test these<br>services.<br>- Upgrade the Flutter frontend with the latest, make sure it runs locally.<br>- Deploy a Flutter web frontend for it so I can test the UI afterwards.<br>- Make sure all READMEs and scripts for local testing and also cloud deployment<br>are also updated.<br>- Once the upgrade is done, do a review of the UI, make sure all the icons and<br>text displays properly, images are properly sized and doesn&#39;t take too much space.<br>Try to make it look pretty.<br>- Make all the changes locally without checking into Git. Once finished, I&#39;ll do<br>the checkin myself once I&#39;m happy with the changes.<br>- Try to be as autonomous as possible unless you really need my feedback</pre><h3>What Gemini 4 did on its own</h3><p>Once I hit enter, Gemini 4 went to work. It pulled in the gcloud and cloud-run-basics skills from the google-cloud-developer plugin, created a 5-phase implementation_plan.md and a task.md checklist to track its own progress, and worked through the entire modernization end-to-end without a single intervention from me.</p><p>Here is a brief look at the implementation_plan.md it generated:</p><pre># Amathus Backend &amp; Frontend Upgrade Plan<br><br>## Phase 1: .NET Backend Upgrade (`netcoreapp3.1` → `net10.0`)<br>1. Project &amp; Package Upgrades: Update &lt;TargetFramework&gt; to net10.0 across all 5 .csproj<br>files and upgrade NuGet dependencies (Google.Cloud.Firestore, Google.Cloud.Storage.V1,<br>System.ServiceModel.Syndication, etc.).<br>2. Code &amp; Reliability Improvements: Fix async/await bug in ReadController.cs, update<br>FeedReader to use HttpClient, add an image proxy endpoint in Amathus.Web for Flutter<br>Web CORS, and update Dockerfiles to .NET 10.0 images.<br><br>## Phase 2: Audit &amp; Update RSS News Sources<br>1. Test all 31 RSS feeds in amathussources.json and their logo URLs.<br>2. Discover updated RSS/logo URLs on active websites and remove defunct sources.<br>3. Update Source.cs, FeedConverter.cs, and functional tests, and verify with dotnet test.<br><br>## Phase 3: Deploy .NET Backend to Google Cloud (`events-atamel`)<br>Update Amathus/scripts/ to target events-atamel and deploy amathus-reader,<br>amathus-converter, and amathus-web to Cloud Run.<br><br>## Phase 4: Flutter Frontend Upgrade (`AmathusClient`) &amp; Web Deployment<br>1. Upgrade pubspec.yaml to Dart 3 / Flutter 3.41 and replace discontinued packages.<br>2. Migrate to Sound Null Safety and SharedPreferences for Web compatibility.<br>3. Modernize UI with Material 3, responsive layouts, and deploy amathus-client to Cloud Run.<br><br>## Phase 5: Scripts &amp; Documentation Update<br>Update READMEs and helper scripts, leaving all changes uncommitted for user review.</pre><p>And the task.md checklist it updated as it completed each phase:</p><pre># Amathus Upgrade Tasks<br><br>- [x] **Phase 1: .NET Backend Upgrade (`net10.0`)**<br>- [x] Upgrade all 5 `.csproj` files from `netcoreapp3.1` to `net10.0` and update NuGet packages<br>- [x] Fix async upload bug in `ReadController.cs` and improve `FeedReader` / `DateTolerantXmlTextReader`<br>- [x] Update Dockerfiles to `.NET 10.0` images and `appsettings*.json` to `events-atamel`<br>- [x] **Phase 2: Verify &amp; Update News Feeds (`amathussources.json`)**<br>- [x] Audit all 31 RSS feed URLs and logo URLs<br>- [x] Find updated RSS/logo URLs for moved sources and remove obsolete sources (17 active feeds verified)<br>- [x] Update `Source.cs`, `FeedConverter.cs`, `FeedTest.cs`, and `FeedtemTest.cs`<br>- [x] Verify all functional tests pass (`dotnet test` — 35/35 passed) and backend runs locally (`InMemory`)<br>- [x] **Phase 3: Deploy Backend Microservices to Google Cloud (`events-atamel`)**<br>- [x] Inspect existing resources on `events-atamel` (Cloud Run, Cloud Storage, Pub/Sub, Firestore, Cloud Scheduler)<br>- [x] Update `Amathus/scripts/` for `events-atamel` and test scripts (`test_services`)<br>- [x] Build and deploy `amathus-reader`, `amathus-converter`, and `amathus-web` to Cloud Run<br>- [x] End-to-end test all 3 deployed backend microservices<br>- [x] **Phase 4: Upgrade Flutter Frontend (`AmathusClient`) &amp; Deploy Web App**<br>- [x] Upgrade `pubspec.yaml` to Dart 3 / Flutter 3.41 and modern packages<br>- [x] Migrate codebase to Sound Null Safety and Web compatibility<br>- [x] Redesign &amp; polish UI (icons, typography, properly sized images, cards, responsive web layout)<br>- [x] Verify Flutter app locally and deploy Flutter Web frontend (`amathus-client`) to Cloud Run<br>- [x] **Phase 5: Update READMEs and Scripts**<br>- [x] Update `README.md`, `AmathusClient/README.md`, and deployment/testing scripts</pre><p>Let’s break down what Gemini did in each subtask.</p><h4>Upgraded the backend from .NET Core 3.1 to .NET 10.0</h4><p>Gemini updated all 5 .csproj files to net10.0, upgraded all NuGet packages and Dockerfiles, fixed a subtle async void bug in ReadController.cs where Cloud Storage uploads weren&#39;t being awaited, and even added an image proxy endpoint to prevent CORS issues on Flutter Web.</p><p><strong>Gemini’s response:</strong></p><pre>▸ Thought for 22s: The plan is to upgrade the Amathus project&#39;s `.csproj` files to a newer target framework<br>▸ Thought for 35s: Examining the Dockerfiles for the three services, the plan is to update the base image to the .NET 10.0 runtime<br>▸ Thought for 40s: Build verification confirms successful compilation on the specified .NET version, with no detected errors<br>...<br>#### 1. .NET Backend Upgrade (netcoreapp3.1 -&gt; net10.0)<br>• Upgraded all 5 projects (Amathus.Common.csproj, Amathus.Reader.csproj,<br>Amathus.Converter.csproj, Amathus.Web.csproj, Amathus.FuncTests.csproj) to<br>.NET 10.0 (net10.0) and updated all NuGet packages (Google.Cloud.Firestore 3.10.0,<br>Google.Cloud.Storage.V1 4.13.0, Microsoft.AspNetCore.Mvc.NewtonsoftJson 10.0.0,<br>System.ServiceModel.Syndication 10.0.0, MSTest 3.8.2).<br>• Updated all 3 Dockerfiles to mcr.microsoft.com/dotnet/sdk:10.0 and<br>mcr.microsoft.com/dotnet/aspnet:10.0.<br>• Fixed an async void bug in ReadController.Read (await Task.WhenAll(...) instead<br>of feeds.ForEach(async ...)).<br>• Updated FirestoreFeedStore and CloudStorageSyndFeedStore to use<br>GoogleCredential.FromComputeCredential() when running on Cloud Run (K_SERVICE)<br>and reuse the StorageClient instance.<br>• Added ImageProxyController (GET /api/v1/imageproxy?url=...) to proxy remote news<br>logos and article thumbnails with CORS and cache headers for Flutter Web.</pre><h4>Audited and repaired 6 years of link rot in RSS feeds</h4><p>Six years is an eternity for local newspaper websites. Out of the 31 RSS feeds in amathussources.json, many had changed their feed URLs, switched logo paths, or shut down entirely. Gemini wrote a script to probe every RSS feed and logo URL, discovered the new links on the active newspaper websites, removed defunct sources, and updated the XML parsers and functional tests.</p><p><strong>Gemini’s response:</strong></p><pre>▸ Thought for 37s: Running a script to check the status of all RSS feeds and their logos should provide a current overview<br>▸ Thought for 22s: 33 out of 39 tests passed immediately, indicating an initial success rate.<br>▸ Thought for 1m 10s: Initial data retrieval seems successful: the feed fetch completed quickly with all expected items<br>...<br>#### 2. RSS News Sources Audit &amp; Update<br>• Audited all 31 sources in amathussources.json. Updated moved RSS feed URLs and<br>HTTPS logo URLs for 17 active sources (cyprustoday, detaykibris, diyalog,<br>gazeddakibris, giynik, gundemkibris, haberalkibrisli, halkinsesi, havadis,<br>kibrisgazetesi, kibrisgercek, kibrismanset, kibristime, londragazete, vatan,<br>yenicag, yeniduzen) and removed defunct/blocked sources.<br>• Updated DateTolerantXmlTextReader and the feed item converters in<br>Amathus.Common/Converter to handle modern &lt;atom:link rel=&quot;self&quot;&gt;, &lt;enclosure&gt;,<br>and &lt;media:content&gt; tags.<br>• Verified all 35/35 functional tests pass (dotnet test Amathus.sln).</pre><h4>Migrated Flutter 1.x to 3.41 and visually debugged the UI</h4><p>On the frontend, Gemini migrated AmathusClient from Dart 2.7 to Dart 3 Sound Null Safety and Flutter 3.41 with Material 3, replaced dart:io file storage with SharedPreferences for web compatibility, and containerized the Flutter Web build with Nginx.</p><p>Most impressively, after deploying the web app, <strong>Gemini opened the live URL, took screenshots to visually inspect its own UI work, and fixed visual issues it spotted</strong> — such as placing white transparent newspaper logos on dark contrast badges and fixing clipped card edges.</p><p><strong>Gemini’s response:</strong></p><pre>▸ Thought for 31s: Upgrading the Flutter frontend, `AmathusClient`, begins with updating the Dart SDK constraint in `pubspec.yaml`<br>▸ Thought for 57s: Implemented image constraints for different feed item types: fixed size thumbnails for compact lists<br>▸ Thought for 16s: Attempting to capture a screenshot of the specified URL to visually inspect the rendered content<br>▸ Thought for 41s: It&#39;s necessary to investigate why the right edge of cards were clipped in a previous screenshot<br>▸ Thought for 15s: Successfully built the web application. Proceeding to rebuild and redeploy the client to the cloud environment<br>...<br>#### 3. Flutter Frontend Upgrade (AmathusClient) &amp; UI Redesign<br>• Upgraded pubspec.yaml from Dart 2.7 to Dart 3.11 / Flutter 3.41 with sound null<br>safety and web-compatible packages (http, shared_preferences, url_launcher,<br>share_plus, flutter_widget_from_html_core, font_awesome_flutter, timeago).<br>• Replaced dart:io file storage in FeedsStorage with SharedPreferences so custom<br>source ordering works across Web, mobile, and desktop.<br>• Redesigned the UI across FeedsList, FeedListTile, FeedImage, FeedItemListTile,<br>FeedItemListTileWide, FeedItemView, AppDrawer, AppBottomNavigationBar,<br>SettingsView, and ContactView:<br>• Contrast-safe logo badges: Automatically places white-text transparent logos<br>(cyprustoday, detaykibris, gundemkibris, haberalkibrisli, kibrisgazetesi,<br>londragazete) on a dark slate badge and dark/colored logos on a white badge<br>alongside the source name and last updated time.<br>• Properly sized images: Constrained article thumbnails (96x76 in source lists,<br>124x92 in recent news cards, and 280px max height in article detail view)<br>with rounded corners and BoxFit.cover.<br>• Responsive layout: Adapts from a 1-column list on mobile to a 2/3-column<br>grid on tablet/desktop screens.</pre><h4>Deployed everything to Cloud Run</h4><p>Using the gcloud and cloud-run-basics skills, Gemini set up the Cloud Storage bucket, Pub/Sub topic and push subscription, Firestore database, and Cloud Scheduler job, and deployed all three backend microservices and the Flutter Web frontend to Cloud Run in events-atamel ( europe-west1).</p><p><strong>Gemini’s response:</strong></p><pre>● Read(~/.gemini/config/plugins/google-cloud-developer/skills/gcloud/SKILL.md)<br>▸ Thought for 18s: Considering the deployment target is Cloud Run, information relevant to &quot;cloud-run-basics&quot; must be retrieved...<br>▸ Thought for 1m 4s: Creating the storage bucket and Pub/Sub topic are the initial infrastructure steps.<br>▸ Thought for 13s: Services are now deployed and accessible. Proceeding to configure the necessary infrastructure components<br>▸ Thought for 26s: Investigating the root cause of the &quot;default credentials not found&quot; error in `amathus-converter`<br>▸ Thought for 11s: Deployment of the latest `amathus-client` image to Cloud Run in the specified region and project is required<br>...<br>Both the .NET backend (Amathus) and the Flutter frontend (AmathusClient) have been<br>upgraded to the latest versions, verified locally, and deployed to your Google Cloud<br>project (events-atamel in europe-west1). All changes remain uncommitted in your<br>local working tree so you can review and commit them when ready.<br>──────<br>### Live Cloud Run Services (events-atamel / europe-west1)<br>Service           | Role                                        | Live URL<br>-------------------|---------------------------------------------|----------------------------------------------<br>amathus-client    | Flutter Web Frontend                        | amathus-client-1086570528700.europe-west1.run.app<br>amathus-web       | Public REST API (/api/v1/feeds,             | amathus-web-1086570528700.europe-west1.run.app<br>| /api/v1/feeditems, /api/v1/imageproxy)      |<br>amathus-reader    | Scheduled RSS Feed Reader (amathus-reader-  | amathus-reader-1086570528700.europe-west1.run.app<br>| job every 10m -&gt; gs://amathus-events-       |<br>| atamel-bucket)                              |<br>amathus-converter | Pub/Sub Push Converter (amathus-events-     | amathus-converter-1086570528700.europe-west1.run.app<br>| atamel-topic -&gt; Firestore feeds)            |</pre><h3>Going further: A flurry of new features</h3><p><strong>Seeing Gemini 4 complete a 6-year full-stack upgrade and cloud deployment in onego was earth-shattering.</strong> I didn’t want to stop there. Staying in Turbo mode, I kept prompting Gemini to evolve the app into something more (see <a href="https://github.com/meteatamel/amathus/commits/master">all commits</a>).</p><h4>Bilingual UI &amp; navigation cleanup</h4><p>First, I asked for a few quick UX improvements.</p><p><strong>Prompt:</strong></p><pre>Great. I want you to make minor changes to the UI:<br><br>1- Currently the app is in Turkish, but add English as well and a way to toggle<br>between Turkish and English in settings.<br>2- Make &quot;latest news&quot; the main/first tab and &quot;all news&quot; as second. Also, rename<br>&quot;all news&quot; to something else. It&#39;s basically news from each news source,<br>maybe something like &quot;newspapers&quot;?<br>3- Change &quot;Contact&quot; page and only include my Twitter: https://twitter.com/meteatamel<br>and my dev email</pre><p><strong>Gemini’s response:</strong></p><pre>All three UI updates have been implemented, tested locally, and redeployed to Cloud Run:<br><br>• Live Web App URL: https://amathus-client-1086570528700.europe-west1.run.app<br><br>### Summary of Changes<br><br>1. Turkish &amp; English Localization + Settings Toggle:<br>• Added bilingual (tr / en) localization state and persistence via SharedPreferences<br>in constants.dart and reactive rebuilding in AmathusApp (including timeago relative<br>timestamps in feeditem_view.dart, feed_list_tile.dart, feeditem_list_tile.dart, and<br>feeditem_list_tile_wide.dart).<br>• Added a Language selector card (Türkçe / English SegmentedButton) at the top of<br>SettingsView.<br>2. Reordered Tabs &amp; Renamed &quot;All News&quot; to &quot;Newspapers&quot; (Gazeteler):<br>• Made Latest News (Son Haberler / Latest News, FeedItemsRecentView) the default home<br>screen and first tab (index: 0).<br>• Made Newspapers (Gazeteler / Newspapers, FeedsView) the second tab (index: 1)<br>across AppBottomNavigationBar and AppDrawer.<br>3. Updated Contact Page:<br>• Updated ContactView and constants.dart to remove Facebook and display only your<br>Twitter (https://twitter.com/meteatamel) and dev email (atameldev@gmail.com).</pre><h4>Expanding to all of Cyprus: Turkish, Greek, and English</h4><p>Originally, Amathus only aggregated Turkish-language newspapers from Cyprus. I asked Gemini to research and add news sources from across the <strong>entire island of Cyprus</strong> in <strong>Turkish, Greek, and English</strong>, verify that every feed had current articles and working logos, and add language filtering with flag icons.</p><p><strong>Prompt:</strong></p><pre>Currently this news reader app only contains news sources in Turkish<br>(and some English) from northern Cyprus. I want to expand it to include news<br>sources from all Cyprus and in Greek as well. Can you do a research on what other<br>news sources I can add? I&#39;m looking for news sources from Cyprus with RSS feeds<br>and logos and in Turkish, Greek, or English.</pre><p>Gemini inspected the XML of every candidate RSS feed, filtered out empty or stale feeds, onboarded 17 new Greek, English, and Turkish feeds (doubling the total to <strong>34 active Cyprus newspapers</strong>), and added full Greek ( Ελληνικά) UI localization and vector flag icons.</p><p><strong>Gemini’s response:</strong></p><pre>### Summary of Changes<br><br>#### 1. Backend (Amathus/) — 34 Cyprus News Sources &amp; Language Support<br>- amathussources.json: Added all 17 verified news sources (8 Greek: AlphaNews.Live,<br>Dialogos, Lemesos Tv, Pafos Press, Philenews, Politis, Sigmalive, ToThemaOnline;<br>5 English: Cyprus Mail, Cyprus Today, Financial Mirror, Politis English, T-Vine;<br>21 Turkish total) along with a &quot;Language&quot; property (&quot;tr&quot;, &quot;el&quot;, or &quot;en&quot;).<br>- Source.cs &amp; Feed.cs: Added source ID constants and [FirestoreProperty] public string<br>Language { get; set; } so language metadata is persisted in Firestore and returned<br>by the API.<br>- Amathus.Web: Added optional ?language= query parameter filtering to /api/v1/feeds<br>and /api/v1/feeditems.<br><br>#### 2. Frontend (AmathusClient/) — Trilingual UI &amp; Flag Icons<br>- Rebranded app from North Cyprus News (KKTC) to Cyprus News (Kıbrıs Haber /<br>Ειδήσεις Κύπρου / Cyprus News).<br>- Added full Greek (Ελληνικά) UI localization and timeago messages alongside Turkish<br>and English.<br>- Created FlagIcon widget rendering crisp vector Turkish, Greek, and British flags<br>across all platforms, and added SourceLanguageFilterBar to filter news sources by<br>All, Turkish, Greek, or English.</pre><h4>On-demand translation &amp; “Cyprus Bicommunal News”</h4><p>Having Turkish, Greek, and English newspapers side-by-side naturally begged the next question: <em>what if you can’t read Greek or Turkish?</em> I asked Gemini to add article translation using a Google Cloud service, allow hiding specific newspapers in Settings, rebrand the app to <strong>Cyprus Bicommunal News</strong>, and even generate a new logo for it.</p><p><strong>Prompt:</strong></p><pre>Thanks. Now, I want a translation feature. For example, if news is in Turkish,<br>I want to be able to translate to Greek or English, and if it&#39;s Greek, I want to<br>be able to translate to Turkish and English, and if it&#39;s purely English, then<br>translate to Turkish or Greek. I want this button in every news item, ideally<br>by the share button on the top, but feel free to find a better place. Not sure<br>what you&#39;d use for translation API, but prefer to use Google or Google Cloud<br>service and inform me about your decision.<br>...<br>I want the app to be called &quot;Cyprus Bicommunal News&quot;... Also, have a way to<br>unselect/hide some of the newspapers in settings... And I want a logo for my<br>Cyprus Bicommunal News app. Ideally, it should include the map of Cyprus and a<br>group of people (men, women, children) and some newspaper theme.</pre><p><strong>Gemini’s response:</strong></p><pre>### 1. Translation API Decision: Google Cloud Translation API V2 (translate.googleapis.com)<br>- Why this service:<br>1. Already enabled in events-atamel: translate.googleapis.com was already active on<br>your GCP project, and amathus-web authenticates automatically on Cloud Run via<br>Application Default Credentials (ADC) — so no API keys are needed or exposed to<br>the Flutter client.<br>2. HTML-aware translation: Using Google.Cloud.Translation.V2 (TranslateTextAsync for<br>Title/Summary and TranslateHtmlAsync for Detail) with in-memory caching.<br><br>### 2. Frontend Translation, Newspaper Hiding &amp; Branding<br>- Added TranslateController and TranslateIconButton offering source-aware target<br>translation languages with flag icons (Turkish -&gt; Greek/English, Greek -&gt;<br>Turkish/English, English -&gt; Turkish/Greek) and [Source Flag] -&gt; [Translation Flag]<br>indicators.<br>- Added an eye/visibility icon button per newspaper in SettingsView to hide unselected<br>newspapers from Latest News and Newspapers.<br>- Integrated the new Cyprus Bicommunal News logo across the AppBar banners, navigation<br>drawer header, web favicon/PWA icons, and Android/iOS/macOS launcher icons.</pre><h4>One-click bulk translation &amp; UI polish</h4><p>As I played with the live web app, I realized translating articles one by one was too much clicking when browsing headlines. Over a few quick follow-up prompts, I asked Gemini to replace the per-card translate buttons with a single top-right bulk <strong>Translate</strong> dropdown button and clean up the card layout.</p><p><strong>Prompt:</strong></p><pre>Instead of having translate buttons for each news item in both latest news and<br>newspapers pages, just keep the one translate button on top right. Somehow make<br>it more obvious that this is a translation button. When translation is in effect,<br>still show the to/from flags but move it in place of the translation button of<br>the news item now (left of share button basically). This should simplify UI.</pre><p><strong>Gemini’s response:</strong></p><pre>All updates have been applied and deployed to amathus-client:<br><br>1. Single Prominent Top-Right Translate Button (BulkTranslateIconButton):<br>Styled the top-right translate control as a clear dropdown button showing<br>[Translate Icon] Translate ▾ (localized to Çevir / Μετάφραση / Translate), and<br>[Translate Icon / Spinner] [Flag] &lt;Language&gt; ▾ when a translation is active.<br>2. Left Source Dropdown:<br>Updated SourceLanguageFilterBar to place the source language dropdown (All,<br>Türkçe, Ελληνικά, English) on the left and BulkTranslateIconButton on the right.<br>3. Removed Per-Item Translate Buttons in Lists:<br>Removed the individual translate buttons from each news card on both Latest News<br>and Newspaper pages, and placed the [From Flag] → [To Flag] indicator directly to<br>the left of the Share button when a translation is active.</pre><h3>The Final Result</h3><p>Here is what <strong>Cyprus Bicommunal News</strong> looks like running live on the web today:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zs_09AmtvEdVHabs.png" /></figure><p>You can try out the live Flutter Web version on Cloud Run and check out the updated code on GitHub:</p><ul><li><strong>Live Web App</strong>: <a href="https://amathus-client-1086570528700.europe-west1.run.app/">https://amathus-client-1086570528700.europe-west1.run.app/</a></li><li><strong>GitHub Repo</strong>: <a href="https://github.com/meteatamel/amathus">https://github.com/meteatamel/amathus</a></li></ul><h3>Wrapping up</h3><p>Looking back at my <a href="https://atamel.dev/posts/2020/01-31_an-app-modernization-story---part-1--prototype/">2020 app modernization blog series</a>, every phase of modernization back then, lifting and shifting, containerizing, moving to serverless, and migrating frameworks took weekends of manual effort and careful troubleshooting.</p><p>Fast forward to today: <strong>Gemini 4</strong> took a 6-year-old codebase, migrated the backend across 7 major versions of .NET, migrated the frontend from Flutter 1.x to Flutter 3.41 with Sound Null Safety, repaired dozens of broken RSS feeds, deployed 4 microservices to Cloud Run, visually debugged its own UI via screenshots, and then helped me turn a single-language news reader into a trilingual, real-time translated <strong>Cyprus Bicommunal News</strong> web and mobile app in a <strong>matter of hours</strong>.</p><p>This is just the beginning. With Gemini 4, I have so many ideas on how to improve the app further, such as adding a AI news overview, AI news categorization, a daily AI generated podcast and more. Sky is the limit!</p><p>If you have an old side project gathering dust on GitHub, now is a great time to point Gemini 4 at it and see how far you can go.</p><p><strong>Great time to be alive as a builder!</strong></p><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/10-13_modernizing_6_year_old_app_gemini_4/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a4d2fa2ff318" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/6-years-of-tech-debt-1-prompt-modernizing-a-legacy-app-with-gemini-4-a4d2fa2ff318">6 Years of Tech Debt, 1 Prompt: Modernizing a Legacy App with Gemini 4</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Scheduling Tasks in Antigravity]]></title>
            <link>https://medium.com/google-cloud/scheduling-tasks-in-antigravity-b37f6c9b61ca?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/b37f6c9b61ca</guid>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[generative-ai]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Tue, 22 Sep 2026 15:58:53 GMT</pubDate>
            <atom:updated>2026-09-22T15:58:53.225Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*D28zT2F2teY7Si_X.png" /></figure><p>Antigravity (AGY) allows you to schedule instructions to run on a recurring schedule or one-time timer. However, it has two different ways of scheduling that results in two different behaviors.</p><p>In this post, I’ll share my learnings on scheduling tasks in AGY and the gotchas to look out for.</p><h3>Two ways of scheduling tasks in AGY</h3><p>In AGY, there are two ways of scheduling tasks:</p><ol><li>With the /schedule command in the chat.</li><li>With the <strong>Scheduled Tasks</strong> panel on the top left.</li></ol><p>While both let you schedule tasks, they are completely independent from each other and the way they schedule and display tasks are completely different as well. This can be confusing, so let’s look into each in more detail to clear up the confusion.</p><h3>Permissions for scheduled tasks</h3><p>Before we dive into how to schedule tasks, it’s important to note that scheduled tasks require the same permissions that you’d grant to the agent in your own conversations.</p><p>For example, let’s say you want to schedule this task:</p><blockquote>Get the current weather in London from <a href="https://open-meteo.com/">https://open-meteo.com/</a> and save it to a timestamped file</blockquote><p>This prompt requires that the agent has write access to the project folder and it also has HTTP access to the Open-Meteo API. Normally, you’d allow these permissions as you go along at the conversation or project level but in scheduled tasks, you need to add these permissions ahead of time.</p><blockquote><em>💡 </em><strong><em>Tip</em></strong></blockquote><blockquote><strong><em>Before scheduling a task, dry run the task in a project and add all the necessary permissions at the project level to make sure the scheduled task will run without permissions issues later.</em></strong></blockquote><p>In this case, I created a project and ran the prompt in the chat. I got the following prompt to allow HTTP access:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*RjuQ5YpEpSXm4Fc8.png" /></figure><p>I allowed it for the project and now, I’m ready to schedule this prompt as a task in this project.</p><h3>/schedule command</h3><p>The first way to schedule tasks in AGY is with the /schedule command in a conversation. This command allows you to schedule <strong>one-time</strong> or <strong>recurring</strong> instructions as background tasks in the <strong>same conversation</strong>. As these background tasks are scheduled and executed, you can continue chatting with the agent in the same conversation as usual.</p><h3>One-time task</h3><p>For one-time task, you can try a prompt like this:</p><blockquote>/schedule In 1 minute, get the current weather in London from <a href="https://open-meteo.com/">https://open-meteo.com/</a> and save it to a timestamped file in the current folder.</blockquote><p>First, you’ll see a background task scheduled in the chat:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Zuv0d7fS4u5W-_BE.png" /></figure><p>You’ll also see the same background task on the right panel:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/590/0*R8WHsh19mDVwW8hA.png" /></figure><p>If you click on the background task on the right panel, you can also see the logs of the task which is quite useful:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/814/0*l9zm5ePhZygL095V.png" /></figure><p>Once the task is done, you’ll see updates in the chat and also the file created in the project folder:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6DMOTclVfJUGbBdC.png" /></figure><h3>Recurring task</h3><p>For a recurring task, you can try this:</p><blockquote>/schedule Every 10 minutes, get the current weather in London from <a href="https://open-meteo.com/">https://open-meteo.com/</a> and save it to a timestamped file</blockquote><p>You should see a similar background task in the chat and on the right hand side panel:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*66uRBiCKiSmqv9Ag.png" /></figure><p>Similar to the one-time task, you can click on the background task to see the logs.</p><p>Every time the task runs, you’ll see updates in the chat and also the file created in the project folder.</p><p>If you close AGY desktop app and come back later, the background task will still be there and continue running at the scheduled interval.</p><p>However, if you further kill the AGY process like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/324/0*V6BopEv-2v3fxyEq.png" /></figure><p>The background task will be cancelled:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/824/0*wdmj0Iz9HhU7I9ch.png" /></figure><blockquote><strong><em>⚠️ Warning</em></strong></blockquote><blockquote><strong><em>Recurring tasks scheduled with </em></strong><strong><em>/schedule command are cancelled if the AGY process is killed and you need to reschedule them.</em></strong></blockquote><p>Furthermore, you won’t see one-time or recurring tasks scheduled with the /schedule command in the <strong>Scheduled Tasks</strong> panel on the top left:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*5yT4xfZ1vEMu05Jy.png" /></figure><blockquote><strong><em>⚠️ Warning</em></strong></blockquote><blockquote><strong><em>It’s confusing that tasks scheduled with </em></strong><strong><em>/schedule command in a conversation do not show up on the Scheduled Tasks panel.</em></strong></blockquote><h3>Scheduled Tasks panel</h3><p>The second way to schedule tasks in AGY is with the <strong>Scheduled Tasks</strong> panel on the top left but it’s different from the/schedule command in a number of ways:</p><ol><li>You can only schedule recurring tasks with a cron schdule (i.e. no one-time tasks).</li><li>The scheduled tasks create their own conversation (i.e. not part of an existing conversation).</li><li>The scheduled tasks are actually scheduled sidecars with the added benefit of surviving AGY process restarts.</li></ol><p>Let’s create the same example as a scheduled task and look at the differences.</p><p>Go to the <strong>Scheduled Tasks</strong> panel and create a new task as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/992/0*Ien7nCK9BM9T-uLE.png" /></figure><p>Once you create the task, you’ll see it listed under <strong>Scheduled Tasks</strong> panel.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2Nlbo6flp7FYxdic.png" /></figure><p>You can also get the hint that it’s actually a sidecar. You can verify this by realizing that a new sidecar file is created in ~/.gemini/config/sidecars/london-weather-every-ten-min/sidecar.json:</p><pre>{<br>  &quot;builtin&quot;:  &quot;schedule&quot;,<br>  &quot;args&quot;:  [<br>    &quot;*/10 * * * *&quot;,<br>    &quot;agentapi&quot;,<br>    &quot;new-conversation&quot;,<br>    &quot;Get the current weather in London from https://open-meteo.com/ and save it to a timestamped file&quot;<br>  ],<br>  &quot;display_name&quot;:  &quot;london-weather-every-ten-mins&quot;<br>}</pre><p>The sidecar is also enabled in ~/.gemini/config/config.json:</p><pre>{<br>  &quot;sidecars&quot;: {<br>    &quot;london-weather-every-ten-mins&quot;: {<br>      &quot;enabled&quot;: true,<br>      &quot;projectId&quot;: &quot;17bc69c1-0072-48b5-9b44-180b5e1c8694&quot;<br>    }<br>  },<br>  ...</pre><p>After 10 minutes, you should see the sidecar creating a new conversation and running the task. You’ll see updates in the chat and also the file created in the project folder.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*hBotaeXBGtG351x-.png" /></figure><p>If you close AGY desktop app and come back later, the scheduled task will still be there and continue running at the scheduled interval.</p><p>If you kill the AGY process, the sidecar will also be killed and you can verify this by checking the logs in ~/.gemini/antigravity/sidecar_data/london-weather-every-ten-mins/logs:</p><pre>ERROR: logging before google.Init: I0922 12:24:50.517034      39 schedule.go:99] <br>[schedule] Scheduler shutting down cleanly on signal: interrupt</pre><p>However, if you start AGY app again, a new sidecar process will start up and will continue running the task at scheduled interval.</p><pre> [schedule] Next execution scheduled in 20s (at 2026-09-22 15:30:00 UTC), <br> will run by 2026-09-22 15:31:33 UTC</pre><blockquote><strong><em>ℹ️ Note</em></strong></blockquote><blockquote><strong><em>If you want scheduled tasks to survive restarts, it’s better to use the Scheduled Tasks panel.</em></strong></blockquote><h3>Summary</h3><p>In this blog post, we’ve looked at two ways to schedule tasks in AGY. Here’s a summary of the key differences:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qV_Pnj3GbORJZeRyhp9aeA.png" /></figure><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/09-22_scheduling_tasks_agy/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b37f6c9b61ca" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/scheduling-tasks-in-antigravity-b37f6c9b61ca">Scheduling Tasks in Antigravity</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Agent Permissions in Antigravity]]></title>
            <link>https://medium.com/google-cloud/agent-permissions-in-antigravity-da9d326e63f4?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/da9d326e63f4</guid>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Mon, 14 Sep 2026 10:54:21 GMT</pubDate>
            <atom:updated>2026-09-14T10:54:21.417Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*LVrZQoO_iGr8z35k.png" /></figure><p>Antigravity (AGY) has an extensive permission system that defines how the agent should behave and what it has access to. However, these permissions are scattered across different settings and saved in various files that might make reasoning them difficult.</p><p>In this post, I’ll deep dive into these permission settings and share my learnings.</p><h3>Permission Hierarchy</h3><p>Before looking at the permission settings in detail, it’s important to note that the permissions in AGY are organized in a hierarchy:</p><ol><li>Global (user) permissions.</li><li>Project (workspace) permissions.</li><li>Conversation (sesssion) permissions.</li></ol><p>Each level inherits the permissions of the level above it but can override them. A project inherits global permissions but it can override them and a conversation can override both project and global permissions.</p><h4>Global permissions</h4><p>Global permissions apply to all projects and conversations of the user. They can be access via Settings -&gt; General and are scattered across Agent Settings, Agent Behavior, Network Permissions, Terminal &amp; Tooling Permissions and Browser sections:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/656/0*zhZ27IhwYEO2VrSX.png" /></figure><p>These permissions are persisted in ~/.gemini/config/config.json file.</p><h4>Project permissions</h4><p>Project permissions inherit global permissions and can override them at the project level. These settings can be accessed via Project Settings and scattered across Agent Settings, Agent Behavior, and Local Permissions sections:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/647/0*etLCwUk4MhCI4QcT.png" /></figure><p>They’re very similar to global settings but arranged slightly differently in the UI and persisted in ~/.gemini/config/projects/&lt;project_id&gt;.json where&lt;project_id&gt; is the UUID of the project.</p><h4>Conversation permissions</h4><p>Conversation permissions are specific to a single conversation and they are triggered when an agent needs to perform an action that is not allowed by default:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/773/0*hs5ZXnNwAtJbFfwH.png" /></figure><p>You can allow the action once or only in the current conversation. You also have the option to always allow it at the project level or globally. In both cases, the relevant configuration files are updated.</p><h3>Permissions</h3><p>Now, let’s look into different permissions in detail. They’re presented slightly differently depending on whether you’re looking at global or project level settings.</p><h4>Security preset</h4><p>Security Preset dropdown is shown under Agent Settings both in global and project level settings. It is not a permission per say but rather it&#39;s a pre-set of 3 permissions: Outside of folders file access policy, Terminal Command Auto Execution and Enable Sandbox Mode with the following pre-set configurations:</p><ul><li>Default - manual review of terminal commands and files outside project folders.</li><li>Full Machine - manual review of terminal commands and ability to read/write to any file.</li><li>Turbo Mode - no review of terminal commands and ability to read/write to any file.</li><li>Custom - custom settings.</li></ul><blockquote><em>💡 Tip</em></blockquote><blockquote><em>Always choose </em><em>Custom in order to see and control the actual 3 underlying permissions.</em></blockquote><p>Let’s break down the individual permissions in more detail.</p><h4>Outside of folders file access policy</h4><p>Configures how the agent tries to access files outside of its working folders:</p><ul><li>Always Ask (Default)</li><li>Allow</li><li>Deny</li></ul><h4>Terminal command auto execution</h4><p>Controls whether terminal commands require your approval before running:</p><ul><li>Require Review (Default)</li><li>Proceed in Sandbox</li><li>Always Proceed</li></ul><h4>Enable sandbox mode</h4><p>Restricts agent tools to a secure, isolated local sandbox:</p><ul><li>False (Default)</li><li>True</li></ul><h4>Artifact review policy</h4><p>Under Agent Behavior, there&#39;s Artifact Review Policy that determines whether the agent asks your review of artifacts (e.g. implementation plan) before proceeding:</p><ul><li>Always Ask (Default)</li><li>Always Proceed</li></ul><h4>Browser Javascript execution policy</h4><p>Only in global settings, under Browser, you&#39;ll find Browser Javascript Execution Policy that controls whether the agent can run custom JavaScript to automate complex browser actions:</p><ul><li>Disable (Default)</li><li>Request Review</li><li>Always Proceed</li></ul><h4>Agent Permissions</h4><p>In addition to the settings above, there’s a number of fine-grained <a href="https://antigravity.google/docs/permissions/#agent-permissions">Agent Permissions</a> on various actions such as file or network access, MCP tools, terminal commands, etc. with Deny, Ask, Allow options and a flexible pattern to specify what&#39;s allowed (e.g. read_file(/path) or read_file(dir), or read_file(*)).</p><p>These permissions show up in slightly different places in global and project settings but they’re for the following:</p><ul><li><strong>File Access Rules</strong>: Allowed/denied file paths for read_file and write_file actions.</li><li><strong>Network Access Rules</strong>: Allowed/denied URLs for read_url action.</li><li><strong>Browser Actuation Rules</strong>: Allowed/denied URLs for execute_url action.</li><li><strong>Terminal Commands</strong>: Allowed/denied terminal commands for command action.</li><li><strong>Commands Outside Sandbox</strong>: Allowed/denied commands for unsandboxed action.</li><li><strong>MCP Tools</strong>: Allowed/denied MCP tools for mcp action.</li></ul><h3>Configuration files</h3><p>It’s useful to know where these settings are persisted, in order to back them up or just see what’s being used without having to rely on the UI.</p><p>If you set these permissions globally, they end up in ~/.gemini/config/config.json under userSettings as follows:</p><pre>{<br>  ...<br>  &quot;userSettings&quot;: {<br>    &quot;artifactReviewMode&quot;: &quot;ARTIFACT_REVIEW_MODE_ALWAYS&quot;,<br>    &quot;autoExecutionPolicy&quot;: &quot;CASCADE_COMMANDS_AUTO_EXECUTION_OFF&quot;,<br>    &quot;browserJsExecutionPolicy&quot;: &quot;BROWSER_JS_EXECUTION_POLICY_DISABLED&quot;,<br>    &quot;enableTerminalSandbox&quot;: false,<br>    &quot;nonWorkspaceFileAccessPolicy&quot;: &quot;AGENT_SETTING_POLICY_ASK&quot;,<br>    &quot;globalPermissionGrants&quot;: {<br>      &quot;allow&quot;: [<br>        &quot;read_file(/Users/atamel/dev/local/testing)&quot;,<br>        &quot;read_url(https://www.bbc.co.uk/)&quot;,<br>        &quot;command(ls)&quot;,<br>        &quot;unsandboxed(ls)&quot;,<br>        &quot;mcp(cloudrun)&quot;<br>      ],<br>      &quot;ask&quot;: [<br>        &quot;execute_url(https://www.bbc.co.uk/)&quot;,<br>        &quot;command(ps)&quot;<br>      ],<br>      &quot;deny&quot;: [<br>        &quot;write_file(/Users/atamel)&quot;,<br>        &quot;command(rm)&quot;<br>      ]<br>    }<br>  }<br>}</pre><p>If you set them per project, they end up in ~/.gemini/config/projects/&lt;project_id&gt;.json under settings as:</p><pre>{<br>  ...<br>  &quot;permissionGrants&quot;: {<br>    &quot;permissionGrants&quot;: {<br>      &quot;allow&quot;: [<br>        &quot;read_file(/Users/atamel/dev/local/testing)&quot;,<br>        &quot;read_url(https://www.bbc.co.uk/)&quot;,<br>        &quot;command(rm)&quot;,<br>        &quot;unsandboxed(ls)&quot;,<br>        &quot;mcp(cloudrun)&quot;<br>      ],<br>      &quot;deny&quot;: [<br>        &quot;write_file(/Users/atamel)&quot;,<br>        &quot;command(ls)&quot;<br>      ],<br>      &quot;ask&quot;: [<br>        &quot;command(ps)&quot;<br>      ]<br>    }<br>  },<br>  &quot;settings&quot;: {<br>    &quot;artifactReviewMode&quot;: &quot;ARTIFACT_REVIEW_MODE_ALWAYS&quot;,<br>    &quot;autoExecutionPolicy&quot;: &quot;CASCADE_COMMANDS_AUTO_EXECUTION_OFF&quot;,<br>    &quot;fileAccessPolicy&quot;: &quot;AGENT_SETTING_POLICY_ASK&quot;,<br>    &quot;sandboxMode&quot;: false<br>  }<br>}</pre><blockquote><em>ℹ️ Note</em></blockquote><blockquote><em>Global and project level settings have slightly different configuration names and organization, as you can see from the json snippets above.</em></blockquote><h3>What about AGY CLI and AGY IDE?</h3><p>Everything I talked about above applies to AGY but you might be wondering: <strong>What about AGY CLI and AGY IDE and IDE extensions?</strong></p><p>In AGY CLI, there’s a /permissions command that allows you to set some permissions and some of them end up in~/.gemini/antigravity-cli/settings.json and some end up in the global~/.gemini/config/config.json file. However, it&#39;s not quite intuitive and I couldn&#39;t figure out exactly which permissions ends up where and why.</p><p>In AGY IDE standalone, it seems like some of the permissions are saved in totally different place ~/Library/Application Support/Antigravity IDE/User/globalStorage/state.vscdb but I didn&#39;t dig into it further.</p><p>In a future post, I might try to explore permissions in AGY CLI and IDE in more detail.</p><h3>Summary</h3><p>This is a lot of information but hopefully the following table can be a good reference for all the permissions you can set in AGY:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/737/1*-fVKrgttxDtdF1Bpc56PvA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/732/1*HqgWzs2k8XFniVZx3svO6A.png" /></figure><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/09-14_agent_permissions_agy/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=da9d326e63f4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/agent-permissions-in-antigravity-da9d326e63f4">Agent Permissions in Antigravity</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Summary : Where does Antigravity look for configurations?]]></title>
            <link>https://medium.com/google-cloud/summary-where-does-antigravity-look-for-configurations-309f88165e40?source=rss-6444b44155fa------2</link>
            <guid isPermaLink="false">https://medium.com/p/309f88165e40</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[generative-ai]]></category>
            <category><![CDATA[generative-ai-tools]]></category>
            <category><![CDATA[google-cloud-platform]]></category>
            <dc:creator><![CDATA[Mete Atamel]]></dc:creator>
            <pubDate>Fri, 21 Aug 2026 12:12:53 GMT</pubDate>
            <atom:updated>2026-08-21T13:38:39.630Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*FDnV1-iQPKa0XM3C.png" /></figure><p>💡 This post is the concluding summary of a series on Antigravity configuration:</p><ul><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-agent-skills-a703518d68c5">Where does Antigravity look for Skills?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-mcp-servers-ed3727b4686c">Where does Antigravity look for MCP Servers?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-rules-and-workflows-1fcdf070bac8">Where does Antigravity look for Rules and Workflows?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-hooks-c9e9f57f167f">Where does Antigravity look for Hooks?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-sidecars-20e7002b9246">Where does Antigravity look for Sidecars?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-agents-ab0dd955929d">Where does Antigravity look for Agents?</a></li><li><a href="https://medium.com/google-cloud/where-does-antigravity-look-for-plugins-0bc7e9d99028">Where does Antigravity look for Plugins?</a></li></ul><p>Over the past few weeks, I explored the ways to configure and extend Google Antigravity (AGY). What started as a simple exploration into agent skills quickly grew into a journey uncovering MCP servers, rules, workflows, lifecycle hooks, background sidecars, custom agents, and plugins.</p><p>Because Antigravity comes in three flavours (AGY, AGY CLI, and AGY IDE), and spans multiple configuration scopes (workspace, global, and plugin bundles), keeping track of what goes where can be challenging.</p><p>In this post, I’ll bring everything together into a single, comprehensive reference guide and cheat sheet.</p><h3>Ways to Extend Antigravity</h3><p>Here is a quick summary of what each customization mechanism is designed for:</p><ol><li><strong>Skills</strong>: Specialized folders containing instructions, scripts, and reference documents that give agents on-demand capabilities for complex workflows.</li><li><strong>MCP Servers</strong>: External tool integrations using the Model Context Protocol standard to connect to databases, APIs, and cloud services.</li><li><strong>Rules and Workflows</strong>: Rules are always-on or triggered behavioral guidelines and coding standards (system-level instructions). Workflows are user-triggered prompt templates executed on demand.</li><li><strong>Hooks</strong>: Synchronous lifecycle event listeners that run scripts to enforce policies, add context, or log telemetry.</li><li><strong>Sidecars</strong>: Autonomous background processes managed by AGY for long-running services, asynchronous listeners, or recurring schedules.</li><li><strong>Custom Agents</strong>: Tailored agent personas with restricted tool sets, specialized system prompts, and dedicated models for specific subtasks.</li><li><strong>Plugins</strong>: Distribution bundles packaging skills, MCP servers, rules, hooks, and custom agents together into a single distributable unit.</li></ol><h3>Directory Hierarchy</h3><p>Below is the map of directories and files recognized across Antigravity:</p><pre>~/.gemini/                         # GLOBAL<br>├── GEMINI.md                      # Rules<br>├── config/<br>│   ├── agents/                    # Agents<br>│   ├── config.json                # Config to enable sidecars<br>│   ├── global_workflows/          # Workflows<br>│   ├── hooks.json                 # Hooks<br>│   ├── mcp_config.json            # MCP servers<br>│   ├── plugins/                   # Plugins<br>│   │   ├── &lt;plugin-name&gt;/<br>│   │   │   ├── agents/            # Agents (plugin)<br>│   │   │   ├── hooks.json         # Hooks (plugin)<br>│   │   │   ├── mcp_config.json    # MCP servers (plugin)<br>│   │   │   ├── rules/             # Rules (plugin)<br>│   │   │   └── skills/            # Skills (plugin)<br>│   ├── sidecars/                  # Sidecars<br>│   └── skills/                    # Skills<br>|<br>├── antigravity/                   # AGY<br>│   ├── builtin/skills/            # Skills (builtin)<br>│   ├── mcp/                       # Cached MCP servers<br>│   └── sidecar_data/              # Sidecar runtime data<br>|<br>├── antigravity-cli/               # AGY CLI<br>│   ├── builtin/skills/            # Skills (builtin)<br>│   └── mcp/                       # Cached MCP servers<br>|<br>└── antigravity-ide/               # AGY IDE<br>    └── mcp/                       # Cached MCP servers<br><br>&lt;workspace-root&gt;/                  # WORKSPACE<br>└── .agents/<br>    ├── agents/                    # Agents<br>    ├── hooks.json                 # Hooks<br>    ├── mcp_config.json            # MCP servers<br>    ├── plugins/                   # Plugins<br>    │   ├── &lt;plugin-name&gt;/<br>    │   │   ├── agents/            # Agents (plugin)<br>    │   │   ├── hooks.json         # Hooks (plugin)<br>    │   │   ├── mcp_config.json    # MCP servers (plugin)<br>    │   │   ├── rules/             # Rules (plugin)<br>    │   │   └── skills/            # Skills (plugin)<br>    ├── rules/                     # Rules<br>    ├── skills/                    # Skills<br>    └── workflows/                 # Workflows</pre><h3>Wrapping Up</h3><p>Antigravity provides a comprehensive and flexible customization options.</p><p>Check out the individual posts linked above for deeper walk-throughs.</p><p><em>Originally published at </em><a href="https://atamel.dev/posts/2026/08-21_where_agy_configuration_summary/"><em>https://atamel.dev</em></a><em>.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=309f88165e40" width="1" height="1" alt=""><hr><p><a href="https://medium.com/google-cloud/summary-where-does-antigravity-look-for-configurations-309f88165e40">Summary : Where does Antigravity look for configurations?</a> was originally published in <a href="https://medium.com/google-cloud">Google Cloud - Community</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>