Module 08: Agent Runtimes & State Management Lesson 22 / 41
35–40 min read Level: Advanced
Part 3 — Autonomous AI Agents

Lesson 16.22: Agent Planning & Task Decomposition

Techniques for decomposing complex, multi-faceted objectives into discrete, actionable sub-tasks: task dependency graphs (DAG), sequential and parallel scheduling, Pydantic v2 structured plan validation, decoupled tri-component architecture (Planner, Executor, Verifier), and runtime replanning upon failure.

What You Will Learn

  • 1. The foundational architecture and operational mechanics of AI Agent Planning & Task Decomposition.
  • 2. Transforming ambiguous user goals into granular, cleanly scoped sub-tasks with deterministic criteria.
  • 3. Identifying task dependencies and constructing executable Directed Acyclic Graphs (DAGs).
  • 4. Analyzing the performance trade-offs of Sequential, Parallel, and Conditional execution modes.
  • 5. Designing a decoupled tri-component architecture separating Planner, Executor, and Verifier.
  • 6. Building type-safe Pydantic v2 execution schemas with built-in DAG cycle and self-dependency guards.
  • 7. Leveraging OpenAI Structured Outputs and the Responses API to parse model-generated plans reliably.
  • 8. Implementing server-side security checks: tool whitelists, required argument parsing, and permission isolation.
  • 9. Handling unexpected runtime failures via Error Classification Matrices and dynamic replanning workflows.
  • 10. Distinguishing Task-Level tool output validation from overarching Goal-Level objective verification.
  • 11. Implementing an enterprise AI Invoice Verification Agent validated by a comprehensive Pytest test suite.
  • 12. Measuring planning quality metrics: plan validity rates, step efficiency, and unnecessary call ratios.

01 The Challenge: Executing Multi-Step Complex Goals

In the previous lesson, we learned how an autonomous AI agent uses the ReAct loop (Observe → Decide → Act → Observe Again) to select tools and react dynamically to real-time observations. However, in enterprise environments, users rarely present simple, single-turn prompts. Imagine instructing Byteion's AI Financial Assistant:

"Verify this invoice, reconcile supplier details against our ERP database, check for duplicate historical payments, and generate a comprehensive audit report highlighting discrepancies."

This is not an isolated question answerable by a single tool call. It encompasses multiple interdependent operations across disparate systems:

1. Field Extraction: Extract vendor identity, tax numbers, line items, and grand totals from the invoice document.
2. Supplier Record Verification: Cross-reference active supplier licensing and verified bank routing details.
3. Historical Duplicate Search: Query historical invoices for identical bill amounts, invoice numbers, or payment dates.
4. Purchase Order Reconciliation: Compare line-item pricing against approved enterprise purchase orders.
5. Audit Report Generation: Synthesize all diagnostic findings into a structured, verifiable audit report.

If an agent is given the vague instruction "verify the invoice" without an explicit execution blueprint, it risks invoking supplier queries before extracting the supplier ID, making redundant queries, or looping indefinitely.

Defining Decomposition and Planning:

Task Decomposition is the systematic process of breaking an overarching goal into discrete, manageable sub-tasks with defined inputs, outputs, and dependencies. Agent Planning defines the ordering, tooling, validation contracts, and dynamic replanning criteria required to reach the objective.

02 Anatomy of a High-Quality Subtask

Proper decomposition requires clarifying the operational scope of a goal. Stating "Verify Invoice INV-1042" actually entails six distinct verification questions: Is the format valid? Is the supplier active? Does the total match the line items?

An enterprise-grade, executable sub-task schema must capture the following metadata attributes:

Attribute Data Type Operational Significance
task_id Integer / UUID Unique step identifier within the execution plan.
goal / purpose String Concise description of the specific objective to achieve.
tool Literal Whitelist Permitted backend tool registered in the dispatcher whitelist.
inputs / arguments JSON Dict Validated argument payloads or references to prior step outputs.
dependencies List[task_id] Precursor task IDs that must successfully complete prior to dispatch.
expected_output Schema Reference Formal schema definition expected from tool execution.
failure_policy Enum Fallback directives: retry, skip, fallback query, or abort execution.
{
  "task_id": 2,
  "goal": "Lookup verified supplier record",
  "tool": "get_supplier",
  "inputs": {"supplier_id": "SUP-1001"},
  "dependencies": [1],
  "expected_output": "SupplierRecord",
  "failure_policy": "mark_supplier_missing_and_continue"
}

