When we first built RidgeText, we followed the industry-standard "monolithic agent" pattern. We had one core LLM loop powered by Google's Vertex AI, loaded with a system prompt that described everything our application could do, and a single array of 15+ tool declarations.
Whenever a user sent an SMS, that single brain decided whether to search Google, fetch local weather, lookup a wildfire perimeter, geocode coordinates, or generate a custom topo map.
It worked great in early testing. But as our feature set grew, our monolithic agent began to experience context congestion.
The LLM carried too much cognitive load. For simple weather queries, we were paying the high token price of heavier models (like Gemini 3.5 Flash and Pro) just so the model had the capacity to hold our entire system instructions list. Worse, the crowded tool list increased the risk of non-deterministic errors, such as the model entering infinite search loops when it got confused about when to look up coordinates vs when to fetch active radar.
This week, we completed a total refactor of our AI pipeline, moving away from a single massive agent to a modular, multi-agent hierarchy. By splitting the service into logical, focused sub-agents and a final presentation guardrail, we drastically reduced the cognitive load on our main model, making our tool execution incredibly reliable and 100% bulletproof.
Here is what we built, why we built it, and the design patterns behind it.
Why Top-Level Classifiers Fail
Our initial draft of this refactor centered around a fast, top-level "intent classifier." The idea was simple: when a user texts us, a small, cheap model inspects the text and routes it. If they ask about the weather, route them to a Weather Agent. If they ask about anything else, route them to the general agent.
It sounds perfect on paper, but it fails the moment it hits real-world user behavior.
Outdoor recreationists rarely ask single-purpose questions. Instead, they send compound, mixed-intent requests:
- "Suggest a 5-mile trail with a lake nearby and check the weather there."
- "Find a campground in Yosemite and see if there are active fire alerts."
A top-level classifier cannot route these cleanly. If it sends the query to the Weather Agent, the user never gets their trail suggestion. If it sends it to the general agent, the general agent has to carry all the weather tools anyway, defeating the purpose of the refactor.
The Solution: Sub-Agents as Tools
To solve this, we embraced the Agent-as-a-Tool (or Sub-Agent-as-a-Tool) pattern. Instead of routing requests before they reach the main agent, we let the Main Orchestrator remain the central point of contact.
We started with our weather tools as a proof of concept. Rather than cluttering the core orchestrator with low-level geocoding, forecast fetching, alert parsing, and radar map rendering APIs, we stripped those weather-specific tools from the main registry. In their place, we registered a single, high-level sub-agent tool: get_weather_analysis.
In subsequent phases of our agentic refactor, we will be extending this exact same architecture to all related domains (including wildfire perimeters, trail condition maps, and calendar syncs). We also introduced a specialized formatting agent (the SMS Summary Agent) as the final step in the pipeline to guarantee beautiful, carrier-compatible output before dispatch.
The Agent-as-a-Tool delegation flow. The Main Orchestrator handles compound user requests and coordinates tools, delegating weather-specific loops dynamically to the Weather Sub-Agent via a single tool call.
Now, when a user asks: "Suggest a 5-mile trail with a lake nearby and check the weather there," the Main Orchestrator handles it gracefully:
- It calls the local
retrieve_trail_layertool to find the lake trail. - It calls the
get_weather_analysistool for the trail's coordinates. - It calls
generate_mapto build the custom topographic map. - It synthesizes the final message.
The main agent doesn't need to know how to geocode a city, fetch hourly forecasts, parse severe alerts, or draw radar reflections. It simply delegates the weather portion of the task to a specialized, dedicated subordinate and awaits the result.
Shattering the Monolithic Tool-Call Ceiling
Context congestion isn't just about prompt length; it also directly impacts safety limits.
To prevent AI systems from entering infinite loops, hallucinating recursive functions, or endlessly search-querying themselves when confused, we enforce a strict global tool-call limit (typically 10 steps per execution).
In a monolithic agent, every tool-call turn chips away at this single, global ceiling:
- User asks: "Find a campsite in Cle Elum and see if there are weather alerts there."
- Monolith calls
get_coordinatesfor Cle Elum (Turn 1). - Monolith calls
find_nearby_campsites(Turn 2). - Monolith calls
retrieve_weather_alerts(Turn 3). - Monolith calls
generate_map(Turn 4). - Monolith parses results and answers.
If the query is any more complex—such as checking multiple alternate campgrounds or layer selections—the monolith easily bangs against the 10-step ceiling and crashes or fails silently, providing a broken user experience.
Decentralized Budgets for an Intelligent Network
The Agent-as-a-Tool pattern solves this by decentralizing the tool-call budget. Because each sub-agent is an isolated, independent model instance, it operates under its own local, scoped tool limit.
When the Main Orchestrator calls the get_weather_analysis sub-agent tool, that single action represents just 1 step from the Orchestrator's perspective.
However, behind that single step, the Weather Sub-Agent spins up with its own fresh, independent budget of 10 tool calls. It can geocode cities, pull weather data, monitor severe warnings, and render composite radar map layers within its own micro-budget without ever touching the Orchestrator's master ceiling.

