cd /news/artificial-intelligence/building-an-ai-forensic-investigator… Β· home β€Ί topics β€Ί artificial-intelligence β€Ί article
[ARTICLE Β· art-115046] src=dev.to β†— pub= topic=artificial-intelligence verified=true sentiment=Β· neutral

Building an AI Forensic Investigator for Vehicle Failures

A developer built FaultTrace, an autonomous vehicle-forensics agent for the TrueForge Agent Harness Hackathon, designed to investigate vehicle failures by gathering evidence, testing hypotheses, and running Bayesian analysis. The agent stops for human approval before physical actions, and the project aims to generalize to other safety-sensitive systems.

read13 min views1 publishedAug 29, 2026

Built for the TrueForge Agent Harness Hackathon (Aug 24–30, 2026).

"Something expensive broke. Figure out why and prove it. Ask a human before you touch anything."

That was basically the idea behind FaultTrace.

I wanted to build something that felt more like an actual investigation than another chatbot with a few tools attached. The result is an autonomous vehicle-forensics agent that can gather evidence, test competing explanations, run actual analysis, and stop when it reaches a physical-world action that needs a human.

And honestly, the interesting part wasn't getting the first version working.

It was getting the whole thing to keep working reliably.

This is how I built it, and what went wrong along the way.

A car throws:

P0171 β€” System Too Lean

A chatbot can explain what P0171 means in a few seconds.

But that's not really the hard part.

A technician still has to figure out why the car thinks it's running lean. It could be a vacuum leak, a dirty MAF sensor, a fuel-delivery problem, or even an O2 sensor that's giving misleading information.

So the real problem isn't:

"What does P0171 mean?"

It's:

"Which of several possible causes actually explains the evidence?"

That's an investigation.

That distinction is what led me to build FaultTrace.

Given a vehicle failure event, FaultTrace:

For the hackathon, I kept the scope deliberately concrete: vehicle diagnostic forensics.

The hero scenario is a cracked brake-booster vacuum hose on a 2003 Honda Accord, resulting in P0171

  • P0300

.

It's a small enough problem to demonstrate end-to-end, but complicated enough to make the agent actually investigate rather than just look up a DTC.

The vehicle domain is the implemented MVP. The underlying investigation pattern is intended to generalize to other safety-sensitive physical systems later.

Here's the high-level architecture:

flowchart TB

    U["User / Technician"] --> AG

    subgraph TF["TrueForge Harness"]

        direction TB

        AG["Investigator Agent<br/>(faulttrace-investigator)"]

        SUB["Dynamic Subagents<br/>(per-hypothesis fan-out)"]

        SBX["Harness sandbox<br/>(optional agent-generated checks)"]

        RANK["Bayesian ranking<br/>prior Γ— likelihood β†’ posterior"]

        SES["Persistent session"]

    end

    subgraph MCP["faulttrace-vehicle MCP server"]

        R1["get_dtcs Β· get_freeze_frame"]

        R2["get_sensor_log Β· get_compact_telemetry"]

        R3["lookup_dtc_knowledge Β· get_vehicle_info"]

        RA["run_analysis"]

        G2["request_measurement β€” Tier 2"]

        G3["clear_codes Β· order_part β€” Tier 3"]

    end

    AG --> R1
    AG --> R2
    AG --> R3
    AG --> SES

    AG -- "hypothesis fan-out" --> SUB
    SUB -- "supporting / contradictory evidence" --> AG

    AG -- "run analysis" --> RA
    RA --> FIXED["fixed analyze.py (server-side, deterministic)"]
    FIXED --> RANK
    RANK --> AG

    SUB -- "optional custom checks" --> SBX

    AG -- "propose physical action" --> AP["Human approval gate"]
    AP -- "approved β†’ invoke" --> G2
    AP -- "approved β†’ invoke" --> G3
    AP -- "rejected β†’ cancel" --> XL["no tool call"]

The important thing here isn't the number of boxes.

It's the loop.

Failure event
(DTC + freeze-frame + sensor conditions)
        ↓
Observe
(read-only evidence via MCP)
        ↓