03 Task Dependencies & Directed Acyclic Graphs (DAG)

A dependency exists when Task B cannot proceed until Task A produces its output. In robust software engineering, task relationships are formalized as a Directed Acyclic Graph (DAG):

flowchart TD T1["Task 1: Extract Invoice Data"] --> T2["Task 2: Validate Required Fields"] T2 --> T3["Task 3: Lookup Supplier Record"] T2 --> T4["Task 4: Check Duplicate Invoices"] T2 --> T5["Task 5: Match Purchase Order"] T3 --> T6["Task 6: Synthesize Findings"] T4 --> T6 T5 --> T6 T6 --> T7["Task 7: Generate Final Audit Report"]

Notice that Task 3, Task 4, and Task 5 all depend strictly on the validated fields from Task 2. However, they share zero mutual dependencies among themselves. Consequently, all three can be executed simultaneously in parallel!

04 Sequential, Parallel & Conditional Execution

The latency and throughput profile of an agent is determined by how sub-tasks are scheduled across runtime threads:

1. Sequential Execution

Tasks run consecutively in strict chronological order. Output of step $N$ serves as direct input for step $N+1$.

Extract → Validate → Lookup
2. Parallel Execution

Mutually independent tasks execute concurrently across async workers, drastically lowering cumulative wall-clock latency.

Supplier // Duplicate // PO Match
3. Conditional Execution

Dynamic runtime branching determined by previous results (e.g. aborting duplicate checks if an invoice is identified as fraudulent).

IF valid THEN check ELSE alert
Concurrency Warning: Never schedule state-mutating write actions (database updates, payments) in parallel without distributed locking. Uncontrolled parallel writes trigger race conditions and ledger corruption. Parallelism is safe primarily for idempotent read-only queries.

05 Should LLMs Always Generate Plans?

Relying indiscriminately on an LLM to formulate every execution step introduces unnecessary nondeterminism. Three planning paradigms exist:

  • Deterministic Planning (Code-Based): Fixed business logic hardcoded into linear Python functions (e.g., standard regulatory tax audits). Uncompromisingly predictable with zero API cost.
  • Model-Generated Planning (Dynamic LLM): Applied when user intent is heterogeneous and the sequence of steps cannot be known in advance. The LLM formulates a custom candidate plan.
  • Hybrid Planning (Production Best-Practice): Guardrail policies and core DAG boundaries are enforced by backend code, while the LLM is leveraged for intent decomposition, query reformulation, and semantic classification.

06 Decoupled Architecture: Planner, Executor & Verifier

In reliable agent design, the responsibilities of planning, executing tools, and verifying outcomes are strictly decoupled into three dedicated components:

flowchart TD REQ(["User Goal & Context"]) --> PLANNER["1. Planner (Generates DAG Plan)"] PLANNER --> VALIDATOR["2. Schema & Security Validator"] VALIDATOR -->|"Plan Approved"| EXECUTOR["3. Executor (Dispatches Whitelisted Tools)"] EXECUTOR --> TOOLS[("External APIs, Databases & Services")] TOOLS --> EXECUTOR EXECUTOR --> VERIFIER["4. Verifier (Task & Goal Evaluation)"] VERIFIER -->|"Checks Passed"| OUT(["Final Answer / Verified Report"]) VERIFIER -->|"Recoverable Failure"| PLANNER VERIFIER -->|"Terminal / Unrecoverable"| ESCALATE(["Human-in-the-Loop Review"])

07 Structured Execution Plans with Pydantic v2

To prevent models from hallucinating invalid tools or circular dependencies, we enforce structured validation contracts with Pydantic v2:

# app/planning_schema.py
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, model_validator

class PlanStep(BaseModel):
    model_config = ConfigDict(extra="forbid")
    step_id: int = Field(ge=1, le=10, description="Unique sequential step identifier")
    tool_name: Literal["get_order_status", "search_knowledge_base"]
    purpose: str = Field(min_length=3, max_length=300)
    order_id: str | None = Field(default=None, pattern=r"^ORD-[0-9]{4,}$")
    query: str | None = Field(default=None, min_length=2, max_length=200)
    limit: int = Field(default=3, ge=1, le=5)
    depends_on: list[int] = Field(default_factory=list)

