Inspiration

A lost shooting day on a mid-budget feature costs forty to eighty thousand dollars, and what makes it expensive is rarely the day — it is what the day was holding up. A blocked courtyard costs three scenes plus the VFX plates they capture, which a vendor has locked a delivery date around. The first AD works this out on a whiteboard at 19:00 the night before, from memory, while their phone rings.

The obvious build is a chatbot answering "what should we do?". That is the wrong product, and dangerous: a model that invents a schedule and a cost reads authoritative and cannot be checked. Nobody reschedules a 34-person crew on that.

So we built the opposite. Gemini interprets. Software decides. And the software is allowed to say no — including to its own plans.

What it does

PRI is a stateful digital twin of a production. When reality changes it:

  1. Reads the disruption off a Confluent topic and computes blast radius with a NetworkX dependency graph.
  2. Generates options from four enumerated strategy families — defer, swap, relocate, compress.
  3. Validates each one against ten deterministic rules: crew turnaround, daily hours, cast availability, permits, rental windows, prerequisite ordering, environment matching.
  4. Rejects its own plans, and repairs them. In the demo a candidate collapses crew turnaround to nine hours against a ten-hour minimum. The loop reads the rule that rejected it, computes a legal call time from the minimum plus a buffer, checks the shortened day still fits the location's permit, and emits a repaired plan.
  5. Ranks survivors on a Pareto frontier over delay, cost and risk, so a producer sees trade-offs rather than one answer.
  6. Refuses to execute without approval — seven guards in fixed order, each audited before the next runs.
  7. Verifies what it wrote with six checks running inside the write transaction; a failure aborts it, so the version was never written.
  8. Regenerates the call sheets, stamped with state version and digest.

You can also type a disruption in plain English. It is classified, published to Kafka, consumed and replanned through the identical pipeline — and if you name something that is not in the loaded production, it declines and shows you what does exist. The planner selects from the production; it does not invent entities.

How we built it

PRI architecture

Green computes. Blue explains. Red refuses. The same diagram renders live on GitHub.

The design thesis is the split, and it is enforced rather than documented:

  • The agent's only planning input is strategy_hints: list[str], matched against four constants. It cannot author a move or return a figure.
  • No numeric literal exists in the scoring arithmetic — every rate and weight comes from config/scoring.yaml, auditable in one file.
  • A test AST-walks every engine module and asserts no AI SDK import reaches the code producing numbers. Another re-runs the planner independently and asserts every score in the agent's tool payload matches exactly.

Stack. Python 3.12, FastAPI, frozen Pydantic v2, async SQLAlchemy over PostgreSQL with an append-only state_versions table, NetworkX, confluent-kafka, native google-adk with Gemini on Vertex AI, ReportLab, and a Next.js 15 console using @xyflow/react — on Cloud Run, with Cloud SQL, Secret Manager and Cloud Storage.

IBM Bob built it from a prompt pack of 27 prompts, 22 of which produced code — archived in ibm/bob/prompts/ with an artifact per session in ibm/bob/artifacts/. One prompt, one session, one commit: the typed domain model and immutable apply(); persistence with optimistic concurrency and event idempotency; the NetworkX impact engine; the ten constraint rules and their violation strings; the strategy families, scoring model and Pareto frontier; the replanning loop and its repair path; the verification engine and seven-guard transition service; the FastAPI surface and SSE stream; the Confluent producer and consumer; the ADK agent; and the console.

Challenges we ran into

Two of our own specs contradicted each other, and the tests caught it. We tightened the prerequisite rule to require a strictly earlier date — correct, because a dependency surviving only through shot-list order is not one you can rely on. That made our fixture illegal and made deferral spend both reserve days to recover one. Fixing it meant changing the fixture, not weakening the rule.

A disruption's claim was never written into the state being validated. For an "actor unavailable" event, the relocate family produced a plan that moved the blocked scenes to a different location and left them on the day the actor was away — and it validated, because the cast-availability rule had nothing to check against. The fix was not a new rule but a projection: the event's window becomes a fact the existing validator already knows how to read.

Rollback on an append-only table. Verification must run after the write to mean anything, but there is no DELETE path — so the checks run inside the insert's transaction, and a failure aborts it.

Pricing honesty. Our first cost model made relocating a VFX plate nearly free, so "shoot it somewhere else" dominated every axis. It should not: a plate against the wrong background is the one failure discovered after wrap. We repriced it in config, and documented why.

Accomplishments that we're proud of

The rejection is real and on screen. C001: observed 9.0h, required >= 10.0h travels from the validator to the browser unreformatted, because the narration reads it aloud and a UI that turns "9.0h" into "9 hours" makes the narrator wrong.

It refuses input it cannot ground. Type a location that is not in the production and it declines with the alternatives that do exist. Adversarial tests prove the boundary holds even when the model returns a plausible id with full confidence.

The loop runs in about 1.3 seconds and consecutive runs are identical, asserted by a test. 771 tests pass, mypy --strict is clean across 64 modules, and the assertions the video narrates are tests — if the video is wrong, CI is red.

What we learned

Containment is a design feature. Deciding early that the LLM could only pass one of four strings made every later decision easier, and made the system explainable to people who distrust language models — on a film set, everyone.

Writing the failure strings before the rules was the right order: every rule then had to say what it saw and what it wanted, in a sentence a producer accepts.

A rejected plan is worth more on screen than a hidden one. The visible failure is what makes the valid options believable.

And rehearsing the demo found two bugs no test could: every screen except one read a hard-coded production id, and the demo button published an event naming a location the uploaded film did not have. Both failed silently — correct-looking output about the wrong thing.

What's next for PRI — Production Resilience Intelligence

Seven pieces of work, each naming the file it would change, in docs/future.md:

A second unit — the rules already refuse to double-book, but no family generates a plan that uses one. Costs that reconcile against a real production ledger. The three disruption types that produce valid plans nobody has checked for usefulness, which is why typed input refuses them. Redis behind the stream broker, so the single-instance pin can be lifted. An identity provider, so the fourth of seven guards stops checking a name against a list. Call sheets that reach the crew — durable already, undelivered still. And another domain: swap the constraint pack, the PDF renderer and the importer schema, and field service dispatch is the cheapest honest test of the claim that this is a replanning engine, not a film tool.

What we would not build: a chatbot that answers "what should we do?", and a model that authors moves.

Built With

Share this project:

Updates

Submission history