System reference · high-level architecture

PR1SM Demo Factory

One recorded sales call goes in. A working, evidence-checked product demo comes out — its own database, its own dashboard, its own AI assistant that answers questions about that client’s business.

Repository
prism-auto-demo-gen
Scope
build pipeline + demo runtime
Agent systems
2 (builder, supervisor)
Typical build
10–25 min, unattended
DEMO-BUILDER/ · platform/

Two agent systems, easy to confuse. Keep them apart.

Two agents, one factory

The builder is the agent that manufactures a demo. The supervisor is the agent that ships inside it. The builder runs for twenty minutes on a laptop and is never seen by the client; the supervisor runs in front of the client for months.

Everything below follows that split. The first half is the factory floor: how the builder plans, acts, checks its own work, and refuses to hand over a demo it could not verify. The second half is the product: the agent runtime the demo actually is, and the memory and connectors that keep it useful past the first question.

The two meet at exactly one place — integrate.py, which merges what the builder generated into a copy of the base app.

transcript.txt one client call Builder agent 7 phases, tool-driven loops until QA READY output/runs/<slug>/ the demo bundle integrate.py 8 patches + verify builds/<slug>/ the running demo platform/ the base app, never edited skills/ 7 phase guides tools 9 + subagents reads writes reads emits copied
Figure 1 — the whole pipeline. The base app is copied, never modified: a build failure can spoil one demo, never the template every future demo comes from. integrate.py reads the bundle’s QA verdict first and refuses anything marked NOT READY.
agent.py → make_agent()

deepagents · LangGraph
SqliteSaver checkpoints
reasoning effort: medium

How the builder runs

The builder is a single agent in a loop. It is given a system prompt that names seven phases, a folder of skills, nine tools, two subagents it can delegate to, and a sandboxed filesystem rooted at DEMO-BUILDER/. Nothing about the pipeline is hard-coded as a state machine — the phases live in the prompt and in the skills, and the model walks them.

What makes that trustworthy rather than hopeful is the rule the prompt opens with: never claim something works without having run the corresponding tool. Every phase ends in an artifact on disk and a tool call that proves the artifact is good.

load skill SKILL.md for phase N plan write_todos act tool or subagent observe rows, PASS/FAIL, stderr decide artifact proven? phase N+1 not proven — act again on the same phase output/runs/_checkpoints.sqlite every step persisted and keyed by thread_id — a crashed build resumes where it stopped
Figure 2 — the inner loop, run once per phase. The return edge is the whole point: a phase does not advance because the model wrote a file, it advances because a tool confirmed the file is right. Checkpoints are durable on disk, so an eleven-minute build that dies in phase 7 restarts at phase 7, not phase 1.

Human in the loop, when you want one

Running with --approve-loads registers an interrupt on the one tool that writes to the database. The graph pauses there and waits for your approval before any data lands in Postgres. Everything else runs unattended.

DEMO-BUILDER/skills/

Seven folders, one SKILL.md each. Loaded on entry to a phase, not held in the prompt.

Skills: the instructions it reads on the way in

A skill is a markdown file with a name, a one-line description, and a procedure. The builder’s system prompt names the seven phases and says read the skill before starting the phase — so the detailed instructions stay on disk until the moment they apply, instead of crowding the context from the first token. Editing a skill changes how every future demo is built; no code changes.

#SkillProducesRun by
1transcript-extractionClient profile and executive summary. Searches the web only to fill gaps the transcript left.orchestrator
2data-modelingThe star schema for this client — data_model.json, schema.sql.orchestrator
3demo-narrativeThe story bible: scale anchors taken from what the client said, client-aligned names, planted insights and their target magnitudes.orchestrator
4mock-data-generationA deterministic generator that renders the story bible into 18–24 months of CSV, loaded into Postgres.data-engineer
5dashboard-generationKPI cards, charts and detail tables — every query executed before it ships.orchestrator
6prism-integrationThe platform artifacts: supervisor prompt context, data-subagent blocks, front-end constants, demo questions.orchestrator
7testing-qaqa/qa_report.md — the verdict the whole build is judged on.qa-verifier

Phase 3 is deliberately kept by the orchestrator rather than delegated: the story bible needs the full transcript in context, and it is where the demo’s credibility is won or lost. Every number the client stated must be reproducible from the generated data; every complaint they voiced must have a signal someone can find.

DEMO-BUILDER/tools.py

Nine tools. Five of them exist only to disprove the model’s own claims.

Tools: acting, and checking

Tools split cleanly into two groups, and the second group is the larger investment. Anything the builder asserts about the demo has a tool that can contradict it.

Acting on the world

  • run_python_script — executes the data generator, iterating until its reconciliation table prints clean.
  • load_csvs_to_postgres — loads a run into its own <slug>_demo schema.
  • install_dashboard_html — places the 20 KB dashboard template, which the model is forbidden to hand-write because long writes truncate.
  • internet_search — a few targeted queries when the transcript names a tool or company it cannot place. Never overrides a stated fact.
  • read_platform_file — reads the base app, so generated artifacts match what they will be pasted into.

