DEV Community

Cover image for Execution Trees, Not More Logs: A Better Debugging Model for AI Agents
Raju Dandigam
Raju Dandigam

Posted on

Execution Trees, Not More Logs: A Better Debugging Model for AI Agents

Mental timeline reconstruction fails at scale

A flat log can tell you that five things happened. It often cannot tell you which operation caused the next one, which failure triggered a fallback, or whether three tool calls were children of one planning step or unrelated work.

That distinction matters for AI agents because the path is part of the behavior.

I maintain AgentInspect, an open-source TypeScript toolkit for inspecting agent executions locally. This article explains why I chose execution trees as the primary debugging model, using synthetic fixtures verified against agent-inspect@6.17.4.

The problem with reading an agent run as a timeline

Consider a support agent that performs these operations:

09:00:00.000 plan started
09:00:00.020 inventory request started
09:00:00.060 inventory request failed: 503
09:00:00.061 inventory request started
09:00:00.120 inventory request succeeded
09:00:00.150 answer completed
Enter fullscreen mode Exit fullscreen mode

This is enough to reconstruct a simple story, but the reconstruction is happening in your head. Add nested agents, parallel tools, reused operation names, and interleaved application logs, and timestamps stop being a reliable picture of causality.

An execution tree makes the relationship explicit:

support-agent
├── plan
├── fetch-inventory (failed: 503)
├── fetch-inventory (success)
└── draft-answer
Enter fullscreen mode Exit fullscreen mode

The tree does not replace raw event data. It is a projection of that data for the question developers usually ask first: What path did this run take?

Capture meaningful boundaries in TypeScript

AgentInspect provides wrappers for a run and for named steps. Here is a deliberately small example:

import { inspectRun, step } from "agent-inspect";

await inspectRun(
  "travel-planner",
  async () => {
    const plan = await step("plan", async () => ({
      destinations: ["SFO", "SEA"],
    }));

    const [flights, hotels] = await Promise.all([
      step.tool("search-flights", async () => [
        { id: "F-101", price: 220 },
      ]),
      step.tool("search-hotels", async () => [
        { id: "H-202", nightly: 180 },
      ]),
    ]);

    return step.llm("rank-options", async () => ({
      plan,
      flights,
      hotels,
    }));
  },
  { traceDir: "./.agent-inspect" },
);
Enter fullscreen mode Exit fullscreen mode

This is manual instrumentation. It does not claim that a wrapper can automatically discover every framework-internal operation. The purpose is to record the boundaries you care about: the run, its planning step, the two sibling tool calls, and the final model-facing step.

Then inspect the run locally:

npx agent-inspect view travel-planner \
  --dir .agent-inspect \
  --summary
Enter fullscreen mode Exit fullscreen mode

Four shapes that reveal different classes of bugs

1. Nested work exposes ownership

A three-level synthetic fixture renders like this:

Execution Tree:
✔ outer (120ms)
  ✔ middle (80ms)
    ✔ inner (50ms)
Enter fullscreen mode Exit fullscreen mode

Those two spaces are not decoration. They tell us that inner belongs to middle, which belongs to outer. If inner fails, we know which higher-level operation owned it. With flat logs, matching IDs or surrounding timestamps would be required to infer the same structure.

Nesting is especially useful when one agent delegates to another, a tool performs several sub-operations, or a retrieval step owns both a query rewrite and a vector search.

2. Fallbacks expose recovery behavior

Now consider an error-recovery fixture:

Execution Tree:
✖ tool:primary-search (100ms)
    Error: primary search unavailable
✔ tool:fallback-search (200ms)
✔ handle-recovered-result (50ms)
Enter fullscreen mode Exit fullscreen mode

The final run may still be successful. If we looked only at the answer, the failed primary search could disappear from the debugging story. The tree preserves both facts:

  • the primary path failed;
  • the recovery path completed.

That distinction can change the engineering decision. A successful answer produced by a fallback may be acceptable, but a sudden rise in fallback use could still indicate a degraded dependency or an expensive routing change.

3. Repeated siblings expose retries

Retries deserve their own visible shape:

