Why Your AI Agent Can't Execute Its Own Plan: Bridging the Gap Between Local LLM Intelligence and Real-World Software Reliability A developer's analysis on tamiz.pro identifies why AI agents fail to execute their own plans, attributing the issue to an architectural mismatch between probabilistic LLMs and deterministic software systems. The post categorizes failure modes such as opaque natural-language plans and state drift, and proposes formal intermediate representations and typed action schemas to improve execution reliability. Originally published on tamiz.pro. You prompt your agent to orchestrate a multi-step workflow. It generates a beautifully reasoned plan. Then it fails on step three. Or eight. Or quietly produces wrong output that no one notices until it's too late. This isn't a prompt engineering problem. It's an architecture problem — one rooted in the fundamental mismatch between probabilistic language models and deterministic software systems. Understanding why agents fail to execute their own plans is the prerequisite to building ones that don't. This article dissects the technical failure modes, traces them through the agent stack, and explores architectural patterns that close the gap between LLM-grade reasoning and production-grade execution reliability. Before diagnosing the failure modes, we need to be precise about what's actually happening when an agent "executes a plan." A modern agentic system has two conceptual layers that are conflated in practice: The LLM lives entirely in the reasoning layer. When we say an agent "executes its plan," what's really happening is that the LLM generates text that a software harness interprets as instructions. The reliability of the whole system is bounded by whichever of these two layers is weaker. In practice, the execution layer is where things collapse. And they collapse in predictable, categorizable ways. An LLM-generated plan is a sequence of natural language instructions. When you say "the agent will execute this plan," you're implicitly assuming the plan is executable by something other than another LLM. But here's the problem: the plan is opaque. What the LLM generates as a "plan": plan = {"step": 1, "action": "query user database", "params": "active users last 30 days"}, {"step": 2, "action": "aggregate metrics", "params": "count by region"}, {"step": 3, "action": "format report", "params": "PDF with charts"}, {"step": 4, "action": "send to stakeholder", "params": "weekly digest list"} Step 3 says "format report" with a param of "PDF with charts." That's not a program. That's a description of intent. The execution engine needs to: Every one of these decisions is a potential point of failure. The LLM that generated the plan doesn't actually understand any of this — it's predicting the next reasonable word in a sequence. The execution engine that does need to understand it is a separate system, often hand-written, and almost always incomplete. The fix: Plans must be expressed in a formal intermediate representation IR , not natural language. This means the LLM generates structured output that maps to concrete, typed actions: // Formal plan representation interface ExecutablePlan { steps: ExecutionStep ; constraints: PlanConstraints; validation: ValidationRules ; } interface ExecutionStep { id: string; action: ActionType; // Enum, not string inputs: TypedSchema; // JSON Schema validated dependencies: string ; // DAG, not implicit ordering retryPolicy: RetryConfig; timeout: Duration; } enum ActionType { DATABASE QUERY, API CALL, FILE WRITE, EMAIL SEND, // ... exhaustively enumerated } The LLM fills in parameters, but the shape of execution is constrained by the type system. This eliminates the semantic gap between "what the plan says" and "what the code does." Even when the plan is well-formed, a silent killer is state drift — the world changes between when the plan is generated and when a step is executed. Consider this sequence: T=0s : Agent plans → "fetch user X's data from API endpoint /v2/users" T=5s : API endpoint /v2/users is deprecated; /v3/users is now live T=6s : Agent executes step 1 → 404 error, plan halts Or worse — a silently wrong result: T=0s : Agent plans → "calculate total revenue from transactions table" T=5s : A deployment changes the schema; a new currency column appears T=6s : Agent executes → queries sum revenue but gets mixed currencies T=7s : Agent reports $1.2M revenue to stakeholders → wrong by 3x The LLM has no awareness of these state transitions. It generates a plan based on its training data and whatever context you provided. It doesn't know the schema changed yesterday. It doesn't know the API version you meant. The fix: Agents need a state awareness layer that validates plan assumptions against current reality before execution: python class PlanValidator: def init self, state probe: StateProbe : self.probe = state probe def validate plan self, plan: ExecutablePlan - ValidationReport: for step in plan.steps: Check that referenced schemas still exist schema = self.probe.get schema step.inputs if not schema.matches step.params.schema : raise SchemaMismatchError step.id, schema, step.params.schema Check that endpoints are reachable if step.action == ActionType.API CALL: health = self.probe.check endpoint step.params.url if health.status = "healthy": self.flag risk step.id, f"Endpoint degraded: {health.status}" return ValidationReport completed=True, warnings=self.warnings This turns plan execution from a blind leap into a verified execution. The agent still generates plans naturally, but they're validated against the live system state before any side effect occurs. LLMs are excellent at generating plans. They're terrible at handling deviations from plans — because error recovery requires situational awareness that the planning process doesn't carry forward. When an agent encounters an unexpected error, it has several options: The LLM, operating in a stateless request-response loop, has no memory of the original plan's intent beyond what's in the context window. It doesn't know which steps are on the critical path. It doesn't know whether a failure is transient or permanent. It makes a best-guess decision based on whatever context happened to be in the prompt. The fix: Implement structured error handling as a first-class component of the agent architecture: class ErrorRecoveryEngine: """ Separates error handling logic from the LLM's planning logic. Uses deterministic rules and bounded LLM calls for recovery. """ RECOVERY STRATEGIES = { "timeout": "retry with backoff", "skip with log", "abort" , "validation error": "retry with corrected params", "ask for clarification", "abort" , "auth failure": "retry with refreshed token", "abort" , "dependency unavailable": "retry after delay", "use fallback", "abort" , } def handle self, step: ExecutionStep, error: ExecutionError, context: PlanContext - RecoveryDecision: strategy type = self. classify error error strategies = self.RECOVERY STRATEGIES.get strategy type, "abort" Deterministic first-pass filtering viable = s for s in strategies if self. is viable step, s, context Bounded LLM call for nuanced decisions if len viable 1: decision = self. llm select recovery step, error, viable, context else: decision = RecoveryDecision action=viable 0 return decision def is viable self, step: ExecutionStep, strategy: str, ctx: PlanContext - bool: """Deterministic checks — no LLM involved.""" if strategy == "retry with backoff" and step.retry count = step.retry policy.max attempts: return False if strategy == "use fallback" and not step.has fallback: return False if strategy == "skip with log" and step.is critical path: return False return True The key insight: don't ask the LLM to solve everything. Use deterministic logic for structural decisions can we retry? have we exhausted retries? is this critical? and reserve LLM calls for genuinely ambiguous situations where contextual judgment is needed. Each LLM call should be bounded — few tokens, focused question, short response. Your agent's context window is its short-term memory. And it's a bad one. At any given point, the context window contains: As the plan executes, this window grows. At some point, it hits the token limit. What gets truncated? Usually, it's the oldest messages — which often includes the original plan and the reasoning that produced it. So the agent is now executing step five, but it can no longer see why it chose the approach in step one. It's making decisions in a vacuum, optimizing for local correctness rather than global coherence. This is the amnesic agent problem : the agent literally cannot remember its own rationale as it progresses through a long plan. The fix: Explicit external memory management — the plan and its rationale must be persisted outside the context window: class AgentMemory: """ External memory store decoupled from the LLM context window. Persists plan state, rationale, and execution history. """ def init self, store: KVStore : self.store = store def save plan context self, run id: str, context: PlanContext : """Persist full context for retrieval at any step.""" self.store.set f"plan:{run id}", context, ttl=3600 def retrieve relevant context self, run id: str, current step: int, query: str - ContextSnapshot: """ Retrieve only the context relevant to the current decision point. Uses embedding similarity to avoid loading everything. """ plan = self.store.get f"plan:{run id}" Load rationale for nearby steps, not all steps window = range max 0, current step - 2 , min len plan.steps , current step + 1 relevant steps = plan.steps i for i in window return ContextSnapshot original intent=plan.intent, relevant steps=relevant steps, recent outcomes=plan.executed steps -3: , current query=query This gives you two benefits: first, the agent always has access to its original intent; second, you can control what gets loaded into the context window at each step, keeping it lean and focused. Most agent frameworks treat tool use as a simple function-calling interface: python @tool description="Search the knowledge base" def search knowledge base query: str - str: results = kb.search query return format results results The LLM sees the tool name and description and decides when to call it. But the description is always a natural language approximation of what the tool actually does . There's a semantic gap between "search the knowledge base" and the actual Elasticsearch query being constructed, the pagination logic, the relevance scoring, the error handling. When the LLM calls a tool with slightly wrong parameters, the result is wrong. When it calls the tool at the wrong time, the plan derails. When the tool returns an error the LLM doesn't recognize, the agent loops or hallucinates a fix. The fix: Formal tool contracts — every tool must declare its preconditions, postconditions, and error semantics: interface ToolContract { name: string; description: string; // What must be true before calling preconditions: precondition ; // What the caller can expect after successful execution postconditions: postcondition ; // Exhaustive error taxonomy with recovery guidance errors: ErrorCode ; // Max latency the caller should expect latencySLA: Duration; // Whether results are cached idempotent? caching: CachingPolicy; } interface ErrorCode { code: string; message: string; recovery: RecoveryStrategy; // "retry", "abort", "ask user", etc. hint: string; // What the LLM should try differently } This transforms tools from black boxes into verifiable components. The agent's execution engine can check preconditions before calling, interpret errors against a known taxonomy, and apply deterministic recovery strategies rather than hoping the LLM figures it out. Perhaps the most subtle failure mode: agents don't learn from execution. A plan is generated, executed, and if it fails, the LLM is shown the error and asked to continue. But the LLM doesn't retain anything from this failure. The next time a similar plan is generated, it makes the same mistake. The system has no memory of what went wrong, no model of its own failure modes, no way to improve. This is especially damaging because LLMs have a well-documented sycophancy problem — they tend to double down on their initial reasoning rather than self-correct when presented with contradictory evidence. When an agent fails at step 3 and you feed the error back, the LLM might acknowledge the error but then proceed to make the same type of error at step 4. The fix: Build execution feedback into the planning pipeline as a first-class loop, not an afterthought: class ExecutionFeedbackLoop: """ Captures execution outcomes and feeds them back to improve planning. Operates at two levels: online per-session and offline cross-session . """ def init self, session store: SessionStore, improvement engine: ImprovementEngine : self.session = session store self.improve = improvement engine def process outcome self, plan: ExecutablePlan, outcome: ExecutionOutcome : Online: update the current plan with what we learned adjusted plan = self. adjust plan plan, outcome Offline: accumulate patterns for systemic improvement self.improve.record f failure pattern outcome return adjusted plan def adjust plan self, plan: ExecutablePlan, outcome: ExecutionOutcome - ExecutablePlan: """ Apply deterministic adjustments based on execution results. Not an LLM call — just rule-based plan modification. """ adjusted = copy.deepcopy plan if outcome.step failed: failed step = outcome.failed step Add explicit error handling to the failed step failed step.error handling = self. infer error handler failed step, outcome Add dependency on the failed step succeeding for dependent in adjusted. find dependents failed step.id : adjusted. add dependency dependent.id, failed step.id return adjusted The key distinction: the online loop makes adjustments to the current plan using deterministic rules add retry, add error handling, add dependencies . The offline loop accumulates failure patterns across sessions and uses them to improve the planner itself — perhaps by fine-tuning, perhaps by improving system prompts, perhaps by building a failure-mode database that the planner consults. All of these failure modes share a common root: the LLM is being asked to do something it's architecturally unsuited for — bridging the gap between high-level intent and low-level execution. The solution is an executive layer — a software component that sits between the LLM the planner and the execution environment the tools, APIs, databases . Its responsibilities are: interface ExecutiveLayer { / Receive a plan from the LLM, validate, and execute / execute plan: LLMGeneratedPlan : AsyncGenerator