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 program
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:

TypeScript
<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, sinceLastLook returns 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. learnInterrupt persists 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. predict answers “what has tapping this historically led to” with no live actions; plan gives the shortest known path to a screen; and timeout: auto takes 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_drift beside 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 like n42, 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, and screenshot as 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, and steer — 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
explain walks the deterministic why-chain behind a coordinate — blame is a query, not an inference — and verify_citations re-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_runs and affected_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
// 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

Terminal
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:

Terminal
squiggle agent kit                 # write skills, subagents, hooks, commandssquiggle agent rules               # an AGENTS.md fragment, generated from the catalogue

The 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.