Form competing root-cause hypotheses
(each with a predicted signature)
        ↓
Run computed analysis
(real computation, not LLM text math)
        ↓
Evaluate supporting vs contradictory evidence
        ↓
Bayesian update
prior Γ— likelihood β†’ posterior differential
        ↓
Identify remaining uncertainty
        ↓
Choose the next diagnostic
(expected information gain + cost)
        ↓
STOP for human approval
before Tier 2 / Tier 3 actions
        ↓
New evidence arrives
        ↓
Continue investigation
        ↓
Defensible root-cause conclusion

That is the part I wanted to get right.

The model isn't supposed to just produce a diagnosis and call it a day. It needs to figure out what it knows, what it doesn't know, and what it should do next.

This was one of the design questions I kept coming back to.

If I could replace the whole system with:

"Paste your DTC into ChatGPT"

then I hadn't really built an agent.

FaultTrace needs to:

So the distinction is pretty simple:

A chatbot gives you an answer. FaultTrace runs an investigation.

This was important to me.

I didn't want to build a normal chatbot and then bolt TrueForge onto it just so I could say I used the sponsor's technology.

The harness is actually doing a lot of the work.

FaultTrace runs as a TrueForge agent, defined by a manifest containing the model, instructions, and MCP configuration.

The agent talks to a real vehicle MCP server over HTTP.

The tools aren't simulated function descriptions sitting inside the prompt. The agent actually reaches the server and gets data back.

FaultTrace can fan out the investigation by hypothesis.

For example:

                    FaultTrace
                        β”‚
              Bayesian differential
                        β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          ↓             ↓             ↓
     Vacuum leak     Misfire /      Sensor
     investigator    ignition     plausibility
                     investigator   investigator

Each subagent gets its own thread and sandbox and reports back supporting and contradictory evidence.

The model can propose analysis, but the important computation happens in code.

The fixed analyze.py

library performs the deterministic diagnostic calculations, including the Bayesian differential and expected information gain.

An investigation isn't just one request/response.

The TrueForge session can for an approval, reconnect, and continue the same investigation. In the demo, the investigation actually resumes mid-flow.

This is probably my favorite part.

If the agent decides it needs a physical measurement or wants to clear codes/order a part, the harness s the action and puts the decision in front of a human.

Nothing executes until it's approved.

The vehicle MCP server currently exposes 13 tools across the investigation and safety workflow.

Some of the important ones are:

Tool What it does
list_vehicles
Discover available vehicles
get_vehicle_info
Retrieve vehicle metadata
get_dtcs
Retrieve diagnostic trouble codes
get_freeze_frame
Retrieve the fault-state snapshot
get_pid_list
Discover available telemetry PIDs
get_compact_telemetry
Retrieve bounded current telemetry
get_sensor_log
Retrieve historical sensor telemetry
lookup_dtc_knowledge
Retrieve scenario-specific diagnostic knowledge
run_analysis
Run the deterministic computed differential
request_measurement
Request an additional diagnostic measurement
clear_codes
Clear diagnostic codes
order_part
Request a replacement part

The last two are intentionally gated.

That distinction matters because the agent can be autonomous without being allowed to do whatever it wants.

One thing I didn't want was a model looking at a bunch of numbers and then casually saying:

"I'm 91% confident this is a vacuum leak."

That's not very convincing.

So FaultTrace has a deterministic analysis layer.

The model supplies the investigation context, but the actual analysis code calculates the differential.

Conceptually:

P(hypothesis | evidence)
        ∝
P(evidence | hypothesis) Γ— P(hypothesis)

The analyzer takes scenario-specific priors and telemetry-derived likelihoods, performs the Bayesian update, and returns a normalized posterior ranking.

It also calculates expected information gain for the available diagnostic tests.

That gives the agent something more useful than "try another test":

Current uncertainty
        ↓
Evaluate available tests
        ↓
Calculate expected information gain
        ↓
Consider test cost
        ↓
Select the most useful next test

The result is reproducible because the computation is deterministic and seeded.

That separation is important to the architecture:

