<?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: Vivek Kasturi</title>
    <description>The latest articles on DEV Community by Vivek Kasturi (@vivekkasturi).</description>
    <link>https://dev.to/vivekkasturi</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983161%2F46cfbfc9-84cc-4a87-bb24-ca94b611c6a0.jpg</url>
      <title>DEV Community: Vivek Kasturi</title>
      <link>https://dev.to/vivekkasturi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vivekkasturi"/>
    <language>en</language>
    <item>
      <title>Building Adaptive HealthOS: A Multi-Agent AI System with Gemini, Google ADK, and MongoDB Atlas</title>
      <dc:creator>Vivek Kasturi</dc:creator>
      <pubDate>Sat, 13 Jun 2026 20:30:28 +0000</pubDate>
      <link>https://dev.to/vivekkasturi/building-adaptive-healthos-a-multi-agent-ai-system-with-gemini-google-adk-and-mongodb-atlas-7lm</link>
      <guid>https://dev.to/vivekkasturi/building-adaptive-healthos-a-multi-agent-ai-system-with-gemini-google-adk-and-mongodb-atlas-7lm</guid>
      <description>&lt;p&gt;&lt;em&gt;How I designed an orchestrated agent pipeline that plans, routes, executes, and remembers — instead of just responding.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most AI fitness apps collect data but don't act on it. They track workouts, log meals, and generate charts — but users remain responsible for turning information into decisions.&lt;/p&gt;

&lt;p&gt;Adaptive HealthOS takes a different approach: a multi-agent system that classifies your intent, routes to the right specialist, executes tool chains against a persistent memory layer, and updates your state — all from a single natural language input. No form-filling. No manual categorization. No static plans that never change.&lt;/p&gt;

&lt;p&gt;The stack: &lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt;, &lt;strong&gt;Google ADK&lt;/strong&gt;, &lt;strong&gt;MongoDB Atlas&lt;/strong&gt;, &lt;strong&gt;FastMCP&lt;/strong&gt;, &lt;strong&gt;FastAPI&lt;/strong&gt;, &lt;strong&gt;React&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Six Agents, One Pipeline
&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%2F26hndeqfml3ptcy19q3a.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%2F26hndeqfml3ptcy19q3a.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The system is built on a &lt;strong&gt;manager-specialist pattern&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User message
    ↓
OrchestratorAgent       ← classifies intent as structured JSON
    ↓
[route to specialist]
    ↓
NutritionAgent          ← food logs, macro tracking, meal plans
WorkoutAgent            ← exercise logs, plan adjustments
RecoveryAgent           ← sleep analysis, rest day decisions
ProgressAgent           ← weight trends, goal tracking
ForecastingAgent        ← 30-day projection with 3 scenarios
AccountabilityAgent     ← streak management, motivational check-ins
    ↓
MongoDB Atlas           ← all state, decisions, logs, plans persisted
    ↓
React UI                ← dashboard, agent activity panel, gamification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every agent is an ADK &lt;code&gt;LlmAgent&lt;/code&gt; with its own system prompt, tool bindings, and runner. The orchestrator doesn't just route — it also extracts structured data from the user's message (calories, duration, weight values) and passes it downstream, so specialist agents receive clean inputs rather than raw natural language.&lt;/p&gt;

&lt;p&gt;This distinction matters. It's the difference between "I ate a burger" reaching the NutritionAgent as prose versus as &lt;code&gt;{"name": "burger", "calories": 550, "protein_g": 28, "meal_type": "lunch"}&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The MCP Layer: MongoDB as a Tool Interface
&lt;/h2&gt;

&lt;p&gt;The most architecturally significant decision in this system was how agents interact with data.&lt;/p&gt;

