<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Astrodevil</title>
    <description>The latest articles on DEV Community by Astrodevil (@astrodevil).</description>
    <link>https://dev.to/astrodevil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F647287%2Fed664d6c-6825-4dd6-9767-e0ad901afa6a.jpg</url>
      <title>DEV Community: Astrodevil</title>
      <link>https://dev.to/astrodevil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/astrodevil"/>
    <language>en</language>
    <item>
      <title>How to Build Self-Healing AI Agents with Monocle, Okahu MCP and OpenCode</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Wed, 08 Apr 2026 22:04:48 +0000</pubDate>
      <link>https://dev.to/astrodevil/how-to-build-self-healing-ai-agents-with-monocle-okahu-mcp-and-opencode-1g4e</link>
      <guid>https://dev.to/astrodevil/how-to-build-self-healing-ai-agents-with-monocle-okahu-mcp-and-opencode-1g4e</guid>
      <description>&lt;p&gt;Coding agents write code. When that code fails, who debugs it? Right now, that's still you. The agent writes, you interpret error logs, you prompt the agent to fix. The debugging loop stays open.&lt;/p&gt;

&lt;p&gt;The fix is giving agents access to their own telemetry. An agent that can query its own traces can verify its work, diagnose failures, and iterate without waiting for a human to read logs.&lt;/p&gt;

&lt;p&gt;This tutorial shows you how to build a &lt;strong&gt;self-healing agent&lt;/strong&gt; that runs tests, queries its own production traces via &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;, identifies root causes, and fixes bugs without human intervention.&lt;/p&gt;

&lt;p&gt;By the end, you'll have a working demo where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A buggy Text-to-SQL application fails its test suite&lt;/li&gt;
&lt;li&gt;An agent queries its own traces from &lt;a href="https://portal.okahu.co/" rel="noopener noreferrer"&gt;Okahu Cloud&lt;/a&gt; via MCP&lt;/li&gt;
&lt;li&gt;The agent identifies and fixes bugs based on trace analysis&lt;/li&gt;
&lt;li&gt;All tests pass, without a human reading logs or prompting fixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What we'll cover:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is Monocle and why auto-instrumentation matters&lt;/li&gt;
&lt;li&gt;What is &lt;a href="https://docs.okahu.ai/okahu_mcp/" rel="noopener noreferrer"&gt;Okahu MCP&lt;/a&gt; and how agents consume telemetry&lt;/li&gt;
&lt;li&gt;Setting up the self-healing demo environment&lt;/li&gt;
&lt;li&gt;Running the agent and watching it fix itself&lt;/li&gt;
&lt;li&gt;Key takeaways for building autonomous debugging loops&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start by understanding the two core technologies that make this possible. &lt;/p&gt;

&lt;h2&gt;
  
  
  What is Monocle?
&lt;/h2&gt;

&lt;p&gt;In traditional software, you read the source code to understand what the application does. The logic is deterministic and inspectable. In agent-driven applications, the code is scaffolding. The actual decision-making happens inside the model at runtime.&lt;/p&gt;

&lt;p&gt;An agent that can't access traces is an agent working without documentation. It will guess where failures occur and propose changes based on incomplete information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/monocle2ai/monocle" rel="noopener noreferrer"&gt;&lt;strong&gt;Monocle&lt;/strong&gt;&lt;/a&gt; helps developers and platform engineers building or managing generative AI apps monitor these in prod by making it easy to instrument their code to capture traces that are compliant with open-source cloud-native observability ecosystem. It automatically captures traces from LLM SDKs like OpenAI, LangChain, and LlamaIndex without any manual span creation.&lt;/p&gt;

&lt;p&gt;Agents won't manually add telemetry to their generated code. If instrumentation requires effort, it won't happen. Monocle solves this by auto-instrumenting supported SDKs the moment they're used.&lt;/p&gt;

&lt;p&gt;Here's what it takes to enable automatic trace capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;monocle_apptrace&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;setup_monocle_telemetry&lt;/span&gt;

&lt;span class="c1"&gt;# One line to enable automatic trace capture
&lt;/span&gt;&lt;span class="nf"&gt;setup_monocle_telemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workflow_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text_to_sql_analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. From this point forward, every OpenAI SDK call, every database query, every tool invocation is captured as a trace span. &lt;/p&gt;

&lt;p&gt;These traces include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM calls&lt;/strong&gt;: Inputs, outputs, model name, token usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App traces&lt;/strong&gt;: OpenTelemetry compatible for exporting traces/spans from an application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool invocations&lt;/strong&gt;: Agent framework tool calls and responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors and latency&lt;/strong&gt;: Exception details and timing data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something goes wrong, the trace contains everything an agent needs to diagnose the problem. The question is: how does the agent access that trace data?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbctzkztzov4rfeovybq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frbctzkztzov4rfeovybq.png" alt="Zero-config instrumentation using monocle" width="800" height="359"&gt;&lt;/a&gt; Zero-config instrumentation using monocle&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It is also worthy to note that Monocle does not depend on existing OpenTelemetry instrumentation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Okahu MCP?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.okahu.ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;Okahu&lt;/strong&gt;&lt;/a&gt; is a cloud observability platform built for AI applications. It ingests traces from Monocle, stores them, and provides dashboards for visualization.&lt;/p&gt;

&lt;p&gt;Dashboards are designed for human eyes. They're full of charts, graphs, and interactive trace viewers. An agent can't click through a dashboard. The data exists, but agents can't access it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;**MCP&lt;/a&gt; (Model Context Protocol)** is a standard for exposing data sources to AI agents. Okahu MCP turns the observability platform into a programmable API that agents can query directly. Instead of a human clicking through a dashboard, an agent calls &lt;code&gt;/okahu:get_latest_traces&lt;/code&gt; and parses the JSON response.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Observability platforms must evolve from human dashboards to programmatic interfaces that agents can consume. MCP is how that happens.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To connect an agent to Okahu MCP, add this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"okahu"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.okahu.ai/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"x-api-key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-okahu-api-key"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the agent can query its own traces. It sees the same data a human would see in the dashboard, but in a format it can parse, reason about, and act on. Make sure you authenticate the server by running these command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;opencode mcp auth okahu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will generate a URL and will redirect you to authenticate with Okahu cloud and a successful authentication will show a screen like the one below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztmzscjb8z35i62wwzxy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztmzscjb8z35i62wwzxy.png" alt="Okahu MCP auth" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Monocle capturing traces and Okahu MCP exposing them, we have the infrastructure for self-healing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.10+&lt;/strong&gt; installed on your system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI API key&lt;/strong&gt; for LLM calls (the demo uses GPT-4o)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.okahu.ai" rel="noopener noreferrer"&gt;Okahu&lt;/a&gt; API key&lt;/strong&gt; for trace storage (sign up at &lt;a href="https://okahu.ai/" rel="noopener noreferrer"&gt;okahu.ai&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://opencode.ai/" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt;&lt;/strong&gt; or a similar coding agent with MCP support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you have these ready, let's clone the demo repository and set up the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Clone and Set Up the Demo
&lt;/h2&gt;

&lt;p&gt;Start by cloning the demo repository and creating a virtual environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Arindam200/awesome-ai-apps
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp_ai_agents/telemetry-mcp-okahu

&lt;span class="c"&gt;# Create and activate virtual environment&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;monocle_apptrace monocle_test_tools openai fastapi pytest python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a &lt;code&gt;.env&lt;/code&gt; file with your API keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-openai-key"&lt;/span&gt;
&lt;span class="nv"&gt;OPENAI_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"gpt-4o"&lt;/span&gt;
&lt;span class="nv"&gt;OKAHU_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your-okahu-key"&lt;/span&gt;
&lt;span class="nv"&gt;MONOCLE_EXPORTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"okahu"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;MONOCLE_EXPORTER=okahu&lt;/code&gt; setting tells Monocle to send traces to Okahu Cloud instead of logging them locally. This is important because we're suppressing all local logs to force the agent to use MCP for debugging.&lt;/p&gt;

&lt;p&gt;Finally, initialize the sample database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python setup_db.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;sales.db&lt;/code&gt; SQLite database with &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt; tables. The Text-to-SQL application will generate queries against this database.&lt;/p&gt;

&lt;p&gt;Now let's look at what makes this demo interesting: the intentionally buggy application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Understand the Buggy Application
&lt;/h2&gt;

&lt;p&gt;The demo includes a pre-built &lt;code&gt;analyst.py&lt;/code&gt; with &lt;strong&gt;three intentional bugs&lt;/strong&gt;. Each bug produces a specific trace signature that the agent must find and interpret:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;What's Wrong&lt;/th&gt;
&lt;th&gt;What the Trace Shows&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;Uses &lt;code&gt;client.completions.create()&lt;/code&gt; instead of &lt;code&gt;client.chat.completions.create()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;API error: "NotFoundError: /v1/completions not found for gpt-4o"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Accesses &lt;code&gt;.text&lt;/code&gt; instead of &lt;code&gt;.message.content&lt;/code&gt; on the response&lt;/td&gt;
&lt;td&gt;AttributeError in trace span&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Schema prompt says &lt;code&gt;customers/products&lt;/code&gt; but DB has &lt;code&gt;users/orders&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SQL execution error: "no such table: customers"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why pre-built bugs instead of letting the agent write code from scratch? Two reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reproducibility&lt;/strong&gt;: Every demo run starts with the same failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No guessing&lt;/strong&gt;: The agent must read traces to understand what's wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can reset to the buggy state anytime with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python reset_demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script overwrites &lt;code&gt;analyst.py&lt;/code&gt; with the buggy version, ready for the agent to fix.&lt;/p&gt;

&lt;p&gt;With the buggy application in place, let's run the tests and see it fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run Tests and See Failures
&lt;/h2&gt;

&lt;p&gt;The test suite uses &lt;strong&gt;Monocle Test Tools&lt;/strong&gt; to validate not just outputs, but traces. We're testing that the right LLM calls were made, not just that the code ran.&lt;/p&gt;

&lt;p&gt;Here is what the &lt;code&gt;test_analyst.py&lt;/code&gt; file looks like and what it tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@MonocleValidator&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;monocle_testcase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql_generation_test_cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_generate_sql_with_monocle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_test_case&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Test SQL generation using Monocle validator.

    This validates:
    - OpenAI inference spans are generated correctly
    - SQL output matches expected patterns (similarity check)

    If this fails, check Okahu MCP traces for:
    - Missing inference spans (wrong API method used)
    - Incorrect SQL generation (schema mismatch)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nc"&gt;MonocleValidator&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;test_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generate_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;my_test_case&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Direct database tests (no monocle validation needed - these test the DB itself)
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_execute_query_users_table&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Test direct SQL execution on the actual database.
    This verifies the users table exists and has data.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users LIMIT 3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Should return 3 users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Verify column structure: (user_id, username, email)
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Each user row should have 3 columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_execute_query_orders_table&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Test direct SQL execution on orders table.
    This verifies the orders table exists and has data.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM orders WHERE amount &amp;gt; 100 LIMIT 5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Should find orders over $100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# Verify column structure: (order_id, user_id, amount, order_date)
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Each order row should have 4 columns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest test_analyst.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see output like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrctawc5q1lypig35occ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrctawc5q1lypig35occ.png" alt="Test output in terminal" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The direct database tests pass (the database itself is fine), but the LLM-powered SQL generation fails.&lt;/p&gt;

&lt;p&gt;Here's what makes this demo realistic: &lt;strong&gt;there are no useful local logs&lt;/strong&gt;. We've suppressed all local logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CRITICAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent cannot read local logs to debug. It must query Okahu MCP to understand what went wrong. This forces the self-healing loop through observability infrastructure, exactly how it would work in production.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fec2wpj85kmgcu1gfo9ao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fec2wpj85kmgcu1gfo9ao.png" alt="The trace shows exactly what went wrong. The agent queries this via MCP" width="800" height="449"&gt;&lt;/a&gt; The trace shows exactly what went wrong. The agent queries this via MCP&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The trace shows exactly what went wrong. The agent queries this via MCP&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Configure the Self-Healing Agent
&lt;/h2&gt;

&lt;p&gt;The demo uses an OpenCode agent mode called &lt;code&gt;@analyst_v3&lt;/code&gt;. This is a custom agent configuration stored in &lt;code&gt;.opencode/agents/analyst_v3.md&lt;/code&gt; with specific rules for self-healing behavior.&lt;/p&gt;

&lt;p&gt;Tool access alone isn't enough. The agent needs structured rules that encode how an experienced developer would debug.&lt;/p&gt;

&lt;p&gt;Here are the summary of the core rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gs"&gt;**Self-Healing Rules:**&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; Run tests first, observe failures
&lt;span class="p"&gt;2.&lt;/span&gt; Wait 5 seconds for trace ingestion
&lt;span class="p"&gt;3.&lt;/span&gt; Query Okahu MCP: &lt;span class="sb"&gt;`/okahu:get_latest_traces`&lt;/span&gt; with workflow_name='text_to_sql_analyst_v3'
&lt;span class="p"&gt;4.&lt;/span&gt; Fix bugs based on trace analysis only
&lt;span class="p"&gt;5.&lt;/span&gt; Archive old code to &lt;span class="sb"&gt;`versions/analyst_vN.py`&lt;/span&gt; before each fix
&lt;span class="p"&gt;6.&lt;/span&gt; Record the trace ID used to diagnose each fix
&lt;span class="p"&gt;7.&lt;/span&gt; Repeat until all tests pass

&lt;span class="gs"&gt;**Critical Constraint:**&lt;/span&gt;
NO GUESSING. If no traces are available, STOP and report failure.
Do not attempt to fix code without trace evidence.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "no guessing" rule is the key constraint. Without it, the agent might make up fixes based on general knowledge. By requiring trace evidence, every fix must be grounded in telemetry. The agent can cite its sources.&lt;/p&gt;

&lt;p&gt;The agent also has a list of approved packages it can install if missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;monocle_apptrace, monocle_test_tools, openai, fastapi, pytest, python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it encounters a &lt;code&gt;ModuleNotFoundError&lt;/code&gt;, it can install from this list, but nothing else.&lt;/p&gt;

&lt;p&gt;With the agent configured, let's trigger the self-healing loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Run the Self-Healing Loop
&lt;/h2&gt;

&lt;p&gt;With traces accessible via MCP, the agent can close the loop itself. It runs, fails, queries traces, diagnoses, fixes, and repeats.&lt;/p&gt;

&lt;p&gt;Trigger the agent with this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;@analyst_v3 Fix the buggy Text-to-SQL codebase:

The &lt;span class="sb"&gt;`analyst.py`&lt;/span&gt;, &lt;span class="sb"&gt;`test_analyst.py`&lt;/span&gt;, and &lt;span class="sb"&gt;`main.py`&lt;/span&gt; files already exist but have bugs.
&lt;span class="p"&gt;
1.&lt;/span&gt; Run Tests: Execute &lt;span class="sb"&gt;`pytest test_analyst.py -v`&lt;/span&gt; to see failures.
&lt;span class="p"&gt;2.&lt;/span&gt; Analyze Traces: Wait 5s, then query Okahu MCP with workflow_name='text_to_sql_analyst_v3'.
&lt;span class="p"&gt;3.&lt;/span&gt; Fix Loop:
&lt;span class="p"&gt;    -&lt;/span&gt; Archive current &lt;span class="sb"&gt;`analyst.py`&lt;/span&gt; to &lt;span class="sb"&gt;`versions/analyst_vN.py`&lt;/span&gt;
&lt;span class="p"&gt;    -&lt;/span&gt; Fix the bug based on trace analysis
&lt;span class="p"&gt;    -&lt;/span&gt; Record the trace ID used to diagnose each fix
&lt;span class="p"&gt;    -&lt;/span&gt; Run tests again
&lt;span class="p"&gt;    -&lt;/span&gt; Repeat until all tests pass
&lt;span class="p"&gt;4.&lt;/span&gt; Final Report: Output a summary table of all issues fixed with their associated trace IDs.

Rules: No debug files. Debug only via Okahu MCP traces. Always call the MCP tool to get the logs from traces, do not use the local logs in the terminal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what happens, completely autonomously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent runs tests&lt;/strong&gt; → 3 failures detected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent waits 5 seconds&lt;/strong&gt; → Allows traces to be ingested into Okahu&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent queries Okahu MCP&lt;/strong&gt; → Retrieves trace data for the failed runs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent reads the first trace&lt;/strong&gt; → Sees "NotFoundError: /v1/completions not found for gpt-4o"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent archives&lt;/strong&gt; &lt;code&gt;analyst.py&lt;/code&gt; → Saves to &lt;code&gt;versions/analyst_v1.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent fixes Bug #1&lt;/strong&gt; → Changes &lt;code&gt;completions.create()&lt;/code&gt; to &lt;code&gt;chat.completions.create()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent runs tests&lt;/strong&gt; → 2 failures remain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent queries MCP again&lt;/strong&gt; → Sees AttributeError for &lt;code&gt;.text&lt;/code&gt; access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent fixes Bug #2&lt;/strong&gt; → Changes &lt;code&gt;.text&lt;/code&gt; to &lt;code&gt;.message.content&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent runs tests&lt;/strong&gt; → 1 failure remains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent queries MCP&lt;/strong&gt; → Sees "no such table: customers"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent fixes Bug #3&lt;/strong&gt; → Updates schema in prompt from &lt;code&gt;customers/products&lt;/code&gt; to &lt;code&gt;users/orders&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent runs tests&lt;/strong&gt; → All 5 tests pass&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwm0agose1yg5x68tfcv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqwm0agose1yg5x68tfcv.png" alt="Self healing look" width="800" height="700"&gt;&lt;/a&gt; Agent runs, fails, queries traces, fixes, repeats until all tests pass.&lt;/p&gt;

&lt;p&gt;The entire loop runs without human intervention. The agent reads, reasons, and repairs using only its own telemetry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: The Fix Summary
&lt;/h2&gt;

&lt;p&gt;The agent produces a traceable summary. Every fix links to the trace that prompted it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Trace ID&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;Wrong OpenAI API (completions → chat.completions)&lt;/td&gt;
&lt;td&gt;trace_a1b2c3d4...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Wrong response attribute (.text → .message.content)&lt;/td&gt;
&lt;td&gt;trace_e5f6g7h8...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Schema mismatch (customers/products → users/orders)&lt;/td&gt;
&lt;td&gt;trace_i9j0k1l2...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can take any trace ID, look it up in Okahu, and see exactly what error led to that fix. The human's role shifts from doing the debugging to auditing the system that did the debugging.&lt;/p&gt;

&lt;p&gt;The archived versions in &lt;code&gt;versions/&lt;/code&gt; show the progression:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;analyst_v1.py&lt;/code&gt;: Original buggy version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyst_v2.py&lt;/code&gt;: After fixing API method&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyst_v3.py&lt;/code&gt;: After fixing response attribute&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;analyst.py&lt;/code&gt;: Final working version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The human wasn't in the debugging loop. The human set up the environment, triggered the agent, and reviewed the summary. The debugging itself was autonomous.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnp7l9efadtnrc3xo2glq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnp7l9efadtnrc3xo2glq.png" alt="OpenCode agent in action" width="800" height="505"&gt;&lt;/a&gt; OpenCode agent running test, using monocle traces on Okahu via MCP to debug and fix the buggy app&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;Here's what we've shown and why it matters:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Auto-instrumentation is essential for self-healing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents won't manually add telemetry to their generated code. If you want agents to debug their own work, instrumentation must be automatic. Monocle captures traces from supported SDKs with a single line of setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MCP enables agent-native observability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dashboards are designed for humans. MCP turns observability platforms into programmable APIs that agents can query. Okahu MCP exposes the same trace data a human would see, but in a format agents can parse and act on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "No Trace, No Fix" prevents hallucinated fixes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without grounding in telemetry, agents might guess at fixes based on general knowledge. The "no trace, no fix" rule ensures every repair is evidence-based. If the agent can't find trace data, it stops and reports the issue rather than guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Trace-based testing validates the full pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monocle Test Tools doesn't just check that code runs. It validates that the right traces exist. If the LLM wasn't called correctly, the inference span won't exist, and the test fails. This catches issues that output-only testing would miss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Human role shifts from debugging to monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The human sets up the environment, triggers the agent, and reviews the fix summary. The iterative debugging loop (run, fail, diagnose, fix, repeat) is fully autonomous. This is a fundamental shift in how we build and maintain AI applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evolution: Who Consumes Telemetry?
&lt;/h2&gt;

&lt;p&gt;This demo represents a real shift in how software gets built:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Who Writes Code&lt;/th&gt;
&lt;th&gt;Who Reads Telemetry&lt;/th&gt;
&lt;th&gt;Human Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Software 1.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Human&lt;/td&gt;
&lt;td&gt;Human&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Software 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coding Agent&lt;/td&gt;
&lt;td&gt;Human&lt;/td&gt;
&lt;td&gt;Prompts + interprets errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autonomous&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Coding Agent&lt;/td&gt;
&lt;td&gt;Coding Agent&lt;/td&gt;
&lt;td&gt;Monitors only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsrfkbasrq6etmbvolsl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdsrfkbasrq6etmbvolsl.png" alt="Who consumes telemetry data?" width="800" height="462"&gt;&lt;/a&gt; Who consumes telemetry data?&lt;/p&gt;

&lt;p&gt;In Software 1.0, humans wrote code, read dashboards, and fixed bugs. In Software 2.0, agents write code, but humans still interpret errors and prompt fixes. The agent is the "hands," but the human remains the "brain."&lt;/p&gt;

&lt;p&gt;In the autonomous phase, which is what this demo shows, the agent writes, runs, debugs, and fixes. The human monitors the process and reviews outcomes. The agent consumes its own telemetry.&lt;/p&gt;

&lt;p&gt;The lesson from every team that has pushed agents toward autonomy is the same: the critical work is never writing code. It's designing the environment, encoding constraints, structuring knowledge, and building feedback loops. As human involvement decreases, the systems that steer and verify agent behavior must become more robust, not less.&lt;/p&gt;

&lt;p&gt;This requires a shift in how we build observability platforms. Dashboards aren't enough. Platforms must expose MCP interfaces that agents can query programmatically. The data is the same; the consumer is different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Self-healing agents are possible when three conditions are met:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instrumentation is automatic&lt;/strong&gt;: Monocle captures traces without manual span creation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry is programmatically accessible&lt;/strong&gt;: Okahu MCP exposes traces as an API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing validates traces, not just outputs&lt;/strong&gt;: Monocle Test Tools ensures the right calls were made&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a coding agent that can debug its own code by querying production telemetry. It runs tests, reads traces, identifies root causes, and applies fixes, closing the loop without human intervention.&lt;/p&gt;

&lt;p&gt;The organizations that invest early in giving agents access to telemetry, embedding evaluation into workflows, and exposing programmatic observability interfaces will scale agent-driven development effectively. Those that don't will find their agents operating blind, producing code that looks correct but behaves unpredictably.&lt;/p&gt;

&lt;p&gt;The question is no longer "who writes the code?" It's "who consumes the telemetry?" When agents can do both, the loop closes, and we've entered a new phase of software engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Monocle&lt;/strong&gt;: &lt;code&gt;pip install monocle_apptrace&lt;/code&gt; | &lt;a href="https://github.com/monocle2ai/monocle" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monocle Test Tools&lt;/strong&gt;: &lt;code&gt;pip install monocle_test_tools&lt;/code&gt; | Trace-based validation framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Okahu Cloud&lt;/strong&gt;: &lt;a href="https://okahu.ai/" rel="noopener noreferrer"&gt;okahu.ai&lt;/a&gt; | AI observability platform with MCP support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo Repository&lt;/strong&gt;: &lt;a href="https://github.com/Arindam200/awesome-ai-apps/tree/main/mcp_ai_agents/telemetry-mcp-okahu" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | Full source code for this tutorial&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode&lt;/strong&gt;: Install &lt;a href="https://opencode.ai/" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article demonstrates a proof-of-concept for autonomous agent debugging. The patterns shown here (auto-instrumentation, MCP-based telemetry access, and trace-driven testing) are the building blocks for self-healing AI systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build Collaborative AI Whiteboard Like Mural Using Velt Agent Skills and MiniMax🔥</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Tue, 07 Apr 2026 16:13:20 +0000</pubDate>
      <link>https://dev.to/astrodevil/build-collaborative-ai-whiteboard-like-mural-using-velt-agent-skills-and-minimax-10ce</link>
      <guid>https://dev.to/astrodevil/build-collaborative-ai-whiteboard-like-mural-using-velt-agent-skills-and-minimax-10ce</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Building a real-time collaborative canvas is harder than it looks. The interface seems simple enough, boxes, lines, and cursors moving around. But underneath it is a constant stream of concurrent edits, conflicting updates, and shared state that has to stay consistent for everyone at the same time. The hard part is not drawing on the canvas. It is making sure multiple people can work on the same board simultaneously without things breaking quietly in the background.&lt;/p&gt;

&lt;p&gt;In this article, we will build a collaborative whiteboard using &lt;a href="https://velt.dev/" rel="noopener noreferrer"&gt;Velt&lt;/a&gt; that supports shared editing, live presence, comments, and AI-assisted interactions all inside the same canvas. The focus is not just on rendering nodes but on wiring the system in a way that keeps every user in sync without adding friction to the experience.&lt;/p&gt;

&lt;p&gt;Here is what the final result looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2szpuqmb2ga4mldl7m0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk2szpuqmb2ga4mldl7m0.gif" alt="App demo" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now let's look at the stack behind it and how each piece fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; for the app framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ReactFlow&lt;/strong&gt; for the infinite canvas and custom node types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Velt&lt;/strong&gt; for CRDT sync, live cursors, presence, comments, and notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MiniMax M2.5&lt;/strong&gt; for the AI features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS &amp;amp; Shadcn&lt;/strong&gt; for styling and dark mode&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zustand&lt;/strong&gt; for state management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used &lt;a href="https://docs.velt.dev/get-started/mcp-installer" rel="noopener noreferrer"&gt;Velt Docs MCP&lt;/a&gt; and &lt;a href="https://docs.velt.dev/get-started/skills" rel="noopener noreferrer"&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/a&gt; throughout the build. The MCP server pulls Velt's API references directly into your editor context, and Skills are pre-built prompt templates that tell the agent exactly how to set up features like presence, comments, and notifications. Adding collaborative features that would have taken a couple of days ended up taking a few prompts with Skills and Velt Docs MCP.&lt;/p&gt;

&lt;p&gt;For the editor side, I paired with GitHub Copilot Agent inside VS Code, it cut down the back-and-forth that usually comes with integrating a new SDK. If you haven't tried the &lt;a href="https://github.com/github/copilot-sdk?utm_source=blog-cli-sdk-repo-cta&amp;amp;utm_medium=blog&amp;amp;utm_campaign=cli-sdk-jan-2026" rel="noopener noreferrer"&gt;Copilot Agent&lt;/a&gt; yet, it's worth a look. It takes multi-step actions in the editor rather than just completing code at the cursor, which works well for integration-heavy projects like this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How CRDT Makes This Work&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The stack above gives you the canvas and the AI, but collaboration only works if multiple users can edit the same board without stepping on each other. This is handled through Velt’s CRDT-based sync.&lt;/p&gt;