Execution Tree:
✖ tool:fetch-inventory (40ms)
    Error: synthetic 503 from upstream
✖ tool:fetch-inventory (45ms)
    Error: synthetic 503 from upstream
✔ tool:fetch-inventory (60ms)
✔ handle-recovered-result (30ms)
Enter fullscreen mode Exit fullscreen mode

A final success status would hide the cost of reaching success. The repeated tool name makes the retry sequence visible. It also gives a deterministic check something concrete to evaluate: for example, whether fetch-inventory exceeded an allowed call count.

The tree alone does not tell us whether the retry policy was correct. It gives us evidence that the policy was exercised.

4. Parallel siblings expose concurrency

A parallel fixture renders as sibling operations:

Execution Tree:
✔ tool:search-hotels (300ms)
✔ tool:search-flights (200ms)
✔ tool:search-cars (100ms)
Enter fullscreen mode Exit fullscreen mode

The durations are not meant to be added. These steps are siblings, and may overlap. That protects us from a common timeline mistake: assuming each timestamped operation waited for the previous one.

The tree does not prove that concurrency was optimally implemented, but it accurately preserves the structural relationship needed to investigate it.

Trees are a view, not the entire evidence model

It is tempting to turn a readable tree into the only stored artifact. I avoided that because a human-readable view necessarily compresses information.

The underlying trace may include identifiers, timestamps, status, inputs or outputs (subject to capture policy), observations, and metadata. Different questions need different projections:

structured trace
├── tree      -> what path happened?
├── check     -> did an invariant hold?
├── diff      -> what changed between runs?
├── report    -> what should a reviewer read?
└── bundle    -> what evidence can be shared?
Enter fullscreen mode Exit fullscreen mode

An execution tree is the fastest entry point, not a substitute for checks or analysis.

Turn a suspicious shape into a deterministic check

Suppose the retry tree reveals that an inventory tool can run three times. If the intended policy permits at most two calls, encode that expectation rather than relying on future visual inspection.

At the CLI level, a trajectory check can require tools and fail on recorded observations:

npx agent-inspect check travel-planner \
  --dir .agent-inspect \
  --preset trajectory \
  --required-tool search-flights \
  --fail-on-observation failed
Enter fullscreen mode Exit fullscreen mode

For richer rules, AgentInspect exposes an experimental TraceContract API that can express tool requirements, forbidden tools, maximum calls, ordering, run status, duration, model allowlists, and token ceilings. Because that API is beta in the referenced release, pin the version and test the exact semantics before using it as a CI gate.

The important workflow is broader than one API:

  1. inspect the tree;
  2. identify a stable behavioral invariant;
  3. encode it as a deterministic check;
  4. keep human judgment for context-dependent questions.

What the tree cannot tell you

A clean tree does not prove that an answer is correct. A required retrieval step may return irrelevant documents. A model call may produce unsupported claims. A tool can succeed technically while returning stale data.

Execution trees are strongest for structural questions:

  • Which operations ran?
  • Which operation owned a failure?
  • Was a fallback or retry used?
  • Which work happened as siblings?
  • Where did a run stop?

Use semantic evaluators, domain tests, and human review for content quality. The most reliable agent debugging workflow combines these layers rather than asking one visualization to answer every question.

Debug the path, not only the answer

The final response is what the user sees, but the execution path is what the engineer can improve. A tree turns that path from an inferred narrative into a concrete artifact.

That is the design principle behind AgentInspect’s local view: preserve causal structure, expose unsuccessful work even when recovery succeeds, and make suspicious patterns easy to convert into repeatable checks.

You can explore the exact release used here on GitHub. If you try it, start with a synthetic failure-and-fallback fixture. A perfect happy path is the least interesting test of a debugger.

Top comments (40)

Collapse
 
heinrichneb profile image
Heinrich Neb

There is a fifth shape I would add to the four, because it is invisible in every view except the tree: sibling calls to the same tool with paraphrased inputs and no failure between them. Not a retry (nothing failed), not a fallback (nothing was replaced), just the same question asked again in different words. We found it in our own agent traces around memory retrieval: the agent recalls, gets a plausible answer, and recalls again with a reworded query, sometimes several times, before it acts. Every one of those siblings looks healthy on its own. The cost only shows when you notice that each extra call is also an extra model round, and each round resends the full context.