The LLM decides what to investigate. The analysis code calculates the numbers.

Every hypothesis isn't just a label.

It comes with a prediction:

"If this hypothesis is actually true, what should I see in the data?"

For example, a vacuum leak should produce a different pattern from a MAF fault or an ignition problem.

That gives the agent something concrete to test.

Instead of:

Hypothesis: Vacuum leak

we have:

Hypothesis:
Vacuum leak

Predicted signature:
- elevated fuel trims
- stronger effect at idle
- abnormal airflow relationship
- correlation with misfire behavior

The analysis then checks the telemetry against those signatures.

This also makes the final explanation much more useful because we can show both:

why a hypothesis fits

and

why another hypothesis doesn't.

I wanted the agent to actively look for evidence against its own hypotheses too.

So a final differential isn't just:

Vacuum leak
βœ“ Fuel trims support this
βœ“ MAF relationship supports this

It should look more like:

VACUUM LEAK

Supporting evidence
βœ“ Positive fuel trim at idle
βœ“ Airflow relationship matches predicted behavior
βœ“ Misfire pattern is consistent

Contradictory evidence
⚠ Idle instability is weaker than expected

Missing evidence
? Fuel pressure under load

And for another hypothesis:

MAF FAULT

Supporting evidence
βœ“ Some airflow irregularity

Contradictory evidence
βœ• Fuel-trim behavior is more consistent with
  unmetered air

Missing evidence
? Independent airflow measurement

That "why not?" reasoning is a big part of making the result feel forensic rather than classificatory.

This is another part I really wanted to avoid making into a hard-coded flowchart.

Suppose the agent has narrowed the problem down to two plausible causes:

1. Vacuum leak
2. Weak fuel delivery

The agent shouldn't just say:

"More data is needed."

It should ask:

"What measurement would actually separate these two explanations?"

That's where expected information gain comes in.

The analysis evaluates the available tests and returns something like:

Recommended test:
fuel_pressure_under_load

Expected information gain:
X.XX bits

Cost:
Low

Reason:
The result is expected to distinguish the two leading hypotheses.

And then the agent stops.

It doesn't execute the physical test automatically.

This is deliberately simple.

The agent can do these autonomously:

Human approval required:

Human approval required:

The rule is:

Investigate freely. Act carefully.

There are two layers of protection here. TrueForge provides the actual approval gate, and the MCP server independently refuses gated calls that don't contain the required approval.

So even if something goes wrong in the agent layer, the server has another line of defense.

This was probably the biggest practical lesson I got from the build.

I initially assumed that if a model was good enough at reasoning, I could just swap it into the same agent and everything would behave roughly the same.

Nope.

The behavior around tools and subagents can be dramatically different.

I initially used:

openrouter/z-ai-glm-5.3-flash

because it was cheap and fast.

It did something interesting: it actually spawned real create_sub_agent

threads.

I could see three separate thread.created

events with different thread IDs.

So, great?

Not quite.

The subagents then hit a wall because the harness's local sandbox is macOS/Linux only, while I was running the project on Windows. They ended up trying to execute their Python in a cloud sandbox and timing out.

So I had:

beautiful fan-out
       ↓
three real subagents
       ↓
sandbox timeout
       ↓
zero useful results

That wasn't exactly the demo I wanted.

I switched the primary model to Gemini 2.5 Flash.

Now I had the opposite problem.

Gemini would talk about subagents without actually creating them.

I'd see things like:

Sub-agent: investigating vacuum leak...
Sub-agent: checking ignition...

but there were no real thread.created

events.

It was essentially role-playing the delegation.

That's a surprisingly important distinction when you're building an agent system.

I eventually had to make delegation explicit and give the subagents very concrete instructions.

Things like:

One small example caused a ridiculous amount of pain:

rpm

was the actual PID.

The subagent would sometimes assume:

engine_rpm

and the whole analysis would fail.

Another lesson: when running Python through the shell, I had much better results writing it to a file and executing the file than trying to pipe complex Python through echo

.

Sometimes the "AI problem" is just shell quoting.

