RC2026

AI Agent Case Study

·
DEMO

Supply Genie

Personal demo of an AI worker for supply-chain exceptions, with a live phone agent and human approval gates.

Artifacts

Stack

AI Agent Case StudyVoice AITool CallingHuman-in-the-LoopFailure Handling

The Product

Screenshots
Supply Genie operator workspace in a local demo run with made-up data: a shipment-delay incident whose Slack alert, supplier call and purchase-order amendment are done, with the customer email waiting for an operator to approve or reject it and the action history on the right.
Three steps done; the customer email waits for a person. (Local demo run, made-up data, external calls stubbed.)
A delay incident whose purchase-order step failed because the order number does not exist: the step shows Failed with its retry count and error, a Retry failed button sits in the context panel, and the customer email still waits for approval.
A failed step keeps its error and retry count; the operator can retry it.
A worker-absence incident marked resolved: roster updated, replacement requested from the contractor agency, and the site manager alerted, with the worker's details in the context panel.
A worker absence, resolved: roster updated, agency called, site manager alerted.

Synopsis

Supply Genie is a personal demo of an AI worker for two supply-chain exceptions, a shipment delay and a worker absence. A webhook opens an incident, a fixed playbook turns the incident into ordered actions (for a delay: a Slack alert, a one-way Twilio call that reads the new ETA to the supplier, a purchase-order amendment and a customer email), and an operator workspace shows every step. The customer email waits for an operator's approval. Operators can ask a Gemini 2.5 Flash chat about the incident or tell the chat to run a step, and a separate phone agent streams live calls into Gemini Live. The project was built while applying to HappyRobot and is a conceptual demo inspired by how HappyRobot approaches the problem, not a production system: no users, no deployment and no outcome metrics, and the repo's prompts and UI labels still carry the HappyRobot name. The demo is about the engineering around the model: retry-safe intake, approvals stored as data, versioned purchase-order writes, and failure handling that refuses to auto-retry a Slack post or a phone call when the outcome of the first attempt is unknown.

Architecture

System Layers

Operator workspace (Next.js)

Next.js 16, React 19, Tailwind 4 and shadcn/ui. The incident inbox polls every five seconds and groups incidents into needs-action, escalated and resolved; a playbook strip marks the gated step with a lock; approval cards sit in the center column; an action history shows each step's retries, errors and approval decision; and a chat panel relays questions and commands to Gemini through the API. Every status on screen comes from a database row.

Iglobe

API (FastAPI)

Python and FastAPI. The delay and absence webhooks require an Idempotency-Key header; other routes handle approvals, retries, the dead-letter list and requeue, chat, and the Twilio voice webhooks plus the media-stream WebSocket. Middleware stamps a correlation ID on every request and response and stops non-voice requests after 90 seconds (default). The readiness probe reports database health, circuit-breaker states and active calls.

IIserver

Agents (Gemini)

The operator chat calls Gemini 2.5 Flash with three function tools (incident status, shipment lookup, run a command) and loops for up to five tool rounds per message. The phone agent runs Gemini Live with native audio inside a Pipecat pipeline and has five tools, adding call-progress tracking and hang-up. Neither model talks to Slack, Twilio or Resend directly: both agents request incident actions through the same command list, executor and approval rule. After each call, Gemini 2.5 Flash turns the transcript into a structured summary that is saved with the call and posted to Slack.

IIIcpu

State (PostgreSQL and Redis)

PostgreSQL, through SQLAlchemy and Alembic, holds suppliers, shipments, purchase orders, incidents, action runs, approvals, voice sessions and transcripts. Each step's status, attempt count, request and response payloads and last error live on a single action_runs row, and in queued mode the same table is the work queue. Redis holds only short-lived call state: a checkpoint per active call, with a one-hour expiry by default, and a command list per call.

IVdatabase

Connectors and failure handling

Slack (slack-sdk), Twilio (REST calls and Media Streams) and Resend are real integrations; the purchase-order system is a PostgreSQL table with a version column, and the labor (HRIS) system is a mock. Slack, Twilio and Resend calls each run behind a per-connector timeout and circuit breaker. Failed actions retry with exponential backoff and full jitter, then move to a dead-letter list that can be requeued through the API.

Vlayers

Core Engineering

