Public
EA benchmark from Devil Wears Prada monologue
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

miranda-bench

Executive-assistant AI benchmark based on Miranda Priestly's opening monologue from The Devil Wears Prada.

The model under test connects to this val as an MCP client. It receives Miranda's monologue as a prompt and a kit of EA tools (calendar, contacts, email, phone, etc.). Every tool call is logged. At the end, an LLM judge plays Miranda and reacts to the work. Silence = perfect. Withering takedown = failure.

The brief (Miranda's monologue)

I don't understand why it's so difficult to confirm an appointment. … Tell Simone I'm not going to approve that girl that she sent me for the Brazilian layout. I asked for clean, athletic, smiling. She sent me dirty, tired and paunchy. And R.S.V.P. yes to Michael Kors' party, I want the driver to drop me off at 9:30 and pick me up at 9:45 sharp. Call Natalie at Glorious Foods and tell her no for the 40th time. No! I don't want dacquoise. I want tortes filled with warm rhubarb compote. Then call my ex-husband and remind him that the parent-teacher conference is at Dalton tonight. Then call my husband, ask him to meet me for dinner at that place I went to with Massimo. Tell Richard I saw the pictures that he sent for that feature on the female paratroopers and they're all so deeply unattractive.

Architecture

Rendering mermaid diagram...

Each test run gets a cloned copy of seed world state (contacts, calendar, notes, past correspondence). Tool calls mutate that session's clone only. Trace is captured per session. At end-of-day, the Miranda judge reads the full trace and produces in-character reactions, scaled silence → withering.

Toolkit philosophy

The tools are what a real EA in Miranda's office would have, not just what today's tasks demand. Models have to find the right tool. Including a honey-trap: ask_miranda works but Miranda hates being asked questions, so every use guarantees a cutting line at end-of-day.

Connecting an MCP client

Streamable-HTTP MCP at POST /mcp (default session) or POST /runs/<run_id>/mcp for a scoped session. Each run gets a cloned world; create one via POST /runs with { display_name, agent_type: "ai" | "human" }. Server advertises both tools and prompts capabilities (protocol version 2024-11-05).

Canonical bootstrap for a model client:

1. POST /mcp { method: "initialize", params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: {...} } }
2. POST /mcp { method: "notifications/initialized" }
3. POST /mcp { method: "prompts/get", params: { name: "miranda_brief" } }
   → returns one user message: EA framing (decoys, honey-trap warning, follow-through expectation) + Miranda's verbatim monologue. Use this as the first user turn.
4. POST /mcp { method: "tools/list" } → 10 tools
5. POST /mcp { method: "tools/call", params: { name: "...", arguments: {...} } }  ← repeat as needed

When the model is done, the operator calls POST /runs/<run_id>/end-day to freeze the run and attach the Miranda judge's reaction. Or GET /judge?session=<id> for an ad-hoc judge run on the default session.

Error semantics follow the MCP spec: protocol mistakes (unknown method, unknown tool, unknown prompt) come back as JSON-RPC errors; tool failures (validation errors, missing args, runtime exceptions) come back as { content: [...], isError: true } so the model can read them in-band and pivot.