class AgentPlan(BaseModel):
    model_config = ConfigDict(extra="forbid")
    goal: str = Field(min_length=5, max_length=500)
    steps: list[PlanStep] = Field(min_length=1, max_length=5)

    @model_validator(mode="after")
    def validate_plan_dag(self):
        step_ids = [s.step_id for s in self.steps]
        if len(step_ids) != len(set(step_ids)):
            raise ValueError("Step IDs must be strictly unique.")
        
        seen_ids = set()
        for step in self.steps:
            if step.step_id in step.depends_on:
                raise ValueError(f"Step {step.step_id} cannot depend on itself.")
            for dep in step.depends_on:
                if dep not in seen_ids:
                    raise ValueError(f"Step {step.step_id} depends on step {dep} which has not appeared earlier.")
            seen_ids.add(step.step_id)
        return self

08 LLM Plan Generation with Structured Outputs

Using the OpenAI Responses API responses.parse() method, we request structured outputs mapped directly into our validated Pydantic schema:

# app/planner.py
import os
from openai import OpenAI
from app.planning_schema import AgentPlan

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"), timeout=30.0)
MODEL_NAME = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")

PLANNER_SYSTEM_PROMPT = """
You are an expert deterministic task planner for an enterprise customer support agent.
Decompose the user's objective into a minimal, strictly executable DAG plan.
Allowed Tools:
1. 'get_order_status': Requires order_id (e.g. 'ORD-1001').
2. 'search_knowledge_base': Requires query and limit (1-5).
Rules:
- Never plan destructive write actions (refunds, cancellations, deletions).
- Only declare a dependency when Step B genuinely requires the output from Step A.
- Keep plans within 1 to 5 steps maximum.
"""

def generate_plan(user_goal: str) -> AgentPlan:
    if not user_goal.strip():
        raise ValueError("User goal cannot be empty.")

    response = client.responses.parse(
        model=MODEL_NAME,
        input=[
            {"role": "system", "content": PLANNER_SYSTEM_PROMPT},
            {"role": "user", "content": user_goal}
        ],
        text_format=AgentPlan
    )
    plan = response.output_parsed
    if not plan:
        raise RuntimeError("Planner failed to return a validated AgentPlan.")
    return plan

09 Why Server-Side Plan Validation is Mandatory

Directly dispatching LLM-proposed plans without server-side verification is a critical security risk. Three security invariants must be validated:

# app/plan_validator.py
from app.planning_schema import AgentPlan, PlanStep

def validate_step_arguments(step: PlanStep) -> None:
    if step.tool_name == "get_order_status":
        if not step.order_id:
            raise ValueError(f"Step {step.step_id}: order_id is required for get_order_status.")
    elif step.tool_name == "search_knowledge_base":
        if not step.query or len(step.query.strip()) < 2:
            raise ValueError(f"Step {step.step_id}: valid query required for search_knowledge_base.")

def validate_plan_security(plan: AgentPlan, max_steps: int = 5) -> None:
    if len(plan.steps) > max_steps:
        raise ValueError(f"Plan exceeds safety ceiling of {max_steps} steps.")
    for step in plan.steps:
        validate_step_arguments(step)

10 Engineering the Task Executor

The Executor dispatches steps in topological order and verifies that upstream dependencies have resolved successfully before invoking tools:

# app/executor.py
from app.planning_schema import AgentPlan, PlanStep
from app.plan_validator import validate_plan_security
from app.tools import get_order_status, search_knowledge_base

def execute_single_step(step: PlanStep) -> dict:
    if step.tool_name == "get_order_status":
        return get_order_status(step.order_id)
    elif step.tool_name == "search_knowledge_base":
        return search_knowledge_base(step.query, step.limit)
    raise ValueError(f"Unsupported tool: {step.tool_name}")

def execute_plan(plan: AgentPlan) -> dict:
    validate_plan_security(plan)
    step_results = {}
    completed_log = []

    for step in plan.steps:
        # Dependency Verification
        unmet_deps = [dep for dep in step.depends_on if dep not in step_results]
        if unmet_deps:
            raise RuntimeError(f"Step {step.step_id} cannot execute; missing dependencies: {unmet_deps}")

        result = execute_single_step(step)
        step_results[step.step_id] = result
        completed_log.append({"step_id": step.step_id, "tool": step.tool_name, "status": "completed"})

    return {"goal": plan.goal, "results": step_results, "completed_steps": completed_log}

11 Task Lifecycle & State Tracking

Tracking sub-task lifecycles protects against cascading failures and ensures partial observations are preserved across turns:

pending → running → [ completed | failed | cancelled | needs_approval ]

