How to Verify AI Agent Assertions Using Runtime Context

How to Verify AI Agent Assertions Using Runtime Context

In this blog we’re going to look at using an AI agent to analyze the behavior of a process. We’ll use both source code analysis and analysis using runtime context from Undo’s AI tooling. We’ll have an agent compare the two analyses, and use Undo again to verify the analysis. We’ll also see how combining the two approaches gives great results.

The purpose of this is to show, in a real-life example, how essential it is to have information on what really happened. Without it, agents both miss things and cast too wide a net and include things that aren’t really happening.

The example is a real-world daemon process running on a router (with details and names changed.)

Introducing the concept of a recording

At its core, Undo is a record-and-replay engine. Our record tools let you capture program execution, which is saved in a recording. Our replay tools load that recording and let you inspect every single point in time, right down to machine code and register level.

This accurate view of what happened is invaluable to AI to eliminate guesswork. Undo provides an MCP server and gives the agent strategies to inspect the recording. At Undo we talk a lot about debugging and root-cause analysis; that is, tracking down why something went wrong by working backwards from bad behavior. In this article there was no bad behavior or crash; it’s about using the tool to learn about the process and to verify agent assertions.

Introduction to the process

The process we’re going to look at is a daemon that looks after the case that the router lives inside – we’ll call it routerd. We know that when it is “idle”, it’s actually busy running a number of periodic tasks, and we want to know what tasks are running. Perhaps we’re a new hire, or someone from elsewhere in the company, or an experienced engineer with brain overload. Whatever the case, we’ve forgotten and the documentation isn’t helping us. There are many, many different platforms compiled from the same code, and all have different periodic tasks, for example LED control, fan control, statistics gathering.

What source code analysis told us

Note: we’re using Codex with GPT-5.5 and xhigh effort throughout.

To start, we ask:

> What does the routerd process do when it is idle on the X platform?

This gives us a list of 5 tasks, which we know is way too few. It’s not our first time playing this game, so we ask:

> Enumerate all the periodic tasks.

Now we have a list of 24 tasks. Better!

The problem is that the agent has separated these tasks into different categories:

  • Always started
  • Boot/restart periodic
  • Conditional
  • Feature gated/compiled near this path

We’re pretty happy with “always started”, not sure what “boot/restart periodic” even means, unsure of whether conditional tasks are actually run or not, and a bit queasy about “compiled near this path”. Near?

We can, of course, push the agent further and try to get it to figure out exactly what’s going on, but we’re starting to lose faith a bit after getting “compiled nearby” tasks reported. It seems like the agent doesn’t have enough information to give concrete answers. Time to get an Undo recording.

We attach Undo’s recording tool to the case manager process and let it record for 5 minutes. We then ask about it:

> What is happening in the recording routerd.undo? Just look at the recording, not the source code.

This gives us a list of 12 tasks, which is a lot less. We expected less as we knew that some tasks were conditional, but the tasks reported by Undo are not a strict subset of the tasks found by source code analysis. What’s going on? We ask the agent:

> This is very different from the source code analysis of 24 tasks. Why?

The agent enumerated all the tasks again, with reasons for the differences. We can take these in two categories: the ones it missed, and the ones it imagined.

Source code analysis missed some tasks

We see that there were 5 tasks identified in the recording that were not found by the source analysis at all. In the main dispatch loop, they were all called in similar patterns to this:

if (task) 
    dispatch_task(task)

Where task is task_t*:

typedef struct 
{ 
    type_t type; 
    void* func;
} task_t;

At runtime, func is the address of a function to be called, and type enumerates what sort of function it is: what arguments to pass. This gives the program flexibility to call tasks without having to change the code in the main loop.

To know what function func is from source code analysis, we have to find where type and func were assigned. In our case, this is made difficult by different tasks being set up by different subsystems, some of which are dependent on router configuration options that are only visible at runtime. The agent would have had to look at a far larger set of files, which would have been slow at best and still incomplete at worst.

It’s certainly not impossible for a human or for an agent, but it is hard and can be error prone. The very worrying part is that the source analysis gave no indication to us that it was incomplete. It looked just as convincing and authoritative as a complete list would have been.

With Undo, these tasks are no different from directly called tasks – they appear in the called-functions list that Undo produces for the agent. We can trust them as they come from unambiguous, deterministic data collection.

Source code analysis included many tasks that were not run

We’re also interested in what tasks weren’t run. Let’s say we want to implement some additional functionality in one of the tasks, and it’s not called in this idle loop, so we’re not sure if we can use this device for testing. Going back to the agent’s earlier categorization of differences, we can summarize the reasons:

  • Platform configuration choices are undetectable from the source code.
  • Function pointers are not populated as the code took a different initialization path than expected by the source analysis
  • The task is actually set up in a failure path.
  • The recording is not long enough to capture this task.

The agent also volunteers a confidence level for each of the things it has figured out. That’s nice! What’s not so nice is:

  • Many are “medium confidence”
  • There is no detail on what “different initialization path” might mean

Verifying and refining the agent’s assertions

Our next step could be to interrogate the agent further or switch to human mode and figure it out ourselves. Or, we can get another recording! This time we’ll record from router startup so that we can capture every initialization path in full. This takes a little longer as we have to wait for the router to reboot, but it’s likely to be faster than using human brain confirmation, and more accurate than an agent’s analysis.

Once recorded, we give it to the agent:

> I have an Undo recording from startup: routerd_startup.undo. Use it to verify your analysis.

The agent is able to confirm and refine some of its points and correct others.

Let’s say that the task we’re interested in was described as “different initialization path” by the agent. With Undo, we can see that the exact reason is that the task is only run in a first-boot situation – this run was classed as a reboot. It had classified this earlier as “boot/restart periodic”, but that forward-slash was doing a lot of heavy lifting in telling us these were two different things. The agent was correct: but given the information from runtime it was able to pinpoint the exact place the decision was made. We now know that this hardware is suitable for developing our new code, and what exactly to tweak during development for testing.

The agent was also able to make corrections: we see that several tasks described as “different initialization path” were in fact replaced at link time with empty stubs. This happens when you have:

__attribute__((weak)) void init_led_task(void) { };

As well as a real implementation of init_led_task that sets up a periodic task. In the initialization function this is just a plain call:

init_led_task();

The agent would need to detect both the real and stub implementation, and then it would have to inspect the link rules for the platform (correctly understanding a complicated build system along the way) to find out whether the empty stub or the real implementation was called. Again, possible, but it becomes trivial with runtime information.

Runtime context turns AI assertions into verifiable answers

  • Using Undo AI shifts the scenario from the periodic tasks being outputs from the agent to them being inputs. A full list of called functions (with counts) is provided by Undo at the start of the session, generated during the triage step. This is one of the things we mean by “runtime context”: it gives the agent a clear starting point for further analysis. Agents are great at working forwards from a known point.
  • It’s much easier for the agent to respond to “why was this task not called?” for a conditional task. A source code analysis can give you the same information, but it can’t give you the same certainty.
  • Undo gives you more information, more context – it doesn’t take away anything. The agent will combine data from source code, runtime logs, Undo-provided runtime context, and whatever else is available to come up with the answer.

What do you think? Could I have asked better questions of the agent? What dynamic paths do you have in your software that source code analysis might miss?

Send me your thoughts on the form linked below.

Send your thoughts

Stay informed. Get the latest in your inbox.