<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>CloudThinker Engineering</title><description>The engineering team at CloudThinker writes about how we build software with AI agents.</description><link>https://engineering.cloudthinker.io/</link><item><title>Agentic system design, entry 2: Eager Tool Calling: How We Cut Agent Latency by 50% on Long Tool Chains</title><link>https://engineering.cloudthinker.io/journeys/agentic-system-design/02/</link><guid isPermaLink="true">https://engineering.cloudthinker.io/journeys/agentic-system-design/02/</guid><pubDate>Wed, 15 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;1-introduction--the-latency-nobody-was-measuring&quot;&gt;1. Introduction — The Latency Nobody Was Measuring&lt;/h2&gt;
&lt;p&gt;An eight-tool agent task in our staging environment clocked in at twenty-four seconds. The model was fast. The individual tools were fast. The wall clock was slow. Where did the time go?&lt;/p&gt;
&lt;p&gt;We dropped the trace into a flame graph and the answer was embarrassing. For the first four seconds the model streamed tokens while every tool sat idle. Then the model finished, fired all eight tools in parallel, and spent another twenty seconds waiting for them. Two phases, stacked end to end, with no overlap between them.&lt;/p&gt;
&lt;p&gt;We rewrote that codepath. The same request now finishes in roughly five seconds — the four-second stream and the tool execution happen in the same four seconds instead of consecutively. Across a representative sample of production traces, median end-to-end task completion is &lt;strong&gt;50% faster&lt;/strong&gt;, and longer tool chains see proportionally larger wins.&lt;/p&gt;
&lt;p&gt;The technique is called &lt;strong&gt;eager tool calling&lt;/strong&gt; (internally we call it &lt;em&gt;tool-call pipelining&lt;/em&gt;). Think of it as CPU instruction pipelining for agents: just as a modern CPU doesn’t wait for one instruction to retire before decoding the next, an eager runtime doesn’t wait for the model to finish before starting the tools it has already described. The idea is small. The engineering was not. Here is what it is, how it works, the production bugs that taught us what not to do, and the numbers we measured.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-the-problem-where-agent-latency-actually-hides&quot;&gt;2. The Problem: Where Agent Latency Actually Hides&lt;/h2&gt;
&lt;p&gt;The common intuition is that parallel tool calling already solved this. It didn’t. Parallel tool calling is necessary but not sufficient.&lt;/p&gt;
&lt;h3 id=&quot;the-classic-agent-loop&quot;&gt;The classic agent loop&lt;/h3&gt;
&lt;p&gt;A single agent turn looks like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Model reasons over the context and begins streaming a response.&lt;/li&gt;
&lt;li&gt;The response contains one or more &lt;code&gt;tool_use&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;Runtime waits for &lt;code&gt;message_stop&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Runtime executes the tools.&lt;/li&gt;
&lt;li&gt;Runtime collects the &lt;code&gt;tool_result&lt;/code&gt; blocks and starts the next turn.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Total wall clock for one turn = &lt;strong&gt;stream time + tool time&lt;/strong&gt;, added serially. The tool layer is a passive consumer that refuses to start work until the producer has completely finished.&lt;/p&gt;
&lt;h3 id=&quot;the-parallel-tool-calling-half-fix&quot;&gt;The parallel tool calling half-fix&lt;/h3&gt;
&lt;p&gt;Modern APIs — Anthropic, OpenAI, Bedrock — let the model emit multiple &lt;code&gt;tool_use&lt;/code&gt; blocks in a single assistant message, and mature runtimes run those blocks concurrently.&lt;/p&gt;
&lt;p&gt;That change moves the tool phase from &lt;em&gt;sum&lt;/em&gt; of tool durations to &lt;em&gt;max&lt;/em&gt; of tool durations. Genuinely valuable. On an eight-tool turn with tools averaging 2.5 seconds, parallel tool calling takes ~2.5 seconds for the tool phase instead of 20. Big win.&lt;/p&gt;
&lt;p&gt;But the stream phase still happens first. The tools still wait for &lt;code&gt;message_stop&lt;/code&gt;. If the model takes four seconds to emit its response, those four seconds are pure dead time on the tool side.&lt;/p&gt;
&lt;h3 id=&quot;what-we-wanted&quot;&gt;What we wanted&lt;/h3&gt;
&lt;p&gt;Tools that start running the instant the model finishes emitting them, &lt;em&gt;while the model continues streaming the rest of the message.&lt;/em&gt; Not tools-parallel-with-tools. &lt;strong&gt;Tools-parallel-with-generation.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Parallel tool calling overlaps tools with each other. Eager tool calling overlaps tools with the model itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;3-three-eras-of-tool-calling&quot;&gt;3. Three Eras of Tool Calling&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;When tools start&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sequential&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;After each prior tool finishes&lt;/td&gt;
&lt;td&gt;Σ(stream + all tools)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tools with tools&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;message_stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;stream + max(tool)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Eager (pipelined)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tools with tools &lt;strong&gt;and&lt;/strong&gt; tools with generation&lt;/td&gt;
&lt;td&gt;The moment each tool block finishes streaming&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;max(stream, max(tool))&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The move from sequential to parallel collapses one dimension of latency. The move from parallel to eager collapses the other. Time goes from sum to max in both directions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/eager-vs-parallel-timeline.svg&quot; alt=&quot;Wall clock comparison of parallel and eager tool calling with the same 4.0s stream and three 2.0s tools: parallel starts all tools at message_stop and finishes at 6.0s; eager starts tools A, B and C at 0.5s, 1.5s and 2.5s during the stream and finishes at 4.5s, 1.5s saved, 1.33× faster&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;4-how-eager-tool-calling-works&quot;&gt;4. How Eager Tool Calling Works&lt;/h2&gt;
&lt;h3 id=&quot;the-key-insight--the-seal&quot;&gt;The key insight — the seal&lt;/h3&gt;
&lt;p&gt;Each &lt;code&gt;tool_use&lt;/code&gt; block the model emits has a stable &lt;code&gt;tool_call_id&lt;/code&gt;. During streaming, the runtime sees chunks arrive one at a time. Most chunks carry a &lt;code&gt;tool_call_id&lt;/code&gt;; partial argument chunks carry only an &lt;code&gt;index&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here is the observation that makes eager tool calling possible: &lt;strong&gt;the moment a chunk arrives with a &lt;code&gt;tool_call_id&lt;/code&gt; different from the previous one, the previous tool call is definitionally complete.&lt;/strong&gt; The model has moved on. Its arguments are fully accumulated. It is safe to execute &lt;em&gt;right now&lt;/em&gt;, even though the assistant message is still streaming.&lt;/p&gt;
&lt;p&gt;We call that transition the &lt;strong&gt;seal event&lt;/strong&gt;. A tool is &lt;em&gt;sealed&lt;/em&gt; the instant the model starts streaming a different tool — or when the message itself ends.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works-in-practice&quot;&gt;How it works in practice&lt;/h3&gt;
&lt;p&gt;Each chunk that arrives with a new tool ID is proof the previous tool is fully assembled — we fire it immediately, while the model keeps streaming the rest of the message.&lt;/p&gt;
&lt;h3 id=&quot;visualizing-the-overlap&quot;&gt;Visualizing the overlap&lt;/h3&gt;
&lt;p&gt;In a correct eager execution, the tool lanes are &lt;em&gt;not&lt;/em&gt; end-to-end. They &lt;em&gt;overlap&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;stream : [==================================]
tool A :   [=========]            ← fires when B&apos;s id arrives
tool B :       [=========]        ← fires when C&apos;s id arrives; runs concurrent with A
tool C :           [=========]    ← fires at message_stop; runs concurrent with A + B&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Drop a vertical line at the 50% mark and it cuts through all three bars. That is the signature of eager tool calling. If your “eager” diagram shows tools end-to-end, you have implemented sequential tool calling under a different name.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/seal-mechanism.svg&quot; alt=&quot;Seal mechanism: stream chunks id=A, id=A, then id=B arrive; the seal detector sees prev_id != new_id, marks tool A complete and fires on_tool_call_sealed(A); the executor pool starts tool A while chunk B keeps streaming, with tools B and C queued&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;5-inside-cloudthinkers-stream-handler&quot;&gt;5. Inside CloudThinker’s Stream Handler&lt;/h2&gt;
&lt;p&gt;The dispatch logic is straightforward to describe, even if the implementation required care.&lt;/p&gt;
&lt;p&gt;We track the most-recent tool call ID seen in the stream. The moment a new ID arrives, the previous tool is complete — we dispatch it immediately to a background worker pool without waiting for the rest of the message. That’s the entire seal trigger: one state comparison per chunk.&lt;/p&gt;
&lt;p&gt;Partial argument chunks don’t carry an ID — they only carry a positional index. So we maintain a small index-to-ID map that lets us route each partial chunk back to the right accumulation buffer, keeping the arguments intact even when multiple tools are streaming interleaved.&lt;/p&gt;
&lt;p&gt;The dispatch is fire-and-forget by design. If a background worker raises an exception, it’s caught and surfaced as an error &lt;code&gt;tool_result&lt;/code&gt; on the next turn. The stream reader never sees it, and the other tools keep running.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/stream-handler-architecture.svg&quot; alt=&quot;Stream handler architecture in five stages: provider stream of SSE chunks, StreamHandler with SealDetector, on_tool_call_sealed callback, ExecutorPool of asyncio tool tasks, and a ToolMessage buffer for the next turn; the stream-to-callback boundary is fire-and-forget and the callback-to-executor boundary is a cancellation scope&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;6-production-lessons--what-we-learned-the-hard-way&quot;&gt;6. Production Lessons — What We Learned the Hard Way&lt;/h2&gt;
&lt;p&gt;The first surprise was &lt;strong&gt;tool retraction&lt;/strong&gt;. The model occasionally emits a tool call mid-stream, reconsiders, and replaces it. Rare, but real. A tool that has already fired eagerly can’t be un-fired. We added a per-tool &lt;code&gt;idempotent&lt;/code&gt; flag: non-idempotent operations — payments, destructive commands, outbound emails — fall back to the classic path and only fire after &lt;code&gt;message_stop&lt;/code&gt;. The eager fast path is reserved for reads and safe operations.&lt;/p&gt;
&lt;p&gt;The second was &lt;strong&gt;cancellation&lt;/strong&gt;. When the user interrupts or the model emits a stop sequence mid-stream, every in-flight eager tool needs to cancel cleanly. Leaking a tool execution into a dead conversation is both a correctness bug and a resource leak. We tied each dispatch to a cancellation scope whose lifetime matches the stream reader — stream dies, scope cancels, tools abort.&lt;/p&gt;
&lt;p&gt;Then a &lt;strong&gt;flaky S3 list operation crashed an entire agent turn for a minute&lt;/strong&gt; before we realized we had no exception boundary around individual tool dispatches. One failing tool was bringing down the stream reader. Now every sealed dispatch runs in its own exception boundary; failure produces an error &lt;code&gt;tool_result&lt;/code&gt; and nothing else propagates.&lt;/p&gt;
&lt;p&gt;Finally, eager execution &lt;strong&gt;hides behavior inside the stream&lt;/strong&gt; in a way classic execution doesn’t. “Why is tool X slow?” becomes unanswerable without knowing when it was sealed versus when it finished. Every seal event now emits an observability span with &lt;code&gt;seal_latency_ms&lt;/code&gt;, &lt;code&gt;tool_call_id&lt;/code&gt;, and &lt;code&gt;conversation_id&lt;/code&gt;. When something misbehaves, the timeline tells you exactly which stage was slow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;7-the-numbers&quot;&gt;7. The Numbers&lt;/h2&gt;
&lt;p&gt;We benchmarked 16 workloads representative of real agent traffic, comparing sequential baseline, parallel tool calling, and eager tool calling. P50 wall clock from request-received to final assistant message. The numbers below come from the open-source benchmark in &lt;a href=&quot;https://github.com/cloudthinker-ai/eager-tools/blob/main/bench/results.md&quot;&gt;eager-tools&lt;/a&gt; — synthetic, deterministic, reproducible by anyone via &lt;code&gt;make bench&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Workload&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Sequential p50&lt;/th&gt;
&lt;th&gt;Parallel p50&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Eager p50&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Speedup vs parallel&lt;/th&gt;
&lt;th&gt;Speedup vs sequential&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Analytics query&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;4.9s&lt;/td&gt;
&lt;td&gt;3.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.9s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.21×&lt;/td&gt;
&lt;td&gt;1.69×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Search &amp;amp; retrieval&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5.7s&lt;/td&gt;
&lt;td&gt;4.2s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.20×&lt;/td&gt;
&lt;td&gt;1.63×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Customer support&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;8.6s&lt;/td&gt;
&lt;td&gt;5.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.22×&lt;/td&gt;
&lt;td&gt;1.91×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Deploy preflight&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;29.9s&lt;/td&gt;
&lt;td&gt;17.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.20×&lt;/td&gt;
&lt;td&gt;2.11×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Incident triage&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;17.6s&lt;/td&gt;
&lt;td&gt;9.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.46×&lt;/td&gt;
&lt;td&gt;2.71×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Security sweep&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;37.8s&lt;/td&gt;
&lt;td&gt;14.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.37×&lt;/td&gt;
&lt;td&gt;3.71×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Research synthesis&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;22.5s&lt;/td&gt;
&lt;td&gt;10.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.25×&lt;/td&gt;
&lt;td&gt;2.81×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Lead enrichment&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;13.4s&lt;/td&gt;
&lt;td&gt;7.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.27×&lt;/td&gt;
&lt;td&gt;2.44×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Content moderation&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;19.5s&lt;/td&gt;
&lt;td&gt;13.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.30×&lt;/td&gt;
&lt;td&gt;1.95×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;DB migration&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;20.9s&lt;/td&gt;
&lt;td&gt;11.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.9s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.24×&lt;/td&gt;
&lt;td&gt;2.35×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Release notes&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;19.6s&lt;/td&gt;
&lt;td&gt;9.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.7s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.42×&lt;/td&gt;
&lt;td&gt;2.93×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Legal review&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;28.4s&lt;/td&gt;
&lt;td&gt;11.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.4s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.22×&lt;/td&gt;
&lt;td&gt;3.02×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Sales outreach&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;23.6s&lt;/td&gt;
&lt;td&gt;11.0s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.6s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.28×&lt;/td&gt;
&lt;td&gt;2.75×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;Ad campaign sweep&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;30.4s&lt;/td&gt;
&lt;td&gt;11.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.8s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.31×&lt;/td&gt;
&lt;td&gt;3.46×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;Invoice processing&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;13.5s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.44×&lt;/td&gt;
&lt;td&gt;2.60×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Onboarding flow&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;13.1s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.50×&lt;/td&gt;
&lt;td&gt;2.62×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The synthetic harness removes network jitter so the comparison isolates the dispatch strategy — speedup vs parallel ranges from 1.20× to 1.50×, median ~1.28×. &lt;strong&gt;In production, where tail latency and provider variance compound, end-to-end task completion is ~50% faster on the median trace, and longer tool chains pull further ahead.&lt;/strong&gt; Treat the OSS bench as the lower-bound version anyone can reproduce on a laptop; production wins are larger.&lt;/p&gt;
&lt;h3 id=&quot;cost-impact&quot;&gt;Cost impact&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Output tokens per task:&lt;/strong&gt; effectively unchanged (same reasoning, same answer)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Net cost reduction:&lt;/strong&gt; ~35% on tool-heavy workloads — fewer retries from timeouts, more conversations completing inside the cache TTL window, and faster end-to-end task completion freeing up agent slots.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-the-speedup-comes-from&quot;&gt;Where the speedup comes from&lt;/h3&gt;
&lt;p&gt;Two stacked effects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Generation / execution overlap.&lt;/strong&gt; The biggest chunk. Every millisecond of stream time that used to sit idle on the tool side now has a tool running in parallel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fewer turns.&lt;/strong&gt; A task that used to take three model turns often completes in one. Each saved turn saves prefill, network round-trip, and reasoning overhead.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/bench-results-chart.svg&quot; alt=&quot;Bar chart of p50 wall clock for sequential, parallel and eager: 3-tool analytics query 4.9s, 3.5s, 2.9s (1.21× vs parallel); 9-tool incident triage 17.6s, 9.5s, 6.5s (1.46×); 15-tool ad campaign sweep 30.4s, 11.5s, 8.8s (1.31×)&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;8-when-not-to-use-it&quot;&gt;8. When NOT to Use It&lt;/h2&gt;
&lt;p&gt;Being honest about the limits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fast tools (sub-50ms).&lt;/strong&gt; If your tools finish in milliseconds, there is nothing to hide behind the stream. Seal/dispatch overhead exceeds the latency saved. Don’t bother.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sequentially dependent tools.&lt;/strong&gt; If tool B needs tool A’s result to even be formulated, the model won’t emit B until A returns. No pipeline opportunity; eager and classic are identical.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-streaming backends.&lt;/strong&gt; You can’t seal per block without incremental parsing. If your provider or gateway buffers the full response before forwarding, eager tool calling is impossible until that changes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-idempotent tools.&lt;/strong&gt; Already covered above. Destructive operations, payments, outbound messages — these stay on the classic path.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;9-a-mental-model-that-helps&quot;&gt;9. A Mental Model That Helps&lt;/h2&gt;
&lt;p&gt;Eager tool calling is &lt;strong&gt;CPU instruction pipelining&lt;/strong&gt; for agents. The agent runtime is the CPU, the model stream is the instruction fetch, the tools are the execution units, and the seal event is the register-ready signal that lets execution begin before the rest of the batch is decoded. Once that analogy clicks, the rest of the design falls out of it naturally.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;10-try-it-yourself&quot;&gt;10. Try It Yourself&lt;/h2&gt;
&lt;h3 id=&quot;build-it-yourself&quot;&gt;Build it yourself&lt;/h3&gt;
&lt;p&gt;Building this requires a streaming SSE parser, a seal detector, an async executor pool tied to the stream’s lifetime, per-tool idempotency flags, and observability spans on every seal event. The open-source &lt;a href=&quot;https://github.com/cloudthinker-ai/eager-tools&quot;&gt;eager-tools&lt;/a&gt; library ships all of this.&lt;/p&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;Eager tool calling is not a novel idea in the abstract. CPUs have been pipelining instructions since the 1980s. What’s novel is that the agent ecosystem spent two years treating streaming as a UX-only feature — something you do to make tokens appear live in a chat window — rather than as an opportunity to parallelize execution against generation.&lt;/p&gt;
&lt;p&gt;For any production agent running multiple tools per turn, this pattern is not optional. It is the difference between an impressive demo and a system fast enough to replace a human operator.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;First published on &lt;a href=&quot;https://cloudthinker.io/blogs/eager-tool-calling-50-percent-faster-agents&quot;&gt;cloudthinker.io&lt;/a&gt; on April 15, 2026.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Henry Bui</author></item><item><title>Generative UI, entry 1: Generative UI in Production: Lessons from a Pydantic-to-DSL Migration</title><link>https://engineering.cloudthinker.io/journeys/generative-ui/01/</link><guid isPermaLink="true">https://engineering.cloudthinker.io/journeys/generative-ui/01/</guid><pubDate>Mon, 06 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Our agent dashboards used to take 30-40 seconds and cost roughly $0.50 per report. Today they stream in under 10 seconds for around $0.08 — and they’re built on the same design system as the rest of the product. This is the story of a migration we expected to take a week and ended up rethinking from first principles.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A year ago, asking a CloudThinker agent for a cost dashboard meant watching a spinner. The agent would call a &lt;code&gt;dashboard()&lt;/code&gt; tool, the model would fill in a strict Pydantic schema field by field, and 30-something seconds later — once the entire JSON tree validated — a fully-formed dashboard would snap into place. It looked great. It used the platform’s design system. It was also slow enough that most users would tab away and forget what they’d asked for.&lt;/p&gt;
&lt;p&gt;Today, the dashboard streams in. KPI cards render first with current-month totals. A line chart fills in as the agent emits it. A service breakdown appears as a bar chart. Everything still matches CloudThinker’s design system. Total time: under 10 seconds. Total cost: about $0.08. The chart is interactive because it &lt;strong&gt;is&lt;/strong&gt; the product, not a snapshot of it — click through to the expensive region and you drill in immediately.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/generative-ui/artifact-streaming-storyboard.svg&quot; alt=&quot;A cost dashboard streaming in over four frames: KPI cards first, then the full card grid, then charts rendering while the DSL is still arriving, then a recommendations table and gauges&quot;&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Was&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wall-clock latency&lt;/td&gt;
&lt;td&gt;30–40s&lt;/td&gt;
&lt;td&gt;~10s&lt;/td&gt;
&lt;td&gt;~75% faster end-to-end&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per report&lt;/td&gt;
&lt;td&gt;~$0.50&lt;/td&gt;
&lt;td&gt;~$0.08&lt;/td&gt;
&lt;td&gt;~84% cheaper per artifact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Render mode&lt;/td&gt;
&lt;td&gt;Atomic PNG&lt;/td&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;Progressive, line by line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design consistency&lt;/td&gt;
&lt;td&gt;Drifted&lt;/td&gt;
&lt;td&gt;Locked&lt;/td&gt;
&lt;td&gt;Enforced by construction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This post is about how we got from the first version to the second — and, more importantly, why the change wasn’t a micro-optimization. It was a category shift the whole industry made at the same time, and it took us two architectures to land on the right one.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;act-0--the-two-detours-we-skipped&quot;&gt;Act 0 — The Two Detours We Skipped&lt;/h2&gt;
&lt;p&gt;Before talking about what we built, it’s worth being honest about what we &lt;em&gt;didn’t&lt;/em&gt; build. When we first sat down with the “agent generates dashboards” problem, the two obvious paths were already on the whiteboard. We crossed both of them out fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Matplotlib PNGs.&lt;/strong&gt; The most popular pattern in 2023. Let the LLM write Python, run it in a sandbox, capture the PNG. Research from the &lt;a href=&quot;https://arxiv.org/html/2402.11453v3&quot;&gt;MatPlotAgent paper (arXiv 2402.11453)&lt;/a&gt; is worth quoting: it &lt;em&gt;“struggles to handle complex charts that require fine-grained numerical accuracy and precise visual-textual alignment of chart labels with plot components.”&lt;/em&gt; A separate study, &lt;a href=&quot;https://arxiv.org/html/2403.06158v1&quot;&gt;&lt;em&gt;Are LLMs Ready for Visualization?&lt;/em&gt; (arXiv 2403.06158)&lt;/a&gt;, documented that &lt;em&gt;“numerous attempts to modify the default configuration of scatterplots and bubble charts were unsuccessful.”&lt;/em&gt; Five runs of the same prompt give five different results. There’s a 4-8 second cold-start tax on &lt;code&gt;import matplotlib&lt;/code&gt;. PNGs aren’t interactive, can’t be themed without a fight, and are effectively invisible to screen readers — the &lt;a href=&quot;https://dl.acm.org/doi/full/10.1145/3715336.3735780&quot;&gt;ACM DIS 2025 GenUI Study&lt;/a&gt; called accessibility &lt;em&gt;“a frequently-mentioned constraint”&lt;/em&gt; for generated output. And every chart means executing LLM-generated Python in a sandbox. The best way to not have a sandbox escape is to not execute the code in the first place.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/generative-ui/matplotlib-pipeline.svg&quot; alt=&quot;The matplotlib pipeline: user prompt, LLM writes matplotlib code, sandbox exec, matplotlib savefig, chat embeds PNG, with five failure modes alongside (design drift, stochastic output, 4-8s cold start, no interactivity, zero accessibility)&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The agent writes Python, a sandbox executes matplotlib, and a PNG embeds in chat. Five specific failure modes compound: design drift, stochastic output, cold-start overhead, zero interactivity, and accessibility gaps.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raw HTML / JSX freestyle.&lt;/strong&gt; &lt;em&gt;“Modern frontier models are great at React and Tailwind. Why don’t we just let the agent write the UI directly?”&lt;/em&gt; Everyone has this idea. Hardik Pandya’s essay &lt;a href=&quot;https://hvpandya.com/llm-design-systems&quot;&gt;&lt;em&gt;Expose your design system to LLMs&lt;/em&gt;&lt;/a&gt; captures why it’s a trap: LLMs &lt;em&gt;“fabricate token names, drift on values within a session, lose all context between sessions, and never notice when the upstream library ships breaking changes.”&lt;/em&gt; Token economics collapse — a single dashboard becomes 2,000-4,000 tokens of markup before a single data point. Half-streamed HTML is broken HTML, so progressive rendering is impossible. Every dashboard becomes a one-off; the design system is a suggestion, not a contract.&lt;/p&gt;
&lt;p&gt;And then there’s the security bomb.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OWASP LLM05 — Improper Output Handling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our injection vector wasn’t hypothetical. Cloud resource names are user-controlled. A customer can tag an EC2 instance with an &lt;code&gt;onerror&lt;/code&gt; attribute payload, and the next time our agent generates a dashboard referencing that resource, the tag flows through the LLM’s output straight into &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The agent isn’t even “attacked” — it’s a pass-through for attacker-controlled strings in the most structurally dangerous way possible. The OWASP guidance is blunt: &lt;em&gt;treat LLM output as untrusted user input&lt;/em&gt;. The only durable fix is to make it structurally impossible for the schema to represent a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag in the first place.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://genai.owasp.org/llmrisk/llm05-supply-chain-vulnerabilities/&quot;&gt;OWASP LLM05: Improper Output Handling&lt;/a&gt; is officially catalogued in the OWASP Top 10 for LLM Applications. &lt;a href=&quot;https://auth0.com/blog/owasp-llm05-improper-output-handling/&quot;&gt;Auth0’s writeup&lt;/a&gt; puts it bluntly: &lt;em&gt;“Improper Output Handling is the New XSS.”&lt;/em&gt; The &lt;a href=&quot;https://portswigger.net/web-security/llm-attacks&quot;&gt;PortSwigger Web Security Academy LLM lab&lt;/a&gt; walks through the exact attack: a model is coaxed — directly or via indirect prompt injection — into emitting an &lt;code&gt;onerror&lt;/code&gt; payload that ends up in &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. The OWASP guidance is blunt: &lt;em&gt;treat LLM output as untrusted user input&lt;/em&gt;. The only way to be sure a model can’t emit a dangerous tag is to make the schema incapable of representing one.&lt;/p&gt;
&lt;p&gt;We skipped both detours. What we picked instead seemed clever at the time — and it &lt;em&gt;was&lt;/em&gt;, until it wasn’t.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;act-1--the-pydantic-era&quot;&gt;Act 1 — The Pydantic Era&lt;/h2&gt;
&lt;p&gt;A year ago, we shipped a &lt;code&gt;dashboard()&lt;/code&gt; tool. The agent calls it with strictly typed Pydantic widgets — KPI cards, charts, tables — placed on a 12-column grid. The frontend renders them with the same React components the rest of the product uses. No Python. No HTML. No PNG. Just a typed object emitted through OpenAI’s structured-output mode (or Bedrock’s equivalent), with the model’s decoding constrained against the schema.&lt;/p&gt;
&lt;p&gt;The shape, lightly trimmed:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;KpiCardWidget&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;KPICard&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    layout&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LayoutConfig          &lt;span class=&quot;token comment&quot;&gt;# x, y, w, h on a 12-col grid&lt;/span&gt;
    &lt;span class=&quot;token builtin&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Literal&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;kpi_card&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    section_id&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ChartWidget&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ChartStructuredOutput&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    layout&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LayoutConfig
    &lt;span class=&quot;token builtin&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Literal&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;chart&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# categories, datasets, axes, target_line, threshold_zones...&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TableWidget&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;TableStructuredOutput&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    layout&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; LayoutConfig
    &lt;span class=&quot;token builtin&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Literal&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;table&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# columns, rows, summary_row...&lt;/span&gt;

