Parsetongue

The input-layer intent router. Type anything — vague idea, slash command, natural language — and werbos figures out what you meant, routes to the right skill or cache row, and serves. Parse + parseltongue: the language that lets you speak to systems most people can't talk to directly.

Type “my back hurts” and werbos runs /stretch with target=back. Type “tell me a joke about ssh tunnels” and it runs /joke. Type “i have idea” and it asks you back: a new skill to add | a brainstorm to riff on | something to remember. Three buttons.

That’s parsetongue: the top of the input stack. It takes whatever the operator typed and emits a structured Intent the rest of werbos routes on. Slash commands still work — they’re just the fast path inside the same primitive. The natural-language path is new.

Sprint SE.6 shipped this end-to-end in one stretch on 2026-05-30: SPEC + schema, parser + router, CLI + server integration, all green tests. Lives at werbos.parsetongue — in-werbos module, not a sibling repo. The decision was deliberate (architecture.md has the long version).

Why it exists

Three problems converged on one layer:

  1. The input-layer gap. Developers think in pipelines and precise briefs. Non-developers think in vague ideas. Werbos’s execution layer has been solid for months; the input layer wasn’t. Operators had to memorize slash commands or speak in complete syntax. Parsetongue is the bridge over that gap.

  2. The cache-key problem. SE.5’s precook cache needs a stable key so two phrasings of the same intent hit the same row. Parsetongue emits the canonical key as part of every Intent. “explain quantum entanglement” and " EXPLAIN QUANTUM entanglement" hash to the same SHA-256.

  3. One input layer, not five. Werbos had separate parsers for chat, slash commands, CLI, intent.md briefs, studio prompts. Parsetongue collapses them. Everything that takes operator input now calls parse() and gets an Intent back.

The Intent

Intent is a frozen dataclass carrying everything downstream needs to route. The full SPEC is at werbos/parsetongue/SPEC.md; the essentials:

@dataclass(frozen=True, slots=True)
class Intent:
    _schema: str             # "parsetongue/1" — load-bearing
    raw_input: str
    kind: IntentKind         # skill | tool | lookup | imperative | novel | unclear
    confidence: float        # 0.0 — 1.0
    skill_name: str = ""
    tool_name: str = ""
    slots: tuple[tuple[str, str], ...] = ()
    clarifying_question: str = ""
    clarifying_options: tuple[str, ...] = ()
    cache_key: str = ""
    trace_id: str = ""
    notes: str = ""

The _schema field is the archaeology marker. A 2050 reader of a 2026 trace file routes on it to find the right parser version. Adding new fields is append-only safe within v1.x; renaming, retyping, or breaking semantics ships as parsetongue/2 in a sibling module. The v1 reader keeps working untouched.

IntentKind is a closed enum — six members, locked. Adding a seventh is by definition a v2.

How it parses

Two paths, picked on the leading character:

Fast path — literal slash command. /joke about ssh resolves to kind=SKILL, skill_name="joke", confidence=1.0. No LLM involved. Unknown slashes fall to kind=UNCLEAR with did-you-mean suggestions.

Slow path — natural language. A single LLM classifier call with the entire skill catalog inlined as the system prompt. The model returns a JSON verdict (kind + skill_name + confidence + slots + reasoning). Parsetongue parses tolerantly (fences, prose, partial JSON all OK) and falls through to kind=NOVEL on any failure — the operator never crashes parsetongue, by design.

Confidence thresholds are locked in SPEC:

confidencebehavior
≥ 0.85auto-route
0.50–0.85emit kind=UNCLEAR + 2–4 options
< 0.50fall through to NOVEL → chat backend
≥ 0.95elevated floor for destructive intents

Destructive intents (skills like forget, tools like werbos.shell.run, imperative verbs like delete / wipe) carry that elevated floor. Below it, parsetongue raises DestructiveActionPending — the chat handler translates to a PermissionAskRequired so the operator confirms explicitly.

How it routes

route(intent) -> RouteResult. Per-kind dispatch:

  • SKILL → werbos.prompts.run_prompt_skill(skill_name, text)
  • TOOL → tool registry
  • LOOKUP → SE.5 precook cache (when present)
  • IMPERATIVE → action verb dispatcher
  • UNCLEAR → clarifying flow (operator picks an option, the choice re-enters parsetongue as fresh input)
  • NOVEL → falls through to existing chat backend (the always-available fallback)