&lt;p&gt;CRDT records every change as an operation rather than a replacement, so edits from different users merge naturally, and the document keeps moving forward. Velt builds on Yjs and manages this layer for you. Wrap the app with &lt;code&gt;VeltProvider&lt;/code&gt;, call &lt;code&gt;client.setDocument()&lt;/code&gt;, and the shared state starts syncing immediately. Node positions, text, cursors, and comments all stay inside the same document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36hwg5kp6szl1os096l0.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F36hwg5kp6szl1os096l0.gif" alt="Velt CRDT" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That sync layer is already running the moment you call &lt;code&gt;setDocument()&lt;/code&gt;. Everything else in this build sits on top of it. Before getting into the implementation, here is how to get the project running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Start
&lt;/h2&gt;

&lt;p&gt;Clone the repo and add your environment variables to get it running locally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Studio1HQ/claude-velt-mcp-app
&lt;span class="nb"&gt;cd &lt;/span&gt;velt-whiteboard
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these three keys to your &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;NEXT_PUBLIC_VELT_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_velt_key
&lt;span class="nv"&gt;MINIMAX_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_minimax_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;npm run dev&lt;/code&gt; and open &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can grab your Velt API key from the &lt;a href="https://console.velt.dev/" rel="noopener noreferrer"&gt;Velt dashboard&lt;/a&gt; and your MiniMax key from the &lt;a href="https://platform.minimax.io/docs/guides/text-ai-coding-tools" rel="noopener noreferrer"&gt;MiniMax platform&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest of this post walks through how the project is structured and how each piece is built, so you have the full context of what you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up Velt
&lt;/h2&gt;

&lt;p&gt;We did not write any of the code setup manually. During the building of this project, we used &lt;a href="https://docs.velt.dev/get-started/skills" rel="noopener noreferrer"&gt;Velt Agent Skills&lt;/a&gt; and the &lt;a href="https://docs.velt.dev/get-started/mcp-installer" rel="noopener noreferrer"&gt;Velt MCP server&lt;/a&gt;. One prompt handled the entire setup, the agent scanned the project, picked the right provider location, and generated the files.&lt;/p&gt;

&lt;p&gt;If you are on Cursor or Claude Code, Velt also has an &lt;a href="https://docs.velt.dev/get-started/plugins" rel="noopener noreferrer"&gt;&lt;strong&gt;AI Plugin&lt;/strong&gt;&lt;/a&gt; that bundles the MCP server, skills, rules, and an expert agent in one install, so you can skip even these three steps.&lt;/p&gt;

&lt;p&gt;We used VS Code, so we set up the skills and MCP server separately. Here is how:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Install Agent Skills&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add velt-js/agent-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pulls four skill sets into your project: setup best practices, comments, CRDT, and notifications. Your agent picks the right one automatically based on what you ask it to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Add the Velt MCP server to your editor&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @velt-js/mcp-installer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart your editor after running this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Start the installation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In your AI agent chat (Copilot, Claude, Cursor,  whichever you use), type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;install &lt;/span&gt;velt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent walks through the setup one step at a time: your project path, API key, which features you want, and where to put &lt;code&gt;VeltProvider&lt;/code&gt;. It scans the codebase to detect your auth pattern and document ID source, shows you its findings, and generates an implementation plan before touching anything. You approve the plan, it applies the changes, and then runs a QA pass automatically.&lt;/p&gt;

&lt;p&gt;The prompt we used for this project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Set up Velt collaboration in my Next.js app. I need comments, presence, cursors, and CRDT for ReactFlow. My API key is in .env as NEXT_PUBLIC_VELT_API_KEY.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What would have taken a couple of hours of reading docs came down to that one prompt. The folder structure and code below are exactly what came out of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="nx"&gt;providers&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nx"&gt;velt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;  &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;wraps&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;VeltProvider&lt;/span&gt;
    &lt;span class="nx"&gt;velt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;     &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;identifies&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;sets&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;
  &lt;span class="nx"&gt;whiteboard&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nx"&gt;whiteboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;             &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;
    &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;                     &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;StickyNote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ShapeNode&lt;/span&gt;
  &lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;                 &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt; &lt;span class="nx"&gt;all&lt;/span&gt; &lt;span class="nx"&gt;Velt&lt;/span&gt; &lt;span class="nx"&gt;UI&lt;/span&gt; &lt;span class="nx"&gt;components&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt;
&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
  &lt;span class="nx"&gt;whiteboard&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two separate provider files here. &lt;code&gt;VeltProviderWrapper&lt;/code&gt; just wraps the app with &lt;code&gt;VeltProvider&lt;/code&gt; and passes the API key. &lt;code&gt;VeltAuthenticator&lt;/code&gt; sits inside it and handles the actual user identification and document setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// velt-authenticator.tsx&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;identify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;photoUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;photoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// runs after identify resolves&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;documentId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order matters. &lt;code&gt;setDocument&lt;/code&gt; only runs after &lt;code&gt;isUserIdentified&lt;/code&gt; flips to true, which is tracked with a &lt;code&gt;useState&lt;/code&gt; flag. If you call &lt;code&gt;setDocument&lt;/code&gt; before the user is identified, Velt won't associate that session correctly with the document; that’s why the agent put these in one order.&lt;/p&gt;

&lt;p&gt;The demo uses hardcoded users with a dropdown switcher. If you want to plug in real auth, swap the &lt;code&gt;currentUser&lt;/code&gt; object in the store with whatever your auth provider returns, and the rest of the setup stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Canvas
&lt;/h2&gt;

&lt;p&gt;The canvas layer is built on two packages, &lt;code&gt;@xyflow/react&lt;/code&gt; for the canvas itself and &lt;code&gt;zustand&lt;/code&gt; for global state management.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw20k3j4cw2i1pyiib9wi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw20k3j4cw2i1pyiib9wi.png" alt="Canvas" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ReactFlow handles the canvas, nodes, edges, and interactions. Zustand is for global state management across the app.&lt;/p&gt;

&lt;p&gt;Having everything in a single store makes it much easier to wire up features like AI later, since any part of the app can read or write the same state without prop drilling or extra context layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  State Management
&lt;/h3&gt;

&lt;p&gt;The store is in &lt;code&gt;lib/store/whiteboard-store.ts&lt;/code&gt;. It keeps track of the active tool, selected shape, selected template, a node ID counter, and a few other things. One thing worth noting is how &lt;code&gt;setSelectedTool&lt;/code&gt; works. When you switch to &lt;code&gt;"shapes"&lt;/code&gt; or &lt;code&gt;"templates"&lt;/code&gt;, it also flips the corresponding panel open automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;setSelectedTool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;selectedTool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;shapes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isShapesPanelOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;templates&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isTemplatesPanelOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node ID counter is just an incrementing number prefixed with &lt;code&gt;"node-"&lt;/code&gt;. Simple, but it prevents collisions when multiple nodes are dropped quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up ReactFlow
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;whiteboard.tsx&lt;/code&gt;, you register all custom node types before passing them to &lt;code&gt;&amp;lt;ReactFlow&amp;gt;&lt;/code&gt;. This maps a string like &lt;code&gt;"sticky"&lt;/code&gt; to the actual React component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;StickyNote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShapeNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;nodeTypes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodeTypes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onNodesChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onNodesChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onEdgesChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onEdgesChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onConnect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onConnect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onPaneClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handlePaneClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Background&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Controls&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;ReactFlow&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onPaneClick&lt;/code&gt; is where click-to-place logic sits. When a tool or shape is selected in the store, clicking the canvas converts that canvas coordinate to a flow position and creates a new node there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkoansjydqyw4wrs4cwzo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkoansjydqyw4wrs4cwzo.gif" alt="Canvas Demo" width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a Node
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;StickyNote.tsx&lt;/code&gt; is a good starting point for understanding how all nodes work. Every node receives &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;selected&lt;/code&gt;, and &lt;code&gt;id&lt;/code&gt; as props via &lt;code&gt;NodeProps&lt;/code&gt;. The component keeps its own local state for &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;, and &lt;code&gt;isEditing&lt;/code&gt;. On double-click, it switches to a textarea, and on blur, it commits back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;StickyNote&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;NodeProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isEditing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsEditing&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NodeResizeControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NodeResizeControl&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NodeToolbar&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* color picker */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;NodeToolbar&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Handle&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"source"&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Right&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isEditing&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;StickyNote&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;memo()&lt;/code&gt; at the bottom. Without it, every canvas interaction re-renders all nodes. ReactFlow recommends this for any non-trivial node. The &lt;code&gt;Handle&lt;/code&gt; components on all four sides is what allows edges to connect from any direction. &lt;code&gt;ShapeNode&lt;/code&gt; follows the same pattern, but uses a &lt;code&gt;switch&lt;/code&gt; on &lt;code&gt;shapeType&lt;/code&gt; to render different SVG shapes, rectangles, circles, diamonds, and so on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Templates
&lt;/h3&gt;

&lt;p&gt;Templates are in &lt;code&gt;lib/constants/templates.ts&lt;/code&gt; as a plain array of &lt;code&gt;TemplateType&lt;/code&gt; objects. Each template has an &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and a &lt;code&gt;nodes&lt;/code&gt; array. That nodes array is just ReactFlow node definitions with preset positions, types, colors, and labels. No runtime logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;kanban&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Kanban Board&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;📋 Backlog&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;➡️ Up next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you click a template in the sidebar, it sets &lt;code&gt;selectedTemplate&lt;/code&gt; in the store. The next click on the canvas calls &lt;code&gt;getNextNodeId()&lt;/code&gt; for each node in the template, offsets all positions relative to where you clicked, and adds them to the ReactFlow nodes array in one go. The template disappears from the selection state immediately after.&lt;/p&gt;

&lt;p&gt;To add your own template, you define the layout once by hand and commit it to the array. If the layout is repetitive (like the brainstorm grid), you can use &lt;code&gt;Array.from&lt;/code&gt; with a generator to compute positions instead of writing them all out. The existing templates show both approaches.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jou5m5uizgelej7arug.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0jou5m5uizgelej7arug.gif" alt="Canvas 1" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the canvas layer in place, the next piece is layering Velt on top of it for real-time collaboration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-Time Collaboration with Velt
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;VeltProvider&lt;/code&gt; wraps the app and the document is set, the collaboration layer is essentially ready. The only thing left is placing the right components in the right spots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursors and Presence
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;VeltCursor&lt;/code&gt; and &lt;code&gt;VeltPresence&lt;/code&gt; are both mounted inside &lt;code&gt;VeltProviderWrapper&lt;/code&gt; in &lt;code&gt;VeltProvider.tsx&lt;/code&gt;, sitting alongside the app children. This means they are active across the entire canvas, not scoped to a specific component. Every connected user gets a labeled cursor that moves in real time, and their avatar shows up in the "Online" section of the header.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw420w9vv2g9p5c0gemx.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flw420w9vv2g9p5c0gemx.gif" alt="Cursor and Presence" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Header Tools
&lt;/h3&gt;

&lt;p&gt;All four collaboration controls sit in &lt;code&gt;TopBar.tsx&lt;/code&gt;. The component imports &lt;code&gt;VeltCommentTool&lt;/code&gt;, &lt;code&gt;VeltPresence&lt;/code&gt;, &lt;code&gt;VeltNotificationsTool&lt;/code&gt;, and &lt;code&gt;VeltSidebarButton&lt;/code&gt; from &lt;code&gt;@veltdev/react&lt;/code&gt; and drops them into the header alongside the user switcher dropdown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;VeltPresence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;VeltNotificationsTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;VeltCommentTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;VeltSidebarButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@veltdev/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inside the header JSX:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltCommentTool&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltSidebarButton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltNotificationsTool&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltPresence&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VeltCommentTool&lt;/code&gt; activates the comment pin mode so users can click anywhere on the canvas to leave a comment. &lt;code&gt;VeltSidebarButton&lt;/code&gt; toggles a panel that lists every comment on the document in one place. &lt;code&gt;VeltNotificationsTool&lt;/code&gt; shows a bell icon with a badge that updates when someone mentions you or replies to your thread. &lt;code&gt;VeltPresence&lt;/code&gt; renders the avatar stack at the right side of the header.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comments
&lt;/h3&gt;

&lt;p&gt;The comment editor Velt ships uses SlateJS under the hood, so it supports rich text out of the box, including &lt;code&gt;@mentions&lt;/code&gt;. When a user clicks anywhere on the canvas with comment mode active, a pin drops at that position, and a comment thread opens. The pin stays anchored there, visible to everyone on the document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiatea008yis4hkwfpnhj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiatea008yis4hkwfpnhj.gif" alt="Comments" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dark Mode
&lt;/h3&gt;

&lt;p&gt;Velt's components use Shadow DOM by default, which means your global CSS won't reach them. To apply custom theming, you first disable Shadow DOM on the components you want to style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltComments&lt;/span&gt; &lt;span class="na"&gt;shadowDom&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VeltCommentsSidebar&lt;/span&gt; &lt;span class="na"&gt;shadowDom&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you use Velt's &lt;a href="https://playground.velt.dev/themes" rel="noopener noreferrer"&gt;theme playground&lt;/a&gt; to generate your CSS token set. It gives you a full palette for both light and dark mode that you can copy directly. Paste it inside the &lt;code&gt;body&lt;/code&gt; tag in &lt;code&gt;globals.css&lt;/code&gt; and it covers both themes automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Border Radius */&lt;/span&gt;
  &lt;span class="py"&gt;--velt-border-radius-xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--velt-border-radius-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Light Mode */&lt;/span&gt;
  &lt;span class="py"&gt;--velt-light-mode-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e31646&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--velt-light-mode-background-0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--velt-light-mode-text-0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0a0a0a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Dark Mode */&lt;/span&gt;
  &lt;span class="py"&gt;--velt-dark-mode-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e31646&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--velt-dark-mode-background-0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#000000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--velt-dark-mode-text-0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* and so on... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The theme playground is the fastest way to get this right. You pick your accent color, adjust the radius and spacing, and it generates the full token set.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI on the Canvas
&lt;/h2&gt;

&lt;p&gt;With the canvas rendering and state managed globally, let's add AI to the canvas.&lt;/p&gt;

&lt;p&gt;To add AI to the canvas, you need three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an API route that calls the model,&lt;/li&gt;
&lt;li&gt;a set of structured action types that the model can return, and&lt;/li&gt;
&lt;li&gt;a function that converts those actions into ReactFlow nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI sidebar in this project connects all three.&lt;/p&gt;

&lt;p&gt;The sidebar has four modes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ask AI for free-form chat,&lt;/li&gt;
&lt;li&gt;Brainstorm to generate ideas as sticky notes,&lt;/li&gt;
&lt;li&gt;Summarize to get a read of what's on the board, and&lt;/li&gt;
&lt;li&gt;A Template namer that looks at the current layout and suggests what to call it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You type a prompt, it reads the live canvas state, and either responds with text or drops new nodes directly onto the canvas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Model
&lt;/h3&gt;

&lt;p&gt;The model powering this is MiniMax M2.5, a large context model that's fast and works well for structured output tasks like this one. What made it easy to drop in is that it runs on an Anthropic-compatible API. You get the API key from &lt;a href="https://platform.minimax.io/docs/guides/text-ai-coding-tools" rel="noopener noreferrer"&gt;MiniMax's platform&lt;/a&gt;, then point the Anthropic SDK at their base URL and swap the model name. No new SDK, no adapter layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MINIMAX_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.minimax.io/anthropic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MiniMax-M2.5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CANVAS_SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;contextSummary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context window is large enough that you can serialize the entire node list and send it with every request. The current canvas state gets appended to the system prompt as a JSON summary of all nodes, their IDs, types, text, and colors, so the model always knows what's already on the board before it responds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MiniMax M2.5 in the Editor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since the same API is Anthropic-compatible, you can also wire it into GitHub Copilot Chat inside VS Code, not just inside the project. During the build, we added MiniMax M2.5 as the active model in Copilot and used it to ask questions and build features directly from the editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26us4o9uc016xqr293y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F26us4o9uc016xqr293y7.png" alt="IDE" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It works the same way as any other model in Copilot Chat, except it is your own key and your own model.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canvas Actions
&lt;/h3&gt;

&lt;p&gt;Back to the app, the model needs to do more than just respond with text. It needs to place nodes, update colors, and interact with the canvas in a predictable way. Rather than letting the model return free-form text and trying to parse intent from it, the system prompt instructs it to always return structured JSON with two fields: &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;actions&lt;/code&gt;. The &lt;code&gt;actions&lt;/code&gt; array is a typed list of operations the model wants to perform on the canvas.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Added 5 brainstorming sticky notes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add_sticky&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Reduce onboarding steps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fef08a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add social login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#fef08a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buildNodesFromActions()&lt;/code&gt; in &lt;code&gt;canvas-actions.ts&lt;/code&gt; takes that actions array plus a canvas anchor point and converts it into ReactFlow nodes, laid out in a grid starting from that position. If the action is &lt;code&gt;update_color&lt;/code&gt;, &lt;code&gt;applyColorUpdates()&lt;/code&gt; maps over existing nodes and patches their color in place. Both functions are pure, they take nodes in and return nodes out, with no side effects.&lt;/p&gt;

&lt;p&gt;The AI does not directly touch the ReactFlow state. It just returns data. The sidebar calls &lt;code&gt;buildNodesFromActions&lt;/code&gt;, gets the result, and passes it to &lt;code&gt;setNodes&lt;/code&gt;. That separation keeps the AI logic completely independent of how the canvas renders.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y2mcgvcr48whm0b7j31.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3y2mcgvcr48whm0b7j31.gif" alt="app in action" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With everything wired up, run &lt;code&gt;npm run dev&lt;/code&gt; and open &lt;code&gt;localhost:3000&lt;/code&gt;. Velt takes a second to initialize on the first load, after that, the canvas is live.&lt;br&gt;
Switch between the two demo users using the dropdown in the header to simulate two people on the same board. Both cursors show up, comments are shared, and the AI sidebar works independently from either user session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Demo
&lt;/h3&gt;

&lt;p&gt;You can check the deployed version of our app here - &lt;a href="https://mural-velt.vercel.app/" rel="noopener noreferrer"&gt;https://mural-velt.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ujx16a62lmojqknxy58.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ujx16a62lmojqknxy58.gif" alt="Mural like app demo" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;This article covered building a collaborative whiteboard from the canvas layer up, real-time sync with Velt's CRDT, live cursors and comments, and an AI sidebar that reads and writes to the board. The pieces are modular enough that extending it is mostly additive.&lt;/p&gt;

&lt;p&gt;A few things worth building on top of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lottie reactions on nodes so collaborators can leave emoji responses without opening a comment thread&lt;/li&gt;
&lt;li&gt;Export to PNG or PDF so teams can take the board outside the browser&lt;/li&gt;
&lt;li&gt;More node types like embeds, images, or code blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CRDT layer is already there, so most of these would be new node types or toolbar additions rather than architectural changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.velt.dev/mcp" rel="noopener noreferrer"&gt;Velt Docs MCP&lt;/a&gt;, worth setting up in your IDE before you start a Velt integration&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.velt.dev/get-started/skills" rel="noopener noreferrer"&gt;Velt Skills&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.minimax.io/docs/guides/text-ai-coding-tools" rel="noopener noreferrer"&gt;MiniMax Docs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build a Real-Time Social Media App with InsForge, MiniMax, and Next.js</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Wed, 25 Mar 2026 18:57:24 +0000</pubDate>
      <link>https://dev.to/astrodevil/build-a-real-time-social-media-app-with-insforge-minimax-and-nextjs-3jb8</link>
      <guid>https://dev.to/astrodevil/build-a-real-time-social-media-app-with-insforge-minimax-and-nextjs-3jb8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we will build a full-stack social platform where users post, like, repost, follow each other, get real-time notifications, and chat with an in-app AI assistant.&lt;/p&gt;

&lt;p&gt;Here is what we will be building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js frontend with a real-time feed, post composer, profile pages, notifications, explore, and an AI chat screen&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;&lt;strong&gt;InsForge&lt;/strong&gt;&lt;/a&gt; as the backend platform, managing the database, auth, file storage, real-time pub/sub, and AI gateway from a single place&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.minimax.io/models/text/m27" rel="noopener noreferrer"&gt;&lt;strong&gt;MiniMax M2.7&lt;/strong&gt;&lt;/a&gt; via GitHub Copilot as the agent that builds the entire application through InsForge Agent Skills and &lt;a href="https://docs.insforge.dev/mcp-setup" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stitch.withgoogle.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Stitch&lt;/strong&gt;&lt;/a&gt; for generating the design reference before the agent builds&lt;/li&gt;
&lt;li&gt;Deployment triggered from inside GitHub Copilot, with no manual steps outside the editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you will have a working social platform template you can fork and adapt to whatever you are building next. &lt;/p&gt;

&lt;p&gt;Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What Is InsForge&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;InsForge is an open-source backend platform that bundles a Postgres database, a REST API layer via PostgREST, an AI model gateway that routes to any OpenRouter-compatible model, a real-time pub/sub system, serverless edge functions, and a CLI, all into a single deployable platform. You can self-host it with Docker or use the managed cloud. You bring the application logic. InsForge handles what's underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Are Using InsForge For
&lt;/h3&gt;

&lt;p&gt;Three things in particular make InsForge the right fit for a project like this one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent Skills:&lt;/strong&gt; When you run &lt;code&gt;insforge create&lt;/code&gt;, the CLI installs a &lt;code&gt;.agents/&lt;/code&gt; folder into your project. That folder contains the InsForge SDK documentation, API patterns, and auth setup in a format the agent can read directly. Before the agent writes a single file, it reads that folder. This is why the build prompt can stay short. The agent already knows how to talk to InsForge before you type anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI gateway:&lt;/strong&gt; InsForge manages the AI provider keys automatically on the backend, you don’t need to put an OpenRouter key in your frontend &lt;code&gt;.env&lt;/code&gt; file. From there, any AI call in your frontend app using the SDK hits one InsForge endpoint and passes a model string. To swap models, you simply change that string; nothing else in the codebase needs to be touched. The backend gateway securely routes the request through OpenRouter, supporting models from OpenAI, Anthropic, Google, DeepSeek, X-AI, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhtj6gl7l3zjoszv12f3x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhtj6gl7l3zjoszv12f3x.png" alt="InsForge AI Gateway" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The PostgREST layer:&lt;/strong&gt; Every table in your InsForge database is automatically a REST endpoint. The agent writes queries against the InsForge SDK. There is no data access layer to build, no custom API routes to wire up. You describe the schema, and the endpoints are there.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Setting Up the Project&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Every InsForge project starts with the CLI. Install it once globally, log in, and you are set for every project you build after this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @insforge/cli
insforge login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a one-time setup. From here on, every time you want to start a new project, you create a folder and run &lt;code&gt;insforge create&lt;/code&gt; inside it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;ripple
&lt;span class="nb"&gt;cd &lt;/span&gt;ripple
insforge create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI asks you to pick a template. We picked &lt;strong&gt;Next.js&lt;/strong&gt;. After that, it installs the Agent Skills, writes &lt;code&gt;skills-lock.json&lt;/code&gt;, and asks if you want to set up deployment now. Say no for now. We will come back to that at the end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd8kkquqshvm2vbi8iync.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd8kkquqshvm2vbi8iync.png" alt="CLI" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more thing, before you start building, install the MCP server. The quickest way is through the InsForge VS Code extension. Install it from the marketplace, and it will show you a one-click option to connect the MCP. Once done, you will see the &lt;strong&gt;MCP Connected&lt;/strong&gt; indicator in the top-right corner of your &lt;a href="https://insforge.dev/" rel="noopener noreferrer"&gt;InsForge dashboard&lt;/a&gt;, and your agent is ready to act.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3zng9nidba7pxeumyl0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3zng9nidba7pxeumyl0.png" alt="InsForge Dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing with Google Stitch
&lt;/h2&gt;