The needs_approval state is vital: whenever an action incurs monetary cost or external mutations (e.g. updating bank routing), execution suspends pending human-in-the-loop sign-off.

12 & 13 & 14 Replanning & Verification Architecture

When a task encounters failure, blind repetitive retries waste tokens. We govern dynamic recovery via an Error Classification Matrix:

Failure Category Remediation Policy Replan Required?
Invalid / Missing Argument Emit validation diagnostic and prompt user for correct order format No (Validation Halt)
Temporary Network Timeout Bounded retry with exponential backoff (max 2 attempts) No (Safe Retry)
Permission Denied (403) Terminate execution immediately and log security policy incident No (Bypass Forbidden)
No Search Results Found Perform semantic query rewriting with synonyms Yes (Replan Step)
Upstream Dependency Failed Skip reliant downstream steps and execute alternative fallback branch Yes (Replanning DAG)

Task-Level vs Goal-Level Verification:
• Task-Level: Verifies tool output conforms to expected schema (e.g. valid order record retrieved).
• Goal-Level: Evaluates whether cumulative outputs answer the user's overarching objective (e.g. audit report synthesized).

15 & 16 DAG Cycle Detection & Parallel Task Scheduling

If an execution plan contains cyclic dependencies (Step A requires Step B, which requires Step A), the runtime deadlocks. Our Pydantic validator guarantees that dependencies must reference strictly prior steps, mathematically preventing cycles:

# Invalid Cyclic / Forward Dependency Example (Blocked at Schema Level)
bad_plan = {
    "goal": "Verify Order",
    "steps": [
        {"step_id": 1, "tool_name": "get_order_status", "order_id": "ORD-1001", "depends_on": [2]},
        {"step_id": 2, "tool_name": "search_knowledge_base", "query": "policy", "depends_on": []}
    ]
}
# Raises ValueError: Step 1 depends on step 2 which has not appeared earlier.

17 & 18 Key Metrics for Planning Quality

Delivering a correct final answer is insufficient; engineers must evaluate plan efficiency using quantifiable telemetry metrics:

1. Plan Validity Rate:

Percentage of candidate plans that pass backend schema and security validation on turn 1.

2. Unnecessary Tool Call Rate:

Proportion of executed tool calls that contributed zero unique evidence toward the final answer.

3. Step Efficiency:

Average number of plan steps required to achieve goal resolution without redundant steps.

4. Replanning Cost Ratio:

Marginal token expenditure incurred when formulating secondary or revised DAG plans.

19 Seven Critical Planning Anti-Patterns

1. Over-Decomposition: Generating 5 unnecessary steps for a trivial factual question (wasting tokens and latency).
2. Dependency Neglect: Dispatching dependent queries before preceding prerequisite outputs have been captured.
3. Blind Model Trust: Executing raw LLM-suggested tool names directly without server-side schema whitelisting.
4. Unbounded Retry Loops: Looping indefinitely on persistent server timeouts, racking up cloud billing costs.
5. Concurrent State Mutation: Dispatching parallel database writes without atomic transactional isolation.
6. Omitting Goal Verification: Assuming successful individual tools imply overarching objective completion.
7. Permission Bypass via Replanning: Attempting to query forbidden resources with alternative tools on 403 errors.

20 Custom Orchestrators vs OpenAI Agents SDK & LangGraph

Our custom Pydantic DAG engine provides maximum determinism, auditability, and debuggability. For enterprise fleets with hundreds of collaborating agents, existing frameworks offer higher-level abstractions:

• OpenAI Agents SDK: Optimized for multi-agent delegation handoffs and conversational session primitives.
• LangGraph: Industry standard for cyclical state graphs, persistent checkpoints, and human intervention hooks.

Interactive Lab: Agent Planning & DAG Control Center

Experience interactive task decomposition graphs, test Pydantic plan validation constraints, and simulate dynamic runtime replanning.

Select an operational business scenario to visualize how complex objectives decompose into structured DAG plans:

21 Practical Project: AI Invoice Verification Agent

Below is the production FastAPI invoice agent router and its automated Pytest test suite:

# app/invoice_agent_service.py
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, Field
from app.planner import generate_plan
from app.executor import execute_plan

router = APIRouter(prefix="/v1/invoice-agent", tags=["Invoice Agent"])

class VerificationRequest(BaseModel):
    user_goal: str = Field(min_length=5, max_length=500)