The check we derived from it is deterministic in your sense: same tool, siblings under one parent, input similarity above a threshold, zero failures in between. That pattern is a policy problem, not a dependency problem, and it responds to a one-line instruction change ("recall once, then act") rather than to any change in the tool. The tree is what made it a countable thing instead of a feeling that the agent was "chatty".

Collapse
 
raju_dandigam profile image
Raju Dandigam

@heinrichneb, that is a useful fifth shape: redundant sibling work without an error edge. I’d make the check rely first on a stable operation or resource key plus normalized arguments, with semantic similarity as an optional signal; otherwise a paraphrase threshold can make a supposedly deterministic rule drift. Recording an explicit purpose or dedupe key at the adapter boundary could make “recall once, then act” enforceable as well as visible.

Collapse
 
heinrichneb profile image
Heinrich Neb

The stable-key-first ordering is right, and I built it last night to check - it came back empty, in a way that sharpens your caveat rather than contradicting it.

The detector pulled typed key/value pairs out of 735 stored entries: env assignments, pinned versions, host:port, addresses. 58 keys, 56 in agreement, 2 collisions. Both collisions were false. port: looked like a key and is not one - a machine legitimately runs many services on many ports. And the one env-variable clash turned out to be the same variable name in two different systems.

So the key has to be single-valued by nature and carry a scope. OLLAMA_KEEP_ALIVE is not a key; kanzlei:OLLAMA_KEEP_ALIVE and node-1:OLLAMA_KEEP_ALIVE are two. Which is your normalized-arguments point, one level earlier than I had it.

On "recall once, then act" being enforceable: we have an accidental measurement. That line sits in our benchmark's instruction appendix. A config change dropped it without anyone noticing, and recalls per session went from 2.87 to 4.00 on identical tasks. Same corpus, same model. The rule was doing real work, and its absence was invisible until we diffed two runs file by file - which is your argument for recording the intent at the boundary, made by its absence.

Thread Thread
 
raju_dandigam profile image
Raju Dandigam

@heinrichneb, that measurement is unusually useful because it separates two failure modes: a key without scope creates false collisions, while a missing intent constraint creates silent behavioral drift. I’d encode the first as a composite identity such as (system, resource, key) and record the second as versioned policy evidence on the trace root. The 2.87 → 4.00 recall shift then becomes a deterministic regression: same task fixture, same model, different policy version, and measurable extra sibling work.

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

Repeated siblings expose only the retries that live above the boundary you wrapped. A step.tool around a client that retries internally records one node whether the upstream saw one request or three, so an empty retry shape is not evidence that no retry happened — the same "success hides the cost" the tree was built to surface, one layer down.

The deterministic check inherits it: a max-calls rule counts instrumented calls, so the gate stays green while the dependency took three. Worth stating next to the projection list, because the tree is the artifact people will trust for "was a fallback or retry used?".

Collapse
 
raju_dandigam profile image
Raju Dandigam

@vinhnguyenthanhdn, exactly—this is an observability-boundary problem, and “no retry observed” must not be reported as “no retry happened.” If the SDK retries below a wrapped step.tool, agent-inspect only has evidence for the outer operation unless the adapter or client instrumentation emits each attempt. The clean contract is to expose adapter capabilities and mark retry evidence unavailable when that boundary is opaque, then use explicit attempt metadata when the integration can see it. I’ll make this caveat more explicit because a green max-calls gate without that context would be misleading.

Collapse
 
hannune profile image
Tae Kim

The causality gap is real and it's worse than it looks when you've got concurrent tool calls. I'd been chasing a retry bug for two days and the timeline looked fine; it was only when I drew the parent-child relationship on paper that I saw a planning step that had silently spawned a second branch no one was accounting for. The tree structure you're describing is exactly that made automatic, and it's the right unit to debug against. One thing I'm curious about: how does AgentInspect handle steps that share a name across multiple runs, like a polling loop that calls the same tool twenty times?

Collapse
 
raju_dandigam profile image
Raju Dandigam

