Agents & MCP
Every agent stack in the wild puts the model between perceive and act: one inference per UI action. Squiggle inverts it. The model compiles one program, a deterministic runtime executes it, and the model is woken only at genuine decision points.
Why the loop is the product
A conventional agent driving an app does this: screenshot, infer, tap, screenshot, infer, tap. Every action costs an inference, every screenshot costs thousands of tokens, and any of them can hallucinate a button that is not there. Twenty steps is twenty chances to go wrong and twenty times the bill.
Squiggle makes the model a compiler. It emits one goal-conditioned intent program — a plan with branches, declared escalation points, and a goal that gets verified afterwards — and the runtime executes it against the live semantic graph. Turns become O(escalations) rather than O(actions).
intent: check out with a saved cardgoal: route: OrderConfirmation visible: { text: "Order placed" }plan: - tap: { testID: cart } - scrollUntilVisible: { element: { testID: checkout }, direction: down } - tap: { testID: checkout } - branch: if: { visible: { text: "Add a card" } } then: - escalate: { reason: "no saved card on this account", want: decision } else: - tap: { testID: pay-now } - waitFor: { route: OrderConfirmation, timeout: auto }On the reference journey, run from a dirty app, that is 3/3 passes at zero model turns, and 20/20 on repeat. The model was called once, to write the program.
What happens before anything is tapped
A program is gated offline before it is allowed to touch the device, through a pyramid that degrades honestly rather than silently:
- Static
- Does every selector name something the route graph has actually seen? A program aiming at a screen that does not exist is stopped here, with zero live actions.
- Replay
- Do the program’s assertions hold against the newest relevant recording? This checks the plan against territory we have already been through.
- No coverage
- A program with no covering recording degrades to static-only with a note saying so. It never passes silently — an ungated run that claims to be gated is worse than no gate.
alternatives turns this into best-of-N: emit several candidate programs in the same turn, let the gates score them all, run the winner, and keep the runners-up as pre-validated fallbacks. Zero extra model turns.
The deny rail
An actuation blocklist enforced at delivery time — below the model, in the interpreter, not in a prompt. A model cannot talk its way past it because it is not being asked.
Your app can declare what a control is, and a declaration always beats an inference from copy:
<Pressable testID="delete-account" {...agentHint({ destructive: true, role: 'button', note: 'permanent, no undo' })}/>A rail hit that the run genuinely needs escalates as needs_approval. The grant is scoped, single-use and logged — and approvals are never cached.
Escalation packets
When the runtime hits a real decision point it freezes and builds a packet: the program counter, the reason, delta narration since the program started, ranked heal candidates, the log tail, and an evidence coordinate. Big payloads stay behind resource references rather than being pasted in.
A seeded rename — someone changed a testID — is repaired by exactly one escalation in a 1,143-byte packet. The answer is one constrained arm: pick, patch, retry, skip, abort, approve, or learnInterrupt.
Why it gets cheaper the more you use it
This is the part that compounds, and it is worth reading as a list because each item removes a future model call permanently:
- Delta narration — 97% cheaper perception
- After the first look,
sinceLastLookreturns what changed rather than a second full dump — measured at 97% cheaper while reconstructing ground truth exactly. Every subsequent observation in a session is nearly free. - Learned interrupts — one escalation per popup, ever
- An unhandled interstitial escalates once.
learnInterruptpersists the handler, and interstitial autopilot runs it below every step boundary from then on. The tenth time that cookie banner appears it costs nothing. - The flow library — flows are skills
- A successful program can be saved, generalised into a parameterised version, and replayed. Replaying a library flow costs zero model turns. Your agent’s capability grows as a library rather than as a longer prompt.
- The route graph — knowledge instead of exploration
- Every run records observed transitions with counts and p95 timings.
predictanswers “what has tapping this historically led to” with no live actions;plangives the shortest known path to a screen; andtimeout: autotakes its number from what the app has actually done rather than from the model guessing milliseconds. Unknown transitions return “no history” — the graph never guesses. - The decision cache — a repeat escalation costs nothing
- Answers are memoised by screen signature, failure kind, goal shape and app build. A hit costs zero model calls and still flows through the rail and the gates, so a signature collision produces a failed gate rather than a wrong action.
- Inferred contracts — tests nobody wrote
- What held across every green recording of a screen becomes a contract, with its confidence attached. Violations report as
contract_driftbeside the goal, never as a hard failure, because inferred things can be wrong.
The tools
Seventy-five tools over one persistent warm session, held across calls, so each verb is a ~3 ms socket round trip rather than a process boot. Registering the server never boots a simulator.
- Read this first: squiggle_status
- The capability matrix — transport, session, injected?, dylib freshness, captures, and a map of which capabilities are unusable right now and why. An unknown dylib version is treated as stale, never as trustworthy.
- Perception
describe(interactive-first, token-budgeted, stable short refs liken42, or delta narration),find(a confidence verdict — unique / ambiguous / none — with ranked alternatives up front, so you disambiguate before acting rather than after failing),query,assert,wait_for, andscreenshotas the escape hatch, never the default.- Actuation
tap,input_text,swipe,scroll,back,launch_app,run_flow. A success means dispatched, not effected — which is why programs are the fast path.- Programs
intent_schema,run_intent,intent_wait(long-poll to the next boundary),resume_intent,intent_stop, andsteer— a hint injected into a live run, enforced at the guard seam rather than offered as advice, because a hint the program can ignore is a suggestion.- Memory and planning
library_list/library_save,predict,route,interface(the route graph compiled into a typed per-screen SDK),diff_runs,affected_by_diff(which library flows an interface change puts at risk — expanded through the import graph, so a change to a shared hook marks the screens it reaches, not nothing).- Evidence
explainwalks the deterministic why-chain behind a coordinate — blame is a query, not an inference — andverify_citationsre-resolves every evidence link in a narrative and flags the ones that do not hold. An uncited report is reported as uncheckable.- Data
- The whole perf and product surface — the MCP catalogue is a superset of those CLI families, wrapping the same in-process functions rather than shelling out.
What each tool costs you
Every tool carries a licence posture, and the four classes are the whole taxonomy: 1 free, 20 drive, 31 analyze, 23 agent.
- free —
squiggle_status - Exactly one, and it is the one that tells you why the others are refusing. A capability report that needed a licence to read would be the worst possible thing to gate.
- drive
- Anything that touches the device: perception, actuation, recording, the flow library,
branch,crawl. - analyze
- The perf and product families, plus
query,explain,diff_runsandaffected_by_diff— everything that reads the store. - agent
- The intent runtime itself: programs, escalations, approvals, briefings,
exec,interface,invariants,legibility.
The mapping is not a comment — it is a table three test suites enumerate, one of them against a live server, so a newly added tool cannot quietly inherit a default. It fails closed: unclassified means paid, and a fail-closed default is safe but is still not a decision, which is what the suite exists to say.
Read code-mode
squiggle_exec collapses roughly twenty-five flat read tools into one verb. You write an async function body against a typed read capability surface and return only what you need — filtering and aggregating server-side:
// squiggle_exec — write a program, return only what you needconst slow = await perf.sql(` select name, avg(dur_us) avg from spans where kind = 1 group by name order by avg desc limit 5`);const insights = await perf.insights();return { slow, blocking: insights.filter(i => i.severity === 'error').length };Returning 20 MB so you can grep it in context is the exact failure this exists to prevent, and there is a hard result cap. Reads are low-consequence, so it trades “zero parse failures” for a tight runtime-error → edit → rerun loop.
Acts get no such treatment. They stay the governed intent DSL, because a tap can log out, delete, or pay. Security is layered and the sandbox is the last layer, not the guarantee: actuation is absent rather than gated (there is no drive verb to reach for), the authorization proxy runs in the parent, the database rejects writes, there is no network or filesystem, and every run is traced in a killable child process.
Setting it up
claude mcp add squiggle -- squiggle mcp# or, in .mcp.json at your project root:{ "mcpServers": { "squiggle": { "command": "squiggle", "args": ["mcp"] } } }Then install the methodology that travels with the tools:
squiggle agent kit # write skills, subagents, hooks, commandssquiggle agent rules # an AGENTS.md fragment, generated from the catalogueThe kit is a build artifact, compiled from the DSL schema, the capability library and the mode briefings, and version-pinned — so a stale skill is detectable by its version rather than merely wrong. It ships four skills (squiggle-setup, squiggle-drive, squiggle-perf, squiggle-triage), four subagents (qa, perf, a11y, explore), slash commands, and optional hooks. Every skill is also readable over the wire as a squiggle://skill/… resource, so a client that is not Claude Code still gets the guidance rather than seventy-five tools and no instructions.
A mode is a prompt, a goal template and a hard tool allowlist — not emphasis. The allowlist becomes the generated subagent’s frontmatter, so a perf agent cannot reach a drive verb by accident.
Governance
- One live run per device
- Agent and human can never actuate the same simulator at once.
- Budgets
- Escalations, actions and wall-clock, enforced by the runtime and reported in the run. A ceiling you hit at speed is a wall, so the runtime can advise “finish fast” and let the answerer drop optional work instead of losing the run.
- Every acting call is logged
- Each acting tool call appends to the project’s shared history tagged
source: mcp, the same file the CLI and the desktop write. A human reading the log sees both halves of the work. - Every intent run auto-arms a recording
- A failure already has its reproduction.
- Licensing refuses inside the protocol
- The server always starts — a non-zero exit reads to a client as a broken install, so the customer files a bug instead of buying a licence. It serves, and the refusal travels in the result envelope for the agent to relay. There is deliberately no activate tool: that would mean a licence key typed into a conversation, and into whatever that transcript syncs to.
A human can answer instead
The Agent Center in the desktop app watches a run’s journal stream and lets you answer its escalation packets by hand. Once a packet is structured, a human is a perfectly good model — and free. The same seam serves Claude Code over MCP, a scripted answerer in a test, or you.