Agentic Design Patterns Audit¶
This document analyzes the platform's architecture against the Google Cloud Agentic Design Patterns. The platform is classified as a Hybrid Multi-Agent System that balances deterministic workflows with dynamic LLM orchestration.
Summary of Patterns¶
| Pattern Category | Key Patterns | Implementation in this Project |
|---|---|---|
| Multi-Agent (MAS) | Coordinator | orrery_chat_agent uses AgentTool to route requests. |
| Workflow Graph | orrery_triage_workflow is an ADK 2.0 graph executing a fixed pipeline. | |
| JoinNode Barrier | health_join synchronizes concurrent K8s, Kafka, and Docker checks. | |
| Hierarchical | Root Orchestrator → Specialist Workers / Triage Sub-agent. | |
| Iterative & Feedback | ReAct | Default behavior for all LlmAgent instances (Thought/Action/Observation). |
| Graph Cycles | Remediation subgraph uses verify_route to loop: act → verify → retry. | |
| Generator/Critic | Evaluation framework uses LLM-as-a-judge (AEP-002). | |
| Specialized | Human-in-the-Loop | GuardrailsPlugin gates tools with @confirm and @destructive. |
| Custom Logic | Enforced via native Python FunctionNodes like triage_route. |
Composition at a Glance¶
graph TD
ROOT[orrery_chat_agent<br/>LLM Orchestrator]
ROOT -->|AgentTool| KAFKA[kafka_health_agent]
ROOT -->|AgentTool| K8S[k8s_health_agent]
ROOT -->|AgentTool| OBS[observability_agent]
ROOT -->|AgentTool| DOCKER[docker_agent]
ROOT -->|AgentTool| JOURNAL[ops_journal_agent]
ROOT -->|AgentTool| TRIAGE[incident_triage_agent]
WORKFLOW[orrery_triage_workflow<br/>Graph]
WORKFLOW -->|START| HC[health_checkers<br/>Parallel edges]
HC --> HC1[kafka_checker]
HC --> HC2[k8s_checker]
HC --> HC3[docker_checker]
HC --> HC4[obs_checker]
HC --> HC5[es_checker]
HC1 & HC2 & HC3 & HC4 & HC5 --> HJ[health_join<br/>JoinNode]
HJ --> SUM[triage_summarizer]
SUM --> JW[journal_writer]
JW --> TROUTE{triage_route}
TROUTE -->|remediate| ACT[remediation_actor]
ACT --> VER[remediation_verifier]
VER --> VROUTE{verify_route}
VROUTE -->|retry| ACT Deterministic workflows (triage, remediation) are modeled as native graph edges in orrery_triage_workflow — their execution order is fixed. Specialists live behind AgentTool on the orrery_chat_agent so the conversational LLM picks them based on the user's intent. See ADR-003 for the rationale.
Detailed Analysis¶
1. Multi-Agent Systems (MAS)¶
The project leverages the Coordinator Pattern as its primary entry point. The root orrery_chat_agent handles user requests and routes to specialists. A separate deterministic pipeline, orrery_triage_workflow, handles complex repetitive tasks.
- LLM-Driven Routing (AgentTool): As defined in ADR-002 (and preserved in ADR-003), specialists like
kafka_health_agentandk8s_health_agentare exposed as tools on theorrery_chat_agent. The LLM decides when to invoke them based on their descriptions. - Deterministic Workflows (Graph Edges): For complex, repeatable tasks like incident triage, the system uses ADK 2.0
Workflowgraph edges. The parallel health checkers ensure that all systems are checked simultaneously before thehealth_joinnode synchronizes execution and proceeds to the summary.
2. Iterative & Feedback Patterns¶
Every individual agent in the project follows the ReAct (Reason and Act) pattern. When an agent is given a task, it iterates through "Thoughts" and "Actions" (tool calls) until it observes enough information to provide a final response.
Iterative Refinement is implemented in the remediation layer using a graph-based cyclic subgraph:
- Act: The
remediation_actorattempts a fix (restart, scale, or rollback a deployment). - Verify: The
remediation_verifierchecks system health using diagnostic tools. - Decide: The
verify_routenode (a deterministicFunctionNode) increments a state counter. If resolved, it routes to"done". If still failing, it routes back to"retry"— up to 3 iterations.
Planning (opt-in). Reasoning-heavy agents — the root orchestrator, the triage_summarizer, and the remediation_actor — accept an optional ADK planner via create_agent(planner=…). Set ORRERY_PLANNER=plan_react to attach a provider-agnostic PlanReActPlanner (works with Gemini, Claude, OpenAI, and Ollama via LiteLLM); set ORRERY_PLANNER=builtin to use Gemini's native thinking tokens (BuiltInPlanner). The planner is resolved once at import time and shared across the opted-in agents in orrery-assistant. Tool-leaf agents — per-system health checkers, the remediation verifier, the journal writer — intentionally skip the planner: they execute one short tool sequence per turn, so an extra reasoning pass would add latency without changing the output. Default is no planner (zero behavior change). See Configuration → LLM Provider for the env-var matrix.
3. Specialized Patterns¶
The platform implements a robust Human-in-the-Loop pattern via the GuardrailsPlugin. * Safety Gating: Tools marked with @confirm or @destructive decorators are intercepted by the before_tool_callback. * Bypass Prevention: The system uses an arguments-hash and invocation-ID tracking mechanism to ensure that the agent only proceeds if the user has explicitly provided confirmation for the exact operation requested.
Best Practices: Sizing Tools & Agents¶
Agentic systems degrade gracefully in capability but sharply in tool selection accuracy as the tool/agent surface grows. The numbers below are the budgets this project targets, with the reasoning behind each.
Tool count per agent¶
| Budget | Value | Rationale |
|---|---|---|
| Sweet spot | 5–15 tools | LLM function-calling accuracy stays near ceiling. |
| Soft ceiling | ~20 tools | Measurable accuracy drop on frontier models (see benchmarks below). |
| Hard ceiling | ~25 tools | Tool-selection errors dominate; eval scores collapse. |
Why it matters:
- Accuracy degradation. Public benchmarks — the Berkeley Function-Calling Leaderboard (BFCL), Gorilla, and ToolBench/τ-bench — consistently show that tool-selection accuracy drops as tool count grows, especially when tool descriptions are semantically similar (e.g.
get_pod_logsvs.describe_podvs.get_pod_events). - Context tax. Every tool schema is serialised into the system prompt on every turn. 30 tools × ~150 tokens ≈ 4.5k tokens before the user has spoken. That also churns the prompt-cache key and erases the gains documented in AEP-007.
- Eval combinatorics. Each tool pair is a potential wrong-tool failure mode. 10 tools → 45 pairs; 30 tools → 435 pairs. Eval coverage (AEP-002) gets expensive fast.
Current state in this project:
| Agent | Tools | Status |
|---|---|---|
kafka_health_agent | 4–8 | ✅ in sweet spot |
k8s_health_agent | 11–12 | ✅ in sweet spot |
docker_agent | 3–5 | ✅ in sweet spot |
observability_agent | 4 | ✅ in sweet spot |
ops_journal_agent | 2 | ✅ in sweet spot |
Agent count per orchestrator¶
| Budget | Value | Rationale |
|---|---|---|
| Sweet spot | 3–7 direct children | Routing is tool-selection in disguise; same limits apply. |
| Hard ceiling | ~10 direct children | Misrouting becomes the dominant eval failure. |
The orrery_assistant root currently exposes 6 AgentTools + 1 sub-agent — right in the pocket. Adding a 7th specialist should retire or merge an existing one.
Hierarchy depth¶
- Keep it shallow: 2–3 levels. Each level = one extra LLM turn = added latency (~1–3 s), added token cost (context re-summarisation), and a new routing failure mode.
- Current project depth: 3 (
root → triage → health_check → specialist_checker). That is the ceiling, not a floor to exceed.
When to split an agent¶
Split when any of these holds:
- Tool count crosses ~15.
- Tools span different privilege tiers (viewer / operator / admin) — splitting mirrors RBAC cleanly (see ADR-001).
- Tools touch different external systems with distinct failure modes — per-system circuit breakers (resilience) work better when isolated.
- The system prompt starts containing "if the user asks X use tool Y; if Z use tool W" — that routing belongs in sub-agent selection, not prose.
- A tool group drags down the eval score — isolate and iterate.
When NOT to split¶
- Three tools always called together don't need their own agent.
- "Separation of concerns": agents are not microservices. Every extra agent adds one LLM turn on the critical path.
- A tool invoked once per session — keep it on the parent.
Signals you've overgrown¶
- Orchestrator chooses the wrong sub-agent in >5 % of eval runs.
- p95 latency per user turn > 10 s because of multi-level routing.
- Context-cache hit rate trending down (tool/agent churn).
- Prose disambiguation creeping into the system prompt.
Framework & Model Limits¶
Know where the ceiling is imposed by the model or the framework, not by your design.
Model-level limits¶
| Model family | Hard tool-list cap | Practical cap (per public benchmarks / vendor guidance) |
|---|---|---|
| Gemini 2.x / 2.5 | 128 function declarations per request | Accuracy drops past ~20; keep ≤15 for parallel tool calls. |
| Claude (Sonnet/Opus 4.x) | No hard documented cap; limited by context window (200k) | Anthropic's tool-use guide recommends keeping tool count "small"; descriptions matter more than count. |
| OpenAI (GPT-4.x / o-series) | 128 tools per request (documented) | Function-calling quality degrades past ~20 similar tools. |
| Ollama / local models | Varies; most open models degrade sharply past ~10 tools | Strict budget ≤10; prefer narrow specialists. |
Implication for multi-provider operation: because this project supports MODEL_PROVIDER switching via LiteLLM, design to the tightest cap you might deploy against — currently Ollama. A 10-tool budget per agent is a safe floor that works on every supported provider.
Framework-level limits (ADK)¶
- No hard numeric cap on tools or sub-agents — ADK delegates these limits to the underlying model.
- Graph cycles should always enforce an iteration cap. This project uses
MAX_REMEDIATION_ITERATIONS = 3inverify_route(see ADR-003). There is no default safety net. - Graph tuple fan-out is bounded only by your Python asyncio thread pool and the downstream systems' concurrency limits. Add per-system semaphores where relevant.
session.stateis a dict shared across agents in a run. It is not size-limited by ADK, but large payloads here bloat every subsequent LLM call. Preferoutput_keywith small summaries over dumping raw tool output.- Plugins apply globally on the
Runner. There is no per-agent plugin scoping — keep plugins cheap because they run on every tool call.
Context-window budgeting¶
Plan the system prompt so that, at steady state:
- Tool schemas < 10 % of context window.
- Session state + activity log < 20 %.
- Conversation history < 40 %.
- Leave ≥ 30 % headroom for the user turn + tool results + model output.
If any of those is breached, it's a sign to split the agent, summarise state, or enable context caching (AEP-007) where the provider supports it (Gemini today).
When to Reach for A2A (Scaling Across Processes)¶
The in-process model (sub-agents + AgentTool) is the right default. Reach for the A2A protocol (see AEP-005) only when one of the following is true — not to feel more "distributed":
- Independent deployment cadence. A team needs to ship the Kafka agent without redeploying the orchestrator (or vice versa).
- Independent scaling. One agent's load profile is radically different from the rest — e.g. a log-analysis agent that wants 10× replicas while the orchestrator is fine on one.
- Cross-language agents. A Go or Java specialist needs to plug into the Python orchestrator.
- Cross-org reuse. Another platform wants to consume one of your agents without importing Python code.
- Blast-radius isolation. An agent touches systems that warrant a separate security boundary (separate IAM, separate network segment, separate audit stream).
- Resource isolation. An agent has a heavy native dependency (e.g. a vendored binary, a large ML model) that shouldn't live inside the orchestrator image.
When NOT to use A2A¶
- To make the system "more microservice-like". Each hop adds network latency, a new failure mode, and an extra serialisation boundary for tool results.
- For performance. A2A is strictly slower than in-process calls. It buys deployment flexibility, not speed.
- Before evals exist. Without the eval harness (AEP-002) you cannot prove the split didn't regress behaviour.
- For one-off fan-out. If you need parallel work, a graph
Workflownode-tuple edge (fan-out) plus aJoinNodebarrier already gives you that in-process, without a network hop.
Scaling playbook¶
| Scale stage | Pattern | Typical trigger |
|---|---|---|
| Single process | AgentTool + sub_agents (today) | ≤ a few hundred QPS, one team. |
| Single process, horizontally replicated | Same, behind a load balancer with shared session store | State isolation + stateless agents. |
| A2A per agent | A2AServer / RemoteA2aAgent (AEP-005) | Independent deploy, scale, or ownership. |
| A2A + async bus | A2A + Pub/Sub for long-running work (see google-chat-bot worker) | Work exceeds HTTP timeouts; needs retry/DLQ semantics. |
Each step up adds operational surface area — take it only when the prior step can't absorb the load or organisational constraint.
References¶
- Google Cloud: Choose a design pattern for an agentic AI system
- A2A Protocol specification
- Berkeley Function-Calling Leaderboard (BFCL)
- Gorilla: Large Language Model Connected with Massive APIs
- τ-bench (ToolBench)
- ADR-001: RBAC via Guardrail Metadata
- ADR-002: AgentTool vs Sub-Agents (superseded by ADR-003)
- ADR-003: Graph Workflow Inversion
- AEP-002: Agent Evaluation Framework
- AEP-004: LoopAgent for Self-Healing
- AEP-005: A2A Protocol Support
- AEP-007: Context Caching