@hannune, each run is scoped by its own run ID, and repeated steps keep distinct span IDs and parentage even when the display name is identical. A polling loop calling the same tool twenty times should therefore appear as twenty occurrences rather than one collapsed node. A max-calls rule can count those occurrences within the relevant parent. An explicit attempt or iteration attribute is also useful when repetition is intentional; otherwise the tree shows repetition but not the loop’s intended boundary. Thanks for raising that distinction.

Collapse
 
unitbuilds profile image
UnitBuilds

Actually 1 of the reasons why I built a custom VC for my IDE and velocity mcp, with a merkle root audit trail. Between the 2 of them, the sitemap + the tool call history creates a clear picture of what changed where, by who and why they did it. Clean intent, clean action, clean credentials, so everything is auditable. Eg. changing a bool to a nullable, would sit in the sitemap's merkle root along with agent context, to say it was made nullable, because of a schema change, along with the tool calls executed to do so. That in turn traces to the schema change, which reveals it was made nullable for a new feature added. That distinction allows you to trace the versions of the app, so if a flaw surfaces, you know when it became a problem and when it was resolved, so you have a window of clients you know you have to patch

Collapse
 
raju_dandigam profile image
Raju Dandigam

@unitbuilds, the Merkle-root provenance is a strong complement to execution tracing. The trace answers what actually ran and where it failed; the versioned root and intent record explain why that code or schema existed. Linking an evidence bundle to the commit or Merkle root would make it possible to move from a runtime failure back to the exact change and rationale that introduced it.

Collapse
 
unitbuilds profile image
UnitBuilds

That was the idea, so if anything breaks and gets patched, it's permanent, the history shows exactly why it changes, so if any agent wants to revert the change, they know it wont work and wont even try it, unless they can prove it would work properly.

Thread Thread
 
raju_dandigam profile image
Raju Dandigam

@unitbuilds, exactly—the stored rationale becomes a guardrail against cycling back into a known-bad state. I would still make the barrier evidence-based rather than absolute: an agent may propose a revert, but it must address the original failing contract and produce new passing evidence before the change is accepted. That keeps history informative without turning an old decision into permanent dogma.

Collapse
 
mateo_ruiz_6992b1fce47843 profile image
Mateo Ruiz

The execution tree framing makes a lot of sense because causality is usually the first thing you lose when agents start delegating, retrying, and running tools in parallel. One thing I’d add is correlating the tree with cost and latency at each node. A fallback might produce a correct answer, but if it consistently adds 3x the tokens or latency, that’s an architectural signal worth surfacing. We’ve run into this kind of issue at IT Path Solutions when looking at production agent workflows the trace becomes much more useful when it explains not just what path the agent took, but what that path actually cost.

Collapse
 
raju_dandigam profile image
Raju Dandigam

@mateo_ruiz_6992b1fce47843, exactly—the tree becomes more useful when duration, tokens, and estimated cost stay attached to each node. I would keep those as facts on the underlying event stream and let the tree project them, so a recovered run can still show that the fallback tripled cost or latency. Aggregate fallback frequency is the production signal I’d watch alongside the single-run view.

Collapse
 
zira125 profile image
Zira

The adapter-boundary caveat is the part I would make machine-readable. A trace node should record whether retries are instrumented at the wrapper, client, or transport layer, plus an attempts_observed versus attempts_unknown field. Then a max-calls check can fail closed, or at least surface “incomplete evidence,” instead of treating one successful wrapper span as proof that the dependency was called once. That keeps the execution tree useful without overstating what it can see.

Collapse
 
raju_dandigam profile image
Raju Dandigam

@zira125, yes—visibility coverage needs to be part of the evidence contract. I like a tri-state check result here: pass, fail, or inconclusive. attempts_observed: 1 with attempts_unknown: true must not satisfy maxAttempts: 1; it should identify the missing instrumentation layer. That lets the tree stay honest about both what happened and what the adapter could actually observe.

Collapse
 
eduzsh profile image
Edu Peralta