Field Notes
01.

Retry-safe intake and versioned purchase orders

The delay and absence webhooks reject a request without an Idempotency-Key header (422). A known key returns the existing incident with 200 instead of 201, so a sender that retries never opens a second incident or runs the playbook twice, for example calling the contractor agency twice for a single absence. When two requests race with the same new key, a unique constraint lets a single insert through; the losing request rolls back and returns the winning incident. Purchase-order amendments use a version compare-and-swap instead of a lock: the UPDATE bumps the version and only matches while the version is unchanged since the read, so a concurrent edit produces a conflict error rather than an overwrite.

02.

Playbooks run by plain Python

A playbook is a Python list, so a delay always becomes four action runs (Slack alert, supplier call, PO amendment, customer email) and an absence three (labor-system update, contractor call, site-manager alert in Slack). A deterministic executor walks the runs in sequence order, sends each pending action to the matching connector, saves the request and response payloads on the row, and writes every action in a separate database transaction, so a later failure cannot roll back an earlier PO change. An incident resolves once every run is completed or skipped, and escalates when any run is dead-lettered. No model decides what a playbook does next.

03.

Approval gates stored as rows

A customer-facing action, here the customer email, is created as needs_approval next to a pending row in the approvals table, under a setting that is on by default. The gate follows the action type, not a model's sense of risk, and neither agent has a tool that approves. A decision is a single UPDATE that only matches while the approval is still pending, so a double click or a racing request gets 409 instead of a second email. Approving moves the action to pending and dispatches the action; rejecting marks the action skipped. Asking either agent to email the customer produces a gated approval instead of an email.

04.

Failure handling per connector

Slack, Twilio and Resend calls each run behind a dedicated timeout (defaults: 10 s, 15 s and 10 s) and a circuit breaker that opens after five straight failures, lets calls through again after 60 seconds and closes after two successes (defaults). A failed action retries with exponential backoff and full jitter (200 ms base, 10 s cap) and is dead-lettered after the third failed attempt, with a fallback note stored on the row. Timeouts get stricter treatment: a timed-out Slack post, Twilio call or manager alert may still have gone through, and no idempotency key reaches Slack or Twilio, so the action goes straight to the dead-letter list for a person to check. Email stays retriable because every attempt sends Resend the same idempotency key. Dead-lettered actions can be listed and requeued through the API.

05.

A PostgreSQL queue, and no durable engine yet

By default, actions run inline, inside the API request that created or approved the actions. With queued mode switched on in configuration, the actions are marked queued instead, and a background worker claims the oldest queued action with SELECT … FOR UPDATE SKIP LOCKED, runs that incident's queued actions, and requeues failed actions once the backoff window has passed. A queue-status endpoint reports queue depth, retriable and dead-lettered counts, and worker health. A written evaluation in the repo compares the queue with a durable workflow engine such as Temporal and recommends keeping the queue until defined adoption gates are met; selecting the durable mode of the workflow-engine interface raises an error on purpose.

06.

Live phone agent: Twilio, Pipecat, Gemini Live

Inbound calls, and outbound calls started through the voice API, get TwiML that opens a Twilio Media Stream to a WebSocket on the backend. A Pipecat pipeline converts the 8 kHz telephone audio and streams the audio to and from Gemini Live, with Gemini's voice-activity detection tuned (high start and end sensitivity, 260 ms of silence to end a turn; defaults). When the caller talks over the agent, a custom Pipecat processor sends Twilio a clear event so buffered agent audio stops playing. The agent marks four call objectives (cause, new ETA, mitigation, risk) through a tool and closes the call after a brief closing statement once all four are confirmed; the transcript is saved and summarized into Slack. The playbook's supplier and contractor calls are different: one-way Twilio calls that read a fixed message.

07.

Operator chat with Gemini tool calling

The chat panel sends the operator's message, the conversation so far and a snapshot of the incident to Gemini 2.5 Flash with three function tools: get_incident_status, list_active_shipments and execute_command. The backend runs each tool call and returns the result to the model, for up to five rounds per message. The execute_command tool accepts only a fixed list of commands: internal steps run immediately through the same executor as the playbook and report success or the connector's error, while a request to email the customer becomes an approval card instead of an email. Status answers are grounded in the action rows, because get_incident_status returns each step's status and last error.

