Skip to main content
The agenda is a shipped state template for a conversation that handles MANY intents per inbound, each intent living through phases across several turns, with dependencies between phases, and one composed reply per turn. An intent is the flow’s job for an inbound — not the user’s wish — so a single message can open several. The agenda is data end to end: a state-template document (its schema, write regimes, declarations, and template jq) plus the bulky jq bodies beside it. A client attaches it to a state and drives it from a flow’s per-node bindings — input injections read the input template jq to decide what is due, and update template jq write the ledger after each step. The pattern is client-agnostic: a client declares only its intents, phases, follow-ons, and reply parts — the manifest — as the attachment’s declarations.
The agenda ships with babelfish as resource files under resources/state-template/agenda/ — the one state-template document and its template jq bodies — not as engine code and not as a flow. It is installed like any other state template (tai state-templates put, then attach it to a state). Babelfish is proprietary and licensed commercially; the agenda ships with it, not on PyPI.

What the template stores

The template’s schema fragment holds four keyed lists and three single slots. The lists are under the composing writer regime — many keyed writers per list — while turn_talk, last_outbound, and transcript are each single, one whole-object writer:
  • ledger — one row per intent: id, kind, an optional item (the intent’s item identity when the kind declares one intent per item; null for an itemless kind), source (inbound | event | tick | standing | follow_on — a record derives tick when its event turn’s inbound source is event:tick, the event kind a schedule adapter posts through the event door), the classifier’s extracted fields, a rank (the order among open intents of a sequence group), a minted_run {run, pass}, and a closed {resolution, sent_run} mark that compose writes. A closed intent is pruned at the next mint once no open intent still names one of its phases (below).
  • ledger_phases — one upserted row per phase key <intent id>:<item>:<phase>, carrying the outcome label the flow returned, its derived class (resolution — settled: done, or one of the manifest’s declared not-done names) or wait (wait_forwho a party, or waits_on another phase key when who is phase), the outcome fields, the writing run/pass, and a sent run stamp compose sets.
  • ledger_inbound — one row per binding of an inbound span to a phase; relation records HOW it bound (answers, ambiguous, event, yes_no, form, option, reactivate, follow_on), and an ambiguous binding carries an attempt counter (the clarify round, from 1).
  • turn_talk — the classifier’s free text for THIS turn ({run, text}), an input to the compose talk slot.
  • last_outbound — a single {run} slot compose sets to the turn id whenever it emits any outbound content (a part or the turn’s talk); a fully silent turn (no part and no talk) re-emits the prior pointer rather than erasing it. It names the last run whose reply emitted anything; a bare yes/no binds only to a wait whose own sent stamp equals that run.
  • transcript{lines: [{role, text, run}]}, the composed-talk window. Compose is the sole writer — the platform exposes no thread read to plugins — appending a user inbound line and, when it speaks, an assistant outbound line each turn, truncated to the talk slot’s transcript_window_lines. The talk slot reads the PRIOR window as context.

Two ids named run

A ledger and ledger_phases row each carry a field called run, and the trace stamp carries another — they mean different things, and the page states the contrast so a dump reads right:
  • A row’s own run (and minted_run.run) is the turn id — the unit of work, the turn the row was written for.
  • _trace.run is the writing run’s session id — the client flow run in which the update template jq wrote the row.

The trace

trace.enabled is true, so every row carries a _trace object: {meta, run, turn, inbound, at} — the writer-supplied meta, the run’s session id, the turn id, the turn’s inbound id, and the write time. The engine supplies meta: {node: <node id>}, so the writing node reads at _trace.meta.node. Because a composing list admits several keyed writers per row kind and nothing statically pins one node per field, the trace stamp is the evidence a dump needs for which node wrote which row. Record ids are <turn.inbound.id>/<kind>/<n>, where n is the 0-based mint ordinal within the run across every source (inbound spans, standing conditions, follow-ons) — one counter takes the next ordinal for every row it mints, so n is unique per turn. Provenance lives in source and event, never in the id. Both the mint and the record update derive source through the template’s shared turn_source input jq — tick when the inbound source is event:tick, event for any other event turn, else inbound.