Flat logs make me invent causality that was never there. When a coding agent retries a tool, spawns a nested plan, and interleaves three "search" calls, the timeline reads as order while the bug was which parent owned the failure. Trees fix the question I actually ask when a run goes sideways: was this a fallback, a parallel sibling, or a second attempt of the same step. Manual step boundaries are annoying to add. They beat scrolling a successful looking log that hid the branch that mattered.

Collapse
 
raju_dandigam profile image
Raju Dandigam

@eduzsh, “the timeline reads as order” captures the failure mode precisely. I’d let adapters infer framework-native boundaries and reserve manual steps for domain transitions the framework cannot know about—approval granted, retrieval accepted, or a durable commit completed. That keeps instrumentation manageable while preserving the parentage needed to distinguish retries, fallbacks, and parallel siblings.

Collapse
 
routinekit profile image
RoutineKit

The “path is part of the behavior” line is the whole argument — flat logs treat siblings and children the same, so you debug symptoms instead of the plan that spawned them.

I’ve started forcing a tiny pre-flight before any agent turn: outcome, out of scope, done looks like, never invent. The third line is where the tree should attach — if “done” doesn’t name which child steps must succeed, a fallback can look like progress.

Curious whether your fixtures make it obvious when three tool calls are children of one planning step vs three unrelated roots, or if that still takes a human stare at the tree.

Collapse
 
khalisollis profile image
Khali Sollis

What I find especially interesting here is the distinction between successful output and healthy execution.

We tend to evaluate intelligent systems from the endpoint because the endpoint is visible. Did the agent answer correctly? Did the task complete? Did the workflow return what we expected?

But a successful result can conceal an extraordinary amount of dysfunction upstream.

An agent can fail repeatedly, invoke an unnecessary fallback, duplicate work, consume far more resources than expected, and still hand the user a perfectly acceptable final answer. If we evaluate only the endpoint, all of that disappears into the word success.

The execution tree therefore seems valuable for something larger than debugging: it preserves the difference between what happened and what ultimately worked.

That distinction matters because resilient systems are often designed to absorb failure. But the better a system becomes at recovering from failure, the easier it may become to overlook the failures it is continuously recovering from.

There is almost a paradox there:

Successful recovery can reduce the visibility of the condition that made recovery necessary.

And once failure becomes invisible, “the system still works” can become a dangerously reassuring metric.

I particularly liked your example of retries. Three attempts followed by success and one successful attempt may produce the same visible outcome, but they are not equivalent executions. One tells us only that the task succeeded. The other tells us something about the condition of the system that produced that success.

This also makes your separation between the structured trace and its different projections important. A tree answers one question; checks, diffs, reports, and semantic evaluation answer others. No single representation should be mistaken for the evidence itself.

Perhaps the broader principle is this: when systems become sufficiently good at recovering, observability has to preserve the failures that success would otherwise erase.

Excellent piece, Raju. “Debug the path, not only the answer” is a useful engineering principle — but I suspect it is also a much broader way of thinking about how we evaluate complex systems.

Collapse
 
raju_dandigam profile image
Raju Dandigam

@khalisollis, that recovery paradox is exactly why I think outcome status and execution health have to remain separate. A run can be functionally successful while operationally degraded, so retries, fallbacks, and recovered errors should consume an explicit recovery budget rather than disappear behind the final answer. The trend in that recovery tax is often the earliest signal that a system is normalizing failure.

Collapse
 
michielinksee profile image
michielinksee

The execution-tree framing matches what we see measuring agent-SaaS integrations: flat logs can't tell you whether a retry loop was a fallback or a bug. One thing I'd add from our data, the first causal failure is often upstream of the agent entirely (auth/scope issues on the SaaS side). Does agent-inspect distinguish tool-side vs agent-side causes?

Collapse
 
raju_dandigam profile image
Raju Dandigam

@michielinksee, only when the capture boundary provides enough evidence. A tool span can preserve the upstream error, safe metadata, and its parentage, so an adapter may classify an auth or scope failure as tool-side. But agent-inspect should not infer root cause from a generic exception; when provenance is not observable, it should remain unknown rather than being labeled agent-side. Your SaaS example is a good case for a normalized failure-domain field emitted by the adapter. Which distinctions have been most useful in your data—auth, rate limiting, schema errors, or availability?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.