# Expresso — Language Specification (v0) > Expresso is an **effect-typed reactive language** for agents over a live event > log. A program is a deterministic function of the log; all nondeterminism is > confined to statically-identified, journaled, typed **model leaves**, and all > external effects are made idempotent. This document defines the grammar, the > event model, a small-step operational semantics, the effect type system, and > the three guarantees the system makes — so the *meaning* of a program lives in > this spec, not in any one interpreter. > > The reference implementation (`expresso.js`) implements everything in §1–§5. > §7 lists what is specified-but-deferred (durable transport, watermarks, real > model provider) and tracked in `ROADMAP.md`. --- ## 1. Grammar (EBNF) ```ebnf program = { leafdecl | agent } ; leafdecl = "leaf" leafname "(" ident ":" "Span" ")" "->" schema [ "{" "prompt" string "}" ] ; leafname = ident { "." ident } ; (* names may be namespaced: ai.extract_actions *) schema = "{" field { [","] field } "}" | scalar ; field = ident ":" scalar ; scalar = ("text" | "number" | "bool" | "date" | "user" | "string" | "int" | "integer" | "float" | "boolean" (* aliases → text/number/bool *) | "enum" "(" ident { "," ident } ")" | "list" ( "(" schema ")" | "<" schema ">" )) [ "?" ] ; agent = "agent" string "{" { member } "}" ; member = "grants" "{" [ cap { [","] cap } ] "}" (* commas optional — newline-separated reads as a manifest *) | "at" "most" grade | "voice" string | "avatar" ("initials" | string) | "lives" "in" ("meeting" | "meetings" | "channel") [ tag ] | "memory" ident "=" literal | "on" trigger block ; grade = "pure" | "model" | "world" ; cap = ident { "." ident } ; (* action caps: tasks.create … · observation caps: ai.* *) trigger = "someone" "says" match | "message" [ "in" tag ] [ match ] | "silence" "for" duration | "meeting" ("starts" | "ends" | "ended") | "schedule" string ; match = "/" regex "/" (* PURE *) | "~" string ; (* MODEL *) block = "{" { stmt } "}" ; stmt = "let" ident "=" expr | "if" expr block [ "else" block ] | "for" ident "in" expr block (* iterate a list<…>; see §3 loop keys *) | "remember" ident ("=" | "+=") expr | action ; action = "create_task" expr { ("assignee"|"due") ":" expr } | "create_note" "title" ":" expr "body" ":" expr | "say" expr | "reply" expr | "react" string | "notify" expr expr | dotted-action ; (* the capability name as the verb *) dotted-action= ("tasks.create" | "notes.create" | "notify.send" | "message.send") { optkey ":" expr } ; optkey = "title" | "body" | "to" | "text" | "assignee" | "due" | "target" ; expr = orexpr ; orexpr = andexpr { "or" andexpr } ; andexpr = cmp { "and" cmp } ; cmp = postfix { ("=="|"!="|">"|"<"|">="|"<="|"contains") postfix } ; postfix = primary { "." ident } ; primary = string | number | duration | tag | "summarize" "(" span ")" (* MODEL *) | "leaf" leafname "(" expr ")" "->" schema (* MODEL — inline observation: declare + call *) | leafname "(" expr ")" (* MODEL — typed model leaf call *) | "." ident { "." (ident|number) } (* binding: .speaker .match.1 *) | ident (* let-var, memory ref, or frame ref: transcript / meeting *) | "(" expr ")" ; span = "meeting" | "recent" | "last" duration | "here" ".." "end_of_turn" ; ``` The **model sigils** — `~"…"`, `summarize(…)`, and (inline or declared) `leaf` calls — are the *only* syntax that introduces model nondeterminism, and **each requires an observation capability** in `grants {…}` (§4). Everything else is compiled. Two frame references are bound per firing: `transcript` (the meeting-so-far as `Speaker: text` lines) and `meeting` (`{ title, host }`). --- ## 2. Event model The input to a program is a single **event log** `L = e₀ e₁ …`, totally ordered **per partition** by event time. An event is an envelope: ``` e = { id, partition, type, payload, t_event, rev } ``` - `id` — a stable, source-derived identifier (`transcript:`, `task::`). Idempotency and journal keys are built only from replayable data like this — never wall-clock, never a counter. - `partition` — `(workspace, room|channel)`. Ordering is meaningful only *within* a partition. - `type` ∈ { `utterance`, `message`, `silence`, `meeting_start`, `meeting_end`, `schedule` }. - `rev` — revision; a corrected utterance supersedes by `(id, rev)`. The reference runtime consumes an in-memory log; the durable transport (Redis nudge + poll, watermarks) is specified in §7 and `ROADMAP.md`. --- ## 3. Small-step operational semantics A configuration is `⟨ L, S, J, W ⟩`: - `L` — the remaining event log. - `S` — per-agent state: `{ memory, cursor, dedup }`. - `J` — the **journal**: `firingId · leafPath ↦ value` (realized model outputs). - `W` — the **world table**: `idemKey ↦ receipt` (the system-of-record for "did this fire"). Identities (replay-stable, composed only from log data): ``` firingId(a, b, e) = b.site · "|" · e.id (b.site = a.name · "#" · behaviorOrdinal) journalKey(fId, leaf) = fId · "/" · leaf (leaf = b.site · "/" · leafOrdinal [":" loopIdx { "." loopIdx }] · "/" · name) idemKey(e, leaf, class) = e.id · "|" · leaf · "|" · class ``` Inside a `for x in xs { … }` body, every journal/idempotency path carries the **loop index chain** (`…/1:0/create_task`, `…/1:1/create_task`, nested loops `…/1:0.2/…`). The indices are positions in a list that came from a **journaled** model output, so they are themselves replay-stable — N extracted items yield N distinct exactly-once actions, and replay re-derives the same N keys without re-calling the model. Keys are **verbatim structured strings** — not hashed into a small space, not built from a wall-clock or a counter, and per-action-unique (the `leafOrdinal` distinguishes two same-verb actions or two `summarize` calls in one behavior). They are functions of replay-stable log data (`e.id`) and a structural id (`agentName#behaviorOrdinal/leafOrdinal`). **Stability is scoped to edits that preserve the agent name and the order of its behaviors**: editing a behavior's body, comments, or whitespace does not shift keys, but *renaming an agent* or *reordering its behaviors* is a deliberate structural change that re-keys its effects. (A stable author-assigned `id "…"` to decouple identity from name/position, and `rev`-based revision, are deferred — §7.) ### Reduction rules ``` e = head(L) (E-Skip) ───────────────────────────────────────────── ¬routes(a,e) ∨ e.id ∈ S[a].dedup ⟨L, S, J, W⟩ → ⟨tail?(L), S, J, W⟩ (advance when all agents done with e) e = head(L) routes(a,e) e.id ∉ S[a].dedup ¬structuralOk(a,b,e) (E-NoFire) ───────────────────────────────────────────────────────────────────── a,b contribute nothing for e structuralOk(a,b,e) regex(b) m = match(b.regex, e.text) = ⊥ (E-Regex⊥) ───────────────────────────────────────────────────────────────────── a,b do not fire for e (PURE, no journal entry) structuralOk(a,b,e) fuzzy(b) fId = firingId(a,b,e) v, J' = MODEL(J, fId, b.site·"/trigger", match(e.text)) v = false (E-Fuzzy⊥) ───────────────────────────────────────────────────────────────────── ⟨…,J,…⟩ → ⟨…,J',…⟩ (model decision journaled even when it does not fire) structuralOk(a,b,e) enabled(b,e) (regex hit / fuzzy=true / silence / lifecycle) fId = firingId(a,b,e) ⟨J',W'⟩ = exec(body(b), frame(e), S[a], fId, J, W) (E-Fire) ───────────────────────────────────────────────────────────────────────────────── ⟨L, S, J, W⟩ → ⟨L, S[a.dedup ∪ e.id], J', W'⟩ ``` `exec` evaluates the body left-to-right. Its only two non-trivial leaves: ``` MODEL(J, fId, leaf, req): if journalKey(fId,leaf) ∈ J: return (J[…], J) // replay — no model call else: v = provider(req); return (v, J[journalKey ↦ v]) // first run — journal it WORLD(W, k=idemKey(e,leaf,class), payload): if k ∈ W: return W // dedup — exactly-once else: return W[k ↦ receipt(payload)] // apply + record ``` Determinism follows because every rule's result is a function of `(e, S, J, W)` only, and `MODEL`/`WORLD` are memoized by replay-stable keys. --- ## 4. Effect type system The lattice is the three-point total order **`PURE ⊑ MODEL ⊑ WORLD`** with join `⊔`. A type judgment is `Γ ⊢ e : τ ! ε` (expression `e` has type `τ` and effect `ε`). The agent carries a capability environment `Γ.grants ⊆ Cap` and a ceiling `κ`. ``` (T-Lit/Bind) ───────────────────── literals, .speaker, .match.n, {interp}, memory reads Γ ⊢ x : τ ! PURE (T-Regex) ───────────────────── /re/ match + captures Γ ⊢ match : Bool ! PURE obs(~) = ai.match ∈ Γ.grants (T-Fuzzy) ────────────────────────────── ~"φ" Γ ⊢ ~φ : Bool ! MODEL ℒ : Span → τ Γ ⊢ a : Span ! ε obs(ℒ) ∈ Γ.grants (T-Leaf) ───────────────────────────────────────────────────── summarize / typed leaf ℒ(a) Γ ⊢ ℒ(a) : τ ! (ε ⊔ MODEL) where obs(ℒ) = ℒ.name if ℒ.name is namespaced (ai.extract_actions) = "ai."·ℒ.name otherwise; obs(summarize) = ai.summarize Γ ⊢ xs : list⟨τ⟩ ! ε Γ, x:τ ⊢ sᵢ : _ ! εᵢ (T-For) ───────────────────────────────────────────── for x in xs { s… } Γ ⊢ for : Unit ! ( ε ⊔ ⨆ εᵢ ) (E_TYPE if xs is not a list) Γ ⊢ aᵢ : _ ! εᵢ cap(v) ∈ Γ.grants (T-World) ───────────────────────────────────── v ∈ {create_task, notify, say, …} Γ ⊢ v(a…) : Unit ! ( ⨆ εᵢ ⊔ WORLD ) + idemKey minted Γ ⊢ s : _ ! ε ε ⊑ κ (T-Agent) ───────────────────────────── body checked against declared ceiling κ ⊢ agent : ok ``` **Capabilities come in two classes, both enforced the same way.** *Action* capabilities (`tasks.create`, `notify.send`, …) gate what the agent may do to the world; *observation* capabilities (`ai.`, `ai.summarize`, `ai.match`) gate what it may learn through a model. `grants {…}` is therefore the agent's **entire surface** — a reviewer reads one block and knows both what it can observe and what it can touch. Two static errors fall directly out of the rules: - **`E_GRANT`** — `(T-World)` with `cap(v) ∉ Γ.grants`, or `(T-Leaf)`/`(T-Fuzzy)` with `obs(ℒ) ∉ Γ.grants`. No ambient authority: an agent may only touch the world — or consult a model — through capabilities it declares. Additional static rejections: **`E_DUP_AGENT`** (two agents with one name would silently shadow each other's state), a **parse error** for redeclaring a leaf — inline or top-level — with a different schema (call-site types must never lie), and a **parse error** for an invalid `meeting` phase or a non-duration `silence for` argument. Routing is trigger-aware: `lives in` sets the default home, but an agent that declares `on message` receives message events regardless of scope (and vice versa), so a declared behavior can always fire. - **`E_CEILING`** — `(T-Agent)` with `ε ⋢ κ`, e.g. a `MODEL`/`WORLD` leaf (including a fuzzy `~"…"` **trigger**) under `at most pure`. (Time is read from `e.t_event`, never the wall clock, and the grammar exposes no `now()`/`random()` surface — so a PURE agent is genuinely a function of the log; there is no ambient nondeterminism to leak in.) Omitting `at most` defaults the ceiling `κ` to `world` (most permissive); declare a tighter ceiling to make the compiler prove a stronger property about the agent. Inference is a join over the dataflow graph. The checker enforces a **floor**: it rejects any *missing* authority (`E_GRANT`). Over-granting is permitted but visible in the source, so authority is always at least *declared*, never ambient. `say`/`reply`/`react` are **WORLD**, not lesser — they emit into the very stream the program reads, so anything weaker would let T1 lie. --- ## 5. Theorems Let `⟦P⟧(L, J, W)` be the world effects a program `P` applies over log `L` given journal `J` and world table `W`. - **T1 (Pure Replay Determinism).** If `ε(a) = PURE` then agent `a`'s behaviour is a function of `L` alone: `⟦a⟧(L, ∅, ∅)` is fixed and contains no world effects. *Sketch:* PURE excludes `MODEL` (no provider) and `WORLD` (no effects); every leaf is total over bound data; time comes from `e.t_event`. ∎ - **T2 (Journaled Replay Determinism).** For any `a`, `⟦a⟧(L, J, W) = ⟦a⟧(L, J', W)` once `J ⊇` the model keys realized on the first run: re-execution makes **zero** provider calls (every `MODEL` hits `journalKey ∈ J`) and yields identical decisions. *Sketch:* `MODEL` is memoized by `journalKey`, which is a function of replay-stable log data; the first run populates `J`; replay reads it. ∎ - **T3 (Idempotent World / Exactly-Once, relative to an idempotent world port).** Every `WORLD` leaf is guarded by `idemKey ∈ W`, so re-delivery applies **zero** new effects and the runtime double-acts on nothing. The crash window (a process death *between* an external dispatch and recording its `idemKey`) is closed by the host's **write-ahead protocol** (`host.js`): a receipt is persisted as `intent` *before* the side effect and marked `done` *after* it succeeds, so a crash mid-send **re-delivers** (at-least-once) rather than losing the action; `done` receipts never re-fire. End-to-end exactly-once then requires the receiver to honor the `Idempotency-Key` header it is sent (Stripe-style) — that contract is stated, not hidden. Loop-indexed keys (§3) extend per-action uniqueness through `for` fan-out. *Sketch:* `idemKey = e.id · leaf[:loopIdx] · class` is a verbatim, deploy-stable, per-action-unique string; `WORLD` short-circuits on a `done` hit; `intent` rows re-dispatch on recovery. Exhibited by `test/host.js` and `examples/serve-demo/demo.sh` (outage + kill -9). ∎ The reference test suite (`test/smoke.js`) exhibits T1 (the `Counter` agent is certified `replayable`), T2 (replay reports `0` provider calls), and T3 (replay + crash-resume report `0` new effects; exactly one task survives a crash between journal-write and next leaf). --- ## 6. Worked example (hand-verifiable) ```expresso leaf classify(s: Span) -> { kind: enum(bug, question, churn), urgency: enum(low, soon, now) } agent "Triage" { grants { react.add, tasks.create, notify.send, message.send, ai.classify } at most world lives in channel #support memory open_bugs = 0 on message { let c = classify(.message) // : {kind,urgency} ! MODEL (journaled; needs ai.classify) if c.kind == bug { // c.kind : enum ! PURE react "🐞" // ! WORLD cap react.add create_task "Bug: {.message}" assignee: "oncall" due: "+1d" // ! WORLD cap tasks.create remember open_bugs += 1 // ! PURE (agent-local state) reply "Logged. Open bugs: {open_bugs}." // ! WORLD cap message.send } if c.kind == churn { notify "oncall" "Churn risk from {.speaker}" reply "Escalating now." } } } ``` Inferred agent effect: `WORLD` (join of the leaves). Capabilities inferred: `{react.add, tasks.create, message.send, notify.send, ai.classify}` — all in `grants`, so it type-checks. Under `at most pure` it would be rejected with `E_CEILING`; drop `tasks.create` (or `ai.classify`) from `grants` and it is rejected with `E_GRANT`. Run over `m1 = "…500… #bug"`, `m2 = "where do I change my avatar?"`, `m3 = "I'm furious … cancelling our plan"`: | event | model leaf (journaled) | world effects (idemKey) | |---|---|---| | m1 | `classify → {kind:bug,…}` (fresh) | `react`, `create_task`, `reply` (applied) | | m2 | `classify → {kind:question,…}` (fresh) | — | | m3 | `classify → {kind:churn,…}` (fresh) | `notify`, `reply` (applied) | Replay with the journal: **0** provider calls, **0** new world effects — bit-identical (T2+T3). --- ## 7. Status: shipped vs deferred (see ROADMAP.md) **Shipped beyond the reference engine** (all exercised by the test suite): - **Real model provider** — `RealModel({ provider: "openai" | "anthropic" })` routes `match` / `summarize` / `infer` through a live LLM behind the typed, journaled boundary; the injectable transport lets `test/real-adapter.js` exercise the exact wire format offline. The deterministic mock remains the offline test double so T1/T2 are provable with no key. - **Durable host** (`host.js`) — on-disk journal + world table, the write-ahead `intent`/`done` protocol (T3), real sinks (webhook/slack/http/file) with `Idempotency-Key` headers, and `expresso serve` (an HTTP listener: events in, exactly-once actions out, crash-safe). **Specified but deferred**, staged behind the same leaf taxonomy (a port-swap, not a rewrite): - **Durable transport at scale** — event-log table, Redis-nudge + poll subscriber, per-partition cursors, an async runner (the reference runtime is synchronous). - **Watermarks & revisions** — out-of-order/late ASR handling, `on interim` opt-in, retract+reassert on correction. - **Signals** — typed `emit`/`on signal` for agent-to-agent composition (today: stigmergic, via the shared event log). ## 8. Honest open problems - **Watermark trust:** determinism is *relative to* a declared watermark schedule; the schedule must be part of the log to keep replay total. - **Journal/world skew:** a crash between a `WORLD` dispatch and its log record is closed by a service-side idempotency table (the world-of-record), not the runner alone. - **Refinement decidability:** schema refinements are kept to a decidable fragment (linear arithmetic over fields, enum/option case analysis) on purpose. - **Capability inference burden:** inference proposes the minimal grant set, but the author confirms it — we trade a little ceremony for "no silent authority widening."