@router.post("/verify")
def process_invoice_verification(request: VerificationRequest):
    try:
        # Step 1: LLM Generates Structured DAG Plan
        plan = generate_plan(request.user_goal)
        # Step 2: Validate & Execute via Hardened Dispatcher
        execution_report = execute_plan(plan)
        return {"status": "success", "plan": plan.model_dump(), "execution": execution_report}
    except ValueError as ve:
        raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=f"Plan validation failed: {str(ve)}")
    except RuntimeError as re:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Execution error: {str(re)}")
# tests/test_agent_planning.py
import pytest
from pydantic import ValidationError
from app.planning_schema import AgentPlan, PlanStep
from app.executor import execute_plan

def test_prevent_cyclic_and_forward_dependencies():
    # Step 1 depends on future Step 2 -> Must fail validation
    with pytest.raises(ValidationError) as exc:
        AgentPlan(
            goal="Test invalid dependency",
            steps=[
                PlanStep(step_id=1, tool_name="get_order_status", order_id="ORD-1001", purpose="Look up", depends_on=[2]),
                PlanStep(step_id=2, tool_name="search_knowledge_base", query="late delivery", purpose="Search", depends_on=[])
            ]
        )
    assert "depends on step 2 which has not appeared earlier" in str(exc.value)

def test_prevent_self_dependency():
    with pytest.raises(ValidationError) as exc:
        AgentPlan(
            goal="Test self dependency",
            steps=[
                PlanStep(step_id=1, tool_name="get_order_status", order_id="ORD-1001", purpose="Self dep", depends_on=[1])
            ]
        )
    assert "cannot depend on itself" in str(exc.value)

def test_valid_plan_execution():
    plan = AgentPlan(
        goal="Valid two step support lookup",
        steps=[
            PlanStep(step_id=1, tool_name="get_order_status", order_id="ORD-1001", purpose="Check order", depends_on=[]),
            PlanStep(step_id=2, tool_name="search_knowledge_base", query="delay policy", purpose="Check policy", depends_on=[1])
        ]
    )
    res = execute_plan(plan)
    assert len(res["completed_steps"]) == 2
    assert res["results"][1]["found"] is True

22 Knowledge Check: Multiple-Choice Assessment

Test your technical mastery across 8 real-world agent planning scenarios:

1. What is the fundamental objective of Task Decomposition in AI Agents?

Explanation: Task decomposition breaks complex multi-step goals into manageable sub-tasks so execution correctness can be tracked.

2. What does a Task Dependency represent in an execution graph?

Explanation: Dependencies govern execution order so that downstream tools receive necessary, validated input parameters.

3. Under which condition is Parallel Execution optimal and safe?

Explanation: Independent read-only queries scheduled concurrently minimize cumulative wall-clock response latency.

4. What is the key architectural benefit of decoupling Planner, Executor, and Verifier?

Explanation: The tri-component architecture maximizes modularity and isolates failure points during agent execution.

5. Should candidate execution plans generated by LLMs be dispatched directly?

Explanation: Unvalidated plans can invoke unauthorized tools or malformed arguments, leading to crashes or security breaches.

6. How should an agent respond when encountering a Permission Denied (403) failure?

Explanation: Permission errors represent strict security boundaries; replanning must never be used to bypass access controls.

7. What is the fundamental difference between Task-Level and Goal-Level Verification?

Explanation: Even if every individual sub-task succeeds, omitting required sections from the final report leaves the goal unfulfilled.

8. What does 'Acyclic' mean in a Directed Acyclic Graph (DAG), and why is it critical in planning?

Explanation: Acyclic graphs guarantee that tasks can be ordered topologically with zero circular deadlocks.

23 Lesson Summary & Next Steps

In this lesson, we advanced from simple single-turn ReAct loops to visionary AI agent planning. We explored how to decompose complex user goals, construct Directed Acyclic Graphs (DAGs), enforce Pydantic validation contracts, and implement decoupled tri-component architectures (Planner, Executor, Verifier) to ensure enterprise-grade reliability and security.

Key Architectural Takeaways:

  • Plan Before You Leap: In complex, multi-step operations, construct an explicit DAG plan prior to tool invocation.
  • Decoupled Architecture: Isolating Planner, Executor, and Verifier into discrete units simplifies unit testing and telemetry.
  • Zero Blind Execution: Enforce strict server-side Pydantic schemas and dispatcher whitelists on all candidate plans.
  • Graceful Replanning: Handle runtime failures through structured error classification matrices rather than naive infinite retries.