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:
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.
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.
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:
| confidence | behavior |
|---|---|
| ≥ 0.85 | auto-route |
| 0.50–0.85 | emit kind=UNCLEAR + 2–4 options |
| < 0.50 | fall through to NOVEL → chat backend |
| ≥ 0.95 | elevated 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_codepointsfromwerbos.session.untrustedruns first. Blocks BIDI overrides, zero-width joiners, format codepoints that could steer the LLM classifier’s parse.trace_turnbinds 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
PendingActionvia the existingwerbos.session.permsmachinery — the operator approves explicitly before anything happens. - Audit field sanitization via
werbos.session.perms._sanitize_audit_fieldclamps 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 atwerbos/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_keyfield 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.