How it works
Architecture & guardrails
FleetAlert AI investigates fleet-maintenance alerts end to end: Claude reasons over telemetry and a knowledge base, proposes a fix, and then waits — structurally, not just by convention — for a human to confirm before anything executes.
Try the live demo or read the source and full decision record.
The investigation loop
Every alert runs through Step Functions, not client-side logic. RAG happens inside the Agent Loop itself — one of the model’s own tools queries the knowledge base mid-loop, rather than a separate retriever component sitting in front of it.
The five-minute version
Confirmation is structurally unskippable, not just prompted
The agent loop can only ever end in “awaiting confirmation” or “routed to support” — it never executes anything itself. Step Functions pauses the whole execution on a task-token callback (ADR-0001) that only a human confirm/reject action can resolve. There is no code path from the model’s own reasoning straight to a running fix.
A fixed whitelist, not model judgment, decides what can run
Three fix types — restart_sensor, schedule_service_visit, send_diagnostic_reset — are the entire set of actions the agent is allowed to execute (ADR-0002). Anything else it proposes is automatically routed to a human. The check runs twice: once when the fix is proposed, again immediately before ExecuteFix runs, as defense in depth against the first check ever being bypassed.
Rejecting a fix triggers another RAG pass, not a dead end
A rejected proposal used to just end the run. Now it loops back into the same Agent Loop Lambda with the rejected fix_id named in the model’s own prompt, so it searches the knowledge base for a genuinely different angle instead of repeating itself — enforced in code, not just prompted, since the finalize step refuses a repeated fix_id regardless of what the model does. Capped at one retry for this demo before it gives up and routes to support.
No free text ever reaches the model
Visitors pick from five pre-seeded alert scenarios; nothing they type reaches Claude (ADR-0003). This is a public demo making real, paid API calls with no login — that constraint caps the worst-case cost and removes prompt injection from strangers as a category of problem, not something to detect and block.
What's deferred is written down, not just skipped
Backpressure between an alert feed and the investigation queue is fully designed — SQS, a dead-letter queue, a concurrency-capped dispatcher — but not built, because there's no real alert traffic yet to design it against (ADR-0006). Same treatment for CORS before the frontend's domain existed (ADR-0007) and for Claude API-level retries, since the Step Functions retry already makes failures non-silent (ADR-0008).
A production bug that only showed up live
Rejecting a fix would flip the alert's status and then just hang forever. The cause: a Step Functions Catch clause's default behavior replaces the entire state input with its own error object, silently wiping the alert_id before the retry Lambda ever saw it — six retries, no error surfaced, nothing written back to the database. Fixed with an explicit ResultPath — the kind of failure mode that only shows up once guardrails are actually exercised end to end, not in a unit test.
The model’s tool output is untrusted input
Claude occasionally called the propose_fix tool without fields its own schema marks as required — once crashing the run with a KeyError, once storing a half-empty proposal that showed up in the UI as a bare title and two buttons. Every tool call is now validated, and a malformed one goes back to the model as an error to correct, inside the same bounded loop, instead of taking the investigation down or reaching a person half-finished.
Key decisions, from the record
By the numbers
Built with an AI agent
Every guardrail, deferral, and fix lives in a numbered decision record — an ADR-first workflow run with Claude Code as a pair. The agent handled breadth: the Lambda handlers, the Step Functions definition, the Terraform for all of it, CI/CD via GitHub OIDC, and diagnosing a production bug from raw AWS error output pasted back into the chat.
The judgment calls stayed with a person — where the whitelist boundary sits, what a public demo is and isn’t allowed to do, and how many times a rejected fix gets to try again before a human takes over. The result is a working guardrailed agent system, with the reasoning behind every constraint written down next to the code that enforces it.