Checking the work

  • run_query — SQL against the loaded schema. This is the source of truth: if a planted insight is not visible in query results, the generator gets fixed, not the claim.
  • test_dashboard_config — runs every chart’s query and validates the shape it returns.
  • check_dashboard_server — confirms the preview actually serves the built dashboard.
  • validate_demo_guide — confirms the generated JavaScript constants parse and export what the app imports.

A stray character should not cost a build

The filesystem the builder writes through is wrapped so that a malformed path comes back as a failed tool call the model can read and retry, rather than an exception that unwinds the whole run. A twenty-minute build used to be lost to one stray dot in a filename. Genuine path traversal is still rejected outright, one layer earlier.

agent.py → subagents[]

data-engineer — write access
qa-verifier — read only

Delegation, and why the grader has no eraser

The builder can hand work to two subagents. Each is a fresh agent with its own context, its own system prompt, and a deliberately narrow toolset.

data-engineer owns phase 4. It gets the three tools that write and read data, and its instructions are to iterate until the numbers reconcile, then prove them with SQL and report the evidence as “client said X → data shows Y” pairs.

qa-verifier owns phase 7 and cannot fix anything. It holds four tools, all read-only. It runs the checklist, writes the report, and returns a verdict plus the exact list of failures. Grading and repairing are separated on purpose: an agent that can quietly patch what it is grading will always find its work acceptable.

skills/testing-qa/ · agent.py

Verify → route → repair → verify again. Cap: 3 rounds.

The QA cycle

Phase 7 is not a final check, it is a loop the orchestrator owns. The verifier reports; the orchestrator routes each failure to whoever can fix that class of problem; then the verifier runs again. The rule that makes it hold together: after the last repair, one more verification pass always runs. A repair that is never re-verified leaves the report describing the broken state, and the gate downstream reads the report, not the repair.

qa-verifier read-only subagent runs the full checklist qa/qa_report.md verdict of record integrate.py reads the verdict first builds/<slug>/ handed over writes READY merge NOT READY data, dates, insights data-engineer regenerates dashboard fix config, re-run both checks artifacts rewrite, then read it back re-verify cap: 3 repair rounds — a verify-only pass never counts
Figure 3 — the repair cycle. Failures are routed by class, not by whoever noticed them. Data problems go back to the engineer that owns the generator; a chart that disagrees with the KPI beside it is fixed in the config. If three rounds are genuinely exhausted, the build stops and says precisely what remains for a human to decide.

What the checklist actually tests

  • Data integrity — row counts, date ranges, referential joins.
  • Client alignment — the credibility section: do the stated numbers reproduce, and does the data look like this business rather than a round-robin fill?
  • Planted insights — is each complaint the client voiced actually discoverable, at the magnitude claimed?
  • Dashboard — every query run directly, then run again through the app’s real endpoint.
  • Demo questions — each question’s evidence SQL executed, after checking the file parses at all.
  • Bundle completeness — every handoff file present and non-truncated.
DEMO-BUILDER/integrate.py

Eight targeted patches, then a ninth pass that verifies them.

Handoff: turning a bundle into an app

The bundle is not the demo. integrate.py copies the base app, then patches nine named places in it — the backend and frontend environments, the supervisor’s prompt context, the data subagent’s schema blocks, the shared data context, the dashboard branding, the demo guide, and the two dashboard files — and finally re-reads its own work.

It patches by exact name and path, which makes those names a contract rather than a detail. Renaming one of them in the base app without updating the integrator produces a build that looks fine and is missing its client. A dry run exists precisely so that contract can be proven before a change lands.

Two guarantees are worth stating plainly: the base app is copied and never edited, and the integrator reads the QA verdict before it starts. A bundle marked NOT READY is refused.

Part II

The product it ships

Everything from here describes the demo itself — the agent runtime the client talks to, and what keeps it coherent past the first question.

backend2/app/main.py

One supervisor, four nested agents, plus plain tools and connectors.

The runtime

The generated demo is a chat application over a WebSocket. Behind it sits a supervisor agent holding a mixed toolbelt: four of its “tools” are whole agents with their own loops and their own memory, and the rest are plain calls out to systems. The supervisor does not know the difference — that is the point of the pattern — but the interface does, so the user sees “Data subagent working” rather than a raw function name.

Supervisor agent built per run, never shared gpt-5.6-terra MongoDB checkpointer every call behind a deadline — 150 s nested, 60 s plain NESTED AGENTS — each runs its own loop and keeps its own thread data_subagent SQL over the demo schema python_llm_agent charts and forecasts, in process rag_llm_agent questions over uploaded documents document_store_agent the client file store PLAIN TOOLS — one call out, no inner loop GoogleSearch live web lookup Office 365 tools mail, calendar, drafts MCP tools loaded from mcp_servers.json Postgres — <slug>_demo one schema per demo documents + vector store uploads, retrieval, saved artifacts Serper live web results Microsoft Graph + MCP servers Business Central, Outlook, JobDiva
Figure 5 — the demo’s agent runtime. The dashed line is the deadline boundary. All tool calls in one model turn run as a batch and the model only continues when every one has returned, so a single wedged call used to freeze the whole turn; now each returns a notice on expiry and the model answers from what it did collect.
session_manager.py
subagent_checkpoint.py
utils/context_middleware.py