The manifest a client declares

The attachment’s declarations carry intents, phases, outcomes, and parties — the wait parties beyond the engine’s own — plus clarify_kind, unmatched_kind, clarify_limit and talk_slot. The schema admits no other key. A placeholder manifest — kinds status and utilities, the synthetic vocabulary this page uses throughout:

Intents

An intent declares its kind, classifier description, an optional sequence group, its sources, a standing predicate, and EITHER items (a fixed list; empty means the single implicit item _) OR item {field} (one intent per item, the item identity read from a classifier-extracted field — exclusive with items). A kind may also declare fields {<name>: <JSON-Schema>}, the per-kind extraction schema merged into the classifier’s forced per-span object beside the mandatory text; an item.field, and every rank.match key targeting the kind, must name one of those fields. A kind with item {field} is single-item in the phase sense (its item segment is _); the item identity lives on the ledger row, not in the phase key. An intent’s standing is a jq predicate string that decides whether a standing intent of the kind should be open. Because it reads a sibling state — a value on another state the client also binds, such as an opt-in flag — it lives in the client’s flow binding over that state, not in the agenda template (a template-scoped input jq sees only its own attached subtree). The client’s mint step evaluates it each pass and opens one intent of the kind when it holds and none of the kind is already open (a null predicate contributes no intent). An intent’s sources lists the record sources the kind admits — inbound a classifier span, event/tick an event-door turn. When non-empty the record update ENFORCES it: a span minted for the kind from a source the list excludes raises agenda-record: kind <kind> does not admit source <source> (a standing or follow_on record is exempt — those are not inbound sources).

Phases, labels, and follow-ons

A phase names its key, kind, item, a predecessor, its ordering_waits, an optional expected_input, and outcomes — the outcome labels the phase can record. A phase names no handler — the flow runs its handler for the phase and returns the outcome. A predecessor is a phase key (satisfied when that phase resolves done), "record" (the intent’s item order — satisfied once the earlier items’ same phase is settled in any way), a {phase, outcomes} object (satisfied when the predecessor phase’s current outcome is one of the named labels), or null (a first phase). ordering_waits is a list of {kind, phase} cross-intent gates, phase "*" meaning every phase of the kind. An expected_input declares its kind (yes_no | text | option | form | event) and follow_ons keyed by the answer value. An event input is {kind: "event", event, key, via?}: event is the event kind that correlates here, key the payload key whose value names the item, and an optional via a jq program over the flow root (with $key bound to the payload’s key value). For an item kind (one-per-item, or a static-item phase) via must yield the wait’s item — the ledger row’s item (item_of) for a one-per-item kind, else the phase key’s item segment — and the event binds the wait naming it; a via yielding nothing binds no item wait, and an absent via matches the key value itself. For an itemless kind (item segment _) a via yielding null — which a wait that declares no via also yields — means this event is ours and binds the kind’s single open expectation, while a via yielding the empty no-value (jq empty) means not ours and leaves the event unmatched. An event whose key matches zero or several waits binds nothing and mints one unmatched_kind record; a payload missing key raises agenda: event <kind> carries no '<key>'. Each label is {outcome, resolution? | wait_for {who}, fields, follow_on?}, declaring exactly one of resolution (a class) or wait_for (a party). A resolution is one of two classes: done, the class the engine names, whose successors are reached and whose chain continues; or a not-done resolution, which stops the chain — the intent runs out of open phases, closes, and compose speaks that resolution. The engine names no not-done class of its own: every not-done name is declared by the deployment in parameters.resolutions and referenced by name from a label. A wait_for names the party holding the wait: user and phase are the engine’s own (who: "phase" waits on another phase named at run time by the handler’s waits_on), and every other party is declared in the manifest’s parties list and referenced there by name. fields are the fields the label carries; an optional follow_on fires when the label is recorded. A follow-on is exactly one action:
An item_field on a mint binds the open (kind, item) intent’s waiting phase (else its next reached-unsettled phase, else mints a fresh record carrying that item); an item_field on a close settles the open intent of that (kind, item). A mint’s item_field may carry a list: the mint then fans out, applying that same match-else-mint rule once PER ELEMENT in the same pass — one record (or bind) per element, each carrying its element as item and starting its own pass count. An empty list places nothing; an absent value still raises. A fan-out targets a kind that declares item {field} (one intent per item), so each record carries a single element — a list into any other kind raises at attach, as does a list on a close, which names one item. A follow-on rides an outcome label’s outcomes[].follow_on, and a label’s readable fields ride outcomes[].fields.