&lt;p&gt;Before writing a single prompt, we used Google Stitch to design the UI for Ripple. We used this prompt to get started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Build a social media app called Ripple. Amber gold (#F59E0B) as the primary color.
Screens: feed, composer, profile, notifications, Wave AI chat, auth screens.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stitch exports a &lt;code&gt;design.md&lt;/code&gt; file with the full design system, colors, typography, component structure, and screen layouts. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fch10dpper9k6edzpx517.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fch10dpper9k6edzpx517.png" alt="Google Stitch" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy that file and save it in your project root in VS Code. When you reference it in your agent prompt, the agent has all the visual context it needs upfront, so you are not going back and forth on colors or layout later.&lt;/p&gt;

&lt;p&gt;With the design in place, we had everything the agent needed to build the UI and wire the backend in a single pass. Time to write the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the App
&lt;/h2&gt;

&lt;p&gt;We used GitHub Copilot as the agent, running MiniMax M2.7*&lt;em&gt;,&lt;/em&gt;* because it handles long multi-step tasks well and stays on track across a full project build. We gave it one prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Build a social media app called Ripple using InsForge as the backend platform.
Use InsForge MCP Server for all operations.

Features:
&lt;span class="p"&gt;-&lt;/span&gt; Auth: sign up, login with name, @handle, email, password
&lt;span class="p"&gt;-&lt;/span&gt; Feed: post (called Ripple) with text + image/video upload, like (Wave),
  repost (Spread), reply, bookmark
&lt;span class="p"&gt;-&lt;/span&gt; Realtime feed updates
&lt;span class="p"&gt;-&lt;/span&gt; Post composer with draft save
&lt;span class="p"&gt;-&lt;/span&gt; Profile page with cover, avatar, bio, followers/following
&lt;span class="p"&gt;-&lt;/span&gt; Notifications: likes, replies, follows, mentions
&lt;span class="p"&gt;-&lt;/span&gt; Explore: trending topics, suggested users
&lt;span class="p"&gt;-&lt;/span&gt; Wave AI: chat interface connected to InsForge AI gateway via OpenRouter
&lt;span class="p"&gt;-&lt;/span&gt; Wave AI has collapsible right panel with chat history and bookmarks
&lt;span class="p"&gt;-&lt;/span&gt; Deploy on InsForge

Follow the design system in design.md for colors, typography, and components.
Use InsForge for all backend. Read .agents folder for skills.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before touching any file, the agent read &lt;code&gt;.agents/skills/insforge/&lt;/code&gt; to understand the InsForge SDK, then laid out the full database schema &lt;code&gt;profiles&lt;/code&gt;, &lt;code&gt;ripples&lt;/code&gt;, &lt;code&gt;waves&lt;/code&gt;, &lt;code&gt;spreads&lt;/code&gt;, &lt;code&gt;follows&lt;/code&gt;, &lt;code&gt;notifications&lt;/code&gt;, &lt;code&gt;drafts&lt;/code&gt;, &lt;code&gt;ai_chat_history&lt;/code&gt;, and more, and created a build plan for itself. Only after that did it start writing code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkr7cufwfszju1x2pozj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqkr7cufwfszju1x2pozj.gif" alt="Vs code IDE" width="600" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The file structure was produced in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ripple/
├── src/app/
│   ├── feed/page.tsx
│   ├── profile/[handle]/page.tsx
│   ├── notifications/page.tsx
│   ├── ripple/[id]/page.tsx
│   └── wave/page.tsx
├── src/components/
│   ├── ripple/RippleCard.tsx
│   └── ripple/RippleComposer.tsx
├── src/lib/
│   ├── insforge.ts
│   └── auth-context.tsx
└── .agents/
    └── skills/insforge/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What is worth noticing here is that &lt;code&gt;insforge.ts&lt;/code&gt; is not a custom wrapper; the agent read the skill and knew exactly how to initialize the InsForge client.&lt;/p&gt;

&lt;p&gt;Same with &lt;code&gt;auth-context.tsx&lt;/code&gt;: it wired sessions directly to InsForge Auth without any manual setup from us. Now, all of this came from one prompt. But what the agent actually built inside each of these files, how it handled auth sessions, how it wired realtime, how AI talks to the InsForge gateway, that is where things get interesting. So let’s see exactly what agent built inside each of those files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What InsForge Handled, Feature by Feature
&lt;/h2&gt;

&lt;p&gt;Let's start with auth, since that is what everything else depends on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth
&lt;/h3&gt;

&lt;p&gt;Authentication in Ripple runs entirely through InsForge Auth: sign up, email verification, login, and session management. All the state stays in a single React Context, the agent is generated and wired up in one pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33jfhmianw30ltara04k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33jfhmianw30ltara04k.png" alt="Auth via InsForge" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sign-up is a two-step flow. The agent calls &lt;code&gt;insforge.auth.signUp()&lt;/code&gt;, checks if email verification is required, and stores the pending profile in &lt;code&gt;localStorage&lt;/code&gt; until the OTP is confirmed. Once verified, it inserts the &lt;code&gt;profiles&lt;/code&gt; record using the authenticated user ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;requireEmailVerification&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripple_pending_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;requireEmailVerification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// after OTP confirmed&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pendingName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pendingHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signup page tracks a &lt;code&gt;step&lt;/code&gt; state that switches between the form and the verify screen. Login is simpler, one call to &lt;code&gt;insforge.auth.signInWithPassword()&lt;/code&gt; and the session is set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkf9zmxvzyq334tntk5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frkf9zmxvzyq334tntk5x.png" alt="Our App UI" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;With auth out of the way, the agent moved on to the database. InsForge runs on PostgreSQL under the hood, and every table gets automatically exposed as a REST endpoint through PostgREST, which means the agent never had to write a single custom API route.&lt;/p&gt;

&lt;p&gt;The schema the agent built covers the full surface area of the app. The core tables are &lt;code&gt;profiles&lt;/code&gt;, &lt;code&gt;ripples&lt;/code&gt;, &lt;code&gt;ripples_media&lt;/code&gt;, &lt;code&gt;waves&lt;/code&gt; (likes), &lt;code&gt;spreads&lt;/code&gt; (reposts), &lt;code&gt;follows&lt;/code&gt;, &lt;code&gt;notifications&lt;/code&gt;, and &lt;code&gt;ai_chat_history&lt;/code&gt; for the Wave AI sessions. The &lt;code&gt;notifications&lt;/code&gt; table also has a Postgres trigger attached to it, so every insert immediately fires a real-time broadcast over the WebSocket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F713zjf3twqnawhay69rn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F713zjf3twqnawhay69rn.png" alt="InsForge DB" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- setup_trigger.sql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;notify_new_notification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="k"&gt;TRIGGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;PERFORM&lt;/span&gt; &lt;span class="n"&gt;pg_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'new_notification'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'user_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'type'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'actor_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'ripple_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ripple_id&lt;/span&gt;
    &lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt; &lt;span class="k"&gt;LANGUAGE&lt;/span&gt; &lt;span class="n"&gt;plpgsql&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because PostgREST understands foreign keys, the agent could request deeply nested relational data in a single query rather than chaining multiple fetches. Here is the feed query from &lt;code&gt;page.tsx&lt;/code&gt;, which pulls ripples alongside their author profiles, attached media, waves, and spreads in one request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// page.tsx — fetching the main feed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripples&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    *,
    profiles (*),
    ripples_media (*),
    waves (*),
    spreads (*)
  `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reply_to&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ascending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;profiles (*)&lt;/code&gt; in the select string is where PostgREST detects the foreign key between &lt;code&gt;ripples.user_id&lt;/code&gt; and &lt;code&gt;profiles.id&lt;/code&gt; and performs the join automatically on the backend, returning the author's data nested inside each post object. The same pattern applies to &lt;code&gt;waves&lt;/code&gt; and &lt;code&gt;spreads&lt;/code&gt;, so the UI always knows the engagement state of a post without a second request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;

&lt;p&gt;The database takes care of structured data, but for every file, avatars, cover photos, and post media, the agent provisioned three separate InsForge Storage buckets: &lt;code&gt;avatars&lt;/code&gt;, &lt;code&gt;covers&lt;/code&gt;, and &lt;code&gt;ripples&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t1vrdrzewul58kf6ogw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t1vrdrzewul58kf6ogw.png" alt="Storage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;InsForge Storage is S3-compatible and sits natively beside the auth and database layers, so the SDK handles uploads, hashing, and public URL generation in a single call without any custom middleware.&lt;/p&gt;

&lt;p&gt;For profile photos, the agent used &lt;code&gt;.uploadAuto()&lt;/code&gt;, which takes a &lt;code&gt;File&lt;/code&gt; object and returns a public URL directly. After each upload resolves, it immediately writes that URL back to the &lt;code&gt;profiles&lt;/code&gt; table in the database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// profile/edit/page.tsx&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avatars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadAuto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;avatarUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;covers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadAuto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;coverUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;avatarUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cover_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;coverUrl&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For post media, the pattern is slightly different because a single ripple can carry up to four attached images. The agent wrote a loop that runs after the post row is created, uploads each file to the &lt;code&gt;ripples&lt;/code&gt; bucket, and inserts a matching record into &lt;code&gt;ripples_media&lt;/code&gt; that binds the file URL back to the post's ID.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// RippleComposer.tsx&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;media&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripples&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadAuto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;uploadData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripples_media&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
    &lt;span class="na"&gt;ripple_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ripple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;uploadData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Realtime
&lt;/h3&gt;

&lt;p&gt;With the database writing data correctly, the next thing the app needed was for that data to reach every connected client without a page refresh. InsForge Realtime runs on WebSockets, and the agent wired up four live channels: &lt;code&gt;ripples&lt;/code&gt; for the global feed, &lt;code&gt;ripples_media&lt;/code&gt; for media updates, &lt;code&gt;trending_topics&lt;/code&gt; for live topic changes, and &lt;code&gt;notifications:%&lt;/code&gt; for per-user alerts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqehzgcplbordl4cvu4l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxqehzgcplbordl4cvu4l.png" alt="InsForge Realtime" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The feed component subscribes to &lt;code&gt;ripples&lt;/code&gt; on mount and handles two event types: &lt;code&gt;INSERT&lt;/code&gt; for new posts and &lt;code&gt;UPDATE&lt;/code&gt; for engagement count changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// page.tsx — feed subscription&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripples&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newRipple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Ripple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;newRipple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reply_to&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ripples&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*, profiles (*), ripples_media (*), waves (*), spreads (*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newRipple&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setRipples&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Ripple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;realtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Ripple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setRipples&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
          &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;wave_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wave_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spread_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spread_count&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;INSERT&lt;/code&gt; payload only carries the raw new row, so the agent does a quick &lt;code&gt;.select()&lt;/code&gt; with joins before pushing it into the state, same pattern as the feed query from the Database section. The &lt;code&gt;UPDATE&lt;/code&gt; handler just patches the counts in-place without refetching the full post.&lt;/p&gt;

&lt;p&gt;For notifications, the Postgres trigger from &lt;code&gt;setup_trigger.sql&lt;/code&gt; does the broadcasting on the database side. When a new row hits the &lt;code&gt;notifications&lt;/code&gt; table, the trigger publishes directly to &lt;code&gt;notifications:{user_id}&lt;/code&gt; over the WebSocket, and &lt;code&gt;realtime-context.tsx&lt;/code&gt; picks it up on the frontend to increment the bell count instantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7gxqebesoolofrvjvag.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz7gxqebesoolofrvjvag.png" alt="Realtime Architecture" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;AI Gateway&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;After real-time, the agent focused on the AI gateway. Ripple comes with an in-app AI called Wave AI. Instead of connecting directly to OpenAI or Anthropic, dealing with separate billing, and worrying about exposing keys on the frontend, it uses InsForge's AI gateway. This gateway manages routing, authentication, and model access all in one place.&lt;/p&gt;

&lt;p&gt;The gateway call follows the same shape as the OpenAI SDK, so it feels familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anthropic/claude-sonnet-4-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are Wave AI, a helpful assistant on the Ripple social platform. You are talking to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;pastMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swapping the model is one line, change &lt;code&gt;"anthropic/claude-sonnet-4-5"&lt;/code&gt; to &lt;code&gt;"openai/gpt-4o"&lt;/code&gt; or any other model the gateway supports, and nothing else changes.&lt;/p&gt;

&lt;p&gt;One thing the AI gateway doesn't do on its own is remember past conversations. Every time you call &lt;code&gt;insforge.ai.chat.completions.create&lt;/code&gt;, it only knows what's in the &lt;code&gt;messages&lt;/code&gt; array you pass. Close the tab and that's gone.&lt;/p&gt;

&lt;p&gt;So the agent added two database tables, &lt;code&gt;ai_chat_sessions&lt;/code&gt; to group conversations, and &lt;code&gt;ai_chat_history&lt;/code&gt; to store individual messages. Every time the user sends a message, it gets written to the database. Every time Wave AI replies, that gets written too. When you come back to the &lt;code&gt;/ai&lt;/code&gt; page later, a &lt;code&gt;useEffect&lt;/code&gt; fetches that session and loads all the messages back in. The conversation picks up exactly where it left off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucde2vn8mib7j582fli3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucde2vn8mib7j582fli3.gif" alt="Wave AI Demo" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With auth, database, storage, realtime, and Wave AI all working, the app is ready to run, and it's time to test it locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Running Locally&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before deploying, run the app locally to make sure everything works end-to-end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;install&lt;/span&gt;
&lt;span class="nx"&gt;npm&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:3000&lt;/code&gt;, sign up with a test account, go through the full flow, auth, posting a Ripple, checking the feed. If your InsForge credentials are in &lt;code&gt;.env.local&lt;/code&gt;, everything should work on the first run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Finsforge.dev%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fd1yrmc4hue7p9m.cloudfront.net%252Fassets%252Fimages%252Fai-social-media-app-insforge-minimax%252Frunning-locally.gif%26w%3D828%26q%3D75" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Finsforge.dev%2F_next%2Fimage%3Furl%3Dhttps%253A%252F%252Fd1yrmc4hue7p9m.cloudfront.net%252Fassets%252Fimages%252Fai-social-media-app-insforge-minimax%252Frunning-locally.gif%26w%3D828%26q%3D75" alt="Demo 2" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: InsForge takes care of all the backend wiring, but the frontend is yours to shape. I made a few tweaks to the UI, adjusting the layout and refining some interactions, to make it feel more like my own, and that is the nice part: the backend is handled, so you can spend your time on the experience instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying from GitHub Copilot
&lt;/h2&gt;

&lt;p&gt;Once everything worked locally, deploying took one prompt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Deploy to InsForge.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent read the project config, picked up the credentials, and called the &lt;code&gt;create-deployment&lt;/code&gt; MCP tool, all from inside Copilot. No browser dashboard, no separate deploy config.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffntmg0jwbop8lospaw0x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffntmg0jwbop8lospaw0x.gif" alt="IDE chat" width="1152" height="648"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It zipped the source, uploaded it, and InsForge ran &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt; in a container with the environment variables injected. The live URL came back in the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;At this point, you have a fully working social platform running on InsForge. Auth, a real-time feed, media uploads, notifications, and an AI assistant, all live and deployable from inside GitHub Copilot.&lt;/p&gt;

&lt;p&gt;From here, the project is yours to extend. Swap the AI model by updating one value in the InsForge dashboard. Replace the schema entirely, and the same SDK patterns, the same Agent Skills, and the same deployment flow all carry forward to whatever you build next. You can fork the &lt;a href="https://github.com/Studio1HQ/insforge" rel="noopener noreferrer"&gt;&lt;strong&gt;project repo&lt;/strong&gt;&lt;/a&gt; and start from there.&lt;/p&gt;

&lt;p&gt;To learn more about InsForge, check out the &lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub repo&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Managing Multi Provider AI Workflows in the Terminal with Bifrost CLI</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Sat, 21 Mar 2026 10:52:18 +0000</pubDate>
      <link>https://dev.to/studio1hq/managing-multi-provider-ai-workflows-in-the-terminal-with-bifrost-cli-ece</link>
      <guid>https://dev.to/studio1hq/managing-multi-provider-ai-workflows-in-the-terminal-with-bifrost-cli-ece</guid>
      <description>&lt;p&gt;Command-line tools are still a common way to work with AI. They give better control and fit naturally into everyday workflows, which is why many people continue to use them.&lt;/p&gt;

&lt;p&gt;A common issue with CLI-based tools is that they are often tied to a single provider. Switching between options usually means updating configs and handling multiple API keys. In some cases, it may even involve changing tools. This can slow things down and make everyday work feel a bit frustrating.&lt;/p&gt;

&lt;p&gt;Bifrost CLI aims to simplify this setup. It provides a single way to connect CLI tools to multiple providers, without changing how the tools are used. In this article, let us look at how it works and how to get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Bifrost CLI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.getmaxim.ai/bifrost" rel="noopener noreferrer"&gt;Bifrost&lt;/a&gt; is an &lt;a href="https://github.com/maximhq/bifrost" rel="noopener noreferrer"&gt;open-source AI gateway&lt;/a&gt; that works between applications and model providers. It offers provider-compatible endpoints such as OpenAI, Anthropic, and Gemini formats. It manages request routing, API keys, and response formatting in one place, so separate setups for each provider are not required.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.getbifrost.ai/quickstart/cli/getting-started" rel="noopener noreferrer"&gt;Bifrost CLI&lt;/a&gt; was recently released to extend this setup to command-line workflows. It allows existing CLI tools to connect through the Bifrost gateway in place of calling providers directly. The CLI tool continues to work in the same way, with only the endpoint updated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq1juk7uh66enws74o00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvq1juk7uh66enws74o00.png" alt="Bitfrost CLI" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The CLI tool is configured with Bifrost as the base URL. After this, all requests go through the gateway. Bifrost routes each request to the selected provider, converts it into the required API format, and returns a compatible response. The CLI workflow stays the same, with support for multiple providers through a single endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features of Bifrost CLI
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI brings several practical features that improve how CLI-based workflows are set up and managed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic Setup for CLI Tools:&lt;/strong&gt; Configures base URLs, API keys, and model settings for each agent. This reduces manual steps and keeps the environment ready to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Discovery from Gateway:&lt;/strong&gt; Fetches available models directly from the Bifrost gateway using the &lt;code&gt;/v1/models&lt;/code&gt; endpoint. This ensures the CLI always reflects the current set of available options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Integration for Tool Access:&lt;/strong&gt; Attaches Bifrost’s MCP server to tools like Claude Code. This allows access to external tools and extended capabilities from within the CLI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Activity Indicators:&lt;/strong&gt; Displays activity badges for each tab. It becomes easy to see if a session is running, idle, or has triggered an alert.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure Credential Storage:&lt;/strong&gt; Stores selections and keys securely. Virtual keys are saved in the OS keyring and are not written in plain text on disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI is quick to set up and runs directly from the terminal. The flow includes starting the gateway, launching the CLI, and selecting the agent and model through a guided setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Start the Bifrost Gateway
&lt;/h3&gt;

&lt;p&gt;Make sure the gateway is running locally (default: &lt;code&gt;http://localhost:8080&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @maximhq/bifrost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Install and Launch Bifrost CLI
&lt;/h3&gt;

&lt;p&gt;In a new terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; @maximhq/bifrost-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk8d7qf1ycaa6vmnnnu1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhk8d7qf1ycaa6vmnnnu1.png" alt="Terminal" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If installed, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bifrost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Enter Gateway Details
&lt;/h3&gt;

&lt;p&gt;Provide the Bifrost endpoint URL.&lt;/p&gt;

&lt;p&gt;For local setup, this is usually: &lt;code&gt;http://localhost:8080&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If authentication is enabled, you can also enter a virtual key at this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Choose a CLI Agent
&lt;/h3&gt;

&lt;p&gt;Select the CLI agent you want to use, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Codex CLI&lt;/li&gt;
&lt;li&gt;Claude Code&lt;/li&gt;
&lt;li&gt;Gemini CLI&lt;/li&gt;
&lt;li&gt;Opencode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CLI shows which agents are available and can install missing ones during setup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzg89ikeylge3qwp6wpyc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzg89ikeylge3qwp6wpyc.png" alt="CLI UI" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Select a Model
&lt;/h3&gt;

&lt;p&gt;The CLI fetches available models from the gateway and shows them in a searchable list.&lt;/p&gt;

&lt;p&gt;You can choose one directly or enter a model name manually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsicwoqe9jep2tpd8jt9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqsicwoqe9jep2tpd8jt9.png" alt="Choose model name" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Launch the Session
&lt;/h3&gt;

&lt;p&gt;Review the configuration and start the session. The selected agent runs with the chosen model and setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Work with Sessions
&lt;/h3&gt;

&lt;p&gt;After launch, the CLI stays open in a tabbed interface.&lt;/p&gt;

&lt;p&gt;You can open new sessions, switch between them, or close them without restarting the CLI. Each tab shows the current activity state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Bifrost CLI Session Flow
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI is built for repeated, session-based use in the terminal. You can switch between runs, update settings, and continue your work without having to go through the full setup again each time. &lt;/p&gt;

&lt;p&gt;Here are the key steps in the session flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3226ybyduzng3dmsakr0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3226ybyduzng3dmsakr0.png" alt="Session flow" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Launch:&lt;/strong&gt; Select the agent and model, then start the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Work:&lt;/strong&gt; Use the agent as usual. All requests go through Bifrost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch Sessions:&lt;/strong&gt; Press &lt;code&gt;Ctrl + B&lt;/code&gt; to open the tab bar, switch between sessions, or start a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Return:&lt;/strong&gt; When a session ends, the CLI returns to the setup screen with the previous configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relaunch:&lt;/strong&gt; Change the agent or model, or rerun the same setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence:&lt;/strong&gt; The last configuration is saved and shown the next time the CLI starts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Working with Multiple Models
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI makes it easy to work with different models from the same setup. You do not need to change configurations or restart the tool each time you want to try a different option.&lt;/p&gt;

&lt;p&gt;During setup, the CLI fetches available models from the Bifrost gateway and shows them in a list. You can select one directly or enter a model name if you already know what you want to use.&lt;/p&gt;

&lt;p&gt;If you want to try another model, you can start a new session and choose a different one. Each session runs separately, so you can compare outputs or test different setups side by side.&lt;/p&gt;

&lt;p&gt;All requests go through Bifrost, so differences between providers are handled in the background. The CLI experience stays the same across models.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Bifrost CLI
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI is useful when working with multiple providers or running repeated sessions from the terminal. Since it is built on top of Bifrost, it also brings the benefits of a central gateway into CLI workflows.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing Different Models:&lt;/strong&gt; Try different models across providers from the same setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running Iterative Sessions:&lt;/strong&gt; Start, stop, and relaunch sessions with minor configuration changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working from the Terminal:&lt;/strong&gt; Keep the entire workflow inside the CLI, with Bifrost handling routing in the background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparing Outputs:&lt;/strong&gt; Run multiple sessions side by side and observe how different models respond.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managing Multiple Providers:&lt;/strong&gt; Use Bifrost as a single entry point to work across providers in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized Control with Bifrost:&lt;/strong&gt; Route all requests through Bifrost for consistent handling of API keys, requests, and responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup helps keep workflows consistent and organized across different providers and sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Bifrost CLI brings multi-provider access into the terminal through a single setup. It keeps existing workflows intact and reduces the need to manage separate configurations.&lt;/p&gt;

&lt;p&gt;You can run sessions, switch agents, and try different models from the same interface, with Bifrost handling routing and integration in the background.&lt;/p&gt;

&lt;p&gt;To get started or explore more details, check the &lt;a href="https://docs.getbifrost.ai/quickstart/cli/getting-started" rel="noopener noreferrer"&gt;Bifrost CLI documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why Your OpenClaw Agent Gets Slower and More Expensive Over Time</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Fri, 20 Mar 2026 21:00:34 +0000</pubDate>
      <link>https://dev.to/studio1hq/why-your-openclaw-agent-gets-slower-and-more-expensive-over-time-5c5e</link>
      <guid>https://dev.to/studio1hq/why-your-openclaw-agent-gets-slower-and-more-expensive-over-time-5c5e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;OpenClaw feels fast in the first week. You send a message, the agent responds, and the workflow makes sense. Then gradually, without any obvious change, responses take a little longer, and the API bill at the end of the month is higher than it was two weeks ago, with no single thing you can point to as the cause.&lt;/p&gt;

&lt;p&gt;That is not a coincidence, and it is not bad luck. It is what happens when three separate problems compound on each other quietly, over time, without any of them being obvious on its own.&lt;/p&gt;

&lt;p&gt;Context bloating, static content being reprocessed on every call, and every request hitting the same model regardless of what it actually needs, these are not dramatic failures. They are the kind of inefficiencies that feel invisible until they are not, and by the time the invoice makes them obvious, they have been running for weeks.&lt;/p&gt;

&lt;p&gt;In this post, we will break down what is driving each of them and why routing, not prompt tuning or model switching, is the fix that addresses all three at the layer where they actually live.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why the Default Setup Works Against You Over Time&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;OpenClaw's default configuration is built to get you started. It is not designed to remain efficient as your usage grows, and the gap between the two becomes apparent faster than most people expect. Three things are responsible for most of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Context grows faster than you think&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Before you type a single message, your agent has already loaded a significant amount into the context window. &lt;code&gt;SOUL.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, bootstrap files, the results of a memory search against everything you have accumulated, all of it lands in the prompt before your request even starts.&lt;/p&gt;

&lt;p&gt;That base footprint is manageable in week one. By week three, the memory graph has grown, the search results are broader, and the conversation history from your previous sessions is traveling with every new request. The agent is not selectively pulling relevant data; it loads everything it has access to every time.&lt;/p&gt;

&lt;p&gt;The result is a base token cost per request that is meaningfully higher than it was when you started, without any deliberate change on your part.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Static tokens are processed fresh every time&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A large portion of what is loaded into every request consists of content that has not changed since last week, system instructions, bootstrap files, and agent configuration. Provider-side caching exists specifically to avoid paying full price for static content on repeat calls, but the default OpenClaw setup does not use it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zje4v0xidwlrqxrneqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zje4v0xidwlrqxrneqi.png" alt="Every call. Same cost. No cache" width="800" height="446"&gt;&lt;/a&gt; The same unchanged content, reprocessed from scratch on every heartbeat call.&lt;/p&gt;

&lt;p&gt;Every call processes that unchanged content from scratch. For a setup running a 30-minute heartbeat, that means a full API call with no caching, hitting the configured model, every half hour, regardless of whether anything meaningful is happening in the session. Most users never think of the heartbeat as a cost source, but over a full month, it adds up to a figure worth paying attention to.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Every request hits the same model&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;OpenClaw routes all requests to a single globally configured model. There is no built-in distinction among task types: a status check, a memory lookup, a formatting task, and a multi-step reasoning problem all map to the same endpoint at the same price.&lt;/p&gt;

&lt;p&gt;In practice, the majority of what an agent handles day-to-day is simple work. Summaries, lookups, structured output, short responses. None of it requires a frontier model, but all of it gets one anyway. That is not a usage problem; it is a configuration gap, and it is the highest-leverage thing to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Structural Fix: Routing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The problem with every approach people try first, switching to a cheaper model, trimming prompts, and reducing heartbeat frequency, is that they address one variable at a time. The bill declines slightly, then rises again. What is needed is a layer that sits between OpenClaw and the provider, evaluates each request before it is sent, and determines which model to route it to. That is what routing is, and that is why it is a structural fix rather than a configuration tweak.&lt;/p&gt;

&lt;p&gt;That layer is &lt;a href="https://manifest.build/" rel="noopener noreferrer"&gt;Manifest&lt;/a&gt;, an open-source OpenClaw plugin built specifically to solve this. It sits between your agent and the provider, and the original OpenClaw configuration remains unchanged.&lt;/p&gt;

&lt;p&gt;Manifest intercepts every request before it reaches the LLM. The routing decision takes under 2 ms with zero external calls, after which the request is forwarded to the appropriate model. During that interval, five distinct mechanisms run before the request moves anywhere, starting with how the scoring algorithm decides which tier a request belongs to.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;How the scoring algorithm works&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Before any request leaves your setup, Manifest runs a scoring pass across 23 dimensions. These dimensions fall into two groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;13 keyword-based checks that scan the prompt for patterns like "prove", "write function", or "what is", and&lt;/li&gt;
&lt;li&gt;10 structural checks that evaluate token count, nesting depth, code-to-prose ratio, tool count, and conversation depth, among others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each dimension carries a weight. The weighted sum maps to one of four tiers through threshold boundaries. Alongside the tier assignment, Manifest produces a confidence score between 0 and 1 that reflects how clearly the request fits that tier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe211zoxmig3h19y1wzrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe211zoxmig3h19y1wzrg.png" alt="Manifest scores" width="800" height="450"&gt;&lt;/a&gt; How Manifest scores a request across 23 dimensions and assigns it a tier in under 2 ms.&lt;/p&gt;

&lt;p&gt;One edge case worth knowing: short follow-up messages like "yes" or "do it" do not get scored in isolation. Manifest tracks the last 5 tier assignments within a 30-minute window and uses that session momentum to keep follow-ups at the right tier, rather than dropping them to simple because they contain almost no content.&lt;/p&gt;

&lt;p&gt;Certain signals also force a minimum tier regardless of score. Detected tools push the floor to the standard. Context above 50,000 tokens forces complex. Formal logic keywords move the request directly to reasoning.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The four tiers and what they route&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The tier system is where the cost reduction actually happens. Manifest defines four tiers, each mapped to a different class of model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple:&lt;/strong&gt; greetings, definitions, short factual questions. Routed to the cheapest model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard:&lt;/strong&gt; general coding help, moderate questions. Good quality at low cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex:&lt;/strong&gt; multi-step tasks, large context, code generation. Best quality models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning:&lt;/strong&gt; formal logic, proofs, math, multi-constraint problems. Reasoning-capable models only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a typical active session, most requests fall into the simple or standard category. Routing those away from frontier models, while sending only what genuinely needs it to complex or reasoning, is where the up to 70% cost reduction reported by users comes from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtsw09jblpxao5q4zcco.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxtsw09jblpxao5q4zcco.png" alt="Manifest maps each request type" width="800" height="450"&gt;&lt;/a&gt; How Manifest maps each request type to the cheapest model that can handle it.&lt;/p&gt;

&lt;p&gt;Every routed response returns three headers you can inspect: &lt;code&gt;X-Manifest-Tier&lt;/code&gt;, &lt;code&gt;X-Manifest-Model&lt;/code&gt;, and &lt;code&gt;X-Manifest-Confidence&lt;/code&gt;. If a request was routed differently than you expected, those headers tell you exactly what the algorithm saw.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;OAuth and provider auth&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Manifest lets users authenticate with their own Anthropic or OpenAI credentials directly through OAuth. If OAuth is unavailable or a session is inactive, it falls back to an API key. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fre8deeyllgjry2faztj6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fre8deeyllgjry2faztj6.gif" alt="Manifest Auth" width="600" height="337"&gt;&lt;/a&gt; Manifest lets users authenticate with their own Anthropic or OpenAI credentials&lt;/p&gt;

&lt;p&gt;This keeps your model access under your own account, which matters for rate limits, spend visibility, and not routing your traffic through a third-party proxy. More providers are being added.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Fallbacks and what they protect&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each tier supports up to 5 fallback models. If the primary model for a tier is unavailable or rate-limited, Manifest automatically moves to the fallback chain. The request still resolves, just against the next available model in that tier's list. This is particularly relevant for the reasoning tier, where model availability can be less predictable during high-traffic periods, and losing a request entirely is more costly than a slight capability downgrade.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Spend limits without manual monitoring&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Manifest lets you set rules per agent against two metrics: tokens and cost. Each rule has a period (hourly, daily, weekly, or monthly), a threshold, and an action. Notify sends an email alert when the threshold is crossed. Block returns HTTP 429 and stops requests until the period resets.&lt;/p&gt;

&lt;p&gt;Rules that block are evaluated on every ingest, while rules that notify run on an hourly cron and fire once per rule per period to avoid repeated alerts for the same breach. For a setup with a 30-minute heartbeat running continuously, a daily cost block is the most direct way to prevent a runaway spend event from compounding overnight without any manual check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rest Is Worth Knowing
&lt;/h2&gt;

&lt;p&gt;Routing is the core of what Manifest does, but it ships with a few other things that are worth understanding before you use it in production.&lt;/p&gt;

&lt;p&gt;Manifest provides a dashboard that gives a full view of each call: input tokens, output tokens, cache-read tokens, cost, latency, model, and routing tier. Cost is calculated against a live pricing table covering 600+ models, so nothing is estimated. The message log stores all requests and is filterable by agent, model, and time range.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfc9ccmp7jszkhuo2ik6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfc9ccmp7jszkhuo2ik6.png" alt="Manifest dashboard" width="800" height="450"&gt;&lt;/a&gt; Manifest dashboard&lt;/p&gt;

&lt;p&gt;In local mode, nothing leaves your machine. In cloud mode, only OpenTelemetry metadata is sent: model name, token counts, and latency. Message content never moves. The full codebase is open source and self-hostable at &lt;a href="https://github.com/mnfst/manifest" rel="noopener noreferrer"&gt;github.com/mnfst/manifest&lt;/a&gt;, and the routing logic is fully documented.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A quick note before we move on.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everything in this post reflects how Manifest works at the time of writing, and the space is moving fast enough that some details may already look different by the time you read it. The OAuth providers, the supported models, the scoring thresholds, and the team were shipping changes even while this article was being written. For anything that has moved since, the &lt;a href="https://manifest.build/docs/introduction" rel="noopener noreferrer"&gt;docs&lt;/a&gt; are the right place to check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With that said, back to the article. Here is how all of it fits together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;The three problems do not take turns. They compound on the same request, every time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8605ni77vqje298aahx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg8605ni77vqje298aahx.png" alt="Three problems" width="800" height="450"&gt;&lt;/a&gt; Three problems converging into every single request, all at once.&lt;/p&gt;

&lt;p&gt;A heartbeat call on a 30-minute cycle loads accumulated context, reprocesses unchanged system files, and hits a frontier model for a task that needed none of that. Week one is a small number. In week three, it is a pattern you cannot see until the invoice lands.&lt;/p&gt;

&lt;p&gt;Routing is the layer that addresses all three at once, not because it solves context or caching directly, but because it changes the cost of every request before it leaves your setup, and once that layer is in place, the three problems no longer have room to compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Where to Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The order matters here. Do not start by switching models or trimming prompts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Manifest and let it run for a few days without changing anything else. The dashboard will show you where the cost is actually coming from.&lt;/li&gt;
&lt;li&gt;Check the model distribution. If simple and standard requests are hitting your highest-tier model, routing is the first thing to configure.&lt;/li&gt;
&lt;li&gt;Set a daily cost block rule to prevent a runaway session from compounding overnight.&lt;/li&gt;
&lt;li&gt;Once routing is active, the cache read token metric indicates how much static content was served from cache versus processed fresh. That number is worth watching.&lt;/li&gt;
&lt;li&gt;Add per-tier fallbacks to prevent availability gaps from interrupting the session.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://manifest.build/docs/introduction" rel="noopener noreferrer"&gt;&lt;strong&gt;Manifest docs&lt;/strong&gt;&lt;/a&gt; cover installation, routing configuration, and limit setup in full. If you want the broader context on what makes OpenClaw production-ready, &lt;a href="https://dev.to/arindam_1729/5-openclaw-plugins-that-actually-make-it-production-ready-14kn"&gt;this post&lt;/a&gt; is a good place to start.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Production-Ready Multi-Agent Investment Committee with AgentField</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Thu, 19 Mar 2026 11:49:06 +0000</pubDate>
      <link>https://dev.to/astrodevil/building-a-production-ready-multi-agent-investment-committee-with-agentfield-md7</link>
      <guid>https://dev.to/astrodevil/building-a-production-ready-multi-agent-investment-committee-with-agentfield-md7</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This tutorial walks through building &lt;a href="https://github.com/Arindam200/awesome-ai-apps/tree/main/advance_ai_agents/agentfield_finance_research_agent" rel="noopener noreferrer"&gt;&lt;strong&gt;Argus&lt;/strong&gt;&lt;/a&gt;, a multi-agent system that performs &lt;strong&gt;automated stock research&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Using &lt;a href="https://dub.sh/agentf" rel="noopener noreferrer"&gt;&lt;strong&gt;AgentField&lt;/strong&gt;&lt;/a&gt;, agents run as modular microservices with &lt;strong&gt;typed skills and reasoners&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The architecture enables &lt;strong&gt;parallel analysis, structured workflows, and full observability&lt;/strong&gt; for production-ready AI systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Many early Agentic applications start with high-level orchestrators like LangChain or CrewAI. For simple use cases, these frameworks work well and are often the fastest way to prototype an idea.&lt;/p&gt;

&lt;p&gt;However, as the complexity of the task grows, especially when moving from a notebook to a production service, this pattern begins to break down.&lt;/p&gt;

&lt;p&gt;Traditional agent frameworks often focus on "agentic reasoning" but neglect the "production engineering." In a real-world system, you can't just have one model or orchestrator responsible for multiple stages of a workflow in a sequential, opaque loop. Failures are harder to trace because the entire workflow is coupled to a single orchestration logic. Evaluating or improving individual stages becomes difficult without a clear separation of concerns.&lt;/p&gt;

&lt;p&gt;A more robust alternative is to structure the system as a set of specialized, independent components.&lt;/p&gt;

&lt;p&gt;In this tutorial we will build &lt;a href="https://github.com/Arindam200/awesome-ai-apps/tree/main/advance_ai_agents/agentfield_finance_research_agent" rel="noopener noreferrer"&gt;&lt;strong&gt;Argus&lt;/strong&gt;&lt;/a&gt;, an AI agent system that performs stock research using a coordinated set of specialized agents. Argus operates similarly to an investment committee: multiple agents analyze the same company from different perspectives and their outputs are combined into a structured research report.&lt;/p&gt;

&lt;p&gt;Argus is built using &lt;a href="https://dub.sh/agentf" rel="noopener noreferrer"&gt;&lt;strong&gt;AgentField&lt;/strong&gt;&lt;/a&gt;, an open-source backend framework designed for building and orchestrating AI agents as production services.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Single-Prompt Systems Fail in Production
&lt;/h2&gt;

&lt;p&gt;Many early AI applications rely on a single prompt that attempts to complete an entire task in one step. This pattern is simple to prototype but introduces several issues in production systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High hallucination risk&lt;/strong&gt;: When a single prompt performs research, reasoning, and reporting at the same time, the model lacks reliable grounding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limited observability&lt;/strong&gt;: If the entire workflow occurs inside one prompt, it becomes difficult to trace how results were produced.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor separation of responsibilities&lt;/strong&gt;: Research, analysis, and synthesis happen in the same step. This makes systems difficult to maintain or extend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficult debugging&lt;/strong&gt;: Failures cannot easily be isolated to a specific stage of the workflow. For production AI systems, structured workflows provide a more stable foundation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7271gr5vatzqmhl6sb56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7271gr5vatzqmhl6sb56.png" alt="Understanding autonomous agents workflow" width="800" height="446"&gt;&lt;/a&gt; Understanding autonomous agents workflow&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AgentField?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dub.sh/agentf" rel="noopener noreferrer"&gt;AgentField&lt;/a&gt; is the &lt;strong&gt;Control Plane for AI Agents&lt;/strong&gt;. If other frameworks focus on "how an agent thinks," AgentField focuses on how agents &lt;strong&gt;run, scale, and communicate&lt;/strong&gt; in a production environment. Think of it as &lt;strong&gt;Kubernetes for AI Agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;AgentField isn't just an orchestration layer; it transforms your agents into production-ready microservices. It is built on three core pillars:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt;: The fundamental unit of deployment. Each Agent is an independent, versioned microservice with its own lifecycle, endpoints, and health monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt;: Deterministic tools (fetching APIs, database queries, file system access). Skills are strictly typed and can be exposed as standalone REST endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoners&lt;/strong&gt;: The "brains" that use LLMs to process data. Reasoners use structured data contracts (&lt;a href="https://pydantic.dev/" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt;) to ensure outputs are predictable and reliable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When these concerns are separated, AgentField provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Observability&lt;/strong&gt;: Every execution, skill call, and reasoning step is traced automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async Concurrency&lt;/strong&gt;: Native support for &lt;code&gt;asyncio&lt;/code&gt; allows agents to run in parallel without the complex state management of traditional frameworks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Hub&lt;/strong&gt;: A unified dashboard (the Control Plane) where you can monitor health, latency, and costs across multiple agent clusters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We will now have a walkthrough on how Argus works and how one can set it up following this guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 5-Agent Architecture
&lt;/h2&gt;

&lt;p&gt;Building a production AI system isn't just about the "brain", it's about the &lt;strong&gt;workflow&lt;/strong&gt;. Argus doesn't rely on a single, long-running agent. Instead, it operates as a coordinated investment committee, where specialized agents handle specific stages of the research pipeline.&lt;/p&gt;

&lt;p&gt;This multi-agent design allows for &lt;strong&gt;true concurrency&lt;/strong&gt;: while the Analyst builds the bull case, the Contrarian simultaneously hunts for risks. This parallel execution minimizes latency and ensures that each perspective is developed independently, without bias from the other.&lt;/p&gt;

&lt;p&gt;Before we write any code, let's look at the orchestration flow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj3gs5dc5wby33two5yv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdj3gs5dc5wby33two5yv.png" alt="Argus’s 5-agent investment committee working in parallel" width="800" height="446"&gt;&lt;/a&gt; Argus’s 5-agent investment committee working in parallel&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key insight&lt;/strong&gt;: The Analyst and Contrarian run in parallel. Then both Editors run in parallel. This is true concurrency via &lt;code&gt;asyncio.gather&lt;/code&gt;, not sequential execution.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;The next step is to start building the our project. We have a &lt;a href="https://github.com/Arindam200/awesome-ai-apps/tree/main/advance_ai_agents/agentfield_finance_research_agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; repository for the project, so to keep the code here concise, you can always refer to it for the complete implementation.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project Organization
&lt;/h2&gt;

&lt;p&gt;We will build Argus with a modular design. Every file has a specific responsibility. Create a directory named &lt;code&gt;argus-agentfield&lt;/code&gt; and set up the following structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;argus-agentfield/
├── .env                 # API Keys (NEBIUS_API_KEY)
├── requirements.txt     # Dependencies
├── src/
│   ├── __init__.py      # App initialization — shared Agent instance
│   ├── schemas.py       # Data contracts (Pydantic models)
│   ├── skills.py        # Deterministic data fetching (yfinance)
│   ├── reasoners.py     # Agent logic, prompts, orchestration
│   ├── stream.py        # SSE streaming pipeline + UI routes
│   └── main.py          # Server entry point
└── ui/
    └── index.html       # Single-page vanilla JS frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;requirements.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agentfield
yfinance
nebius
python-dotenv
pydantic&amp;gt;=2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install them with &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; (fast Python package manager):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; uv pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  AgentField Control Plane
&lt;/h3&gt;

&lt;p&gt;AgentField includes a local control plane that gives you a live dashboard to monitor your agents. Install the &lt;code&gt;af&lt;/code&gt; CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://agentfield.ai/get | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don’t need this to build or run Argus — the agent works fully standalone. But once you want to see workflow graphs, execution traces, and performance metrics, you can start the control plane with &lt;code&gt;af server&lt;/code&gt;. We will set this up in Section 8. After a successful installation, you can confirm it with this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;af&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ws4l237rtogx8pqukh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5ws4l237rtogx8pqukh4.png" alt="A terminal showing Agentfield CLI installation and version verification" width="800" height="574"&gt;&lt;/a&gt; A terminal showing Agentfield CLI installation and version verification&lt;/p&gt;

&lt;h2&gt;
  
  
  Initialization
&lt;/h2&gt;

&lt;p&gt;We start by creating a shared &lt;code&gt;Agent&lt;/code&gt; instance. This object manages our LLM configuration and serves as a production hub. Every agent in AgentField is a standard microservice.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/__init__.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Argus — Autonomous Research Agent
Exports the shared `app` Agent instance used across all modules.

Authentication is handled automatically via environment variables:
  NEBIUS_API_KEY       — used by AgentField/LiteLLM for all app.ai() calls
  AGENTFIELD_SERVER    — control plane URL (default: http://localhost:8080)
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agentfield&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AIConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;node_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;argus-research-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# LiteLLM requires the provider prefix: nebius/&amp;lt;model&amp;gt;
&lt;/span&gt;    &lt;span class="n"&gt;ai_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AIConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nebius/openai/gpt-oss-120b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;# Connect to the AgentField control plane for dashboard visibility
&lt;/span&gt;    &lt;span class="n"&gt;agentfield_server&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENTFIELD_SERVER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;agentfield_server&lt;/code&gt; parameter tells the agent where to find the control plane. On startup, the agent registers itself, reporting its reasoners, skills, and health. If you are not running a control plane, the agent works fully standalone with no issues.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note: You can use any other model API keys, such as GPT or Claude, instead of Nebius. We use Nebius because it provides access to a collection of several good open models in one place.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Data Contracts (The Schemas)
&lt;/h2&gt;

&lt;p&gt;Every interaction in AgentField is structured. We use &lt;a href="https://pydantic.dev/" rel="noopener noreferrer"&gt;Pydantic&lt;/a&gt; models to define our data contracts. This prevents the “hallucination tax” where LLMs return unpredictable strings.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/schemas.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
schemas.py — Pydantic models for the Argus Investment Committee pipeline.

Data flow (streaming — SSE pipeline in stream.py):
  User Query
    → ResearchPlan         (Manager)
    → AnalystFinding       (Analyst) ─┬─ parallel
    → RiskAssessment       (Contrarian)─┘
    → ResearchReport × 2  (EditorShort ‖ EditorLong, parallel) → DualResearchReport
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchPlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The Manager&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s decomposition of a user query into a research plan.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;reasoning_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Step-by-step reasoning: how you interpreted the query, why you chose this ticker, key assumptions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stock ticker symbol, e.g. AAPL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Full company name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hypotheses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2-4 key hypotheses to investigate (bull and bear)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data_needs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;List of data points needed to validate the hypotheses&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;focus_areas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Specific areas for deep-dive: e.g. revenue growth, debt levels, competitive moat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ─────────────────────────────────────────────────────────────────────────────
# Additional schemas follow the same pattern. Full implementations in repo:
# ─────────────────────────────────────────────────────────────────────────────
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how each schema includes &lt;code&gt;reasoning_steps&lt;/code&gt;. This is Chain-of-Thought prompting built into the data contract. The LLM must show its work before giving an answer. &lt;/p&gt;

&lt;p&gt;Next thing to setup are the AgentField &lt;a href="https://www.agentfield.ai/api/python-sdk/overview#skills" rel="noopener noreferrer"&gt;Skills&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills (The Facts)
&lt;/h2&gt;

&lt;p&gt;Skills are deterministic functions. They provide the facts that agents use to make decisions. In AgentField, every Skill is automatically registered as a REST endpoint &lt;strong&gt;and&lt;/strong&gt; can be called directly by &lt;a href="https://www.agentfield.ai/api/python-sdk/overview#reasoners" rel="noopener noreferrer"&gt;Reasoners&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz3ms5zkf4mo639srvhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz3ms5zkf4mo639srvhe.png" alt="Argus representation of Agentfields skills" width="800" height="446"&gt;&lt;/a&gt; Argus representation of Agentfields skills and how they run in parallel&lt;/p&gt;

&lt;p&gt;We’ll use &lt;a href="https://pypi.org/project/yfinance/" rel="noopener noreferrer"&gt;yfinance&lt;/a&gt; for all data and it’s free, needs no API key.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/skills.py&lt;/code&gt; and start with the imports and a helper function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
skills.py — Deterministic data-fetching tools for the Argus agent.

All skills use yfinance (free, no API key needed) to pull real financial data
from Yahoo Finance. They are registered as @app.skill decorators so AgentField
exposes them as REST endpoints AND the Reasoners can call them directly.

Skills intentionally return plain JSON-serialisable dicts/lists so the LLM
can reason over them without needing to understand yfinance objects.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yfinance&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;yf&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_df_to_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Convert a pandas DataFrame (yfinance financials) to a clean dict.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="c1"&gt;# Transpose so rows = metrics, cols = dates; convert to string keys
&lt;/span&gt;    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_df_to_records&lt;/code&gt; helper converts pandas DataFrames into clean dictionaries that the LLM can understand. Since &lt;code&gt;yfinance&lt;/code&gt;returns financial statements as DataFrames, we convert them into JSON-serializable dictionaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Skill 1: Ticker Validation
&lt;/h3&gt;

&lt;p&gt;The first skill validates that a ticker actually exists and is actively trading. This prevents the agents from hallucinating analysis for fake companies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.skill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_ticker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[skill] Validating ticker:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ticker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yfinance raised an error:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No data returned for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It may be delisted, never listed, private, or misspelled.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regularMarketPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currentPrice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;quote_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quoteType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;exchange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fullExchangeName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shortName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ticker&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;) has no live market price. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It is likely delisted, suspended, or no longer trading.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;quote_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quote_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;quote_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_in_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_fetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Skills 2-8: Financial Data &amp;amp; Market Intelligence
&lt;/h3&gt;

&lt;p&gt;The remaining skills follow the same pattern as &lt;code&gt;validate_ticker&lt;/code&gt;: wrap synchronous yfinance calls in &lt;code&gt;run_in_executor&lt;/code&gt; for async compatibility. &lt;/p&gt;

&lt;p&gt;Here’s one example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.skill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_income_statement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;annual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch income statement data (annual or quarterly).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[skill] Fetching income statement:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fetch&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Ticker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;income_stmt&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;annual&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quarterly_income_stmt&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_df_to_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run_in_executor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_fetch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ─────────────────────────────────────────────────────────────────────────────
# The remaining skills follow the same pattern. Full implementations in repo:
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;run_in_executor&lt;/code&gt;?&lt;/strong&gt; yfinance is synchronous and makes blocking HTTP calls. Wrapping it in &lt;code&gt;run_in_executor&lt;/code&gt; lets us run multiple fetches concurrently without blocking the event loop. Now that we are clear on what AgentField Skills does, we will move on to Reasoners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reasoners (The Brains)
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://www.agentfield.ai/api/python-sdk/overview#reasoners" rel="noopener noreferrer"&gt;Reasoner&lt;/a&gt; is where the agency happens. This is the heart of Argus, the 5-agent investment committee that orchestrates research. This is the reasoning process which the agents use to arrive at their respective decisions. To add that to our agent orchestration engine:&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;src/reasoners.py&lt;/code&gt;. Start with imports and a helper function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
reasoners.py — The five-agent Investment Committee for Argus.
Agent roles:
  1. plan_research      → The Manager      (Adaptive Supervisor)
  2. conduct_research   → The Analyst      (Bull Case) ─┬─ parallel
  3. assess_risks       → The Contrarian   (Bear Case) ─┘
  4. editor_short       → Short-Term View  (1–6 month horizon) ─┬─ parallel
  5. editor_long        → Long-Term View   (1–5 year horizon)  ─┘
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AnalystFinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DualResearchReport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResearchPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResearchReport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RiskAssessment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.skills&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;get_analyst_targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_balance_sheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_cash_flow_statement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_company_facts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_income_statement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_insider_transactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_market_news&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;validate_ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compact JSON serialisation for passing data into app.ai() prompts.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model_dump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;_json&lt;/code&gt; helper serialises Pydantic models to JSON strings for injection into prompts. This is how agents share structured data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reasoners 1-3: Editor, Contrarian, Analyst
&lt;/h3&gt;

&lt;p&gt;These three reasoners follow the same pattern: receive structured input, call &lt;code&gt;app.ai()&lt;/code&gt; with a system prompt and schema, return a typed Pydantic model. Here are their signatures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.reasoner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research/editor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;committee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;synthesize_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;analyst_finding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AnalystFinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;risk_assessment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RiskAssessment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ResearchReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    The Editor: synthesizes bull + bear cases into a balanced report.
    In streaming mode (stream.py), replaced by EditorShort + EditorLong.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Calls app.ai() with system prompt for balanced synthesis
&lt;/span&gt;    &lt;span class="c1"&gt;# Returns ResearchReport with verdict and confidence
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.reasoner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research/contrarian&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;committee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assess_risks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResearchPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;analyst_finding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AnalystFinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RiskAssessment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    The Contrarian: Devil&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s advocate searching for risks.
    Filters news for risk keywords, challenges the bull thesis.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Fetches risk-focused news, filters for negative sentiment
&lt;/span&gt;    &lt;span class="c1"&gt;# Calls app.ai() to identify regulatory, competitive, valuation risks
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@app.reasoner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research/analyst&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;committee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;conduct_research&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ResearchPlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AnalystFinding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    The Analyst: builds the bull case from financial data.
    Runs 9 data fetches in parallel via asyncio.gather.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# asyncio.gather: income, balance, cashflow, facts, news, targets, insiders
&lt;/span&gt;    &lt;span class="c1"&gt;# Calls app.ai() to synthesize bull case thesis
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reasoner 4: The Manager
&lt;/h3&gt;

&lt;p&gt;The Manager is the entry point and orchestrator. It creates the research plan, validates the ticker, dispatches agents, and includes an adaptive retry loop for quality control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.reasoner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;committee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;plan_research&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DualResearchReport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    The Manager: entry point for direct API queries (POST /research).
    Decomposes the query into a ResearchPlan, dispatches Analyst and Contrarian
    in parallel, then runs EditorShort and EditorLong in parallel to produce
    a DualResearchReport.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Decompose the query into a structured ResearchPlan
&lt;/span&gt;    &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;create_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Validate ticker before running the full committee
&lt;/span&gt;    &lt;span class="n"&gt;ticker_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;validate_ticker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ticker_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot analyse &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ticker&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ticker_check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Parallel dispatch → Analyst (bull) + Contrarian (bear)
&lt;/span&gt;    &lt;span class="n"&gt;analyst_finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_assessment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;conduct_research&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;assess_risks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 3: Run dual editors in parallel
&lt;/span&gt;    &lt;span class="n"&gt;short_report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;long_report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;editor_short_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;analyst_finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_assessment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;editor_long_term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;analyst_finding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_assessment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DualResearchReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_term&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;short_report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;long_term&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;long_report&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key patterns to notice:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid Model Strategy&lt;/strong&gt;: The main analytic agents (Manager, Analyst, Contrarian) use &lt;code&gt;gpt-oss-120b&lt;/code&gt; for deep reasoning, while the Editors use &lt;code&gt;gpt-oss-20b&lt;/code&gt; for faster final synthesis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parallel execution with &lt;code&gt;asyncio.gather&lt;/code&gt;&lt;/strong&gt;: The Analyst and Contrarian run simultaneously, as do both Editors. This significantly reduces total latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled Agency&lt;/strong&gt;: Each agent focuses on a specific task (bull or bear) without waiting for the other, allowing the Editor to perform a truly independent synthesis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Tracking&lt;/strong&gt;: Every call to an &lt;code&gt;@app.reasoner&lt;/code&gt; decorated function is automatically visible in the AgentField dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Streaming Pipeline (The Realtime UX)
&lt;/h2&gt;

&lt;p&gt;In a production AI application, the user experience is defined by responsiveness. A research committee can take 30–60 seconds to complete; forcing a user to stare at a static loading bar is a missed opportunity for engagement.&lt;/p&gt;

&lt;p&gt;AgentField allows you to build &lt;strong&gt;live, stateful interfaces&lt;/strong&gt; by transforming your orchestration logic into an asynchronous streaming pipeline. In this section, we'll implement a Server-Sent Events (SSE) system that lets the user follow the research process in real-time as it unfolds.&lt;/p&gt;

&lt;p&gt;Instead of just returning a final report, we will build a pipeline that "narrates" its work. Create &lt;code&gt;src/stream.py&lt;/code&gt; and start with the core dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
stream.py — Server-Sent Events (SSE) streaming for the Argus UI.

Adds raw FastAPI routes to the AgentField app:
  GET  /          → serves the single-page frontend
  POST /research/stream/start       → starts a session, returns session_id
  GET  /research/stream/events/{id} → streams SSE events

Event types:
  agent_start    — an agent has started working
  agent_note     — a progress log from inside an agent
  agent_complete — an agent has finished, with its structured output
  error          — something went wrong
  complete       — both ResearchReports (short + long term) are ready

Agent identifiers used in events:
  manager, analyst, contrarian, editor_short, editor_long
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asynccontextmanager&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ContextVar&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncGenerator&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamingResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AnalystFinding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResearchPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ResearchReport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RiskAssessment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.skills&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;get_analyst_targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_balance_sheet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_cash_flow_statement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_company_facts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_income_statement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;get_insider_transactions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_market_news&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;validate_ticker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;How the Event-Driven Pipeline Works&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In a production environment, users shouldn't wait 30 seconds for a final JSON blob. Argus uses &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt; to provide a live, play-by-play view of the investment committee at work.&lt;/p&gt;

&lt;p&gt;Instead of writing a complex state machine, we use a simple &lt;strong&gt;Event Bus&lt;/strong&gt; pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session Management&lt;/strong&gt;: Every time a user starts a search, we create a unique &lt;code&gt;session_id&lt;/code&gt; and a dedicated &lt;code&gt;asyncio.Queue&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;emit()&lt;/code&gt; Function&lt;/strong&gt;: We place &lt;code&gt;emit()&lt;/code&gt; calls throughout our agent logic. This function pushes small JSON payloads (events) into the session's queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The SSE Stream&lt;/strong&gt;: A background task (the "Generator") watches the queue. As soon as an event appears, it formats it for SSE and sends it to the frontend.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Event Lifecycle&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Each agent in the pipeline follows a predictable three-stage lifecycle that lights up the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agent_start&lt;/code&gt;&lt;/strong&gt;: Signals that a specific agent (e.g., The Analyst) has been dispatched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agent_note&lt;/code&gt;&lt;/strong&gt;: Sends real-time progress logs (e.g., "Fetching balance sheet for AAPL..."). This transforms a "loading spinner" into an interactive experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;agent_complete&lt;/code&gt;&lt;/strong&gt;: Delivers the final structured data for that specific agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Orchestration via Concurrency&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The streaming pipeline is just a wrapper around the same logic used in &lt;code&gt;reasoners.py&lt;/code&gt;, but it leverages Python's &lt;code&gt;asyncio.gather&lt;/code&gt; to drive the UI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Research&lt;/strong&gt;: The Analyst and Contrarian are triggered simultaneously. On the frontend, you see both "cards" start pulsing at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decoupled Synthesis&lt;/strong&gt;: As soon as the research agents finish, the two Editors start in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Final Handshake&lt;/strong&gt;: Once all agents are done, a &lt;code&gt;complete&lt;/code&gt; event is emitted with the full &lt;code&gt;DualResearchReport&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Full implementation of the streaming logic can be found in the &lt;a href="https://github.com/Studio1HQ/argus-agentfield/blob/main/src/stream.py" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This approach ensures the backend remains the "source of truth" while the frontend stays reactive and responsive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up FastAPI Routes
&lt;/h3&gt;

&lt;p&gt;Finally, we setup the endpoints that will expose the streaming pipeline for the UI to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ---------------------------------------------------------------------------
# Raw FastAPI routes added directly to the Agent (which is a FastAPI subclass)
# ---------------------------------------------------------------------------
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StreamQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research/stream/start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;StreamQuery&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Start a streaming research session. Returns a session_id.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# Run pipeline in background, bound to this session's queue
&lt;/span&gt;    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_current_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_current_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_run_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}})&lt;/span&gt;

    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;_current_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/research/stream/events/{session_id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SSE endpoint — streams events for a given session.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;_event_generator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cache-Control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no-cache&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Accel-Buffering&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keep-alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HTMLResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serve_ui&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Serve the Argus UI.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ui_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ui&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HTMLResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ui_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;POST to &lt;code&gt;/research/stream/start&lt;/code&gt; → get a &lt;code&gt;session_id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open EventSource to &lt;code&gt;/research/stream/events/{session_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Receive SSE events as JSON until &lt;code&gt;complete&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F877vulfhtu6bzff100q3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F877vulfhtu6bzff100q3.png" alt="SSE events streaming" width="800" height="800"&gt;&lt;/a&gt; SSE events streaming from the backend to the frontend&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Frontend
&lt;/h2&gt;

&lt;p&gt;The UI is a single-file vanilla JavaScript application (~1400 lines). It connects to the SSE endpoint and animates agent cards as events arrive. Rather than embed the entire file here, get it directly from the repository: &lt;a href="https://github.com/Arindam200/awesome-ai-apps/blob/main/advance_ai_agents/agentfield_finance_research_agent/ui/index.html" rel="noopener noreferrer"&gt;ui/index.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key features of the UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent cards&lt;/strong&gt; — Each of the 5 agents gets a card that glows when active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live reasoning&lt;/strong&gt; — A “thought drawer” types out each agent’s chain-of-thought in real time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabbed results&lt;/strong&gt; — Short-term and long-term verdicts appear in separate tabs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE connection&lt;/strong&gt; — Uses &lt;code&gt;EventSource&lt;/code&gt; to stream events from &lt;code&gt;/research/stream/events/{id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark theme&lt;/strong&gt; — Built with CSS custom properties for easy theming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI listens for these SSE event types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;agent_start&lt;/code&gt; — Card starts glowing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent_note&lt;/code&gt; — Progress update (logs to thought drawer)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agent_complete&lt;/code&gt; — Card turns green, reasoning steps revealed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error&lt;/code&gt; — Something went wrong&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;complete&lt;/code&gt; — Both reports ready, render the tabbed result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6z49eglkflunlh3vqx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr6z49eglkflunlh3vqx9.png" alt="True parallel execution — Analyst and Contrarian running simultaneously" width="800" height="640"&gt;&lt;/a&gt; True parallel execution — Analyst and Contrarian running simultaneously&lt;/p&gt;

&lt;h2&gt;
  
  
  Running Argus and Testing
&lt;/h2&gt;

&lt;p&gt;Now that we've built the research engine and the streaming UI, it’s time to put everything together. &lt;/p&gt;

&lt;p&gt;In this section, we'll configure our environment, launch the AgentField dashboard, and start the Argus server so you can see your 5-agent committee in action.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Set your keys
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;NEBIUS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your_key_here&lt;/span&gt;
&lt;span class="py"&gt;AGENTFIELD_SERVER&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;
&lt;span class="py"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;8081&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Start the AgentField Control Plane
&lt;/h3&gt;

&lt;p&gt;If you installed the &lt;code&gt;af&lt;/code&gt; CLI in Section 1, start the control plane in its own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;af server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the dashboard at &lt;code&gt;http://localhost:8080/ui&lt;/code&gt;. Keep this terminal running - the agent will register with it on startup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhbqsxb2klob0puaisx6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvhbqsxb2klob0puaisx6.png" alt="AgentField control-plane dashboard showing agents offline, 100% success rate across 22 executions, and workflow performance charts." width="800" height="682"&gt;&lt;/a&gt; AgentField control-plane dashboard showing agents offline, 100% success rate across 22 executions, and workflow performance charts.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Boot the agent
&lt;/h3&gt;

&lt;p&gt;In a &lt;strong&gt;new terminal&lt;/strong&gt;, create &lt;code&gt;src/main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
main.py — Entry point for the Argus autonomous research agent.

Usage:
    uv run python3 src/main.py

The agent will start on http://localhost:8081 (separate from the AgentField
control plane on :8080) and expose:
    POST /research          → Full investment committee pipeline (Manager entry point)
    POST /research/analyst  → Analyst (bull case) only
    POST /research/contrarian → Contrarian (bear case) only
    POST /research/editor   → Editor (synthesis) only
    + all /skills/* endpoints

Example query:
    curl -X POST http://localhost:8081/research&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;         -H &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type: application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;         -d &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Should I invest in AAPL?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="c1"&gt;# Ensure the project root is on sys.path so `src` is importable
# when running as: python3 src/main.py
&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abspath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="c1"&gt;# Load .env before importing anything that reads env vars
&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Import and boot the shared Agent instance
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: E402
&lt;/span&gt;
&lt;span class="c1"&gt;# Register skills + reasoners by importing their modules.
# The @app.skill / @app.reasoner decorators fire on import.
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;src.skills&lt;/span&gt;    &lt;span class="c1"&gt;# noqa: F401, E402
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;src.reasoners&lt;/span&gt; &lt;span class="c1"&gt;# noqa: F401, E402
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;src.stream&lt;/span&gt;    &lt;span class="c1"&gt;# noqa: F401, E402 — SSE endpoints + UI serving
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔬 Argus Research Agent starting on http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🎛️  AgentField Control Plane dashboard: http://localhost:8080/ui&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;📈 5-Agent Investment Committee:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   POST http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/research               ← Full pipeline (all 5 agents)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   POST http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/research/analyst       ← Bull case only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   POST http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/research/contrarian    ← Bear case only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   POST http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/research/stream/start  ← SSE streaming (used by UI)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   GET  http://localhost:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/                       ← Live UI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run python3 src/main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see the agent register with the control plane:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshplv0nvz9lgg7t7olz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fshplv0nvz9lgg7t7olz9.png" alt="Argus server starting up with all registered endpoints" width="800" height="597"&gt;&lt;/a&gt; Argus server starting up with all registered endpoints&lt;/p&gt;

&lt;p&gt;You can also verify this in the Agentfield dashboard. You should see your agents registered with all the reasoners and skills as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fowbhpkp020ljn3nwcoqf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fowbhpkp020ljn3nwcoqf.png" alt="AgentField control-plane dashboard" width="800" height="688"&gt;&lt;/a&gt; AgentField control-plane dashboard showing the &lt;strong&gt;argus-research-agent&lt;/strong&gt; node status and a list of registered reasoners and skills.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test the API directly
&lt;/h3&gt;

&lt;p&gt;You can test the full pipeline without the UI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8081/research &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query": "Should I invest in NVDA?"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or test individual agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test the Analyst alone (requires a ResearchPlan as input)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8081/research/analyst &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
       "plan": {
         "reasoning_steps": ["User wants to invest in Apple"],
         "ticker": "AAPL",
         "company_name": "Apple Inc",
         "hypotheses": ["Strong iPhone sales", "Services growth"],
         "data_needs": ["Revenue", "Margins"],
         "focus_areas": ["iPhone", "Services", "Wearables"]
       }
     }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Test from the UI
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://localhost:8081&lt;/code&gt; in your browser. Type a query like “Should I invest in NVDA?” and watch the 5-agent committee work in real-time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2c0qs9i34v8bim3tuqq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2c0qs9i34v8bim3tuqq.png" alt="Completed research report with dual time horizon verdicts" width="800" height="648"&gt;&lt;/a&gt; Completed research report with dual time horizon verdicts&lt;/p&gt;

&lt;h3&gt;
  
  
  6. View the Workflow in the AgentField Dashboard
&lt;/h3&gt;

&lt;p&gt;While the agents are running (or after they complete), open the AgentField control plane dashboard at &lt;code&gt;http://localhost:8080/ui&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Navigate to &lt;strong&gt;Workflow Executions&lt;/strong&gt; to see the full execution graph:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhyxczfzht8f3ritbw7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxhyxczfzht8f3ritbw7p.png" alt="AgentField dashboard showing a workflow execution graph" width="800" height="464"&gt;&lt;/a&gt; AgentField dashboard showing a workflow execution graph with multiple connected nodes (skills and reasoners) and a “Succeeded” status indicator.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;@app.reasoner()&lt;/code&gt; and &lt;code&gt;@app.skill()&lt;/code&gt; call appears as a node in the workflow graph. You can trace the exact flow: Manager → Analyst → Contrarian → EditorShort / EditorLong. Each node shows execution time, input data, and output data. Skills called within reasoners (like &lt;code&gt;get_income_statement&lt;/code&gt;, &lt;code&gt;get_insider_transactions&lt;/code&gt;) appear as child nodes.&lt;/p&gt;

&lt;p&gt;This is the observability layer that makes Argus production-ready. Instead of guessing what your agents did, you can trace every step, inspect every input and output, and measure performance, all without writing a single line of instrumentation code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AgentField is different
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dub.sh/agentf" rel="noopener noreferrer"&gt;AgentField&lt;/a&gt; is built for the move from “prototypes” to “production”:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agents as Microservices&lt;/strong&gt;: Every agent becomes a standard REST API with OpenAPI documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic Identity&lt;/strong&gt;: Every action is signed and verified. This is the only way to build trustworthy autonomous systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Agent Internet&lt;/strong&gt;: AgentField prepares you for a web where agents negotiate and execute intent on your behalf.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building with AgentField means agentic development has moved from “intent” to “execution.” It is no longer just about writing a prompt. It is about building a secure, scalable, and auditable system.&lt;/p&gt;

&lt;p&gt;You can expand Argus by adding more specialized agents such as an ESG analyst, legal risk assessor, or macro forecaster. Because every component is a modular Skill or Reasoner, the system can grow without breaking.&lt;/p&gt;

&lt;p&gt;The architecture we built here includes typed schemas, parallel execution, SSE streaming, and agents as microservices. This is the same pattern used in production AI systems. The difference between a weekend project and a production system comes down to structure, type safety, and observability from day one.&lt;/p&gt;




&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Check &lt;a href="https://dub.sh/agentf" rel="noopener noreferrer"&gt;AgentField Docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;a href="https://github.com/Arindam200/awesome-ai-apps/tree/main/advance_ai_agents/agentfield_finance_research_agent" rel="noopener noreferrer"&gt;Project Repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;a href="https://github.com/Agent-Field/SWE-AF" rel="noopener noreferrer"&gt;Autonomous software engineering fleet of 400+ AI agents for production-grade PRs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>devops</category>
    </item>
    <item>
      <title>5 Things Developers Get Wrong About Inference Workload Monitoring</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Fri, 13 Mar 2026 09:28:15 +0000</pubDate>
      <link>https://dev.to/astrodevil/5-things-developers-get-wrong-about-llm-performance-monitoring-3i6f</link>
      <guid>https://dev.to/astrodevil/5-things-developers-get-wrong-about-llm-performance-monitoring-3i6f</guid>
      <description>&lt;p&gt;Most LLM applications reach production with monitoring built for traditional backend services. Dashboards show average latency, overall error rate, and total tokens consumed. These indicators provide a quick sense of system health and cost exposure and often appear reassuring during early rollout, when traffic is predictable.&lt;/p&gt;

&lt;p&gt;LLM inference operates under a different set of mechanics. Each request moves through GPU scheduling, queueing, prefill computation, and token generation. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p8mepq3pyluvuy7is7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p8mepq3pyluvuy7is7h.png" alt="LLM inference" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt length changes how much work happens before the first token appears. Concurrency affects how resources are shared across requests. These factors interact in ways that averages alone cannot explain.&lt;/p&gt;

&lt;p&gt;When monitoring fails to reflect how inference actually runs, teams see symptoms but miss underlying causes. This article examines five common mistakes developers make when evaluating LLM performance and clarifies what deserves closer attention in real production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bridging the LLM Observability Gap
&lt;/h2&gt;

&lt;p&gt;LLM systems often show performance drift before they show failure. Latency increases for certain requests. First-token timing becomes inconsistent. Throughput changes under higher concurrency. Traditional dashboards may still display stable averages.&lt;/p&gt;

&lt;p&gt;The gap forms because inference behavior depends on prompt size, queue depth, GPU allocation, and workload mix. Surface metrics hide these interactions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dub.sh/AIStudio" rel="noopener noreferrer"&gt;Nebius Token Factory&lt;/a&gt; addresses this gap at the inference layer. It is a production-grade LLM inference platform with built-in observability designed for real production workloads&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/a2aC4-58OsA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #1: Treating Average Latency as a Reliable Performance Indicator
&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes in LLM performance monitoring is relying on average latency as the primary signal of system health. &lt;/p&gt;

&lt;p&gt;Developers choose this metric because it produces a single number that looks clear in dashboards and reports. When the mean response time remains steady, the system appears stable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Weakens Production Insight
&lt;/h3&gt;

&lt;p&gt;LLM workloads do not behave evenly. Prompt length varies across requests. Output size varies with task complexity. Concurrency increases during peak usage. Some requests complete quickly. Others require more prefill compute or wait longer in the queue.&lt;/p&gt;

&lt;p&gt;An average hides this variation. A portion of requests can slow down significantly, and the mean may still look acceptable. In chat and agent systems, slower requests degrade the user experience even when most responses are fast. Monitoring only averages hides tail latency until complaints surface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4g7102r2xkia1g5tzt8p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4g7102r2xkia1g5tzt8p.png" alt="Latency Distribution" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How Nebius Token Factory Addresses This
&lt;/h3&gt;

&lt;p&gt;Nebius Token Factory Observability treats latency as a distribution problem. The platform calculates and displays percentile values for each endpoint and model across selected time windows.&lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;p50 latency&lt;/strong&gt;, which reflects typical request behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p90 latency&lt;/strong&gt;, which highlights emerging stress under moderate load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;p99 latency&lt;/strong&gt;, which exposes tail performance under heavier concurrency&lt;/li&gt;
&lt;li&gt;Percentiles for both &lt;strong&gt;End-to-End Latency&lt;/strong&gt; and &lt;strong&gt;Time to First Token&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These percentile charts update continuously over rolling aggregation windows. Developers can filter by endpoint, project, region, prompt length, or latency band. This allows us to isolate slow requests and examine their correlation with traffic volume or token size.&lt;/p&gt;

&lt;p&gt;The observability layer also supports integration with Prometheus and Grafana. Teams can build custom alerts based on p95 or p99 thresholds instead of averages. This allows production monitoring to focus on tail behavior where real user impact occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #2: Collapsing All Failures Into a Single Error Rate
&lt;/h2&gt;

&lt;p&gt;Another serious mistake in LLM performance monitoring is collapsing all failures into a single overall error rate. A single percentage may show that failures exist. It does not explain the type of failure or which layer caused it.&lt;/p&gt;

&lt;p&gt;LLM systems fail at different points in the request lifecycle. Input validation can fail. Capacity limits can trigger throttling. Infrastructure can return execution errors. These failures carry different operational meanings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Reduces Diagnostic Precision
&lt;/h3&gt;

&lt;p&gt;Each error category signals a different problem.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;4xx error&lt;/strong&gt; often points to invalid input, unsupported parameters, or prompt size limits.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;429 error&lt;/strong&gt; indicates rate limiting or capacity constraints under higher concurrency.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;5xx error&lt;/strong&gt; indicates an internal execution or infrastructure issue.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If monitoring aggregates all of these into one number, diagnosis slows down. The system shows instability but does not indicate the source. Developers must inspect logs manually to separate validation errors from capacity pressure.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Nebius Token Factory Addresses This
&lt;/h3&gt;

&lt;p&gt;Nebius Token Factory Observability exposes error metrics as structured dimensions.&lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Error rate grouped by HTTP status code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Separate visibility into 4xx, 429, and 5xx categories&lt;/li&gt;
&lt;li&gt;Filtering by endpoint, region, project, API key, and time window&lt;/li&gt;
&lt;li&gt;Correlation with traffic metrics such as requests per minute and token flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics appear alongside latency percentiles and throughput charts. Developers can examine whether 429 responses increase during traffic spikes. They can inspect whether 5xx errors concentrate on a specific endpoint. They can filter by prompt length to identify validation failures linked to context size.&lt;/p&gt;

&lt;p&gt;Metrics remain available through Prometheus and Grafana integrations for alerting and long-term analysis. Structured error visibility enables precise root-cause identification across the validation, capacity, and execution layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #3: Overlooking Time to First Token and Inference Stages
&lt;/h2&gt;

&lt;p&gt;Many monitoring setups measure only total response time from request submission to final token delivery. That metric appears complete because it captures the full lifecycle of a request. In interactive LLM systems, users react when the first token appears on screen. &lt;/p&gt;

&lt;p&gt;A delay at the start creates a perception of slowness even if total completion time stays within limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Impact on Performance Visibility
&lt;/h3&gt;

&lt;p&gt;Inference executes in distinct stages. A request enters a queue. The system processes the entire prompt during prefill. Token generation begins after prefill completes. Total latency combines all these steps into a single value.&lt;/p&gt;

&lt;p&gt;Time to First Token reflects queue delay and prompt processing. Decode time reflects token generation speed after the first token appears. When monitoring tracks only the total duration, it becomes difficult to determine whether the delay is due to queue buildup, larger prompts, or decoding throughput.&lt;/p&gt;

&lt;p&gt;Separating these signals clarifies how the inference pipeline behaves under higher concurrency and heavier workloads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favmwhw4sq1fhg05xrtp1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Favmwhw4sq1fhg05xrtp1.png" alt="Latency Breakdown" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How Nebius Token Factory Provides Stage-Level Visibility
&lt;/h3&gt;

&lt;p&gt;Nebius Token Factory integrates observability directly into the inference pipeline. It exposes separate metrics for full request duration, Time to First Token, and token generation speed. Each metric appears as percentile distributions such as p50, p90, and p99.&lt;/p&gt;

&lt;p&gt;Time to First Token reflects queue delay and prompt processing time. Output speed shows decoding throughput after generation begins. End-to-end latency captures the complete request lifecycle. Viewing these signals together allows clear identification of where delay occurs.&lt;/p&gt;

&lt;p&gt;The platform presents these metrics alongside traffic volume and active replica data. Developers can examine whether higher concurrency increases TTFT or whether scaling activity stabilizes latency. Filters allow analysis by endpoint, region, project, prompt length, and time window. Prometheus and Grafana integrations support alerting on TTFT percentiles and stage-level latency trends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake #4: Ignoring Scaling and Capacity Signals
&lt;/h2&gt;

&lt;p&gt;Many LLM monitoring setups focus only on request-level metrics such as latency and error rate. They do not track how the underlying infrastructure behaves when traffic increases. &lt;/p&gt;

&lt;p&gt;When latency rises under load, attention often turns to the model. The actual cause may relate to replica allocation or capacity limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Consequences
&lt;/h3&gt;

&lt;p&gt;LLM inference depends on available computing resources. Higher request volume increases queue depth. Scaling events change how traffic is distributed across replicas. &lt;/p&gt;

&lt;p&gt;New instances may introduce an initialization delay before handling traffic. These infrastructure changes directly affect Time to First Token and high-percentile latency.&lt;/p&gt;

&lt;p&gt;If monitoring does not expose replica activity or capacity state, it becomes difficult to connect traffic growth with performance behavior. Latency may increase during scaling transitions, yet the monitoring view shows only slower responses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nebius Token Factory Capacity Visibility
&lt;/h3&gt;

&lt;p&gt;Nebius Token Factory Observability surfaces capacity and scaling signals alongside latency and traffic metrics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Active Replica Metrics:&lt;/strong&gt; The platform shows how many replicas actively serve requests. This helps identify whether latency growth aligns with scaling activity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic and Token Flow Metrics:&lt;/strong&gt; Requests per minute and token volume appear in the same view. Developers can correlate concurrency growth with capacity utilization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency Distribution with Scaling Context:&lt;/strong&gt; Percentile latency metrics can be examined together with replica counts. This reveals whether p99 increases during load growth or stabilizes after new replicas come online.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Filtering by endpoint, region, project, and time window allows focused analysis. Prometheus and Grafana integrations support alerting tied to scaling behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 5# Treating Prompt Length as a Cost Metric Only
&lt;/h2&gt;

&lt;p&gt;Many developers track prompt length only to estimate token cost. Input and output tokens appear in billing views, and analysis stops there. Prompt size rarely enters performance discussions.&lt;/p&gt;

&lt;p&gt;In production systems, prompt length directly influences compute time, queue behavior, and latency distribution. Ignoring it as a performance variable hides important signals.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Gets Missed
&lt;/h3&gt;

&lt;p&gt;The difference becomes clear when prompt size is treated purely as a billing metric versus a performance variable.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;If Prompt Length Is Viewed Only as Cost&lt;/th&gt;
&lt;th&gt;What Actually Happens in Production&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tokens are tracked for billing only&lt;/td&gt;
&lt;td&gt;Longer prompts increase prefill compute time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per request is monitored&lt;/td&gt;
&lt;td&gt;Large prompts raise TTFT and p99 latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output token totals are reviewed&lt;/td&gt;
&lt;td&gt;Long generations affect decoding throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No correlation with traffic load&lt;/td&gt;
&lt;td&gt;Heavy prompts amplify queue depth under concurrency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prompt distribution shapes the inference pipeline's behavior. Two endpoints with the same request rate can perform very differently if one processes longer contexts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq89iqrj31jqgtr2wbrzb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq89iqrj31jqgtr2wbrzb.png" alt="Prompt size vs TTFT" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How Nebius Connects Token Usage to Performance
&lt;/h3&gt;

&lt;p&gt;Nebius Token Factory treats token usage as an operational signal.&lt;/p&gt;

&lt;p&gt;It provides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input and output tokens per minute&lt;/li&gt;
&lt;li&gt;Distribution of tokens per request&lt;/li&gt;
&lt;li&gt;Filtering by prompt length&lt;/li&gt;
&lt;li&gt;Correlation between token metrics, TTFT percentiles, and throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Developers can compare short and long prompts within the same endpoint. They can observe how larger contexts affect prefill time and tail latency. They can inspect whether token growth aligns with scaling activity or throughput limits.&lt;/p&gt;

&lt;p&gt;This connection between workload shape and execution behavior allows prompt size to be analyzed as a performance factor, not only a billing metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;LLM systems require monitoring that reflects inference mechanics. Averages and blended metrics hide latency distribution, workload impact, and scaling behavior.&lt;/p&gt;

&lt;p&gt;Clear visibility into percentiles, stage-level timing, structured errors, and token flow improves production analysis and reduces guesswork.&lt;/p&gt;

&lt;p&gt;Nebius Token Factory embeds observability directly into the inference layer and surfaces the signals that matter under real load. If you operate LLM systems in production, evaluate whether your monitoring captures how inference truly behaves. &lt;a href="https://docs.tokenfactory.nebius.com/ai-models-inference/observability" rel="noopener noreferrer"&gt;Explore Nebius Token Factory Observability&lt;/a&gt; to build performance visibility designed for scale.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build a Real-Time AI Analytics Dashboard with InsForge, FastAPI, and Claude Code</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Thu, 12 Mar 2026 16:39:14 +0000</pubDate>
      <link>https://dev.to/astrodevil/build-a-real-time-ai-analytics-dashboard-with-insforge-fastapi-and-claude-code-5h3i</link>
      <guid>https://dev.to/astrodevil/build-a-real-time-ai-analytics-dashboard-with-insforge-fastapi-and-claude-code-5h3i</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this tutorial, we will build a fully functional analytics dashboard from scratch. The kind that ingests user events, shows live metrics and charts, and generates AI insights that stream word by word into the browser.&lt;/p&gt;

&lt;p&gt;Here is what we will be building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A FastAPI backend with event ingestion, metrics aggregation, AI streaming insights, and an event simulator&lt;/li&gt;
&lt;li&gt;A Next.js frontend with a live metrics panel, event volume and breakdown charts, a live event feed, and a streaming AI insights panel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;InsForge&lt;/a&gt; as the backend platform, managing our database, AI models, and REST API layer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai/code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; as the agent that builds the backend through a conversation with our live InsForge instance via &lt;a href="https://docs.insforge.dev/mcp-setup" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you will have a working template you can drop your own event schema into. Let's get started.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is InsForge?
&lt;/h2&gt;

&lt;p&gt;InsForge is an open-source backend platform that you can also self-host with Docker. It gives you a Postgres database, a REST API layer built on PostgREST, an AI model gateway that routes to any OpenRouter-compatible model, a real-time pub/sub system, and serverless function support, all running on your own infrastructure.&lt;/p&gt;

&lt;p&gt;Think of it as the infrastructure layer for data-driven applications. Instead of stitching together a database, an API server, and an AI integration separately, InsForge bundles them into a single deployable platform.&lt;/p&gt;

&lt;p&gt;You bring your application logic, and InsForge handles the plumbing underneath.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zmf7wqhsc3fn374t576.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5zmf7wqhsc3fn374t576.png" alt="InsForge" width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Are Using InsForge For
&lt;/h3&gt;

&lt;p&gt;Three things in particular make InsForge the right choice for an AI-first project like this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The managed AI gateway:&lt;/strong&gt; You configure your OpenRouter API key once inside InsForge, and the platform handles all model routing from there. Your application calls one InsForge endpoint and passes a model string. Swap the string, and everything else stays the same. No per-model SDKs, no separate credentials in your codebase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP server:&lt;/strong&gt; InsForge ships with an &lt;a href="https://docs.insforge.dev/mcp-setup" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; that gives Claude Code direct access to your live backend. The agent can read your schema, fetch documentation, and generate auth tokens as part of a conversation. This is what makes the one-prompt build possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The PostgREST layer:&lt;/strong&gt; Every table in your InsForge database is automatically exposed as a REST endpoint. You do not write data access code. You describe your schema, and InsForge handles the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you connect OpenRouter inside InsForge, the platform provisions the models and manages all routing. &lt;/p&gt;

&lt;p&gt;For this project, we used anthropic/claude-sonnet-4.5, but you can switch models by changing a single string. Here is what available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Input&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Output&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;anthropic/claude-sonnet-4.5&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;openai/gpt-4o-mini&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;x-ai/grok-4.1-fast&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;deepseek/deepseek-v3.2&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;minimax/minimax-m2.1&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;google/gemini-3-pro-image-preview&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;td&gt;Text + image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Getting InsForge Ready&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Creating Your InsForge Project&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Head to &lt;a href="https://insforge.dev/" rel="noopener noreferrer"&gt;insforge.dev&lt;/a&gt; and sign up. Once you create a project, the dashboard gives you three things you will need throughout this build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base URL&lt;/strong&gt; — your project's unique API endpoint, for example, &lt;a href="https://xxxxxxxx.us-east.insforge.app" rel="noopener noreferrer"&gt;https://xxxxxxxx.us-east.insforge.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anon Key&lt;/strong&gt; — for browser-side and public API operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Key&lt;/strong&gt; — for privileged server-side operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy those and keep them close. That is all the platform setup InsForge needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Configuring the AI Gateway&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Inside your InsForge dashboard, go to the AI Integration section and add your &lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; API key. InsForge connects to OpenRouter and provisions the available models automatically. From this point on, your application calls InsForge, and InsForge handles the routing. You pick the model. InsForge does the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Setting Up Your Project Folder&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Create a new folder for the project and open your terminal inside it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mkdir insforge-dashboard&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cd insforge-dashboard&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Connecting Claude Code via MCP&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we touch any application code, let's connect Claude Code to our live InsForge instance using the &lt;a href="https://docs.insforge.dev/mcp-setup" rel="noopener noreferrer"&gt;InsForge MCP server&lt;/a&gt;. MCP (Model Context Protocol) is an open standard that lets AI coding agents connect to external tools and live data sources as part of a conversation. When it is set up, Claude Code can reach into your running InsForge backend and work against it directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Installing the MCP&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Run this command inside your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @insforge/install &lt;span class="nt"&gt;--client&lt;/span&gt; claude-code &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_insforge_api_key &lt;span class="se"&gt;\&lt;/span&gt;

&lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;API_BASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-project.us-east.insforge.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The MCP installs and registers itself with Claude Code automatically. Restart Claude Code, and the connection is live.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxy5xky4na78ek96jtmbu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxy5xky4na78ek96jtmbu.png" alt="InsForge MCP" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open Claude Code and start with this prompt to see what the agent has access to:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Connect to my InsForge instance and tell me what you can see.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the output we got with Claude:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;What the agent saw&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Details&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;3 tables: events ((1,000 records), ai_insights (4 records), event_hourly_stats (0 records)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI models&lt;/td&gt;
&lt;td&gt;6 models configured and ready including Claude Sonnet 4.5, GPT-4o-mini, Grok 4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;GitHub and Google OAuth configured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schemas&lt;/td&gt;
&lt;td&gt;Full column definitions and types for all three tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SDK docs&lt;/td&gt;
&lt;td&gt;InsForge REST API documentation fetched and read automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Claude Code connects to the live backend, reads the schema, and fetches the SDK documentation through the MCP connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Building the Backend&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With Claude Code connected to InsForge via MCP, run the following prompt to generate the FastAPI backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="ge"&gt;*Build me a FastAPI backend with four routers: events, metrics, insights, and simulate. Use the InsForge SDK to connect to my backend. The insights router should stream AI responses using the anthropic/claude-sonnet-4.5 model through the InsForge AI gateway.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before writing a single file, Claude Code uses the MCP connection to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fetched the InsForge SDK documentation&lt;/strong&gt; to understand the correct API patterns for database and AI calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read all three table schemas&lt;/strong&gt; so that the code it generated matched our actual data structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generated a JWT token&lt;/strong&gt; for authenticated database access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspected the existing project structure&lt;/strong&gt; to understand what was already in place&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was a complete project structure generated in a single pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;insforge&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;dashboard&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# App entry point, CORS, router registration
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# InsForge credentials from environment
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# Shared InsForgeClient with database and AI helpers
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="n"&gt;routers&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# GET + POST /events
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# GET /metrics/summary and /metrics/hourly
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;insights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# POST /insights/generate — SSE streaming
&lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="n"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt; &lt;span class="c1"&gt;# POST /simulate/events
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;The InsForge Client&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The generated InsForgeClient communicates directly with the InsForge REST API using httpx. The database and AI gateway share the same client, base URL, and auth header, which reflects how InsForge unifies both services under a single interface.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InsForgeClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INSFORGE_BASE_URL&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_anon_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INSFORGE_ANON_KEY&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_anon_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/database/records/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;raw_total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Total-Count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;raw_total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/database/records/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Prefer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;return=representation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;The AI Streaming Method&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;ai_stream&lt;/code&gt; method on the client calls the InsForge AI gateway and yields raw SSE lines back to the caller. The application calls the InsForge AI gateway directly. OpenRouter is configured once inside the InsForge dashboard and managed by the platform. The application codebase requires no OpenRouter credentials or model-specific SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AI_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;anthropic/claude-sonnet-4.5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ai_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AI_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;systemPrompt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/ai/chat/completion&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aiter_lines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To switch models, update the &lt;code&gt;AI_MODEL&lt;/code&gt; string at the top of &lt;code&gt;client.py&lt;/code&gt;. The streaming logic, frontend integration, and persistence layer require no changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The Metrics Router&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The metrics endpoint reads from the events and event_hourly_stats tables and aggregates the results in Python. InsForge's PostgREST layer does not expose GROUP BY directly, so we use Python's Counter to group by event name and page after the rows come back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_summary&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;select&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;event_name,user_id,session_id,page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;event_counts&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;event_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unique_users&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="n"&gt;unique_sessions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;session_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="n"&gt;page_counts&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;unique_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_sessions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unique_sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;events_by_name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_pages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/hourly&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_hourly_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bucket_start.desc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;event_hourly_stats&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;The Insights Router&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When a user clicks Generate Insight, the insights router fetches recent event data, formats it as a structured context summary, and passes it to Claude Sonnet via the InsForge AI gateway. The stream proxies directly to the browser. Once it completes, the full response is saved to the &lt;code&gt;ai_insights&lt;/code&gt; table so it persists across page refreshes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You are an expert product analytics consultant. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;You will receive a structured summary of user event data and a specific question. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Provide clear, concise, and actionable insights. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Structure your response with labeled sections &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(e.g. ## Key Findings, ## Recommendations). &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Be specific — reference actual numbers from the data where relevant.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_and_save&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;accumulated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sse_line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ai_stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;data_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sse_line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chunk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;accumulated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;chunk&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;

        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;sse_line&lt;/span&gt;

    &lt;span class="c1"&gt;# Persist the full response once streaming is complete
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;accumulated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;full_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accumulated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;insforge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ai_insights&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;insight_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insight_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;full_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;time_range&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time_range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_events&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_users&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;unique_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unique_sessions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unique_sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;event_breakdown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;}])&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# Don't let a save failure break the delivered stream
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;StreamingResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;stream_and_save&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/event-stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Cache-Control&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no-cache&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Accel-Buffering&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that the save happens after the stream closes. The user gets the full streaming experience, and the insight is persisted in the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Running the Backend&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="n"&gt;venv&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;

&lt;span class="c1"&gt;# Windows
&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;\&lt;span class="n"&gt;Scripts&lt;/span&gt;\&lt;span class="n"&gt;activate&lt;/span&gt;

&lt;span class="c1"&gt;# Mac / Linux
&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;venv&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;activate&lt;/span&gt;

&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;requirements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;span class="n"&gt;uvicorn&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nb"&gt;reload&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test that it is working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;

&lt;span class="c1"&gt;# {"total_events": 1000, "unique_users": 198, "unique_sessions": 445,
&lt;/span&gt;
&lt;span class="c1"&gt;# "events_by_name": {"page_view": 214, "search": 208, "purchase": 205, ...}}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Building the Frontend&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Use the following prompt to generate the Next.js frontend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="ge"&gt;*Build a Next.js frontend for this analytics dashboard. It should have a metrics summary row, an event volume chart, an event breakdown chart, a live event feed, and an AI insights panel that streams responses word by word. Poll the FastAPI backend every 5 seconds for live data.*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated frontend uses Tailwind CSS and Recharts, with all components connected to the FastAPI backend. The two most important pieces are the polling mechanism and the SSE streaming implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Keeping the Dashboard Live&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The dashboard polls /metrics/summary and /events every 5 seconds, so it stays current. The data loads immediately on mount, and the interval keeps it fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;loadMetrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;loadEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;loadMetrics&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nf"&gt;loadEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Streaming AI Insights&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;When a user clicks Generate Insight, the AIInsightsPanel opens an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events" rel="noopener noreferrer"&gt;SSE (Server-Sent Events)&lt;/a&gt; connection to POST /insights/generate and reads the response body as a stream, appending each chunk to a buffer as it arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;insight&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each onChunk call appends to a streamBuffer in state, and the component renders the buffer progressively with a blinking cursor. When onDone fires, the buffer is cleared, and the persisted insight is prepended to the list. The panel header displays "claude-sonnet-4.5 · InsForge", confirming the model is served through the InsForge gateway.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Starting the Frontend&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;. Use the simulator to populate the dashboard with realistic events if you have not already:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"http://localhost:8000/simulate/events"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;count&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: 50}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it a few times and refresh the dashboard. The metrics panel, charts, and event feed will populate with the simulated data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing it all work
&lt;/h2&gt;

&lt;p&gt;With both servers running and some events in the database, here is what the finished dashboard shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total Events, Unique Users, Unique Sessions, and Top Event in the metrics row at the top&lt;/li&gt;
&lt;li&gt;An Event Volume chart showing activity over the selected time range, switchable between 1 hour, 24 hours, and 7 days&lt;/li&gt;
&lt;li&gt;An Event Breakdown bar chart grouping events by type&lt;/li&gt;
&lt;li&gt;A Live Event Feed showing recent events with user IDs, pages, and timestamps, updating every 5 seconds&lt;/li&gt;
&lt;li&gt;An AI Insights panel where you submit a question and Claude Sonnet streams a structured analysis through the InsForge gateway in real time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click Generate Insight, and see the results you get.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3a63i72vxvqxg9mpbj0b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3a63i72vxvqxg9mpbj0b.png" alt="Insights from the app" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Live Updates with InsForge Realtime&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;InsForge includes a built-in real-time system for pushing updates to connected clients over WebSockets. It is channel-based and built directly into the platform alongside the database and AI gateway, so there is nothing additional to configure.&lt;/p&gt;

&lt;p&gt;To add Realtime to the dashboard, run this prompt in Claude Code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Add InsForge Realtime to the app. When a new event is inserted via POST /events or the simulator, publish it to a channel called analytics:events. On the frontend, subscribe to that channel using the InsForge Realtime SDK and push incoming events directly into the live event feed as they arrive.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code registers the channel and creates a database trigger on the events table that fires &lt;code&gt;realtime.publish()&lt;/code&gt; on every insert. This covers both the API endpoint and the batch simulator. &lt;/p&gt;

&lt;p&gt;The InsForge Realtime dashboard logs every message flowing through the system, showing the event name, channel, payload, and timestamp for each publish call.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4cncworbikov8f4tmaf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw4cncworbikov8f4tmaf.png" alt="InsForge Dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Deploying the Application&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once the dashboard is working locally, deploying it is a matter of giving Claude Code a prompt. Because the MCP connection is still active, the agent understands the project structure and can generate the deployment configuration without any additional context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating the Deployment Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the following prompt in Claude Code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Prepare this project for deployment to Zeabur. Create a Dockerfile for the FastAPI backend and a Dockerfile for the Next.js frontend using standalone output. Include a .dockerignore for each service.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude Code will generate the following files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;insforge&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;dashboard&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;Dockerfile&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;FastAPI&lt;/span&gt; &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="o"&gt;---&lt;/span&gt; &lt;span class="nx"&gt;Python&lt;/span&gt; &lt;span class="mf"&gt;3.11&lt;/span&gt; &lt;span class="nx"&gt;slim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;uvicorn&lt;/span&gt; &lt;span class="nx"&gt;on&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dockerignore&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="nx"&gt;frontend&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;Dockerfile&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt; &lt;span class="o"&gt;---&lt;/span&gt; &lt;span class="nx"&gt;multi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="nx"&gt;build&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;standalone&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dockerignore&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deploying to Zeabur&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Push the project to GitHub, then go to zeabur.com and create a new project. Add two services from the same repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend service:&lt;/strong&gt; point Zeabur at the root directory. It detects the Dockerfile automatically. Set the following environment variables: &lt;code&gt;INSFORGE_BASE_URL&lt;/code&gt; and &lt;code&gt;INSFORGE_ANON_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend service:&lt;/strong&gt; point Zeabur at the &lt;code&gt;/frontend&lt;/code&gt; subdirectory. Set &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; to the public URL Zeabur assigns to your backend service, for example, &lt;a href="https://your-backend.zeabur.app/" rel="noopener noreferrer"&gt;https://your backend.zeabur.app&lt;/a&gt;. This value must be set before the build runs, as it is baked into the Next.js bundle at build time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once both services are deployed, click Generate Domain on each to assign a public URL. The frontend will be accessible at its public URL and will communicate with the backend through the &lt;code&gt;NEXT_PUBLIC_API_URL&lt;/code&gt; you configured.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What's Next?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At this point, you have a fully working AI analytics dashboard running on InsForge. Claude Code generates the backend through a single MCP-connected prompt. AI insights stream through the InsForge gateway with no OpenRouter configuration required in the application. The dashboard stays current via polling, and every insight is persisted to the database.&lt;/p&gt;

&lt;p&gt;From here, the project is yours to extend. Swap in your own event schema, add new metrics endpoints, or change the AI model to gpt-4o-mini or grok-4.1-fast by updating a single string in client.py. The MCP connection stays live, so Claude Code remains a capable collaborator for any further work. You can clone the &lt;a href="https://github.com/Studio1HQ/insforge-sample" rel="noopener noreferrer"&gt;project’s repo&lt;/a&gt; and extend the project further.&lt;/p&gt;

&lt;p&gt;To learn more about Insforge, check out the &lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>analytics</category>
      <category>fastapi</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building a plugin for a React visual editor with Puck</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Thu, 12 Mar 2026 16:38:54 +0000</pubDate>
      <link>https://dev.to/puckeditor/building-a-plugin-for-a-react-visual-editor-with-puck-4igh</link>
      <guid>https://dev.to/puckeditor/building-a-plugin-for-a-react-visual-editor-with-puck-4igh</guid>
      <description>&lt;p&gt;Page builders and visual editors have become central to modern product development. Frontend engineers, product teams, and content operations groups rely on them to build landing pages, dashboards, documentation systems, and internal tools quickly and consistently.&lt;/p&gt;

&lt;p&gt;As adoption grows, expectations grow with it. Editors must support customization, structured content, automation, and domain-specific workflows without increasing development and maintenance overhead.&lt;/p&gt;

&lt;p&gt;The market reflects this demand. The global low-code development platform market is projected to reach &lt;a href="https://www.psmarketresearch.com/market-analysis/low-code-development-platform-market" rel="noopener noreferrer"&gt;$167 billion by 2030&lt;/a&gt;. Organizations continue to invest in visual development systems that enable faster iteration and broader collaboration. As these systems expand in scope and adoption, the architectural responsibility placed on editor frameworks also increases.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gl7a82j7wd95bl9e7jd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gl7a82j7wd95bl9e7jd.png" alt="Low-code development platform market" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Growth introduces complexity. Every feature added directly to the core increases surface area and long-term maintenance costs. As teams introduce metadata panels, validation rules, workflow controls, and UI extensions, the editor becomes tightly coupled and difficult to evolve without clear extension boundaries.&lt;/p&gt;

&lt;p&gt;Plugin systems create those boundaries. They define controlled integration points, isolate functionality, and protect the editor’s foundation.&lt;/p&gt;

&lt;p&gt;In this article, we examine how to design plugin systems for visual editors and build a working example using &lt;a href="https://puckeditor.com/?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Puck&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Plugin System: Architecture Deep Dive
&lt;/h2&gt;

&lt;p&gt;A plugin system defines a controlled way to extend software without modifying its core. It exposes extension points through a stable contract and allows external modules to register new behavior, UI, or logic.&lt;/p&gt;

&lt;p&gt;The core remains responsible for orchestration, lifecycle management, and state ownership. Plugins operate within boundaries defined by that core.&lt;/p&gt;

&lt;p&gt;At an architectural level, a plugin system introduces three primary layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Core Engine&lt;/strong&gt;: Owns state, rendering, persistence, and lifecycle management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extension API (Plugin Contract)&lt;/strong&gt;: Defines how plugins register, what hooks they can access, and what capabilities they receive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin Modules&lt;/strong&gt;: Independent units that implement features through the exposed contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qq6p55e5zljf5yg8x06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2qq6p55e5zljf5yg8x06.png" alt="Architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This separation enforces control. The core decides when a plugin runs, where it renders, and what data it can access. The plugin does not directly mutate internal systems. It communicates through defined interfaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Architectural Properties
&lt;/h3&gt;

&lt;p&gt;A well-designed plugin system should provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt;: Plugins cannot corrupt the global state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Lifecycle&lt;/strong&gt;: Mount, update, and unmount phases are predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit Extension Points&lt;/strong&gt;: UI slots, event hooks, and state access are intentional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encapsulation&lt;/strong&gt;: Editor state and internal systems remain protected behind defined APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composable Registration&lt;/strong&gt;: Multiple plugins can coexist without conflict.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture scales by allowing new capabilities to be introduced without changing the foundation. Features can be enabled or removed independently, while the core remains lean, stable, and protected.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Plugins vs Core Features
&lt;/h2&gt;

&lt;p&gt;Extensible editors require deciding which capabilities belong in the core and which should be implemented as plugins. Core features define the editor’s fundamental architecture, such as rendering, state management, and persistence. Plugins extend the editor through defined extension points without modifying that foundation.&lt;/p&gt;

&lt;p&gt;The distinction becomes clearer when viewed side by side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0mb6ss1hz7d82ln9r1y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa0mb6ss1hz7d82ln9r1y.png" alt="distinction becomes clearer when viewed side by side in table" width="729" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Puck Implements the Plugin Contract
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://puckeditor.com/?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Puck&lt;/a&gt; is a React-based page builder that lets users create pages by dragging and dropping their own components. Developers register these components through configuration objects, and the editor uses them to build and render pages. Puck is fully customizable and extensible, providing APIs to extend editor behavior or package additional functionality as plugins.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtwjna3l3efuynewr2f7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frtwjna3l3efuynewr2f7.png" alt="Puck Editor Interface" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plugin integration in Puck is straightforward. Plugins can register sidebar panels, add controls, and respond to editor events through clear extension surfaces. They interact with the editor state through documented APIs without reaching into internal systems or modifying rendering logic directly.&lt;/p&gt;

&lt;p&gt;The plugin contract in Puck focuses on three responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Registration&lt;/strong&gt;: A plugin declares its identity and attaches to the editor during initialization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI Injection&lt;/strong&gt;: The plugin connects to defined surfaces such as sidebars or inspector regions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle Participation&lt;/strong&gt;: Plugins can hook into editor behavior such as loading, saving, or validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once registered, the plugin runs as part of your core editor integration. This keeps the editor implementation stable while allowing additional functionality to be added through independent plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building an Author Info Plugin using Puck
&lt;/h2&gt;

&lt;p&gt;We will build a simple Author Info plugin that demonstrates plugin registration, UI injection, and lifecycle participation inside Puck. The plugin will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a panel to the left sidebar&lt;/li&gt;
&lt;li&gt;Capture author name, role, and avatar&lt;/li&gt;
&lt;li&gt;Store this data alongside the page state&lt;/li&gt;
&lt;li&gt;Validate the metadata before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Create a New Puck App
&lt;/h3&gt;

&lt;p&gt;Start by generating a new app using the official &lt;a href="https://github.com/puckeditor/puck?tab=readme-ov-file#recipes?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Puck starter&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-puck-app author-info-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose the &lt;strong&gt;Next.js&lt;/strong&gt; option when prompted. After the setup completes, navigate to the project directory and run the application in development mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;author-info-plugin
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F471ooabis96z54hqwdrr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F471ooabis96z54hqwdrr.png" alt="Terminal" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open your browser at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Puck editor interface for the homepage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Understand the Plugin API
&lt;/h3&gt;

&lt;p&gt;Puck plugins can extend the editor interface through the &lt;a href="https://puckeditor.com/docs/extending-puck/plugins#rendering-ui-in-the-plugin-rail?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Plugin Rail&lt;/a&gt; on the left. Plugins may render UI in this rail, but they can also extend editor behavior through &lt;a href="https://puckeditor.com/docs/extending-puck/ui-overrides?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;overrides&lt;/a&gt; and other integrations.&lt;/p&gt;

&lt;p&gt;A plugin object looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;My Plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Icon&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;My UI&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plugins are wired into the editor by passing them to the &lt;code&gt;plugins&lt;/code&gt; prop of the &lt;a href="https://puckeditor.com/docs/api-reference/components/puck?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Puck /&amp;gt;&lt;/code&gt;&lt;/a&gt; component. &lt;/p&gt;

&lt;h3&gt;
  
  
  3. Create Your Plugin File
&lt;/h3&gt;

&lt;p&gt;Inside your project, create a file at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;app/puck/plugins/AuthorInfoPlugin.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can create it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; app/puck/plugins
&lt;span class="nb"&gt;touch &lt;/span&gt;app/puck/plugins/AuthorInfoPlugin.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since this example uses icons from &lt;strong&gt;lucide-react&lt;/strong&gt;, install it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;lucide-react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the file you created and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createUsePuck&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@puckeditor/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lucide-react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usePuck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createUsePuck&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AuthorInfoPlugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author-info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Author Info&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt;
  &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePuck&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePuck&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updateAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replaceRoot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Author Info&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Author Name"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateAuthor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Author Role"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateAuthor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Avatar URL"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;updateAuthor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;avatar&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This plugin renders a component that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads the current page data via &lt;a href="https://puckeditor.com/docs/extending-puck/internal-puck-api?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;&lt;code&gt;usePuck&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Updates author metadata via &lt;a href="https://puckeditor.com/docs/api-reference/puck-api#dispatchaction?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Puck’s dispatcher&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This demonstrates how plugins can integrate with the editor state.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Register the Plugin
&lt;/h3&gt;

&lt;p&gt;Open the route where the editor is rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;puck&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;puckPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the &lt;code&gt;&amp;lt;Puck /&amp;gt;&lt;/code&gt; component and update the &lt;code&gt;plugins&lt;/code&gt; prop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AuthorInfoPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../plugins/AuthorInfoPlugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Puck&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onPublish&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/pages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AuthorInfoPlugin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your plugin will appear in the Plugin Rail.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Test the Plugin UI
&lt;/h3&gt;

&lt;p&gt;Reload the editor at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the left Plugin Rail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click &lt;strong&gt;Author Info&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter a name, role, and avatar URL in the fields&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Publish&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of your &lt;code&gt;onPublish&lt;/code&gt; Implementation: the data will be sent to &lt;code&gt;/api/pages&lt;/code&gt; and saved to &lt;code&gt;database.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzyx5hgsusmzp1qajpyx9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzyx5hgsusmzp1qajpyx9.png" alt="Plugin UI" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Confirm Persistence
&lt;/h3&gt;

&lt;p&gt;After publishing, open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;database.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"root"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"props"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Your Author"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Writer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"avatar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/avatar.png"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zones"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms the author data was correctly persisted.&lt;/p&gt;

&lt;p&gt;Note: In the starter project, this data is saved to a local file, but in a real application, &lt;a href="https://puckeditor.com/docs/api-reference/components/puck#onpublishdata?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;it could be stored in any backend&lt;/a&gt;, such as a database or API service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c6e8ldl0eqe9zsyd72u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9c6e8ldl0eqe9zsyd72u.png" alt="Database json will appear like this" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  7. (Optional) Render Author Info on the Frontend
&lt;/h3&gt;

&lt;p&gt;To display author metadata on the frontend, update the page that renders your content. For example, open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;puckPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following below the &lt;code&gt;&amp;lt;Render /&amp;gt;&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Render&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Author&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;strong&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Avatar"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm0ypemu7gc97uxf4ssc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flm0ypemu7gc97uxf4ssc.png" alt="Final demo" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also place that in the &lt;a href="https://puckeditor.com/docs/integrating-puck/root-configuration#the-root-render-function?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;root configuration&lt;/a&gt; to display the saved metadata on published pages.&lt;/p&gt;

&lt;p&gt;That’s it. You now have a fully working &lt;strong&gt;Author Info plugin&lt;/strong&gt; that integrates with the editor state, renders a sidebar panel in the Plugin Rail, and persists metadata through the publishing flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Plugin systems give visual editors a structured path for growth. With clear contracts and defined extension points, teams can introduce new capabilities without reshaping the underlying architecture. This keeps responsibilities separated, reduces risk, and allows the platform to adapt as product requirements expand.&lt;/p&gt;

&lt;p&gt;The Author Info plugin we built using &lt;a href="https://puckeditor.com?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;Puck&lt;/a&gt; shows how an extension can be registered independently, integrate with the editor state, and persist structured metadata. The core remains stable while the plugin delivers focused functionality, demonstrating how modular design supports scalable and maintainable editor systems.&lt;/p&gt;

&lt;p&gt;For deeper implementation details, refer to the official documentation: &lt;a href="https://puckeditor.com/docs?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=building_plugins_with_puck" rel="noopener noreferrer"&gt;https://puckeditor.com/docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How Context-First MCP Design Reduces Agent Failures on Backend Tasks</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Mon, 09 Mar 2026 19:47:29 +0000</pubDate>
      <link>https://dev.to/astrodevil/how-context-first-mcp-design-reduces-agent-failures-on-backend-tasks-44jk</link>
      <guid>https://dev.to/astrodevil/how-context-first-mcp-design-reduces-agent-failures-on-backend-tasks-44jk</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem:&lt;/strong&gt; AI agents handle frontend tasks well, but backend operations expose a consistent gap. Even when MCP is connected, most backends return table names without record counts, schema without RLS state, and tool responses without success signals. The agent fills that gap with extra queries, retries, and guesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why existing backends fall short:&lt;/strong&gt; Platforms like Supabase and Postgres MCP were designed for human operators who can read a dashboard, check a UI, and make judgment calls. When an agent is the one operating the backend, that design assumption breaks at every step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What changes with an agent-native backend:&lt;/strong&gt; InsForge builds MCP into the backend from the start, not on top of it. Two tool calls give the agent a full map of the backend and deep context for the table it is working on, with live record counts, RLS state, and policy definitions returned before the agent writes a single query. Across 21 MCPMark tasks, the design produces 47.6% Pass⁴ accuracy, 30% fewer tokens, and 1.6x faster execution compared to Postgres MCP and Supabase MCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;AI agents handle frontend tasks well. Give one a component structure, a routing pattern, or a state management problem, and it will usually get it right. But backend operations are different.&lt;/p&gt;

&lt;p&gt;Ask an agent to implement Row Level Security on a production Postgres database. It will write the policies, run the migration, and return success. But if the MCP server it is connected to does not expose RLS status in its schema response, the agent has no way to verify what it actually changed. It assumes the operation worked. The policies it wrote may be incomplete, applied to the wrong roles, or missing entire tables. Nothing throws an error, but the data access rules are silently wrong.&lt;/p&gt;

&lt;p&gt;This is not a model quality problem. It is a context problem. And it happens consistently, across models, across tasks, even when MCP is part of the setup.&lt;/p&gt;

&lt;p&gt;In this article, we examine where agents consistently fail on backend tasks, what the current MCP layer design in most backends is missing, and how a backend built specifically for agents changes the execution pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;MCP Exists but Agents Still Fail.&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most major backend platforms have MCP servers now. Supabase has one. Postgres has one. The assumption is that once you connect an agent to a backend via MCP, the backend visibility problem is solved, but connecting to an MCP does not determine what it returns.&lt;/p&gt;

&lt;p&gt;MCP is a protocol. It defines how tools are called and how responses are structured. It does not define what those responses contain. That part is entirely up to whoever built the MCP server.&lt;/p&gt;

&lt;p&gt;Here is what Supabase MCP returns when an agent calls &lt;code&gt;list_tables&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;"users"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"orders"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"products"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"sessions"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains table names only, with no record counts, no foreign key relationships, no RLS status, no policy definitions, no trigger logic, and no index information.&lt;/p&gt;

&lt;p&gt;Supabase was built for a specific use case, and it does that well. Tools like Lovable, Bolt, and v0 use it because it is fast to set up, visually manageable, and good for getting a product off the ground quickly. The dashboard-first design works for that workflow because a human is always in the loop, reading errors, checking the UI, and making judgment calls.&lt;/p&gt;

&lt;p&gt;When an agent is the one operating the backend, there is no human in the loop. The agent cannot open the Supabase dashboard and read the RLS policy panel. It can only work with what the MCP tool returns. And what it returns is not enough to act correctly on anything beyond a basic read or write.&lt;/p&gt;

&lt;p&gt;So the agent does what any system does when it lacks information. It runs more queries. It guesses. It retries. Each of those extra steps costs tokens and time, and none of them are guaranteed to surface the right answer.&lt;/p&gt;

&lt;p&gt;Every extra query the agent runs, every retry, every guess is a symptom of the same underlying gap. The MCP layer was built for a human operator, and the agent is paying the cost of that assumption at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Agents Actually Fail
&lt;/h2&gt;

&lt;p&gt;The surface problem shows up in four consistent failure patterns. These are not edge cases or misconfigured setups. They happen across models, across tasks, and across backends that have MCP connected.&lt;/p&gt;

&lt;p&gt;Two of the failures below reference &lt;a href="https://mcpmark.ai/" rel="noopener noreferrer"&gt;MCPMark&lt;/a&gt; tasks. MCPMark is an open-source benchmark that measures how well MCP servers support language models on real database tasks. Each task is a non-trivial backend operation run against actual databases with real data volumes. The task names map directly to the operation being tested. They are used here because they are reproducible, measurable, and not hypothetical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 1: Non-Deterministic Tool Calls
&lt;/h3&gt;

&lt;p&gt;Agents interact with backends by calling tools, and some of those tools trigger real, irreversible operations like creating a resource, provisioning infrastructure, or writing to a database. When one of those call times out, the agent has no success signal to work from. The tool response came back empty, the operation has no ID, and the backend has no idempotency key, so the agent does what it was built to do: it retries. By the time the second request lands, the first one has already gone through. Two identical resources now exist in production, and neither the agent nor the developer caught it in real time.&lt;/p&gt;

&lt;p&gt;APIs built for humans assume someone will check the dashboard after an operation. Agents cannot do that. They need a deterministic success or failure signal from the tool response itself, or they will keep retrying until something breaks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 2: Schema Blindness in Real Databases
&lt;/h3&gt;

&lt;p&gt;The MCPMark task &lt;code&gt;employees__employee_demographics_report&lt;/code&gt; asks an agent to generate gender statistics, age group breakdowns, birth month distributions, and hiring year summaries from an HR database.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;employee&lt;/code&gt; table had 300,024 rows. The &lt;code&gt;salary&lt;/code&gt; table had 2,844,047 rows. That is roughly 9.5 salary records per employee.&lt;/p&gt;

&lt;p&gt;The MCP server returns table names and column definitions. No record counts. The agent sees a join between two tables and writes the most natural query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;SELECT&lt;/span&gt; &lt;span class="nt"&gt;gender&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;COUNT&lt;/span&gt;&lt;span class="o"&gt;(*)&lt;/span&gt;  &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;counts&lt;/span&gt; &lt;span class="nt"&gt;salary&lt;/span&gt; &lt;span class="nt"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;not&lt;/span&gt; &lt;span class="nt"&gt;employees&lt;/span&gt;
&lt;span class="nt"&gt;FROM&lt;/span&gt; &lt;span class="nt"&gt;employee&lt;/span&gt; &lt;span class="nt"&gt;e&lt;/span&gt;
&lt;span class="nt"&gt;LEFT&lt;/span&gt; &lt;span class="nt"&gt;JOIN&lt;/span&gt; &lt;span class="nt"&gt;salary&lt;/span&gt; &lt;span class="nt"&gt;s&lt;/span&gt; &lt;span class="nt"&gt;ON&lt;/span&gt; &lt;span class="nt"&gt;e&lt;/span&gt;&lt;span class="nc"&gt;.id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;s&lt;/span&gt;&lt;span class="nc"&gt;.employee_id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query runs clean. No error. The output is 9.5 times too large because &lt;code&gt;COUNT(*)&lt;/code&gt; multiplies across joined salary rows instead of counting distinct employees. The agent returns it as correct.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkc0pbl6oryg8ahlphi6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkc0pbl6oryg8ahlphi6u.png" alt="Why COUNT(*) returns the wrong number on a joined table, and what the correct query looks like" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why COUNT(*) returns the wrong number on a joined table, and what the correct query looks like&lt;/p&gt;

&lt;p&gt;The backend had the record counts. It just never surfaced them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 3: Missing Context Compounds Cost
&lt;/h3&gt;

&lt;p&gt;The MCPMark task &lt;code&gt;security__rls_business_access&lt;/code&gt; asks an agent to implement Row Level Security policies across 5 tables on a social media platform.&lt;/p&gt;

&lt;p&gt;The MCP server's &lt;code&gt;get_object_details&lt;/code&gt; returns schema, columns, constraints, and indexes for each table. No &lt;code&gt;rlsEnabled&lt;/code&gt; field. No &lt;code&gt;policies&lt;/code&gt; array. The agent does not know RLS is disabled. It has to run a separate SQL query just to check the current RLS status before it can start the actual task. Then it runs verification queries after implementation to confirm the policies applied correctly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;list_schemas&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;schema&lt;/span&gt; &lt;span class="nt"&gt;names&lt;/span&gt; &lt;span class="nt"&gt;only&lt;/span&gt;
&lt;span class="nt"&gt;list_objects&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="nt"&gt;names&lt;/span&gt; &lt;span class="nt"&gt;only&lt;/span&gt;
&lt;span class="nt"&gt;get_object_details&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;5&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;no&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt; &lt;span class="nt"&gt;status&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt;           &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;check&lt;/span&gt; &lt;span class="nt"&gt;current&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt; &lt;span class="nt"&gt;status&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;policies&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;verification&lt;/span&gt; &lt;span class="nt"&gt;queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;23 turns. 581K tokens. Every extra query in that path exists because one field was missing from the original response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure 4: No Guardrails for Autonomous Operations
&lt;/h3&gt;

&lt;p&gt;An agent runs a schema migration. The migration has a logic error. There is no audit log of what the agent changed, no agent-scoped permissions limiting what operations it can run, and no rollback path. Production breaks. Nothing was recorded about what happened.&lt;/p&gt;

&lt;p&gt;Backends built for human developers assume a human is reviewing the migration before it runs. When an agent is the one initiating the change, that assumption breaks. There is no mechanism to scope what the agent is allowed to modify, no record of what it actually did, and no way to recover cleanly if something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an Agent-Native Backend Looks Like
&lt;/h2&gt;

&lt;p&gt;The failures in the previous section share a common cause. The backend gave the agent a name when it needed a state. A table name when it needed a record count. A schema when it needed a policy definition.&lt;/p&gt;

&lt;p&gt;Fixing this is not about a better prompt or a smarter model. It is about what the MCP layer returns by default. That means the backend itself has to be designed around what an agent needs to see, not retrofitted with MCP after the fact.&lt;/p&gt;

&lt;p&gt;That is a different design starting point entirely. It is what separates a backend built for humans from one built for agents. &lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;InsForge&lt;/a&gt; is built on that starting point.&lt;/p&gt;

&lt;p&gt;InsForge is an open-source backend platform built for AI-assisted development. It provides database management, authentication, storage, serverless functions, and AI integrations, with APIs structured specifically for deterministic agent execution. Unlike backends that expose MCP as a layer on top of a human-facing platform, InsForge builds the MCP design into the &lt;a href="https://github.com/InsForge/InsForge/tree/main/backend" rel="noopener noreferrer"&gt;backend&lt;/a&gt; itself.&lt;/p&gt;

&lt;p&gt;Its &lt;a href="https://github.com/InsForge/InsForge/tree/main/openapi" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; is built around two principles: hierarchical context and a live source of truth. Every tool call is designed so the agent gets exactly what it needs for the current step, with a clear signal pointing to what it needs next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb10hkzq744nq6fpkiqf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb10hkzq744nq6fpkiqf9.png" alt="Backend retrofitted with MCP vs a backend built around agent execution from the start" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend retrofitted with MCP vs a backend built around agent execution from the start&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In practice, **that means two layers, one that gives the agent a map of the entire backend, and one that gives it the full detail for exactly the table it is working on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: Global Context with &lt;code&gt;get-backend-metadata&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The first call an agent makes is &lt;code&gt;get-backend-metadata&lt;/code&gt;. It returns the full backend surface in one response: every table with its live record count, auth configuration, storage buckets, AI model integrations, and a built-in hint that tells the agent exactly what tool to call next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;"auth":&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;"oauths":&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;
      &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;"provider":&lt;/span&gt; &lt;span class="err"&gt;"google",&lt;/span&gt;
        &lt;span class="err"&gt;"clientId":&lt;/span&gt; &lt;span class="err"&gt;null,&lt;/span&gt;
        &lt;span class="err"&gt;"redirectUri":&lt;/span&gt; &lt;span class="err"&gt;null,&lt;/span&gt;
        &lt;span class="err"&gt;"scopes":&lt;/span&gt; &lt;span class="err"&gt;["openid",&lt;/span&gt; &lt;span class="err"&gt;"email",&lt;/span&gt; &lt;span class="err"&gt;"profile"],&lt;/span&gt;
        &lt;span class="err"&gt;"useSharedKey":&lt;/span&gt; &lt;span class="err"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;"database"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;"tables":&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;
      &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"tableName":&lt;/span&gt; &lt;span class="err"&gt;"users",&lt;/span&gt; &lt;span class="err"&gt;"recordCount":&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;"hint"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"To retrieve detailed schema information for a specific table, call the get-table-schema tool with the table name."&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;"storage"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;"buckets":&lt;/span&gt; &lt;span class="err"&gt;[],&lt;/span&gt;
    &lt;span class="err"&gt;"totalSizeInGB":&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;"aiIntegration"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;"models":&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;
      &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;"inputModality":&lt;/span&gt; &lt;span class="err"&gt;["text",&lt;/span&gt; &lt;span class="err"&gt;"image"],&lt;/span&gt;
        &lt;span class="err"&gt;"outputModality":&lt;/span&gt; &lt;span class="err"&gt;["text"],&lt;/span&gt;
        &lt;span class="err"&gt;"modelId":&lt;/span&gt; &lt;span class="err"&gt;"anthropic/claude-sonnet-4.5"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;"version"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"1.0.0"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;recordCount&lt;/code&gt; field directly resolves Failure 2. Before writing a single query, the agent already knows there are 300,024 employee rows and 2,844,047 salary rows. It knows this is a many-to-one relationship. It knows &lt;code&gt;COUNT(*)&lt;/code&gt; on a join will multiply rows. It writes &lt;code&gt;COUNT(DISTINCT e.id)&lt;/code&gt; on the first attempt.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hint&lt;/code&gt; field resolves the discovery loop problem. The agent does not have to guess what tool to call next or run exploratory queries to understand the backend topology. The response tells it directly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92b2viv9rcvaf6t219yw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F92b2viv9rcvaf6t219yw.png" alt="Everything the agent gets from a single get-backend-metadata call" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything the agent gets from a single get-backend-metadata call&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Local Context with &lt;code&gt;get-table-schema&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Once the agent knows which table it needs to work with, it calls &lt;code&gt;get-table-schema&lt;/code&gt;. This returns the full definition for that table in a single response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;"users":&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;"schema":&lt;/span&gt; &lt;span class="err"&gt;[&lt;/span&gt;
      &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"id",&lt;/span&gt; &lt;span class="err"&gt;"dataType":&lt;/span&gt; &lt;span class="err"&gt;"uuid",&lt;/span&gt; &lt;span class="err"&gt;"isNullable":&lt;/span&gt; &lt;span class="err"&gt;"NO",&lt;/span&gt; &lt;span class="err"&gt;"columnDefault":&lt;/span&gt; &lt;span class="err"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"nickname",&lt;/span&gt; &lt;span class="err"&gt;"dataType":&lt;/span&gt; &lt;span class="err"&gt;"text",&lt;/span&gt; &lt;span class="err"&gt;"isNullable":&lt;/span&gt; &lt;span class="err"&gt;"YES",&lt;/span&gt; &lt;span class="err"&gt;"columnDefault":&lt;/span&gt; &lt;span class="err"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"bio",&lt;/span&gt; &lt;span class="err"&gt;"dataType":&lt;/span&gt; &lt;span class="err"&gt;"text",&lt;/span&gt; &lt;span class="err"&gt;"isNullable":&lt;/span&gt; &lt;span class="err"&gt;"YES",&lt;/span&gt; &lt;span class="err"&gt;"columnDefault":&lt;/span&gt; &lt;span class="err"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"created_at",&lt;/span&gt; &lt;span class="err"&gt;"dataType":&lt;/span&gt; &lt;span class="err"&gt;"timestamp&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;time&lt;/span&gt; &lt;span class="err"&gt;zone",&lt;/span&gt; &lt;span class="err"&gt;"isNullable":&lt;/span&gt; &lt;span class="err"&gt;"YES",&lt;/span&gt; &lt;span class="err"&gt;"columnDefault":&lt;/span&gt; &lt;span class="err"&gt;"now()"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"updated_at",&lt;/span&gt; &lt;span class="err"&gt;"dataType":&lt;/span&gt; &lt;span class="err"&gt;"timestamp&lt;/span&gt; &lt;span class="err"&gt;with&lt;/span&gt; &lt;span class="err"&gt;time&lt;/span&gt; &lt;span class="err"&gt;zone",&lt;/span&gt; &lt;span class="err"&gt;"isNullable":&lt;/span&gt; &lt;span class="err"&gt;"YES",&lt;/span&gt; &lt;span class="err"&gt;"columnDefault":&lt;/span&gt; &lt;span class="err"&gt;"now()"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;"indexes"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;"indexname":&lt;/span&gt; &lt;span class="err"&gt;"users_pkey",&lt;/span&gt;
        &lt;span class="err"&gt;"indexdef":&lt;/span&gt; &lt;span class="err"&gt;"CREATE&lt;/span&gt; &lt;span class="err"&gt;UNIQUE&lt;/span&gt; &lt;span class="err"&gt;INDEX&lt;/span&gt; &lt;span class="err"&gt;users_pkey&lt;/span&gt; &lt;span class="err"&gt;ON&lt;/span&gt; &lt;span class="err"&gt;public.users&lt;/span&gt; &lt;span class="err"&gt;USING&lt;/span&gt; &lt;span class="err"&gt;btree&lt;/span&gt; &lt;span class="err"&gt;(id)",&lt;/span&gt;
        &lt;span class="err"&gt;"isUnique":&lt;/span&gt; &lt;span class="err"&gt;true,&lt;/span&gt;
        &lt;span class="err"&gt;"isPrimary":&lt;/span&gt; &lt;span class="err"&gt;true&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;"foreignKeys"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;"constraintName":&lt;/span&gt; &lt;span class="err"&gt;"users_id_fkey",&lt;/span&gt;
        &lt;span class="err"&gt;"columnName":&lt;/span&gt; &lt;span class="err"&gt;"id",&lt;/span&gt;
        &lt;span class="err"&gt;"foreignTableName":&lt;/span&gt; &lt;span class="err"&gt;"accounts",&lt;/span&gt;
        &lt;span class="err"&gt;"foreignColumnName":&lt;/span&gt; &lt;span class="err"&gt;"id",&lt;/span&gt;
        &lt;span class="err"&gt;"deleteRule":&lt;/span&gt; &lt;span class="err"&gt;"CASCADE",&lt;/span&gt;
        &lt;span class="err"&gt;"updateRule":&lt;/span&gt; &lt;span class="err"&gt;"NO&lt;/span&gt; &lt;span class="err"&gt;ACTION"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;"rlsEnabled"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;true&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;"policies"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="err"&gt;"policyname":&lt;/span&gt; &lt;span class="err"&gt;"Enable&lt;/span&gt; &lt;span class="err"&gt;read&lt;/span&gt; &lt;span class="err"&gt;access&lt;/span&gt; &lt;span class="err"&gt;for&lt;/span&gt; &lt;span class="err"&gt;all&lt;/span&gt; &lt;span class="err"&gt;users",&lt;/span&gt;
        &lt;span class="err"&gt;"cmd":&lt;/span&gt; &lt;span class="err"&gt;"SELECT",&lt;/span&gt;
        &lt;span class="err"&gt;"roles":&lt;/span&gt; &lt;span class="err"&gt;"{public&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;qual&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;true&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;withCheck&lt;/span&gt;&lt;span class="s1"&gt;": null
      },
      {
        "&lt;/span&gt;&lt;span class="nt"&gt;policyname&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;Disable&lt;/span&gt; &lt;span class="nt"&gt;delete&lt;/span&gt; &lt;span class="nt"&gt;for&lt;/span&gt; &lt;span class="nt"&gt;users&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;cmd&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;DELETE&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;qual&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;false&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;withCheck&lt;/span&gt;&lt;span class="s1"&gt;": null
      },
      {
        "&lt;/span&gt;&lt;span class="nt"&gt;policyname&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;Enable&lt;/span&gt; &lt;span class="nt"&gt;update&lt;/span&gt; &lt;span class="nt"&gt;for&lt;/span&gt; &lt;span class="nt"&gt;users&lt;/span&gt; &lt;span class="nt"&gt;based&lt;/span&gt; &lt;span class="nt"&gt;on&lt;/span&gt; &lt;span class="nt"&gt;user_id&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;cmd&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;UPDATE&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;authenticated&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;qual&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;withCheck&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;uid&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;"
      },
      {
        "&lt;/span&gt;&lt;span class="nt"&gt;policyname&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;Allow&lt;/span&gt; &lt;span class="nt"&gt;project_admin&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="nt"&gt;update&lt;/span&gt; &lt;span class="nt"&gt;any&lt;/span&gt; &lt;span class="nt"&gt;user&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;cmd&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;UPDATE&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;roles&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;project_admin&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;qual&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;true&lt;/span&gt;&lt;span class="s1"&gt;",
        "&lt;/span&gt;&lt;span class="nt"&gt;withCheck&lt;/span&gt;&lt;span class="s1"&gt;": "&lt;/span&gt;&lt;span class="nt"&gt;true&lt;/span&gt;&lt;span class="s1"&gt;"
      }
    ],
    "&lt;/span&gt;&lt;span class="nt"&gt;triggers&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;
  &lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the agent knows which table it needs to work with, it calls &lt;a href="https://github.com/InsForge/InsForge/tree/main/shared-schemas" rel="noopener noreferrer"&gt;get-table-schema&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This single response resolves Failure 3 completely. The agent sees &lt;code&gt;rlsEnabled: true&lt;/code&gt; and the full policy definitions before it writes any SQL. It knows exactly which roles have access to which operations, what the &lt;code&gt;qual&lt;/code&gt; conditions are, and what &lt;code&gt;withCheck&lt;/code&gt; constraints apply. With full policy definitions and RLS state returned upfront, there is nothing left to discover and guess.&lt;/p&gt;

&lt;p&gt;Compare this to what Postgres MCP returns for the same table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;"basic":&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;"schema":&lt;/span&gt; &lt;span class="err"&gt;"public",&lt;/span&gt; &lt;span class="err"&gt;"name":&lt;/span&gt; &lt;span class="err"&gt;"users",&lt;/span&gt; &lt;span class="err"&gt;"type":&lt;/span&gt; &lt;span class="err"&gt;"table"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;"columns"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[...],&lt;/span&gt;
  &lt;span class="s1"&gt;"constraints"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[...],&lt;/span&gt;
  &lt;span class="s1"&gt;"indexes"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;[...]&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;rlsEnabled&lt;/code&gt; field. No &lt;code&gt;policies&lt;/code&gt; array. The agent has no way to know RLS exists on this table from this response alone. It proceeds, hits a permission error, and starts the retry loop that costs 285K extra tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cm84xwj7h09l53ltlxl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3cm84xwj7h09l53ltlxl.png" alt="Postgres MCP vs InsForge: what the same table call returns" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postgres MCP vs InsForge: what the same table call returns&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Is an Architecture Decision, Not a Feature
&lt;/h3&gt;

&lt;p&gt;The two-layer design is intentional. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get-backend-metadata&lt;/code&gt; is global context: it gives the agent a high-level map of the entire backend without overloading the context window. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get-table-schema&lt;/code&gt; is local context: scoped, deep, and called only for the table the agent is actively working on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This matters because of how context windows work in practice. Loading full schema details for every table upfront can consume 20K to 30K tokens of irrelevant information, pushing out logic the agent wrote earlier in the session. The hierarchical design keeps the context window clean while ensuring the agent always has what it needs for the current operation.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;hint&lt;/code&gt; field in &lt;code&gt;get-backend-metadata&lt;/code&gt; is what connects the two layers. The agent does not have to reason about what to fetch next. The backend tells it. That is the difference between a backend that was retrofitted with MCP and one that was designed around how agents actually operate.&lt;/p&gt;

&lt;p&gt;The context layer handles Failures 2 and 3. But a fully agent-native backend has to go further. Failures 1 and 4, non-deterministic operations and unguarded autonomous changes, are addressed at the platform level. InsForge's tool contracts return deterministic success and failure signals by design. Agent-initiated schema changes are logged, scoped, and reversible. The MCP layer and the platform contract layer work together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqdt66nmw2p0n454ggww.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqdt66nmw2p0n454ggww.png" alt="How the two-layer context cycle works: global map first, table detail second" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How the two-layer context cycle works: global map first, table detail second&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;The architecture described in the previous section is not a theoretical improvement. MCPMark makes it measurable.&lt;/p&gt;

&lt;p&gt;InsForge, Supabase MCP, and Postgres MCP were all evaluated against the same 21 tasks using Anthropic Claude Sonnet 4.5 as the model. Each task was run 4 consecutive times.&lt;/p&gt;

&lt;p&gt;The accuracy metric used is Pass⁴. A task counts as successful only if the agent completes it correctly in all four independent runs. Not once. Not three out of four. All four. This is what reliability actually looks like in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case Study 1: Demographics Report
&lt;/h3&gt;

&lt;p&gt;Task: &lt;code&gt;employees__employee_demographics_report&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Generate gender statistics, age group breakdowns, birth month distributions, and hiring year summaries from an HR database with an &lt;code&gt;employee&lt;/code&gt; table (300,024 rows) and a &lt;code&gt;salary&lt;/code&gt; table (2,844,047 rows).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Success Rate&lt;/th&gt;
&lt;th&gt;Tokens Used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;InsForge&lt;/td&gt;
&lt;td&gt;4/4 (100%)&lt;/td&gt;
&lt;td&gt;207K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase MCP&lt;/td&gt;
&lt;td&gt;3/4 (75%)&lt;/td&gt;
&lt;td&gt;204K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres MCP&lt;/td&gt;
&lt;td&gt;2/4 (50%)&lt;/td&gt;
&lt;td&gt;220K&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Token usage is similar across all three. The failures are not a cost problem. They are a correctness problem caused entirely by missing record count information.&lt;/p&gt;

&lt;p&gt;Neither Supabase MCP nor Postgres MCP tells the agent how many rows each table contains. The agent sees a join between two tables and writes the most natural query, which counts salary rows instead of employees. The output is 9.5 times too large. No error is thrown. The agent returns it as correct.&lt;/p&gt;

&lt;p&gt;InsForge surfaces record counts in the first call, so the agent sees the row ratio before writing any SQL, knows COUNT() will multiply rows on this join, and writes COUNT(DISTINCT e.id) on the first attempt. 4/4 every time. The only difference is two fields in the first MCP response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case Study 2: RLS Setup
&lt;/h3&gt;

&lt;p&gt;Task: &lt;code&gt;security__rls_business_access&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Implement Row Level Security policies across 5 tables on a social media platform: users, channels, posts, comments, channel_moderators.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Success Rate&lt;/th&gt;
&lt;th&gt;Tokens Used&lt;/th&gt;
&lt;th&gt;Turns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;InsForge&lt;/td&gt;
&lt;td&gt;4/4 (100%)&lt;/td&gt;
&lt;td&gt;296K&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase MCP&lt;/td&gt;
&lt;td&gt;1/4 (25%)&lt;/td&gt;
&lt;td&gt;340K&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres MCP&lt;/td&gt;
&lt;td&gt;4/4 (100%)&lt;/td&gt;
&lt;td&gt;581K&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;InsForge and Postgres MCP both reach 100% accuracy. But Postgres MCP uses 581K tokens and 23 turns to get there. InsForge uses 296K tokens and 15 turns. The 285K token difference is not model behavior. It is the direct cost of the agent not knowing RLS state upfront.&lt;/p&gt;

&lt;p&gt;Postgres MCP's execution path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;list_schemas&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;schema&lt;/span&gt; &lt;span class="nt"&gt;names&lt;/span&gt; &lt;span class="nt"&gt;only&lt;/span&gt;
&lt;span class="nt"&gt;list_objects&lt;/span&gt;          &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;table&lt;/span&gt; &lt;span class="nt"&gt;names&lt;/span&gt; &lt;span class="nt"&gt;only&lt;/span&gt;
&lt;span class="nt"&gt;get_object_details&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;5&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;no&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt; &lt;span class="nt"&gt;status&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt;           &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;query&lt;/span&gt; &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="nt"&gt;check&lt;/span&gt; &lt;span class="nt"&gt;current&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt; &lt;span class="nt"&gt;status&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;8&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;policies&lt;/span&gt;
&lt;span class="nt"&gt;execute_sql&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;4&lt;/span&gt;       &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;verification&lt;/span&gt; &lt;span class="nt"&gt;queries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;InsForge's execution path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;get-instructions&lt;/span&gt;        &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;learns&lt;/span&gt; &lt;span class="nt"&gt;InsForge&lt;/span&gt; &lt;span class="nt"&gt;workflow&lt;/span&gt;
&lt;span class="nt"&gt;get-backend-metadata&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;all&lt;/span&gt; &lt;span class="err"&gt;5&lt;/span&gt; &lt;span class="nt"&gt;tables&lt;/span&gt; &lt;span class="nt"&gt;at&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="nt"&gt;glance&lt;/span&gt;
&lt;span class="nt"&gt;get-table-schema&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;5&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;full&lt;/span&gt; &lt;span class="nt"&gt;schema&lt;/span&gt; &lt;span class="nt"&gt;with&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt; &lt;span class="nt"&gt;status&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;run-raw-sql&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="err"&gt;6&lt;/span&gt;         &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;functions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;RLS&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;create&lt;/span&gt; &lt;span class="nt"&gt;policies&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Postgres agent runs extra queries to check RLS status and then verify the policies applied correctly because the information was never in the original response. InsForge skips both phases entirely because it was already there.&lt;/p&gt;

&lt;p&gt;Supabase MCP reaches only 1/4. Without visibility into existing policies or RLS state, the agent could not reliably implement the required security model across 4 consecutive runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Aggregate Results
&lt;/h3&gt;

&lt;p&gt;Across all 21 tasks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;InsForge&lt;/th&gt;
&lt;th&gt;Postgres MCP&lt;/th&gt;
&lt;th&gt;Supabase MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pass⁴ Accuracy&lt;/td&gt;
&lt;td&gt;47.6%&lt;/td&gt;
&lt;td&gt;38.1%&lt;/td&gt;
&lt;td&gt;28.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg Tokens Per Run&lt;/td&gt;
&lt;td&gt;8.2M&lt;/td&gt;
&lt;td&gt;10.4M&lt;/td&gt;
&lt;td&gt;11.6M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg Time Per Task&lt;/td&gt;
&lt;td&gt;150 seconds&lt;/td&gt;
&lt;td&gt;200+ seconds&lt;/td&gt;
&lt;td&gt;200+ seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;InsForge is 1.6x faster, uses 30% fewer tokens, and achieves 47.6% Pass⁴ accuracy against Postgres MCP's 38.1% and Supabase MCP's 28.6%.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib8qqmgxry4qmwv130z4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib8qqmgxry4qmwv130z4.png" alt="Token cost of missing RLS context across three backends on the same task" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token cost of missing RLS context across three backends on the same task&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The accuracy gap is significant given what Pass⁴ measures. Passing once is not the bar. The bar is passing the same complex backend operation 4 times in a row, without mistakes, without retries caused by missing context. At that bar, InsForge is the only backend that consistently clears it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The future of agent-native development will not be defined by better models alone. It will be defined by what those models can see, the context they are given, the signals they receive, and the backend layer that determines both.&lt;/p&gt;

&lt;p&gt;If this is the problem you are working on, InsForge is open source and we welcome contributions from the community. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try &lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;InsForge&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quickstart guide &lt;a href="https://github.com/InsForge/InsForge?tab=readme-ov-file#quickstart" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI Slop vs Constrained UI: Why Most Generative Interfaces Fail</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Tue, 24 Feb 2026 11:52:06 +0000</pubDate>
      <link>https://dev.to/puckeditor/ai-slop-vs-constrained-ui-why-most-generative-interfaces-fail-pm9</link>
      <guid>https://dev.to/puckeditor/ai-slop-vs-constrained-ui-why-most-generative-interfaces-fail-pm9</guid>
      <description>&lt;h3&gt;
  
  
  TL;DR
&lt;/h3&gt;

&lt;p&gt;AI can generate structured interface layouts and assemble component hierarchies from natural language prompts. When generation is unconstrained, the output diverges from design systems, lacks determinism, and requires downstream refactoring before deployment. Production-grade generative UI requires predefined component registries, validated schemas, and explicit architectural constraints, which is the model implemented by &lt;a href="https://puckeditor.com/?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;Puck&lt;/a&gt; and its constrained generation layer, &lt;a href="https://puckeditor.com/docs/ai/overview?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;Puck AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faws6m1ko2u1fc2f41li8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faws6m1ko2u1fc2f41li8.png" alt="Puck AI" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;AI systems can now generate complete interface layouts from natural language prompts. Tasks that previously required manual section planning, component selection, and layout composition can now start from a single instruction.&lt;/p&gt;

&lt;p&gt;Yet when these generated interfaces are evaluated against real production standards, limitations become clear. Outputs often conflict with established design systems, introduce structural inconsistencies, and produce layouts that require refactoring before integration.&lt;/p&gt;

&lt;p&gt;What appears efficient at the prompt layer frequently shifts complexity downstream into engineering workflows. This gap between demo output and deployable software has led to growing skepticism around generative UI, sometimes informally labeled as “AI slop” to describe output that appears complete but fails architectural validation.&lt;/p&gt;

&lt;p&gt;In this article, we will examine where AI meaningfully supports interface generation, where it breaks down in production environments, and why constrained, schema-driven systems are essential for making generative UI operational at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What AI Is Actually Good At in UI Workflows
&lt;/h2&gt;

&lt;p&gt;Before evaluating the limitations of generative UI, it is important to isolate where it provides measurable value. AI is effective at accelerating early-stage interface assembly, particularly when the objective is to convert high-level intent into an initial layout draft.&lt;/p&gt;

&lt;p&gt;Consider a product team building a new SaaS landing page. A prompt such as “Create a landing page for an AI analytics platform with a hero section, feature highlights, pricing tiers, and customer testimonials” can reliably produce a logically organized page structure within seconds. The output may not be production-ready, but it establishes a usable structural baseline.&lt;/p&gt;

&lt;p&gt;AI performs well in the following areas:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k4rvau8ysli5k5xcaki.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4k4rvau8ysli5k5xcaki.png" alt="UI workflows" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Translating Intent into Layout Structure&lt;/strong&gt;: AI can map abstract, well-known requirements into recognizable interface patterns. For example, it understands that a landing page typically includes a hero, feature highlights, social proof, and a call to action. This reduces the cognitive load of structuring the page from scratch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generating First-Pass Scaffolding&lt;/strong&gt;: AI can assemble a preliminary hierarchy of sections and placeholder content. This enables teams to visualize information architecture quickly before investing time in refinement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automating Repetitive Component Assembly&lt;/strong&gt;: When working with predefined components, AI can configure repeated structures such as feature cards, pricing tiers, or testimonial blocks with consistent prop patterns. This is particularly useful in systems with modular design libraries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Supporting Rapid Experimentation&lt;/strong&gt;: AI allows teams to generate multiple layout variations in minutes, enabling faster exploration of structural alternatives without manual reconfiguration.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI is therefore effective at structural acceleration, particularly in early-stage development where system rules, design boundaries, and composition patterns are still being defined.&lt;/p&gt;

&lt;p&gt;In established product environments, however, structural acceleration must operate within existing architectural constraints to remain viable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Unbounded UI Generation Breaks Down
&lt;/h2&gt;

&lt;p&gt;Many unbounded generation tools produce raw code that must be reviewed, integrated, and redeployed before it can be used in production. Even when the output appears complete, it is not directly executable within an existing system. This shifts responsibility to engineering teams and introduces friction into workflows that are intended to be self-service.&lt;/p&gt;

&lt;p&gt;For non-technical users such as marketers or content authors, this model is impractical. Page updates require developer involvement, slowing content publishing and limiting autonomy. Instead of enabling cross-functional workflows, generative UI becomes a developer-only tool.&lt;/p&gt;

&lt;p&gt;Below are a few additional architectural limitations to consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design System Violations&lt;/strong&gt;: Unbounded generation does not inherently respect spacing scales, typography tokens, or component composition rules. It may introduce arbitrary margins, inconsistent heading hierarchies, or layout patterns that are not part of the approved system. Even if visually acceptable, these deviations fragment the design language and undermine maintainability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inconsistent Component Usage&lt;/strong&gt;: In systems with established component libraries, specific components are intended for specific contexts. Free-form generation may misuse primitives, duplicate existing abstractions, or bypass higher-level components entirely. This creates parallel patterns that increase technical debt and weaken reuse.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Non-Deterministic Outputs&lt;/strong&gt;: Identical prompts can yield structurally different layouts across executions. In production environments, this lack of determinism complicates testing, review processes, and content governance. Predictability is a requirement for scalable systems.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Brand and Compliance Drift&lt;/strong&gt;: Without embedded context, models default to generic language and layout conventions. They lack awareness of regulatory constraints, accessibility standards, and brand-specific positioning. This introduces risk in industries where messaging and structure must adhere to policy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Output Requiring Engineering Cleanup&lt;/strong&gt;: Generated code or markup frequently requires normalization before integration. Engineers must refactor styles, align components with existing abstractions, and correct structural inconsistencies. The perceived acceleration at generation time is offset by downstream rework.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free-form UI generation, therefore, conflicts with production architecture. Systems designed for reliability, reuse, and governance require structured constraints, not unconstrained synthesis.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Layer: Constraints
&lt;/h2&gt;

&lt;p&gt;In this context, constraints define what can be generated, how it can be configured, how components can be composed, and how the result is represented at runtime. In real systems, those constraints are implemented through concrete mechanisms like component registry boundaries, schema validation, composition rules, and structured runtime output.&lt;/p&gt;

&lt;p&gt;Component registry boundaries limit generation to approved React components. Instead of synthesizing arbitrary markup, the model assembles interfaces from predefined primitives such as &lt;code&gt;Hero&lt;/code&gt;, &lt;code&gt;FeatureGrid&lt;/code&gt;, or &lt;code&gt;PricingTable&lt;/code&gt;. Prop schema enforcement validates inputs against typed definitions, ensuring that required fields, enumerations, and data shapes conform to system expectations. For example, a pricing component may require a structured array of tiers with defined attributes rather than free-form content.&lt;/p&gt;

&lt;p&gt;Layout rules and composition limits restrict how components can be nested, preventing structurally invalid trees. Business context injection embeds brand, regulatory, or domain constraints directly into the generation process. Deterministic output structures, typically expressed as structured JSON, ensure predictable rendering and traceable state transitions across environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj060lzeprx47gh3il62d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj060lzeprx47gh3il62d.png" alt="Missing Layer: Constraints" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Puck AI Implements Architectural Constraints
&lt;/h2&gt;

&lt;p&gt;With Puck and Puck AI, the constraints we discussed above are implemented and enforced at the system level rather than inferred at prompt time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://puckeditor.com/docs/ai/getting-started?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;Puck AI is a generative UI layer&lt;/a&gt; built on top of Puck’s React visual editor that enables page generation within predefined architectural boundaries.&lt;/p&gt;

&lt;p&gt;It operates by assembling interfaces exclusively from registered React components instead of generating code. When a user requests something like “a landing page for an AI analytics platform,” the system composes that page from the components already configured in the application. The result is a deterministic component tree interpreted by Puck at runtime, keeping everything aligned with the existing design system and application logic.&lt;/p&gt;

&lt;p&gt;In this workflow, generation is an orchestration process over defined primitives. The AI behavior is shaped by the editor configuration and the components supplied by the development team.&lt;/p&gt;


  
  Your browser does not support the video tag.


&lt;h3&gt;
  
  
  Puck AI Characteristics
&lt;/h3&gt;

&lt;p&gt;Puck AI demonstrates bounded generation through the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generation from Registered React Components&lt;/strong&gt;: The AI selects and composes only those components explicitly registered in the Puck &lt;a href="https://puckeditor.com/docs/integrating-puck/component-configuration?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;configuration&lt;/a&gt;. If the system exposes Hero, FeatureGrid, and PricingTable, those are the only structural primitives available. No new layout elements are introduced outside the approved library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Structured Page Schema Output&lt;/strong&gt;: The result of the generation is a &lt;a href="https://puckeditor.com/docs/api-reference/data-model/data?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;structured page definition&lt;/a&gt; that maps component types to their configured props and hierarchical placement. This schema is interpreted by Puck at runtime to render the interface. The AI does not directly control rendering logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://puckeditor.com/docs/ai/business-context?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;Business Context and Brand Rules&lt;/a&gt;&lt;/strong&gt;: Configuration layers allow teams to define tone, domain context, and structural expectations. These parameters influence how sections are assembled and how content fields are populated, ensuring alignment with product positioning and organizational standards.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Design System Preservation&lt;/strong&gt;: Because rendering occurs through predefined components, spacing, typography, and layout behavior remain governed by the existing design system. Visual consistency is enforced by the component implementation, not by prompt phrasing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deterministic Behavior via Configuration&lt;/strong&gt;: Through controlled configuration of available components, fields, and generation parameters, teams can narrow variability in output. The same structural intent produces predictable component trees aligned with system rules.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Decision Framework: When AI Should and Shouldn’t Generate UI
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3cb37ejytdez0297gb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3cb37ejytdez0297gb9.png" alt="When AI Should and Shouldn’t Generate UI" width="731" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Verdict
&lt;/h2&gt;

&lt;p&gt;Generative UI is effective when it operates within defined architectural boundaries and established component systems. When generation bypasses those constraints, inconsistency and downstream rework become unavoidable. Production-grade outcomes require schema enforcement, controlled composition, and system-level governance.&lt;/p&gt;

&lt;p&gt;To see this model in practice, &lt;a href="https://puckeditor.com/?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;explore Puck&lt;/a&gt; for structured visual editing and &lt;a href="https://cloud.puckeditor.com/sign-up?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=ai_slop_vs_constrained_ui" rel="noopener noreferrer"&gt;try Puck AI&lt;/a&gt; for constrained page generation workflows. Puck AI is currently available in beta for teams evaluating controlled generative UI in production environments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>WebMCP: A Browser-Native Execution Model for AI Agents</title>
      <dc:creator>Astrodevil</dc:creator>
      <pubDate>Sun, 22 Feb 2026 16:37:51 +0000</pubDate>
      <link>https://dev.to/astrodevil/webmcp-a-browser-native-execution-model-for-ai-agents-125n</link>
      <guid>https://dev.to/astrodevil/webmcp-a-browser-native-execution-model-for-ai-agents-125n</guid>
      <description>&lt;p&gt;On February 13, Google announced the &lt;a href="https://developer.chrome.com/blog/webmcp-epp" rel="noopener noreferrer"&gt;Early Preview of WebMCP&lt;/a&gt;, introducing a browser-native way for AI agents to interact with websites. To understand why this matters, consider how agents operate today. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F149wl033x8aveht5uqye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F149wl033x8aveht5uqye.png" alt="WebMCP" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AI agents interpret interfaces by parsing the DOM, inspecting accessibility trees, analyzing rendered pages, and then simulating clicks or inputs. Each action depends on inference over presentation layers. This increases token usage, adds latency, and often leads to brittle execution.&lt;/p&gt;

&lt;p&gt;The limitation is structural. The web was designed for people navigating interfaces. Agents, however, require clearly defined capabilities they can invoke programmatically.&lt;/p&gt;

&lt;p&gt;WebMCP addresses this gap by allowing websites to register structured JavaScript functions that agents can call directly within the browser runtime. These tools execute under existing session state and same-origin constraints, exposing only what the site explicitly defines.&lt;/p&gt;

&lt;p&gt;The result is a more direct model of interaction that aligns frontend systems with the deterministic tool patterns already established in backend MCP integrations.&lt;/p&gt;

&lt;p&gt;In this article, we examine WebMCP’s architecture, how it compares to traditional MCP, and what it signals for agent-driven web infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Context Protocol (MCP): Current State and Browser Constraints
&lt;/h2&gt;

&lt;p&gt;Model Context Protocol (MCP) established a structured model for how AI agents interact with external systems. Tools are defined with clear schemas, agents invoke them with structured inputs, and responses return in predictable formats. This ensures deterministic execution rather than relying on free-form reasoning.&lt;/p&gt;

&lt;p&gt;The architecture is typically client–server. An agent connects to an MCP server that exposes tools wrapping APIs, databases, or internal services. This model fits naturally in backend environments where execution happens outside the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ovojy8py7qa2agfenmr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ovojy8py7qa2agfenmr.png" alt="MCP" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Web applications operate under different assumptions. User identity, session state, and much of the application logic live inside the browser. Authentication flows depend on cookies and federated login systems tied to that session. An external MCP server does not automatically inherit this context, which complicates authorization and state management.&lt;/p&gt;

&lt;p&gt;Because of this separation, agents interacting with web applications often end up controlling the interface itself instead of invoking structured capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebMCP Technical Overview
&lt;/h2&gt;

&lt;p&gt;WebMCP is a browser-native API that allows websites to expose structured, agent-callable tools directly within the page runtime. It adapts the conceptual model of Model Context Protocol schema-defined tools invoked by agents, but implements it specifically for client-side execution inside the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnxw7nncrv84xthjpv1t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqnxw7nncrv84xthjpv1t.png" alt="WebMCP is in early preview" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At its core, WebMCP introduces a new browser surface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;navigator.modelContext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This interface allows a web page to register capabilities that AI agents can discover and invoke. Each tool consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;name&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;description&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;input schema&lt;/strong&gt; (structured definition of parameters)&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;execution handler&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike traditional MCP, WebMCP does not rely on a separate JSON-RPC server. The web page itself becomes the tool provider. Execution occurs in the same JavaScript environment as the application logic.&lt;/p&gt;

&lt;p&gt;The formal specification is being developed under the W3C Web Machine Learning Community Group and is available at: &lt;a href="https://webmachinelearning.github.io/webmcp/" rel="noopener noreferrer"&gt;https://webmachinelearning.github.io/webmcp/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool Exposure and Execution Model
&lt;/h2&gt;

&lt;p&gt;WebMCP defines how capabilities are exposed and how agents invoke them inside the browser runtime. It supports two exposure &lt;/p&gt;

&lt;h3&gt;
  
  
  1. Declarative API (HTML-based)
&lt;/h3&gt;

&lt;p&gt;Forms can be annotated with metadata that enables automatic tool registration. The browser derives the tool definition from form inputs, enabling simple actions to be agent-callable without additional JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Imperative API (JavaScript-based)
&lt;/h3&gt;

&lt;p&gt;Developers can programmatically register tools using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;modelContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method provides full control over input schemas and execution logic, enabling dynamic, state-aware, or complex capabilities.&lt;/p&gt;

&lt;p&gt;When an agent loads a WebMCP-enabled page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The browser exposes the registered tools.&lt;/li&gt;
&lt;li&gt;The agent inspects available capabilities.&lt;/li&gt;
&lt;li&gt;The agent invokes a selected tool with structured parameters.&lt;/li&gt;
&lt;li&gt;The handler executes inside the page runtime.&lt;/li&gt;
&lt;li&gt;A structured response is returned to the agent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The defining characteristic of WebMCP is locality. Tool execution happens inside the browser session, inheriting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current authentication state&lt;/li&gt;
&lt;li&gt;Session cookies&lt;/li&gt;
&lt;li&gt;Same-origin boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This removes the need for an external transport layer or a separate authorization stack.&lt;/p&gt;

&lt;p&gt;WebMCP focuses specifically on schema-defined tool invocation optimized for browser environments, adapting MCP concepts to client-side execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core Architectural Components
&lt;/h2&gt;

&lt;p&gt;WebMCP introduces a browser-mediated architecture that connects agents directly to application capabilities without external transport layers.&lt;/p&gt;

&lt;p&gt;Below is the full execution path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8uqxzpk0kut9syudkrt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh8uqxzpk0kut9syudkrt.png" alt="WebMCP Architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WebMCP defines a browser-mediated execution model that connects agents directly to declared application capabilities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI Agent:&lt;/strong&gt; The agent discovers registered tools, selects one based on user intent, sends structured input that conforms to the declared schema, then receives structured output. Interaction occurs through explicit capabilities rather than direct interface manipulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser Runtime Control Plane:&lt;/strong&gt; The browser exposes &lt;code&gt;navigator.modelContext&lt;/code&gt;, which maintains the tool registry, validates inputs against schemas, routes invocations to the appropriate handler, enforces same origin boundaries, and executes handlers within the active page context. This removes the need for an external transport layer or separate MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool Layer Capability Surface:&lt;/strong&gt; Each tool defines a named capability, its expected input schema, and an execution handler. These tools form a contract between the application and the agent. Only declared capabilities are accessible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Execution Layer:&lt;/strong&gt; Handlers run in the same JavaScript environment as the web application. They can access session cookies, rely on existing authentication state, call internal services, and update application state. Execution remains within the active browser session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The overall flow is direct. The page loads and registers tools. The agent inspects available capabilities and invokes one with structured input. The browser validates the request, executes the handler inside the page runtime, and returns structured output to the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison with Traditional MCP and Browser Automation
&lt;/h2&gt;

&lt;p&gt;WebMCP sits between backend MCP servers and browser automation frameworks. The differences become clearer when compared across architecture, execution model, and capability exposure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Traditional MCP&lt;/th&gt;
&lt;th&gt;Browser Automation (Selenium / Playwright)&lt;/th&gt;
&lt;th&gt;WebMCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution Location&lt;/td&gt;
&lt;td&gt;External server&lt;/td&gt;
&lt;td&gt;Inside browser via UI control&lt;/td&gt;
&lt;td&gt;Inside browser via declared tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transport Layer&lt;/td&gt;
&lt;td&gt;JSON-RPC or similar&lt;/td&gt;
&lt;td&gt;WebDriver protocol&lt;/td&gt;
&lt;td&gt;Browser-native API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interaction Surface&lt;/td&gt;
&lt;td&gt;Structured tools&lt;/td&gt;
&lt;td&gt;DOM elements and selectors&lt;/td&gt;
&lt;td&gt;Schema-defined tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session Inheritance&lt;/td&gt;
&lt;td&gt;Requires coordination&lt;/td&gt;
&lt;td&gt;Native to browser session&lt;/td&gt;
&lt;td&gt;Native to browser session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication Handling&lt;/td&gt;
&lt;td&gt;Separate from browser&lt;/td&gt;
&lt;td&gt;Uses active browser state&lt;/td&gt;
&lt;td&gt;Uses active browser state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency on UI Layout&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token Overhead&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High due to DOM inspection&lt;/td&gt;
&lt;td&gt;Low due to structured schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium, selector-dependent&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Traditional MCP provides structured invocation but operates outside the browser context. Browser automation preserves session state but relies on interface manipulation. WebMCP combines structured schemas with in-browser execution, exposing declared capabilities without depending on layout or selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Model and Execution Boundaries
&lt;/h2&gt;

&lt;p&gt;WebMCP narrows the interaction surface between agents and web applications by constraining execution to explicitly declared tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Explicit Capability Exposure:&lt;/strong&gt; Only registered tools are visible to the agent. The agent cannot arbitrarily traverse the DOM or trigger undocumented behaviors unless those capabilities are intentionally exposed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same Origin Enforcement:&lt;/strong&gt; Tool execution occurs under the browser’s same-origin policy. A page can expose capabilities only within its own origin boundary. Cross-site execution is not permitted by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Inheritance:&lt;/strong&gt; Tools execute within the active browser session. They inherit authentication state, cookies, and user context already established in the page. There is no additional credential exchange layer introduced by WebMCP itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled Invocation Surface:&lt;/strong&gt; Input parameters must conform to declared schemas. The browser validates structured inputs before routing execution, limiting malformed or unexpected calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebMCP reduces the attack surface compared to interface-level automation by limiting what the agent can access to declared functions. It does not eliminate broader risks, such as prompt injection within tool logic, but it constrains execution to defined capability boundaries enforced by the browser runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome Early Preview and Built-In AI Strategy
&lt;/h2&gt;

&lt;p&gt;WebMCP is available through &lt;a href="https://developer.chrome.com/docs/ai/join-epp" rel="noopener noreferrer"&gt;Chrome’s Early Preview Program&lt;/a&gt; and can be enabled in experimental Chromium builds. The preview allows developers to test tool registration via &lt;code&gt;navigator.modelContext&lt;/code&gt; and evaluate structured agent interaction inside the browser.&lt;/p&gt;

&lt;p&gt;WebMCP complements Chrome’s Built-In AI APIs, which support on-device model execution. While Built-In AI enables local inference, WebMCP defines how agents interface with web applications through declared tools.&lt;/p&gt;

&lt;p&gt;Together, these initiatives position the browser as both an AI execution environment and a structured capability surface for external agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  InsForge and Model Context Protocol
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;InsForge&lt;/a&gt; is an open-source backend-as-a-service platform built for AI-assisted development. It provides core backend infrastructure, including database management, authentication, storage, serverless functions, and AI integrations. Its APIs are structured to support deterministic agent execution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1mqz99jcmp99njrbf2e5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1mqz99jcmp99njrbf2e5.png" alt="InsForge" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At its core, InsForge exposes a Model Context Protocol server that allows AI agents to interact with backend resources through schema-defined tools. Agents can inspect database schemas, execute queries, manage authentication, perform storage operations, and invoke backend functions using structured inputs and predictable responses.&lt;/p&gt;

&lt;p&gt;This MCP-based design enables agents to complete backend workflows with clearer execution paths and reduced ambiguity. By exposing explicit capability contracts, InsForge supports reliable multi-step operations without relying on interface-level automation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;WebMCP gives AI agents a defined way to interact with web apps inside the browser. Instead of scraping the DOM or simulating clicks, agents call explicitly declared functions with typed schemas.&lt;/p&gt;

&lt;p&gt;Those functions execute within the user’s active session and respect normal browser security boundaries. This makes agent behavior more predictable and easier to reason about.&lt;/p&gt;

&lt;p&gt;InsForge leverages Model Context Protocol (MCP) to provide structured, schema-defined backend capabilities for AI agents, enabling deterministic execution and more reliable infrastructure for AI-native applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try &lt;a href="https://github.com/InsForge/InsForge" rel="noopener noreferrer"&gt;InsForge&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quickstart guide &lt;a href="https://github.com/InsForge/InsForge?tab=readme-ov-file#quickstart" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Early Preview of &lt;a href="https://developer.chrome.com/blog/webmcp-epp" rel="noopener noreferrer"&gt;WebMCP&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