Comparison between a monolithic global tool-call ceiling and a decentralized, nested per-agent budget. Scoping tool limits per-agent prevents global loop recursion while scaling task capability.
This nested structure allows our system to execute up to a hundred theoretical tool-steps across an intelligent, cooperative network of agents with high resiliency, while keeping each individual loop perfectly protected from infinite recursion.
Sub-Agent Autonomy & Focus
When get_weather_analysis is invoked, we spin up an isolated, standalone instance of our Weather Sub-Agent (weatherAgent.js).
This sub-agent runs its own autonomous multi-turn loop on the ultra-cheap gemini-3.1-flash-lite model. It carries only 6 highly-focused tools: coordinate lookups, current weather feeds, active alerts, and four National Weather Service radar mapping tools.
By narrowing its scope, we unlocked major advantages:
- 100% Tool-Selection Accuracy: With a tiny, distinct toolset, the sub-agent has zero cognitive confusion. It executes its geocoding and forecasting routines flawlessly.
- Targeted Model Scaling (and Secondary Efficiency): Because the sub-agent does 95% of the heavy lifting for its domain, we run it entirely on
gemini-3.1-flash-lite. While the primary goal of this refactor was simplifying the main orchestrator's cognitive load, aligning the task's complexity with a leaner model provides huge resource and token efficiencies in the long run. At $0.25 per million input tokens, this model is 6× more cost-effective than the Pro models we use for high-level reasoning. - Domain Specialization: We gave the sub-agent a highly-focused system prompt optimized for mountain meteorology, requiring it to analyze mountain microclimates, elevation temperature lapses (how temperatures drop with altitude), wind chill factors, and convective lightning safety.
The Weather Sub-Agent executes its tools, generates radar images, formats a comprehensive weather analysis, and returns it to the Main Orchestrator for final summarization.
The Universal Formatting Guardrail
Even with a highly-focused sub-agent, LLMs are fundamentally verbose and prone to using expressive formatting. While emojis and bold text (**bold**) look great in a web browser, they are highly disruptive over standard SMS and consume precious characters in multi-part messages.
To solve this deterministically, all compiled responses pass through a final Summary Agent before dispatch. It acts as an automated output guardrail:
- Enforces a strict 900-character budget to prevent fragmented, out-of-order SMS delivery.
- Strips all emojis, Markdown formatting, and decorative symbols.
- Converts lists to plain, single-dash bullet points (
-). - Scrubs raw file storage links. In our earlier iteration, the model would occasionally print out long, unreadable GCS bucket URLs (e.g.,
https://storage.googleapis.com/.../map.jpg) or say "your map is attached below." The Summary Agent forbids this entirely, recognizing that modern smartphones display MMS attachments inline within the text bubble, making literal text references redundant.
A Generic Pattern for Infinite Scale
The beauty of the Sub-Agent-as-a-Tool pattern is that it is fully generic and infinitely extensible.
We have established a reusable framework for adding new domains. If we want to add expert wildfire analysis, trail maintenance reports, or complex trip planning, we don't need to bloat our core orchestrator's prompt or tools list. We simply register:
get_wildfire_analysis-> Delegates to a Wildfire Sub-Agent.get_trail_analysis-> Delegates to a Trail Sub-Agent.get_trip_itinerary-> Delegates to a Guide Sub-Agent.
By dividing labor among a network of simple, cooperative sub-agents rather than trying to build a single "all-knowing" monolith, we've unlocked an architecture that scales sustainably in both intelligence and capability.