Parts rendered from fields

An outcome maps a (kind, phase, outcome) to a reply part. Every outcome row carries its kind: a phase key can repeat across kinds, so (kind, key) is the phase identity everywhere — in the outcome map, the expectations items, and the attach check. A part is rendered from the row’s fields, never a literal payload: A part may also declare repeat_over (a list field — compose emits ONE part per element, the element bound as item in the render context) and to (user, the default, or a party the manifest declares in parties — a part sent to a declared party leaves compose as a deliveries entry, never in the reply to the user). fields lists the fields the part reads; every field the part names — fields, field, schema_field, values_field, repeat_over, a links target, a params value, url_field — must be a member of the label’s fields. A form’s values_field prefill is forwarded as the platform’s per-send form data.values; compose refuses loudly (naming the part and the key) a key that is not a top-level property of the form schema, or a value that does not satisfy that property’s schema — never a silent partial prefill. A client seeds each part.text path through the platform’s template-upload door — POST /api/upload-template with {path, content} writes the content-store path the part names — the surface the stored-content guide documents.

The composed-talk slot

talk_slot (optional; null = off) turns on the composed-talk slot: {prompt_ref: <path>, transcript_window_lines}, where prompt_ref is one content-store talk prompt template id and transcript_window_lines (default 10) bounds the transcript window. When declared, compose runs the talk step after the fixed parts render, handing it the talk facts, the fixed parts, the prior transcript window, the inbound text, the prompt, and the classifier’s turn_talk text; it speaks ONE verified reply appended last (see the compose step). A talk-disposition part with no talk_slot declared is refused at attach.

The rules the attach check enforces

The template’s declarations.check is the agenda’s structural rule; it runs at attach, names the first offender, and the attach refuses on any message it returns. In an action message <via> is follow_on.<label> (a label’s follow-on) or answer.<key> (an expected_input.follow_ons entry). The close rule spans every phase of the target kind because the target phase is chosen at run time, so the outcome map must cover the resolution for all of them. The manifest declares no data-fact preconditions, no discard rules, and no thresholds — dueness is computed from the ledger, never gated on stored values. The same attach seam also compiles each jq the manifest declares against the template’s schema — a phase’s via correlation program — and refuses one that does not compile or whose value type contradicts what the field expects. The cross-state standing predicate is NOT checked here: it reads another state and lives in the client’s binding, so the client’s flow validates it against that state.

Re-attaching over open intents

An attach that replaces the manifest while intents are still open is checked against those open records too. When an open intent names a kind, a phase, an outcome label, or a part the NEW manifest no longer declares, the attach is REFUSED with 422 and the refusal lists every orphaned record — the declarations never change under a conversation whose next turn could no longer resolve:
Open intents whose declarations survive are untouched: their ledger rows, waits, and bindings cross the attach unchanged. To retire those declarations anyway, the caller attaches with options.orphans = "close" and names a declared not-done resolution in options.resolution (one of the new manifest’s resolution values other than done; a name the new declarations do not declare is itself refused). The attach then closes every intent the new manifest no longer covers with that resolution instead of refusing — set on the intent’s open phase rows and marked closed on its ledger row. The close carries no sent_run, so it is unspoken: the next mint’s prune drops the intent and compose says nothing for it.

The parameters