08.

Call control from any server

Groundwork for running several backend servers, built but never deployed to more than a single server. Each active call records the owning server's ID and a heartbeat, and checkpoints state to Redis every five seconds and on each progress or end-call tool call: objective flags, close state and the transcript tail, with an expiry. A control command (only end_call is supported) runs locally when the call lives on the receiving server; otherwise the command goes onto a per-call Redis list that the owning server polls every second. Routing fails closed: no Redis bus returns 503, no checkpoint returns 404, and a checkpoint older than 20 seconds returns 409 (timings are defaults). Kubernetes and NGINX sticky-routing templates and a multi-pod smoke-test runbook are in the repo; the smoke test has not been run.

Execution Flow

Dispatch
Execution Trace6 steps
01

Webhook opens an incident

An upstream system posts a shipment delay with an Idempotency-Key. A repeated key returns the existing incident with 200. A new key creates the incident and four action runs in one transaction, with the customer email stored as needs_approval next to a pending approval.

02

Internal steps run in order

The executor, inline by default or in the queue worker, posts the Slack alert, places the one-way supplier call and amends the purchase order with the version check. Each step is saved in a separate transaction and recorded as an OpenTelemetry span.

03

Failures stay contained

A failed step stores the error and the next retry time. Retries use jittered backoff and come from the queue worker or the Retry failed button in the workspace. A timeout on Slack or Twilio dead-letters the step at once; a third failure dead-letters the step and moves the incident to Escalated in the inbox. Dead-lettered steps are requeued through the API.

04

The gate waits for a person

The workspace marks the email step as awaiting approval and shows an approval card in the center column; the operator can ask the chat for a status summary first. Approve sends the email through Resend with the action's idempotency key; Reject marks the step skipped. A second decision on the same approval gets 409.

05

A live call, when needed

Separately from the playbook, a call into the Twilio number, or an outbound call started through the voice API, reaches the Gemini Live agent. The agent reads incident status, can request a step through the same approval rule, and leaves a structured summary in Slack after hang-up.

06

The incident resolves

Once every action is completed or skipped, the incident is marked resolved and moves to Recently resolved in the inbox. Each step's payloads, errors and approval decision stay on the action and approval rows.

Complete✓ OK
Reflections
Outcomes
  • “Keep the model out of the failure path. Gemini picks from a fixed tool list, and plain Python owns retries, breakers, timeouts and state changes. A bad model turn can request the wrong step, but cannot approve a customer email or change how failures are retried.
  • “Idempotency belongs at the edge, before any side effect. A required header plus a unique constraint stop a retried webhook from opening a second incident and calling the same contractor agency twice.
  • “An approval is a row, not a prompt. A needs_approval status plus a compare-and-swap on the decision means a double click returns 409 and the customer gets a single email; a prompt telling the model to ask first would guarantee neither outcome.
  • “A timeout means unknown, not failed. A Twilio call that timed out may still ring, so a non-idempotent connector should stop and hand the action to a person; email can retry because every attempt sends Resend the same idempotency key.
  • “Adopting a durable workflow engine is a decision with entry criteria. The repo's evaluation keeps the PostgreSQL queue until at least one adoption gate holds: workflows that regularly run past 30 minutes with timers, compensation across more than three external systems, worker restarts that keep needing manual fixes, or audits that need replayable history.
Next Steps
  • Send OpenTelemetry traces to a real backend (spans only print to the console today, and only when enabled) and rebuild the KPI view the UI dropped, on top of the existing SQL KPI endpoint.
  • Deploy behind a public wss:// host, apply the sticky-routing templates and run the multi-pod smoke test, then record voice latency with the probe scripts already in the repo.
  • Add evals for the chat and voice agents, for example whether a request maps to the right command and whether customer-facing steps always land in the approval queue.

One Last Thing

The core challenge: Supply-chain exceptions such as a late shipment or a missing worker are coordinated by hand across phone, email, Slack and order systems. Handing that work to an AI agent is only safe if a retried webhook cannot open a second incident, a timed-out call is never placed twice by an automatic retry, a purchase order is never silently overwritten, and a customer email never goes out without a person's approval.

✦Built with
intention

More from the Archive

View All