&lt;p&gt;I built a custom &lt;strong&gt;FastMCP server&lt;/strong&gt; (&lt;code&gt;mcp_server.py&lt;/code&gt;) that wraps every MongoDB Atlas operation and exposes them as &lt;strong&gt;Model Context Protocol tools&lt;/strong&gt;. Agents don't write raw queries. They call structured, named tools:&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;@mcp.tool&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;insert_health_log&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_type&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;Insert a health log entry (food, workout, weight, sleep) into MongoDB.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;log_id&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;_insert&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;user_id&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="n"&gt;log_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;data&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&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;log_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;log_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;logged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&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;log_agent_decision&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;agent_name&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;trigger&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;decision&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;actions_taken&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="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;Persist an agent decision to MongoDB agent_decisions collection.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;doc_id&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;_log&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;decision_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;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;logged&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;This matters for three reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Agents are tool-bound, not query-bound.&lt;/strong&gt; A NutritionAgent cannot accidentally write to the wrong collection. It can only call the tools it was given. This is deterministic behavior, not hoped-for behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. State is an explicit action, not a side effect.&lt;/strong&gt; Every time an agent writes to MongoDB — logging a decision, inserting a health entry, updating a streak — it's a named function call that appears in the &lt;code&gt;tools_used&lt;/code&gt; list returned to the UI. The agent activity panel in the dashboard shows exactly which tools each agent called, in order. This is observable intelligence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Memory is first-class.&lt;/strong&gt; Agents aren't stateless responders. The ForecastingAgent calls &lt;code&gt;get_recent_logs&lt;/code&gt; to retrieve 30 days of weight history before generating projections. The RecoveryAgent reads sleep quality scores to decide whether to recommend a rest day. Context lives in MongoDB and is retrieved as needed — persistent across sessions, queryable by any agent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hard Problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. LLM JSON Instability
&lt;/h3&gt;

&lt;p&gt;The OrchestratorAgent needs to return structured JSON every time:&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;"intent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FOOD_LOG"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extracted_data"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grilled chicken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"calories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;"routing_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User described a meal with specific food items"&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;In practice, Gemini sometimes wraps the JSON in markdown code fences, adds an explanation sentence before it, or returns slight variations in key names. A naive &lt;code&gt;json.loads(response)&lt;/code&gt; fails frequently.&lt;/p&gt;

&lt;p&gt;The fix was a substring extraction approach: find the first &lt;code&gt;{&lt;/code&gt; and last &lt;code&gt;}&lt;/code&gt; in the response string, extract that slice, parse it, and fall back to &lt;code&gt;intent: "GENERAL"&lt;/code&gt; if parsing still fails. Simple, but essential for production reliability. A fragile parser here breaks the entire routing pipeline.&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&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;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rfind&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&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="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;start&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;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;intent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&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;intent&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;GENERAL&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;This pattern — parsing LLM output as structured data with explicit fallback — is something every multi-agent system needs and few tutorials cover.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ADK Session vs. MongoDB State
&lt;/h3&gt;

&lt;p&gt;Google ADK uses &lt;code&gt;InMemorySessionService&lt;/code&gt; for conversation context. MongoDB owns persistent state. These two layers serve different purposes and must not be conflated.&lt;/p&gt;

&lt;p&gt;The ADK session manages the short-term context window for a single agent run. MongoDB manages everything that needs to survive across requests: logs, plans, agent decisions, forecasts, gamification state.&lt;/p&gt;

&lt;p&gt;The architectural rule I settled on: &lt;strong&gt;ADK sessions are ephemeral and per-request. MongoDB is the source of truth for all agent memory.&lt;/strong&gt; Each call to &lt;code&gt;run_agent()&lt;/code&gt; creates a fresh ADK session, but every meaningful state change is persisted to MongoDB via MCP tools before the session ends. This keeps the agent execution stateless while maintaining rich, queryable history.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Runner Lifecycle and Caching
&lt;/h3&gt;

&lt;p&gt;ADK's &lt;code&gt;Runner&lt;/code&gt; is the execution engine for an agent. Creating a new runner per request is expensive and unnecessary — but creating runners at import time causes issues with async initialization order.&lt;/p&gt;