Two attach parameters tune the template without editing its document:
  • resolutions — the enum schema substituted into the resolution field at the {"$parameter": "resolutions"} marker, default {"enum": ["done", null]}. done is the only resolution the engine names; every OTHER member of the enum is a not-done name the deployment declares here, and a label referencing one closes its intent instead of reaching the phase’s successors. The template jq read the allowed set as $parameters.resolutions.enum; a deployment names its own classes (deferred, expired, declined) and keeps the wording neutral, so none inherits a business vocabulary.
  • yes_no_words — the bare yes/no word list the mint binds without the classifier, default {"yes": ["yes", "y"], "no": ["no", "n"]}, read as $parameters.yes_no_words.

The template jq

The agenda’s template_jq split into two purposes. The input programs are read-only predicates over the record subtree — the client’s flow reads them through the template-jq prelude (tjq_<name>({…})) or the record GET …/template-jq/{name} route to decide what is due each pass. . is the attached record subtree; $declarations is the manifest, $parameters the attach parameters, and a program’s declared params ride the single object $params. The core predicates read verbatim: The rest of the 56 input programs: answer_target, answered, anything_due, bound, clarify_rows, clarify_step, close_targets, closed, complete, compose, current_outcome, deadlocked, done_outcome, due, due_set, expectations, fill_target, follow_on_instances, group_of, has_open_phase, intent_has_inbound, intent_row, intent_user_wait, intent_wordless, items_of, key_parts, manifest_phase, open_clarify, open_intents, open_option_expectations, open_yes_no_expectation, outcome_after_binding, pending_follow_ons, placed, rank_targets, reactivated, reply_safe, resolve, settled, settled_this_run, turn_source, unresolved_class, via, waiting, wordless_conflict, and the graph helpers the attach check reads to refuse cycles (mint_adjacency, ow_adjacency, seq_edges, reachable_cyclic). due_set orders the due items by intent rank, ties by the total mint order, one grouped intent per pass; expectations returns the open waits [{key, kind, phase, outcome, who, expected_input, fields, question_ref, schema_field, schema, options}], the expected_input joined from the manifest phase (the wait row carries only wait_for and fields), the schema_field/options read from the outcome map’s part — so the mint binds a form re-entry by the row’s own schema (fields[schema_field].required) and an option tap by the row’s own candidates, never by a static phase schema. pending_follow_ons reads the same placed predicate the seed writes against, so the pair never disagrees — a branch never fires over an empty slice, and the loop never over-runs a placed follow-on. complete asserts every reached phase has settled or is waiting, and it holds wherever the loop stops, because the attach check keeps each kind’s ordering_waits, predecessor, and mint graphs acyclic. At loop end nothing is pending, or the loop would not have stopped; a reached phase that is neither settled nor waiting would be pending but ordering-blocked, so its blocker is a pending same-kind phase, whose blocker is another pending phase, and so on — a chain that, over an acyclic wait graph, must end at a pending, ordering-satisfied phase, which is due. That contradicts the loop having stopped, so complete failing is a real bug — a handler that fired without recording an outcome — never a benign ordering stall. The stale disjunct re-opens settled phases, and it terminates for the same reason: a stale phase stops being stale once it re-records at a pass at or after its predecessor’s, each re-record stamps a pass no earlier than the predecessor’s, and the predecessor graph is a DAG (the attach check refuses a cycle), so the re-reach cascade runs down finitely many edges. A settled phase whose predecessor was re-settled this run is therefore due, not complete’s business: complete stays true over it while the loop keeps going, exactly as it does over a reactivated open wait.

The update programs

