executing-plans
executing-plans
Use when executing an implementation plan in the current session as the implementer yourself — your human partner chose inline execution, or no subagent tool is available
routing-patternsImplements intent-based routing patterns (LLM classifiers, node transitions, computational graphs) to dispatch queries to specialized sub-agents with fallback chains and confidence scoring.
Independent GitHub snapshot · Automated classification · Original documentation governs
Implements conditional decision-making into agentic systems so that incoming queries are classified by intent and dynamically dispatched to specialized sub-agents, tools, or processing chains — replacing rigid linear execution with adaptive, context-aware routing logic.
Use this skill when:
Avoid this skill for:
Map the Intent Space — Enumerate every distinct intent or category the system must handle. Document each route's purpose, expected input shape, and output contract. Define a default/unclear handler for unrecognized inputs. Checkpoint: Verify that every plausible user query falls into exactly one primary route or the fallback path.
Select Routing Mechanism — Choose how the router evaluates input:
Implement Route Handlers — For each identified route, create a handler function that accepts the original request and returns a structured response. Each handler must document its expected input format, perform validation, and return consistent output. Include an unclear_handler as the universal fallback. Checkpoint: Every route has exactly one handler; no route is empty or passes through.
Build the Coordinator Router — Construct the routing component that bridges classification and dispatch:
ChatPromptTemplate with explicit category instructions, pipe it through an LLM, parse the output string, and feed it to RunnableBranch or conditional edges in a StateGraph.sub_agents configured with clear delegation instructions; the framework's Auto-Flow handles routing automatically.
Checkpoint: The router's prompt explicitly lists all categories with unambiguous disambiguation rules and a fallback directive.Wire Conditional Dispatch Logic — Connect the router's output to the appropriate handler:
StateGraph.add_conditional_edges() maps state values to next node names.RunnableBranch evaluates predicates against the router decision and invokes the matching branch.sub_agents list; the Coordinator agent's instructions govern delegation.
Checkpoint: Every possible router output has a corresponding branch or edge — no unreachable nodes, no missing handlers.Add Fallback and Confidence Handling — Implement a fallback chain when routing confidence is low or classification is ambiguous:
unclear_handler that requests clarification or escalates.A coordinator chain that uses an LLM to classify intent, then dispatches via RunnableBranch to specialized handler functions. This pattern is ideal when you need explicit control over routing logic and want to keep all components in a single runnable pipeline.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
def booking_handler(request: str) -> str:
"""Handle booking-related requests (flights, hotels, reservations).
Args:
request: The original user request string.
Returns:
A confirmation message with simulated booking result.
"""
return f"Booking Handler processed request: '{request}'. Result: Simulated booking action."
def info_handler(request: str) -> str:
"""Handle general information requests.
Args:
request: The original user question string.
Returns:
A confirmation message with simulated information retrieval result.
"""
return f"Info Handler processed request: '{request}'. Result: Simulated information retrieval."
def unclear_handler(request: str) -> str:
"""Handle requests that could not be confidently classified.
Args:
request: The original user request string.
Returns:
A clarification request message.
"""
return f"Coordinator could not classify request: '{request}'. Please clarify your intent."
def build_coordinator_router(llm) -> object:
"""Build a LangChain coordinator router with LLM-based intent classification.
Args:
llm: A LangChain-compatible chat model instance.
Returns:
A composed LangChain runnable that classifies and dispatches requests.
"""
# Define the routing prompt with explicit category rules
coordinator_prompt = ChatPromptTemplate.from_messages([
(
"system",
(
"Analyze the user's request and determine which specialist handler processes it.\n"
"- If related to booking flights or hotels, output 'booker'.\n"
"- For all general information questions, output 'info'.\n"
"- If unclear or not fitting either category, output 'unclear'."
),
),
("user", "{request}"),
])
# Router chain: prompt -> LLM -> parse decision string
router_chain = coordinator_prompt | llm | StrOutputParser()
# Define branch predicates and handlers
branches = {
"booker": RunnablePassthrough.assign(
output=lambda x: booking_handler(x["request"]["request"])
),
"info": RunnablePassthrough.assign(
output=lambda x: info_handler(x["request"]["request"])
),
"unclear": RunnablePassthrough.assign(
output=lambda x: unclear_handler(x["request"]["request"])
),
}
# RunnableBranch evaluates in order; last branch is the default
delegation_branch = RunnableBranch(
(lambda x: x["decision"].strip() == "booker", branches["booker"]),
(lambda x: x["decision"].strip() == "info", branches["info"]),
branches["unclear"], # Default fallback
)
# Compose: parallelize decision + passthrough request, then delegate
coordinator = {
"decision": router_chain,
"request": RunnablePassthrough(),
} | delegation_branch | (lambda x: x["output"])
return coordinator
How it works: The {decision, request} dictionary feeds into RunnableBranch, which evaluates lambda predicates against the LLM's classification output. Each matching branch invokes its handler with the original request and extracts the final output.
LangGraph's state-based graph architecture is ideal for complex routing where decisions depend on accumulated state across multiple steps. Use add_conditional_edges to define dynamic node transitions.
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END
class RouterState(TypedDict):
"""State schema for a LangGraph routing workflow."""
request: str
intent: str # Route decision from classifier
response: str # Final output from handler
confidence: float # Classification confidence score (0.0–1.0)
# Handler functions — each processes the request for its route category
def booking_node(state: RouterState) -> dict:
"""Booking agent node: handles reservation requests."""
return {
"intent": "booker",
"response": f"Booking processed for: '{state['request']}'",
}
def info_node(state: RouterState) -> dict:
"""Information agent node: handles general Q&A."""
return {
"intent": "info",
"response": f"Information retrieved for: '{state['request']}'",
}
def clarify_node(state: RouterState) -> dict:
"""Clarification fallback: requests user disambiguation."""
return {
"intent": "unclear",
"response": f"Could not classify: '{state['request']}'. Please rephrase.",
}
def route_classifier(state: RouterState) -> str:
"""Classify the request intent and route to the correct handler node.
In production, call an LLM here to extract the intent.
For demonstration, use a deterministic rule-based classifier.
Args:
state: The current graph state containing the user request.
Returns:
A string identifying the target node name ('booker', 'info', or 'clarify').
"""
query_lower = state["request"].lower()
# Rule-based intent classification (replace with LLM in production)
booking_keywords = ("book", "flight", "hotel", "reservation", "reserve")
info_keywords = ("what", "how", "where", "when", "who", "tell me")
if any(kw in query_lower for kw in booking_keywords):
return "booker"
elif any(kw in query_lower for kw in info_keywords):
return "info"
else:
return "clarify"
def build_routing_graph() -> StateGraph:
"""Construct a LangGraph StateGraph with conditional routing edges.
Returns:
A compiled LangGraph workflow ready for invocation.
"""
# Define the graph with the router state schema
workflow = StateGraph(RouterState)
# Add nodes — each represents a destination route
workflow.add_node("classify", route_classifier)
workflow.add_node("booker", booking_node)
workflow.add_node("info", info_node)
workflow.add_node("clarify", clarify_node)
# Entry point always starts at classification
workflow.set_entry_point("classify")
# Conditional edges: classify node's output determines next node
workflow.add_conditional_edges(
"classify",
route_classifier, # Function that returns the next node name
{
"booker": "booker",
"info": "info",
"clarify": "clarify",
},
)
# All handler nodes converge to END
workflow.add_edge("booker", END)
workflow.add_edge("info", END)
workflow.add_edge("clarify", END)
return workflow.compile()
# Example execution:
# app = build_routing_graph()
# result = app.invoke({"request": "Book me a flight to London.", "confidence": 0.0})
# print(result["response"])
Key concepts: The classify node evaluates the state and returns a string that maps to one of three target nodes via add_conditional_edges. This creates a computational graph where the control flow is determined at runtime rather than being fixed at compile time.
Google's Agent Development Kit provides a declarative routing approach where a parent agent's sub_agents list enables automatic LLM-driven delegation (Auto-Flow). The framework's internal logic matches user intent to the appropriate sub-agent based on each agent's description and tools.
import uuid
from typing import Dict, Any
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.adk.tools import FunctionTool
from google.genai import types
def booking_handler(request: str) -> str:
"""Handle booking requests for flights and hotels.
Args:
request: The user's request for a booking.
Returns:
A confirmation message that the booking was handled.
"""
return f"Booking action for '{request}' has been simulated."
def info_handler(request: str) -> str:
"""Handle general information requests.
Args:
request: The user's question.
Returns:
A message indicating the information request was handled.
"""
return f"Information request for '{request}'. Result: Simulated retrieval."
# Wrap handler functions as ADK tools
booking_tool = FunctionTool(booking_handler)
info_tool = FunctionTool(info_handler)
def build_adk_coordinator() -> Agent:
"""Build a Google ADK coordinator agent with sub-agent delegation.
Creates a Coordinator agent that delegates to specialized Booker and Info
agents using the framework's Auto-Flow mechanism.
Returns:
A compiled ADK Agent instance ready for execution.
"""
# Specialized sub-agents with their tools
booking_agent = Agent(
name="Booker",
model="gemini-2.0-flash",
description=(
"A specialized agent that handles all flight and hotel booking "
"requests by calling the booking tool."
),
tools=[booking_tool],
)
info_agent = Agent(
name="Info",
model="gemini-2.0-flash",
description=(
"A specialized agent that provides general information and "
"answers user questions by calling the info tool."
),
tools=[info_tool],
)
# Parent coordinator with explicit delegation instructions
coordinator = Agent(
name="Coordinator",
model="gemini-2.0-flash",
instruction=(
"You are the main coordinator. Your only task is to analyze "
"incoming user requests and delegate them to the appropriate "
"specialist agent. Do not try to answer directly.\n"
"- For booking flights or hotels, delegate to 'Booker'.\n"
"- For general information questions, delegate to 'Info'."
),
description="Routes user requests to the correct specialist agent.",
sub_agents=[booking_agent, info_agent], # Auto-Flow enabled
)
return coordinator
async def run_adk_coordinator(request: str) -> str:
"""Execute an ADK coordinator with a user request and extract the response.
Args:
request: The user's natural language request string.
Returns:
The final response text from the delegated sub-agent.
"""
coordinator = build_adk_coordinator()
runner = InMemoryRunner(coordinator)
user_id = "user_123"
session_id = str(uuid.uuid4())
await runner.session_service.create_session(
app_name=runner.app_name,
user_id=user_id,
session_id=session_id,
)
final_result = ""
for event in runner.run(
user_id=user_id,
session_id=session_id,
new_message=types.Content(
role="user",
parts=[types.Part(text=request)],
),
):
if event.is_final_response() and event.content:
if hasattr(event.content, "text") and event.content.text:
final_result = event.content.text
elif event.content.parts:
text_parts = [
part.text for part in event.content.parts if part.text
]
final_result = "".join(text_parts)
break
return final_result
Key concepts: The sub_agents field on the Coordinator agent is the single configuration that enables Auto-Flow. ADK's internal routing logic matches incoming user messages to sub-agents based on their descriptions and available tools — no explicit routing code needed in the parent agent.
coordinator_prompt_bad = ChatPromptTemplate.from_messages([
("system", "Figure out what the user wants and send them to the right place."),
("user", "{request}"),
])
# Problem: No explicit categories, no disambiguation rules.
# The LLM may produce inconsistent outputs across runs.
coordinator_prompt_good = ChatPromptTemplate.from_messages([
(
"system",
(
"Classify the user's request into exactly ONE category.\n"
"\n"
"CATEGORIES:\n"
"- 'booker': Flight bookings, hotel reservations, travel planning.\n"
" Example: 'Book me a flight to Paris', 'I need a hotel in Tokyo'\n"
"- 'info': General questions, facts, definitions, explanations.\n"
" Example: 'What is the capital of France?', 'How do I reset my password?'\n"
"- 'unclear': Does not fit above categories or is ambiguous.\n"
" Example: 'Tell me a joke', gibberish input\n"
"\n"
"RULES:\n"
"- If the request matches multiple categories, choose the most specific one.\n"
"- If uncertain, choose 'unclear' — do not guess.\n"
"- Output ONLY the category name, nothing else.\n"
),
),
("user", "{request}"),
])
# Each category has clear examples and boundary rules.
# Fallback ('unclear') is explicit with a tie-breaking rule.
When using LLM classifiers, guard against low-confidence classifications by extracting confidence scores and routing uncertain requests to a clarification handler.
from dataclasses import dataclass
@dataclass
class RouteDecision:
"""Structured output from an intent classifier."""
category: str
confidence: float # 0.0 to 1.0
raw_output: str
def classify_with_confidence(
llm,
request: str,
confidence_threshold: float = 0.75,
) -> RouteDecision:
"""Classify a request and return the category with a confidence score.
Uses a two-shot prompt that asks the LLM to also estimate its
classification confidence as a percentage.
Args:
llm: A LangChain-compatible chat model.
request: The user request string to classify.
confidence_threshold: Minimum confidence required to accept the routing decision.
Returns:
A RouteDecision with category, confidence (0.0–1.0), and raw output.
"""
prompt = ChatPromptTemplate.from_messages([
(
"system",
(
"Classify this request into one of three categories: 'booker', "
"'info', or 'unclear'. Also estimate your confidence as a number "
"from 0 to 100. Output in JSON format only.\n"
"Format: {{\"category\": \"<category>\", \"confidence\": <0-100>}}"
),
),
("user", "{request}"),
])
chain = prompt | llm | StrOutputParser()
raw_output = chain.invoke({"request": request})
# Parse JSON output to extract category and confidence
import json
try:
parsed = json.loads(raw_output)
confidence = min(max(parsed["confidence"] / 100.0, 0.0), 1.0)
category = parsed["category"].strip()
except (json.JSONDecodeError, KeyError):
# Fallback: low confidence on parse failure
confidence = 0.0
category = "unclear"
return RouteDecision(
category=category if confidence >= confidence_threshold else "unclear",
confidence=confidence,
raw_output=raw_output,
)
def apply_fallback(decision: RouteDecision) -> str:
"""Apply fallback logic for low-confidence or unclear classifications.
Args:
decision: The classification result from classify_with_confidence.
Returns:
A clarification request message if confidence is below threshold.
"""
if decision.category == "unclear":
return (
f"I'm not confident I understood your request correctly "
f"(confidence: {decision.confidence:.0%}). Could you rephrase?"
)
# Confident classification — proceed with normal routing
handler_map = {
"booker": lambda: f"Booking processed for '{decision.raw_output}'",
"info": lambda: f"Information retrieved for '{decision.raw_output}'",
}
handler = handler_map.get(decision.category)
return handler() if handler else "unclear_handler fallback"
Always define a fallback handler — Every routing system must have an unclear_handler (or equivalent) for low-confidence or unrecognized inputs. Never let an unclassified request fall through without handling it explicitly. Follow the Fail Fast law from code-philosophy: invalid states halt with a clarification request, not a silent default.
Document every route's input/output contract — Each handler function must specify its expected input shape and return type. Use Python type hints and docstrings as required by the Atomic Predictability principle (pure, predictable functions).
Ensure exhaustive branch coverage — Every possible router output string must map to exactly one branch or edge in the dispatch logic. Verify with static analysis: count branches against category count — they must match.
Provide explicit disambiguation rules in the classifier prompt — When categories could overlap (e.g., "book a restaurant" vs. "restaurant review"), include tie-breaking rules in the system prompt so the LLM produces deterministic output.
Log routing decisions and fallback events — Every classification (category, confidence, raw output) should be logged for monitoring. Fallback events are especially important: they indicate gaps in your route definitions or prompt quality.
Reference code-philosophy laws — All routing code must follow the 5 Laws of Elegant Defense:
Test routing with adversarial inputs — Include test cases for ambiguous, multi-intent, and out-of-scope queries to verify that the fallback chain activates correctly.
Never skip the fallback handler — An empty or missing unclear_handler is a critical bug. The system must always return something meaningful, never crash on unclassifiable input.
Do not embed routing logic inside handlers — Handlers should only execute their domain task. Routing (classification + dispatch) is a separate concern handled by the coordinator/router component.
Never use bare string matching without normalization — Always .strip() and normalize case before comparing router outputs. Inconsistent whitespace causes silent misrouting bugs.
Do not hardcode routing categories as magic strings scattered across the codebase — Define category constants or an Enum (e.g., RouteCategory.BOOKER) to prevent typos that create unreachable branches.
Never route based solely on a single keyword without context — A request containing "book" might mean "book a flight" (routable) or "book a research paper" (unclear). Use multi-signal classification, not single-keyword heuristics.
Do not add new routes without updating the fallback and test suite — Every new route must have: an updated classifier prompt with category examples, a handler implementation, and at least two test cases (one positive, one ambiguous).
When applying this skill, produce output in the following structure:
ChatPromptTemplate (or equivalent) with all categories, examples, and disambiguation rules.RunnableBranch, LangGraph add_conditional_edges, or ADK sub-agent configuration connecting the router to handlers.| Skill | Purpose |
|---|---|
prompt-chaining | Sequential workflows that precede or follow routed decisions — combine when routing feeds into multi-step chains |
multi-agent-collaboration | Coordination between routed sub-agents after dispatch — handles inter-agent communication once routing completes |
parallelization | Routes queries to parallel sub-workers for independent processing — use when multiple routes can execute concurrently |
📖 skill(local cache): customize-opencode, code-philosophy