&lt;p&gt;The solution: a lazy runner cache keyed by agent name.&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;_runners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&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;Runner&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;def&lt;/span&gt; &lt;span class="nf"&gt;_get_runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&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;Runner&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;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_runners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_runners&lt;/span&gt;&lt;span class="p"&gt;[&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;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&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;app_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;healthos&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_service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_session_service&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="n"&gt;_runners&lt;/span&gt;&lt;span class="p"&gt;[&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;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One runner per agent, created on first use, reused for every subsequent request. This keeps initialization cost at one-time-per-agent while avoiding the overhead of reconstruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Async Orchestration Latency
&lt;/h3&gt;

&lt;p&gt;The pipeline runs two Gemini calls per user message: first the OrchestratorAgent, then the specialist. Under a naive implementation this compounds latency. &lt;/p&gt;

&lt;p&gt;The &lt;code&gt;run_async&lt;/code&gt; event loop in ADK streams events — tool calls, intermediate responses, and the final response — as they occur. Capturing tool usage alongside the final response required iterating over the full event stream rather than awaiting only the final response:&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;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_async&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&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;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function_call&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_call&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;tools_used&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;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="nf"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;response_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_final_response&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — accumulating tool calls from the stream, not just extracting the final text — is what enables the UI to show real agent activity rather than a simulated trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The ForecastingAgent Multi-Step Chain
&lt;/h3&gt;

&lt;p&gt;The ForecastingAgent executes a four-step tool chain autonomously, without human intervention between steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;get_user(user_id)&lt;/code&gt; — retrieve goal and target weight&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_recent_logs(user_id, "weight", limit=30)&lt;/code&gt; — fetch weight history&lt;/li&gt;
&lt;li&gt;Calculate trajectory across three scenarios (optimistic, realistic, pessimistic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;save_forecast(...)&lt;/code&gt; — persist the projection&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;log_agent_decision(...)&lt;/code&gt; — record the analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the clearest example of genuine agentic behavior in the system. The agent is not responding to a query — it is executing a workflow. It decides what data it needs, fetches it, reasons over it, produces a structured output, and stores that output for other agents and the UI to consume. No human in the loop between steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the MCP + ADK Combination Works
&lt;/h2&gt;

&lt;p&gt;What made Google ADK + MCP feel like the right architecture (rather than a raw LangChain agent or a custom prompt loop) was the separation it enforces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ADK handles agent cognition&lt;/strong&gt; — the reasoning loop, tool call execution, session context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP handles tool exposure&lt;/strong&gt; — what tools exist, what their schemas are, how they connect to data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB handles memory&lt;/strong&gt; — persistent state, queryable history, cross-agent data sharing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each layer has a single responsibility. Agents don't know about MongoDB's collection structure. The MCP server doesn't know about agent logic. The result is a system where you can swap out any layer without cascading rewrites — which matters when you're building and iterating fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Agentic" Actually Means
&lt;/h2&gt;

&lt;p&gt;There's a tendency to call anything that uses an LLM "agentic." It isn't.&lt;/p&gt;

&lt;p&gt;A chatbot answers questions. An agent executes workflows.&lt;/p&gt;

&lt;p&gt;The distinction shows up in concrete behavior. When you tell HealthOS you ate a meal, the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classifies your input (OrchestratorAgent → &lt;code&gt;FOOD_LOG&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Extracts nutritional estimates (same agent, structured JSON output)&lt;/li&gt;
&lt;li&gt;Writes the log to MongoDB (NutritionAgent → &lt;code&gt;insert_health_log&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Checks today's running macros (→ &lt;code&gt;get_daily_nutrition_totals&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Compares against your active nutrition plan (→ &lt;code&gt;get_active_plan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Updates the plan if needed (→ &lt;code&gt;create_plan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Awards XP (→ &lt;code&gt;award_xp&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Updates your streak (→ &lt;code&gt;update_streak&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Logs the full decision trace (→ &lt;code&gt;log_agent_decision&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's nine tool calls triggered by one natural language input. No form. No menu. No "click save." The user said something, the system acted.&lt;/p&gt;

&lt;p&gt;That's what agentic means.&lt;/p&gt;




&lt;h2&gt;
  
  
  By the Numbers
&lt;/h2&gt;

&lt;p&gt;Some concrete figures from the system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;11 MCP tools&lt;/strong&gt; exposed by the FastMCP server — each maps to a named MongoDB operation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 MongoDB collections&lt;/strong&gt;: &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;health_logs&lt;/code&gt;, &lt;code&gt;plans&lt;/code&gt;, &lt;code&gt;agent_decisions&lt;/code&gt;, &lt;code&gt;forecasts&lt;/code&gt;, &lt;code&gt;gamification&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6–9 MCP tool calls&lt;/strong&gt; triggered by a typical food logging request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2 Gemini calls per request&lt;/strong&gt; — OrchestratorAgent + specialist — running sequentially&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~3–4 seconds&lt;/strong&gt; average end-to-end latency for standard requests (food log, workout log)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~6–8 seconds&lt;/strong&gt; for ForecastingAgent requests, which run a 5-step tool chain over 30 days of weight history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent routing accuracy&lt;/strong&gt;: 94%+ on common health inputs in manual testing; falls back to &lt;code&gt;GENERAL&lt;/code&gt; intent for anything ambiguous&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The latency comes almost entirely from the two sequential Gemini calls — MongoDB ops over Atlas are fast. The main optimization lever, if needed, is parallelizing orchestration and data prefetch, not the database layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Didn't Work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Letting agents query MongoDB directly.&lt;/strong&gt; The first prototype gave agents a raw database connection. They hallucinated collection names, wrote malformed queries, and occasionally inserted data into the wrong place. The MCP tool interface exists because direct DB access for LLM agents is a bad idea — named tools with typed inputs are the only reliable contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-agent design.&lt;/strong&gt; I tried building a single "health assistant" agent with all tools attached. It worked on simple inputs and failed on complex ones — it would log food correctly but forget to check macros or update the plan. Specialist agents with narrow scopes and clear responsibilities outperform one generalist agent with many tools attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt-only routing.&lt;/strong&gt; Early versions used a prompt like "decide what to do and call the right function." The orchestrator would sometimes skip routing and try to handle everything itself, or route to the wrong agent based on superficial keywords. Extracting routing into a dedicated classification step with structured JSON output — separate from the specialist response — made the pipeline reliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared tool access across agents.&lt;/strong&gt; When all agents could call all tools, they would call irrelevant ones "just in case." The WorkoutAgent would call &lt;code&gt;get_daily_nutrition_totals&lt;/code&gt;. The NutritionAgent would call &lt;code&gt;update_streak&lt;/code&gt;. Scoping each agent's tool list to only what it needs eliminated this noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-lived ADK sessions.&lt;/strong&gt; Keeping the same session open across multiple user requests caused context accumulation — previous tool results would bleed into new decisions. Per-request sessions with MongoDB as the durable state layer was the right separation.&lt;/p&gt;




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

&lt;p&gt;The interesting question isn't whether agents can call tools. That's solved. The challenge is designing systems where multiple agents can coordinate, persist state, and make decisions that compound over time — without becoming brittle, unpredictable, or expensive to debug.&lt;/p&gt;

&lt;p&gt;The patterns that made this work: a dedicated orchestration layer that handles routing separately from reasoning, MCP tool boundaries that force explicit contracts between agents and data, and a persistent memory layer that every agent reads from and writes to. None of these are novel individually. Together, they make the system behave like something that actually owns its state rather than something that just responds to prompts.&lt;/p&gt;

&lt;p&gt;Adaptive HealthOS is one experiment in that direction. The same architecture — orchestrated routing, scoped tool access, persistent agent memory — applies to any domain where users have longitudinal data and goals that evolve over time. Health is just a convenient starting point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built solo for the Google Cloud Rapid Agent Hackathon 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Stack: Google ADK · Gemini 2.5 Flash · MongoDB Atlas · FastMCP · FastAPI · React · Vercel · Railway&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/Vivek-Kasturi/adaptive-healthos" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://adaptive-healthos.vercel.app/dashboard" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Vivek Kasturi — Applied AI Systems Engineer&lt;/em&gt;&lt;br&gt;
&lt;em&gt;&lt;a href="https://www.linkedin.com/in/vivekkasturi/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://x.com/vivek_builds_ai" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentaichallenge</category>
      <category>devposthackathon</category>
    </item>
  </channel>
</rss>