The update template jq write the ledger. Each takes {record, input}.record the current subtree, .input the value the binding adapter shaped from the node’s output — and returns a template-relative op batch under the writer regimes; the record POST …/template-jq/{name} route (or a node binding’s update) applies it. The five are:
  • mint — turns each inbound into records or binds it to an open expectation WITHOUT language judgment (writes ledger, ledger_phases, ledger_inbound). It mints standing intents whose predicate holds, binds an event turn by expected_input.event and its via to the waiting phase whose item matches, binds a form submission by its own row schema, binds a tapped option by its own row candidates, and binds a bare yes/no (via yes_no_words, no classifier call) when exactly one open_yes_no_expectation is open. An event with zero or several hits, or a form with no hit, mints an unmatched_kind record; a form with several hits opens a clarify. It also prunes intents already closed and no longer referenced, and computes the open expectations the classifier reads.
  • record — takes the classifier’s structured verdict (or null) and writes per-kind spans as ledger records (source from turn_source, item from the kind’s item {field} when declared), answers bindings as ledger_inbound rows, and each ambiguous span as a clarify — re-binding an already-open clarify and climbing its attempt up to clarify_limit, at which point it records the unresolved label (writes ledger, ledger_phases, ledger_inbound, turn_talk). With a talk_slot declared and a non-blank verdict.talk, it is the sole writer of turn_talk. Before it writes a row it refuses two bindings that resolve to one phase key in one pass (agenda: two bindings resolve to phase <key> in one pass; a phase is answered once per pass, never twice) and an ambiguous binding naming fewer than two candidates (agenda: ambiguous binding on <span> names <n> candidate(s); ambiguous needs two or more — the classifier contract, not the template, guarantees an ambiguity has options to choose between).
  • seed — runs every pass; re-evaluates standing conditions and places pending follow-ons by one match-else-mint rule (bind to the open intent’s waiting phase, else its next reached-unsettled phase, else mint a new record — never a closed intent) (writes ledger, ledger_phases, ledger_inbound). Follow-ons come from a wait’s answer and from each label (resolution OR wait) recorded this run, read as label.follow_on. A close writes the target’s open phase a full outcome row visible to due in the SAME pass; a rank is a merge_by_key on the target ledger row. Every action is bounded by the shared placed predicate — one placement per instance — and, before it writes a row, seed refuses two follow-ons that resolve to one phase key or to one intent id in a pass (agenda: two follow-ons resolve to phase <key> in one pass; … / agenda: two follow-ons resolve to intent <id> in one pass; …).
  • outcome — validates each phase’s returned {key, outcome, fields, waits_on?} (the key is due this pass; the outcome names a label the fired phase declares; every fields key is one the label lists; a resolution label’s derived class is among the attached resolutions; a wait_for label’s derived party is user, phase, or one of the manifest’s declared parties; a who: "phase" label requires waits_on) and writes one ledger_phases row per outcome — the class and party read from the declaration, never the handler (writes ledger_phases). Two results carrying one key are refused before a row is written (agenda: two results resolve to phase <key> in one pass; …). A breach raises loudly.
  • compose_mark — after the pass loop, writes the closed mark on every fully-settled intent, the sent run stamp on each rendered row, last_outbound {run} when the turn speaks, and the transcript window (writes ledger, ledger_phases, transcript, last_outbound). See the compose step.
reconcile is the template’s own three-program set (orphans, close, resolutions) the platform runs when a re-attach retires declarations over open records.

The compose step

Compose runs after the pass loop, reading the rows the outcome update committed. The compose input program first asserts complete($run), else the flow raises agenda: incomplete run: <intents> (the open intents’ ids) — which becomes the route’s error reply. Then, for each row of settled_this_run and each open wait whose question has not yet been sent, it resolves the outcome map’s part — keyed (kind, phase, outcome) — and renders it from the row’s fields:
  • text renders through render_text over the part’s content-store template id, the row’s fields as the render context, and the platform’s renderer resolves the locale from the subject — the template selects no language variant. A text that is not a template id string raises agenda: part text for phase <key> is not a template id rather than sending an empty message.
  • options, form, template, media build their surfaces from the same fields — options from a list field or the fixed values, form from fields[schema_field], template/media from their named fields. A repeat_over part fans out to one part per element of the named list field, the element bound as item.
  • talk hands its fields to the talk slot as a fact and speaks no part; carried merges its fields into the named same-intent carrier; none speaks nothing but is still stamped sent. A carried row whose carrier is not spoken this turn raises agenda: carried row <label> has no carrier this run — unless the row is an open wait and the carrier’s current outcome is the label the part names and already carries a sent stamp, in which case the question stands as already asked: the row sends nothing, renders no unit, and is stamped sent this run. (A reactivation re-records an open wait and drops its sent stamp, so compose meets the wait again while its carrier stays unsent.)
  • a to naming a declared party routes the part into deliveries — a list of {to, part} the client’s own send nodes deliver — instead of the reply to the user.