DashboardWidget &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Annotated&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    KpiCardWidget &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; TableWidget &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; ChartWidget&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Discriminator&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Dashboard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BaseModel&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    title&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;
    widgets&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;DashboardWidget&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    sections&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;DashboardSection&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;
    time_context&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; TimeContext &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent emits a &lt;code&gt;Dashboard&lt;/code&gt; object via tool call. Discriminated unions guarantee every widget has a known shape. Validators enforce layout rules — &lt;em&gt;KPI cards must be in row 0&lt;/em&gt;, &lt;em&gt;chart width must be at least 3 columns&lt;/em&gt;, &lt;em&gt;widgets sharing a row must belong to the same section&lt;/em&gt; — and feed structured error messages back to the model when something’s off. The renderer is a single &lt;code&gt;&amp;lt;DashboardTab&amp;gt;&lt;/code&gt; component that walks the tree and maps each typed widget to a real platform component (Highcharts inside &lt;code&gt;Card&lt;/code&gt; containers, native tables, themed KPI cards).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This was a smart starting point.&lt;/strong&gt; We knew matplotlib and HTML freestyle were dead ends, and constrained decoding gave us a real escape hatch. Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Design consistency for free.&lt;/strong&gt; Every chart is a Highcharts &lt;code&gt;&amp;lt;LineChart&amp;gt;&lt;/code&gt; from the design system. There is no “agent invented a new shade of primary blue” because the agent is selecting components, not styling them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero XSS surface.&lt;/strong&gt; The agent physically cannot emit &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. The schema has no field that lets it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No code execution.&lt;/strong&gt; No sandbox, no &lt;code&gt;import matplotlib&lt;/code&gt;, no Python interpreter on the hot path.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation is structural.&lt;/strong&gt; Bad widget = clean error message back to the agent (&lt;em&gt;“KPI card width must be 3, 4, or 6”&lt;/em&gt;). The agent fixes itself most of the time without a retry loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditable.&lt;/strong&gt; Dashboards persist as typed JSON in PostgreSQL. Every report a customer ever saw is reconstructable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For about ten months, this was our answer. It shipped, customers used it, and it didn’t break. But by month six the cracks were obvious to anyone watching the latency dashboard.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token economics.&lt;/strong&gt; A four-widget dashboard was 1,500-3,000 tokens of JSON before a single data point. Discriminated unions are verbose by nature: every widget repeats &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;layout: {x, y, w, h}&lt;/code&gt;, &lt;code&gt;section_id&lt;/code&gt;, and the full nested chart config. Multiply by a long-running investigation that emits multiple reports, multiply by users, multiply by frontier-model output pricing. The line item showed up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No real streaming.&lt;/strong&gt; This is the one that hurt the most. &lt;strong&gt;Constrained decoding makes the LLM wait until the entire JSON tree parses cleanly before it commits a single token to the output.&lt;/strong&gt; Structured-output mode (Pydantic schemas, JSON Schema, function-call parameters — they’re all the same shape) fundamentally trades streamability for validity. The user sees a spinner for 30+ seconds, then the whole dashboard appears at once. Wall-clock latency was OK by 2025 standards. Perceived latency was the worst part of the product.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schema rigidity.&lt;/strong&gt; Want a new chart type? Add a discriminated variant. Update the validator. Regenerate frontend types via &lt;code&gt;pnpm gen:local&lt;/code&gt;. Ship a backend release. Ship a frontend release. A new component took a sprint. A new layout pattern took half a sprint. Every time the design team wanted to try something, the answer was &lt;em&gt;“yes, in the next release.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;One artifact per conversation.&lt;/strong&gt; The tool was modeled around &lt;code&gt;create_dashboard / add_widgets / update_widgets&lt;/code&gt;. A single artifact lifecycle. Agents that wanted to emit &lt;em&gt;multiple&lt;/em&gt; small reports in a single thread had to overwrite the previous one or skip.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pixel arithmetic instead of intent.&lt;/strong&gt; A &lt;code&gt;Stack(KPI, KPI, Chart)&lt;/code&gt; layout had to be expressed as four absolute coordinates (&lt;code&gt;{x: 0, y: 0, w: 4}&lt;/code&gt;, &lt;code&gt;{x: 4, y: 0, w: 4}&lt;/code&gt;, &lt;code&gt;{x: 8, y: 0, w: 4}&lt;/code&gt;, &lt;code&gt;{x: 0, y: 1, w: 12, h: 3}&lt;/code&gt;). The agent burned tokens reasoning about grid math instead of describing what it wanted to show. We had a half-page of layout examples in the tool description that the agent had to re-read on every call.&lt;/p&gt;