The other thing that bit me was the approval flow.

I needed the model to produce the actual gated tool call so TrueForge could surface the approval UI.

Instead, Gemini would sometimes do this:

The next step would be to request
a fuel-pressure measurement.

Would you like me to proceed?

Looks reasonable to a human.

But it completely bypasses the actual approval mechanism.

There was no approval button because there was no tool call.

So I added an explicit rule:

If the agent has decided on a gated action, the turn must end with the gated tool call rather than a prose question.

That was one of those tiny prompt changes that made a huge difference.

I also ran Qodo code review on the project's pull requests.

It wasn't just a checkbox for the hackathon. A few findings actually changed the implementation.

Qodo flagged that run_analysis

could be treated as one source of posterior probabilities while the orchestrator had already derived another view from subagent evidence.

That was a dangerous design.

I changed the architecture so:

run_analysis

is the single authoritative source of posterior probabilities.

Subagents contribute evidence and narrative analysis, but they don't modify the posterior.

Qodo also caught a mismatch where a tool description exposed scenario information that the response itself intentionally kept hidden.

I aligned the MCP contract with the actual allow-listed response.

This one was particularly important.

A subagent recipe had accidentally hard-coded Scenario A's VIN.

That meant Scenario B or C could have caused the subagent to investigate the wrong vehicle.

Qodo caught it.

The fix was simple: every subagent now receives and reuses the VIN from the actual failure event.

Qodo also suggested converting a smoke test to CommonJS.

I pushed back on that one.

The repository uses ESM with "type": "module"

, and the actual tests follow that convention. So I dismissed the suggestion with the reasoning recorded in the PR.

That's a useful lesson too:

Code review tools are extremely useful, but they aren't infallible. You still need to understand the code you're reviewing.

It's the fact that FaultTrace knows when to stop.

The agent can investigate a problem for as long as the work is read-only.

But eventually it might conclude:

The next useful step is a physical measurement.

At that point:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         HUMAN APPROVAL REQUIRED             β”‚
β”‚                                             β”‚
β”‚  Request fuel-pressure measurement?         β”‚
β”‚                                             β”‚
β”‚       [ Approve ]       [ Reject ]          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The agent waits.

If the human rejects it, nothing happens.

If the human approves it, the action executes and the same investigation can continue with the new evidence.

That's the boundary I wanted:

Autonomous investigation. Human-controlled action.

A hackathon project should be honest about this.

Scenarios B (dirty MAF) and C (stuck O2) exist for regression coverage.

Scenario A (vacuum leak) is the hero.

The broader architecture could eventually apply to industrial machinery, robotics, energy systems, and other physical systems, but those are future applications, not claims about what this MVP already implements.

The basic setup is:

npm install
npm start

cd mcp-server
npm install
npm run start:http

The TrueForge harness runs on:

http://localhost:8790

The repository contains the remaining configuration and environment setup required to run the demo.

I've kept the main demo focused on one investigation rather than trying to show every feature.

The flow is:

DTC event
   ↓
MCP evidence collection
   ↓
Competing hypotheses
   ↓
Dynamic subagents
   ↓
Sandbox analysis
   ↓
Bayesian differential
   ↓
Expected information gain
   ↓
Recommended diagnostic
   ↓
Human approval
   ↓
Investigation resumes
   ↓
Root-cause report

If I had to summarize the whole project in one sentence:

The hard part of an agent isn't getting a model to reason β€” it's making the entire loop reliable.

Getting an LLM to say:

"I think this is a vacuum leak"

is easy.

Getting it to:

is a very different problem.

That's what made FaultTrace interesting to build.

And that's probably the biggest thing I took away from the hackathon:

The model is only one component of an agent. The orchestration around it is where the real engineering starts.

Built for the TrueForge Agent Harness Hackathon. AI coding assistants were used during development, and the code was reviewed throughout the project, including with Qodo.

── more in #artificial-intelligence 4 stories Β· sorted by recency
── more on @faulttrace 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/building-an-ai-foren…] indexed:0 read:13min 2026-08-29 Β· β€”