A part whose fields names a field the row does not carry raises agenda: part <kind>.<phase> <outcome> needs field '<f>' the row does not carry, and an outcome with no matching map row raises agenda: no part for <kind>.<phase> <outcome> before any closed mark, so an unmapped outcome never silently closes its intent. The talk slot. When the template declares a talk_slot and either a talk fact was handed or the classifier supplied turn_talk text, the talk step runs after the fixed parts render, and its one verified reply appends last. The classifier’s talk is an INPUT to the slot, never spoken as a part directly, and when the slot is declared but no fact and no classifier talk exists, no talk part is emitted. The slot’s check enforces two fatal rules and one non-fatal one:
  1. every handed fact leaf must appear in the text, else agenda-talk: fact <label>.<field> not spoken (fatal);
  2. every id-shaped token ([A-Z][A-Z0-9]*-[0-9]+, e.g. A-42) must trace to a handed source — a fact, a fixed part, a transcript line, or the inbound — else agenda-talk: text carries '<tok>', which was not handed (fatal);
  3. every bare numeric token not found in those sources is NON-FATAL — collected on unverified_numbers (visible in the run record’s node output) so ordinary prose numbers (“two options”, “in 5 minutes”) never fail a turn.
compose returns {parts, deliveries}parts the ordered AnswerPart[] the route returns as its reply, deliveries the other-party parts. The compose_mark update then writes the closed mark on every fully-settled intent, the sent run stamp on each rendered row (the guard that sends a wait’s question exactly once, never re-asking an open wait whose question already went out), last_outbound {run} whenever this turn speaks (a silent turn re-emits the prior pointer), and the transcript window (written after the talk, so the stored outbound line includes the talk text).

Using the agenda from a flow

A client attaches the agenda template to a state and drives it from one flow, wiring the pieces the template does not itself run: a classifier, the pass loop, and the reply. There is no shipped node, no lowering, and no chips — the template is data, and the flow orchestrates it through per-node bindings and the template-jq prelude:
  • mint — the flow applies the mint update at the start of the turn (input injections read the open expectations and turn_source), turning the inbound into records or binding it to an open expectation.
  • classify — the flow’s own classifier node (a tools_agent) runs once over the inbound when the mint left spans to classify; its forced response schema and prompt are built by jq from the manifest’s intents. The flow applies the record update over the verdict.
  • the pass loop — a Router in Loop mode runs the passes: each pass applies the seed update, reads due_set/anything_due (the loop’s stop condition) through the prelude, dispatches each due item to the flow’s handler for that intent — one run per due item, an unsequenced kind in parallel, a sequenced kind one at a time in rank order — and applies the outcome update over each handler’s {key, outcome, fields, waits_on?}.
  • compose — once the loop stops, the flow reads the compose input program for the reply and applies the compose_mark update; the route returns .parts and feeds deliveries to its own send nodes.
Because the agenda is a state template, it appears on the platform States screen: it lists among the state-template documents, and a client attaches it to a state and edits the attachment’s declarations there — its manifest. A handler for an intent reads the due item — its {key, intent, item, kind, phase, rank, fields} — from the pass, does its work through the flow’s own tools or a published flow, and returns {key, outcome, fields, waits_on?} where outcome is a label the fired phase declares and waits_on is supplied only for a who: "phase" wait. The handler reads state only through the flow’s own bindings and never writes the agenda ledger directly — the flow’s outcome update records the returned label, deriving the class and party from the declaration.