&lt;p&gt;End-of-Act-1 scoreboard: &lt;strong&gt;30-40 seconds&lt;/strong&gt; wall-clock, &lt;strong&gt;roughly $0.50&lt;/strong&gt; per report. Functionally correct, design-system-consistent, secure — and unit economics that didn’t pencil out for what we wanted to build next.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;act-2--the-dsl-wed-been-dreaming-about&quot;&gt;Act 2 — The DSL We’d Been Dreaming About&lt;/h2&gt;
&lt;p&gt;We’d been talking about &lt;em&gt;“what if the agent emitted a tiny language instead of a typed JSON tree”&lt;/em&gt; for months. We even sketched a grammar on a whiteboard once. Then we kept shipping other things, and the dashboard kept being good enough.&lt;/p&gt;
&lt;p&gt;What changed in early 2026: the rest of the industry caught up to where we’d been doodling. &lt;strong&gt;Generative UI&lt;/strong&gt; became its own category, with multiple serious implementations converging on the same basic idea — and several of them shipped to production while we were heads-down on other features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Generative UI landscape, 2024–2026&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Project&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;01&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.openui.com/docs/openui-lang&quot;&gt;OpenUI Lang&lt;/a&gt; (our choice)&lt;/td&gt;
&lt;td&gt;Compact, line-oriented DSL. Published benchmarks show 67.1% fewer tokens than Vercel JSON-Render.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://vercel.com/blog/ai-sdk-3-generative-ui&quot;&gt;Vercel AI SDK&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;streamUI with React Server Components. As of late 2025, RSC development is officially paused; Vercel recommends AI SDK UI for production.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;03&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://docs.langchain.com/langsmith/generative-ui-react&quot;&gt;LangChain / LangGraph&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Tool calls mapped to pre-registered React components with type-safe streaming via assistant-ui and useStream().&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;04&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.thesys.dev/&quot;&gt;Thesys C1&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;OpenAI-compatible endpoint that outputs UI components instead of text. Drop-in for chat interfaces that want to become dashboards.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;05&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.copilotkit.ai/ag-ui&quot;&gt;CopilotKit AG-UI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Event/state protocol between agents and UI. 120K+ weekly installs; adopted by Google, AWS, LangChain, Microsoft, Mastra, PydanticAI.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;06&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://research.google/blog/generative-ui-a-rich-custom-visual-interactive-user-experience-for-any-prompt/&quot;&gt;Google A2UI&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Declarative spec from Google Research for agent-generated UIs. Part of the same wave as AG-UI and OpenUI.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://vercel.com/blog/ai-sdk-3-generative-ui&quot;&gt;Vercel AI SDK 3.0&lt;/a&gt;&lt;/strong&gt; introduced &lt;code&gt;streamUI&lt;/code&gt; in early 2024 — stream React Server Components from LLM tool calls. Worth weighing: per &lt;a href=&quot;https://github.com/vercel/ai/discussions/3251&quot;&gt;Vercel’s own discussion thread&lt;/a&gt;, &lt;em&gt;“Development of AI SDK RSC is paused. We recommend using AI SDK UI.”&lt;/em&gt; Even Vercel couldn’t make “stream React components from the server” work well enough to recommend for production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.langchain.com/langsmith/generative-ui-react&quot;&gt;LangChain / LangGraph Generative UI&lt;/a&gt;&lt;/strong&gt; maps tool calls to pre-registered React components with type-safe streaming. The &lt;code&gt;assistant-ui&lt;/code&gt; library provides an out-of-the-box renderer with progressive updates during long-running graph execution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.thesys.dev/&quot;&gt;Thesys C1&lt;/a&gt;&lt;/strong&gt; is an OpenAI-compatible endpoint that outputs UI components instead of text. Ask it &lt;em&gt;“show me monthly revenue trends”&lt;/em&gt; and get a rendered line chart instead of a paragraph. See also their &lt;a href=&quot;https://www.thesys.dev/blogs/how-to-build-generative-ui-applications&quot;&gt;guide to building Generative UI applications&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.copilotkit.ai/ag-ui&quot;&gt;CopilotKit AG-UI&lt;/a&gt;&lt;/strong&gt; is a bidirectional agent↔UI event protocol — &lt;a href=&quot;https://www.copilotkit.ai/blog/the-developer-s-guide-to-generative-ui-in-2026&quot;&gt;adopted by Google, LangChain, AWS, Microsoft, Mastra, and PydanticAI&lt;/a&gt;, with over 120K weekly installs as of early 2026.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.copilotkit.ai/blog/build-with-googles-new-a2ui-spec-agent-user-interfaces-with-a2ui-ag-ui&quot;&gt;Google A2UI&lt;/a&gt;&lt;/strong&gt; is Google’s declarative spec for agent-generated UIs. &lt;a href=&quot;https://research.google/blog/generative-ui-a-rich-custom-visual-interactive-user-experience-for-any-prompt/&quot;&gt;Google Research has also published&lt;/a&gt; their own generative UI work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.openui.com/docs/openui-lang/overview&quot;&gt;OpenUI Lang&lt;/a&gt;&lt;/strong&gt; takes the most opinionated line: a compact, line-oriented DSL designed for LLMs to stream UI structure directly. Their &lt;a href=&quot;https://www.openui.com/docs/openui-lang/benchmarks&quot;&gt;published benchmarks&lt;/a&gt; report 60–67% fewer tokens than JSON-based alternatives and up to 3× faster render on deeply nested UIs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CopilotKit’s framing is useful. They describe &lt;a href=&quot;https://www.copilotkit.ai/blog/the-developer-s-guide-to-generative-ui-in-2026&quot;&gt;three patterns of Generative UI&lt;/a&gt;: &lt;em&gt;Controlled&lt;/em&gt; (high structure, low freedom), &lt;em&gt;Declarative&lt;/em&gt; (shared control), and &lt;em&gt;Open-ended&lt;/em&gt; (agents produce arbitrary UI). Our Pydantic Era was firmly Controlled — we just picked a payload format that didn’t stream. The fix was to keep the Controlled posture and swap the payload.&lt;/p&gt;
&lt;h3 id=&quot;the-pattern-underneath-all-of-them&quot;&gt;The pattern underneath all of them&lt;/h3&gt;
&lt;p&gt;Peel back the branding and these projects agree on more than they disagree on. Every production-leaning approach has landed on the same four properties:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A closed component vocabulary.&lt;/strong&gt; The agent can only emit things the renderer already knows how to draw. No arbitrary markup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A typed, structured payload.&lt;/strong&gt; JSX, JSON, YAML, or a custom DSL — but always something a parser can validate before it reaches the DOM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming-friendly grammar.&lt;/strong&gt; Partial output has to be meaningful. Half an HTML tag is broken; half a tool call or half a DSL block should still render &lt;em&gt;something&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Renderer-owned styling.&lt;/strong&gt; The agent describes intent (&lt;code&gt;status: &quot;success&quot;&lt;/code&gt;); the renderer decides which shade of green.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The shape of the payload varies — tool calls (Vercel/LangChain), JSON schemas (Thesys), event protocols (AG-UI/A2UI), or line-oriented DSLs (OpenUI). But the contract is identical: &lt;strong&gt;agents describe, renderers render&lt;/strong&gt;. The whole category moved this direction because the alternative — letting the agent write the view layer — was strictly worse on every axis that mattered.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/generative-ui/token-efficiency.svg&quot; alt=&quot;Token efficiency, OpenUI Lang vs JSON-based UI formats: relative token count for an equivalent UI, Vercel JSON-Render 100%, Thesys C1 JSON 95%, YAML 85%, OpenUI Lang 33%&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;OpenUI Lang’s published benchmark shows 67.1% fewer tokens than Vercel JSON-Render, 65.4% fewer than Thesys C1 JSON, and 61.4% fewer than YAML across seven real-world UI scenarios. For deeply nested dashboards the gap grows to 3.0× — the main reason we picked it for CloudThinker’s Artifact system. Source: &lt;a href=&quot;https://www.openui.com/docs/openui-lang/benchmarks&quot;&gt;openui.com/docs/openui-lang/benchmarks&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The token economics drive a lot of the architectural choices. JSON is verbose. YAML is slightly less so. Line-oriented DSLs are the most compact of the common options, and that gap compounds: shorter payloads mean smaller system prompts, fewer output tokens, faster streaming, and more predictable costs. A dashboard that a JSON-based renderer takes ~14 seconds to finish can stream in ~5 seconds with a DSL grammar designed for progressive parsing.&lt;/p&gt;
&lt;h3 id=&quot;why-we-didnt-roll-our-own&quot;&gt;Why we didn’t roll our own&lt;/h3&gt;
&lt;p&gt;Designing a DSL is the easy part. Building the parser, the validator, the streaming render path, the error messages, the few-shot examples, the documentation — that’s months. Every serious GenUI project has converged on the same four properties independently. Picking off the shelf was strictly better than re-deriving the same wheel.&lt;/p&gt;
&lt;p&gt;We evaluated against three things that mattered to us, in order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Token economics.&lt;/strong&gt; We already knew the Pydantic JSON was too verbose. Whatever we picked had to be measurably smaller.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming-first grammar.&lt;/strong&gt; Partial parse must render &lt;em&gt;something&lt;/em&gt; — that was the lesson from the Pydantic era’s spinner problem. If the format requires a closing brace before it’s valid, it disqualifies itself.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maps cleanly to a closed component vocabulary we already own.&lt;/strong&gt; We didn’t want to throw away the design-system components from the Pydantic era. Whatever we picked had to be a payload format we could point at our existing renderers.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OpenUI Lang scored highest on all three. The 60-67% token reduction over JSON-based payloads was the visible win, but the hidden win was the line-oriented grammar — each &lt;code&gt;kpi = KPICard(...)&lt;/code&gt; is an independently parseable statement. Half a dashboard renders as half a dashboard, not a spinner. We adapted the OpenUI vocabulary to our FinOps-flavored component palette and built our &lt;strong&gt;Artifact system&lt;/strong&gt; on top.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;act-3--how-cloudthinkers-artifact-system-works-now&quot;&gt;Act 3 — How CloudThinker’s Artifact System Works Now&lt;/h2&gt;
&lt;p&gt;The Artifact system is the third iteration, not the answer that fell from the sky. The agent calls an &lt;code&gt;artifact()&lt;/code&gt; tool. The tool accepts structured parameters — type, title, description, and a &lt;code&gt;body&lt;/code&gt; containing a compact, line-oriented DSL block (inspired by &lt;a href=&quot;https://www.openui.com/docs/openui-lang/overview&quot;&gt;OpenUI Lang&lt;/a&gt;, adapted to a FinOps-flavored component palette). The body is validated against a component whitelist, stored in PostgreSQL, and streamed to the frontend.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/generative-ui/artifact-architecture.svg&quot; alt=&quot;Artifact system, agent to OpenUI Lang to many surfaces: LLM agent makes an artifact(type, title, body) tool call, the OpenUI parser validates it against a whitelist, PostgreSQL stores the plain-text body, and it fans out to a React renderer (web dashboard) and an HTML renderer (WeasyPrint PDF)&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The agent emits typed DSL inside an &lt;code&gt;artifact()&lt;/code&gt; tool call. The parser enforces a component whitelist. The body is stored as plain text in PostgreSQL and rendered to multiple surfaces — React for web, WeasyPrint for PDF — from the same source.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A concrete example. This is the body an agent emits for a Q4 cost dashboard — note how each line is a complete component assignment, parseable and renderable on arrival:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;kpi_spend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; KPICard&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Total Spend&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;$12,450&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-8%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
kpi_save  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; KPICard&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Savings&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;     &lt;span class=&quot;token string&quot;&gt;&quot;$2,400&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;token string&quot;&gt;&quot;+12%&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

trend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LineChart&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Jan&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Feb&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mar&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Series&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Cost&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;13500&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12450&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11800&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

breakdown &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; BarChart&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;EC2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;RDS&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;S3&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Series&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Cost&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5200&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1800&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

kpi_row &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Stack&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;kpi_spend&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; kpi_save&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;row&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Stack&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  kpi_row&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Card&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CardHeader&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Cost Trend&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Monthly spend trajectory&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; trend&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  Card&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CardHeader&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;By Service&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Current month breakdown&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; breakdown&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s the entire payload. Compare it to the equivalent Pydantic Era JSON — same dashboard, but with &lt;code&gt;type&lt;/code&gt; discriminators, &lt;code&gt;layout: {x, y, w, h}&lt;/code&gt; per widget, full chart-axis configs, and &lt;code&gt;section_id&lt;/code&gt; references. Same information, several times the tokens.&lt;/p&gt;
&lt;p&gt;The component palette is deliberately small. Agents can emit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layout&lt;/strong&gt;: &lt;code&gt;Stack&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;CardHeader&lt;/code&gt;, &lt;code&gt;Divider&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data&lt;/strong&gt;: &lt;code&gt;KPICard&lt;/code&gt;, &lt;code&gt;Table&lt;/code&gt;, &lt;code&gt;TextContent&lt;/code&gt;, &lt;code&gt;Callout&lt;/code&gt;, &lt;code&gt;Tag&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Charts&lt;/strong&gt;: &lt;code&gt;BarChart&lt;/code&gt;, &lt;code&gt;LineChart&lt;/code&gt;, &lt;code&gt;AreaChart&lt;/code&gt;, &lt;code&gt;PieChart&lt;/code&gt;, &lt;code&gt;RadarChart&lt;/code&gt;, &lt;code&gt;ScatterChart&lt;/code&gt;, &lt;code&gt;SingleStackedBarChart&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Advanced&lt;/strong&gt;: &lt;code&gt;GaugeChart&lt;/code&gt;, &lt;code&gt;Treemap&lt;/code&gt;, &lt;code&gt;Heatmap&lt;/code&gt;, &lt;code&gt;FeatureMatrix&lt;/code&gt;, &lt;code&gt;ProsCons&lt;/code&gt;, &lt;code&gt;ScoreTable&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s the entire vocabulary. An agent physically cannot emit a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag, a rogue &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, a fabricated color token, or a chart library we haven’t vetted. The OWASP LLM05 attack surface is gone by construction, just like in the Pydantic era — but now with progressive streaming and ~70% fewer tokens.&lt;/p&gt;
&lt;p&gt;The frontend renders the DSL with a registered component library (Highcharts for charts, native components for everything else). The same DSL also feeds a WeasyPrint-backed HTML renderer that produces PDFs — same body, same output, different surface. Write once, render everywhere.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Matplotlib (detour)&lt;/th&gt;
&lt;th&gt;Raw HTML (detour)&lt;/th&gt;
&lt;th&gt;Pydantic Era&lt;/th&gt;
&lt;th&gt;Artifact Era&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;Binary PNG&lt;/td&gt;
&lt;td&gt;HTML markup&lt;/td&gt;
&lt;td&gt;Typed JSON tree&lt;/td&gt;
&lt;td&gt;Line-oriented DSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Streaming&lt;/td&gt;
&lt;td&gt;No — atomic&lt;/td&gt;
&lt;td&gt;Half-HTML is broken&lt;/td&gt;
&lt;td&gt;Wait for valid JSON&lt;/td&gt;
&lt;td&gt;Yes — line by line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactivity&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Arbitrary (risky)&lt;/td&gt;
&lt;td&gt;Componentized&lt;/td&gt;
&lt;td&gt;Componentized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design consistency&lt;/td&gt;
&lt;td&gt;matplotlib defaults&lt;/td&gt;
&lt;td&gt;Drifts every session&lt;/td&gt;
&lt;td&gt;Design-system locked&lt;/td&gt;
&lt;td&gt;Design-system locked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security (XSS)&lt;/td&gt;
&lt;td&gt;Sandbox exec risk&lt;/td&gt;
&lt;td&gt;OWASP LLM05 exposure&lt;/td&gt;
&lt;td&gt;Unrepresentable&lt;/td&gt;
&lt;td&gt;Unrepresentable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens&lt;/td&gt;
&lt;td&gt;~Python code&lt;/td&gt;
&lt;td&gt;2k–4k / report&lt;/td&gt;
&lt;td&gt;1.5k–3k / report&lt;/td&gt;
&lt;td&gt;60–70% less than JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema evolution&lt;/td&gt;
&lt;td&gt;Code-only&lt;/td&gt;
&lt;td&gt;Free-for-all&lt;/td&gt;
&lt;td&gt;Sprint per new widget&lt;/td&gt;
&lt;td&gt;Add to component palette&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF export&lt;/td&gt;
&lt;td&gt;Native (already image)&lt;/td&gt;
&lt;td&gt;Fragile (headless browser)&lt;/td&gt;
&lt;td&gt;Custom renderer&lt;/td&gt;
&lt;td&gt;Same DSL, static renderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debuggability&lt;/td&gt;
&lt;td&gt;Opaque binary&lt;/td&gt;
&lt;td&gt;Opaque DOM&lt;/td&gt;
&lt;td&gt;Verbose JSON diff&lt;/td&gt;
&lt;td&gt;Plain-text, diffable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;five-lessons-from-production&quot;&gt;Five Lessons from Production&lt;/h2&gt;
&lt;h3 id=&quot;lesson-1-constraints-are-the-product-not-the-limitation&quot;&gt;Lesson 1: Constraints Are the Product, Not the Limitation&lt;/h3&gt;
&lt;p&gt;The Pydantic era taught us this lesson once. The Artifact era taught it to us a second time. Both architectures work for the same underlying reason: the agent doesn’t get to invent the visual layer. It picks from a vocabulary we control. The two architectures differ on &lt;em&gt;format&lt;/em&gt; — typed JSON versus line-oriented DSL — but they agree on the contract: the agent describes intent, the renderer owns the pixels.&lt;/p&gt;
&lt;p&gt;When we first proposed a whitelist of components, the obvious pushback was: &lt;em&gt;“Won’t that limit what the agent can do?”&lt;/em&gt; Yes. That’s the entire point. Every dashboard the agent produces inherits CloudThinker’s design system for free — because it’s literally rendered by the same components the rest of the product uses. There is no drift because there is no vocabulary to drift within.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://dl.acm.org/doi/full/10.1145/3715336.3735780&quot;&gt;ACM DIS 2025 GenUI Study&lt;/a&gt; found that 37 UX professionals, after a week with state-of-the-art GenUI tools, agreed &lt;em&gt;“GenUI quality was not production-ready and would require further tweaking and polishing”&lt;/em&gt; — with accessibility and domain-specificity as the main complaints. Both are artifacts of Open-ended generation. In a Controlled pattern with a whitelist, accessibility is a property of the component (we audit &lt;code&gt;LineChart&lt;/code&gt; once), and domain-specificity is the entire reason the vocabulary exists.&lt;/p&gt;
&lt;p&gt;If the agent wants a new kind of component, we add it once. Not per prompt. Not per session. Once.&lt;/p&gt;
&lt;h3 id=&quot;lesson-2-streaming-changes-ux-more-than-you-think&quot;&gt;Lesson 2: Streaming Changes UX More Than You Think&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Streaming insight&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can’t stream a PNG — it’s atomic. You can’t safely stream raw HTML — half-rendered markup is broken markup. You &lt;em&gt;can&lt;/em&gt; stream a line-oriented DSL, because every line is a complete, parseable component. A dashboard that takes JSON &lt;strong&gt;14.2 seconds&lt;/strong&gt; to finish streams in OpenUI Lang in &lt;strong&gt;4.9 seconds&lt;/strong&gt; — not because the model is faster, but because the grammar was designed for progressive rendering from day one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the lesson the Pydantic era couldn’t teach us. Constrained-decoding JSON is &lt;em&gt;correct&lt;/em&gt;, but it’s a wall — the model commits nothing until the tree validates, and the user stares at a spinner. A line-oriented grammar streams differently from anything else. As each line of &lt;code&gt;kpi = KPICard(...)&lt;/code&gt; arrives, the renderer can immediately show that card. The user sees a skeleton, then KPIs, then charts, then tables — progressively, in the order the agent generated them. Perceived latency drops more than actual latency does, because the user has something to look at from the second second onward.&lt;/p&gt;
&lt;p&gt;You cannot do this with a PNG (it’s atomic). You cannot safely do this with raw HTML (half-streamed HTML is broken HTML). You cannot do it with JSON-based payloads either (the tree isn’t valid until the closing brace, and constrained decoding makes the model wait for it). You &lt;strong&gt;can&lt;/strong&gt; do it with a grammar where each line is independently parseable. Streaming isn’t a feature you bolt on to the output format — it’s a constraint the output format has to satisfy from day one, and it’s why every serious GenUI project has ended up rethinking the payload shape.&lt;/p&gt;
&lt;h3 id=&quot;lesson-3-token-economics-compound-at-agent-scale&quot;&gt;Lesson 3: Token Economics Compound at Agent Scale&lt;/h3&gt;
&lt;p&gt;Per-request savings don’t matter at low volume. They matter enormously at agent volume. When the same agent generates many artifacts in a single long-running investigation — dashboards, tables, sub-reports, one-offs — the per-artifact token delta turns into a per-user cost delta, and the per-user delta turns into a unit-economics story.&lt;/p&gt;
&lt;p&gt;The Pydantic era taught us that a strict schema is necessary but not sufficient. JSON costs you tokens you didn’t budget for: every widget repeats its discriminator, every layout repeats its grid math, every chart repeats its axis config. Published benchmarks across the GenUI category show DSL-based payloads running 60-67% fewer tokens than JSON-based alternatives for the body alone. But the bigger win isn’t the payload tokens — it’s the &lt;em&gt;instruction&lt;/em&gt; tokens. Teaching the agent a small vocabulary is much shorter than teaching it “how to fill in our dashboard schema correctly.” The system prompt shrinks. The few-shot examples shrink. The agent has less to think about, so it thinks faster.&lt;/p&gt;
&lt;p&gt;The hook numbers at the top of this post — &lt;strong&gt;30-40 seconds down to around 10 seconds, ~$0.50 per report down to ~$0.08&lt;/strong&gt; — are the compounded effect of all of this. Smaller output, shorter system prompt, faster rendering, no waiting on constrained decoding to finish a JSON tree.&lt;/p&gt;
&lt;h3 id=&quot;lesson-4-one-dsl-many-surfaces&quot;&gt;Lesson 4: One DSL, Many Surfaces&lt;/h3&gt;
&lt;p&gt;Same artifact body, multiple renderers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web&lt;/strong&gt;: React components (Highcharts inside &lt;code&gt;Card&lt;/code&gt; containers)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PDF&lt;/strong&gt;: WeasyPrint-friendly HTML, for digest reports and audit exports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Future Slack / email&lt;/strong&gt;: a straightforward mapping to Slack Block Kit or MJML templates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You cannot do this with matplotlib (one output: PNG). You cannot do this with hand-written React (tightly coupled to a browser runtime). The Pydantic era was halfway there — the JSON tree was renderer-agnostic in principle, but in practice the React renderer and the PDF renderer drifted because the schema was too rich to fully cover twice. The DSL is tight enough that both renderers stay honest.&lt;/p&gt;
&lt;p&gt;This also turns out to be how we handle &lt;em&gt;editing&lt;/em&gt;. A user says “make the chart bigger.” The agent doesn’t re-generate from scratch — it produces a diff against the existing DSL, and the renderer re-paints. The DSL is a first-class document, not a one-shot output.&lt;/p&gt;
&lt;h3 id=&quot;lesson-5-debuggability-is-a-superpower&quot;&gt;Lesson 5: Debuggability Is a Superpower&lt;/h3&gt;
&lt;p&gt;Artifacts are stored as plain-text DSL in PostgreSQL. This has three properties we didn’t plan for but can’t live without:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Diffs.&lt;/strong&gt; “Why did the agent pick a bar chart here instead of a line chart last week?” is a &lt;code&gt;SELECT&lt;/code&gt; query followed by a text diff. The Pydantic era’s JSON dumps were technically diffable too, but the noise-to-signal ratio was bad — every diff was 80% positional reshuffling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replay.&lt;/strong&gt; Loading a past artifact and re-rendering it costs nothing. No code execution, no sandbox, no model call. Useful for bug reports and regression tests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditability.&lt;/strong&gt; Every report is a human-readable, version-controllable document. For customers in regulated industries, this matters a lot.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Contrast: if your agent output is a PNG, good luck explaining to a compliance auditor &lt;em&gt;why&lt;/em&gt; the chart looks the way it does.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-wed-do-differently&quot;&gt;What We’d Do Differently&lt;/h2&gt;
&lt;p&gt;The honest retrospective:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The first migration is the one you should expect.&lt;/strong&gt; The most expensive lesson from the Pydantic era was assuming we’d get the format right on the first try. We knew matplotlib and HTML were dead ends and we picked the next-most-obvious thing — strict typed JSON via constrained decoding. It worked. It was also a stepping stone, not a destination. If you’re building a Generative UI system today, budget for at least one rewrite of the payload format. Preferably plan for it from the start.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ship the DSL spec first, then the components.&lt;/strong&gt; We built components opportunistically the second time around — &lt;code&gt;BarChart&lt;/code&gt; when we needed a bar chart, &lt;code&gt;GaugeChart&lt;/code&gt; when we needed a gauge. In hindsight we should have started by writing down the &lt;em&gt;entire&lt;/em&gt; intended vocabulary as a locked spec, validated it against a handful of target reports, and &lt;em&gt;then&lt;/em&gt; built components. The irony: we &lt;em&gt;did&lt;/em&gt; exactly this with Pydantic schemas the first time. Doing it twice would have been smarter than doing it once perfectly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt engineering is most of the work.&lt;/strong&gt; The DSL is trivial — a few dozen components, a few hundred lines of parser. What took the longest was teaching the agent &lt;em&gt;when&lt;/em&gt; to use a &lt;code&gt;KPICard&lt;/code&gt; versus a &lt;code&gt;Callout&lt;/code&gt;, when a &lt;code&gt;GaugeChart&lt;/code&gt; is better than a single-value metric, when to group cards into &lt;code&gt;Stack(&quot;row&quot;)&lt;/code&gt; versus a vertical layout. The &lt;a href=&quot;https://www.thesys.dev/blogs/how-to-build-generative-ui-applications&quot;&gt;Thesys guidance on building Generative UI applications&lt;/a&gt; turned out to be more useful than we expected — the design patterns matter more than the framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation errors need to be LLM-friendly.&lt;/strong&gt; Our first parser returned Python tracebacks to the agent when the DSL was malformed. The agent got confused by the tracebacks and hallucinated more errors trying to fix them. Now the parser returns structured hints — &lt;em&gt;“You called &lt;code&gt;LineChart&lt;/code&gt; without a labels array. Expected: &lt;code&gt;LineChart(labels, series)&lt;/code&gt;. Did you mean: &lt;code&gt;LineChart([&apos;Jan&apos;, &apos;Feb&apos;], [Series(&apos;Cost&apos;, [100, 200])])&lt;/code&gt;?”&lt;/em&gt; Error messages are prompts. Treat them as such. (The Pydantic era got this right by accident — Pydantic’s structured error messages are already pretty good.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don’t skip the PDF renderer.&lt;/strong&gt; Building the WeasyPrint HTML renderer in parallel with the React one forced us to keep the DSL truly declarative. Any component that secretly needed client-side JavaScript to render was a leak — and having a second renderer exposed every leak immediately. If you’re building a Generative UI system and you &lt;em&gt;only&lt;/em&gt; target the browser, you’ll accrue hidden coupling you won’t notice until the first time you need to export.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next--from-snapshots-to-live-workspaces&quot;&gt;What’s Next — From Snapshots to Live Workspaces&lt;/h2&gt;
&lt;p&gt;Today’s artifacts are honest read-only documents. The agent emits the DSL, the renderer paints it, and that’s where the conversation ends. You can look at the dashboard, but you can’t &lt;em&gt;do&lt;/em&gt; anything to it — every follow-up question kicks off a fresh generation pass and a new artifact appears next to the old one. It works, but it treats the artifact like a printout rather than a workspace.&lt;/p&gt;
&lt;p&gt;The next iteration we’re building flips that. Artifacts become &lt;strong&gt;stateful surfaces the user and the agent share&lt;/strong&gt;, not snapshots the agent throws over the wall. Three concrete shifts:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bidirectional artifacts.&lt;/strong&gt; Click a region on the cost map and the agent drills in. Drag a date range on a line chart and the agent re-runs the underlying query against the new window. Hover a row in a service breakdown and the agent surfaces the related anomalies. The artifact stops being a destination and starts being an input — every interaction is an implicit prompt that the agent picks up on the next turn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Editable artifacts via DSL diffs.&lt;/strong&gt; “Make the time range 90 days” should not require a full regeneration. The agent emits a small &lt;strong&gt;DSL diff&lt;/strong&gt; against the existing artifact, the renderer re-paints in place, and the audit log records the delta. Artifacts become version-controlled documents — you can branch them, replay them, and trace exactly which agent changed which panel and why.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Embedded controls as first-class DSL primitives.&lt;/strong&gt; New components — &lt;code&gt;Filter()&lt;/code&gt;, &lt;code&gt;DateRange()&lt;/code&gt;, &lt;code&gt;Toggle()&lt;/code&gt;, &lt;code&gt;Slider()&lt;/code&gt; — that the agent can drop into an artifact and the renderer wires up to a re-run. The DSL graduates from a layout language to an interaction language, but the contract stays the same: the agent describes intent, the renderer owns the pixels &lt;em&gt;and&lt;/em&gt; the events. The PDF renderer degrades each control gracefully — a &lt;code&gt;DateRange()&lt;/code&gt; becomes a static caption in print, a button in Slack, a live picker in the browser. One body, many surfaces, all of them alive.&lt;/p&gt;
&lt;p&gt;The deeper bet: &lt;strong&gt;Generative UI is going to look obvious in 18 months, the same way streaming chat responses look obvious now.&lt;/strong&gt; The surface area of “what the agent produces” is going to keep getting smaller and more semantic, and the surface area of “what the product renders” is going to keep getting richer. The line between them is the contract that matters — and &lt;a href=&quot;https://rogerwong.me/2025/11/generative-ui-and-the-ephemeral-interface&quot;&gt;as Roger Wong put it&lt;/a&gt;, we’re heading toward interfaces that exist for a single moment, composed on demand, thrown away after.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;A quick shout-out to the teams pushing Generative UI forward: &lt;a href=&quot;https://vercel.com/blog/ai-sdk-3-generative-ui&quot;&gt;Vercel&lt;/a&gt; for shipping the first widely-used streaming UI primitive, &lt;a href=&quot;https://www.langchain.com&quot;&gt;LangChain&lt;/a&gt; for the graph-native approach, &lt;a href=&quot;https://www.thesys.dev&quot;&gt;Thesys&lt;/a&gt; for the category-defining work on C1, &lt;a href=&quot;https://www.copilotkit.ai&quot;&gt;CopilotKit&lt;/a&gt; for the AG-UI protocol, &lt;a href=&quot;https://research.google/blog/generative-ui-a-rich-custom-visual-interactive-user-experience-for-any-prompt/&quot;&gt;Google&lt;/a&gt; for A2UI, and &lt;a href=&quot;https://www.openui.com&quot;&gt;OpenUI&lt;/a&gt; for pushing DSL-based streaming further than anyone else. The ecosystem is better because of all of them — and the pattern is only going to get more prevalent from here.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Further reading: &lt;a href=&quot;https://www.openui.com/docs/openui-lang/benchmarks&quot;&gt;OpenUI Lang Benchmarks&lt;/a&gt; · &lt;a href=&quot;https://www.copilotkit.ai/blog/the-developer-s-guide-to-generative-ui-in-2026&quot;&gt;CopilotKit: The Developer’s Guide to Generative UI in 2026&lt;/a&gt; · &lt;a href=&quot;https://genai.owasp.org/llmrisk/llm05-supply-chain-vulnerabilities/&quot;&gt;OWASP LLM05: Improper Output Handling&lt;/a&gt; · &lt;a href=&quot;https://dl.acm.org/doi/full/10.1145/3715336.3735780&quot;&gt;ACM DIS 2025: The GenUI Study&lt;/a&gt; · &lt;a href=&quot;https://rogerwong.me/2025/11/generative-ui-and-the-ephemeral-interface&quot;&gt;Generative UI and the Ephemeral Interface&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;First published on &lt;a href=&quot;https://cloudthinker.io/blogs/generative-ui-in-production-lessons-shipping-agent-dashboards&quot;&gt;cloudthinker.io&lt;/a&gt; on April 6, 2026.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Henry Bui</author></item><item><title>Agentic infrastructure, entry 1: We Tried Every AI Sandbox. Then We Built Our Own.</title><link>https://engineering.cloudthinker.io/journeys/agentic-infrastructure/01/</link><guid isPermaLink="true">https://engineering.cloudthinker.io/journeys/agentic-infrastructure/01/</guid><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;Hosted sandboxes couldn’t reach our private APIs. Self-hosted ones needed dedicated servers. So we built our own.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One of our AI agents spent 40 minutes debugging a cloud networking issue. It installed diagnostic tools, traced network configurations, and correlated logs across multiple services. Three commands from the root cause — the kind of deep investigation that separates a useful agent from a chatbot.&lt;/p&gt;
&lt;p&gt;Then the session &lt;strong&gt;timed out&lt;/strong&gt;. Everything gone. The agent started over from zero, reinstalling the same tools, re-tracing the same logs, re-learning the same context. Forty minutes of compute burned twice.&lt;/p&gt;
&lt;p&gt;The problem wasn’t just cost — it was the &lt;strong&gt;architecture&lt;/strong&gt;: sandboxes that lived outside our network, couldn’t reach our internal APIs, required enterprise plans for basic features like private network access, and gave us no control over what our agents could access. When you’re running AI agents against production infrastructure, “it works but we can’t see what it’s doing” isn’t a feature — it’s a liability.&lt;/p&gt;
&lt;p&gt;So we evaluated every option on the market, found an open-source project with the right foundation, added the production features it was missing, and open-sourced the result.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks&quot;&gt;OpenSandbox on EKS&lt;/a&gt;: Self-hosted AI sandbox infrastructure with &lt;strong&gt;persistent filesystems&lt;/strong&gt;, &lt;strong&gt;tiered pause/resume&lt;/strong&gt;, &lt;strong&gt;network security controls&lt;/strong&gt;, and &lt;strong&gt;security hardening&lt;/strong&gt; — running on your own AWS account. &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks/blob/main/docs/getting-started.md&quot;&gt;Get started&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;choosing-the-right-sandbox&quot;&gt;Choosing the Right Sandbox&lt;/h2&gt;
&lt;p&gt;We evaluated the main categories of AI sandbox options. Every category broke for different reasons.&lt;/p&gt;
&lt;h3 id=&quot;hosted-sandbox-apis&quot;&gt;Hosted Sandbox APIs&lt;/h3&gt;
&lt;p&gt;Managed infrastructure, fast cold starts, SDKs in every language — we had agents running in an afternoon. Then we tried to connect an agent to our internal Grafana instance. Nothing. The sandbox lived in the provider’s network — completely isolated from our private infrastructure. Internal APIs, private databases, monitoring dashboards — all unreachable. Connecting to our network required expensive enterprise tiers ($3K+/month), and even then you don’t control the underlying infrastructure.&lt;/p&gt;
&lt;p&gt;The pricing model was also wrong for AI workloads. Per-second billing works for quick, short-lived tasks. AI agents are different — they run long sessions, install tools, explore systems, and iterate. You’re paying for compute the entire time, with no way to pause a sandbox to cheaper storage when it’s idle.&lt;/p&gt;
&lt;p&gt;Some hosted sandboxes now offer basic outbound traffic filtering, but you’re configuring security rules through an API you don’t own. When an incident happens, you can’t inspect the rules, review traffic logs, or audit what your agent actually accessed.&lt;/p&gt;
&lt;h3 id=&quot;self-hosted-vm-based-alternatives&quot;&gt;Self-Hosted VM-Based Alternatives&lt;/h3&gt;
&lt;p&gt;The alternative camp offered something different: run sandboxes on your own infrastructure. Open source, self-hosted, full control. Exactly what we wanted — in theory.&lt;/p&gt;
&lt;p&gt;In practice, these tools required dedicated physical servers or traditional VMs. They weren’t designed for Kubernetes — the orchestration platform we’d already invested years building around. That meant no automatic scaling, no cost-saving spot instances, and no integration with the infrastructure we already had.&lt;/p&gt;
&lt;p&gt;The operational model was also limiting. Most VM-based solutions only support destroy-and-recreate — there’s no concept of pausing a sandbox and resuming it later with state intact. For AI agents that work on multi-day investigations, this is a dealbreaker. You either keep the VM running (expensive) or destroy it and lose everything (wasteful).&lt;/p&gt;
&lt;h3 id=&quot;found-a-kubernetes-native-foundation&quot;&gt;Found a Kubernetes-Native Foundation&lt;/h3&gt;
&lt;p&gt;After these dead ends, we found &lt;a href=&quot;https://github.com/alibaba/OpenSandbox&quot;&gt;Alibaba’s OpenSandbox&lt;/a&gt; — an open-source project that got the fundamentals right.&lt;/p&gt;
&lt;p&gt;It was &lt;strong&gt;Kubernetes-native from the ground up&lt;/strong&gt; — designed for container orchestration, not a VM tool with a Kubernetes wrapper bolted on. It included a lifecycle management server, an execution engine inside each sandbox, and SDKs in five languages (Python, TypeScript, Java, C#, Go).&lt;/p&gt;
&lt;p&gt;But honest assessment: it was a foundation, not a production system. For running on AWS at scale, critical features were missing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No filesystem persistence&lt;/strong&gt; — sandbox state didn’t survive restarts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No tiered pause/resume&lt;/strong&gt; — no way to suspend sandboxes at different cost tiers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No network security&lt;/strong&gt; beyond basic defaults — no outbound filtering, no cloud credential protection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal base image&lt;/strong&gt; — missing the programming languages, CLIs, and tools agents actually need&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security gaps&lt;/strong&gt; — admin access, excessive permissions, weak isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The foundation was right. The production layer was ours to build.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problems we kept hitting&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No Private Connectivity.&lt;/strong&gt; Hosted sandboxes live outside your network. Agents can’t reach internal APIs, private databases, or monitoring systems without ugly workarounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expensive at Scale.&lt;/strong&gt; Per-second billing compounds fast with long-running AI sessions. No way to pause idle sandboxes to cheaper storage tiers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No Affordable Persistence.&lt;/strong&gt; Hosted persistence requires expensive enterprise tiers. Self-hosted VMs offer destroy-and-recreate only. No tiered pause/resume to optimize idle costs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited Network Visibility.&lt;/strong&gt; Hosted sandboxes offer basic egress rules, but you can’t see or audit the traffic. Self-hosted gives you full visibility into what your agents access.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;architecture-in-60-seconds&quot;&gt;Architecture in 60 Seconds&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-infrastructure/opensandbox-request-flow.svg&quot; alt=&quot;OpenSandbox request flow: SDKs in Python, TS, Java, C# and Go call the API Server over HTTP/SSE; the API Server writes a Sandbox Resource in the cluster runtime, which the Controller watches and turns into one Sandbox Pod per agent&quot;&gt;&lt;/p&gt;
&lt;p&gt;SDK calls create Sandbox resources, managed by the controller into isolated containers with persistent filesystems.&lt;/p&gt;
&lt;p&gt;The request flow is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Call &lt;code&gt;Sandbox.create()&lt;/code&gt; via any SDK (&lt;strong&gt;Python, TypeScript, Java, C#, Go&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;FastAPI lifecycle server creates a &lt;strong&gt;Sandbox resource&lt;/strong&gt; in Kubernetes&lt;/li&gt;
&lt;li&gt;The controller creates an &lt;strong&gt;isolated container with persistent storage and network rules&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A setup step installs the &lt;strong&gt;execution engine&lt;/strong&gt; into the container&lt;/li&gt;
&lt;li&gt;The startup script sets up the &lt;strong&gt;persistent filesystem&lt;/strong&gt; and &lt;strong&gt;security restrictions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;SDK communicates with the execution engine for &lt;strong&gt;code execution&lt;/strong&gt;, &lt;strong&gt;file ops&lt;/strong&gt;, and &lt;strong&gt;shell commands&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The entire lifecycle — create, pause, resume, archive, terminate — is managed through &lt;strong&gt;standard Kubernetes resources&lt;/strong&gt;. No extra orchestration layers. No external databases. Standard tools work for debugging.&lt;/p&gt;
&lt;p&gt;In an upcoming post, we’ll do a full technical deep dive into filesystem persistence, controller design, network security, and storage tiering.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-we-added&quot;&gt;What We Added&lt;/h2&gt;
&lt;p&gt;Now that you’ve seen how the pieces fit together, here’s what we built on top of the open-source foundation.&lt;/p&gt;
&lt;h3 id=&quot;1-persistent-filesystem&quot;&gt;1. Persistent Filesystem&lt;/h3&gt;
&lt;p&gt;Standard containers lose all changes when they restart. We added a persistent filesystem layer so every package install, config change, and downloaded file survives restarts. The agent installs tools once. They’re still there next week.&lt;/p&gt;
&lt;h3 id=&quot;2-tiered-pause--resume&quot;&gt;2. Tiered Pause &amp;amp; Resume&lt;/h3&gt;
&lt;p&gt;Not all sandboxes are equally active. Running sandboxes sit at full compute cost, but idle ones should cost almost nothing.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;What’s kept&lt;/th&gt;
&lt;th&gt;Resume Time&lt;/th&gt;
&lt;th&gt;Cost (20GB)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full compute + disk&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;~$5/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warm&lt;/strong&gt; (paused)&lt;/td&gt;
&lt;td&gt;Disk only&lt;/td&gt;
&lt;td&gt;~5 seconds&lt;/td&gt;
&lt;td&gt;~$1.60/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold&lt;/strong&gt; (archived)&lt;/td&gt;
&lt;td&gt;Snapshot only&lt;/td&gt;
&lt;td&gt;~30 seconds&lt;/td&gt;
&lt;td&gt;~$0.50/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Snapshots only store the data your agent actually wrote — not the full 20GB volume. A sandbox that used 10GB of its 20GB disk costs about $0.50/mo as a snapshot.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-infrastructure/sandbox-lifecycle-tiers.svg&quot; alt=&quot;Sandbox lifecycle: Running ($5/mo, ready instantly) pauses to Paused ($1.60/mo, resumes in 5s), which archives to Archived ($0.50/mo, resumes in ~30s); resume from any tier restores everything&quot;&gt;&lt;/p&gt;
&lt;p&gt;Cost per agent, 20GB EBS:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No Tiering&lt;/td&gt;
&lt;td&gt;~$5/mo&lt;/td&gt;
&lt;td&gt;Every sandbox stays at full cost, active or idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-Tier Storage&lt;/td&gt;
&lt;td&gt;~$0.50/mo&lt;/td&gt;
&lt;td&gt;Idle sandboxes archive to cold — 10x cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; Agents are idle most of the time — they only wake when an event fires (PR opened, issue created, scheduled task). Cold storage at $0.50/mo is the steady state, not the exception.&lt;/p&gt;
&lt;p&gt;An agent that’s idle for a few hours gets paused to warm. Idle for days, archived to cold. Resume from any tier restores the full filesystem — every installed tool, every config file, every work-in-progress artifact.&lt;/p&gt;
&lt;h3 id=&quot;3-network-security--filtering&quot;&gt;3. Network Security &amp;amp; Filtering&lt;/h3&gt;
&lt;p&gt;Every sandbox gets its own network filtering layer. Unlike hosted solutions where you configure rules through an API you don’t control, OpenSandbox gives you full visibility — standard network policies you can audit, customize, and integrate with your existing security tooling.&lt;/p&gt;
&lt;h3 id=&quot;4-pre-built-environment&quot;&gt;4. Pre-Built Environment&lt;/h3&gt;
&lt;p&gt;Every sandbox comes with programming languages, cloud tools, monitoring integrations, and developer utilities pre-installed. No more spending the first 5 minutes of every session installing tools.&lt;/p&gt;
&lt;h3 id=&quot;5-security-hardening&quot;&gt;5. Security Hardening&lt;/h3&gt;
&lt;p&gt;Running untrusted AI-generated code requires a locked-down environment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No admin access&lt;/strong&gt; — agents run as unprivileged users&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privilege escalation blocked&lt;/strong&gt; — no way for code to gain elevated permissions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal permissions&lt;/strong&gt; — only what’s needed during initial setup, then immediately restricted&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared directories locked down&lt;/strong&gt; — no hijacking temp files or shared paths&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What we added to OpenSandbox&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Persistent Filesystem.&lt;/strong&gt; Install packages once, they survive pause/resume. No more cold environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tiered Pause &amp;amp; Resume.&lt;/strong&gt; Pause idle sandboxes to save ~68%. Resume in ~5s warm, ~30s cold. Storage drops to ~$0.50/mo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Security &amp;amp; Filtering.&lt;/strong&gt; Each sandbox gets its own network rules. Block access to internal services, cloud credentials, and sensitive endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-Built Environment.&lt;/strong&gt; Programming languages, cloud tools, monitoring integrations, and developer utilities — all pre-installed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP Servers Included.&lt;/strong&gt; Monitoring, database, and infrastructure integrations ready to go out of the box.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security Hardening.&lt;/strong&gt; No admin access, privilege escalation blocked, minimal permissions, shared directories locked down.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;opensandbox-at-scale&quot;&gt;OpenSandbox at Scale&lt;/h2&gt;
&lt;p&gt;Here’s what OpenSandbox looks like running at scale on AWS:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-infrastructure/opensandbox-on-eks-production.svg&quot; alt=&quot;OpenSandbox on EKS production architecture: inside an AWS account and private VPC, an auto-scaled EKS cluster runs the API Server and Controller, one network-isolated pod per active agent, paused and archived agents, backed by EBS volumes, EBS snapshots and ECR&quot;&gt;&lt;/p&gt;
&lt;p&gt;Each AI agent gets its own isolated pod with persistent storage, pre-installed tools, and network filtering. Idle agents pause to save ~68%. Dormant agents archive to snapshots at ~$0.50/mo.&lt;/p&gt;
&lt;p&gt;Every AI agent at CloudThinker runs inside its own isolated OpenSandbox environment:&lt;/p&gt;
&lt;p&gt;What’s inside each sandbox (OpenSandbox Pod, per agent, persistent filesystem):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud CLIs&lt;/td&gt;
&lt;td&gt;aws, gcloud, az&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP Servers&lt;/td&gt;
&lt;td&gt;grafana, elasticsearch, sonarqube, zabbix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster &amp;amp; Git&lt;/td&gt;
&lt;td&gt;kubectl, helm, git, gh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dev Utilities&lt;/td&gt;
&lt;td&gt;ripgrep, jq, curl, python3, node, bun&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The typical lifecycle: an agent is assigned a task, creates or resumes a sandbox, works for minutes or hours, then goes idle. The system automatically pauses to warm storage. If the user comes back next week, the sandbox resumes from a snapshot in about 30 seconds — every tool still installed, every file still in place.&lt;/p&gt;
&lt;p&gt;Here’s a concrete example. One agent was debugging an intermittent connection timeout between a cloud service and a database. Day one: installed diagnostic tools, traced the issue to a networking bug. Day two: the engineer asked the agent to continue. It resumed in 4 seconds with everything intact — captured data, scripts, partial analysis. No reinstallation. It identified the fix within an hour.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-we-learned&quot;&gt;What We Learned&lt;/h2&gt;
&lt;h3 id=&quot;persistence-changes-everything&quot;&gt;Persistence changes everything&lt;/h3&gt;
&lt;p&gt;One of our agents had been working for 11 hours — it had installed diagnostic tools, cloned repositories, and was correlating logs across multiple services. At 3 AM, the spot instance it was running on got terminated.&lt;/p&gt;
&lt;p&gt;The system moved it to a new server. Every file, every tool, every piece of progress — intact. The agent didn’t even know it moved.&lt;/p&gt;
&lt;p&gt;Without persistence, that’s 11 hours of wasted compute. With it, it’s a non-event. This is what convinced us persistence isn’t optional — &lt;strong&gt;it’s the foundation&lt;/strong&gt;. Everything else we build assumes the sandbox survives failures.&lt;/p&gt;
&lt;h3 id=&quot;design-for-idle-not-for-active&quot;&gt;Design for idle, not for active&lt;/h3&gt;
&lt;p&gt;Agents are event-driven — they activate when a PR opens or an issue is created, then sit idle. Without tiered storage, you pay full price for every sandbox whether it’s working or not.&lt;/p&gt;
&lt;p&gt;With tiered storage, idle sandboxes drop from ~$5/mo to ~$0.50/mo — &lt;strong&gt;about 90% cheaper&lt;/strong&gt;. At 200 sandboxes where 80% are idle, that’s the difference between ~$1,000/mo and ~$200/mo.&lt;/p&gt;
&lt;p&gt;The insight: most infrastructure is designed for peak load. Agent infrastructure should be designed for idle.&lt;/p&gt;
&lt;h2 id=&quot;why-open-source&quot;&gt;Why Open Source&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;When AI runs code against production, you need to see the isolation layer.&lt;/strong&gt; If an agent executes &lt;code&gt;kubectl delete namespace production&lt;/code&gt;, the difference between “that’s fine, it was sandboxed” and a career-ending incident is the sandbox implementation. That implementation shouldn’t be a black box behind an API.&lt;/p&gt;
&lt;p&gt;The ecosystem also needs a &lt;strong&gt;shared standard&lt;/strong&gt;. Every AI platform is building proprietary sandbox infrastructure — duplicated engineering that doesn’t interoperate. We’d rather compete on what agents accomplish than on container plumbing.&lt;/p&gt;
&lt;p&gt;And we need help. Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Firecracker / gVisor&lt;/strong&gt; — stronger isolation using secure container runtimes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local development&lt;/strong&gt; — running sandboxes on your laptop without a full cloud cluster&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If any of these problems interest you, the &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks/issues&quot;&gt;issue tracker&lt;/a&gt; has tagged good-first-issue items for each.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Secure container runtimes&lt;/strong&gt; — gVisor, Kata Containers, and Firecracker for deeper isolation at the operating system level&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tiered cold storage&lt;/strong&gt; — automatic archival for sandboxes that haven’t been accessed in weeks, driven by usage metrics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Snapshot pre-warming&lt;/strong&gt; — predictive resume that restores sandboxes before the user asks, based on usage patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;get-involved&quot;&gt;Get Involved&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks&quot;&gt;cloudthinker-ai/opensandbox-on-eks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Getting Started&lt;/strong&gt;: &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks/blob/main/docs/getting-started.md&quot;&gt;Quick start guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Building AI agents and fighting sandbox problems? &lt;a href=&quot;https://github.com/cloudthinker-ai/opensandbox-on-eks/issues&quot;&gt;Open an issue&lt;/a&gt; — we read every one.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;coming-up-next&quot;&gt;Coming Up Next&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 2: 1,000 AI Agents, One Cluster — The Architecture That Holds&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The full technical deep dive: how OverlayFS keeps agent work alive across node failures, how tiered storage cuts costs by 80-90%, and why blocking one IP address prevents an entire class of privilege escalation attacks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;First published on &lt;a href=&quot;https://cloudthinker.io/blogs/opensandbox-open-source-announcement&quot;&gt;cloudthinker.io&lt;/a&gt; on March 21, 2026.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Khai Trinh</author></item><item><title>Agentic memory, entry 1: Agent Memory Meets Graph: Introducing MemGraph — Long-Term Memory for AI Cloud Agents</title><link>https://engineering.cloudthinker.io/journeys/agentic-memory/01/</link><guid isPermaLink="true">https://engineering.cloudthinker.io/journeys/agentic-memory/01/</guid><pubDate>Wed, 18 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Your AI agent brilliantly diagnosed a Lambda-to-RDS connection storm last Tuesday. It identified the missing connection pooling, recommended the fix, and walked your team through implementation.&lt;/p&gt;
&lt;p&gt;On Wednesday, the exact same pattern appeared in a different service. The agent started from zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the fundamental problem with AI agents today.&lt;/strong&gt; They’re stateless. Every conversation is a blank slate. The hard-won operational knowledge your team builds through months of incidents, optimizations, and architectural decisions — the agent forgets all of it the moment the session ends.&lt;/p&gt;
&lt;p&gt;We built MemGraph to fix this.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-memory-problem-no-one-talks-about&quot;&gt;The Memory Problem No One Talks About&lt;/h2&gt;
&lt;p&gt;Most AI agent platforms treat memory as an afterthought — a simple vector store that retrieves relevant documents. This approach has three fatal flaws:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No Relationships.&lt;/strong&gt; Every memory is an island. A flat document store can’t express that “before you resize an EKS node group, you must understand node affinity rules.” There’s no concept of prerequisite knowledge, conflicting approaches, or superseded procedures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No Evolution.&lt;/strong&gt; Knowledge grows stale. That gp2 resize procedure from 6 months ago sits alongside the current gp3 migration guide with equal weight. Outdated knowledge is worse than no knowledge — it leads agents to confidently give wrong answers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No Signal.&lt;/strong&gt; All memories are equal. A procedure validated 50 times in production ranks the same as one used once and never confirmed. There’s no concept of earned trust, reinforcement, or contradiction.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Traditional RAG is a filing cabinet. You can find documents, but the cabinet doesn’t know which documents are related, which are outdated, or which ones actually worked in practice. MemGraph is an experienced engineer’s brain — it connects knowledge, prioritizes what’s proven, and quietly deprecates what’s no longer relevant.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;introducing-memgraph-memory-as-a-knowledge-graph&quot;&gt;Introducing MemGraph: Memory as a Knowledge Graph&lt;/h2&gt;
&lt;p&gt;MemGraph treats every piece of operational knowledge not as a document, but as a &lt;strong&gt;node in a graph&lt;/strong&gt; — connected to other knowledge through typed, meaningful relationships.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-architecture.svg&quot; alt=&quot;A conversation goes through Extract and Evolve, which stores, combines or discards each memory, and stored memories join a memory graph that is retrieved back into the conversation.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;five-memory-types&quot;&gt;Five Memory Types&lt;/h3&gt;
&lt;p&gt;Not all knowledge is the same. MemGraph classifies memories into five distinct types, each optimized for different retrieval patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Procedural&lt;/strong&gt; — Step-by-step workflows and tool sequences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Factual&lt;/strong&gt; — Rules, constraints, and best practices&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Episodic&lt;/strong&gt; — Success stories with reasoning&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Incident&lt;/strong&gt; — Root cause analysis from real incidents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Developer Pattern&lt;/strong&gt; — Team-specific habits and preferences&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;procedural-memory--the-how-to-library&quot;&gt;Procedural Memory — The How-To Library&lt;/h4&gt;
&lt;p&gt;Reusable step-by-step workflows that solved real problems. These are the agent’s playbook — battle-tested recipes that worked in your specific environment.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Task: Resize EKS node group with zero downtime
Solution:
  1. Cordon existing nodes (prevent new pod scheduling)
  2. Create new node group with target instance type
  3. Wait for new nodes to reach Ready state
  4. Drain old nodes (graceful pod eviction with PDB respect)
  5. Verify all pods rescheduled on new nodes
  6. Delete old node group
Context: Production EKS clusters with PodDisruptionBudgets configured&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;factual-memory--the-rulebook&quot;&gt;Factual Memory — The Rulebook&lt;/h4&gt;
&lt;p&gt;Hard constraints, best practices, and organizational rules. These prevent agents from suggesting non-compliant configurations, even if they’d be cheaper or simpler.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Task: RDS instance configuration requirements
Solution: Production RDS instances in us-east-1 MUST use Multi-AZ deployment.
          Single-AZ is only permitted for dev/staging environments.
          Minimum backup retention: 7 days for prod, 1 day for non-prod.
Context: Compliance requirement from SOC2 audit (2025-Q3)&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;episodic-memory--the-war-stories&quot;&gt;Episodic Memory — The War Stories&lt;/h4&gt;
&lt;p&gt;Successful resolutions with the reasoning behind why they worked — enabling agents to apply similar reasoning to new problems.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Task: Reduce EBS costs for data processing workloads
Solution: Migrated 47 gp2 volumes to gp3 with custom IOPS/throughput tuning.
          Result: 42% cost reduction ($8,400/month savings) with 15% better throughput.
Reasoning: gp2 IOPS scales linearly with volume size, forcing over-provisioning.
           gp3 decouples IOPS from size, so you only pay for what you use.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;incident-memory--the-post-mortems&quot;&gt;Incident Memory — The Post-Mortems&lt;/h4&gt;
&lt;p&gt;Root cause analysis from real incidents, turned into reusable diagnostic patterns. When a similar pattern emerges, the agent immediately recognizes it and applies the proven fix instead of diagnosing from scratch.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Task: Lambda-to-RDS connection exhaustion (P1 incident)
Root Cause: Lambda functions creating new DB connections per invocation.
            Under load (500 concurrent), exceeded RDS max_connections (150).
Solution: Implemented RDS Proxy with connection pooling.
          Set max_connections_percent=70 to reserve headroom.
          Added CloudWatch alarm on DatabaseConnections metric.&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;developer-pattern-memory--the-team-playbook&quot;&gt;Developer Pattern Memory — The Team Playbook&lt;/h4&gt;
&lt;p&gt;Team-specific preferences, conventions, and tooling choices. These let agents adapt recommendations to match how your team actually works — not generic best practices, but &lt;em&gt;your&lt;/em&gt; best practices.&lt;/p&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;Task: Infrastructure deployment preferences for Platform Team
Solution: Team uses Terraform modules exclusively (no raw CloudFormation).
          Naming convention: {env}-{service}-{resource} (e.g., prod-api-rds).
          Prefers blue-green over canary for stateful services.&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;seven-relationship-types--the-graph-in-memgraph&quot;&gt;Seven Relationship Types — The “Graph” in MemGraph&lt;/h3&gt;
&lt;p&gt;The real power isn’t in individual memories — it’s in how they connect. MemGraph supports seven typed relationships between memory nodes:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-relationships.svg&quot; alt=&quot;An EKS resize memory linked to seven other memories, one per relationship type: prerequisite, enables, alternative, extends, supersedes (an old method, crossed out), conflicts and related.&quot;&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Relationship&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Meaning&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;prerequisite&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Must understand this &lt;em&gt;before&lt;/em&gt; applying the memory&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“Before resizing EKS nodes → understand PodDisruptionBudgets”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;enables&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;This knowledge unlocks a more advanced capability&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“Understanding VPC peering → enables cross-account access patterns”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;alternative&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Different approach to the same problem&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“Spot instances vs. Reserved Instances for batch workloads”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;extends&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Adds depth or capability to existing knowledge&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“Basic S3 lifecycle rules → extended with Intelligent Tiering”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;supersedes&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Replaces outdated knowledge&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“gp3 migration guide supersedes old gp2 resize procedure”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;conflicts&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Contradicts — cannot apply both&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“Spot instances conflict with guaranteed SLA requirements”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;related&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;General semantic connection&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;“RDS optimization relates to connection pooling best practices”&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Why relationships matter&lt;/strong&gt;: When an agent retrieves a memory about EKS node resizing, MemGraph automatically surfaces prerequisite knowledge about PodDisruptionBudgets. It flags that the old EC2-based resize method has been superseded. It warns that the spot instance approach conflicts with this workload’s SLA requirements. The agent doesn’t just get an answer — it gets &lt;strong&gt;the full context to apply it correctly&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-evolution-pipeline--memories-that-learn&quot;&gt;The Evolution Pipeline — Memories That Learn&lt;/h2&gt;
&lt;p&gt;Most memory systems only &lt;strong&gt;append&lt;/strong&gt;. Every new piece of knowledge gets added to an ever-growing pile. Over time, the pile becomes noise.&lt;/p&gt;
&lt;p&gt;MemGraph &lt;strong&gt;curates&lt;/strong&gt;. Every new memory goes through an LLM-driven evolution pipeline that decides how it should integrate with existing knowledge.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-evolution.svg&quot; alt=&quot;The evolution pipeline in four steps: extract 0 to 5 patterns per conversation, search by keyword and meaning, let an LLM decide to store, combine or discard, and connect with typed edges.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Extract&lt;/strong&gt; — After each conversation, an LLM analyzes the dialogue and extracts &lt;strong&gt;0 to 5 reusable patterns&lt;/strong&gt; — not raw chat logs, but distilled operational knowledge: the methodology that worked, the reasoning behind it, the context where it applies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Search&lt;/strong&gt; — For each extracted memory, MemGraph searches the existing knowledge graph for related memories using &lt;strong&gt;hybrid search&lt;/strong&gt; — combining exact keyword matching (BM25) with semantic similarity (vector embeddings) through Reciprocal Rank Fusion.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Evolve&lt;/strong&gt; — An LLM analyzes the new memory against search results and makes one of three decisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STORE&lt;/strong&gt; — This is genuinely new knowledge. Add it to the graph and build typed relationships to existing memories.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;COMBINE&lt;/strong&gt; — This overlaps with something we already know. Merge them into a single, stronger, more complete memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DISCARD&lt;/strong&gt; — We already know this. Don’t pollute the graph with duplicates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Connect&lt;/strong&gt; — For stored or combined memories, the LLM determines which existing memories have meaningful relationships — prerequisites, alternatives, conflicts — and builds typed edges. Keywords from related memories are enriched into the new node.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the architectural edge.&lt;/strong&gt; Over time, MemGraph doesn’t just grow — it gets &lt;strong&gt;sharper&lt;/strong&gt;. Combined memories become more complete. Superseded memories get flagged. Validated procedures get reinforcement signals. The knowledge graph converges toward a refined, high-signal representation of your team’s operational expertise.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;confidence-signals--earned-trust-not-static-scores&quot;&gt;Confidence Signals — Earned Trust, Not Static Scores&lt;/h3&gt;
&lt;p&gt;Every memory in MemGraph carries dynamic confidence signals that evolve over time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Access Count&lt;/strong&gt; — How many times this memory has been retrieved. Frequently accessed memories are likely valuable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reinforcement&lt;/strong&gt; — How many times new evidence has confirmed this memory. Validated knowledge ranks higher.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contradiction&lt;/strong&gt; — How many times new evidence has conflicted with this memory. Flags unreliable knowledge.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supersession&lt;/strong&gt; — Whether this memory has been replaced by a newer, better version. Prevents serving outdated advice.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A procedure that’s been reinforced 50 times gets a &lt;strong&gt;5-50% score boost&lt;/strong&gt; in search results. A memory that’s been contradicted multiple times gets flagged for review. This is &lt;strong&gt;earned trust&lt;/strong&gt; — the graph learns what works in &lt;em&gt;your&lt;/em&gt; environment over time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;hybrid-search--finding-the-right-memory-at-the-right-time&quot;&gt;Hybrid Search — Finding the Right Memory at the Right Time&lt;/h2&gt;
&lt;p&gt;When an agent needs operational knowledge, MemGraph doesn’t just do a simple text search. It combines three retrieval strategies:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-search.svg&quot; alt=&quot;The query RDS connection timeout runs as keyword, semantic and graph search, and Reciprocal Rank Fusion merges the three into one ranked list with a reinforcement boost.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keyword Search (BM25)&lt;/strong&gt; — Sparse vector matching that finds memories containing exact terms from your query. When you search for “RDS connection timeout”, it finds memories that specifically mention those words. Best for known error messages, service names, specific configurations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semantic Search (Embeddings)&lt;/strong&gt; — Dense vector similarity that finds memories conceptually related, even if they use different terminology. A search for “database connection issues” finds memories about “connection pooling” and “RDS Proxy setup.” Best for exploratory queries and “how do I…” questions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Graph Traversal&lt;/strong&gt; — When a relevant memory is found, MemGraph follows its prerequisite and enables edges to surface the full context. Finding “RDS Proxy Setup” automatically pulls in “VPC Security Group Configuration” (prerequisite) and “Multi-AZ Failover Optimization” (enables). Best for complete context retrieval.&lt;/p&gt;
&lt;p&gt;The three strategies are combined using &lt;strong&gt;Reciprocal Rank Fusion (RRF)&lt;/strong&gt;, with an additional boost for memories with high reinforcement counts. The result: agents get the most relevant, battle-tested knowledge — with full context — every time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;real-world-impact&quot;&gt;Real-World Impact&lt;/h2&gt;
&lt;h3 id=&quot;without-memgraph&quot;&gt;Without MemGraph&lt;/h3&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;You: @alex We&apos;re seeing connection timeouts on the orders-api RDS instance

Alex: I&apos;ll investigate the RDS connection timeout issue.
      Let me check the current metrics...

      [Runs CloudWatch queries from scratch]
      [Analyzes connection count patterns]
      [Researches possible causes]
      [Eventually identifies connection pooling issue]

      Time to diagnosis: 12 minutes
      Same diagnosis your team did 3 months ago
      Knowledge lost after session ends&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;with-memgraph&quot;&gt;With MemGraph&lt;/h3&gt;
&lt;pre class=&quot;language-text&quot; data-language=&quot;text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;You: @alex We&apos;re seeing connection timeouts on the orders-api RDS instance

Alex: MemGraph match: This pattern matches a P1 incident from 3 months ago
      (Lambda-to-RDS connection exhaustion, confidence: high, reinforced 12x)

      Prerequisite: Checking if RDS Proxy is configured...
      It&apos;s not — this is the same root cause.

      Recommended fix (proven procedure):
      1. Deploy RDS Proxy with connection pooling (max_connections_percent=70)
      2. Update Lambda security groups for proxy access
      3. Add CloudWatch alarm on DatabaseConnections metric

      Time to diagnosis: 8 seconds
      Battle-tested fix, validated 12 times across your infrastructure
      Related: Also surfacing VPC security group prereqs&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;use-cases-in-production&quot;&gt;Use Cases in Production&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Incident Memory&lt;/strong&gt; — From 12-minute diagnosis to 8-second recognition. When similar incident patterns emerge, MemGraph immediately surfaces the proven root cause and fix — no re-diagnosis needed. The agent learns from every P1 your team resolves.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-Conversation Context&lt;/strong&gt; — Daily summaries and conversation memories let agents understand ongoing decisions: “The team agreed to freeze spot instance changes until after the SOC2 audit.” No one has to re-explain context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Personalized Operations&lt;/strong&gt; — Per-user memory files store communication preferences, priorities, and constraints. The agent knows this SRE prefers Terraform, that security lead requires compliance-first framing, and the CTO wants executive summaries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Team Pattern Recognition&lt;/strong&gt; — MemGraph learns that your platform team uses Helm charts over raw manifests, prefers blue-green over canary, and has specific naming conventions. The agent adapts its suggestions to fit — not fight — your existing practices.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;under-the-hood--architecture&quot;&gt;Under the Hood — Architecture&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-storage-layers.svg&quot; alt=&quot;Three tiers: the agent tools create_memory and search_memory, an async Celery pipeline that extracts, embeds, evolves and stores, and four stores in two Qdrant collections, S3 or MinIO, and Redis.&quot;&gt;&lt;/p&gt;
&lt;p&gt;MemGraph is built on four storage backends, each optimized for its role:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Layer&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Backend&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;What It Stores&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Why This Backend&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Knowledge Graph&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Qdrant (vector DB)&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Memory nodes with typed relationships, dense + sparse vectors&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Hybrid search (keyword + semantic) with metadata filtering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Conversation History&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Qdrant (separate collection)&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Topic summaries, key decisions, action items&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Semantic retrieval across time windows (1-365 days)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Personal Context&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;S3/MinIO&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Per-user preference files, per-workspace daily summaries&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Durable, structured markdown with Redis caching (600s TTL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Performance Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Redis&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Search results, deduplication tracking&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Sub-millisecond reads, automatic expiration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;technical-details&quot;&gt;Technical Details&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Embedding Model&lt;/strong&gt;: Amazon Bedrock Cohere Embed v4 — generates both dense vectors (semantic similarity) and sparse vectors (BM25-style keyword matching) in a single API call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search Algorithm&lt;/strong&gt;: Reciprocal Rank Fusion (RRF) combines keyword and semantic search results, with a configurable reinforcement boost (5-50%) for validated memories.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Processing Pipeline&lt;/strong&gt;: Fully async via Celery task queue. Memory extraction runs on high-priority workers with 300-second timeouts. Search results are cached in Redis for 1 hour to avoid redundant computation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deduplication&lt;/strong&gt;: Content-hash-based dedup (MD5 normalized) with 24-hour TTL prevents the same knowledge from being stored multiple times across concurrent agent sessions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Graph Traversal&lt;/strong&gt;: When &lt;code&gt;include_related=true&lt;/code&gt;, search results are enriched by following prerequisite and enables edges — surfacing the full context chain without requiring additional queries.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-compound-effect&quot;&gt;The Compound Effect&lt;/h2&gt;
&lt;p&gt;Here’s what makes MemGraph fundamentally different from traditional memory systems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every conversation makes your agents smarter.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-memory/memgraph-compound.svg&quot; alt=&quot;An illustrative chart in which a stateless agent stays flat over twelve months while a MemGraph agent keeps improving as patterns, relationships, evolution and senior-engineer context build up.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Unlike fine-tuning — which is expensive, batch-oriented, and risky — MemGraph evolves in &lt;strong&gt;real-time&lt;/strong&gt;, per-workspace, with full auditability. You can inspect every memory node, trace every relationship, and understand exactly why the agent made a recommendation.&lt;/p&gt;
&lt;p&gt;The graph starts sparse. After a month of operations, prerequisite chains emerge. After three months, the evolution pipeline is actively combining related memories and superseding outdated ones. After six months, your agents carry the operational context of a senior engineer who’s been on your team from the start.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;That’s the compound effect.&lt;/strong&gt; Not just remembering — connecting, validating, and refining knowledge with every conversation.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;MemGraph is available today for all CloudThinker workspaces.&lt;/strong&gt; Your agents are already learning from every conversation, building a knowledge graph unique to your infrastructure, your team’s practices, and your operational history.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;First published on &lt;a href=&quot;https://cloudthinker.io/blogs/memgraph-agent-memory&quot;&gt;cloudthinker.io&lt;/a&gt; on March 18, 2026.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Henry Bui</author></item><item><title>Agentic system design, entry 1: CloudThinker Agentic Orchestration and Context Optimization</title><link>https://engineering.cloudthinker.io/journeys/agentic-system-design/01/</link><guid isPermaLink="true">https://engineering.cloudthinker.io/journeys/agentic-system-design/01/</guid><pubDate>Mon, 24 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h2&gt;
&lt;p&gt;We’ve all built chatbots. They are great at answering questions, but terrible at doing actual work. The industry is shifting from reactive chatbots to autonomous agentic systems, but this transition isn’t free.&lt;/p&gt;
&lt;p&gt;When we started building CloudThinker, we wanted more than a chat interface. We wanted an AI that could wake up, analyze a skyrocketing cloud bill, fix the database config causing it, and send us a report—all without us typing a word.&lt;/p&gt;
&lt;p&gt;But moving from &lt;em&gt;“talking”&lt;/em&gt; to &lt;em&gt;“doing”&lt;/em&gt; is hard. Single-agent architectures eventually hit a wall with long-running workflows. They can’t distribute specialized tasks, parallelize operations, or manage the exponential token growth that comes with complex reasoning. If you’re not careful, they burn through $50 of tokens in ten minutes just to tell you &lt;em&gt;“I don’t know.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;We built CloudThinker to solve these scale problems. By implementing novel context optimization techniques—prompt caching, asynchronous context compaction, and parallel tool calling—we achieved &lt;strong&gt;80-95% cost reduction&lt;/strong&gt;, &lt;strong&gt;7x faster task completion&lt;/strong&gt;, and &lt;strong&gt;85% latency reduction&lt;/strong&gt; compared to our baseline.&lt;/p&gt;
&lt;p&gt;Here is the architecture we built to achieve those numbers, and the production lessons we learned moving from a fragile prototype to a system that actually works.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;2-multi-agent-architecture-deep-dive&quot;&gt;2. Multi-Agent Architecture Deep Dive&lt;/h2&gt;
&lt;p&gt;Multi-agent orchestration unlocks the potential of specialized AI systems, but success hinges on coordination strategy. How do you prevent chaos when multiple agents work simultaneously?&lt;/p&gt;
&lt;h3 id=&quot;21-the-coordination-verdict&quot;&gt;2.1 The Coordination Verdict&lt;/h3&gt;
&lt;p&gt;Let’s be clear: &lt;strong&gt;Always start with a single agent.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Multi-agent orchestration is cool, but it’s a nightmare to debug. We didn’t switch to a team of agents because it was trendy. We switched because our single agent hit a ceiling. It couldn’t be an expert in Kubernetes &lt;em&gt;and&lt;/em&gt; billing &lt;em&gt;and&lt;/em&gt; security all at once. It started making mistakes. Only then did we accept the &lt;em&gt;“complexity tax”&lt;/em&gt; of building a team.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Evolution: From Single-Agent to Supervisor&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Our architecture didn’t start with specialists. It evolved because we looked at the metrics. We compared three fundamental approaches:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Pattern&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Coordination Model&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Single-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;No coordination needed&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;❌ &lt;strong&gt;The Ceiling&lt;/strong&gt;: Great for simple tasks, fails hard on complex ones.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Network (Peer-to-Peer)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Distributed consensus&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;❌ &lt;strong&gt;The Trap&lt;/strong&gt;: Like a committee meeting with no agenda. Lots of talking, no decisions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Supervisor&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Centralized coordinator&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;✅ &lt;strong&gt;The Solution&lt;/strong&gt;: Clear ownership. One boss, many workers.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single-Agent (The Baseline)&lt;/strong&gt;: Always start here. We ran a single generalist agent until it started failing on complex, multi-domain tasks. Don’t pay the &lt;em&gt;“coordination tax”&lt;/em&gt; until you have to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network/Peer-to-Peer (The “Chaos” Trap)&lt;/strong&gt;: We tried letting agents talk directly to each other. It was a mess. Without a boss in the room, they argued in infinite loops about who should check the database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Winner: Supervisor Pattern&lt;/strong&gt;: We settled on the &lt;strong&gt;Supervisor Pattern&lt;/strong&gt; (Anna - General Manager). It gave us the reliability we needed: one agent whose only job is to make sure the work gets done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We standardized on the &lt;strong&gt;Supervisor Pattern&lt;/strong&gt; (Anna - General Manager). This provides the “Production-Ready” reliability we needed: clear ownership, explicit routing, and a central point for state management.&lt;/p&gt;
&lt;h3 id=&quot;22-choosing-the-right-supervisor-variant&quot;&gt;2.2 Choosing the Right Supervisor Variant&lt;/h3&gt;
&lt;p&gt;Once we decided on a Supervisor, we had to pick a management style.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Variant&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;When to Use&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;Trade-offs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Flat Supervisor&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Small teams (2-10)&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;✅ &lt;strong&gt;Simple, reliable&lt;/strong&gt; ← &lt;strong&gt;CloudThinker uses this&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Supervisor (as tools)&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Delegation&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;⚠️ Less autonomy, tighter coupling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Hierarchical&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Large Orgs&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;⚠️ Good for scale, bad for latency. Adds middle management.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;&lt;strong&gt;Custom Graph&lt;/strong&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;Complex dependencies&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;❌ Flexible, but debugging it is miserable.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flat Supervisor (CloudThinker’s Choice)&lt;/strong&gt;: We stuck with the simplest option. We have distinct domains (Compute, Database, Security), so a single flat layer is enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hierarchical&lt;/strong&gt;: Only use this if you’re building a massive system. If you need a &lt;em&gt;“Support Team”&lt;/em&gt; that handles tickets without ever bothering the main manager, this makes sense. Otherwise, it’s just extra layers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Graph&lt;/strong&gt;: Avoid this unless you hate yourself. In production, we prefer boring and predictable over &lt;em&gt;“flexible”&lt;/em&gt; and broken.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Production Insight&lt;/strong&gt;: Start with the &lt;strong&gt;Flat Supervisor&lt;/strong&gt;. It incurs the lowest &lt;em&gt;“coordination tax”&lt;/em&gt; while providing sufficient separation of concerns. Only move to Hierarchical if you need to scale beyond 10 agents or require strict &lt;em&gt;“Transfer”&lt;/em&gt; patterns where sub-teams operate completely independently.&lt;/p&gt;
&lt;h3 id=&quot;23-routing-and-activation-optimistic-routing-with-fail-safes&quot;&gt;2.3 Routing and Activation: Optimistic Routing with Fail-Safes&lt;/h3&gt;
&lt;p&gt;In theory, the Supervisor should manage everything. In reality, that’s slow and expensive. Why ask the manager for permission to check the time?&lt;/p&gt;
&lt;p&gt;We use &lt;strong&gt;Optimistic Routing&lt;/strong&gt;—we assume the specialist can handle it, but we keep a safety net.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Default to the Specialist (Fast Path)&lt;/strong&gt;: If you ask &lt;em&gt;“Show running instances&lt;/em&gt;,” we send you straight to &lt;strong&gt;Alex (Cloud Engineer)&lt;/strong&gt;. No meeting with the boss required. Zero orchestration overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual Continuity (Sticky Sessions)&lt;/strong&gt;: If you’re already talking to Tony about a database issue, we keep you there. It’s annoying to be transferred back to the main menu every time you reply.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Escalation Safety Net&lt;/strong&gt;: This is the &lt;em&gt;“fail-safe.”&lt;/em&gt; If you ask Alex (the cloud guy) &lt;em&gt;“Why is my RDS slow?”&lt;/em&gt;, he doesn’t try to guess. He says, &lt;em&gt;“That’s not my job,”&lt;/em&gt; and escalates back to &lt;strong&gt;Anna (Supervisor)&lt;/strong&gt;. She then assigns it to the right person. Complex tasks never fail silently; they just get escalated.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why this matters&lt;/strong&gt;: This approach cuts latency by 40-50% for most tasks. We keep the speed of a single agent but the power of a full team when we need it.&lt;/p&gt;
&lt;h3 id=&quot;24-agent-communication-protocol&quot;&gt;2.4 Agent Communication Protocol&lt;/h3&gt;
&lt;p&gt;If you think getting three humans to agree on a lunch spot is hard, try getting three AI agents to debug a database.&lt;/p&gt;
&lt;p&gt;Without strict rules, multi-agent chats turn into &lt;em&gt;“context pollution”&lt;/em&gt;—agents confusing each other with irrelevant data until the whole system crashes.&lt;/p&gt;
&lt;p&gt;We solve this with a &lt;strong&gt;Group Chat Protocol&lt;/strong&gt; that enforces two simple rules:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Explicit Targeting&lt;/strong&gt;: You can’t just &lt;em&gt;“talk.”&lt;/em&gt; You must address someone (&lt;code&gt;@alex&lt;/code&gt;, &lt;code&gt;@anna&lt;/code&gt;). If you don’t, the message is rejected.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured Handoffs&lt;/strong&gt;: You can’t just dump a 50-page log file into the chat. You have to summarize it first.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Within this framework, we use two patterns to move work around: &lt;strong&gt;Delegation&lt;/strong&gt; (The &lt;em&gt;“Boomerang”&lt;/em&gt;) and &lt;strong&gt;Transfer&lt;/strong&gt; (The &lt;em&gt;“Handoff”&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delegation: The “Boomerang” Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/delegation-pattern.svg&quot; alt=&quot;Delegation pattern: over time, the supervisor delegates a task through the group chat, worker 1 takes it and posts an update, the supervisor takes that and delegates to worker 2, worker 2 posts an update, and the supervisor takes it and responds to the user&quot;&gt;&lt;/p&gt;
&lt;p&gt;Complex infrastructure workflows require multiple specialized agents working in
sequence, but naive implementations create two critical problems: (1)
&lt;strong&gt;exponential token growth&lt;/strong&gt; when each agent inherits all previous context, and
(2) &lt;strong&gt;coordination failures&lt;/strong&gt; when handoffs lose critical task state. Delegation
solves this through structured task assignment where supervisors retain
accountability while workers execute specialized operations.&lt;/p&gt;
&lt;p&gt;Consider a multi-stage AWS cost optimization workflow: (1) Alex (Cloud Engineer) gathers EC2 spending data, (2) Anna (General Manager) analyzes spending patterns and identifies database cost anomalies, (3) Tony (Database Engineer) investigates RDS configuration and proposes optimizations. This can’t be a direct transfer—Anna needs Alex’s cost data to provide strategic context to Tony, but Tony shouldn’t inherit Alex’s verbose CloudWatch API outputs (potentially 50K+ tokens). Delegation enables this through structured handoffs: Anna delegates to Alex → Alex executes and returns summarized results to group chat → Anna takes results, analyzes, then delegates to Tony → Tony executes with only Anna’s analysis context, not Alex’s raw outputs.&lt;/p&gt;
&lt;p&gt;Delegation operates through the group chat tool with explicit continuation markers. The critical optimization: Tony receives Anna’s 200-token delegation message with strategic context (&lt;em&gt;“no compute correlation”&lt;/em&gt;), not Alex’s 50K-token CloudWatch dumps. Anna maintains accountability—she’s waiting for Tony’s results to compile the final optimization plan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Token Economics and State Management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Each agent maintains isolated message history—when Agent A delegates to Agent B, only the handoff message appears in Agent B’s context, not Agent A’s tool outputs. With this context isolation approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Agent A: 50K tokens → produces 200-token summary&lt;/li&gt;
&lt;li&gt;Agent B: 200 tokens (A’s summary) + 30K (B’s work) = 30.2K tokens → produces 200-token summary&lt;/li&gt;
&lt;li&gt;Agent C: 200 tokens (B’s summary) + 20K (C’s work) = 20.2K tokens&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total: 100.4K tokens processed (56% reduction)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In production CloudThinker workflows, delegation with context isolation achieves &lt;strong&gt;50-70% token reduction&lt;/strong&gt; compared to shared-context implementations, directly reducing inference costs and latency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-offs and Production Considerations&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Delegation introduces coordination overhead—each handoff requires supervisor reasoning about which specialist to activate and what context to provide. In our metrics, delegation adds around 5 seconds per handoff for supervisor decision-making, acceptable for complex workflows but unnecessary for simple single-specialist tasks. The most common anti-pattern is delegating too early: if the supervisor hasn’t completed its analysis phase, delegation becomes premature—&lt;em&gt;“@alex go check EC2 costs”&lt;/em&gt; without specifying what patterns to look for forces unnecessary back-and-forth. The supervisor must provide sufficient task framing before delegating. Error handling adds further complexity—when a worker fails mid-delegation chain, the supervising agent must decide: retry with the same specialist, escalate to a different specialist, or abort the workflow. Production systems require explicit error handling policies; our implementation uses a 3-retry policy with exponential backoff before escalating to the user.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transfer: The “Handoff” Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/transfer-pattern.svg&quot; alt=&quot;Transfer pattern: over time, the supervisor transfers the task through the group chat, worker 1 takes it and responds to the group chat and directly to the user, with no return to the supervisor&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not all multi-agent workflows require supervisor oversight. Transfer provides complete ownership handoff for simple, domain-specific queries—the supervisor exits, the specialist takes full control, and all subsequent user interactions happen directly with the specialist. The choice hinges on two questions: (1) Does the task require multi-specialist coordination? (2) Will the supervisor need the results for subsequent analysis? If both answers are &lt;em&gt;“no,”&lt;/em&gt; transfer is optimal.&lt;/p&gt;
&lt;p&gt;Transfer applies to single-specialist queries: &lt;em&gt;“What’s our PostgreSQL version?”&lt;/em&gt; or &lt;em&gt;“Show current Kubernetes pods.”&lt;/em&gt; Delegation handles multi-stage workflows: &lt;em&gt;“Why did costs spike?”&lt;/em&gt; (requires coordination) or &lt;em&gt;“Investigate API slowness”&lt;/em&gt; (requires multiple specialists).&lt;/p&gt;
&lt;p&gt;Transfer eliminates supervisor overhead by archiving the supervisor’s state after handoff—no callbacks needed. Production metrics show &lt;strong&gt;cost reduction&lt;/strong&gt; and &lt;strong&gt;50% latency reduction&lt;/strong&gt; compared to delegation (10.2K tokens → 7.2K tokens, ~4-6s → ~2-3s). However, the most common failure is premature transfer: supervisors must complete decomposition before handing off.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;❌ &lt;code&gt;&quot;App has high latency&quot; → &quot;@alex investigate&quot;&lt;/code&gt; (too vague).&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;&quot;API response times 200ms→1.8s at 14:00 UTC&quot; → &quot;@alex investigate infrastructure causes&quot;&lt;/code&gt; (sufficient context). &lt;strong&gt;Transfer only when the specialist has sufficient context to execute independently.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;3-context-optimization-techniques&quot;&gt;3. Context Optimization Techniques&lt;/h2&gt;
&lt;p&gt;Multi-agent orchestration solves coordination challenges but introduces a new problem: token explosion. Four techniques form our optimization strategy—prompt caching, asynchronous context compaction, tool consolidation, and parallel tool calling.&lt;/p&gt;
&lt;h3 id=&quot;31-prompt-caching&quot;&gt;3.1 Prompt Caching&lt;/h3&gt;
&lt;p&gt;The ratios between chatbot and agentic systems are dramatically different. Chatbot systems typically have a 3:1 input-to-output token ratio, while agentic systems can reach 100:1 ratios. This is because agentic systems need to reason, plan, and execute tasks through multiple tool calls—often 50+ per task. Input token costs dominate total expenses, making prompt caching critical for production viability.&lt;/p&gt;
&lt;p&gt;Prompt caching (also called KV cache) allows LLMs to reuse previously computed context across API calls, delivering two critical benefits: cost reduction and latency improvement. Most implementations charge a premium for cache writes but deliver significant discounts on cache reads (often 80-90% cost reduction) and up to 85% latency reduction on cached requests. Cache hits also dramatically reduce time-to-first-token (TTFT)—the model skips reprocessing cached context and immediately begins generating output. After just a few API calls reusing the same context, the economics shift dramatically in your favor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Cache Breakpoint Challenge&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The rule is simple: &lt;strong&gt;Cache everything static. Keep dynamic stuff at the end.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But &lt;em&gt;“static”&lt;/em&gt; is trickier than it looks. As the &lt;a href=&quot;https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus&quot;&gt;Manus team discovered&lt;/a&gt;, a single timestamp in the wrong place can invalidate your entire cache. We treat &lt;strong&gt;Cache Hit Rate&lt;/strong&gt; as a top-level KPI. If it drops below 90%, we treat it like a production outage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three-Tier Objective Prompt Strategy&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Following the Manus team’s insight that objective prompts should be placed outside cache boundaries, we use a “Three-Tier” architecture to maximize cache reuse while preserving turn-by-turn adaptability:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://engineering.cloudthinker.io/diagrams/agentic-system-design/prompt-caching-three-tier.svg&quot; alt=&quot;Three-tier prompt caching strategy: context at steps N, N+1 and N+2. Tool schemas and system prompt sit at the top, then completed actions and observations, then the cache checkpoint, then the objective. Each turn the checkpoint moves down past the newest action and observation, so earlier blocks are cache hits and only the newest pair is a cache write&quot;&gt;&lt;/p&gt;
&lt;p&gt;The diagram illustrates the critical optimization: the cache checkpoint moves forward after each completed turn. At Step N, the agent generates Action 1 and receives Observation 1. At Step N+1, these completed interactions move behind the checkpoint—now cached and reusable. The Objective (containing live context like team composition, active resources, and current plan) remains after the checkpoint, regenerated each turn with updated information. By Step N+2, both previous action-observation pairs are cached. This &lt;em&gt;“moving breakpoint”&lt;/em&gt; strategy ensures all historical work gets cached while only the dynamic objective block is reprocessed each turn. Compare this to naive implementations that modify system prompts or insert content mid-history—those approaches invalidate the entire cache on every API call.&lt;/p&gt;
&lt;p&gt;The three-tier structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tier 1 - Static System Prompt&lt;/strong&gt;: Tool schemas and core behavioral rules. Cache write on first call, then cache hits forever (95%+ hit rate across conversations)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tier 2 - Conversation History&lt;/strong&gt;: Actions and Observations accumulate as the agent works. The cache checkpoint moves forward after each completed turn, preserving all historical context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tier 3 - Dynamic Objectives&lt;/strong&gt;: Always placed AFTER the checkpoint. Contains live context (team composition, connected resources, active plan, memory retrieval results). Never cached—regenerated each turn with current state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; By keeping dynamic content at the end, everything upstream remains cacheable. As conversations grow longer, the cache absorbs the majority of tokens while only the small objective block is reprocessed per turn—the foundation of the 80-95% cost reduction.&lt;/p&gt;
&lt;h3 id=&quot;32-asynchronous-context-compaction&quot;&gt;3.2 Asynchronous Context Compaction&lt;/h3&gt;
&lt;p&gt;Caching saves money, but you eventually run out of space. Even with a 200k context window, a multi-hour infrastructure analysis can fill the buffer with tool outputs and reasoning traces.&lt;/p&gt;
&lt;p&gt;The standard industry solution is &lt;em&gt;“summarization,”&lt;/em&gt; but the way most teams implement it destroys both User Experience (UX) and accuracy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Failure of Synchronous Summarization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We initially ran summarization synchronously: when the window hit 90%, the agent would pause, call a separate “Summarizer Agent” to compress the history, and then resume.&lt;/p&gt;
&lt;p&gt;This failed for two reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The “Please Wait” Problem&lt;/strong&gt;: Users were left staring at a spinner for 45 seconds in the middle of a debug session while the system &lt;em&gt;“cleaned up.”&lt;/em&gt; It killed the flow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The “External Summarizer” Gap&lt;/strong&gt;: Our benchmarks showed that handing the history to a generic third-party summarizer agent resulted in &lt;strong&gt;performance degradation&lt;/strong&gt; on subsequent tasks. The external agent lacked the implicit reasoning state of the active worker, resulting in generic summaries that stripped out critical technical nuances.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;The Fix: Asynchronous Self-Summarization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We moved to an &lt;strong&gt;Asynchronous Context Compaction&lt;/strong&gt; model that solves both problems.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The 70% Trigger&lt;/strong&gt;: We don’t wait until the cliff edge. When context usage hits 70%, we trigger a background job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-Blocking Execution&lt;/strong&gt;: The user doesn’t see this happen. The main agent continues responding to new queries using the full (un-compacted) context, maintaining zero latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hot-Swapping&lt;/strong&gt;: When the background task completes, we carefully splice the state. We replace the &lt;em&gt;old&lt;/em&gt; history (up to the trigger point) with the new summary, but we keep the &lt;em&gt;new&lt;/em&gt; messages (generated while the background task was running) raw and untouched.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Why Self-Summarization Wins&lt;/strong&gt;: Crucially, we don’t spawn a new &lt;em&gt;“Summarizer”&lt;/em&gt; persona. We ask the &lt;em&gt;active agent itself&lt;/em&gt; to “Summarize your work so far” in that background thread. Because the agent summarizes its own conversation, it intuitively preserves the details relevant to its current goal, rather than creating a generic recap.&lt;/p&gt;
&lt;p&gt;This approach eliminated the &lt;em&gt;“maintenance pause”&lt;/em&gt; entirely and improved long-context task completion rates by keeping recent context raw and immediate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The “Cache Trap” in Summarization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even with asynchronous execution, you still have to pay for the tokens to generate the summary. Here is where most teams lose money. When it’s time to summarize, the instinct is to change the system prompt to: &lt;em&gt;“You are a summarizer.”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a trap.&lt;/strong&gt; Changing the system prompt breaks the cache for the entire history. You effectively pay full price to re-read 100k tokens just to throw them away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Fix: Append, Don’t Replace&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The fix is simple: &lt;strong&gt;Don’t touch the system prompt.&lt;/strong&gt; Just append a final message: &lt;em&gt;“Summarize the conversation above.”&lt;/em&gt; This keeps the 100k tokens in the cache (90% discount).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claude Sonnet 4.5 pricing:&lt;/strong&gt; $3/MTok input, $15/MTok output, $0.30/MTok cache read&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;❌ NAIVE: Create new system prompt with summarization instructions → breaks cache&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;You are a summarizer agent...&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# New system prompt (~500 tokens)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;messages&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Old messages to summarize (100K tokens)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# CACHE BROKEN - must reprocess as uncached input!&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Message 1...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Response 1...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Message 50...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cost breakdown:&lt;/strong&gt; 500 tokens uncached ($0.0015) + 100K uncached ($0.300) + 2K output ($0.030) = &lt;strong&gt;$0.3315&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;✅ OPTIMIZED: Preserve original system prompt + append summarization instruction → preserves cache&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;You are Anna...&quot;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Original system prompt (~8K tokens, already cached)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cachePoint&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Cache hit!&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;messages&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Old messages to summarize (100K tokens)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Message 1...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assistant&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Response 1...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Message 50...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;cachePoint&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;default&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Cache all old messages&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Summarization instruction at the END (~500 tokens, not cached)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; `Summarize the conversation above&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
            Extract&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; key decisions&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; technical analysis&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; errors&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; current state&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;`&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Cost breakdown:&lt;/strong&gt; 8K cached ($0.0024) + 100K cached ($0.030) + 0.5K uncached ($0.0015) + 2K output ($0.030) = &lt;strong&gt;$0.0639&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Savings:&lt;/strong&gt; 80.7% cost reduction ($0.3315 → $0.0639)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trade-offs and Recursive Summarization&lt;/strong&gt;: Summarization loses some conversational nuance and fine-grained detail. We mitigate this through comprehensive summary templates that preserve technical decisions, error patterns, and user feedback—ensuring agents maintain continuity across compaction boundaries.&lt;/p&gt;
&lt;p&gt;For extremely long sessions, we apply recursive summarization: existing summaries get included in subsequent passes, creating hierarchical compression where older context becomes progressively condensed while recent context remains detailed.&lt;/p&gt;
&lt;h3 id=&quot;33-tool-consolidation&quot;&gt;3.3 Tool Consolidation&lt;/h3&gt;
&lt;p&gt;Anthropic recently introduced the &lt;a href=&quot;https://www.anthropic.com/news/model-context-protocol&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt; as a standard for connecting agents to data. It’s a massive step forward—standardizing how agents connect to everything from Slack to Postgres.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The “Tool Pollution” Paradox&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;MCP makes it easy to connect tools. Too easy.
Suddenly, your agent has access to 500 tools. If you dump 500 tool definitions into the context window, you leave no room for reasoning. The agent spends 90% of its brainpower just parsing the API list, or worse, hallucinates parameters because the context is flooded.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The “Code Execution” Pivot (and why we use it sparingly)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To solve this, Anthropic also proposed &lt;a href=&quot;https://www.anthropic.com/engineering/code-execution-with-mcp&quot;&gt;Code Execution with MCP&lt;/a&gt;, suggesting agents write code to &lt;em&gt;“discover”&lt;/em&gt; tools dynamically. Instead of listing tools in the system prompt, you give the agent an execution environment and let it import tool definitions from a file tree.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Reliability Trade-off&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While progressive disclosure is efficient, we found that for our use case, keeping critical tools visible is essential. In CloudThinker, reliability is our primary goal. We think that if an agent doesn’t explicitly see a &lt;em&gt;“Restart Database”&lt;/em&gt; tool in its system prompt, it is less likely to reason about using it as part of a complex solution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The CloudThinker Solution: Consolidated Tools + JIT Schemas&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We chose a pragmatic middle ground: &lt;strong&gt;Consolidate the interface, hide the manual.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Traditional CRUD agent design creates five separate tools (&lt;code&gt;recommendation_list&lt;/code&gt;, &lt;code&gt;recommendation_get&lt;/code&gt;, &lt;code&gt;recommendation_create&lt;/code&gt;, &lt;code&gt;recommendation_update&lt;/code&gt;, &lt;code&gt;recommendation_delete&lt;/code&gt;). We merge these into a single interface.&lt;/p&gt;
&lt;pre class=&quot;language-python&quot; data-language=&quot;python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Minimal description + schema externalized to get_instruction&lt;/span&gt;
&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@tool&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;description&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;MINIMAL_RECOMMENDATION_TOOL_DESCRIPTION&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;recommendation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    command&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Literal&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;get_instruction&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;get_all&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;delete&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;create&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;update&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    recommendation_ids&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;UUID&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    recommendations&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;dict&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# Generic dict instead of typed schema&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The “Just-in-Time” Schema&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Notice the &lt;code&gt;get_instruction&lt;/code&gt; command? That’s our secret weapon.&lt;/p&gt;
&lt;p&gt;Instead of stuffing the full schema into the system prompt (where you pay for it on every token), we hide it behind this command.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Visibility&lt;/strong&gt;: The agent &lt;em&gt;sees&lt;/em&gt; the &lt;code&gt;recommendation&lt;/code&gt; tool. It knows the capability exists (unlike Code Execution).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency&lt;/strong&gt;: It doesn’t pay for the parameter definitions until it needs them.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the agent forgets how to update a recommendation, it calls &lt;code&gt;recommendation(command=&quot;get_instruction&quot;)&lt;/code&gt; to get the manual. This moves documentation from &lt;em&gt;“Always Loaded”&lt;/em&gt; (expensive) to &lt;em&gt;“On Demand”&lt;/em&gt; (cheap), maintaining reliability without the token bloat.&lt;/p&gt;
&lt;h3 id=&quot;34-parallel-tool-calling&quot;&gt;3.4 Parallel Tool Calling&lt;/h3&gt;
&lt;p&gt;Early LLM agent architectures were fundamentally sequential. The ReAct pattern (Reasoning and Acting), introduced in 2022, defined the standard approach: the LLM calls one tool, analyzes the outcome, reasons about the next step, then executes another tool call. This sequential workflow was by design—models weren’t trained to handle multiple simultaneous operations, and frameworks enforced one-tool-at-a-time execution to maintain reliability.&lt;/p&gt;
&lt;p&gt;The landscape shifted dramatically in 2024. When Anthropic announced Claude 3’s tool use general availability in May 2024, parallel tool calling was initially unsupported. However, as foundation models became more capable, providers began training them to handle concurrent operations. Modern Claude models now support calling multiple tools simultaneously within a single response—a critical evolution for complex workflows requiring independent operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enabling Parallel Tool Calling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Model capability alone isn’t sufficient—you must explicitly instruct agents to leverage parallelism. Modern models default to sequential execution unless prompted otherwise. As Anthropic’s documentation recommends: &lt;a href=&quot;https://docs.claude.com/en/docs/agents-and-tools/tool-use/implement-tool-use#maximizing-parallel-tool-use&quot;&gt;“For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.”&lt;/a&gt; This single instruction can reduce multi-tool workflow latency by 3-5x compared to sequential execution.&lt;/p&gt;
&lt;p&gt;In CloudThinker, parallel tool calling emerges naturally during plan execution. When an agent marks a plan step completed and transitions to the next task, it evaluates whether multiple operations can run concurrently. For example, completing &lt;em&gt;“gather cost data”&lt;/em&gt; might trigger parallel steps for &lt;em&gt;“analyze compute patterns,”&lt;/em&gt; &lt;em&gt;“review storage utilization,”&lt;/em&gt; and &lt;em&gt;“audit network traffic”&lt;/em&gt;—all executing simultaneously rather than sequentially, reducing multi-step workflows from minutes to seconds.&lt;/p&gt;
&lt;p&gt;This isn’t just about speed (though it cuts latency by 3-5x). It’s about money. Every round trip to the model costs you tokens. By batching 5 tool calls into one request, we cut out 4 round trips of context processing. That simple change drove a &lt;strong&gt;30-40% cost reduction&lt;/strong&gt; for our complex workflows.&lt;/p&gt;
&lt;h2 id=&quot;4-conclusion&quot;&gt;4. Conclusion&lt;/h2&gt;
&lt;p&gt;We learned the hard way that multi-agent systems fail predictably—and succeed through disciplined optimization.&lt;/p&gt;
&lt;p&gt;The supervisor-worker pattern proved essential for operational clarity—predictable failures, traceable routing, and observable trade-offs. But remember the golden rule:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complexity is a choice.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Start with a single agent. Add prompt caching. Instrument everything. Only when metrics demand it, evolve to a supervisor pattern. Scale when metrics justify complexity.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;First published on &lt;a href=&quot;https://cloudthinker.io/blogs/cloudthinker-agentic-orchestration-and-context-optimization&quot;&gt;cloudthinker.io&lt;/a&gt; on November 24, 2025.&lt;/em&gt;&lt;/p&gt;
</content:encoded><author>Henry Bui</author></item></channel></rss>