Three kinds of memory. They solve different problems and are easy to conflate.

Memory

“Agent memory” covers three separate mechanisms here, and it is worth naming them apart.

1. Build memory — so a crash is not a restart

The builder checkpoints every step to a SQLite file keyed by thread id. Point a new process at the same thread and it picks up the recorded state. Before this was durable, the “resume” hint the builder printed was a lie: the state lived in the process that died with it.

2. Conversation memory — one session, several threads

The running demo persists conversations to MongoDB. The supervisor’s thread is the chat session itself; each nested agent gets a derived thread of its own, so the data agent’s SQL history does not leak into the document agent’s context, and neither floods the supervisor’s.

session_id one chat, one user supervisor thread_id = session_id compacts at 60k tokens or 50 messages, keeps the last 24 data_subagent thread_id = {session_id}-data compacts at 40k tokens or 30 messages, keeps the last 12 python_llm_agent thread_id = {session_id}-python compacts at 40k tokens or 30 messages, keeps the last 12 rag_llm_agent thread_id = {session_id}-rag compacts at 40k tokens or 30 messages, keeps the last 12 MongoDB checkpointer every thread, durable across restarts
Figure 4 — thread topology for one chat. Four histories, four budgets, one session. Every agent with a persistent thread needs a compaction profile; without one its history grows until the model call fails on context length.

3. Compaction — so a long conversation stays answerable

When a thread crosses its trigger, a small model folds the older messages into a summary and the recent ones are kept verbatim. Two details matter. The budgets cover the message history only — system prompts are never touched, which is why the data agent, carrying a large injected schema, has a tighter budget than the supervisor. And the thresholds are set so that every model call stays well inside the range where SQL precision holds up, rather than filling the context window because it is there.

mcp_config_loader.py
routers/auth.py

Connectors are configuration, not code. Identity is Auth0.

Connections and connectors

Outside systems reach the supervisor through three doors.

  • MCP servers are declared in a JSON file, each with its own enable flag and transport — a local process over stdio, or a remote server over streamable HTTP with credentials pulled from environment variables rather than written into the file. Their tools are appended to the supervisor’s belt at startup. Because MCP calls are asynchronous, a deadline genuinely cancels them, which is not true of the synchronous tools. Three servers live in the repo: Business Central, Outlook and JobDiva.
  • Office 365 tools attach when credentials are present, giving the supervisor mail, calendar and draft creation through Microsoft Graph.
  • Web search goes out through Serper as a single plain tool.

Identity is Auth0. A JWT is validated when the socket opens, roles are read from the Management API, and — because those lookups are slow and rate limited — both the management token and the role map are cached and invalidated when a role actually changes. The full set of granted roles is carried into the tool context, not just the first one returned, so an access check honours everything the token granted.

tools_utilities/tool_timeout.py
llm_config.py

Four rails that exist because each one failed at least once.

What keeps the loops from running away

RailSettingWhat it prevents
Nested-agent deadline150 sOne slow subagent freezing a whole turn’s tool batch.
Plain-tool deadline60 sA wedged MCP server or blocked auth prompt holding the user on a spinner.
LLM stall window120 s · 1 retryA stalled provider connection burning the SDK’s ten-minute default. It is a gap-between-chunks limit, so a long answer that keeps streaming is never cut off.
Per-run supervisorno cacheA shared executor serving one user’s access scope to everyone — the prompt carries caller-specific role framing.

One honest caveat is worth carrying: synchronous tools run in worker threads that Python cannot cancel. A timed-out sync tool keeps running in the background and its result is discarded — the user is unblocked, the work is not actually stopped.

agent.py · server.py
integrate.py · cleanup.py

Four commands, in order.

Operating it

StepCommandResult
Buildagent.py --transcript <file> --slug acme10–25 min, unattended. Writes the bundle and loads the data.
Previewserver.py --run acmeServes the generated dashboard on its own.
Integrateintegrate.py --run acmeA runnable copy of the app. Add --dry-run to prove the patch contract first.
Removecleanup.py --run acmeDeletes the run folder, the build copy and the schema. It only ever touches *_demo schemas, so it cannot reach real data.

Every build prints a per-phase timing table and writes it to build_meta.json — model, reasoning effort, seconds per phase, tool-call count — and appends the run to a history file that survives a rebuild, so builds stay comparable across models. Timings are written even when a build fails.

The bar for “done”

A demo is finished when three things are true at once: every number the client stated is reproducible from the generated data; every complaint they voiced has a signal someone can actually find, at the magnitude claimed; and the QA report says READY after a verification pass that ran after the last repair. Anything less is a bundle the integrator will refuse.