The pass-loop rules

  • No asks inside the pass loop. A handler never contains an ask or any interactive preset: a parking ask inside a fan-out re-runs its siblings on resume. An outbound question is a wait_for label the handler returns, spoken by compose — never a mid-loop prompt.
  • No preconditions on data facts. Dueness comes only from the phase graph and the ledger’s outcomes and bindings; the manifest declares no thresholds or stored-value gates.
  • A hit cap is a bug. The loop’s max-runs cap (default 20) is the backstop, not a conversation state: reaching it raises, and the failure travels the flow’s ordinary on_error path. The template computes no static pass bound and the manifest declares none; what makes the loop finite is the attach check’s refusal of a cycle in the predecessor, ordering_waits and mint graphs, plus the rule that a fired phase records an outcome and so stops being due. The cap on the client’s loop must be generous enough for the manifest’s longest predecessor/ordering-wait chain, which the client sizes from its own declarations.

How re-entry binds

No outbound part carries a correlation key (AnswerPart forbids extras), so a later inbound binds to an open wait by MATCH, and the mint update does it without the classifier:
  • an event binds to the waiting phase whose expected_input.event names the event’s kind and whose via (a jq program compiled over the flow root, $key bound to the payload’s key field; absent means the key value itself) resolves to the wait’s item — the ledger row’s item for a one-per-item kind (item_of), else the phase key’s item segment, else the intent id. The event door delivers it as a turn, and a payload missing that key raises agenda: event <kind> carries no '<key>'. An event with zero or several matching waits binds nothing and instead mints one unmatched_kind record; a malformed event (no id/kind) raises agenda: malformed event turn — an event carries an id and a kind.
  • a form submission binds to the open form-wait whose ROW-carried schema’s required properties are all present; several matches open a clarify_kind intent, no match mints an unmatched_kind record carrying the form payload;
  • a tapped option arrives as the option’s value in the message and binds to the single open option-wait whose ROW candidates hold that value;
  • a bare yes/no binds when exactly one yes/no wait was asked in the last outbound line — the mint matches it against open_yes_no_expectation, so a yes/no never lands on a question two turns old.
The unmatched kind. An event that matches no wait (or several), and a form that matches none, mints one record of the manifest’s unmatched_kind (default unmatched) — {kind, source, event: <payload>, fields: {reason, candidates, inbound_kind}, rank, minted_run}, reason no_match (zero) or ambiguous (several), candidates the hit phase keys. It reaches its own phase and is resolved by that kind’s handler (a done or a declared not-done resolution) like any other intent, and compose speaks its part. The record bypasses the record update’s sources gate, so the check enforces the unmatched kind’s admission statically (its sources must be absent/empty or cover both event and inbound); the mint never consults sources at run time. Wait parties. A wait’s who is user, phase, or a party the manifest declares in its parties list and the label names. user and phase are the engine’s own. The classifier binds a span only to a user wait — the platform-owned expectations instruction tells the model so, and every expectation item carries its who. A declared party is held for someone off this conversation: its wait wakes on an event of its declared kind delivered through the event door (whatever posts that event is a deployment-side adapter). A phase wait resolves internally when the phase its handler named in waits_on records a resolution.

Termination

The loop always terminates. due excludes a phase with an outcome after its latest binding this run, and every fired handler records an outcome (the compose assertion catches one that did not), so a phase never re-fires. A phase-predecessor chain ends the moment a phase resolves to anything other than done: a not-done resolution leaves every phase successor unreached, so the intent runs out of open phases and compose closes it, speaking that terminal resolution. Item order under a record predecessor is the exception — it advances on any resolution of the earlier items, so a not-done resolution on one item never drops the items that follow. Cross-intent waits are a DAG over (kind, phase): the attach check refuses a cycle in the ordering_waits graph, the predecessor graph within a kind, and the mint graph, so a chain of waits always bottoms out in a due phase and a fresh intent can never be minted every pass. Re-activations, closes, and ranks are each bounded by the placed predicate — one placement per instance, an empty target set counting as placed — so a settled-first target never keeps the loop alive. A closed intent is never re-opened, and it is pruned once no open intent still names one of its phases. The pass cap is the backstop, and hitting it is a manifest bug.