The router describes what should happen; the caller executes. Separation of policy and mechanism makes the router pure + testable and keeps execution decisions (timeouts, retries, observability) at the caller.

Composing with precook

Parsetongue + precook are sister sprints. Together:

operator types  ──► parsetongue.parse  ──► Intent (with cache_key)
                                              │
                                              ▼
                                       precook.cache_get(key)
                                              │
                              ┌───── hit ─────┴───── miss ──────┐
                              ▼                                  ▼
                       serve from SQLite              live LLM inference
                       (<50ms)                        then cache the result

For opted-in skills (summarize, eli5, quote in v1), a repeated query serves from SQLite in tens of milliseconds. The LLM runs only when the input is genuinely new. Werbos becomes a static-site generator with an LLM at build time, not a live generator — for the 80% of operator usage that repeats.

Measured on the SR8 deploy: eli5 "binary search" ran live in 10.4s, then served cached in 0.4s — ~25× faster, with most of the 0.4s being docker-exec overhead.

Operator surface

werbos parsetongue parse "<input>"
# emit the Intent as JSON to stdout

werbos parsetongue parse "<input>" --route
# also show what the router would dispatch to

werbos parsetongue parse "<input>" --model qwen2.5:14b
# override the LLM classifier model

Server integration is gated behind the WERBOS_ENABLE_PARSETONGUE env var (default OFF). When enabled, the chat-POST handler runs parse() first; if the result is a high-confidence skill, the content is rewritten into the equivalent slash form and existing slash dispatcher handles it. Worst case (parsetongue throws): original content passes through. The chat never breaks.

Security model

Parsetongue sits at the top of the input stack — every typed character flows through it. That’s a real attack surface, treated with full Day-23-tier security from day 1:

  • strip_format_codepoints from werbos.session.untrusted runs first. Blocks BIDI overrides, zero-width joiners, format codepoints that could steer the LLM classifier’s parse.
  • trace_turn binds each parse to a trace_id; every decision emits a structured event (parsetongue/parse, parsetongue/route, parsetongue/clarify, parsetongue/destructive_gate). Glasshouse already understands the layer.
  • The destructive-action gate requires ≥ 0.95 confidence for any intent in the destructive allowlist. Below the floor, the intent is queued as a PendingAction via the existing werbos.session.perms machinery — the operator approves explicitly before anything happens.
  • Audit field sanitization via werbos.session.perms._sanitize_audit_field clamps any attacker-controllable strings before they reach the trace log.

The full security audit is in werbos/parsetongue/SPEC.md §“Anti-patterns explicitly forbidden by v1.”

Versioning

Two artifacts make parsetongue TIMELESS in the Day-23 Archaeologist sense (“the code knows what year it is and tells you”):

  • werbos/parsetongue/SPEC.md — the canonical v1 contract. Code conforms; when they disagree, the SPEC wins and the code is the bug.
  • werbos/parsetongue/VERSIONING.md — explicit evolution rules. Append-only field additions are safe within v1.x; twelve specific change classes force a v2 sibling module living at werbos/parsetongue_v2/. v1 keeps running unchanged; v2 readers route on _schema.

When v2 ships, the v1 module is frozen (bug fixes only) but never deleted while archived v1 records exist that the operator might want to replay.

What’s NOT in v1

  • Parsetongue’s own intent cache. SPEC declares the cache_key field but v1 re-runs the LLM extractor every parse. Memoizing parse results is v1.1 — needs trace data on which intent shapes recur.
  • Embedding-based fuzzy matching for the cache key. v1 is exact-match canonicalization. Embedding similarity is v1.2.
  • Multi-intent inputs. “summarize that and save to my journal” is two intents. v1 returns the dominant one; multi-intent shape is v2.
  • Conversational state across clarify-and-resolve. v1’s clarifying flow returns options; the operator’s choice re-enters as fresh input. Threaded context is v1.1.

Why it matters

Today werbos is operable if you know the slash commands. After parsetongue is enabled in the chat surface, werbos is operable if you can talk. That’s the difference between a tool for developers and a tool for every person who wants a personal AI but doesn’t want to memorize syntax.

Combined with precook (SE.5), the chat-time path stops being where the AI runs. Live inference fires only for genuinely novel input; everything else is parse → lookup → serve. The model runs at 3am to grow the cache, not when you press enter.

That’s the static-rendered AI thesis, made real.