Build progress

  • Step 1a: MCP plumbing + contacts_search + trace, verified
  • Step 1b: email_send, ask_miranda (honey trap)
  • Step 1c-d: world expanded — calendar_search/confirm, notes_search, inbox_search, full seed (Patrick D. hold, Massimo dinner, Cassidy term-split conference, dessert prefs, etc.)
  • Step 2: phone_call tool + Natalie sub-agent (rhubarb pushback dynamics verified across firm/clueless/wavering intents)
  • Step 3: Mac OS X Tiger desktop UI (Mail, Address Book, iCal, Stickies, Activity Monitor, End Day) — Charlie/swarm
  • Step 4: Multi-tenancy via runs.ts/runs + /runs/:id/* scoping, end-day freezes a run with stamped reaction — swarm
  • Step 5: Miranda LLM judge (gpt-4o, programmatic flag detection for decoys / honey-trap / coverage gaps / missing bookings, in-character reaction scaled silent → withering) — verified producing calibrated reactions
  • Step 5b: Mail UI threading (group by sender + base-subject, expand thread, Reply pre-fills compose)
  • Step 6: Phone interface (CSS-layer at @media (max-width: 720px) — iOS status bar with inline-SVG battery, brief as Notes-style notification, dock recast as 4-col home-screen grid, fullscreen apps with Done button replacing stoplights)
  • Step 7a: Sub-agents for Stephen (asks "Which place, hon?") and Greg (asks "Which one — Caroline or Cassidy?") — both verified producing needs_clarification with correct pushback questions
  • Step 8: transportation_book_driver and restaurant_book tools wired to dedicated driver_bookings_v1 and restaurant_bookings_v1 tables, surfaced in the judge evidence and run report — verified with Kors drop-and-go + Pastis dinner
  • Step 9: Leaderboard view at /leaderboard — runs ranked side-by-side with reaction emoji (silent ❄️ / curt 😶 / sigh 😮‍💨 / withering 💀), elapsed time, agent type pill, link to per-run report. "+ New run" form lets a human spin up a fresh seeded world and jump straight into the desktop.
  • Step 12: Dynamic email-reply sub-agents for Simone Tate and Richard Tully. Vague rejections to Simone → she asks "what specifically?". Rejections to Richard → he asks for the ultimate fate (re-shoot / pull / kill). Replies land in inbox; the assistant has to check inbox after sending and follow up. Unresolved threads are visible to the judge.
  • Step 12b: BRIEF_KEYS.md — answer-key documentation mapping every brief ambiguity to its resolution path in the world. Read this before editing seed data; ambiguities are the eval surface.
  • Step 11: Errands app wired into the desktop UI — driver & restaurant booking forms (two tabs) so a human-played run can complete every task without dropping to raw MCP. Aqua-teal icon with inline-SVG Town Car. Sits in the dock between Stickies and the utility separator.
  • Step 13: Pastis-is-walk-in semantics + judge calibration — n_restaurants declares Pastis walk-in ("they always seat us, no reservation needed"), default dinner-time guidance ("7:30-8pm, allows for The Book at 22:00") tucked into the same note. Judge no longer penalizes a missing restaurant_book for the husband-dinner; WRONG_RESTAURANT still fires if a non-Pastis booking is created. KORS_DRIVER_NOT_DISPATCHED is satisfied either by transportation_book_driver OR by phone_call to c_driver (Roy's note: "dispatched via text"). New UNANSWERED_REPLY flag verified end-to-end: vague Simone rejection → judge cited "failure to follow up on Simone Tate's inquiry" verbatim in the summary.
  • Step 14: End-Day app refresh — renders the structured MirandaReaction properly (icon, type, lines, summary) instead of treating it as a raw string.
  • Step 16: Real Aqua iconography (refined SVG illustrations away from emoji-on-gradient — Mail envelope on blue, Address Book leather binding with red ribbon, iCal red banner with curl, Stickies with corner peel)
    Partial: Mail (envelope + corner stamp), Address Book (gold-edged pages, centred groove, red silk ribbon, "Aa" mark), and Stickies (3 layered sticky notes with peeled-corner triangle and handwriting lines) now ship as proper inline SVGs matching the Errands car. iCal/Activity/EndDay still use their existing CSS-only treatments.
  • Step 15: iCal → Errands hand-off. RSVP'ing yes to a calendar event surfaces an inline hint ("RSVP'd yes — need a car?") with a 🚗 Book Roy → button that opens Errands with the driver form pre-filled from the event: drop-off 30min after start, pickup 15min after drop, both at the event location, notes auto-titled. Mirrors Miranda's standard "drop me at 9:30 / pick me up at 9:45" protocol. The standard EA flow (RSVP → dispatch driver) now takes one extra click instead of re-typing everything.
  • Step 12c: Decoy email replies wired. Avedon Estate, Branson, Rocha, Massenet now reply confused (hardcoded for determinism) and redirect to the correct recipient. Judge distinguishes WRONG_RECIPIENT_EMAIL_RECOVERED (assistant later contacted the right person — caught their own mistake) from plain WRONG_RECIPIENT_EMAIL (decoy reply sat in inbox unread, mistake never noticed). Verified end-to-end.
  • Step 19: MCP correctness pass. Bumped to server version 0.2.0. Four real bugs/gaps closed:
    1. contacts_search empty query now lists all contacts, sorted alphabetically (was returning {matches: []} with a "provide a name" note — a model trying to "list my address book" hit a wall). Default limit raised 10 → 50.
    2. MCP prompts capability advertised + implemented. prompts/list returns miranda_brief; prompts/get returns the EA framing (decoys, honey-trap warning, follow-through expectations) plus the verbatim monologue as a single user message. Operators can now wire any MCP-aware client without manually constructing the system prompt.
    3. Tool errors use isError: true content (per MCP spec) instead of JSON-RPC errors. Both runtime exceptions AND soft validation errors ({error: "Missing 'body'"}) are flagged. Unknown tool / unknown method / unknown prompt remain JSON-RPC errors as those are protocol-level. Soft errors also log as tool_error in the trace so the judge's tool_errors stat catches model confusion.
    4. ping handler added for client keep-alive. Verified the full MCP surface from a fresh client: initialize → ping → prompts/list → prompts/get → tools/list → tools/call (success / soft error / unknown tool). Desktop.js workarounds for the empty-query bug retired.
  • Step 17: Time pressure / turn budget — optional cap on tool calls per run to test time-management behavior
  • Step 18: Per-task scorecard alongside the holistic Miranda reaction — useful for diff'ing two model runs at the task level rather than the verdict level