From 4a889a3dc9d5d987c601b60440fdd14e4fc5911c Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 00:14:07 +0100 Subject: [PATCH 01/57] =?UTF-8?q?docs:=20OVOS-PIPELINE-1=20=E2=80=94=20pip?= =?UTF-8?q?eline-plugin=20abstraction=20(draft)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defines the orchestrator and the pipeline-plugin abstraction: opaque-pipeline_id black boxes the orchestrator iterates in session.pipeline_stages order per utterance, first-match-wins. Plugins expose one operation — match(utterance, session) → Match | None, side-effect-free — and are otherwise black boxes. The orchestrator handles dispatch, notifications, and terminal events. Dispatch topic is : where owner is either a skill_id (skill-owned handler) or a pipeline_id (plugin-bundled handler). From outside, skills and plugin-bundled handlers are indistinguishable. Utterance-layer events: - recognizer_loop:utterance (entry) - ovos.intent.matched (positive match notification) - ovos.utterance.cancelled (transformer cancellation) - complete_intent_failure (no plugin claimed) - ovos.utterance.handled (universal end-marker) Handler-lifecycle trio: - ovos.intent.handler.start / .complete / .error Transformer chain: pre-pipeline modification or cancellation. Per-plugin behavioural contracts (converse, fallback, etc.) are out of scope — plugins are black boxes; each defines itself. The spec body is timeless: no mycroft references, no implementation-code citations, no "where this differs from current OVOS" appendix in the spec itself. Current-OVOS context and divergence catalogues belong in APPENDIX.md (covered in a separate commit). Co-Authored-By: Claude Opus 4.7 (1M context) --- APPENDIX.md | 88 ++++--- CHANGELOG.md | 30 +++ README.md | 7 +- pipeline.md | 706 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 793 insertions(+), 38 deletions(-) create mode 100644 pipeline.md diff --git a/APPENDIX.md b/APPENDIX.md index 0f9fc71..6746688 100644 --- a/APPENDIX.md +++ b/APPENDIX.md @@ -122,26 +122,28 @@ engine-agnostic contract and the pipeline. --- -## 3. The pipeline — what these specs do not cover - -The intent specs (OVOS-INTENT-1/2/3) formalize **intent definition**: -the grammar, the resource files, what an intent is, the intent-engine -contract. OVOS-MSG-1 formalizes the bus that carries the result. -The piece that sits *around* both — the multi-stage **pipeline** that -decides which intent engine even gets a turn, interleaves -confidence tiers, runs `converse` / `fallback` / `common_query` / -`ocp` / `persona` stages, and produces the universal -`ovos.utterance.handled` end-marker — is not formalized by any spec -in this repository yet. - -That gap is what makes OVOS structurally distinctive (HA and Rhasspy -have no equivalent layer), and what most reviewers ask about -first. The natural next formalization is a pipeline / utterance- -lifecycle specification; see §7 known gaps. - -One observation worth flagging here: **the engine-agnostic intent -contract is already realized**, not hypothetical. `ovos-persona` plugs -into the pipeline as a first-class LLM stage (`persona-high`, +## 3. The pipeline — the host-side spec + +The intent specs (OVOS-INTENT-1/2/3/4) formalize **intent definition +and delivery**: the grammar, the resource files, what an intent is, +the intent-engine contract, and the bus messages that carry +registration, match, and dispatch. OVOS-MSG-1 formalizes the bus +that carries them. **OVOS-PIPELINE-1** formalizes the +host-side piece that sits *around* all of those — the utterance +lifecycle, the transformer chain, the ordered pipeline of stages +(intent engines, `converse`, `fallback`, `common_query`, `ocp`, +`persona`, …), the confidence-tier convention, and the universal +`ovos.utterance.handled` end-marker. + +What PIPELINE-1 leaves out by design: the **internal behaviour of +non-intent stages**. Each of `converse`, `fallback`, `common_query`, +`ocp`, `persona`, `stop` is the natural subject of its own future +specification — PIPELINE-1 only defines the contract every stage +conforms to. See §7 known gaps for the list. + +One observation worth flagging: **the engine-agnostic intent +contract is already realized**, not hypothetical. `ovos-persona` +plugs into the pipeline as a first-class LLM stage (`persona-high`, `persona-low`) — the OVOS-INTENT-3 §6.2 non-normative note about LLM-backed engines describes something that ships today. The ordered confidence-tier chain (deterministic Adapt before fuzzy @@ -435,9 +437,12 @@ current code: (`mycroft.skill.handler.{start,complete,error}` etc.) are still informal. The natural next bus spec is OVOS-INTENT-4, which builds on OVOS-MSG-1 + OVOS-INTENT-3. -- **A pipeline specification.** Stage ordering, the confidence-tier - model, and the contracts for `converse`, `fallback`, - `common_query`, `ocp`, and `persona` stages are unspecified (§3). +- **Per-stage behavioural specs.** OVOS-PIPELINE-1 defines the + stage *contract* (the `match` signature, the `StageMatch` shape, + the confidence-tier convention) but explicitly defers what each + non-intent stage actually *does*. `converse`, `fallback`, + `common_query`, `ocp`, `persona`, `stop` are each natural + subjects of their own specifications. - **A session specification.** MSG-1 §4 carries `session` opaquely and names only `session_id` and `lang`. Everything else about the session is deferred — see §5.2 for the explicit list: session @@ -505,7 +510,7 @@ the *why*, but that has no place in a normative document. ### 9.1 The set, in two stacks -Built bottom-up in two stacks: +Built bottom-up in three stacks: - The **intent stack**, in dependency order: OVOS-INTENT-1 (template grammar) → OVOS-INTENT-2 (resource files built on it) → @@ -516,6 +521,13 @@ Built bottom-up in two stacks: Originally drafted as two specs (envelope + session/routing) and merged once it became clear the derivations could only meaningfully be defined where the routing keys lived. +- The **host stack**, sitting around the intent and bus stacks: + OVOS-PIPELINE-1 formalizes the utterance lifecycle, the + transformer chain, the ordered pipeline of stages (intent and + non-intent), the confidence-tier convention, and the universal + `ovos.utterance.handled` end-marker. It builds on OVOS-MSG-1 + and provides the host context that all the other specs operate + inside. Each was a formalization pass over machinery already running in production (§1), not a greenfield design. @@ -576,15 +588,19 @@ A specification that does not change between levels keeps its lower version number — OVOS-INTENT-3 is at version 1 in both V1 and V2. -### How the bus stack will be layered in - -OVOS-MSG-1 introduces the bus envelope, which is structurally -orthogonal to the intent stack — a tool can implement the intent -stack without the bus envelope and vice versa. As more bus-layer -specs land, the compatibility-level model is expected to evolve; -the current V0–V2 ladder may grow a second axis or be replaced -with per-stack ladders. - -Until that's settled, the bus-layer specs (OVOS-MSG-1 and the -others in the pipeline behind it) are versioned individually but -not yet placed on a compatibility ladder. +### How the bus and host stacks will be layered in + +OVOS-MSG-1 introduces the bus envelope; OVOS-INTENT-4 introduces +intent registration and dispatch on top of it; OVOS-PIPELINE-1 +introduces the host-side utterance lifecycle. All three are +structurally orthogonal to the intent grammar/resource stack — a +tool can implement either without the other. As a result the +single-axis V0/V1/V2 ladder above is no longer sufficient to +describe the architecture as a whole. + +The compatibility-level model is expected to evolve into either a +multi-axis grid or per-stack ladders. Until that's settled, the +bus and host specs (OVOS-MSG-1, OVOS-INTENT-4, OVOS-PIPELINE-1) +are versioned individually but not yet placed on a compatibility +ladder. Implementers targeting them today should cite each spec's +own `Version` field rather than a compat level. diff --git a/CHANGELOG.md b/CHANGELOG.md index c9625fa..9f69634 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,3 +76,33 @@ tool does not recognize the token and cannot expand the template. authentication, authorization, retry, delivery and ordering guarantees, session lifecycle, and the internal shape of `session` beyond `session_id` and `lang` are explicitly out of scope. + +## OVOS-PIPELINE-1 — Utterance Lifecycle and Pipeline + +### 1 + +- Initial draft. Defines the **orchestrator** and the + **pipeline plugin** abstraction: opaque-`pipeline_id` black + boxes the orchestrator iterates in `session.pipeline_stages` + order per utterance, first-match-wins. Plugins expose one + operation — `match(utterance, session) → Match | None`, + side-effect-free; the orchestrator handles dispatch, + notifications, and terminal events. +- Dispatch topic: `:` where `owner_id` is + either a `skill_id` (skill-owned handler) or a `pipeline_id` + (plugin-bundled handler). Plugins and skills are equivalent + handler owners from the bus's perspective. +- Utterance-layer events: `recognizer_loop:utterance` (entry), + `ovos.intent.matched` (positive match notification), + `ovos.utterance.cancelled` (transformer cancellation), + `complete_intent_failure` (no plugin claimed), + `ovos.utterance.handled` (universal end-marker, fires on every + terminal path). +- Handler-lifecycle trio: `ovos.intent.handler.start` / `.complete` + / `.error`, emitted by whoever runs the handler (skill or + plugin). +- Transformer chain: pre-pipeline modification or cancellation + via `context["canceled"]`. +- Per-plugin behavioural contracts (converse, fallback, + common_query, persona, language-model plugins, etc.) are out + of scope — plugins are black boxes; each defines itself. diff --git a/README.md b/README.md index 74749e3..c0978f5 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ below). Adoption is voluntary; conformance, once adopted, is not. | OVOS-INTENT-2 | [Locale Resource Formats](locale-resource-formats.md) | 2 | Draft | | OVOS-INTENT-3 | [Intent Definition](intent-definition.md) | 1 | Draft | | OVOS-MSG-1 | [Bus Message](message-object.md) | 1 | Draft | +| OVOS-PIPELINE-1 | [Utterance Lifecycle and Pipeline](pipeline.md) | 1 | Draft | Each spec carries its own scope statement, design rationale, and conformance section in its own header. Open the document for the @@ -72,8 +73,10 @@ full picture — the table above is just an index. order: OVOS-INTENT-1 defines the template grammar; OVOS-INTENT-2 builds on it to define the resource files; OVOS-INTENT-3 builds on both to define what an intent is. OVOS-MSG-1 is the bus-layer -envelope and the routing / session model — independent of the -intent stack and readable at any point. +envelope and the routing / session model — readable at any point. +OVOS-PIPELINE-1 is the host-side spec: it defines the utterance +lifecycle that wraps everything else (transformers, ordered stages, +terminal events) and builds on OVOS-MSG-1. For background — design rationale, comparisons with other systems, the catalogue of known divergences from current code, and known diff --git a/pipeline.md b/pipeline.md new file mode 100644 index 0000000..7139cfd --- /dev/null +++ b/pipeline.md @@ -0,0 +1,706 @@ +# Utterance Lifecycle and Pipeline Specification + +**Spec ID:** OVOS-PIPELINE-1 · **Version:** 1 · **Status:** Draft + +This document defines the **utterance lifecycle** — the path an +utterance takes from the moment it enters the assistant to the moment +the assistant is done with it — and the **pipeline plugin** +abstraction the **orchestrator** runs to decide what to do with each +utterance. + +It is the orchestrator-side companion to OVOS-INTENT-3 and +OVOS-INTENT-4. Those specifications define what an intent *is* and +how a skill puts an intent on the bus; this one defines what the +orchestrator does with utterances and the contract every pipeline +plugin conforms to. + +It builds on three companion specifications: + +- the *Bus Message Specification* (OVOS-MSG-1) — the envelope, + routing keys, session carrier, and derivations every Message + defined here travels in; +- the *Intent Definition Specification* (OVOS-INTENT-3) — defines + the *orchestrator* and the intent / handler model; +- the *Intent and Entity Registration Bus Contract* (OVOS-INTENT-4) + — the wire format pipeline plugins consume to learn what intents + to match (when they choose to). + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT** and +**MAY** are used as in RFC 2119. + +--- + +## 1. Scope + +This specification defines: + +- the **pipeline plugin** abstraction (§3) — the only thing the + orchestrator iterates; +- the **match contract** (§4) — the only thing a plugin exposes; +- the **`session.pipeline_stages`** field (§5) — how a session + chooses which plugins and in what order; +- the **utterance lifecycle** (§6) — entry, transformer chain, + iteration, dispatch, terminal events; +- the **dispatch** topic shape (§7) — `:`; +- the **handler-lifecycle trio** (§8) — + `ovos.intent.handler.start` / `.complete` / `.error`; +- the **utterance-layer bus events** (§9) — + `recognizer_loop:utterance`, `ovos.intent.matched`, + `ovos.utterance.cancelled`, `complete_intent_failure`, + `ovos.utterance.handled`; +- the **transformer chain** (§10) — pre-pipeline modification and + cancellation; +- **conformance** (§11). + +It does **not** define: + +- **what any pipeline plugin actually does** — plugins are black + boxes identified by an opaque `pipeline_id`. The orchestrator's + only contract with a plugin is the `match` operation of §4. + Whether a plugin matches by template intents, keyword intents, a + fine-tuned classifier, a chatbot, a language model, or anything + else is the plugin's business. +- **what any handler does** — handlers are black boxes. Skills run + their own; plugins that bundle handlers run theirs. The bus + observes the handler-lifecycle trio (§8) and that is the full + observable contract. +- **how plugins are loaded, discovered, configured, or + instantiated** — a deployment concern. +- **how plugins consume registrations** — OVOS-INTENT-4 puts + registrations on the bus; whether and how a given plugin + subscribes is the plugin's own business. +- **the `session` lifecycle** — `session` is carried opaquely per + OVOS-MSG-1 §4. `session.pipeline_stages` is one internal field + this spec prescribes (§5); other internal fields are deferred to + a future session specification. +- **per-plugin behavioural specs** — plugins have no behavioural + contract beyond §4. A `converse` plugin, a `fallback` plugin, a + persona plugin, a language-model plugin, a chatbot plugin: each + defines itself. + +--- + +## 2. The orchestrator and the pipeline plugin + +The **orchestrator** (OVOS-INTENT-3 §6.1) is the single component +that consumes the utterance entry point `recognizer_loop:utterance`, +iterates plugins per session, emits dispatch and terminal events, +and guarantees the universal end-marker `ovos.utterance.handled`. +The orchestrator is distinct from the **messagebus** (the transport +layer) and from any individual plugin. + +A **pipeline plugin** is a third-party component identified by an +opaque `pipeline_id` — an arbitrary, deployment-unique string. The +orchestrator loads some number of plugins at startup; how it +discovers and instantiates them is a deployment concern. Each +plugin exposes one operation to the orchestrator (§4) and is +otherwise a black box. + +From the orchestrator's perspective, "plugin" and "skill" are +indistinguishable as handler owners. Both are black-box third-party +components. The only difference is where the handler lives: + +- a skill's handler is reached via a `:` + dispatch topic — the skill registered the intent (OVOS-INTENT-4) + and owns the handler; +- a plugin's bundled handler is reached via a + `:` dispatch topic — the plugin matched + the utterance and owns the handler itself. + +From outside either case, the assistant responded. The user does +not know or care which component answered. + +Plugins are diverse by design. A deployment may load plugins that +consume OVOS-INTENT-4 registrations and match against keyword or +template intents, plugins that consume no registrations and match +by their own internal rules (such as language-model-backed +personas), plugins that always claim with a fallback response, or +anything else. The contract is just `match`. + +A deployment running skills that emit OVOS-INTENT-4 keyword or +template intent registrations **SHOULD** load at least one plugin +that consumes those registrations; otherwise those intents will +never match. Whether to load such a plugin is a deployment choice, +not a spec-level requirement. + +--- + +## 3. Pipeline plugins + +A pipeline plugin is identified by an opaque **`pipeline_id`** — +an arbitrary string. The orchestrator's loaded-plugin set is a +mapping `pipeline_id → plugin instance`; the orchestrator does +not interpret the `pipeline_id` string beyond using it as a key. + +Constraints on `pipeline_id` strings: + +- Non-empty. +- Must not contain a colon (`:`) — the colon separates owner from + intent name in the dispatch topic shape (§7), and `pipeline_id` + may appear as the owner. +- Must match the topic-name syntax of OVOS-MSG-1 §2.1 (ASCII + letters, digits, `.`, `_`, `-`; no whitespace). +- Unique within a deployment's loaded-plugin set. + +A plugin **MAY** appear in a session's pipeline more than once +under different `pipeline_id`s if the plugin chooses to expose +multiple matching modes (for example, a strict mode and a +permissive mode). The orchestrator treats each `pipeline_id` as a +distinct stage. + +--- + +## 4. The match contract + +A plugin exposes one operation to the orchestrator: + +``` +match(utterance, session) → Match | None +``` + +Inputs: + +- `utterance` — the user-side input as received in + `recognizer_loop:utterance` (typically a list of one or more + candidate strings; see §9.1); +- `session` — the session carrier from `context.session` of the + utterance Message (OVOS-MSG-1 §4). + +Output: either `None` (decline) or a `Match` object with the +fields below. + +### 4.1 The `Match` shape + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `owner_id` | string | yes | The `skill_id` of the skill that owns the handler, **or** the `pipeline_id` of the plugin itself if the plugin bundles its own handler. | +| `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | +| `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | +| `utterance` | string | no | The specific candidate string from the input list that won the match. | + +The orchestrator interprets a non-`None` return as a definitive +claim. It does not score, rank, or rerank matches across plugins — +**first match wins** (§6). A plugin that wants to express +uncertainty must return `None` and let a later plugin claim. + +### 4.2 Plugins are side-effect-free during `match` + +A plugin **MUST NOT** emit Messages from inside its `match` +operation. `match` is a pure decide-or-decline call. Any side +effects — activating a skill, updating internal session state, +storing conversational history — happen after the orchestrator +has confirmed the claim wins, in the plugin's own handler +(reached via the dispatch topic of §7) or in some other plugin- +internal step the spec does not constrain. + +This is the difference between "matching" and "running": the +orchestrator may call `match` on several plugins before one +claims; plugins that took side effects from declined matches +would corrupt each other's state. + +### 4.3 The capture map + +`Match.captures` is a `{string: string}` mapping (the same shape +OVOS-INTENT-3 §7 defines for template / keyword intent slots). + +For skill-owned matches against intents the plugin previously +consumed from OVOS-INTENT-4 registrations, the capture map keys +are the slot names (template intents) or vocabulary names (keyword +intents) of the matched intent. + +For plugin-owned matches, the capture map is whatever the plugin +chooses to surface. It **MAY** be empty. + +The orchestrator does not interpret the capture map; it forwards +it to the dispatched handler. + +--- + +## 5. `session.pipeline_stages` + +The session (OVOS-MSG-1 §4) carries an ordered list of pipeline +identifiers under the field name **`pipeline_stages`**: + +```json +{ + "session": { + "session_id": "default", + "lang": "en-US", + "pipeline_stages": [ + "padatious-high", + "adapt-high", + "padatious-medium", + "adapt-medium", + "common-qa", + "persona-high", + "fallback-low" + ] + } +} +``` + +`pipeline_stages` is a normative internal field inside `session` +prescribed by this specification (analogous to `session_id` and +`lang` from OVOS-MSG-1 §4). Other internal session fields remain +opaque (deferred to a future session specification). + +For each utterance, the orchestrator iterates `pipeline_stages` +in order, calling `match` on each corresponding plugin (§6.2). + +If a `pipeline_id` in `pipeline_stages` does not correspond to +any loaded plugin, the orchestrator **MUST** skip it and +**SHOULD** log a warning. It **MUST NOT** abort the utterance +over an unknown identifier. + +If `session.pipeline_stages` is absent or empty, the orchestrator +**MAY** fall back to a deployment-configured default. If no +default is configured, the utterance proceeds to no-match +(`complete_intent_failure`, §9.4). + +Different sessions may carry different `pipeline_stages`. This +is how a deployment provides different behaviour to different +participants — for example, a remote-peer session may carry a +restricted pipeline that excludes destructive plugins. + +--- + +## 6. The utterance lifecycle + +Every utterance flows through the same lifecycle, regardless of +which plugin (if any) claims it. The lifecycle is **guaranteed +to terminate** with exactly one `ovos.utterance.handled` event +(§9.6). + +### 6.1 The flow + +``` +recognizer_loop:utterance ← entry (§9.1) + │ + ├─ transformer chain (§10) + │ └─ if any transformer set context["canceled"] = true: + │ ovos.utterance.cancelled (§9.3) + │ ovos.utterance.handled (§9.6) + │ STOP + │ + ├─ session retrieval; pipeline_stages read from session (§5) + │ + ├─ for pipeline_id in session.pipeline_stages: + │ plugin = loaded_plugins[pipeline_id] # skip if not loaded + │ match = plugin.match(utterance, session) + │ if match is not None: + │ ovos.intent.matched (§9.2) + │ dispatch on : (§7) + │ (handler runs; emits lifecycle trio §8) + │ ovos.utterance.handled (§9.6) + │ break + │ + └─ if no plugin matched: + complete_intent_failure (§9.4) + ovos.utterance.handled (§9.6) +``` + +### 6.2 First-match-wins iteration + +For each utterance, the orchestrator **MUST**: + +- iterate `session.pipeline_stages` in order; +- for each `pipeline_id`, call `match` on the corresponding loaded + plugin (skipping unknown identifiers, §5); +- stop at the **first plugin** that returns a non-`None` `Match`; +- if no plugin returns a `Match`, emit `complete_intent_failure` + (§9.4). + +A plugin that raises an exception during `match` is treated as if +it returned `None`. The orchestrator **MUST** continue to the next +plugin and **SHOULD** log the exception. A single plugin's bug +does not fail the whole utterance. + +### 6.3 Plugins do not see each other's matches + +A plugin receives the same utterance every other plugin in the +pipeline received; it has no access to what an earlier plugin +tried or why it declined. Cross-plugin coordination belongs in +`session` (OVOS-MSG-1 §4) or in plugin-side out-of-band state +keyed on `session.session_id` (per OVOS-MSG-1 §5.4 — +"no central correlation, no central state"). + +### 6.4 Terminal events + +Every utterance terminates in exactly one of three ways, each +followed by the universal end-marker `ovos.utterance.handled`: + +| Outcome | Sequence of utterance-layer events | +|---------|------------------------------------| +| Cancelled by transformer | `ovos.utterance.cancelled` → `ovos.utterance.handled` | +| Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.handled` | +| No plugin matched | `complete_intent_failure` → `ovos.utterance.handled` | + +If a dispatched handler emits `ovos.intent.handler.error` (§8) +instead of `.complete`, the orchestrator still emits +`ovos.utterance.handled` afterwards. The "every utterance +terminates with `ovos.utterance.handled`" invariant holds across +all paths. + +--- + +## 7. Dispatch + +When a plugin's `match` returns a non-`None` `Match`, the +orchestrator dispatches the matched handler by emitting a Message +on the topic: + +``` +: +``` + +where `` is `Match.owner_id` (a `skill_id` or a +`pipeline_id`) and `` is `Match.intent_name`. + +### 7.1 Routing and payload + +The dispatch Message's `context` (OVOS-MSG-1 §4): + +- `session` is propagated from the originating utterance; +- `source` and `destination` follow the single-flip routing model + (OVOS-MSG-1 §5.2) — the orchestrator derives the dispatch via + `reply`, so `destination` is the original utterance emitter and + `source` is the orchestrator. + +The dispatch Message's `data`: + +```json +{ + "owner_id": "music.skill", + "intent_name": "play_music", + "lang": "en-US", + "utterance": "play the beatles", + "captures": { "query": "the beatles" } +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `owner_id` | string | yes | The `Match.owner_id` — the topic's prefix, repeated for the handler's convenience. | +| `intent_name` | string | yes | The `Match.intent_name` — the topic's suffix. | +| `lang` | string | yes | The language the utterance was recognized in (`data.lang`, OVOS-MSG-1 §4.2). | +| `utterance` | string | yes | The candidate string that won the match. | +| `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | + +### 7.2 Subscription discipline + +Each handler subscribes to exactly its own +`:` topic. A skill subscribes to topics +under its own `skill_id`; a plugin that bundles its own handlers +subscribes to topics under its own `pipeline_id`. Because each +topic is unique to one handler, the bus delivers the dispatch +only to the intended consumer. + +A consumer that receives a dispatch on a topic it should not be +listening to (a configuration bug) **MUST NOT** run the handler +and **SHOULD** log the discrepancy. The orchestrator does not +police subscriptions. + +### 7.3 In-process equivalence + +When the handler-owning component (skill or plugin) runs in the +same process as the orchestrator, the orchestrator **MAY** invoke +the handler directly without serializing the dispatch Message +over a transport — provided every external observer sees the +same `:` dispatch and the same +handler-lifecycle trio (§8) it would have seen for an +out-of-process handler. This uniformity is what makes a +deployment portable across in-process and out-of-process handler +arrangements. + +--- + +## 8. Handler-lifecycle messages + +The handler — whether a skill or a plugin-bundled handler — is a +black box. The bus observes what it does via three broadcast +notification topics, the **handler-lifecycle trio**: + +| Topic | Meaning | +|-------|---------| +| `ovos.intent.handler.start` | The handler has begun. | +| `ovos.intent.handler.complete` | The handler has finished normally. | +| `ovos.intent.handler.error` | The handler raised. | + +These are emitted by the handler-owning component (skill or +plugin), produced via OVOS-MSG-1 §5.1 `forward` from the +originating dispatch Message — `context` is preserved unchanged. + +The trio is the only observable about handler execution. It is +broadcast so any observer (the orchestrator for timeout +bookkeeping, loggers, transcript viewers, analytics, fallback +chains) can subscribe. + +### 8.1 Order and obligations + +For each accepted dispatch, the handler-owning component +**SHOULD** emit: + +- on normal completion: `ovos.intent.handler.start` followed by + `ovos.intent.handler.complete`; +- on exception: `ovos.intent.handler.start` followed by + `ovos.intent.handler.error`. + +A handler that does not emit the trio still ran (the spec cannot +prevent that) but is non-conformant — its execution is invisible +to the bus. + +### 8.2 Payload + +Each lifecycle message's `data`: + +```json +{ + "owner_id": "music.skill", + "intent_name": "play_music" +} +``` + +`ovos.intent.handler.error` adds an `exception` field: + +```json +{ + "owner_id": "music.skill", + "intent_name": "play_music", + "exception": "RuntimeError: Spotify is not configured" +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `owner_id` | string | yes | The handler-owning component's id (skill_id or pipeline_id). | +| `intent_name` | string | yes | The intent the handler was dispatched for. | +| `exception` | string | `error` only | Human-readable description of the failure. | + +Implementations **MAY** include additional fields but consumers +**MUST NOT** require them. + +### 8.3 Orchestrator timeout + +The orchestrator **MAY** wait for `.complete` or `.error` matching +the dispatched `(owner_id, intent_name, session)` for a +deployment-defined time bound. If neither arrives within the +bound, the orchestrator **MUST** still emit +`ovos.utterance.handled` (§9.6) to satisfy the universal +end-marker invariant. It **MUST NOT** synthesize a `.error` of +its own — error events come from the handler that owns the trio. + +The orchestrator **MUST NOT** re-emit the dispatch Message for the +same match. Re-dispatch is not defined by this specification. + +--- + +## 9. Utterance-layer messages + +This specification formalizes five utterance-layer bus events. +All travel in standard OVOS-MSG-1 envelopes; routing follows the +single-flip model of OVOS-MSG-1 §5.2. + +### 9.1 `recognizer_loop:utterance` + +The **utterance-layer entry point**. Produced by any component +that wants to feed an utterance into the assistant — a listener, +a chat bridge, a CLI, a test harness, a remote-peer client. The +orchestrator subscribes and runs the lifecycle of §6. + +Payload: + +```json +{ + "utterances": ["turn off the lights"], + "lang": "en-US" +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `utterances` | array of strings | yes | One or more candidate utterance strings. | +| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator disambiguates from `session.lang` and other context. | + +### 9.2 `ovos.intent.matched` + +Emitted by the orchestrator after a plugin's `match` returns +non-`None`, before the dispatch (§7) goes out. Broadcast (no +`destination`). + +Payload mirrors the dispatch payload (§7.1) plus the matching +plugin's id: + +```json +{ + "owner_id": "music.skill", + "intent_name": "play_music", + "lang": "en-US", + "utterance": "play the beatles", + "captures": { "query": "the beatles" }, + "pipeline_id": "padatious-high" +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `owner_id`, `intent_name`, `lang`, `utterance`, `captures` | as §7.1 | yes | Same fields as the dispatch payload. | +| `pipeline_id` | string | yes | The `pipeline_id` of the plugin that produced the match. | + +`ovos.intent.matched` is a **notification**, not a dispatch. +Consumers **MUST NOT** treat receipt as permission or instruction +to run a handler — handler invocation happens via the dispatch +topic (§7). + +### 9.3 `ovos.utterance.cancelled` + +Emitted by the orchestrator when a transformer requested +cancellation (§10.2). Broadcast. Payload **MAY** carry +transformer-supplied metadata; this specification does not +normatively define its shape. + +This message **MUST** be followed immediately by +`ovos.utterance.handled` (§9.6). + +### 9.4 `complete_intent_failure` + +Emitted by the orchestrator when pipeline iteration completed +with no plugin claiming the utterance. Broadcast. Payload +**MAY** carry the original utterance data for observability. + +This message **MUST** be followed immediately by +`ovos.utterance.handled` (§9.6). + +This is the **intent-layer failure** signal. It is distinct from +a handler-layer error (§8): `complete_intent_failure` means "no +plugin claimed"; `ovos.intent.handler.error` means "a handler +ran and raised." + +### 9.5 The dispatch topic + +`:` — see §7. + +### 9.6 `ovos.utterance.handled` + +The **universal end-marker** for an utterance. Emitted by the +orchestrator on every terminal path — cancellation, no-match, +matched-and-handler-completed, matched-and-handler-errored, +matched-and-handler-timed-out. + +Broadcast. Payload **MAY** be empty. + +A conformant orchestrator **MUST** emit exactly one +`ovos.utterance.handled` per `recognizer_loop:utterance`. +Multiple emissions for one utterance are malformed; zero is +malformed. + +--- + +## 10. The transformer chain + +Before pipeline iteration, the orchestrator **MAY** run an ordered +chain of **transformers** that can modify the utterance, modify +its `message.context`, or request cancellation. + +This specification gives a minimum contract for transformers; +their loading and ordering are deployment concerns. + +### 10.1 Two transformer roles + +- **Utterance transformers** — may modify the list of utterances + (e.g. punctuation cleanup, profanity filtering, normalization). +- **Metadata transformers** — may modify `message.context` + (e.g. classifying speaker identity, adding tracing identifiers). + +A transformer in either role **MAY** request cancellation of the +utterance by setting `message.context["canceled"] = true`. + +### 10.2 Cancellation semantics + +If any transformer sets `message.context["canceled"] = true`, the +orchestrator **MUST**: + +- not iterate the pipeline for this utterance; +- emit `ovos.utterance.cancelled` (§9.3); +- emit `ovos.utterance.handled` (§9.6). + +### 10.3 Transformer chain is not a plugin + +Transformers run *before* the pipeline. They do not return a +match; they only modify the message (or cancel it). The match +contract of §4 applies to pipeline plugins only. + +--- + +## 11. Conformance + +### An **orchestrator** **MUST**: + +- subscribe to `recognizer_loop:utterance` (§9.1); +- run every received utterance through the lifecycle of §6 + exactly once; +- emit `ovos.utterance.handled` (§9.6) exactly once per + utterance, regardless of which terminal path was taken; +- if any transformer set `context["canceled"] = true`, emit + `ovos.utterance.cancelled` and **MUST NOT** iterate the + pipeline (§10.2); +- iterate `session.pipeline_stages` in order (§6.2) and stop at + the first plugin returning a non-`None` `Match`; +- skip unknown `pipeline_id`s without failing the utterance (§5); +- emit `complete_intent_failure` when no plugin claimed (§9.4); +- emit `ovos.intent.matched` (§9.2) on every successful claim, + before the dispatch; +- dispatch on `:` per §7; +- handle a plugin exception by logging and continuing to the + next plugin (§6.2), not by failing the utterance; +- subscribe to the handler-lifecycle trio (§8) to observe + dispatched-handler outcomes; **MUST NOT** synthesize trio + events of its own (§8.3). + +### A **pipeline plugin** **MUST**: + +- expose a `match(utterance, session) → Match | None` operation + (§4); +- be **side-effect-free during `match`** (§4.2) — no Messages + emitted, no state changed beyond what is needed to decide; +- when claiming, return a `Match` with `owner_id` and + `intent_name` per §4 — never a partial or speculative claim; +- bear a `pipeline_id` distinct from any other loaded plugin's + id (§3). + +### A **handler** (skill or plugin-bundled) **SHOULD**: + +- emit `ovos.intent.handler.start` when invoked (§8.1); +- emit exactly one of `ovos.intent.handler.complete` or + `ovos.intent.handler.error` when it finishes (§8.1); +- include `owner_id` and `intent_name` in the trio payload + (§8.2); +- run the handler at most once per dispatch. + +### A **transformer** **MUST**: + +- modify only the utterance list and / or `message.context`; +- request cancellation via `context["canceled"] = true` if and + only if the utterance is to be dropped (§10.2); +- be side-effect-free beyond its returned modifications. + +### Non-goals + +The following are explicitly outside this specification: plugin +loading and discovery; transformer discovery and ordering; ASR +n-best ranking semantics within plugins; per-plugin behavioural +specs; the `session` object's full internal shape beyond +`session_id`, `lang` (OVOS-MSG-1 §4), and `pipeline_stages` +(§5). + +--- + +## See also + +- *Bus Message Specification* (OVOS-MSG-1) — the envelope, the + single-flip routing model, the `session` carrier that holds + `pipeline_stages`. +- *Intent and Entity Registration Bus Contract* (OVOS-INTENT-4) — + the registration wire format plugins consume (when they choose + to). +- *Intent Definition Specification* (OVOS-INTENT-3) — the intent + concept and the orchestrator role. From a1e5561ab8b1b912c9aa6052072437be77c23cce Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 04:14:36 +0100 Subject: [PATCH 02/57] pipeline: keep session.pipeline; constrain owner_id colon rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert field rename: keep `session.pipeline` (not `pipeline_stages`). The legacy name stays; no need to break observers for a clarity-only edit. - Dispatch topic `:` (§7): clarify that the split is at the FIRST `:`. skill_id and pipeline_id MUST NOT contain `:`; intent_name MAY contain further `:` so handlers can namespace dispatched topics inside their own surface. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pipeline.md b/pipeline.md index 7139cfd..65f8fae 100644 --- a/pipeline.md +++ b/pipeline.md @@ -37,7 +37,7 @@ This specification defines: - the **pipeline plugin** abstraction (§3) — the only thing the orchestrator iterates; - the **match contract** (§4) — the only thing a plugin exposes; -- the **`session.pipeline_stages`** field (§5) — how a session +- the **`session.pipeline`** field (§5) — how a session chooses which plugins and in what order; - the **utterance lifecycle** (§6) — entry, transformer chain, iteration, dispatch, terminal events; @@ -70,7 +70,7 @@ It does **not** define: registrations on the bus; whether and how a given plugin subscribes is the plugin's own business. - **the `session` lifecycle** — `session` is carried opaquely per - OVOS-MSG-1 §4. `session.pipeline_stages` is one internal field + OVOS-MSG-1 §4. `session.pipeline` is one internal field this spec prescribes (§5); other internal fields are deferred to a future session specification. - **per-plugin behavioural specs** — plugins have no behavioural @@ -216,17 +216,17 @@ it to the dispatched handler. --- -## 5. `session.pipeline_stages` +## 5. `session.pipeline` The session (OVOS-MSG-1 §4) carries an ordered list of pipeline -identifiers under the field name **`pipeline_stages`**: +identifiers under the field name **`pipeline`**: ```json { "session": { "session_id": "default", "lang": "en-US", - "pipeline_stages": [ + "pipeline": [ "padatious-high", "adapt-high", "padatious-medium", @@ -239,25 +239,25 @@ identifiers under the field name **`pipeline_stages`**: } ``` -`pipeline_stages` is a normative internal field inside `session` +`pipeline` is a normative internal field inside `session` prescribed by this specification (analogous to `session_id` and `lang` from OVOS-MSG-1 §4). Other internal session fields remain opaque (deferred to a future session specification). -For each utterance, the orchestrator iterates `pipeline_stages` +For each utterance, the orchestrator iterates `pipeline` in order, calling `match` on each corresponding plugin (§6.2). -If a `pipeline_id` in `pipeline_stages` does not correspond to +If a `pipeline_id` in `pipeline` does not correspond to any loaded plugin, the orchestrator **MUST** skip it and **SHOULD** log a warning. It **MUST NOT** abort the utterance over an unknown identifier. -If `session.pipeline_stages` is absent or empty, the orchestrator +If `session.pipeline` is absent or empty, the orchestrator **MAY** fall back to a deployment-configured default. If no default is configured, the utterance proceeds to no-match (`complete_intent_failure`, §9.4). -Different sessions may carry different `pipeline_stages`. This +Different sessions may carry different `pipeline`. This is how a deployment provides different behaviour to different participants — for example, a remote-peer session may carry a restricted pipeline that excludes destructive plugins. @@ -282,9 +282,9 @@ recognizer_loop:utterance ← entry (§9.1) │ ovos.utterance.handled (§9.6) │ STOP │ - ├─ session retrieval; pipeline_stages read from session (§5) + ├─ session retrieval; pipeline read from session (§5) │ - ├─ for pipeline_id in session.pipeline_stages: + ├─ for pipeline_id in session.pipeline: │ plugin = loaded_plugins[pipeline_id] # skip if not loaded │ match = plugin.match(utterance, session) │ if match is not None: @@ -303,7 +303,7 @@ recognizer_loop:utterance ← entry (§9.1) For each utterance, the orchestrator **MUST**: -- iterate `session.pipeline_stages` in order; +- iterate `session.pipeline` in order; - for each `pipeline_id`, call `match` on the corresponding loaded plugin (skipping unknown identifiers, §5); - stop at the **first plugin** that returns a non-`None` `Match`; @@ -356,6 +356,12 @@ on the topic: where `` is `Match.owner_id` (a `skill_id` or a `pipeline_id`) and `` is `Match.intent_name`. +The delimiter is the **first** `:` in the topic. `skill_id` and +`pipeline_id` **MUST NOT** contain `:` so the split is +unambiguous; `intent_name` MAY contain further `:` characters +(downstream consumers can use it to namespace dispatched topics +inside their own surface). + ### 7.1 Routing and payload The dispatch Message's `context` (OVOS-MSG-1 §4): @@ -643,7 +649,7 @@ contract of §4 applies to pipeline plugins only. - if any transformer set `context["canceled"] = true`, emit `ovos.utterance.cancelled` and **MUST NOT** iterate the pipeline (§10.2); -- iterate `session.pipeline_stages` in order (§6.2) and stop at +- iterate `session.pipeline` in order (§6.2) and stop at the first plugin returning a non-`None` `Match`; - skip unknown `pipeline_id`s without failing the utterance (§5); - emit `complete_intent_failure` when no plugin claimed (§9.4); @@ -689,7 +695,7 @@ The following are explicitly outside this specification: plugin loading and discovery; transformer discovery and ordering; ASR n-best ranking semantics within plugins; per-plugin behavioural specs; the `session` object's full internal shape beyond -`session_id`, `lang` (OVOS-MSG-1 §4), and `pipeline_stages` +`session_id`, `lang` (OVOS-MSG-1 §4), and `pipeline` (§5). --- @@ -698,7 +704,7 @@ specs; the `session` object's full internal shape beyond - *Bus Message Specification* (OVOS-MSG-1) — the envelope, the single-flip routing model, the `session` carrier that holds - `pipeline_stages`. + `pipeline`. - *Intent and Entity Registration Bus Contract* (OVOS-INTENT-4) — the registration wire format plugins consume (when they choose to). From 75d917dc324012c4ce83f76f0684469f45cead31 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 04:16:52 +0100 Subject: [PATCH 03/57] =?UTF-8?q?pipeline=20=C2=A77:=20forbid=20`:`=20in?= =?UTF-8?q?=20skill=5Fid=20/=20pipeline=5Fid=20/=20intent=5Fname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simpler than first-colon-wins. The dispatch topic `:` now contains exactly one `:`, so the split is trivially unambiguous and there is no edge case to specify. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pipeline.md b/pipeline.md index 65f8fae..e76f850 100644 --- a/pipeline.md +++ b/pipeline.md @@ -356,11 +356,9 @@ on the topic: where `` is `Match.owner_id` (a `skill_id` or a `pipeline_id`) and `` is `Match.intent_name`. -The delimiter is the **first** `:` in the topic. `skill_id` and -`pipeline_id` **MUST NOT** contain `:` so the split is -unambiguous; `intent_name` MAY contain further `:` characters -(downstream consumers can use it to namespace dispatched topics -inside their own surface). +`skill_id`, `pipeline_id`, and `intent_name` **MUST NOT** contain +`:`. The dispatch topic therefore contains exactly one `:`, and +the split is unambiguous. ### 7.1 Routing and payload From da77bcaa32c6f6b4e478a9919fe6e2e084fabf61 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 04:31:46 +0100 Subject: [PATCH 04/57] PIPELINE-1: release as v2 (incompatible with current OVOS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the draft-stage versioning policy, v1 is reserved for content drop-in compatible with current OVOS. PIPELINE-1 adds the orchestrator passive registration index and normalizes the universal `ovos.utterance.handled` end-marker across all terminal paths (current workshop misses it on the error path) — both require OVOS-side changes, so the first release ships as v2. Also update the CHANGELOG bullet that still referenced the defunct `session.pipeline_stages` rename — kept as `session.pipeline`. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 18 +++++++++++++----- README.md | 2 +- pipeline.md | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f69634..b250490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,12 +79,20 @@ tool does not recognize the token and cannot expand the template. ## OVOS-PIPELINE-1 — Utterance Lifecycle and Pipeline -### 1 +### 2 -- Initial draft. Defines the **orchestrator** and the - **pipeline plugin** abstraction: opaque-`pipeline_id` black - boxes the orchestrator iterates in `session.pipeline_stages` - order per utterance, first-match-wins. Plugins expose one +- Initial draft. Released as **v2** rather than v1: per the + draft-stage versioning policy, v1 is reserved for content + drop-in compatible with current OVOS. PIPELINE-1 introduces a + new orchestrator responsibility (the passive registration index + backing `ovos.intent.list` / `.describe`) and normalizes the + universal `ovos.utterance.handled` end-marker on every terminal + path — both require OVOS-side changes, so the first release is + v2. +- Defines the **orchestrator** and the **pipeline plugin** + abstraction: opaque-`pipeline_id` black boxes the orchestrator + iterates in `session.pipeline` order per utterance, + first-match-wins. Plugins expose one operation — `match(utterance, session) → Match | None`, side-effect-free; the orchestrator handles dispatch, notifications, and terminal events. diff --git a/README.md b/README.md index c0978f5..f9764b6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ below). Adoption is voluntary; conformance, once adopted, is not. | OVOS-INTENT-2 | [Locale Resource Formats](locale-resource-formats.md) | 2 | Draft | | OVOS-INTENT-3 | [Intent Definition](intent-definition.md) | 1 | Draft | | OVOS-MSG-1 | [Bus Message](message-object.md) | 1 | Draft | -| OVOS-PIPELINE-1 | [Utterance Lifecycle and Pipeline](pipeline.md) | 1 | Draft | +| OVOS-PIPELINE-1 | [Utterance Lifecycle and Pipeline](pipeline.md) | 2 | Draft | Each spec carries its own scope statement, design rationale, and conformance section in its own header. Open the document for the diff --git a/pipeline.md b/pipeline.md index e76f850..2524714 100644 --- a/pipeline.md +++ b/pipeline.md @@ -1,6 +1,6 @@ # Utterance Lifecycle and Pipeline Specification -**Spec ID:** OVOS-PIPELINE-1 · **Version:** 1 · **Status:** Draft +**Spec ID:** OVOS-PIPELINE-1 · **Version:** 2 · **Status:** Draft This document defines the **utterance lifecycle** — the path an utterance takes from the moment it enters the assistant to the moment From d87c1f17a0c62f18696ebb2c455ab4e690af2821 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 04:43:32 +0100 Subject: [PATCH 05/57] pipeline: drop transformer chain (out of scope); refine V2 rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related cleanups: 1. **Remove the transformer chain from the normative spec.** Transformers are pre-pipeline message modifiers — they don't match, they don't dispatch, they only mutate the message or cancel it. Their loading/ordering/contract is a separate concern and doesn't need to ride along with the pipeline plugin spec. §10 deleted; §6.1 flow simplified to two terminal paths (matched / no-match); §6.4 terminal-events table loses the cancelled row; §9.3 (`ovos.utterance.cancelled`) deleted and §9.4–§9.6 renumbered to §9.3–§9.5; §11 conformance transformer block deleted and §11 renumbered to §10. Non-goals list updated to explicitly exclude any pre-pipeline utterance- transformer chain. 2. **Refine the V2 rationale** in the CHANGELOG to match the V0/V1/V2 framing: the trigger for V2 is the handler-lifecycle rename (mycroft.skill.handler.* → ovos.intent.handler.*) which actively breaks observers of the legacy names. The passive registration index and the universal `ovos.utterance.handled` end-marker would have been V1-compatible on their own — missing them degrades experience (empty introspection, missed end-marker on workshop's error path) but doesn't break V0 producers/consumers. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 36 ++++++++-------- pipeline.md | 114 +++++++++++---------------------------------------- 2 files changed, 43 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b250490..7fe7703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,36 +81,38 @@ tool does not recognize the token and cannot expand the template. ### 2 -- Initial draft. Released as **v2** rather than v1: per the - draft-stage versioning policy, v1 is reserved for content - drop-in compatible with current OVOS. PIPELINE-1 introduces a - new orchestrator responsibility (the passive registration index - backing `ovos.intent.list` / `.describe`) and normalizes the - universal `ovos.utterance.handled` end-marker on every terminal - path — both require OVOS-side changes, so the first release is - v2. +- Initial draft. Released as **V2** rather than V1 per the + draft-stage versioning policy: a V1 spec must be adoptable + without breaking V0 (current OVOS). PIPELINE-1 renames the + handler-lifecycle trio (`mycroft.skill.handler.start` / + `.complete` / `.error` → `ovos.intent.handler.*`) — existing + observers of the legacy names break under that rename, so the + spec is V2. The other prescribed behaviours (orchestrator + passive registration index, universal `ovos.utterance.handled` + on every terminal path) would, on their own, have been + V1-compatible: missing them only degrades experience + (introspection returns empty; the workshop error path lacks + the end-marker) without breaking V0 producers or consumers. - Defines the **orchestrator** and the **pipeline plugin** abstraction: opaque-`pipeline_id` black boxes the orchestrator iterates in `session.pipeline` order per utterance, - first-match-wins. Plugins expose one - operation — `match(utterance, session) → Match | None`, - side-effect-free; the orchestrator handles dispatch, - notifications, and terminal events. + first-match-wins. Plugins expose one operation — + `match(utterance, session) → Match | None`, side-effect-free; + the orchestrator handles dispatch, notifications, and terminal + events. - Dispatch topic: `:` where `owner_id` is either a `skill_id` (skill-owned handler) or a `pipeline_id` (plugin-bundled handler). Plugins and skills are equivalent handler owners from the bus's perspective. - Utterance-layer events: `recognizer_loop:utterance` (entry), `ovos.intent.matched` (positive match notification), - `ovos.utterance.cancelled` (transformer cancellation), `complete_intent_failure` (no plugin claimed), `ovos.utterance.handled` (universal end-marker, fires on every terminal path). - Handler-lifecycle trio: `ovos.intent.handler.start` / `.complete` / `.error`, emitted by whoever runs the handler (skill or plugin). -- Transformer chain: pre-pipeline modification or cancellation - via `context["canceled"]`. - Per-plugin behavioural contracts (converse, fallback, - common_query, persona, language-model plugins, etc.) are out - of scope — plugins are black boxes; each defines itself. + common_query, persona, language-model plugins, etc.) and any + pre-pipeline utterance-transformer chain are out of scope — + plugins are black boxes; each defines itself. diff --git a/pipeline.md b/pipeline.md index 2524714..dd1b6ac 100644 --- a/pipeline.md +++ b/pipeline.md @@ -39,18 +39,15 @@ This specification defines: - the **match contract** (§4) — the only thing a plugin exposes; - the **`session.pipeline`** field (§5) — how a session chooses which plugins and in what order; -- the **utterance lifecycle** (§6) — entry, transformer chain, - iteration, dispatch, terminal events; +- the **utterance lifecycle** (§6) — entry, iteration, dispatch, + terminal events; - the **dispatch** topic shape (§7) — `:`; - the **handler-lifecycle trio** (§8) — `ovos.intent.handler.start` / `.complete` / `.error`; - the **utterance-layer bus events** (§9) — `recognizer_loop:utterance`, `ovos.intent.matched`, - `ovos.utterance.cancelled`, `complete_intent_failure`, - `ovos.utterance.handled`; -- the **transformer chain** (§10) — pre-pipeline modification and - cancellation; -- **conformance** (§11). + `complete_intent_failure`, `ovos.utterance.handled`; +- **conformance** (§10). It does **not** define: @@ -255,7 +252,7 @@ over an unknown identifier. If `session.pipeline` is absent or empty, the orchestrator **MAY** fall back to a deployment-configured default. If no default is configured, the utterance proceeds to no-match -(`complete_intent_failure`, §9.4). +(`complete_intent_failure`, §9.3). Different sessions may carry different `pipeline`. This is how a deployment provides different behaviour to different @@ -269,18 +266,12 @@ restricted pipeline that excludes destructive plugins. Every utterance flows through the same lifecycle, regardless of which plugin (if any) claims it. The lifecycle is **guaranteed to terminate** with exactly one `ovos.utterance.handled` event -(§9.6). +(§9.5). ### 6.1 The flow ``` recognizer_loop:utterance ← entry (§9.1) - │ - ├─ transformer chain (§10) - │ └─ if any transformer set context["canceled"] = true: - │ ovos.utterance.cancelled (§9.3) - │ ovos.utterance.handled (§9.6) - │ STOP │ ├─ session retrieval; pipeline read from session (§5) │ @@ -291,12 +282,12 @@ recognizer_loop:utterance ← entry (§9.1) │ ovos.intent.matched (§9.2) │ dispatch on : (§7) │ (handler runs; emits lifecycle trio §8) - │ ovos.utterance.handled (§9.6) + │ ovos.utterance.handled (§9.5) │ break │ └─ if no plugin matched: - complete_intent_failure (§9.4) - ovos.utterance.handled (§9.6) + complete_intent_failure (§9.3) + ovos.utterance.handled (§9.5) ``` ### 6.2 First-match-wins iteration @@ -308,7 +299,7 @@ For each utterance, the orchestrator **MUST**: plugin (skipping unknown identifiers, §5); - stop at the **first plugin** that returns a non-`None` `Match`; - if no plugin returns a `Match`, emit `complete_intent_failure` - (§9.4). + (§9.3). A plugin that raises an exception during `match` is treated as if it returned `None`. The orchestrator **MUST** continue to the next @@ -326,12 +317,11 @@ keyed on `session.session_id` (per OVOS-MSG-1 §5.4 — ### 6.4 Terminal events -Every utterance terminates in exactly one of three ways, each +Every utterance terminates in exactly one of two ways, each followed by the universal end-marker `ovos.utterance.handled`: | Outcome | Sequence of utterance-layer events | |---------|------------------------------------| -| Cancelled by transformer | `ovos.utterance.cancelled` → `ovos.utterance.handled` | | Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.handled` | | No plugin matched | `complete_intent_failure` → `ovos.utterance.handled` | @@ -489,7 +479,7 @@ The orchestrator **MAY** wait for `.complete` or `.error` matching the dispatched `(owner_id, intent_name, session)` for a deployment-defined time bound. If neither arrives within the bound, the orchestrator **MUST** still emit -`ovos.utterance.handled` (§9.6) to satisfy the universal +`ovos.utterance.handled` (§9.5) to satisfy the universal end-marker invariant. It **MUST NOT** synthesize a `.error` of its own — error events come from the handler that owns the trio. @@ -555,35 +545,25 @@ Consumers **MUST NOT** treat receipt as permission or instruction to run a handler — handler invocation happens via the dispatch topic (§7). -### 9.3 `ovos.utterance.cancelled` - -Emitted by the orchestrator when a transformer requested -cancellation (§10.2). Broadcast. Payload **MAY** carry -transformer-supplied metadata; this specification does not -normatively define its shape. - -This message **MUST** be followed immediately by -`ovos.utterance.handled` (§9.6). - -### 9.4 `complete_intent_failure` +### 9.3 `complete_intent_failure` Emitted by the orchestrator when pipeline iteration completed with no plugin claiming the utterance. Broadcast. Payload **MAY** carry the original utterance data for observability. This message **MUST** be followed immediately by -`ovos.utterance.handled` (§9.6). +`ovos.utterance.handled` (§9.5). This is the **intent-layer failure** signal. It is distinct from a handler-layer error (§8): `complete_intent_failure` means "no plugin claimed"; `ovos.intent.handler.error` means "a handler ran and raised." -### 9.5 The dispatch topic +### 9.4 The dispatch topic `:` — see §7. -### 9.6 `ovos.utterance.handled` +### 9.5 `ovos.utterance.handled` The **universal end-marker** for an utterance. Emitted by the orchestrator on every terminal path — cancellation, no-match, @@ -599,58 +579,19 @@ malformed. --- -## 10. The transformer chain - -Before pipeline iteration, the orchestrator **MAY** run an ordered -chain of **transformers** that can modify the utterance, modify -its `message.context`, or request cancellation. - -This specification gives a minimum contract for transformers; -their loading and ordering are deployment concerns. - -### 10.1 Two transformer roles - -- **Utterance transformers** — may modify the list of utterances - (e.g. punctuation cleanup, profanity filtering, normalization). -- **Metadata transformers** — may modify `message.context` - (e.g. classifying speaker identity, adding tracing identifiers). - -A transformer in either role **MAY** request cancellation of the -utterance by setting `message.context["canceled"] = true`. - -### 10.2 Cancellation semantics - -If any transformer sets `message.context["canceled"] = true`, the -orchestrator **MUST**: - -- not iterate the pipeline for this utterance; -- emit `ovos.utterance.cancelled` (§9.3); -- emit `ovos.utterance.handled` (§9.6). - -### 10.3 Transformer chain is not a plugin - -Transformers run *before* the pipeline. They do not return a -match; they only modify the message (or cancel it). The match -contract of §4 applies to pipeline plugins only. - ---- - -## 11. Conformance +## 10. Conformance ### An **orchestrator** **MUST**: - subscribe to `recognizer_loop:utterance` (§9.1); - run every received utterance through the lifecycle of §6 exactly once; -- emit `ovos.utterance.handled` (§9.6) exactly once per +- emit `ovos.utterance.handled` (§9.5) exactly once per utterance, regardless of which terminal path was taken; -- if any transformer set `context["canceled"] = true`, emit - `ovos.utterance.cancelled` and **MUST NOT** iterate the - pipeline (§10.2); - iterate `session.pipeline` in order (§6.2) and stop at the first plugin returning a non-`None` `Match`; - skip unknown `pipeline_id`s without failing the utterance (§5); -- emit `complete_intent_failure` when no plugin claimed (§9.4); +- emit `complete_intent_failure` when no plugin claimed (§9.3); - emit `ovos.intent.matched` (§9.2) on every successful claim, before the dispatch; - dispatch on `:` per §7; @@ -680,21 +621,14 @@ contract of §4 applies to pipeline plugins only. (§8.2); - run the handler at most once per dispatch. -### A **transformer** **MUST**: - -- modify only the utterance list and / or `message.context`; -- request cancellation via `context["canceled"] = true` if and - only if the utterance is to be dropped (§10.2); -- be side-effect-free beyond its returned modifications. - ### Non-goals The following are explicitly outside this specification: plugin -loading and discovery; transformer discovery and ordering; ASR -n-best ranking semantics within plugins; per-plugin behavioural -specs; the `session` object's full internal shape beyond -`session_id`, `lang` (OVOS-MSG-1 §4), and `pipeline` -(§5). +loading and discovery; any pre-pipeline utterance transformation +or cancellation chain; ASR n-best ranking semantics within +plugins; per-plugin behavioural specs; the `session` object's +full internal shape beyond `session_id`, `lang` (OVOS-MSG-1 §4), +and `pipeline` (§5). --- From 2418b1427bbfe38a0ebc2e41c53cb28b6a669e0f Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 04:50:51 +0100 Subject: [PATCH 06/57] =?UTF-8?q?README:=20PIPELINE-1=20reading-order=20?= =?UTF-8?q?=E2=80=94=20host-side=20=E2=86=92=20orchestrator-side;=20drop?= =?UTF-8?q?=20transformers=20mention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9764b6..371e944 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ order: OVOS-INTENT-1 defines the template grammar; OVOS-INTENT-2 builds on it to define the resource files; OVOS-INTENT-3 builds on both to define what an intent is. OVOS-MSG-1 is the bus-layer envelope and the routing / session model — readable at any point. -OVOS-PIPELINE-1 is the host-side spec: it defines the utterance -lifecycle that wraps everything else (transformers, ordered stages, +OVOS-PIPELINE-1 is the orchestrator-side spec: it defines the utterance +lifecycle that wraps everything else (ordered stages, terminal events) and builds on OVOS-MSG-1. For background — design rationale, comparisons with other systems, From 1954c771384c846be3d1d63319b44eb01358230b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 18:25:52 +0100 Subject: [PATCH 07/57] scope: drop APPENDIX/CHANGELOG/README; PR #11 is pipeline.md only per 1-file rule --- APPENDIX.md | 88 +++++++++++++++++++++------------------------------- CHANGELOG.md | 40 ------------------------ README.md | 7 ++--- 3 files changed, 38 insertions(+), 97 deletions(-) diff --git a/APPENDIX.md b/APPENDIX.md index 6746688..0f9fc71 100644 --- a/APPENDIX.md +++ b/APPENDIX.md @@ -122,28 +122,26 @@ engine-agnostic contract and the pipeline. --- -## 3. The pipeline — the host-side spec - -The intent specs (OVOS-INTENT-1/2/3/4) formalize **intent definition -and delivery**: the grammar, the resource files, what an intent is, -the intent-engine contract, and the bus messages that carry -registration, match, and dispatch. OVOS-MSG-1 formalizes the bus -that carries them. **OVOS-PIPELINE-1** formalizes the -host-side piece that sits *around* all of those — the utterance -lifecycle, the transformer chain, the ordered pipeline of stages -(intent engines, `converse`, `fallback`, `common_query`, `ocp`, -`persona`, …), the confidence-tier convention, and the universal -`ovos.utterance.handled` end-marker. - -What PIPELINE-1 leaves out by design: the **internal behaviour of -non-intent stages**. Each of `converse`, `fallback`, `common_query`, -`ocp`, `persona`, `stop` is the natural subject of its own future -specification — PIPELINE-1 only defines the contract every stage -conforms to. See §7 known gaps for the list. - -One observation worth flagging: **the engine-agnostic intent -contract is already realized**, not hypothetical. `ovos-persona` -plugs into the pipeline as a first-class LLM stage (`persona-high`, +## 3. The pipeline — what these specs do not cover + +The intent specs (OVOS-INTENT-1/2/3) formalize **intent definition**: +the grammar, the resource files, what an intent is, the intent-engine +contract. OVOS-MSG-1 formalizes the bus that carries the result. +The piece that sits *around* both — the multi-stage **pipeline** that +decides which intent engine even gets a turn, interleaves +confidence tiers, runs `converse` / `fallback` / `common_query` / +`ocp` / `persona` stages, and produces the universal +`ovos.utterance.handled` end-marker — is not formalized by any spec +in this repository yet. + +That gap is what makes OVOS structurally distinctive (HA and Rhasspy +have no equivalent layer), and what most reviewers ask about +first. The natural next formalization is a pipeline / utterance- +lifecycle specification; see §7 known gaps. + +One observation worth flagging here: **the engine-agnostic intent +contract is already realized**, not hypothetical. `ovos-persona` plugs +into the pipeline as a first-class LLM stage (`persona-high`, `persona-low`) — the OVOS-INTENT-3 §6.2 non-normative note about LLM-backed engines describes something that ships today. The ordered confidence-tier chain (deterministic Adapt before fuzzy @@ -437,12 +435,9 @@ current code: (`mycroft.skill.handler.{start,complete,error}` etc.) are still informal. The natural next bus spec is OVOS-INTENT-4, which builds on OVOS-MSG-1 + OVOS-INTENT-3. -- **Per-stage behavioural specs.** OVOS-PIPELINE-1 defines the - stage *contract* (the `match` signature, the `StageMatch` shape, - the confidence-tier convention) but explicitly defers what each - non-intent stage actually *does*. `converse`, `fallback`, - `common_query`, `ocp`, `persona`, `stop` are each natural - subjects of their own specifications. +- **A pipeline specification.** Stage ordering, the confidence-tier + model, and the contracts for `converse`, `fallback`, + `common_query`, `ocp`, and `persona` stages are unspecified (§3). - **A session specification.** MSG-1 §4 carries `session` opaquely and names only `session_id` and `lang`. Everything else about the session is deferred — see §5.2 for the explicit list: session @@ -510,7 +505,7 @@ the *why*, but that has no place in a normative document. ### 9.1 The set, in two stacks -Built bottom-up in three stacks: +Built bottom-up in two stacks: - The **intent stack**, in dependency order: OVOS-INTENT-1 (template grammar) → OVOS-INTENT-2 (resource files built on it) → @@ -521,13 +516,6 @@ Built bottom-up in three stacks: Originally drafted as two specs (envelope + session/routing) and merged once it became clear the derivations could only meaningfully be defined where the routing keys lived. -- The **host stack**, sitting around the intent and bus stacks: - OVOS-PIPELINE-1 formalizes the utterance lifecycle, the - transformer chain, the ordered pipeline of stages (intent and - non-intent), the confidence-tier convention, and the universal - `ovos.utterance.handled` end-marker. It builds on OVOS-MSG-1 - and provides the host context that all the other specs operate - inside. Each was a formalization pass over machinery already running in production (§1), not a greenfield design. @@ -588,19 +576,15 @@ A specification that does not change between levels keeps its lower version number — OVOS-INTENT-3 is at version 1 in both V1 and V2. -### How the bus and host stacks will be layered in - -OVOS-MSG-1 introduces the bus envelope; OVOS-INTENT-4 introduces -intent registration and dispatch on top of it; OVOS-PIPELINE-1 -introduces the host-side utterance lifecycle. All three are -structurally orthogonal to the intent grammar/resource stack — a -tool can implement either without the other. As a result the -single-axis V0/V1/V2 ladder above is no longer sufficient to -describe the architecture as a whole. - -The compatibility-level model is expected to evolve into either a -multi-axis grid or per-stack ladders. Until that's settled, the -bus and host specs (OVOS-MSG-1, OVOS-INTENT-4, OVOS-PIPELINE-1) -are versioned individually but not yet placed on a compatibility -ladder. Implementers targeting them today should cite each spec's -own `Version` field rather than a compat level. +### How the bus stack will be layered in + +OVOS-MSG-1 introduces the bus envelope, which is structurally +orthogonal to the intent stack — a tool can implement the intent +stack without the bus envelope and vice versa. As more bus-layer +specs land, the compatibility-level model is expected to evolve; +the current V0–V2 ladder may grow a second axis or be replaced +with per-stack ladders. + +Until that's settled, the bus-layer specs (OVOS-MSG-1 and the +others in the pipeline behind it) are versioned individually but +not yet placed on a compatibility ladder. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fe7703..c9625fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,43 +76,3 @@ tool does not recognize the token and cannot expand the template. authentication, authorization, retry, delivery and ordering guarantees, session lifecycle, and the internal shape of `session` beyond `session_id` and `lang` are explicitly out of scope. - -## OVOS-PIPELINE-1 — Utterance Lifecycle and Pipeline - -### 2 - -- Initial draft. Released as **V2** rather than V1 per the - draft-stage versioning policy: a V1 spec must be adoptable - without breaking V0 (current OVOS). PIPELINE-1 renames the - handler-lifecycle trio (`mycroft.skill.handler.start` / - `.complete` / `.error` → `ovos.intent.handler.*`) — existing - observers of the legacy names break under that rename, so the - spec is V2. The other prescribed behaviours (orchestrator - passive registration index, universal `ovos.utterance.handled` - on every terminal path) would, on their own, have been - V1-compatible: missing them only degrades experience - (introspection returns empty; the workshop error path lacks - the end-marker) without breaking V0 producers or consumers. -- Defines the **orchestrator** and the **pipeline plugin** - abstraction: opaque-`pipeline_id` black boxes the orchestrator - iterates in `session.pipeline` order per utterance, - first-match-wins. Plugins expose one operation — - `match(utterance, session) → Match | None`, side-effect-free; - the orchestrator handles dispatch, notifications, and terminal - events. -- Dispatch topic: `:` where `owner_id` is - either a `skill_id` (skill-owned handler) or a `pipeline_id` - (plugin-bundled handler). Plugins and skills are equivalent - handler owners from the bus's perspective. -- Utterance-layer events: `recognizer_loop:utterance` (entry), - `ovos.intent.matched` (positive match notification), - `complete_intent_failure` (no plugin claimed), - `ovos.utterance.handled` (universal end-marker, fires on every - terminal path). -- Handler-lifecycle trio: `ovos.intent.handler.start` / `.complete` - / `.error`, emitted by whoever runs the handler (skill or - plugin). -- Per-plugin behavioural contracts (converse, fallback, - common_query, persona, language-model plugins, etc.) and any - pre-pipeline utterance-transformer chain are out of scope — - plugins are black boxes; each defines itself. diff --git a/README.md b/README.md index 371e944..74749e3 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,6 @@ below). Adoption is voluntary; conformance, once adopted, is not. | OVOS-INTENT-2 | [Locale Resource Formats](locale-resource-formats.md) | 2 | Draft | | OVOS-INTENT-3 | [Intent Definition](intent-definition.md) | 1 | Draft | | OVOS-MSG-1 | [Bus Message](message-object.md) | 1 | Draft | -| OVOS-PIPELINE-1 | [Utterance Lifecycle and Pipeline](pipeline.md) | 2 | Draft | Each spec carries its own scope statement, design rationale, and conformance section in its own header. Open the document for the @@ -73,10 +72,8 @@ full picture — the table above is just an index. order: OVOS-INTENT-1 defines the template grammar; OVOS-INTENT-2 builds on it to define the resource files; OVOS-INTENT-3 builds on both to define what an intent is. OVOS-MSG-1 is the bus-layer -envelope and the routing / session model — readable at any point. -OVOS-PIPELINE-1 is the orchestrator-side spec: it defines the utterance -lifecycle that wraps everything else (ordered stages, -terminal events) and builds on OVOS-MSG-1. +envelope and the routing / session model — independent of the +intent stack and readable at any point. For background — design rationale, comparisons with other systems, the catalogue of known divergences from current code, and known From 0a80a29cd735946bf7eff8f21f843fe7688faff7 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 19:01:51 +0100 Subject: [PATCH 08/57] OVOS-PIPELINE-1: orchestrator may be split across cooperating processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §2 — the orchestrator is the logical role; it MAY be implemented as a single process or as multiple cooperating processes (a natural split runs audio-input / utterance-handling / audio-output as separate services). From the spec's perspective those processes together are "the orchestrator"; the split is a deployment / containerization choice. Pipeline plugins, the loaded-plugin set, and the match contract live in the orchestrator process that implements the utterance lifecycle (utterance-handling under the audio-boundary split). Generic voice-OS framing (no "current OVOS does X" wording in the spec body). Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pipeline.md b/pipeline.md index dd1b6ac..175d13f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -79,13 +79,26 @@ It does **not** define: ## 2. The orchestrator and the pipeline plugin -The **orchestrator** (OVOS-INTENT-3 §6.1) is the single component -that consumes the utterance entry point `recognizer_loop:utterance`, +The **orchestrator** (OVOS-INTENT-3 §6.1) is the logical role that +consumes the utterance entry point `recognizer_loop:utterance`, iterates plugins per session, emits dispatch and terminal events, and guarantees the universal end-marker `ovos.utterance.handled`. The orchestrator is distinct from the **messagebus** (the transport layer) and from any individual plugin. +The orchestrator MAY be implemented as a single process or as +multiple cooperating processes — a natural split along the audio +boundary runs an audio-input service (mic, STT), an utterance- +handling service (the pipeline and intent matching specified +here), and an audio-output service (TTS, playback) as separate +processes. From this specification's perspective those processes +together are "the orchestrator"; the split is a deployment / +containerization choice the spec accommodates but does not +prescribe. Pipeline plugins, the loaded-plugin set, and the match +contract of §4 live in the orchestrator process that implements +the utterance lifecycle (the utterance-handling service in the +split shape above). + A **pipeline plugin** is a third-party component identified by an opaque `pipeline_id` — an arbitrary, deployment-unique string. The orchestrator loads some number of plugins at startup; how it From e60a110ae1ad6aa7eaa69d7a44d0ee5e152d4959 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 20:08:46 +0100 Subject: [PATCH 09/57] PIPELINE-1: SESSION-1 / MSG-1 hoists, trio MUST, per-pipeline introspection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §3 pipeline_id: drop colon-rule restatement; reference MSG-1 §2.1.1. §5 session.pipeline: drop field restatement; reference SESSION-1 §2.1 claim and §2.5 deployment-default fallback. Tighten partial- unknown rule (orchestrator MUST NOT fall back to deployment default merely because one identifier is unknown). §7 dispatch topic: drop colon-rule restatement; reference MSG-1 §2.1.1. §8.1 handler trio: tighten from SHOULD to MUST — handler MUST emit exactly one terminal event (complete / error). start stays SHOULD. Reason: §9.5 universal end-marker and §8.3 timeout bookkeeping depend on terminal event being deterministic. §10 (new): per-pipeline_id intent introspection. Pull-query topic ovos.pipeline..intents.list with scatter-response pattern. No aggregate query — consumers walk per-pipeline. Pull-query is source of truth; load-time broadcasts are MAY, consumers MUST NOT rely on them. §11 (renumbered) conformance: handler MUST emit terminal event (was SHOULD); pipeline plugin MUST respond to per-pipeline_id introspection queries; orchestrator MUST NOT synthesize trio events. Non-goals: session shape moved to SESSION-1. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 217 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 165 insertions(+), 52 deletions(-) diff --git a/pipeline.md b/pipeline.md index 175d13f..5c4b2ca 100644 --- a/pipeline.md +++ b/pipeline.md @@ -145,11 +145,12 @@ not interpret the `pipeline_id` string beyond using it as a key. Constraints on `pipeline_id` strings: - Non-empty. -- Must not contain a colon (`:`) — the colon separates owner from - intent name in the dispatch topic shape (§7), and `pipeline_id` - may appear as the owner. -- Must match the topic-name syntax of OVOS-MSG-1 §2.1 (ASCII - letters, digits, `.`, `_`, `-`; no whitespace). +- Bound by OVOS-MSG-1 §2.1.1 (identifiers used as topic + components): no `:`, no `.`, no whitespace, ASCII letters / + digits / `_` / `-` only. The constraint is necessary because + `pipeline_id` may appear as the owner in the dispatch topic shape + `:` (§7) and per-pipeline introspection + topics (§10) build on the same identifier. - Unique within a deployment's loaded-plugin set. A plugin **MAY** appear in a session's pipeline more than once @@ -228,14 +229,18 @@ it to the dispatched handler. ## 5. `session.pipeline` -The session (OVOS-MSG-1 §4) carries an ordered list of pipeline -identifiers under the field name **`pipeline`**: +This specification claims **`pipeline`** as a session field per +OVOS-SESSION-1 §2.1: an array of `pipeline_id` strings, ordered, +session-scoped, propagating with the session, with the +deployment-default-fallback absence rule (an omitted, empty, or +absent `session.pipeline` resolves to the deployment-configured +default at consumption, per OVOS-SESSION-1 §2.5). + +Example: ```json { "session": { - "session_id": "default", - "lang": "en-US", "pipeline": [ "padatious-high", "adapt-high", @@ -249,28 +254,25 @@ identifiers under the field name **`pipeline`**: } ``` -`pipeline` is a normative internal field inside `session` -prescribed by this specification (analogous to `session_id` and -`lang` from OVOS-MSG-1 §4). Other internal session fields remain -opaque (deferred to a future session specification). - -For each utterance, the orchestrator iterates `pipeline` +For each utterance, the orchestrator iterates `session.pipeline` in order, calling `match` on each corresponding plugin (§6.2). -If a `pipeline_id` in `pipeline` does not correspond to -any loaded plugin, the orchestrator **MUST** skip it and -**SHOULD** log a warning. It **MUST NOT** abort the utterance -over an unknown identifier. +If a `pipeline_id` in `session.pipeline` does not correspond to +any loaded plugin, the orchestrator **MUST** skip it and **SHOULD** +log a warning. It **MUST NOT** abort the utterance over an unknown +identifier and **MUST NOT** fall back to the deployment default +merely because one identifier is unknown — the remaining known +identifiers are the effective ordered set. -If `session.pipeline` is absent or empty, the orchestrator -**MAY** fall back to a deployment-configured default. If no -default is configured, the utterance proceeds to no-match -(`complete_intent_failure`, §9.3). +If `session.pipeline` is absent or empty (per OVOS-SESSION-1 §2.5), +the orchestrator falls back to the deployment-configured default +pipeline. If no default is configured, the utterance proceeds to +no-match (`complete_intent_failure`, §9.3). -Different sessions may carry different `pipeline`. This -is how a deployment provides different behaviour to different -participants — for example, a remote-peer session may carry a -restricted pipeline that excludes destructive plugins. +Different sessions may carry different `pipeline`. This is how a +deployment provides different behaviour to different participants — +for example, a remote-peer session may carry a restricted pipeline +that excludes destructive plugins. --- @@ -359,9 +361,10 @@ on the topic: where `` is `Match.owner_id` (a `skill_id` or a `pipeline_id`) and `` is `Match.intent_name`. -`skill_id`, `pipeline_id`, and `intent_name` **MUST NOT** contain -`:`. The dispatch topic therefore contains exactly one `:`, and -the split is unambiguous. +The `:` between the two segments is the only one in the topic: +`skill_id`, `pipeline_id`, and `intent_name` are bound by +OVOS-MSG-1 §2.1.1 (no `:`, no `.`, no whitespace), so the split is +unambiguous. ### 7.1 Routing and payload @@ -444,17 +447,26 @@ chains) can subscribe. ### 8.1 Order and obligations -For each accepted dispatch, the handler-owning component -**SHOULD** emit: +For each accepted dispatch, the handler-owning component **MUST** +emit **exactly one** terminal event — either +`ovos.intent.handler.complete` or `ovos.intent.handler.error` — and +**SHOULD** precede it with `ovos.intent.handler.start`. + +- on normal completion: `ovos.intent.handler.start` (SHOULD) followed + by `ovos.intent.handler.complete` (MUST); +- on exception: `ovos.intent.handler.start` (SHOULD) followed by + `ovos.intent.handler.error` (MUST). -- on normal completion: `ovos.intent.handler.start` followed by - `ovos.intent.handler.complete`; -- on exception: `ovos.intent.handler.start` followed by - `ovos.intent.handler.error`. +The terminal event is **MUST** because the orchestrator's universal +`ovos.utterance.handled` invariant (§9.5) and timeout bookkeeping +(§8.3) depend on it: an utterance whose handler emits no terminal +event leaves the orchestrator unable to satisfy §9.5 without +falling back to the §8.3 timeout — a degraded mode that wastes time +and produces observably-late lifecycle for downstream consumers. -A handler that does not emit the trio still ran (the spec cannot -prevent that) but is non-conformant — its execution is invisible -to the bus. +A handler that emits neither terminal event still ran (the spec +cannot prevent that) but is non-conformant — its execution is +invisible to the bus and forces the orchestrator into §8.3. ### 8.2 Payload @@ -592,7 +604,96 @@ malformed. --- -## 10. Conformance +## 10. Per-pipeline introspection + +Each pipeline plugin owns the set of intents it currently has +loaded. To let consumers (UIs, developer tools, debug viewers, +other plugins) discover that set at runtime, this specification +defines a pull-query / scatter-response pattern keyed on +`pipeline_id`. + +### 10.1 Query and response topics + +| Topic | Direction | Carries | +|-------|-----------|---------| +| `ovos.pipeline..intents.list` | request | empty payload (or filters, see §10.3) | +| `ovos.pipeline..intents.list.response` | reply | the plugin's currently-loaded intent set | + +A consumer that wants the loaded intents of a specific pipeline +**MUST** emit on the per-`pipeline_id` topic above. There is **no +aggregate query** — a consumer that wants the intent set of every +loaded plugin emits one query per `pipeline_id` it cares about and +aggregates the responses itself. + +The `pipeline_id` in the topic is the same identifier carried by +`session.pipeline` (§5) and by `Match.owner_id` when the plugin +owns its own handler (§7); a consumer that has already observed +a `pipeline_id` from any of these sources can query it directly. + +### 10.2 Response payload + +The plugin **MUST** reply with the currently-loaded intent set: + +```json +{ + "pipeline_id": "padatious-high", + "intents": [ + { + "intent_name": "play_music", + "owner_id": "music.skill", + "lang": "en-US" + }, + { + "intent_name": "stop_music", + "owner_id": "music.skill", + "lang": "en-US" + } + ] +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `pipeline_id` | string | yes | The responding plugin's id. | +| `intents` | array | yes | Currently-loaded intents (possibly empty). | +| `intents[].intent_name` | string | yes | Intent identifier. | +| `intents[].owner_id` | string | yes | The owning component (`skill_id` or `pipeline_id` when plugin-owned). | +| `intents[].lang` | string | yes | The language the intent is registered for. | + +A plugin **MAY** include additional per-intent fields (engine +metadata, confidence thresholds, sample templates) but consumers +**MUST NOT** require them. + +### 10.3 Filters + +The request payload **MAY** carry filters: + +```json +{ "lang": "en-US", "owner_id": "music.skill" } +``` + +When a filter is present, the plugin **SHOULD** restrict its +response to intents matching every filter field. Unknown filter +keys are ignored (forward-compatible). + +### 10.4 Pull-query is the source of truth + +Pipeline plugins **MAY** broadcast load-time announcements (e.g. +when a skill registers new intents the plugin recompiles), but +consumers that need accurate state **MUST** query +`ovos.pipeline..intents.list` and **MUST NOT** assume +that any prior broadcast reached them. The bus is asynchronous, +has no delivery guarantees, and a consumer that started after a +load event missed the announcement. + +A plugin **MUST** respond to every query it observes for its own +`pipeline_id`. A consumer that receives no response within a +deployment-defined timeout **MAY** retry; persistent silence +indicates the plugin is not loaded. + +--- + +## 11. Conformance ### An **orchestrator** **MUST**: @@ -623,16 +724,25 @@ malformed. - when claiming, return a `Match` with `owner_id` and `intent_name` per §4 — never a partial or speculative claim; - bear a `pipeline_id` distinct from any other loaded plugin's - id (§3). + id (§3); +- **respond** to every `ovos.pipeline..intents.list` + query with a §10.2 response payload describing its currently-loaded + intent set (§10.4) — pull-query is the source of truth that + consumers rely on. + +### A **handler** (skill or plugin-bundled) **MUST**: + +- emit **exactly one** of `ovos.intent.handler.complete` or + `ovos.intent.handler.error` when it finishes (§8.1) — the + orchestrator's §9.5 universal end-marker and §8.3 timeout + bookkeeping depend on the terminal event being deterministic; +- include `owner_id` and `intent_name` in the trio payload (§8.2); +- run the handler at most once per dispatch. -### A **handler** (skill or plugin-bundled) **SHOULD**: +A handler **SHOULD**: -- emit `ovos.intent.handler.start` when invoked (§8.1); -- emit exactly one of `ovos.intent.handler.complete` or - `ovos.intent.handler.error` when it finishes (§8.1); -- include `owner_id` and `intent_name` in the trio payload - (§8.2); -- run the handler at most once per dispatch. +- emit `ovos.intent.handler.start` when invoked (§8.1) so + observers can scope the handler-execution window. ### Non-goals @@ -640,16 +750,19 @@ The following are explicitly outside this specification: plugin loading and discovery; any pre-pipeline utterance transformation or cancellation chain; ASR n-best ranking semantics within plugins; per-plugin behavioural specs; the `session` object's -full internal shape beyond `session_id`, `lang` (OVOS-MSG-1 §4), -and `pipeline` (§5). +wire shape and field set (owned by OVOS-SESSION-1). --- ## See also - *Bus Message Specification* (OVOS-MSG-1) — the envelope, the - single-flip routing model, the `session` carrier that holds - `pipeline`. + single-flip routing model, the shared topic-component identifier + rule (§2.1.1), the `session` carrier that holds `pipeline`. +- *Session Specification* (OVOS-SESSION-1) — the wire shape of + `session`, the registry mechanism under which this specification + claims the `pipeline` field, and the deployment-default fallback + rule for omitted / empty `session.pipeline`. - *Intent and Entity Registration Bus Contract* (OVOS-INTENT-4) — the registration wire format plugins consume (when they choose to). From db333f21dfa4c0d02b3c00e0e85c74a988cff690 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 20:10:52 +0100 Subject: [PATCH 10/57] =?UTF-8?q?PIPELINE-1=20=C2=A78:=20handler-trio=20is?= =?UTF-8?q?=20orchestrator-owned,=20not=20handler-owned?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per design clarification: third-party handler code carries no obligation under this specification. The orchestrator wraps every handler invocation and emits the trio itself — start before the call, complete on normal return, error on exception or timeout. §8.1: trio MUSTs now bind the orchestrator. No handler-side participation required. §8.3: rename 'Orchestrator timeout' → 'Handler timeout'. On timeout the orchestrator emits .error (it owns the topic), then ovos.utterance.handled. Drops the prior 'orchestrator MUST NOT synthesize .error' rule since the orchestrator now owns it unambiguously. §11 handler section: replaced with a 'no normative obligation' clause. The spec binds the orchestrator that invokes the handler, not the handler. Also addresses the workshop utterance.handled asymmetry: workshop acts as orchestrator-ish wrapper but didn't emit the trio. Under this revision the wrapper IS responsible — that's the right ownership. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 94 +++++++++++++++++++++++------------------------------ 1 file changed, 40 insertions(+), 54 deletions(-) diff --git a/pipeline.md b/pipeline.md index 5c4b2ca..41b466d 100644 --- a/pipeline.md +++ b/pipeline.md @@ -427,46 +427,39 @@ arrangements. ## 8. Handler-lifecycle messages The handler — whether a skill or a plugin-bundled handler — is a -black box. The bus observes what it does via three broadcast -notification topics, the **handler-lifecycle trio**: +black box. **Third-party handler code carries no obligation under +this specification.** The handler-lifecycle trio is emitted by the +**orchestrator** that invokes the handler, wrapping the invocation: +`start` before the call, then `complete` on normal return or `error` +on exception. The handler itself does not emit anything. + +The three broadcast notification topics are the +**handler-lifecycle trio**: | Topic | Meaning | |-------|---------| -| `ovos.intent.handler.start` | The handler has begun. | -| `ovos.intent.handler.complete` | The handler has finished normally. | +| `ovos.intent.handler.start` | The orchestrator is about to invoke the handler. | +| `ovos.intent.handler.complete` | The handler returned normally. | | `ovos.intent.handler.error` | The handler raised. | -These are emitted by the handler-owning component (skill or -plugin), produced via OVOS-MSG-1 §5.1 `forward` from the -originating dispatch Message — `context` is preserved unchanged. - -The trio is the only observable about handler execution. It is -broadcast so any observer (the orchestrator for timeout -bookkeeping, loggers, transcript viewers, analytics, fallback -chains) can subscribe. +Each trio Message is produced via OVOS-MSG-1 §5.1 `forward` from the +originating dispatch Message — `context` (including `session`) is +preserved unchanged. The trio is broadcast so any observer (loggers, +transcript viewers, analytics, fallback chains) can subscribe. ### 8.1 Order and obligations -For each accepted dispatch, the handler-owning component **MUST** -emit **exactly one** terminal event — either -`ovos.intent.handler.complete` or `ovos.intent.handler.error` — and -**SHOULD** precede it with `ovos.intent.handler.start`. - -- on normal completion: `ovos.intent.handler.start` (SHOULD) followed - by `ovos.intent.handler.complete` (MUST); -- on exception: `ovos.intent.handler.start` (SHOULD) followed by - `ovos.intent.handler.error` (MUST). +For each accepted dispatch, the **orchestrator MUST** emit: -The terminal event is **MUST** because the orchestrator's universal -`ovos.utterance.handled` invariant (§9.5) and timeout bookkeeping -(§8.3) depend on it: an utterance whose handler emits no terminal -event leaves the orchestrator unable to satisfy §9.5 without -falling back to the §8.3 timeout — a degraded mode that wastes time -and produces observably-late lifecycle for downstream consumers. +- `ovos.intent.handler.start` immediately before invoking the + handler; +- **exactly one** of `ovos.intent.handler.complete` (on normal + return) or `ovos.intent.handler.error` (on exception) immediately + after the invocation returns or raises. -A handler that emits neither terminal event still ran (the spec -cannot prevent that) but is non-conformant — its execution is -invisible to the bus and forces the orchestrator into §8.3. +A dispatch produces exactly one `start` and exactly one terminal +event. The orchestrator owns the trio in full; no third-party code +is required to participate. ### 8.2 Payload @@ -493,20 +486,18 @@ Each lifecycle message's `data`: |-------|------|----------|---------| | `owner_id` | string | yes | The handler-owning component's id (skill_id or pipeline_id). | | `intent_name` | string | yes | The intent the handler was dispatched for. | -| `exception` | string | `error` only | Human-readable description of the failure. | +| `exception` | string | `error` only | Human-readable description of the failure raised by the handler. | Implementations **MAY** include additional fields but consumers **MUST NOT** require them. -### 8.3 Orchestrator timeout +### 8.3 Handler timeout -The orchestrator **MAY** wait for `.complete` or `.error` matching -the dispatched `(owner_id, intent_name, session)` for a -deployment-defined time bound. If neither arrives within the -bound, the orchestrator **MUST** still emit -`ovos.utterance.handled` (§9.5) to satisfy the universal -end-marker invariant. It **MUST NOT** synthesize a `.error` of -its own — error events come from the handler that owns the trio. +The orchestrator **MAY** bound handler execution by a +deployment-defined time. If the handler has not returned within the +bound, the orchestrator **MUST** emit `ovos.intent.handler.error` +with an `exception` field indicating timeout, then **MUST** proceed +to emit `ovos.utterance.handled` (§9.5). The orchestrator **MUST NOT** re-emit the dispatch Message for the same match. Re-dispatch is not defined by this specification. @@ -711,9 +702,10 @@ indicates the plugin is not loaded. - dispatch on `:` per §7; - handle a plugin exception by logging and continuing to the next plugin (§6.2), not by failing the utterance; -- subscribe to the handler-lifecycle trio (§8) to observe - dispatched-handler outcomes; **MUST NOT** synthesize trio - events of its own (§8.3). +- emit the handler-lifecycle trio (§8) wrapping every handler + invocation: `start` before the call, then exactly one of + `complete` (on normal return) or `error` (on exception or + timeout, §8.3) after. ### A **pipeline plugin** **MUST**: @@ -730,19 +722,13 @@ indicates the plugin is not loaded. intent set (§10.4) — pull-query is the source of truth that consumers rely on. -### A **handler** (skill or plugin-bundled) **MUST**: - -- emit **exactly one** of `ovos.intent.handler.complete` or - `ovos.intent.handler.error` when it finishes (§8.1) — the - orchestrator's §9.5 universal end-marker and §8.3 timeout - bookkeeping depend on the terminal event being deterministic; -- include `owner_id` and `intent_name` in the trio payload (§8.2); -- run the handler at most once per dispatch. - -A handler **SHOULD**: +### A **handler** (skill or plugin-bundled) -- emit `ovos.intent.handler.start` when invoked (§8.1) so - observers can scope the handler-execution window. +Handlers carry **no normative obligation** under this +specification. The orchestrator owns the handler-lifecycle trio +(§8) and the dispatch envelope (§7). A handler is an opaque +callable; the spec binds the orchestrator that invokes it, not the +handler itself. ### Non-goals From 288a1a0e17a07bcb59533267127be2337daeba94 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 20:57:06 +0100 Subject: [PATCH 11/57] =?UTF-8?q?PIPELINE-1=20=C2=A74:=20state=20utterance?= =?UTF-8?q?=20argument=20shape=20explicitly=20at=20the=20plugin=20boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A plugin needs the shape contract without having to read TRANSFORM-1 §3.2 to infer it. Now: 'a non-empty list of candidate strings, may have been modified by utterance-transformer chain, all in the same language, no particular order, plugin chooses how to weight'. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pipeline.md b/pipeline.md index 41b466d..f6a819a 100644 --- a/pipeline.md +++ b/pipeline.md @@ -171,11 +171,17 @@ match(utterance, session) → Match | None Inputs: -- `utterance` — the user-side input as received in - `recognizer_loop:utterance` (typically a list of one or more - candidate strings; see §9.1); +- `utterance` — a **non-empty list of candidate strings**. The + list typically originates from `recognizer_loop:utterance` + (§9.1) and may have been modified by the utterance-transformer + chain (OVOS-TRANSFORM-1 §3.2) before reaching the plugin. A + plugin **MUST** accept this shape: a list of one or more + candidate transcripts, in no particular order, all in the same + language. A plugin is free to consider all candidates, only the + first, or any subset; the orchestrator does not prescribe how + candidates are weighted. - `session` — the session carrier from `context.session` of the - utterance Message (OVOS-MSG-1 §4). + utterance Message (OVOS-MSG-1 §4, OVOS-SESSION-1). Output: either `None` (decline) or a `Match` object with the fields below. From 7d1944105c8f18a4c719f2692d1d42e53ca96fe5 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 21:01:09 +0100 Subject: [PATCH 12/57] =?UTF-8?q?PIPELINE-1=20=C2=A79.1:=20de-prescribe=20?= =?UTF-8?q?recognizer=5Floop:utterance=20topic=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry-topic name is deferred to a separate spec covering audio-input ↔ assistant-core wire contracts. Current OVOS uses recognizer_loop:utterance for compatibility; conformant orchestrators MAY subscribe to that name in the interim. What IS normative is the behaviour after entry: §6 lifecycle, §9.5 end-marker, §§7-8 obligations. The entry name and payload shape are not. Internal refs updated to 'entry topic (§9.1)' style throughout. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 52 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/pipeline.md b/pipeline.md index f6a819a..5091e99 100644 --- a/pipeline.md +++ b/pipeline.md @@ -45,8 +45,9 @@ This specification defines: - the **handler-lifecycle trio** (§8) — `ovos.intent.handler.start` / `.complete` / `.error`; - the **utterance-layer bus events** (§9) — - `recognizer_loop:utterance`, `ovos.intent.matched`, - `complete_intent_failure`, `ovos.utterance.handled`; + the utterance entry topic (§9.1, name deferred), + `ovos.intent.matched`, `complete_intent_failure`, + `ovos.utterance.handled`; - **conformance** (§10). It does **not** define: @@ -80,9 +81,10 @@ It does **not** define: ## 2. The orchestrator and the pipeline plugin The **orchestrator** (OVOS-INTENT-3 §6.1) is the logical role that -consumes the utterance entry point `recognizer_loop:utterance`, -iterates plugins per session, emits dispatch and terminal events, -and guarantees the universal end-marker `ovos.utterance.handled`. +consumes the utterance-layer entry topic (§9.1; topic name +deferred to a future spec), iterates plugins per session, emits +dispatch and terminal events, and guarantees the universal +end-marker `ovos.utterance.handled`. The orchestrator is distinct from the **messagebus** (the transport layer) and from any individual plugin. @@ -172,8 +174,8 @@ match(utterance, session) → Match | None Inputs: - `utterance` — a **non-empty list of candidate strings**. The - list typically originates from `recognizer_loop:utterance` - (§9.1) and may have been modified by the utterance-transformer + list typically originates from the entry topic (§9.1) and may + have been modified by the utterance-transformer chain (OVOS-TRANSFORM-1 §3.2) before reaching the plugin. A plugin **MUST** accept this shape: a list of one or more candidate transcripts, in no particular order, all in the same @@ -292,7 +294,7 @@ to terminate** with exactly one `ovos.utterance.handled` event ### 6.1 The flow ``` -recognizer_loop:utterance ← entry (§9.1) +entry topic ← entry (§9.1) │ ├─ session retrieval; pipeline read from session (§5) │ @@ -516,14 +518,22 @@ This specification formalizes five utterance-layer bus events. All travel in standard OVOS-MSG-1 envelopes; routing follows the single-flip model of OVOS-MSG-1 §5.2. -### 9.1 `recognizer_loop:utterance` +### 9.1 The utterance-layer entry point -The **utterance-layer entry point**. Produced by any component -that wants to feed an utterance into the assistant — a listener, -a chat bridge, a CLI, a test harness, a remote-peer client. The -orchestrator subscribes and runs the lifecycle of §6. +The orchestrator subscribes to an **utterance-layer entry-point +topic** produced by any component that wants to feed an utterance +into the assistant — a listener, a chat bridge, a CLI, a test +harness, a remote-peer client. Receiving on this topic kicks off +the lifecycle of §6. -Payload: +The **topic name itself is not prescribed by this +specification**; it is the subject of a separate spec covering +audio-input ↔ assistant-core wire contracts. Current deployments +use **`recognizer_loop:utterance`** as the entry topic; a +conformant orchestrator MAY subscribe to that name for +compatibility while the entry-point spec is in flight. + +Payload shape on the entry topic (current convention): ```json { @@ -535,7 +545,13 @@ Payload: | Field | Type | Required | Meaning | |-------|------|----------|---------| | `utterances` | array of strings | yes | One or more candidate utterance strings. | -| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator disambiguates from `session.lang` and other context. | +| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator falls back per OVOS-SESSION-1 §3.2. | + +What **is** normative in this specification is the *behaviour +after entry*: every utterance the orchestrator accepts proceeds +through §6, terminates with exactly one `ovos.utterance.handled` +(§9.5), and carries the universal lifecycle obligations of +§§7–8. The entry topic's exact name and payload shape are not. ### 9.2 `ovos.intent.matched` @@ -595,7 +611,7 @@ matched-and-handler-timed-out. Broadcast. Payload **MAY** be empty. A conformant orchestrator **MUST** emit exactly one -`ovos.utterance.handled` per `recognizer_loop:utterance`. +`ovos.utterance.handled` per entry-topic Message (§9.1). Multiple emissions for one utterance are malformed; zero is malformed. @@ -694,7 +710,9 @@ indicates the plugin is not loaded. ### An **orchestrator** **MUST**: -- subscribe to `recognizer_loop:utterance` (§9.1); +- subscribe to the utterance-layer entry topic (§9.1) — name + deferred to a future spec; current deployments use + `recognizer_loop:utterance`; - run every received utterance through the lifecycle of §6 exactly once; - emit `ovos.utterance.handled` (§9.5) exactly once per From 95e89e381fcd4bd3b34d4313ec0e39f9c7d7263d Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 21:50:57 +0100 Subject: [PATCH 13/57] =?UTF-8?q?PIPELINE-1=20=C2=A710:=20clarify=20split-?= =?UTF-8?q?orch=20ownership=20of=20per-pipeline=5Fid=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pipeline.md b/pipeline.md index 5091e99..9722ddb 100644 --- a/pipeline.md +++ b/pipeline.md @@ -704,6 +704,14 @@ A plugin **MUST** respond to every query it observes for its own deployment-defined timeout **MAY** retry; persistent silence indicates the plugin is not loaded. +**Under a split orchestrator** (§2), a pipeline plugin is loaded +into exactly one orchestrator process — typically the +utterance-handling process that owns the match round of §6. That +process answers the per-`pipeline_id` query for plugins it hosts. +Sibling processes do not respond on its behalf. A query is +broadcast; the consumer accepts the single response that arrives +from the hosting process. + --- ## 11. Conformance From 09b3fb16f498b681b955047b0f8c11e0f92ffc0d Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 24 May 2026 21:51:43 +0100 Subject: [PATCH 14/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.1:=20orchestrator=20?= =?UTF-8?q?stamps=20context.skill=5Fid=20on=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per design clarification: the dispatch topic : implicitly tells the orchestrator which skill_id to stamp on context. By stamping at dispatch time, all skill emissions derived from the dispatch (via forward/reply) inherit the correct skill_id — INTENT-4 §3.1's loader-side enforcement becomes automatic for the dispatch path. Any drift between context.skill_id and the dispatch on a skill emission is non-conformant and detectable. Plugin-bundled handlers (owner_id is a pipeline_id, not a skill_id) do NOT get skill_id stamped — they identify via pipeline_id, per the existing dispatch polymorphism. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index 9722ddb..f64f6e7 100644 --- a/pipeline.md +++ b/pipeline.md @@ -382,7 +382,26 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): - `source` and `destination` follow the single-flip routing model (OVOS-MSG-1 §5.2) — the orchestrator derives the dispatch via `reply`, so `destination` is the original utterance emitter and - `source` is the orchestrator. + `source` is the orchestrator; +- when `` is a `skill_id`, the orchestrator **MUST** + stamp `context["skill_id"]` to that `skill_id`. This carries + the skill's identity forward into every Message the handler + emits via `forward` (OVOS-MSG-1 §5.1) — the skill inherits the + context, satisfying OVOS-INTENT-4 §3.1 by construction. When + `` is a `pipeline_id` (plugin-bundled handler), the + orchestrator **MUST NOT** stamp `context["skill_id"]` — + plugin-bundled handlers identify themselves via `pipeline_id`, + not `skill_id`. + +Any Message the skill subsequently emits **MUST** carry +`context["skill_id"]` matching the `` of the dispatch +that invoked it (OVOS-INTENT-4 §3.1). Because the orchestrator +stamps the dispatch context and skills derive their emissions +from it via `forward` / `reply`, this match is automatic — a +skill that emits a Message whose `context["skill_id"]` differs +from the dispatch is non-conformant, and the orchestrator +**SHOULD** detect and log such drift if it is in a position to +do so (loader-side interception per OVOS-INTENT-4 §3.1). The dispatch Message's `data`: From f68f7949a810f241ca44d0e3da7551a066673b31 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 02:10:41 +0100 Subject: [PATCH 15/57] =?UTF-8?q?PIPELINE-1=20=C2=A75:=20claim=20the=20thr?= =?UTF-8?q?ee=20blacklist=20session=20fields=20with=20full=20semantics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SESSION-1's §3 registry pattern: SESSION-1 fixes the field slot, the owner spec defines semantics. The denylist fields registered in SESSION-1 §3 point here; this commit writes the semantics they need. §5 restructured into a multi-field section: - §5.1 session.pipeline (the existing body, unchanged) - §5.2 session.blacklisted_pipelines — orchestrator-only filter, exists because pipeline is a positive whitelist and an origin that doesn't know the deployment default needs a negative knob - §5.3 session.blacklisted_skills — two-tier (plugins SHOULD honour, orchestrator MUST backstop). Plugin internal handling of would-match-but-blacklisted candidates is explicitly unspecified - §5.4 session.blacklisted_intents — same two-tier shape. Entries MUST be fully-qualified :; bare intent_name is rejected to prevent silent cross-skill collision - §5.5 Composition — effective pipeline = (session.pipeline OR deployment default) minus blacklisted_pipelines, computed once per utterance. blacklisted_skills/_intents apply per Match during iteration. Explicit pipeline wins; blacklisted_pipelines is the ergonomic alternative when the origin lacks default knowledge - §5.6 (informative) Layer-2 substrate authorization — denylists + source/destination + session_id give a layer-2 substrate the authorization surface for multi-tenant setups, riding the single-flip routing model with no per-hop re-authorization §1 scope updated to list the four session fields owned here. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 12 deletions(-) diff --git a/pipeline.md b/pipeline.md index f64f6e7..b98be6f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -37,8 +37,10 @@ This specification defines: - the **pipeline plugin** abstraction (§3) — the only thing the orchestrator iterates; - the **match contract** (§4) — the only thing a plugin exposes; -- the **`session.pipeline`** field (§5) — how a session - chooses which plugins and in what order; +- the **session fields owned by this specification** (§5): + `session.pipeline` (positive whitelist + ordering), + `session.blacklisted_pipelines`, `session.blacklisted_skills`, + `session.blacklisted_intents` (negative filters); - the **utterance lifecycle** (§6) — entry, iteration, dispatch, terminal events; - the **dispatch** topic shape (§7) — `:`; @@ -68,9 +70,9 @@ It does **not** define: registrations on the bus; whether and how a given plugin subscribes is the plugin's own business. - **the `session` lifecycle** — `session` is carried opaquely per - OVOS-MSG-1 §4. `session.pipeline` is one internal field - this spec prescribes (§5); other internal fields are deferred to - a future session specification. + OVOS-MSG-1 §4. The session fields this spec owns are listed in §5; + other internal fields are owned by other specifications via the + OVOS-SESSION-1 §2.1 registry mechanism. - **per-plugin behavioural specs** — plugins have no behavioural contract beyond §4. A `converse` plugin, a `fallback` plugin, a persona plugin, a language-model plugin, a chatbot plugin: each @@ -235,14 +237,24 @@ it to the dispatched handler. --- -## 5. `session.pipeline` +## 5. Session fields owned by this specification -This specification claims **`pipeline`** as a session field per -OVOS-SESSION-1 §2.1: an array of `pipeline_id` strings, ordered, -session-scoped, propagating with the session, with the -deployment-default-fallback absence rule (an omitted, empty, or -absent `session.pipeline` resolves to the deployment-configured -default at consumption, per OVOS-SESSION-1 §2.5). +This specification claims four session fields per OVOS-SESSION-1 +§2.1: one **positive** ordering field (§5.1 `pipeline`) and three +**negative** filtering fields (§5.2 `blacklisted_pipelines`, §5.3 +`blacklisted_skills`, §5.4 `blacklisted_intents`). All four are +session-scoped, propagate with the session under OVOS-SESSION-1 §4, +and follow the deployment-default-fallback absence rule of +OVOS-SESSION-1 §2.5: an omitted, empty, or absent field resolves at +consumption to the deployment-configured default. + +§5.5 fixes how the positive and negative fields compose when both +are set; §5.6 is an informative note on layer-2 authorization use. + +### 5.1 `session.pipeline` + +An ordered array of `pipeline_id` strings naming the plugins this +session runs, in iteration order. Example: @@ -282,6 +294,129 @@ deployment provides different behaviour to different participants — for example, a remote-peer session may carry a restricted pipeline that excludes destructive plugins. +### 5.2 `session.blacklisted_pipelines` + +An unordered array of `pipeline_id` strings the orchestrator +**MUST NOT** invoke for this session. + +`blacklisted_pipelines` exists because `session.pipeline` is a +positive whitelist — to express "the deployment default, minus the +LLM plugin" with `pipeline` alone, the session origin would need to +enumerate the deployment's full default. The denylist lets a session +origin (typically a layer-2 substrate or a multi-tenant policy +layer) suppress specific plugins from whatever ordering is in +effect without knowing what that ordering is. + +Filtering is **orchestrator-only**: when the orchestrator iterates +its effective pipeline (per §5.5), it **MUST** skip any +`pipeline_id` listed here as if it were not loaded. No `match` call +is made; no bus event is emitted for the skip. The filtering is +observable only as a non-invocation. + +Unknown `pipeline_id`s in `blacklisted_pipelines` are harmless and +**MUST NOT** cause the utterance to abort — they simply match +nothing. + +An empty array (`[]`) is an explicit "no pipelines are denied for +this session" and is **not** equivalent to omission, which falls +back to the deployment default per OVOS-SESSION-1 §2.5. + +### 5.3 `session.blacklisted_skills` + +An unordered array of `skill_id` strings (OVOS-INTENT-3) whose +intents **MUST NOT** be matched for this session. + +The contract is **two-tier**: + +1. A pipeline plugin **SHOULD NOT** return a `Match` whose + `owner_id` (§7.1) is a `skill_id` listed here. A plugin's + internal handling of would-match-but-blacklisted candidates is + **not specified** — it MAY skip the candidate before scoring, + suppress its score below a match threshold, route to a + plugin-internal default-handler, or anything else — as long as + the returned `Match` does not name a blacklisted skill. +2. A pipeline plugin that does not implement filtering is **not + conformant** with this field. The orchestrator **MUST** therefore + act as backstop: after a plugin returns a candidate `Match`, the + orchestrator **MUST** check `Match.owner_id` against + `blacklisted_skills` and, if listed, **MUST** treat the match as + if the plugin had declined — continue iteration to the next + plugin per §6.2. No bus event is emitted for backstop filtering; + it is observable only as a non-match. + +Empty-array semantics match §5.2: `[]` is explicit "none denied" +and is not equivalent to omission. + +### 5.4 `session.blacklisted_intents` + +An unordered array of fully-qualified `:` +strings (the dispatch-topic shape of §7) whose specific intents +**MUST NOT** be matched for this session. + +The contract is identical in shape to §5.3 (two-tier: +plugin-SHOULD + orchestrator-MUST-backstop), with the comparison +performed against the candidate `Match`'s dispatch identity +`:`. + +The bare `intent_name` form is **not** accepted in this field. +`intent_name` is only unique within an owner, so a bare entry would +silently denylist every same-named intent across every skill and +every pipeline plugin in the deployment — a sharp footgun. A +producer **MUST** emit fully-qualified entries; a consumer **MAY** +reject malformed (non-colon-bearing) entries or **MAY** ignore them +silently, but **MUST NOT** broaden a bare entry to all owners. + +Empty-array semantics match §5.2. + +### 5.5 Composition of the positive and negative fields + +When more than one of §5.1–§5.4 is set, they compose as follows. +The orchestrator computes its **effective pipeline** for an +utterance in this order: + +1. Take `session.pipeline` if set and non-empty; otherwise take the + deployment-default pipeline ordering. +2. Remove any `pipeline_id` listed in `session.blacklisted_pipelines`. + +The result is the ordered list of `pipeline_id`s the orchestrator +iterates for this utterance. + +`session.blacklisted_skills` and `session.blacklisted_intents` are +**not** applied at this stage. They apply per-candidate, against +each `Match` a plugin returns during iteration (§5.3, §5.4). + +Two rules follow from this composition: + +- An **explicit `session.pipeline`** is authoritative. If the + session origin enumerated the plugins it wants, the deployment + default is not consulted; `blacklisted_pipelines` still applies + to the explicit list, though in practice a session origin that + knows the full positive list rarely also needs the negative one. +- `blacklisted_pipelines` only meaningfully *adds* information when + `session.pipeline` is omitted — that is the case it was designed + for. The interaction is intentional: positive control wins, and + the negative field exists as the ergonomic alternative when the + origin lacks the deployment-default knowledge to construct one. + +### 5.6 Use under layer-2 substrates (informative) + +A layer-2 system (per OVOS-MSG-1 §3.4 / §4.4) that already attaches +a per-peer session and uses `source` / `destination` for routing +can use §5.2 / §5.3 / §5.4 as the **authorization surface** for +multi-tenant deployments: when a remote participant opens a +session, the layer-2 system populates the denylists from the peer's +permission grant, and the orchestrator-side enforcement above +guarantees the policy holds even against non-conformant pipeline +plugins. + +This composes with the single-flip routing model of OVOS-MSG-1 §5 +without orchestrator-side changes: the denylists ride on every +derived Message through OVOS-SESSION-1 §4 propagation, so no +per-hop re-authorization is needed. This specification reserves no +fields for layer-2 authorization beyond the three denylists; the +broader authorization model is the layer-2 substrate's concern, +not PIPELINE-1's. + --- ## 6. The utterance lifecycle From e5cb7b2e0dee50c2c419c73f4e9055086a857825 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 02:16:55 +0100 Subject: [PATCH 16/57] =?UTF-8?q?PIPELINE-1=20=C2=A75:=20fix=20composition?= =?UTF-8?q?=20rule=20=E2=80=94=20policy=20overrides=20preference,=20not=20?= =?UTF-8?q?alternatives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous wording in §5.5 framed session.pipeline and blacklisted_pipelines as alternatives ("explicit pipeline wins; blacklist is the ergonomic alternative"). That misread the model. Correct layering: - session.pipeline is the **preference channel** — any session origin (local, remote, layer-2-attached, programmatic) MAY populate it as a request. No authorization implied. - The orchestrator narrows the request to what is loaded (availability) and what policy permits (the denylists). - Denylists override preference: a pipeline_id requested in session.pipeline and also listed in blacklisted_pipelines is dropped. Same per-Match for blacklisted_skills/_intents. §5.5 rewritten around three explicit stages: preference, availability, policy. Empty effective pipeline → no-match, without silent fallback to the default-session pipeline (a fallback would let a policy-rejected request pull in an ordering nobody approved). §5.1 reframed as preference channel; clarified that the "deployment default" is in practice the pipeline configured for the reserved session_id == "default" session (OVOS-SESSION-1 §3.1), which the orchestrator owns. §5.2 reframed as policy channel; explicit "MUST NOT be invoked even if requested in session.pipeline". §5.6 reframed around the intended client/layer-2 split: clients request via session.pipeline; layer-2 substrates that own the session enforce via the denylists. The two roles use different fields, and §5.5 layering makes enforcement automatic. Also scrubbed example pipeline_ids ("padatious-*", "adapt-*") that named real engines, replacing with generic "template-*" / "keyword-*" per the no-named-projects rule for spec bodies. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 173 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 61 deletions(-) diff --git a/pipeline.md b/pipeline.md index b98be6f..bcc7d3a 100644 --- a/pipeline.md +++ b/pipeline.md @@ -253,8 +253,16 @@ are set; §5.6 is an informative note on layer-2 authorization use. ### 5.1 `session.pipeline` -An ordered array of `pipeline_id` strings naming the plugins this -session runs, in iteration order. +An ordered array of `pipeline_id` strings expressing the **session +origin's preference** for which plugins to run and in what order. +It is a preference, not an authorization: the orchestrator narrows +the requested list to what is loaded (below) and what policy permits +(§5.5). + +Any session — local, remote, layer-2-attached, programmatic — MAY +populate `session.pipeline` to request a specific ordering. The +orchestrator does not interpret who set it; the field is a +preference channel. Example: @@ -262,10 +270,10 @@ Example: { "session": { "pipeline": [ - "padatious-high", - "adapt-high", - "padatious-medium", - "adapt-medium", + "template-high", + "keyword-high", + "template-medium", + "keyword-medium", "common-qa", "persona-high", "fallback-low" @@ -285,27 +293,31 @@ merely because one identifier is unknown — the remaining known identifiers are the effective ordered set. If `session.pipeline` is absent or empty (per OVOS-SESSION-1 §2.5), -the orchestrator falls back to the deployment-configured default -pipeline. If no default is configured, the utterance proceeds to -no-match (`complete_intent_failure`, §9.3). +the orchestrator falls back to the **default-session pipeline**: the +pipeline configured for the reserved `session_id == "default"` +session (OVOS-SESSION-1 §3.1). The default-session pipeline is owned +and maintained by the orchestrator and represents what the +deployment runs when no preference is expressed. If the default +session itself has no `pipeline` configured, the utterance proceeds +to no-match (`complete_intent_failure`, §9.3). Different sessions may carry different `pipeline`. This is how a -deployment provides different behaviour to different participants — -for example, a remote-peer session may carry a restricted pipeline -that excludes destructive plugins. +session origin expresses different preferences for different +participants — for example, a remote-peer session may request a +restricted pipeline tailored to that participant's needs. Whether +that preference is honoured is a policy decision (§5.5). ### 5.2 `session.blacklisted_pipelines` An unordered array of `pipeline_id` strings the orchestrator **MUST NOT** invoke for this session. -`blacklisted_pipelines` exists because `session.pipeline` is a -positive whitelist — to express "the deployment default, minus the -LLM plugin" with `pipeline` alone, the session origin would need to -enumerate the deployment's full default. The denylist lets a session -origin (typically a layer-2 substrate or a multi-tenant policy -layer) suppress specific plugins from whatever ordering is in -effect without knowing what that ordering is. +`blacklisted_pipelines` is the **policy channel** for pipeline +selection. Where `session.pipeline` (§5.1) is the session origin's +preference, `blacklisted_pipelines` is enforcement: a plugin listed +here **MUST NOT** be invoked for this session **even if the same +`pipeline_id` is requested in `session.pipeline`**. Policy overrides +preference (§5.5). Filtering is **orchestrator-only**: when the orchestrator iterates its effective pipeline (per §5.5), it **MUST** skip any @@ -368,54 +380,93 @@ silently, but **MUST NOT** broaden a bare entry to all owners. Empty-array semantics match §5.2. -### 5.5 Composition of the positive and negative fields - -When more than one of §5.1–§5.4 is set, they compose as follows. -The orchestrator computes its **effective pipeline** for an -utterance in this order: - -1. Take `session.pipeline` if set and non-empty; otherwise take the - deployment-default pipeline ordering. -2. Remove any `pipeline_id` listed in `session.blacklisted_pipelines`. +### 5.5 Composition: preference, availability, policy + +The four fields layer in a fixed order: a **preference** stage +(§5.1), an **availability** stage (the loaded-plugin set), and a +**policy** stage (§5.2 / §5.3 / §5.4). Each later stage may narrow +the result of the earlier ones; no later stage adds anything an +earlier stage rejected. + +The orchestrator computes the **effective pipeline** for an +utterance: + +1. **Preference.** Start from `session.pipeline` if set and + non-empty; otherwise start from the default-session pipeline + (§5.1). +2. **Availability.** Drop any `pipeline_id` that does not + correspond to a plugin loaded by the orchestrator. Unknown + identifiers do not abort the utterance and do not trigger + fallback to the default-session pipeline — the remaining known + identifiers are the effective ordered set (§5.1). +3. **Policy.** Drop any `pipeline_id` listed in + `session.blacklisted_pipelines`, even if it was explicitly + requested in step 1. Policy overrides preference. The result is the ordered list of `pipeline_id`s the orchestrator iterates for this utterance. `session.blacklisted_skills` and `session.blacklisted_intents` are -**not** applied at this stage. They apply per-candidate, against -each `Match` a plugin returns during iteration (§5.3, §5.4). - -Two rules follow from this composition: - -- An **explicit `session.pipeline`** is authoritative. If the - session origin enumerated the plugins it wants, the deployment - default is not consulted; `blacklisted_pipelines` still applies - to the explicit list, though in practice a session origin that - knows the full positive list rarely also needs the negative one. -- `blacklisted_pipelines` only meaningfully *adds* information when - `session.pipeline` is omitted — that is the case it was designed - for. The interaction is intentional: positive control wins, and - the negative field exists as the ergonomic alternative when the - origin lacks the deployment-default knowledge to construct one. +**not** applied at this stage. They are per-candidate policy filters +applied during iteration against each `Match` a plugin returns +(§5.3, §5.4). The two-tier shape (plugin SHOULD, orchestrator MUST +backstop) ensures policy enforcement regardless of plugin +conformance. + +The intended separation of concerns is sharp: + +- **Any session origin — including the participant on the user + side of the bus — MAY request a preferred pipeline via + `session.pipeline`.** This is a request channel, available to + every emitter without authorization. +- **Only policy** (the denylists, typically populated by the + orchestrator owner or by a layer-2 substrate that owns the + session, see §5.6) can refuse a request. Policy is enforcement; + preference is request. The two fields are layered, not + alternatives. + +If every requested `pipeline_id` is dropped by availability or +policy, the effective pipeline is empty and the utterance proceeds +directly to no-match (`complete_intent_failure`, §9.3). The +orchestrator **MUST NOT** silently fall back to the default-session +pipeline in this case — falling back would let a policy-rejected +preference pull in a different ordering the origin never asked for +and policy never approved. ### 5.6 Use under layer-2 substrates (informative) -A layer-2 system (per OVOS-MSG-1 §3.4 / §4.4) that already attaches -a per-peer session and uses `source` / `destination` for routing -can use §5.2 / §5.3 / §5.4 as the **authorization surface** for -multi-tenant deployments: when a remote participant opens a -session, the layer-2 system populates the denylists from the peer's -permission grant, and the orchestrator-side enforcement above -guarantees the policy holds even against non-conformant pipeline -plugins. - -This composes with the single-flip routing model of OVOS-MSG-1 §5 -without orchestrator-side changes: the denylists ride on every -derived Message through OVOS-SESSION-1 §4 propagation, so no -per-hop re-authorization is needed. This specification reserves no -fields for layer-2 authorization beyond the three denylists; the -broader authorization model is the layer-2 substrate's concern, -not PIPELINE-1's. +The §5.5 layering — preference from any origin, enforcement from +policy — is precisely what a layer-2 substrate (per OVOS-MSG-1 +§3.4 / §4.4) needs to express **granular per-peer permissions** in +a multi-tenant deployment, without inventing a separate +authorization channel. + +The intended split: + +- A **client** (the participant on the user side of the bus — + local device, remote peer, satellite, programmatic caller) sets + `session.pipeline` to request what it would *like* to run. + Clients are not trusted to grant themselves capabilities; they + are only stating a preference. +- A **layer-2 substrate** that owns the session (typically because + it attached the per-peer session at connection time) populates + `session.blacklisted_pipelines`, `session.blacklisted_skills`, + and `session.blacklisted_intents` from the peer's permission + grant. These ride on every derived Message through OVOS-SESSION-1 + §4 propagation, so no per-hop re-authorization is needed and no + orchestrator-side change is required to add authorization. + +The orchestrator enforces the intersection: §5.5 step 3 drops +disallowed pipelines from the request; §5.3 / §5.4 drop disallowed +matches per candidate. A client that requests a forbidden plugin or +intent simply gets no result for that part of its request — its +preference is silently narrowed, exactly as if the plugin were not +loaded. + +This specification reserves no fields for layer-2 authorization +beyond the three denylists; the broader authorization model +(identity verification, peer-to-grant binding, revocation, +auditing) is the layer-2 substrate's concern, not PIPELINE-1's. --- @@ -723,7 +774,7 @@ plugin's id: "lang": "en-US", "utterance": "play the beatles", "captures": { "query": "the beatles" }, - "pipeline_id": "padatious-high" + "pipeline_id": "template-high" } ``` @@ -803,7 +854,7 @@ The plugin **MUST** reply with the currently-loaded intent set: ```json { - "pipeline_id": "padatious-high", + "pipeline_id": "template-high", "intents": [ { "intent_name": "play_music", From 7e3aef3d6307a8d970b73622feec5c4c6e3f8c83 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 02:35:11 +0100 Subject: [PATCH 17/57] =?UTF-8?q?PIPELINE-1:=20claim=20context['pipeline?= =?UTF-8?q?=5Fid']=20for=20plugin=20self-identification;=20fix=20=C2=A75.4?= =?UTF-8?q?=20lang=20note;=20tighten=20=C2=A79.1=20lang=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from the cross-spec audit: - §3.1 (new): plugins MUST stamp Message.context['pipeline_id'] on every emission, parallel to OVOS-INTENT-4 §3.1's context['skill_id'] rule for skills. This gives downstream specs (CONTEXT-1 §5.2 attribution of plugin-emitted ovos.context.set; future telemetry / audit consumers) a wire-level source for plugin attribution. Includes the emitter vs subject distinction (context['pipeline_id'] vs data['pipeline_id']), reserved-key precedence on derivations, mutual exclusion with context['skill_id'] (a Message carries one or the other, never both), and orchestrator-side loader enforcement. - §5.4 blacklisted_intents: added paragraph noting the field is language-agnostic. INTENT-4 §3.2 keys intent identity on (skill_id, intent_name, lang); blacklisted_intents keys only on (owner_id, intent_name) so a single entry denies both en-US and de-DE registrations of the same intent. Per-language denial is expressed via session.lang narrowing the matchable set. - §9.1 entry-topic lang field: previously anchored a normative MUST on OVOS-SESSION-1 §3.2.7 — which is explicitly informative consolidation guidance. Rewrote to name two normative sources (session.lang from §3.2.1, then deployment default) and note that the §3.2.7 cascade applies to downstream stages, not to this entry-point field. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index bcc7d3a..5f027fc 100644 --- a/pipeline.md +++ b/pipeline.md @@ -163,6 +163,63 @@ multiple matching modes (for example, a strict mode and a permissive mode). The orchestrator treats each `pipeline_id` as a distinct stage. +### 3.1 Plugin self-identification on emission + +A pipeline plugin **MUST** set `Message.context["pipeline_id"]` on +**every Message it emits to the bus**, to its own `pipeline_id`. +This is the plugin-side analogue of the skill rule in +OVOS-INTENT-4 §3.1: it makes plugin-originated traffic +attributable to its emitter without parsing topic names or `data` +payloads. + +The rule binds independent of the topic. It applies to bus events +a plugin emits on its own initiative (background telemetry, +diagnostics, plugin-defined topics), and — crucially for downstream +specs — to events on shared topics owned by other specifications. +For example, OVOS-CONTEXT-1 §5.2 reads `context["pipeline_id"]` to +attribute a plugin-emitted `ovos.context.set` to its owning +plugin; without this rule that attribution has no wire-level +source. + +Reserved-key precedence: when a plugin emits via +`Message.forward` / `.reply` / `.response` (OVOS-MSG-1 §5) from a +prior Message that already carries `context["skill_id"]` (a +dispatch the plugin matched but bundles its own handler for, for +example), the plugin **MUST** ensure the outbound Message carries +`context["pipeline_id"]` set to its own identity and **MUST NOT** +leave the inherited `context["skill_id"]` in place. The two keys +identify different component types and **MUST NOT** appear together +on the same Message: a Message carries exactly one of +`context["skill_id"]` (skill-originated) or `context["pipeline_id"]` +(plugin-originated). A consumer that observes both **SHOULD** treat +the Message as malformed and **SHOULD** log the drift. + +`Message.context["pipeline_id"]` is the emitter, parallel to the +`context["skill_id"]` / `data["skill_id"]` distinction of +OVOS-INTENT-4 §3.1.4. The corresponding payload field — when a +topic's `data` schema carries `pipeline_id` to identify a *subject* +of the message rather than its emitter — is owned by that topic's +spec; a consumer reading `data.pipeline_id` is reading a subject, +not an emitter. + +#### Orchestrator-side enforcement + +The orchestrator (or any component that loads pipeline plugins) +**SHOULD** intercept / decorate the plugin's emit pathway at load +time so non-compliant plugin code cannot emit a Message that lacks +or misstates `context["pipeline_id"]`. This places the discipline +on the plugin-loading infrastructure rather than on every plugin +author, mirroring the skill-loader enforcement of OVOS-INTENT-4 +§3.1. + +A consumer that needs to attribute a plugin-emitted Message +**MUST** read `context["pipeline_id"]` — it **MUST NOT** infer the +plugin from `source`, `data` fields, or topic name. A Message +without `context["pipeline_id"]` arriving on a topic that requires +plugin attribution (per the topic's owning spec) is malformed at +that topic's layer; the topic's spec defines the rejection +behaviour. + --- ## 4. The match contract @@ -378,6 +435,15 @@ producer **MUST** emit fully-qualified entries; a consumer **MAY** reject malformed (non-colon-bearing) entries or **MAY** ignore them silently, but **MUST NOT** broaden a bare entry to all owners. +Entries are **language-agnostic.** OVOS-INTENT-4 §3.2 keys intent +identity on the triple `(skill_id, intent_name, lang)`, so a single +intent registered for `en-US` and `de-DE` is two separate +registrations. A `blacklisted_intents` entry +`:` denies both — there is no per-language +denylist. A deployment that needs language-scoped denial expresses +it through a session whose `lang` already narrows the set of +matchable registrations. + Empty-array semantics match §5.2. ### 5.5 Composition: preference, availability, policy @@ -750,7 +816,7 @@ Payload shape on the entry topic (current convention): | Field | Type | Required | Meaning | |-------|------|----------|---------| | `utterances` | array of strings | yes | One or more candidate utterance strings. | -| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator falls back per OVOS-SESSION-1 §3.2. | +| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator **MUST** fall back to `session.lang` (OVOS-SESSION-1 §3.2.1) — the session-scoped user-preference signal — and if that too is absent, to the deployment default. The consolidation guidance of OVOS-SESSION-1 §3.2.7 is informative for downstream stages but does not apply to this entry-point field, which has only the two normative sources named here. | What **is** normative in this specification is the *behaviour after entry*: every utterance the orchestrator accepts proceeds From 344994b549e272c7cc24808b45444c93e9aee050 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 02:35:31 +0100 Subject: [PATCH 18/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20drop=20spurious?= =?UTF-8?q?=20INTENT-4=20=C2=A73.1.4=20sub-section=20ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit INTENT-4 §3.1's emitter-vs-subject treatment is in an unnumbered subsection; cite the parent §3.1 to avoid drift. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index 5f027fc..11d08cf 100644 --- a/pipeline.md +++ b/pipeline.md @@ -196,7 +196,7 @@ the Message as malformed and **SHOULD** log the drift. `Message.context["pipeline_id"]` is the emitter, parallel to the `context["skill_id"]` / `data["skill_id"]` distinction of -OVOS-INTENT-4 §3.1.4. The corresponding payload field — when a +OVOS-INTENT-4 §3.1. The corresponding payload field — when a topic's `data` schema carries `pipeline_id` to identify a *subject* of the message rather than its emitter — is owned by that topic's spec; a consumer reading `data.pipeline_id` is reading a subject, From bdbbd62b2caf529e816edc961e047a990310f7ec Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 02:50:31 +0100 Subject: [PATCH 19/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20drop=20strip-an?= =?UTF-8?q?d-mutual-exclude=20rule;=20allow=20identity=20keys=20to=20coexi?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer feedback: the prior 'Reserved-key precedence' paragraph required plugins emitting via forward/reply to strip an inherited context['skill_id'] and forbade the two keys appearing together. That conflicts with the natural MSG-1 §5 derivation semantics (forward preserves context unchanged) and breaks chain traceability — a plugin emission flowing from a skill dispatch legitimately carries both keys: the skill it dispatched from AND the plugin actually emitting. Reworked the paragraph to acknowledge coexistence: forward/reply preserve upstream identity stamps; the emitting plugin additionally stamps its own pipeline_id. When an attribution consumer needs to pick one, it uses most-specific identity wins, codified in CONTEXT-1 §5.2. Also reworded 'is the emitter' to 'is the plugin's self-attribution' since 'emitter' is ambiguous in chains crossing multiple component types. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/pipeline.md b/pipeline.md index 11d08cf..9fe5272 100644 --- a/pipeline.md +++ b/pipeline.md @@ -181,26 +181,27 @@ attribute a plugin-emitted `ovos.context.set` to its owning plugin; without this rule that attribution has no wire-level source. -Reserved-key precedence: when a plugin emits via +Reserved-key coexistence: when a plugin emits via `Message.forward` / `.reply` / `.response` (OVOS-MSG-1 §5) from a -prior Message that already carries `context["skill_id"]` (a -dispatch the plugin matched but bundles its own handler for, for -example), the plugin **MUST** ensure the outbound Message carries -`context["pipeline_id"]` set to its own identity and **MUST NOT** -leave the inherited `context["skill_id"]` in place. The two keys -identify different component types and **MUST NOT** appear together -on the same Message: a Message carries exactly one of -`context["skill_id"]` (skill-originated) or `context["pipeline_id"]` -(plugin-originated). A consumer that observes both **SHOULD** treat -the Message as malformed and **SHOULD** log the drift. - -`Message.context["pipeline_id"]` is the emitter, parallel to the -`context["skill_id"]` / `data["skill_id"]` distinction of -OVOS-INTENT-4 §3.1. The corresponding payload field — when a -topic's `data` schema carries `pipeline_id` to identify a *subject* -of the message rather than its emitter — is owned by that topic's -spec; a consumer reading `data.pipeline_id` is reading a subject, -not an emitter. +prior Message that already carries `context["skill_id"]` from an +upstream dispatch, the inherited `context["skill_id"]` is +preserved by the derivation rule and is **not** stripped — it +remains a valid attribution of the upstream dispatch this emission +flows from. The plugin additionally stamps `context["pipeline_id"]` +with its own identity. The two keys may legitimately coexist on +the same Message: each names a different component in the chain +that produced it. Attribution consumers (CONTEXT-1 §5.2, audit / +telemetry observers) apply a precedence — most-specific identity +wins — to pick one when they need to attribute a single emitter +(see CONTEXT-1 §5.2). + +`Message.context["pipeline_id"]` is the plugin's **self-attribution**. +Mirroring the `context["skill_id"]` / `data["skill_id"]` distinction +of OVOS-INTENT-4 §3.1, a topic's `data` schema may also carry +`pipeline_id` as the **subject** of the message (the plugin a +query is filtered against, the plugin being described, etc.); a +consumer reading `data.pipeline_id` is reading a subject, not a +self-attribution. #### Orchestrator-side enforcement From 470c595b73ba03e9e6393224ddb602156a928f20 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 03:02:25 +0100 Subject: [PATCH 20/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20split=20stamp?= =?UTF-8?q?=20rule=20into=20originate=20vs=20modify-in-place;=20update=20t?= =?UTF-8?q?ransformer=20cross-ref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligning with the uniform MUST-stamp-on-originate-or-modify rule adopted across INTENT-4, PIPELINE-1, and TRANSFORM-1. The plugin binds the context['pipeline_id'] stamp obligation both on free-form bus emission (originate) and on in-place mutation of a Message it processes (modify in place). Updated the coexistence paragraph to reference the new TRANSFORM-1 §1.3 per-type keys (six _transformer_id slots, not a single generic transformer_id) and to point to the lifecycle-position precedence in CONTEXT-1 §5.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/pipeline.md b/pipeline.md index 9fe5272..faf7dc9 100644 --- a/pipeline.md +++ b/pipeline.md @@ -181,19 +181,31 @@ attribute a plugin-emitted `ovos.context.set` to its owning plugin; without this rule that attribution has no wire-level source. -Reserved-key coexistence: when a plugin emits via +**Stamp rule.** A plugin **MUST** set +`Message.context["pipeline_id"]` to its own identity on: + +1. every Message it **originates** to the bus (a fresh emission, + not a `forward` / `reply` / `response` derivation); and +2. every Message it **modifies in place** before that Message + proceeds — the act of mutating `Message.context` (or any other + field) within the plugin's execution window counts as touching + the chain. + +The two cases together: whenever the plugin's hands have been on +a Message, `context["pipeline_id"]` reflects it. + +**Coexistence with other identity keys.** When a plugin emits via `Message.forward` / `.reply` / `.response` (OVOS-MSG-1 §5) from a prior Message that already carries `context["skill_id"]` from an -upstream dispatch, the inherited `context["skill_id"]` is -preserved by the derivation rule and is **not** stripped — it -remains a valid attribution of the upstream dispatch this emission -flows from. The plugin additionally stamps `context["pipeline_id"]` -with its own identity. The two keys may legitimately coexist on -the same Message: each names a different component in the chain -that produced it. Attribution consumers (CONTEXT-1 §5.2, audit / -telemetry observers) apply a precedence — most-specific identity -wins — to pick one when they need to attribute a single emitter -(see CONTEXT-1 §5.2). +upstream dispatch (or any of the six `_transformer_id` keys +of OVOS-TRANSFORM-1 §1.3 from upstream transformer stages), the +inherited keys are preserved by the derivation rule and **not** +stripped. Each names a different component in the chain that +produced the Message; the plugin additionally stamps its own +`context["pipeline_id"]`. Attribution consumers +(OVOS-CONTEXT-1 §5.2, audit / telemetry observers) apply a +lifecycle-position precedence — see OVOS-CONTEXT-1 §5.2 — to pick +a single owner when they need one. `Message.context["pipeline_id"]` is the plugin's **self-attribution**. Mirroring the `context["skill_id"]` / `data["skill_id"]` distinction From a6835326a903e6a34c101b1381c4d644dd07a568 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 03:09:42 +0100 Subject: [PATCH 21/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20stamp=20rule=20?= =?UTF-8?q?covers=20derivations=20placed=20on=20the=20bus=20by=20the=20plu?= =?UTF-8?q?gin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User feedback: a Message.reply or .response derivation that the plugin performs and emits IS a plugin emission — the resulting Message-on-wire is caused by the plugin and MUST carry context['pipeline_id']. Earlier wording carved derivations out ('a fresh emission, not a forward/reply/response derivation'), which was wrong. Rewrote the stamp rule around 'places on the bus' covering both fresh emissions and derivations (forward, reply, response) the plugin performs and emits. The derivation mechanism is irrelevant; what binds is that the plugin caused a Message to appear on the bus. Modify-in-place rule preserved as a separate clause covering context/data/session mutations that don't themselves emit. Also updated the cross-spec ref to the new _transformer_ids (plural-list) shape from TRANSFORM-1 §1.3. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/pipeline.md b/pipeline.md index faf7dc9..128aec7 100644 --- a/pipeline.md +++ b/pipeline.md @@ -182,22 +182,40 @@ plugin; without this rule that attribution has no wire-level source. **Stamp rule.** A plugin **MUST** set -`Message.context["pipeline_id"]` to its own identity on: - -1. every Message it **originates** to the bus (a fresh emission, - not a `forward` / `reply` / `response` derivation); and -2. every Message it **modifies in place** before that Message - proceeds — the act of mutating `Message.context` (or any other - field) within the plugin's execution window counts as touching - the chain. - -The two cases together: whenever the plugin's hands have been on -a Message, `context["pipeline_id"]` reflects it. +`Message.context["pipeline_id"]` to its own identity on **every +Message it places on the bus** and on every Message it **modifies +in place** before that Message proceeds. "Places on the bus" +covers all the ways a plugin can cause a Message to appear on the +bus: + +- a fresh emission (the plugin constructs and emits a new + Message); +- a derivation it performs and then emits — `Message.forward(...)`, + `Message.reply(...)`, or `Message.response(...)` from a prior + Message the plugin received or held (OVOS-MSG-1 §5). The + derivation mechanism is irrelevant to the stamp rule: the + plugin is the origin of the *resulting* Message-on-wire even + when context propagated from upstream, and the resulting + Message **MUST** carry `context["pipeline_id"]` set to the + plugin's id, overwriting whatever inherited + `context["pipeline_id"]` may have been there from an earlier + pipeline plugin in a multi-plugin chain. + +Modify-in-place covers the case where the plugin mutates an +existing Message (its `context`, its `data`, the session it +carries — for example, a CONTEXT-1 §5.3 direct mutation) without +itself causing a fresh emission; the next emitter still propagates +the modified Message, and `context["pipeline_id"]` must reflect +the plugin that mutated. + +The combined effect: whenever the plugin's hands have been on a +Message that subsequently appears on the bus, `context["pipeline_id"]` +reflects it. **Coexistence with other identity keys.** When a plugin emits via `Message.forward` / `.reply` / `.response` (OVOS-MSG-1 §5) from a prior Message that already carries `context["skill_id"]` from an -upstream dispatch (or any of the six `_transformer_id` keys +upstream dispatch (or any of the six `_transformer_ids` keys of OVOS-TRANSFORM-1 §1.3 from upstream transformer stages), the inherited keys are preserved by the derivation rule and **not** stripped. Each names a different component in the chain that From 83caa2bbc66777d64e343af8c70119df0ff0c1b4 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 03:19:42 +0100 Subject: [PATCH 22/57] =?UTF-8?q?PIPELINE-1=20=C2=A75.2=20=C2=A75.3=20?= =?UTF-8?q?=C2=A75.4:=20drop=20[]=20!=3D=20omission=20distinction;=20align?= =?UTF-8?q?=20with=20SESSION-1=20=C2=A73.4=20SHOULD-omit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per design decision (option B): the deliberate '[] is explicit none-denied, distinct from omission' semantics in the blacklist fields are dropped. [] is now wire-equivalent to omission for all three denylist fields; both resolve to the deployment default at consumption per SESSION-1 §2.5. Producers SHOULD omit empty arrays rather than emit them. The use case the distinction served — a session explicitly clearing a deployment-default denial via [] — is rare enough to not warrant the footgun. Layer-2 substrates wanting to grant 'bypass deployment default' permissions to a peer should use a smaller deployment default in the first place. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pipeline.md b/pipeline.md index 128aec7..c59bd92 100644 --- a/pipeline.md +++ b/pipeline.md @@ -417,9 +417,11 @@ Unknown `pipeline_id`s in `blacklisted_pipelines` are harmless and **MUST NOT** cause the utterance to abort — they simply match nothing. -An empty array (`[]`) is an explicit "no pipelines are denied for -this session" and is **not** equivalent to omission, which falls -back to the deployment default per OVOS-SESSION-1 §2.5. +An empty array (`[]`) is wire-equivalent to omission: both fall +back to the deployment default per OVOS-SESSION-1 §2.5. A +producer with no pipelines to deny **SHOULD** omit the field +rather than emit `[]`, per the wire-weight guidance of +OVOS-SESSION-1 §3.4. ### 5.3 `session.blacklisted_skills` @@ -444,8 +446,9 @@ The contract is **two-tier**: plugin per §6.2. No bus event is emitted for backstop filtering; it is observable only as a non-match. -Empty-array semantics match §5.2: `[]` is explicit "none denied" -and is not equivalent to omission. +Empty-array semantics match §5.2: `[]` is wire-equivalent to +omission. A producer with no skills to deny **SHOULD** omit the +field. ### 5.4 `session.blacklisted_intents` @@ -475,7 +478,8 @@ denylist. A deployment that needs language-scoped denial expresses it through a session whose `lang` already narrows the set of matchable registrations. -Empty-array semantics match §5.2. +Empty-array semantics match §5.2: `[]` is wire-equivalent to +omission. SHOULD-omit when there is nothing to deny. ### 5.5 Composition: preference, availability, policy From d9789f22eb45accd6b66e966186149cd73a5d476 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 03:50:30 +0100 Subject: [PATCH 23/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1,=20=C2=A76.1,=20?= =?UTF-8?q?=C2=A76.2:=20forward=20exemption=20+=20post-match-pre-dispatch?= =?UTF-8?q?=20window=20+=20circuit-breaker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three audit findings actioned: - §3.1 stamp rule: pure-forward propagation exempt. .reply and .response are authorial actions that MUST stamp pipeline_id; .forward is propagation and MUST NOT overwrite an inherited context['pipeline_id']. Preserves upstream attribution chains when a downstream plugin merely propagates an earlier plugin's emission. Authors wanting to claim authorship use .reply, .response, or fresh emit. - §6.1 lifecycle pseudocode: added the post-match-pre-dispatch window where CONTEXT-1 §5.3 sanctions engine-side context mutation and TRANSFORM-1 §3.4 inserts the intent-transformer chain. Also annotated where the utterance, metadata, dialog, and TTS transformer chains plug in. A reader of PIPELINE-1 alone no longer gets a wrong picture of the lifecycle ordering. - §6.2 plugin-exception handling: added a circuit-breaker clause. Orchestrators SHOULD track per-plugin exception counts and SHOULD drop a plugin from the session's effective pipeline after a deployer-tunable threshold (typically three) of consecutive failures. Optional informative ovos.pipeline.dropped diagnostic event named. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 93 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/pipeline.md b/pipeline.md index c59bd92..099a48d 100644 --- a/pipeline.md +++ b/pipeline.md @@ -182,24 +182,31 @@ plugin; without this rule that attribution has no wire-level source. **Stamp rule.** A plugin **MUST** set -`Message.context["pipeline_id"]` to its own identity on **every -Message it places on the bus** and on every Message it **modifies -in place** before that Message proceeds. "Places on the bus" -covers all the ways a plugin can cause a Message to appear on the -bus: +`Message.context["pipeline_id"]` to its own identity on every +Message it places on the bus by **authorial action** and on every +Message it **modifies in place** before that Message proceeds. +Authorial action covers the cases where the plugin is asserting +itself as the originator of a new Message-on-wire: - a fresh emission (the plugin constructs and emits a new Message); -- a derivation it performs and then emits — `Message.forward(...)`, - `Message.reply(...)`, or `Message.response(...)` from a prior - Message the plugin received or held (OVOS-MSG-1 §5). The - derivation mechanism is irrelevant to the stamp rule: the - plugin is the origin of the *resulting* Message-on-wire even - when context propagated from upstream, and the resulting - Message **MUST** carry `context["pipeline_id"]` set to the - plugin's id, overwriting whatever inherited - `context["pipeline_id"]` may have been there from an earlier - pipeline plugin in a multi-plugin chain. +- `Message.reply(...)` or `Message.response(...)` (OVOS-MSG-1 §5) + derived from a prior Message — these derivations swap routing + keys and create a new authorial step. The resulting + Message-on-wire **MUST** carry `context["pipeline_id"]` set to + the plugin's id, overwriting whatever inherited + `context["pipeline_id"]` value may have been there from an + upstream plugin in a multi-plugin chain. + +**Pure-forward propagation is exempt.** `Message.forward(...)` +(OVOS-MSG-1 §5.1) is propagation semantics — it preserves +`context` unchanged by design, and the deriving plugin is not +asserting authorship of the forwarded Message-on-wire. A plugin +that `.forward`s a Message **MUST NOT** overwrite an inherited +`context["pipeline_id"]` with its own — preserving upstream +attribution is the point of the forward derivation. If a plugin +wants to claim authorship of the resulting Message, it +**SHOULD** use `.reply` or `.response`, or emit fresh. Modify-in-place covers the case where the plugin mutates an existing Message (its `context`, its `data`, the session it @@ -583,23 +590,55 @@ to terminate** with exactly one `ovos.utterance.handled` event ``` entry topic ← entry (§9.1) │ - ├─ session retrieval; pipeline read from session (§5) + ├─ session retrieval; effective pipeline composed (§5.5) + │ (preference → availability → policy) │ - ├─ for pipeline_id in session.pipeline: + ├─ utterance-transformer chain runs ← TRANSFORM-1 §3.2 + ├─ metadata-transformer chain runs ← TRANSFORM-1 §3.3 + │ + ├─ for pipeline_id in effective pipeline: │ plugin = loaded_plugins[pipeline_id] # skip if not loaded │ match = plugin.match(utterance, session) │ if match is not None: + │ orchestrator-backstop denylist check (§5.3/§5.4) + │ if filtered: continue + │ + │ ┌── post-match-pre-dispatch window ──────────────┐ + │ │ engine-side context promotion (CONTEXT-1 §5.3) │ + │ │ intent-transformer chain runs (TRANSFORM-1 │ + │ │ §3.4) — may modify Match.captures, MUST NOT │ + │ │ change owner_id / intent_name │ + │ │ post-decay turns_remaining-- (CONTEXT-1 §4) │ + │ └────────────────────────────────────────────────┘ + │ │ ovos.intent.matched (§9.2) │ dispatch on : (§7) │ (handler runs; emits lifecycle trio §8) + │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 + │ (TTS rendering; tts-transformer chain — §3.6) │ ovos.utterance.handled (§9.5) │ break │ - └─ if no plugin matched: + └─ if no plugin matched (or all matches filtered): complete_intent_failure (§9.3) ovos.utterance.handled (§9.5) ``` +The flow diagram shows where companion-spec chains plug into this +specification's iteration loop. The **audio-transformer chain** +(TRANSFORM-1 §3.1) runs entirely in the audio-input service before +the entry topic is emitted and is therefore not visible here. The +**utterance** and **metadata** transformer chains run after entry +and before iteration, against the candidate utterance list. The +**post-match-pre-dispatch window** (highlighted) is where +CONTEXT-1 §5.3 sanctions engine-side `session.intent_context` +mutation and where TRANSFORM-1 §3.4 inserts the intent-transformer +chain over the chosen `Match`. The **dialog** and **TTS** +transformer chains run on handler-emitted speak Messages and on +the rendered audio respectively, off the dispatch return-path. + +Pseudocode is informative; normative rules are in §§4–9. + ### 6.2 First-match-wins iteration For each utterance, the orchestrator **MUST**: @@ -616,6 +655,24 @@ it returned `None`. The orchestrator **MUST** continue to the next plugin and **SHOULD** log the exception. A single plugin's bug does not fail the whole utterance. +**Repeated-exception circuit-breaker.** Persistent plugin failure +is a deployment concern, not an utterance-level concern, but a +defective plugin that raises on every invocation degrades every +utterance until the deployment intervenes. An orchestrator +**SHOULD** track per-plugin exception rates within a session (or +across a configurable window) and **SHOULD** drop a plugin from +the session's effective pipeline (§5.5) after a deployer-tunable +threshold of consecutive exceptions — typically three. A dropped +plugin behaves as if absent for the remainder of the session; +recovery is a deployment concern (process restart, plugin reload). +The threshold, the recovery policy, and whether the drop is +per-session or process-wide are deployer-configurable; this +specification fixes only that the discipline **SHOULD** exist. +The orchestrator **MAY** broadcast an `ovos.pipeline.dropped` +diagnostic event (payload `{pipeline_id, reason, exception_count}`) +to make the drop observable; this event is informative and +**MUST NOT** be relied upon for normative control flow. + ### 6.3 Plugins do not see each other's matches A plugin receives the same utterance every other plugin in the From 22e823b628e6a759986c332b8a650497a4b8c7e2 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 04:02:14 +0100 Subject: [PATCH 24/57] =?UTF-8?q?PIPELINE-1=20=C2=A79.1:=20rename=20entry?= =?UTF-8?q?=20topic=20recognizer=5Floop:utterance=20->=20ovos.utterance.ha?= =?UTF-8?q?ndle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Mycroft-era recognizer_loop:utterance fails the naming conventions of OVOS-MSG-1 §2.1.2 on three counts: - ':' used as a segment separator, where ':' is reserved for the : dispatch topic shape (MSG-1 §2.1.1). - Leading segment names an implementation role ('recognizer loop', the Mycroft audio-input service) rather than a stable assistant root. - Does not pair with the past-tense terminal event ovos.utterance.handled per the request/terminal verb-pairing convention of MSG-1 §2.1.2(d). Renamed to ovos.utterance.handle: dot-separated hierarchy, stable ovos. root, imperative-mood request verb pairing with handled. §9.1 now prescribes the topic name directly; removed the 'name deferred to a future spec' language. §6.1 lifecycle diagram and other in-spec entry-topic refs updated. Migration cost acknowledged: a transitional deployment MAY subscribe to both names; the legacy topic has no normative status under this spec. Depends on MSG-1 v2 (spec/msg1-v2-topic-naming) for the §2.1.2 naming conventions cited. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/pipeline.md b/pipeline.md index 099a48d..4597d56 100644 --- a/pipeline.md +++ b/pipeline.md @@ -47,7 +47,7 @@ This specification defines: - the **handler-lifecycle trio** (§8) — `ovos.intent.handler.start` / `.complete` / `.error`; - the **utterance-layer bus events** (§9) — - the utterance entry topic (§9.1, name deferred), + the utterance entry topic `ovos.utterance.handle` (§9.1), `ovos.intent.matched`, `complete_intent_failure`, `ovos.utterance.handled`; - **conformance** (§10). @@ -83,8 +83,8 @@ It does **not** define: ## 2. The orchestrator and the pipeline plugin The **orchestrator** (OVOS-INTENT-3 §6.1) is the logical role that -consumes the utterance-layer entry topic (§9.1; topic name -deferred to a future spec), iterates plugins per session, emits +consumes the utterance-layer entry topic `ovos.utterance.handle` +(§9.1), iterates plugins per session, emits dispatch and terminal events, and guarantees the universal end-marker `ovos.utterance.handled`. The orchestrator is distinct from the **messagebus** (the transport @@ -588,7 +588,7 @@ to terminate** with exactly one `ovos.utterance.handled` event ### 6.1 The flow ``` -entry topic ← entry (§9.1) +ovos.utterance.handle ← entry (§9.1) │ ├─ session retrieval; effective pipeline composed (§5.5) │ (preference → availability → policy) @@ -881,22 +881,22 @@ This specification formalizes five utterance-layer bus events. All travel in standard OVOS-MSG-1 envelopes; routing follows the single-flip model of OVOS-MSG-1 §5.2. -### 9.1 The utterance-layer entry point +### 9.1 The utterance-layer entry point — `ovos.utterance.handle` -The orchestrator subscribes to an **utterance-layer entry-point -topic** produced by any component that wants to feed an utterance -into the assistant — a listener, a chat bridge, a CLI, a test -harness, a remote-peer client. Receiving on this topic kicks off -the lifecycle of §6. +The orchestrator subscribes to **`ovos.utterance.handle`**, the +**utterance-layer entry-point topic** produced by any component +that wants to feed an utterance into the assistant — a listener, +a chat bridge, a CLI, a test harness, a remote-peer client. +Receiving on this topic kicks off the lifecycle of §6. -The **topic name itself is not prescribed by this -specification**; it is the subject of a separate spec covering -audio-input ↔ assistant-core wire contracts. Current deployments -use **`recognizer_loop:utterance`** as the entry topic; a -conformant orchestrator MAY subscribe to that name for -compatibility while the entry-point spec is in flight. +The topic name follows the naming conventions of OVOS-MSG-1 §2.1.2: +imperative-mood verb (`handle` — a request for the assistant to +handle this utterance), dot-separated hierarchy, no `:` (which is +reserved for component-pair dispatch topics), and pairs with the +past-tense terminal event `ovos.utterance.handled` (§9.5) by +shared root verb. -Payload shape on the entry topic (current convention): +Payload shape: ```json { @@ -910,11 +910,18 @@ Payload shape on the entry topic (current convention): | `utterances` | array of strings | yes | One or more candidate utterance strings. | | `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator **MUST** fall back to `session.lang` (OVOS-SESSION-1 §3.2.1) — the session-scoped user-preference signal — and if that too is absent, to the deployment default. The consolidation guidance of OVOS-SESSION-1 §3.2.7 is informative for downstream stages but does not apply to this entry-point field, which has only the two normative sources named here. | -What **is** normative in this specification is the *behaviour -after entry*: every utterance the orchestrator accepts proceeds -through §6, terminates with exactly one `ovos.utterance.handled` -(§9.5), and carries the universal lifecycle obligations of -§§7–8. The entry topic's exact name and payload shape are not. +**Migration from `recognizer_loop:utterance`.** Pre-spec +deployments use the topic name `recognizer_loop:utterance` for +the same purpose. That name pre-dates the naming conventions of +OVOS-MSG-1 §2.1.2: it uses `:` as a segment separator (where `:` +is reserved for `:` dispatch shapes), and +its leading segment names an implementation role (the audio-input +"recognizer loop") rather than a stable assistant root. A +deployment migrating to this specification **SHOULD** emit +`ovos.utterance.handle` as the canonical entry topic. A +transitional deployment **MAY** subscribe to both names during +migration; the legacy topic carries no normative status under +this specification. ### 9.2 `ovos.intent.matched` From e7c54a66363695c5e76fa76baf82caf18ab2862c Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 04:07:54 +0100 Subject: [PATCH 25/57] =?UTF-8?q?PIPELINE-1=20=C2=A79.1:=20drop=20migratio?= =?UTF-8?q?n=20meta-commentary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the 'Migration from recognizer_loop:utterance' paragraph. The spec now states only the current normative rule (ovos.utterance.handle is the entry topic). Historical predecessor mapping lives in APPENDIX §6.7. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/pipeline.md b/pipeline.md index 4597d56..c2ac618 100644 --- a/pipeline.md +++ b/pipeline.md @@ -910,18 +910,9 @@ Payload shape: | `utterances` | array of strings | yes | One or more candidate utterance strings. | | `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator **MUST** fall back to `session.lang` (OVOS-SESSION-1 §3.2.1) — the session-scoped user-preference signal — and if that too is absent, to the deployment default. The consolidation guidance of OVOS-SESSION-1 §3.2.7 is informative for downstream stages but does not apply to this entry-point field, which has only the two normative sources named here. | -**Migration from `recognizer_loop:utterance`.** Pre-spec -deployments use the topic name `recognizer_loop:utterance` for -the same purpose. That name pre-dates the naming conventions of -OVOS-MSG-1 §2.1.2: it uses `:` as a segment separator (where `:` -is reserved for `:` dispatch shapes), and -its leading segment names an implementation role (the audio-input -"recognizer loop") rather than a stable assistant root. A -deployment migrating to this specification **SHOULD** emit -`ovos.utterance.handle` as the canonical entry topic. A -transitional deployment **MAY** subscribe to both names during -migration; the legacy topic carries no normative status under -this specification. +`ovos.utterance.handle` is the only entry topic name this +specification recognizes. A conformant orchestrator subscribes to +this topic; a conformant producer emits to it. ### 9.2 `ovos.intent.matched` From 18099a24e631225720b83666f0d3e1fc16bf0e94 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 14:47:27 +0100 Subject: [PATCH 26/57] =?UTF-8?q?PIPELINE-1=20=C2=A74,=20=C2=A79.1:=20matc?= =?UTF-8?q?h=20signature=20takes=20explicit=20optional=20lang;=20orchestra?= =?UTF-8?q?tor=20MUST=20NOT=20synthesize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4 match contract: signature becomes match(utterance, lang, session). lang is OPTIONAL, sourced from Message.data.lang. Orchestrator passes it through when present; MUST NOT synthesize from session.lang or any §3.2 signal when absent. Plugin that needs language and receives lang=None MAY consult session for §3.2 signals or apply its own policy. Same shape and rationale as TRANSFORM-1 §3.0. §9.1 entry-topic lang field: rewritten. Previous wording said the orchestrator MUST fall back to session.lang — that was wrong; the orchestrator MUST NOT fabricate a value. data.lang is present only when the producer authoritatively knows the content language; its absence is a faithful 'unknown' signal that consumers resolve per their own stage-appropriate policy. §6.1 pseudocode and §11 conformance refs updated for the new match signature. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pipeline.md b/pipeline.md index c2ac618..3fa04c2 100644 --- a/pipeline.md +++ b/pipeline.md @@ -265,7 +265,7 @@ behaviour. A plugin exposes one operation to the orchestrator: ``` -match(utterance, session) → Match | None +match(utterance, lang, session) → Match | None ``` Inputs: @@ -279,6 +279,18 @@ Inputs: language. A plugin is free to consider all candidates, only the first, or any subset; the orchestrator does not prescribe how candidates are weighted. +- `lang` — the **optional** BCP-47 content-language tag of the + utterance, sourced by the orchestrator from `Message.data.lang` + of the entry-topic Message (§9.1). **Present only when the + producer authoritatively knew the content language; absent + otherwise.** The orchestrator **MUST NOT** synthesize a value + when `Message.data.lang` is absent — in particular, it + **MUST NOT** fall back to `session.lang` or to any per-utterance + language signal of OVOS-SESSION-1 §3.2. A plugin that needs a + language and receives `lang: None` **MAY** consult `session` + for OVOS-SESSION-1 §3.2 signals or apply its own resolution + policy — the choice is the plugin's. A plugin that does not + need the language ignores the parameter. - `session` — the session carrier from `context.session` of the utterance Message (OVOS-MSG-1 §4, OVOS-SESSION-1). @@ -598,7 +610,7 @@ ovos.utterance.handle ← entry (§9.1) │ ├─ for pipeline_id in effective pipeline: │ plugin = loaded_plugins[pipeline_id] # skip if not loaded - │ match = plugin.match(utterance, session) + │ match = plugin.match(utterance, lang, session) │ if match is not None: │ orchestrator-backstop denylist check (§5.3/§5.4) │ if filtered: continue @@ -908,7 +920,7 @@ Payload shape: | Field | Type | Required | Meaning | |-------|------|----------|---------| | `utterances` | array of strings | yes | One or more candidate utterance strings. | -| `lang` | string | no | BCP-47 language tag of the utterance. If absent, the orchestrator **MUST** fall back to `session.lang` (OVOS-SESSION-1 §3.2.1) — the session-scoped user-preference signal — and if that too is absent, to the deployment default. The consolidation guidance of OVOS-SESSION-1 §3.2.7 is informative for downstream stages but does not apply to this entry-point field, which has only the two normative sources named here. | +| `lang` | string | no | BCP-47 language tag of the utterance. **Present only when the producer authoritatively knows the content language** (e.g. a chat client emitting text it locally typed in `de-DE`, or an audio service emitting text from an STT decoder run in `en-US`). When absent, the content language is **not authoritatively known**; the orchestrator **MUST NOT** synthesize a value (in particular, **MUST NOT** fall back to `session.lang` or any per-utterance language signal of OVOS-SESSION-1 §3.2). The absence is propagated through to consumers (pipeline plugins, transformers, skills), each of which decides how to resolve language per its own policy — typically by consulting OVOS-SESSION-1 §3.2 signals (user preference, lang-detect signals) and applying its stage-appropriate consolidation. | `ovos.utterance.handle` is the only entry topic name this specification recognizes. A conformant orchestrator subscribes to @@ -1102,7 +1114,7 @@ from the hosting process. ### A **pipeline plugin** **MUST**: -- expose a `match(utterance, session) → Match | None` operation +- expose a `match(utterance, lang, session) → Match | None` operation (§4); - be **side-effect-free during `match`** (§4.2) — no Messages emitted, no state changed beyond what is needed to decide; From 8fc725dae001be39838ac0c52c1b38f590d8c1e3 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Mon, 25 May 2026 15:18:30 +0100 Subject: [PATCH 27/57] =?UTF-8?q?PIPELINE-1:=20post-audit=20fixes=20?= =?UTF-8?q?=E2=80=94=20Match.lang,=20empty-list=20short-circuit,=20cancell?= =?UTF-8?q?ation=20terminal=20row,=20conformance=20entry-topic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four audit findings actioned: - §4.1 Match shape: add optional 'lang' field. Resolves the TRANSFORM-1 §3.0 contradiction where intent transformers were told to read Match.lang as the language source, but Match didn't declare the field. Authoritative-by-construction for a plugin that received a non-None lang parameter; plugins that determine lang otherwise (multilingual matcher, content- language detector, hard-coded engine) MAY set it freely; absent when the plugin doesn't commit to a language. - §6.2 empty-list short-circuit: when the utterance-transformer chain returns an empty list, the orchestrator MUST NOT invoke any pipeline plugin (match()'s input contract is non-empty). Proceeds directly to complete_intent_failure. Cancellation context (TRANSFORM-1 §8.1) on the same return takes precedence over no-match. - §6.4 terminal-events table: cancellation added as a third row, cross-referencing TRANSFORM-1 §8.2. The 'every utterance ends in handled' invariant remains intact. - §11 conformance: subscribe to ovos.utterance.handle directly, dropping the stale 'name deferred; current deployments use recognizer_loop:utterance' bullet. §9.1 has already prescribed the new name; the conformance bullet was contradicting its own spec. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pipeline.md b/pipeline.md index 3fa04c2..3280b9c 100644 --- a/pipeline.md +++ b/pipeline.md @@ -303,6 +303,7 @@ fields below. |-------|------|----------|---------| | `owner_id` | string | yes | The `skill_id` of the skill that owns the handler, **or** the `pipeline_id` of the plugin itself if the plugin bundles its own handler. | | `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | +| `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | | `utterance` | string | no | The specific candidate string from the input list that won the match. | @@ -655,6 +656,16 @@ Pseudocode is informative; normative rules are in §§4–9. For each utterance, the orchestrator **MUST**: +- run the utterance-transformer and metadata-transformer chains + (OVOS-TRANSFORM-1 §3.2, §3.3) before pipeline iteration begins; +- if the utterance-transformer chain returns an **empty + utterance list**, skip pipeline iteration entirely and proceed + directly to `complete_intent_failure` (§9.3) — `match()` is + contractually defined over a non-empty list (§4) and the + orchestrator **MUST NOT** invoke any plugin with an empty + list. (If the empty list arrived together with cancellation + context per OVOS-TRANSFORM-1 §8.1, the cancellation terminal + path of §8.2 there takes precedence over no-match here.) - iterate `session.pipeline` in order; - for each `pipeline_id`, call `match` on the corresponding loaded plugin (skipping unknown identifiers, §5); @@ -696,13 +707,14 @@ keyed on `session.session_id` (per OVOS-MSG-1 §5.4 — ### 6.4 Terminal events -Every utterance terminates in exactly one of two ways, each +Every utterance terminates in exactly one of three ways, each followed by the universal end-marker `ovos.utterance.handled`: | Outcome | Sequence of utterance-layer events | |---------|------------------------------------| | Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.handled` | | No plugin matched | `complete_intent_failure` → `ovos.utterance.handled` | +| Cancelled by a transformer | `ovos.utterance.cancelled` → `ovos.utterance.handled` (see OVOS-TRANSFORM-1 §8.2) | If a dispatched handler emits `ovos.intent.handler.error` (§8) instead of `.complete`, the orchestrator still emits @@ -1091,9 +1103,8 @@ from the hosting process. ### An **orchestrator** **MUST**: -- subscribe to the utterance-layer entry topic (§9.1) — name - deferred to a future spec; current deployments use - `recognizer_loop:utterance`; +- subscribe to the utterance-layer entry topic + `ovos.utterance.handle` (§9.1); - run every received utterance through the lifecycle of §6 exactly once; - emit `ovos.utterance.handled` (§9.5) exactly once per From 9b17366032c202a782e83d4dae0ca593b5b6dbc7 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 00:21:09 +0100 Subject: [PATCH 28/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.3:=20reserved-intent?= =?UTF-8?q?-name=20and=20dispatch-suppression=20mechanism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a §7 subsection codifying the registry mechanism by which other specs reserve intent_names and modify dispatch semantics. Currently in force: converse and response (both per OVOS-CONVERSE-1, PR #25), both using dispatch suppression (the reserving spec's match-phase or delivery-path code emits the dispatch directly; the orchestrator MUST NOT re-dispatch on the returned Match). Renumbers prior §7.3 (in-process equivalence) to §7.4. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index 3280b9c..4fd534f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -805,7 +805,43 @@ listening to (a configuration bug) **MUST NOT** run the handler and **SHOULD** log the discrepancy. The orchestrator does not police subscriptions. -### 7.3 In-process equivalence +### 7.3 Reserved intent_names and dispatch suppression + +Other normative specifications **MAY** reserve specific +`intent_name` values for orchestrator-internal dispatch semantics +that diverge from §7's "match returns Match → orchestrator +dispatches" flow. When a specification reserves an intent_name: + +- skills and pipelines **MUST NOT** register intents under that + name (OVOS-INTENT-4 §6 — registration MUST be rejected by the + orchestrator); +- the orchestrator **MUST** recognise the reservation and apply + whatever dispatch-modification rule the reserving specification + defines. + +The most common modification is **dispatch suppression**: a +plugin's `match` returns a `Match` whose `intent_name` is the +reserved value, but the dispatch has *already happened* during +the plugin's match-phase work, so the orchestrator emits +`ovos.intent.matched` (§9) and the universal end-marker (§9) but +**MUST NOT** emit a second `:` dispatch on +the topic. Suppression scopes strictly to matches whose +`intent_name` is the reserved value; all other matches dispatch +normally per §7. + +Reservations currently in force: + +| Reserved intent_name | Reserving spec | Modification | +|----------------------|----------------|--------------| +| `converse` | OVOS-CONVERSE-1 §4.3 | dispatch suppression (the converse plugin has already polled the claimant via the same dispatch topic during match) | +| `response` | OVOS-CONVERSE-1 §5.2 | dispatch suppression (the orchestrator itself emits the `:response` delivery dispatch directly from the response-mode delivery path) | + +A reservation costs namespace and is paid only when the reserving +specification's dispatch model strictly requires it; this +specification fixes only the registry mechanism (reservation + +suppression), not the individual reservations. + +### 7.4 In-process equivalence When the handler-owning component (skill or plugin) runs in the same process as the orchestrator, the orchestrator **MAY** invoke From f10f1b756a3a64ffb98b3cc68833d2d83c183f68 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 00:40:35 +0100 Subject: [PATCH 29/57] =?UTF-8?q?PIPELINE-1=20=C2=A74.2,=20=C2=A77.0,=20?= =?UTF-8?q?=C2=A77.1,=20=C2=A77.3:=20relax=20match=20contract;=20explicit?= =?UTF-8?q?=20polymorphism;=20drop=20dispatch=20suppression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three coordinated changes to make the pipeline-plugin model fully uniform and to remove a structural exception that OVOS-CONVERSE-1 required: - §4.2 — drop the "plugin MUST NOT emit during match" prohibition. The match contract is the single obligation: return a Match or null. Plugins MAY emit bus Messages during match if their matching strategy requires it (a converse plugin polling active handlers is the canonical case). The spec retains a SHOULD NOT on session-field mutation during match — declined-match corruption is still real for the few session fields used by later iteration stages, but bus emissions are not policed. - §7.0 (new) — codify identifier polymorphism explicitly. Three component shapes own dispatched handlers: plain skill, plain pipeline plugin with bundled handler, hybrid plugin-skill. For the hybrid case, "if a pipeline plugin registers any intent under OVOS-INTENT-4, that plugin's pipeline_id MUST equal its skill_id" — one identifier, two roles. skill_id conceptually names the voice application; pipeline_id names the matching engine; a component that is both carries one identifier filling both roles. - §7.1 — replace the exclusive "stamp skill_id XOR pipeline_id" rule with two independent stamps. context["skill_id"] applies when owner_id is registered as a skill; context["pipeline_id"] applies when owner_id is loaded as a pipeline plugin. For a hybrid plugin-skill both stamps land and carry the same identifier. - §7.3 — simplify the reserved-intent-name registry. Drop the "dispatch suppression" clause that bent §6's match-then- dispatch flow. A reservation is now a namespace lease only: skills can't register the name, and a pipeline plugin's Match bearing the name dispatches normally per §7 (orchestrator emits :, handler subscribed to that topic runs). Updated the converse/response entries in the table to reflect this — the converse plugin polls active handlers via whatever wire shape OVOS-CONVERSE-1 defines (no longer the dispatch topic), then returns a Match that the orchestrator dispatches normally. Also updates §4.1 Match.owner_id description to cite §7.0's three-shape model rather than the prior implicit OR. Companion PR: OVOS-CONVERSE-1 (#25) needs a matching restructure to drop its dispatch-suppression dependency and rely on the relaxed §4.2 instead. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 171 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 60 deletions(-) diff --git a/pipeline.md b/pipeline.md index 4fd534f..d9fc2bd 100644 --- a/pipeline.md +++ b/pipeline.md @@ -301,7 +301,7 @@ fields below. | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id` | string | yes | The `skill_id` of the skill that owns the handler, **or** the `pipeline_id` of the plugin itself if the plugin bundles its own handler. | +| `owner_id` | string | yes | The identifier of the handler-owner — a `skill_id`, a `pipeline_id`, or (for a hybrid plugin-skill per §7.0) an identifier that is **both** a `skill_id` and a `pipeline_id` (the two roles share one identifier). The orchestrator interprets `` uniformly across the three shapes; the dispatch topic shape is the same. | | `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | @@ -312,20 +312,33 @@ claim. It does not score, rank, or rerank matches across plugins — **first match wins** (§6). A plugin that wants to express uncertainty must return `None` and let a later plugin claim. -### 4.2 Plugins are side-effect-free during `match` - -A plugin **MUST NOT** emit Messages from inside its `match` -operation. `match` is a pure decide-or-decline call. Any side -effects — activating a skill, updating internal session state, -storing conversational history — happen after the orchestrator -has confirmed the claim wins, in the plugin's own handler -(reached via the dispatch topic of §7) or in some other plugin- -internal step the spec does not constrain. - -This is the difference between "matching" and "running": the -orchestrator may call `match` on several plugins before one -claims; plugins that took side effects from declined matches -would corrupt each other's state. +### 4.2 The match contract is the single obligation + +The plugin's `match` operation has **one obligation**: return a +`Match` (§4.1) or `null`. The orchestrator does not constrain +anything else about what `match` does internally — emitting bus +Messages during `match` is **allowed** (a plugin that polls +other components, calls out to a model server, asks the user a +disambiguation question, or runs any other matching strategy +that requires bus communication is conformant), and side effects +on plugin-internal state are the plugin's own business. + +A plugin that takes side effects from declined matches MUST +understand that the orchestrator may call `match` on multiple +plugins before one claims (§6.2 first-match-wins); plugins that +mutate shared state from `match` MUST tolerate being declined +and called again, on a later utterance, with their prior side +effects already applied. The spec **does not police** plugin +internals beyond the return contract. + +A plugin **SHOULD NOT** mutate `session` fields directly from +`match` — `session` carries downstream-visible state (`pipeline` +ordering, blacklists, intent_context, response_mode, active +handlers) that a declined plugin would otherwise corrupt for the +next plugin in iteration. Session mutations happen properly in +the dispatched handler (§7), or via the direct-mutation pathways +the field-owning specifications define (OVOS-CONTEXT-1 §5.3, +OVOS-CONVERSE-1 §3.2, OVOS-TRANSFORM-1 §3.3). ### 4.3 The capture map @@ -734,13 +747,41 @@ on the topic: : ``` -where `` is `Match.owner_id` (a `skill_id` or a -`pipeline_id`) and `` is `Match.intent_name`. - -The `:` between the two segments is the only one in the topic: -`skill_id`, `pipeline_id`, and `intent_name` are bound by -OVOS-MSG-1 §2.1.1 (no `:`, no `.`, no whitespace), so the split is -unambiguous. +where `` is `Match.owner_id` and `` is +`Match.intent_name`. Both segments are bound by OVOS-MSG-1 +§2.1.1 (no `:`, no `.`, no whitespace), so the single `:` split +is unambiguous. + +### 7.0 Identifier polymorphism — `` is one identifier + +`` names the **handler-owner** of the matched intent. +It is a single identifier string, used identically on the +dispatch topic regardless of whether the handler is owned by a +plain skill, a plain pipeline plugin, or a pipeline plugin that +also registers intents. + +The spec recognises three component shapes that own dispatched +handlers: + +| Component shape | Registers intents under OVOS-INTENT-4 | Loaded as pipeline plugin under §3 | Identifier role | +|-----------------|---------------------------------------|------------------------------------|-----------------| +| **Plain skill** | yes | no | identifier is the `skill_id`; `pipeline_id` does not apply | +| **Plain pipeline plugin** (matches only; no bundled handler) | no | yes | identifier is the `pipeline_id`; no dispatch lands on it | +| **Plain pipeline plugin with bundled handler** (e.g., fallback, persona) | no | yes | identifier is the `pipeline_id`; dispatch lands on `:` | +| **Hybrid plugin-skill** (matches AND registers intents AND handles them) | yes | yes | **identifier is shared: `pipeline_id == skill_id`**, both equal to the single `` | + +The hybrid case is normative: **if a pipeline plugin registers +any intent under OVOS-INTENT-4, that plugin's `pipeline_id` MUST +equal its `skill_id`** — they are the same identifier under two +roles, not two identifiers that happen to coincide. The +identifier `` carries both meanings (matching engine +identity and handler-owner identity) for hybrid components. + +Conceptually, `skill_id` names the **voice application** +(handler-owner, user-facing); `pipeline_id` names the **matching +engine** (orchestrator-facing). A component that is both +(persona, common-query, OCP, converse) carries one identifier +that fills both roles. ### 7.1 Routing and payload @@ -751,15 +792,26 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): (OVOS-MSG-1 §5.2) — the orchestrator derives the dispatch via `reply`, so `destination` is the original utterance emitter and `source` is the orchestrator; -- when `` is a `skill_id`, the orchestrator **MUST** - stamp `context["skill_id"]` to that `skill_id`. This carries - the skill's identity forward into every Message the handler - emits via `forward` (OVOS-MSG-1 §5.1) — the skill inherits the - context, satisfying OVOS-INTENT-4 §3.1 by construction. When - `` is a `pipeline_id` (plugin-bundled handler), the - orchestrator **MUST NOT** stamp `context["skill_id"]` — - plugin-bundled handlers identify themselves via `pipeline_id`, - not `skill_id`. +- **`context["skill_id"]` stamping.** When the dispatched + `` is registered as a skill under OVOS-INTENT-4 + (whether as a plain skill or as a hybrid plugin-skill per + §7.0), the orchestrator **MUST** stamp + `context["skill_id"] = `. This carries the skill's + identity forward into every Message the handler emits via + `forward` (OVOS-MSG-1 §5.1), satisfying OVOS-INTENT-4 §3.1 + by construction. When `` denotes a plain pipeline + plugin with no intent registrations (a pipeline-bundled + handler that is not also a skill), the orchestrator **MUST + NOT** stamp `context["skill_id"]` — such handlers identify + themselves via `pipeline_id` only; +- **`context["pipeline_id"]` stamping.** When the dispatched + `` corresponds to a loaded pipeline plugin (whether + as a plain plugin with bundled handler or as a hybrid + plugin-skill per §7.0), the orchestrator **MUST** stamp + `context["pipeline_id"] = `. For a hybrid + plugin-skill both `context["skill_id"]` and + `context["pipeline_id"]` carry the same identifier; consumers + reading either key get the same value. Any Message the skill subsequently emits **MUST** carry `context["skill_id"]` matching the `` of the dispatch @@ -805,41 +857,40 @@ listening to (a configuration bug) **MUST NOT** run the handler and **SHOULD** log the discrepancy. The orchestrator does not police subscriptions. -### 7.3 Reserved intent_names and dispatch suppression +### 7.3 Reserved intent_names Other normative specifications **MAY** reserve specific -`intent_name` values for orchestrator-internal dispatch semantics -that diverge from §7's "match returns Match → orchestrator -dispatches" flow. When a specification reserves an intent_name: - -- skills and pipelines **MUST NOT** register intents under that - name (OVOS-INTENT-4 §6 — registration MUST be rejected by the - orchestrator); -- the orchestrator **MUST** recognise the reservation and apply - whatever dispatch-modification rule the reserving specification - defines. - -The most common modification is **dispatch suppression**: a -plugin's `match` returns a `Match` whose `intent_name` is the -reserved value, but the dispatch has *already happened* during -the plugin's match-phase work, so the orchestrator emits -`ovos.intent.matched` (§9) and the universal end-marker (§9) but -**MUST NOT** emit a second `:` dispatch on -the topic. Suppression scopes strictly to matches whose -`intent_name` is the reserved value; all other matches dispatch -normally per §7. +`intent_name` values for matches produced by a particular +pipeline plugin role. A reserved intent_name is one that: + +- skills and pipelines **MUST NOT** register under OVOS-INTENT-4 + (the orchestrator MUST reject registrations bearing a reserved + name, per OVOS-INTENT-4 §3.5); +- a pipeline plugin **MAY** emit as the `intent_name` of a + returned `Match` to signal "this match was produced by the + role that reserves the name"; the dispatch then proceeds + normally per §7, addressed to `:`, + and the handler subscribed to that topic does whatever the + reserving specification defines. + +A reservation is a **namespace lease**, not a dispatch +modification. Every dispatch in this specification — including +dispatches on reserved intent_names — fires §7.1 stamping, +§7.2 routing, and §8 handler-trio identically. The reserving +specification gets exclusive use of the name across the +deployment's skill set; it gets no other privilege. Reservations currently in force: -| Reserved intent_name | Reserving spec | Modification | -|----------------------|----------------|--------------| -| `converse` | OVOS-CONVERSE-1 §4.3 | dispatch suppression (the converse plugin has already polled the claimant via the same dispatch topic during match) | -| `response` | OVOS-CONVERSE-1 §5.2 | dispatch suppression (the orchestrator itself emits the `:response` delivery dispatch directly from the response-mode delivery path) | +| Reserved intent_name | Reserving spec | Meaning of a Match bearing this name | +|----------------------|----------------|--------------------------------------| +| `converse` | OVOS-CONVERSE-1 §4 | a converse plugin's claim that `` (an active handler) wants this utterance — the orchestrator dispatches `:converse` and the owner's converse handler runs | +| `response` | OVOS-CONVERSE-1 §5 | a converse plugin's signal that `` (the response-mode holder) is to receive the awaited utterance — the orchestrator dispatches `:response` and the owner's response handler runs | -A reservation costs namespace and is paid only when the reserving -specification's dispatch model strictly requires it; this -specification fixes only the registry mechanism (reservation + -suppression), not the individual reservations. +This specification fixes only the registry mechanism (reservation +listing); the per-name semantics are owned by the reserving +specification. Other specifications MAY reserve further names by +adding rows to this table in their own PR. ### 7.4 In-process equivalence From 3891deb25725b030ef38d1b9107c318ebe7896d5 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 00:46:05 +0100 Subject: [PATCH 30/57] =?UTF-8?q?PIPELINE-1=20=C2=A74.1,=20=C2=A74.2,=20?= =?UTF-8?q?=C2=A76:=20Match.updated=5Fsession=20as=20the=20match-phase=20s?= =?UTF-8?q?ession-mutation=20channel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per current ovos-core (sess = match.updated_session or SessionManager.get(message)), a plugin's match operation MAY mutate session state and return the new snapshot via the optional Match.updated_session field. The orchestrator then uses that snapshot for the dispatch and every downstream stage; a declining plugin returns null, so its experimental mutations are discarded at the plugin boundary and never reach later plugins in iteration. Updates: - §4.1 Match table: add updated_session optional field - §4.2: explicit mechanism + 'plugins that mutate session in place without populating updated_session are non-conformant' - §6 flow diagram: insert 'session = match.updated_session or session' immediately after a non-null match, before the post-match-pre-dispatch window This is what makes match-phase mutation safe under §6.2 first-match-wins: only the claiming plugin's session changes land, declined plugins' mutations are scoped to their match call and discarded. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 66 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/pipeline.md b/pipeline.md index d9fc2bd..ca4fc00 100644 --- a/pipeline.md +++ b/pipeline.md @@ -306,6 +306,7 @@ fields below. | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | | `utterance` | string | no | The specific candidate string from the input list that won the match. | +| `updated_session` | object | no | A replacement `session` snapshot the plugin produced during `match` (§4.2). When present, the orchestrator MUST use this snapshot — in place of the inbound utterance's session — for the dispatch and every downstream stage. When absent, the inbound session is carried unchanged. This is the **only** mechanism by which a plugin's match-phase session mutations reach downstream consumers; in-place mutations of the inbound session object are not visible past the plugin boundary. | The orchestrator interprets a non-`None` return as a definitive claim. It does not score, rank, or rerank matches across plugins — @@ -323,22 +324,53 @@ disambiguation question, or runs any other matching strategy that requires bus communication is conformant), and side effects on plugin-internal state are the plugin's own business. -A plugin that takes side effects from declined matches MUST -understand that the orchestrator may call `match` on multiple -plugins before one claims (§6.2 first-match-wins); plugins that -mutate shared state from `match` MUST tolerate being declined -and called again, on a later utterance, with their prior side -effects already applied. The spec **does not police** plugin -internals beyond the return contract. - -A plugin **SHOULD NOT** mutate `session` fields directly from -`match` — `session` carries downstream-visible state (`pipeline` -ordering, blacklists, intent_context, response_mode, active -handlers) that a declined plugin would otherwise corrupt for the -next plugin in iteration. Session mutations happen properly in -the dispatched handler (§7), or via the direct-mutation pathways -the field-owning specifications define (OVOS-CONTEXT-1 §5.3, -OVOS-CONVERSE-1 §3.2, OVOS-TRANSFORM-1 §3.3). +**Session mutation via `Match.updated_session`.** A plugin MAY +mutate session state as part of producing a Match — for +example, an intent plugin setting `session.intent_context` on +the match dispatch, a converse plugin setting +`session.response_mode` because the matched intent enables a +follow-up wait window, or any plugin reordering +`session.pipeline` for subsequent utterances on this session. +The mutation is communicated to the orchestrator via the +`updated_session` field on the returned `Match` (§4.1): the +plugin populates `updated_session` with the new session +snapshot it wants downstream consumers to see, and the +orchestrator MUST use that snapshot for the dispatch and every +subsequent stage of the utterance lifecycle (§6). When +`updated_session` is absent, the inbound utterance's session is +carried unchanged. + +The `updated_session` pathway is **only effective for a claiming +match**. A plugin that returns `null` (declines) does not return +any `Match`, and therefore any session mutation it performed +during its match call is **discarded** at the plugin boundary: +the orchestrator continues iteration with the inbound session +snapshot, untouched. This is what makes match-phase mutation +safe under §6.2 first-match-wins iteration — a declined +plugin's exploratory mutations never reach later plugins or +downstream stages. + +The orchestrator-side pattern is uniform: + +``` +match = plugin.match(utterances, lang, session) +if match is not None: + session = match.updated_session or session + # dispatch and downstream stages use this session +``` + +A plugin that mutates the inbound session object **in place** +without populating `updated_session` is non-conformant — the +in-place mutation may or may not be visible to the orchestrator +depending on object identity, and the field is the spec's only +guaranteed-visible channel. Plugins that need to mutate session +state **MUST** do so via a fresh snapshot returned in +`updated_session`, not via in-place mutation. + +The §6 flow diagram reflects this: `session = match.updated_session +or session` is applied immediately after a non-null match, +before the post-match-pre-dispatch window where intent +transformers and CONTEXT-1's decay tick run. ### 4.3 The capture map @@ -629,6 +661,8 @@ ovos.utterance.handle ← entry (§9.1) │ orchestrator-backstop denylist check (§5.3/§5.4) │ if filtered: continue │ + │ session = match.updated_session or session # §4.1, §4.2 + │ │ ┌── post-match-pre-dispatch window ──────────────┐ │ │ engine-side context promotion (CONTEXT-1 §5.3) │ │ │ intent-transformer chain runs (TRANSFORM-1 │ From 3b82c2c0d67b49caba3867f00cb2b0bc5f1335e2 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 00:47:37 +0100 Subject: [PATCH 31/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.0,=20=C2=A77.1:=20co?= =?UTF-8?q?llapse=20handler-owner=20shapes;=20pipeline=5Fid=20=3D=3D=20ski?= =?UTF-8?q?ll=5Fid=20always?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per design feedback: the prior 'hybrid plugin-skill vs plain plugin with bundled handler' distinction wasn't load-bearing. Collapse to two shapes: - Plain skill: handler reached via skill_id, intents matched by some other plugin via INTENT-4 registrations - Pipeline plugin with bundled handlers: handler reached via pipeline_id == skill_id (the plugin owns its own intent matching and handler dispatch); MUST NOT register its intent_names under INTENT-4 (would create circular dependency) Passive-index registration: a pipeline plugin with handlers SHOULD publish its intent_names via the per-pipeline introspection topic ovos.pipeline..intents.list so deployment-wide tools that enumerate handlers can see them. This is one-way declaration, not INTENT-4 registration. §7.1 stamping simplified accordingly: - context['skill_id'] = owner_id ALWAYS (skill_id is the universal voice-app identity) - context['pipeline_id'] = owner_id WHEN owner is loaded as a plugin (both keys carry the same value for plugin-handlers) §4.1 Match.owner_id description simplified to two shapes. skill_id is conceptually the voice_app_id; pipeline_id is the matching-engine id; a plugin that is both carries one identifier filling both roles. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 105 +++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 47 deletions(-) diff --git a/pipeline.md b/pipeline.md index ca4fc00..b93d2d3 100644 --- a/pipeline.md +++ b/pipeline.md @@ -301,7 +301,7 @@ fields below. | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id` | string | yes | The identifier of the handler-owner — a `skill_id`, a `pipeline_id`, or (for a hybrid plugin-skill per §7.0) an identifier that is **both** a `skill_id` and a `pipeline_id` (the two roles share one identifier). The orchestrator interprets `` uniformly across the three shapes; the dispatch topic shape is the same. | +| `owner_id` | string | yes | The handler-owner's identity — the `skill_id` of the dispatched handler. For a pipeline plugin with bundled handlers, this is the plugin's identity, which is **the same identifier** as its `pipeline_id` (§7.0). The dispatch topic shape `:` is uniform across both handler-owner shapes. | | `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | @@ -789,33 +789,46 @@ is unambiguous. ### 7.0 Identifier polymorphism — `` is one identifier `` names the **handler-owner** of the matched intent. -It is a single identifier string, used identically on the -dispatch topic regardless of whether the handler is owned by a -plain skill, a plain pipeline plugin, or a pipeline plugin that -also registers intents. - -The spec recognises three component shapes that own dispatched -handlers: - -| Component shape | Registers intents under OVOS-INTENT-4 | Loaded as pipeline plugin under §3 | Identifier role | -|-----------------|---------------------------------------|------------------------------------|-----------------| -| **Plain skill** | yes | no | identifier is the `skill_id`; `pipeline_id` does not apply | -| **Plain pipeline plugin** (matches only; no bundled handler) | no | yes | identifier is the `pipeline_id`; no dispatch lands on it | -| **Plain pipeline plugin with bundled handler** (e.g., fallback, persona) | no | yes | identifier is the `pipeline_id`; dispatch lands on `:` | -| **Hybrid plugin-skill** (matches AND registers intents AND handles them) | yes | yes | **identifier is shared: `pipeline_id == skill_id`**, both equal to the single `` | - -The hybrid case is normative: **if a pipeline plugin registers -any intent under OVOS-INTENT-4, that plugin's `pipeline_id` MUST -equal its `skill_id`** — they are the same identifier under two -roles, not two identifiers that happen to coincide. The -identifier `` carries both meanings (matching engine -identity and handler-owner identity) for hybrid components. - -Conceptually, `skill_id` names the **voice application** -(handler-owner, user-facing); `pipeline_id` names the **matching -engine** (orchestrator-facing). A component that is both -(persona, common-query, OCP, converse) carries one identifier -that fills both roles. +It is a single identifier string used identically on the +dispatch topic regardless of whether the handler-owner is a +plain skill or a pipeline plugin that owns its own handlers. + +Conceptually, `skill_id` is the **voice-application identity** — +the identity of whatever component the dispatch lands on. +`pipeline_id` is the **matching-engine identity** — the identity +of a component loaded as a pipeline plugin under §3. + +The spec recognises two handler-owner shapes: + +| Shape | How matches are produced | Identifier roles | +|-------|--------------------------|------------------| +| **Plain skill** | Some pipeline plugin (matching engine) consumes the skill's OVOS-INTENT-4 registrations and returns a `Match` whose `owner_id` is the skill's `skill_id`. | `skill_id` only; `pipeline_id` does not apply. | +| **Pipeline plugin with bundled handlers** (converse, fallback, common-query, persona, OCP, …) | The plugin's own `match` returns a `Match` whose `owner_id` is the plugin's own identity, on an `intent_name` the plugin defines for itself. | `pipeline_id == skill_id` — both names refer to the same identifier. The plugin **MUST NOT** register its bundled-handler intents under OVOS-INTENT-4 (they are not skill registrations and would create a circular dependency through whichever matcher consumes the registry). | + +The pipeline-plugin-with-handlers case is normative: **if a +pipeline plugin emits matches whose `owner_id` equals its own +`pipeline_id`, that identifier is also its `skill_id`** for the +purposes of OVOS-INTENT-3, OVOS-INTENT-4, and every other spec +that addresses skills by id. They are not two identifiers that +happen to coincide; they are the same identifier under two +roles. + +**Passive index registration.** A pipeline plugin with bundled +handlers **SHOULD** publish the set of `intent_name` values it +owns through the per-pipeline introspection topic +`ovos.pipeline..intents.list` (§10). Observers and +introspection tools rely on this index to enumerate every +handler in the deployment; without it, plugin-owned handlers +are invisible to deployment-wide tooling that walks +OVOS-INTENT-4 only. This is **not** OVOS-INTENT-4 registration — +the plugin's matches do not flow through any external matcher — +it is a one-way declaration of "these are the intent_names I +dispatch on." + +From the dispatch path's perspective the two shapes are +indistinguishable. `:` carries no +information about which shape `` is; the orchestrator +applies §7.1 stamping uniformly. ### 7.1 Routing and payload @@ -826,26 +839,24 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): (OVOS-MSG-1 §5.2) — the orchestrator derives the dispatch via `reply`, so `destination` is the original utterance emitter and `source` is the orchestrator; -- **`context["skill_id"]` stamping.** When the dispatched - `` is registered as a skill under OVOS-INTENT-4 - (whether as a plain skill or as a hybrid plugin-skill per - §7.0), the orchestrator **MUST** stamp - `context["skill_id"] = `. This carries the skill's - identity forward into every Message the handler emits via - `forward` (OVOS-MSG-1 §5.1), satisfying OVOS-INTENT-4 §3.1 - by construction. When `` denotes a plain pipeline - plugin with no intent registrations (a pipeline-bundled - handler that is not also a skill), the orchestrator **MUST - NOT** stamp `context["skill_id"]` — such handlers identify - themselves via `pipeline_id` only; +- **`context["skill_id"]` stamping.** The orchestrator **MUST** + stamp `context["skill_id"] = ` on every dispatch. + `skill_id` is the voice-application identity (§7.0) — every + dispatched handler owns one, whether it is a plain skill or a + pipeline plugin with bundled handlers (in which case + `skill_id == pipeline_id`). This stamping carries the + handler-owner's identity forward into every Message the + handler emits via `forward` (OVOS-MSG-1 §5.1), satisfying + OVOS-INTENT-4 §3.1 by construction. - **`context["pipeline_id"]` stamping.** When the dispatched - `` corresponds to a loaded pipeline plugin (whether - as a plain plugin with bundled handler or as a hybrid - plugin-skill per §7.0), the orchestrator **MUST** stamp - `context["pipeline_id"] = `. For a hybrid - plugin-skill both `context["skill_id"]` and - `context["pipeline_id"]` carry the same identifier; consumers - reading either key get the same value. + `` corresponds to a loaded pipeline plugin (the + pipeline-plugin-with-bundled-handlers case of §7.0), the + orchestrator **MUST** additionally stamp + `context["pipeline_id"] = `. For such a handler + `context["skill_id"]` and `context["pipeline_id"]` carry the + same identifier; consumers reading either key get the same + value. For a plain skill (not loaded as a pipeline plugin), + the orchestrator **MUST NOT** stamp `context["pipeline_id"]`. Any Message the skill subsequently emits **MUST** carry `context["skill_id"]` matching the `` of the dispatch From eb72157289c9b988fb147bf941468754c6706b22 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 00:58:49 +0100 Subject: [PATCH 32/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.0:=20pure-matcher=20?= =?UTF-8?q?plugin=20shape;=20plain=20skill=20+=20reserved=20intent=5Fnames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clarifications to the handler-owner table: - A 'pure-matcher' pipeline plugin shape exists alongside the two handler-owner shapes. Padatious / Adapt (skill-intent matchers) and the OVOS-CONVERSE-1 converse plugin (reserved- intent-name matcher) are pure matchers: they are pipeline plugins per §3 but not handler-owners; their match returns matches whose owner_id is some OTHER component's identity. They have a pipeline_id but no skill_id. - A plain skill may also handle a reserved-intent-name dispatch topic when a companion spec defines one. A skill that implements a converse method subscribes to :converse via framework convention, not via INTENT-4 registration (which would be rejected per §7.3). The 'plain skill' row's INTENT-4 path is the normal case; reserved-name framework-convention subscriptions extend it without changing the dispatch shape. Removes 'converse' from the bundled-handler-plugin examples list — the converse plugin is a pure matcher, not a bundled-handler plugin. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index b93d2d3..be20672 100644 --- a/pipeline.md +++ b/pipeline.md @@ -803,7 +803,30 @@ The spec recognises two handler-owner shapes: | Shape | How matches are produced | Identifier roles | |-------|--------------------------|------------------| | **Plain skill** | Some pipeline plugin (matching engine) consumes the skill's OVOS-INTENT-4 registrations and returns a `Match` whose `owner_id` is the skill's `skill_id`. | `skill_id` only; `pipeline_id` does not apply. | -| **Pipeline plugin with bundled handlers** (converse, fallback, common-query, persona, OCP, …) | The plugin's own `match` returns a `Match` whose `owner_id` is the plugin's own identity, on an `intent_name` the plugin defines for itself. | `pipeline_id == skill_id` — both names refer to the same identifier. The plugin **MUST NOT** register its bundled-handler intents under OVOS-INTENT-4 (they are not skill registrations and would create a circular dependency through whichever matcher consumes the registry). | +| **Pipeline plugin with bundled handlers** (fallback, common-query, persona, OCP, …) | The plugin's own `match` returns a `Match` whose `owner_id` is the plugin's own identity, on an `intent_name` the plugin defines for itself. | `pipeline_id == skill_id` — both names refer to the same identifier. The plugin **MUST NOT** register its bundled-handler intents under OVOS-INTENT-4 (they are not skill registrations and would create a circular dependency through whichever matcher consumes the registry). | + +The examples in the second row are plugins whose own `match` +emits matches addressed back to itself. A different shape — a +**pure-matcher** pipeline plugin (Padatious / Adapt for skill +intents; the converse plugin of OVOS-CONVERSE-1 for the +reserved intent_names `converse` / `response`) — is also a +pipeline plugin per §3 but is **not a handler-owner**: its +`match` returns matches whose `owner_id` is some *other* +component's identity. Pure-matcher plugins have only a +`pipeline_id`; they have no `skill_id` and they own no +dispatch-handler subscription. §7.0's table only enumerates +handler-owner shapes (the targets of dispatch); pure-matcher +plugins live alongside both rows. + +A **plain skill** may also handle a reserved-intent-name +dispatch topic when a companion specification defines one. For +example, a skill that defines a `converse` method (OVOS-CONVERSE-1 +§9.3) subscribes to `:converse` via framework +convention, not via OVOS-INTENT-4 registration — the reserved +name is not registrable (§7.3). The plain-skill row above lists +INTENT-4-registered intents as the *normal* path; reserved +intent_names handled by framework convention extend it without +changing the dispatch shape. The pipeline-plugin-with-handlers case is normative: **if a pipeline plugin emits matches whose `owner_id` equals its own From 998c84a6c6243d2aeceb58d9102efbd2ebe26c34 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 01:22:24 +0100 Subject: [PATCH 33/57] =?UTF-8?q?PIPELINE-1=20=C2=A74.2,=20=C2=A76:=20tigh?= =?UTF-8?q?ten=20match-phase=20scope;=20explicit=20null-match=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clarifications from the review: - §4.2 'plugins MUST mutate via updated_session' explicitly scoped to match-phase only. Handler-time, transformer-time, and MSG-1-direct-mutation pathways for session changes are governed by their own specs and unaffected by §4.2's rule. - §6 flow diagram restructured to show the null-match branch explicitly: 'if match is None: continue # any plugin-side updated_session is discarded'. Makes the F5 review point visible — declined plugins' updated_session never reaches the next iteration. Indentation adjusted accordingly for the subsequent ovos.intent.matched / dispatch / trio / end-marker block (one level shallower now that the inner-if collapsed). Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 58 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/pipeline.md b/pipeline.md index be20672..9f447a7 100644 --- a/pipeline.md +++ b/pipeline.md @@ -360,12 +360,20 @@ if match is not None: ``` A plugin that mutates the inbound session object **in place** -without populating `updated_session` is non-conformant — the -in-place mutation may or may not be visible to the orchestrator -depending on object identity, and the field is the spec's only -guaranteed-visible channel. Plugins that need to mutate session -state **MUST** do so via a fresh snapshot returned in -`updated_session`, not via in-place mutation. +during `match` without populating `updated_session` is +non-conformant — the in-place mutation may or may not be visible +to the orchestrator depending on object identity, and the field +is the only guaranteed-visible match-phase channel. Plugins that +need to mutate session state from `match` **MUST** do so via a +fresh snapshot returned in `updated_session`, not via in-place +mutation. + +This rule applies only to `match`-phase mutations. Session +mutation from **handlers** (under §7 dispatch), from +**transformers** (OVOS-TRANSFORM-1 §3), and from the **direct +session-mutation pathway** of OVOS-MSG-1 (which CONTEXT-1 §5.3 +and CONVERSE-1 §3.2 build on) is governed by those specs and is +unaffected by §4.2. The §6 flow diagram reflects this: `session = match.updated_session or session` is applied immediately after a non-null match, @@ -657,27 +665,29 @@ ovos.utterance.handle ← entry (§9.1) ├─ for pipeline_id in effective pipeline: │ plugin = loaded_plugins[pipeline_id] # skip if not loaded │ match = plugin.match(utterance, lang, session) - │ if match is not None: - │ orchestrator-backstop denylist check (§5.3/§5.4) - │ if filtered: continue + │ if match is None: + │ continue # any plugin-side updated_session is discarded │ - │ session = match.updated_session or session # §4.1, §4.2 + │ orchestrator-backstop denylist check (§5.3/§5.4) + │ if filtered: continue │ - │ ┌── post-match-pre-dispatch window ──────────────┐ - │ │ engine-side context promotion (CONTEXT-1 §5.3) │ - │ │ intent-transformer chain runs (TRANSFORM-1 │ - │ │ §3.4) — may modify Match.captures, MUST NOT │ - │ │ change owner_id / intent_name │ - │ │ post-decay turns_remaining-- (CONTEXT-1 §4) │ - │ └────────────────────────────────────────────────┘ + │ session = match.updated_session or session # §4.1, §4.2 │ - │ ovos.intent.matched (§9.2) - │ dispatch on : (§7) - │ (handler runs; emits lifecycle trio §8) - │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 - │ (TTS rendering; tts-transformer chain — §3.6) - │ ovos.utterance.handled (§9.5) - │ break + │ ┌── post-match-pre-dispatch window ──────────────┐ + │ │ engine-side context promotion (CONTEXT-1 §5.3) │ + │ │ intent-transformer chain runs (TRANSFORM-1 │ + │ │ §3.4) — may modify Match.captures, MUST NOT │ + │ │ change owner_id / intent_name │ + │ │ post-decay turns_remaining-- (CONTEXT-1 §4) │ + │ └────────────────────────────────────────────────┘ + │ + │ ovos.intent.matched (§9.2) + │ dispatch on : (§7) + │ (handler runs; emits lifecycle trio §8) + │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 + │ (TTS rendering; tts-transformer chain — §3.6) + │ ovos.utterance.handled (§9.5) + │ break │ └─ if no plugin matched (or all matches filtered): complete_intent_failure (§9.3) From 191b3f28c93f72b8f37e5caf1dc960cfdeb30994 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 02:33:25 +0100 Subject: [PATCH 34/57] =?UTF-8?q?PIPELINE-1=20=C2=A72:=20brief=20note=20on?= =?UTF-8?q?=20SESSION-2=20stateless-for-named,=20owns-default=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIPELINE-1's orchestrator role description predates SESSION-2 and didn't mention the statelessness model. A reader of PIPELINE-1 alone might assume the orchestrator is fully stateful. Add a one-paragraph note pointing to SESSION-2 as the owner of the state-ownership model and stating the working assumption: the orchestrator is stateless for named sessions and holds persistent state only for session_id == 'default'. Editorial; no normative behaviour change. Co-Authored-By: Claude Opus 4.7 (1M context) --- pipeline.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pipeline.md b/pipeline.md index 9f447a7..7949f0d 100644 --- a/pipeline.md +++ b/pipeline.md @@ -103,6 +103,14 @@ contract of §4 live in the orchestrator process that implements the utterance lifecycle (the utterance-handling service in the split shape above). +The orchestrator is **stateless for named sessions** and holds +persistent state only for the reserved `session_id == "default"` +(OVOS-SESSION-1 §3.1). The full state-ownership model is owned by +OVOS-SESSION-2; consumers of this spec MAY take it as a +working assumption that each inbound utterance brings its own +session and the orchestrator does not maintain cross-utterance +state for named sessions. + A **pipeline plugin** is a third-party component identified by an opaque `pipeline_id` — an arbitrary, deployment-unique string. The orchestrator loads some number of plugins at startup; how it From 159d28c521021317eaa19a8e258a9f852f716213 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 05:21:34 +0100 Subject: [PATCH 35/57] PIPELINE-1: fix three spec bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §11 conformance: remove incorrect "side-effect-free during match" MUST — §4.2 body explicitly permits bus emissions from match - §9.1: fix bad cross-reference §2.1.2 → §2.1 (MSG-1 has no §2.1.2) - §9.5: revert accidental status-field addition; payload MAY be empty Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pipeline.md b/pipeline.md index 7949f0d..444a8b7 100644 --- a/pipeline.md +++ b/pipeline.md @@ -1086,7 +1086,7 @@ that wants to feed an utterance into the assistant — a listener, a chat bridge, a CLI, a test harness, a remote-peer client. Receiving on this topic kicks off the lifecycle of §6. -The topic name follows the naming conventions of OVOS-MSG-1 §2.1.2: +The topic name follows the naming conventions of OVOS-MSG-1 §2.1: imperative-mood verb (`handle` — a request for the assistant to handle this utterance), dot-separated hierarchy, no `:` (which is reserved for component-pair dispatch topics), and pairs with the @@ -1300,8 +1300,6 @@ from the hosting process. - expose a `match(utterance, lang, session) → Match | None` operation (§4); -- be **side-effect-free during `match`** (§4.2) — no Messages - emitted, no state changed beyond what is needed to decide; - when claiming, return a `Match` with `owner_id` and `intent_name` per §4 — never a partial or speculative claim; - bear a `pipeline_id` distinct from any other loaded plugin's From 2e2e3e7c1744f1122c2f68cdd218661d92035d5e Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 14:22:38 +0100 Subject: [PATCH 36/57] =?UTF-8?q?MSG-1=20=C2=A72.1.1:=20update=20identifie?= =?UTF-8?q?r-constraint=20wording=20to=20per-topic-shape=20rule=20(PR=20#2?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pipeline.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pipeline.md b/pipeline.md index 444a8b7..e113749 100644 --- a/pipeline.md +++ b/pipeline.md @@ -157,12 +157,11 @@ not interpret the `pipeline_id` string beyond using it as a key. Constraints on `pipeline_id` strings: - Non-empty. -- Bound by OVOS-MSG-1 §2.1.1 (identifiers used as topic - components): no `:`, no `.`, no whitespace, ASCII letters / - digits / `_` / `-` only. The constraint is necessary because - `pipeline_id` may appear as the owner in the dispatch topic shape - `:` (§7) and per-pipeline introspection - topics (§10) build on the same identifier. +- Bound by OVOS-MSG-1 §2.1.1: because `pipeline_id` appears as a + component in colon-separated topic shapes (`:` + in §7, per-pipeline introspection topics in §10), it **MUST NOT** + contain `:`. The recommended form is ASCII letters / digits / `_` / + `-` only. - Unique within a deployment's loaded-plugin set. A plugin **MAY** appear in a session's pipeline more than once @@ -800,9 +799,8 @@ on the topic: ``` where `` is `Match.owner_id` and `` is -`Match.intent_name`. Both segments are bound by OVOS-MSG-1 -§2.1.1 (no `:`, no `.`, no whitespace), so the single `:` split -is unambiguous. +`Match.intent_name`. Both segments are bound by OVOS-MSG-1 §2.1.1 — neither may +contain `:` — so the single `:` split is unambiguous. ### 7.0 Identifier polymorphism — `` is one identifier From 71bcdd40bcd1ae77f6825166d25fbac36ec7b89b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:12:35 +0100 Subject: [PATCH 37/57] PIPELINE-1: fix 5 audit gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §4.4 (new): match-phase timeout SHOULD, parallel to §8.3 handler timeout; timed-out match treated as exception, session unchanged, no bus event - §4.1: Match.utterance made required; plugin that does not track winner MUST use first candidate as fallback; orchestrator forwards verbatim - §7.1: dispatch.data.lang changed to conditional; populated from Match.lang, fallback to entry-topic lang, omitted when neither source provides one; handlers MUST treat as optional - §6.1: clarify dialog/TTS chain timing — ovos.utterance.handled fires at handler completion, not after audio rendering; audio output is fully decoupled from pipeline (chat deployments have no audio output); dialog/TTS chains are causal, not synchronization barriers - §9.3: complete_intent_failure payload table added (utterances, lang, both optional); consistent with other bus-event schemas in §9 Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/pipeline.md b/pipeline.md index e113749..367557f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -312,7 +312,7 @@ fields below. | `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | -| `utterance` | string | no | The specific candidate string from the input list that won the match. | +| `utterance` | string | yes | The specific candidate string from the input list that won the match. A plugin that does not track which candidate won **MUST** populate this with the first element of the input list as a fallback; the orchestrator forwards this value verbatim as `data.utterance` in the dispatch payload (§7.1) and **MUST NOT** substitute another value. | | `updated_session` | object | no | A replacement `session` snapshot the plugin produced during `match` (§4.2). When present, the orchestrator MUST use this snapshot — in place of the inbound utterance's session — for the dispatch and every downstream stage. When absent, the inbound session is carried unchanged. This is the **only** mechanism by which a plugin's match-phase session mutations reach downstream consumers; in-place mutations of the inbound session object are not visible past the plugin boundary. | The orchestrator interprets a non-`None` return as a definitive @@ -387,6 +387,29 @@ or session` is applied immediately after a non-null match, before the post-match-pre-dispatch window where intent transformers and CONTEXT-1's decay tick run. +### 4.4 Match-phase timeout + +The `match` operation is logically synchronous from the orchestrator's +perspective — the orchestrator calls `match` and waits for the return +value. Because §4.2 permits a plugin to communicate over the bus during +`match` (poll a model server, ask the user a disambiguation question, +etc.), the call can block for an unbounded time. + +The orchestrator **SHOULD** bound each `match` invocation by a +deployment-defined time. If a plugin has not returned within the bound, +the orchestrator **MUST** treat the call as if the plugin had raised an +exception — log the timeout, skip to the next plugin per §6.2, and +continue normally. Any partial mutation performed by the plugin during the +timed-out call is discarded (there is no `Match` to carry an +`updated_session`; the inbound session is unchanged). No bus event is +emitted for the timeout at this stage. + +The timeout bound, the recovery policy, and whether a timed-out plugin +triggers the §6.2 circuit-breaker are deployer-configurable. This +specification fixes only that the discipline **SHOULD** exist. + +--- + ### 4.3 The capture map `Match.captures` is a `{string: string}` mapping (the same shape @@ -711,8 +734,19 @@ and before iteration, against the candidate utterance list. The CONTEXT-1 §5.3 sanctions engine-side `session.intent_context` mutation and where TRANSFORM-1 §3.4 inserts the intent-transformer chain over the chosen `Match`. The **dialog** and **TTS** -transformer chains run on handler-emitted speak Messages and on -the rendered audio respectively, off the dispatch return-path. +transformer chains run on handler-emitted natural-language responses and +on the rendered audio respectively, off the dispatch return-path. +**`ovos.utterance.handled` is emitted at handler completion** — +immediately after `ovos.intent.handler.complete` (or `.error`) — and +does **not** gate on any downstream audio rendering. A handler's +obligation to this specification ends when it returns; it queues +natural-language responses (the `speak` Message) without knowing or +caring whether the deployment has any audio-output capability at all. +Audio output is fully decoupled from the pipeline: a chat-only +deployment receives the same utterance lifecycle and the same end-marker +as an audio deployment. The dialog and TTS chains are shown in the flow +diagram in their logical causal position; they are not a +synchronization barrier for `ovos.utterance.handled`. Pseudocode is informative; normative rules are in §§4–9. @@ -923,7 +957,7 @@ The dispatch Message's `data`: |-------|------|----------|---------| | `owner_id` | string | yes | The `Match.owner_id` — the topic's prefix, repeated for the handler's convenience. | | `intent_name` | string | yes | The `Match.intent_name` — the topic's suffix. | -| `lang` | string | yes | The language the utterance was recognized in (`data.lang`, OVOS-MSG-1 §4.2). | +| `lang` | string | conditional | The language the utterance was recognized in. Populated from `Match.lang` when present. When `Match.lang` is absent, the orchestrator falls back to the entry-topic `Message.data.lang` (§9.1) if that field was present. If neither source provides a language, this field **MUST** be omitted. Handlers **MUST** treat this field as optional — its absence means no authoritative content language was determined for this match. | | `utterance` | string | yes | The candidate string that won the match. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | @@ -1142,8 +1176,22 @@ topic (§7). ### 9.3 `complete_intent_failure` Emitted by the orchestrator when pipeline iteration completed -with no plugin claiming the utterance. Broadcast. Payload -**MAY** carry the original utterance data for observability. +with no plugin claiming the utterance. Broadcast. + +```json +{ + "utterances": ["turn off the lights"], + "lang": "en-US" +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `utterances` | array of strings | no | The candidate utterance list that no plugin matched, as it stood after the utterance-transformer chain. Included for observability; consumers **MUST NOT** re-submit it without explicit user intent. | +| `lang` | string | no | BCP-47 tag from the entry-topic Message (§9.1), if it was present. Absent when the entry-topic carried no `lang`. | + +Both fields are optional. An observer that receives no fields still +knows no plugin matched — the topic name alone is normative. This message **MUST** be followed immediately by `ovos.utterance.handled` (§9.5). From 9d6b2ca856dc86a912eb22cde47fbb7706c19643 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:21:37 +0100 Subject: [PATCH 38/57] =?UTF-8?q?PIPELINE-1=20=C2=A79.6:=20define=20ovos.u?= =?UTF-8?q?tterance.speak=20as=20NL=20output=20exit=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frames PIPELINE-1 as defining the natural-language I/O boundary of the voice assistant: ovos.utterance.handle (entry) and ovos.utterance.speak (exit) are the symmetric NL input and output surfaces. Everything between is pipeline; everything downstream of speak (TTS, audio queueing, display) is out of scope and belongs to the output-path companion specification. - §1: add ovos.utterance.speak to scope bullet - §6.1 flow diagram: add ovos.utterance.speak (×0..N) step; update "(TTS rendering)" label to "(output-path delivery)" to reflect decoupling - §6.1 prose: reference §9.6 by topic name instead of "speak Message" - §6.4 terminal events table: add speak ×0..N to matched-by-plugin path - §9.6 (new): full definition — topic naming rationale, payload schema (utterance required, lang optional), derivation via forward/reply from dispatch (session + skill_id propagation), multiplicity rule (zero permitted for silent handlers), broadcast, output-path out of scope Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 80 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/pipeline.md b/pipeline.md index 367557f..e948115 100644 --- a/pipeline.md +++ b/pipeline.md @@ -49,7 +49,8 @@ This specification defines: - the **utterance-layer bus events** (§9) — the utterance entry topic `ovos.utterance.handle` (§9.1), `ovos.intent.matched`, `complete_intent_failure`, - `ovos.utterance.handled`; + `ovos.utterance.handled`, and the natural-language response topic + `ovos.utterance.speak` (§9.6); - **conformance** (§10). It does **not** define: @@ -714,8 +715,9 @@ ovos.utterance.handle ← entry (§9.1) │ ovos.intent.matched (§9.2) │ dispatch on : (§7) │ (handler runs; emits lifecycle trio §8) + │ ovos.utterance.speak (×0..N) (§9.6) │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 - │ (TTS rendering; tts-transformer chain — §3.6) + │ (output-path delivery; tts-transformer chain — §3.6) │ ovos.utterance.handled (§9.5) │ break │ @@ -738,14 +740,14 @@ transformer chains run on handler-emitted natural-language responses and on the rendered audio respectively, off the dispatch return-path. **`ovos.utterance.handled` is emitted at handler completion** — immediately after `ovos.intent.handler.complete` (or `.error`) — and -does **not** gate on any downstream audio rendering. A handler's -obligation to this specification ends when it returns; it queues -natural-language responses (the `speak` Message) without knowing or -caring whether the deployment has any audio-output capability at all. -Audio output is fully decoupled from the pipeline: a chat-only -deployment receives the same utterance lifecycle and the same end-marker -as an audio deployment. The dialog and TTS chains are shown in the flow -diagram in their logical causal position; they are not a +does **not** gate on any downstream output delivery. A handler's +obligation to this specification ends when it returns; it emits zero or +more `ovos.utterance.speak` Messages (§9.6) without knowing or caring +whether the deployment has any audio-output capability at all. Audio +output is fully decoupled from the pipeline: a chat-only deployment +receives the same utterance lifecycle and the same end-marker as an +audio deployment. The dialog and TTS transformer chains are shown in the +flow diagram in their logical causal position; they are not a synchronization barrier for `ovos.utterance.handled`. Pseudocode is informative; normative rules are in §§4–9. @@ -810,7 +812,7 @@ followed by the universal end-marker `ovos.utterance.handled`: | Outcome | Sequence of utterance-layer events | |---------|------------------------------------| -| Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.handled` | +| Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.speak` ×0..N → `ovos.utterance.handled` | | No plugin matched | `complete_intent_failure` → `ovos.utterance.handled` | | Cancelled by a transformer | `ovos.utterance.cancelled` → `ovos.utterance.handled` (see OVOS-TRANSFORM-1 §8.2) | @@ -1219,6 +1221,62 @@ A conformant orchestrator **MUST** emit exactly one Multiple emissions for one utterance are malformed; zero is malformed. +### 9.6 `ovos.utterance.speak` — natural-language response + +`ovos.utterance.speak` is the **natural-language output exit point** of +the pipeline — the symmetric counterpart to the `ovos.utterance.handle` +entry point (§9.1). Together they define the natural-language I/O +boundary of the voice assistant: human speech (or text) arrives on the +entry topic; the assistant's natural-language response departs on this +topic. + +A handler emits `ovos.utterance.speak` to deliver a natural-language +response string for the assistant to convey to the user. What the +deployment does with the Message downstream — TTS rendering, audio +queueing, playback, chat display — is out of scope for this +specification and is defined by the output-path companion specification. +A deployment with no audio output (a text-only chat bridge, a test +harness) receives the same `ovos.utterance.speak` Message as an +audio-capable deployment. + +The topic name follows the naming conventions of OVOS-MSG-1 §2.1: +imperative-mood verb (`speak` — a request for the assistant to speak +this response), dot-separated hierarchy, same `ovos.utterance.*` +namespace as the entry topic. + +**Payload:** + +```json +{ + "utterance": "It is currently 22 degrees and sunny.", + "lang": "en-US" +} +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `utterance` | string | yes | The natural-language response string. | +| `lang` | string | no | BCP-47 tag of the response language. When absent, the output stage resolves language from the session per OVOS-SESSION-1 §3.2. | + +**Derivation and session propagation.** A handler **MUST** derive each +`ovos.utterance.speak` emission via `Message.forward` or +`Message.reply` (OVOS-MSG-1 §5) from the dispatch Message (§7) it +received. This carries `context.session` and `context.skill_id` forward +automatically — downstream stages (dialog-transformer chain +OVOS-TRANSFORM-1 §3.5, the output-path specification) can read the +session and attribute the response without additional wire fields. A +`ovos.utterance.speak` Message that does not derive from a dispatch is +non-conformant. + +**Multiplicity and ordering.** A handler **MAY** emit zero or more +`ovos.utterance.speak` Messages. Zero is permitted — a handler that +acts silently (playing a sound, toggling a device, queuing media) is +conformant. When a handler emits multiple, the order of emission is the +intended delivery order; the output stage **SHOULD** preserve it. + +**Broadcast.** `ovos.utterance.speak` carries no `destination` — it is +broadcast. Any output component subscribed to the topic may consume it. + --- ## 10. Per-pipeline introspection From 52aad8d5a7cc6e5ddf64b90ff9afb1d06b502210 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:26:40 +0100 Subject: [PATCH 39/57] =?UTF-8?q?PIPELINE-1:=20simplification=20pass=20?= =?UTF-8?q?=E2=80=94=207=20structural=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Fix §4.3/§4.4 ordering: capture map is §4.3, timeout is §4.4 (was swapped) 2. §3.1 stamp rule: drop modify-in-place paragraph (unenforceable); fix framing — .reply/.response are routing metadata, not authorial actions; only fresh constructions require stamping; all derivations preserve inherited context["pipeline_id"] 3. §3.1 coexistence paragraph: compressed from 4 sentences to 2 4. §7.0: remove "plain skill + reserved intent_names" paragraph → moved to §7.3; remove "passive index registration" SHOULD → moved to §10 intro; closing summary sentence retained 5. §2: drop "plugins are diverse" flavor paragraph; move deployment SHOULD (load at least one INTENT-4-consuming plugin) to §11 conformance under new "A deployment SHOULD" block 6. §5 intro: drop "§5.5 fixes..." navigation hint sentence 7. §11: drop Non-goals block (duplicates §1) Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 183 ++++++++++++++++++---------------------------------- 1 file changed, 61 insertions(+), 122 deletions(-) diff --git a/pipeline.md b/pipeline.md index e948115..cf70d78 100644 --- a/pipeline.md +++ b/pipeline.md @@ -133,18 +133,6 @@ components. The only difference is where the handler lives: From outside either case, the assistant responded. The user does not know or care which component answered. -Plugins are diverse by design. A deployment may load plugins that -consume OVOS-INTENT-4 registrations and match against keyword or -template intents, plugins that consume no registrations and match -by their own internal rules (such as language-model-backed -personas), plugins that always claim with a fallback response, or -anything else. The contract is just `match`. - -A deployment running skills that emit OVOS-INTENT-4 keyword or -template intent registrations **SHOULD** load at least one plugin -that consumes those registrations; otherwise those intents will -never match. Whether to load such a plugin is a deployment choice, -not a spec-level requirement. --- @@ -191,62 +179,28 @@ source. **Stamp rule.** A plugin **MUST** set `Message.context["pipeline_id"]` to its own identity on every -Message it places on the bus by **authorial action** and on every -Message it **modifies in place** before that Message proceeds. -Authorial action covers the cases where the plugin is asserting -itself as the originator of a new Message-on-wire: - -- a fresh emission (the plugin constructs and emits a new - Message); -- `Message.reply(...)` or `Message.response(...)` (OVOS-MSG-1 §5) - derived from a prior Message — these derivations swap routing - keys and create a new authorial step. The resulting - Message-on-wire **MUST** carry `context["pipeline_id"]` set to - the plugin's id, overwriting whatever inherited - `context["pipeline_id"]` value may have been there from an - upstream plugin in a multi-plugin chain. - -**Pure-forward propagation is exempt.** `Message.forward(...)` -(OVOS-MSG-1 §5.1) is propagation semantics — it preserves -`context` unchanged by design, and the deriving plugin is not -asserting authorship of the forwarded Message-on-wire. A plugin -that `.forward`s a Message **MUST NOT** overwrite an inherited -`context["pipeline_id"]` with its own — preserving upstream -attribution is the point of the forward derivation. If a plugin -wants to claim authorship of the resulting Message, it -**SHOULD** use `.reply` or `.response`, or emit fresh. - -Modify-in-place covers the case where the plugin mutates an -existing Message (its `context`, its `data`, the session it -carries — for example, a CONTEXT-1 §5.3 direct mutation) without -itself causing a fresh emission; the next emitter still propagates -the modified Message, and `context["pipeline_id"]` must reflect -the plugin that mutated. - -The combined effect: whenever the plugin's hands have been on a -Message that subsequently appears on the bus, `context["pipeline_id"]` -reflects it. - -**Coexistence with other identity keys.** When a plugin emits via -`Message.forward` / `.reply` / `.response` (OVOS-MSG-1 §5) from a -prior Message that already carries `context["skill_id"]` from an -upstream dispatch (or any of the six `_transformer_ids` keys -of OVOS-TRANSFORM-1 §1.3 from upstream transformer stages), the -inherited keys are preserved by the derivation rule and **not** -stripped. Each names a different component in the chain that -produced the Message; the plugin additionally stamps its own -`context["pipeline_id"]`. Attribution consumers -(OVOS-CONTEXT-1 §5.2, audit / telemetry observers) apply a -lifecycle-position precedence — see OVOS-CONTEXT-1 §5.2 — to pick -a single owner when they need one. - -`Message.context["pipeline_id"]` is the plugin's **self-attribution**. -Mirroring the `context["skill_id"]` / `data["skill_id"]` distinction -of OVOS-INTENT-4 §3.1, a topic's `data` schema may also carry -`pipeline_id` as the **subject** of the message (the plugin a -query is filtered against, the plugin being described, etc.); a -consumer reading `data.pipeline_id` is reading a subject, not a -self-attribution. +Message it **originates** — constructs and emits fresh. When the +plugin emits a derived Message via `Message.forward`, +`Message.reply`, or `Message.response` (OVOS-MSG-1 §5), these are +**routing-metadata** derivations that adjust addressing but are +not new originations; the plugin **MUST NOT** overwrite the +inherited `context["pipeline_id"]` in any of these cases — +preserving upstream attribution is the point of the derivation. +Only a fresh construction is an origination that requires stamping. + +**Coexistence with other identity keys.** MSG-1 derivation rules +preserve inherited context keys — `context["skill_id"]` from an +upstream dispatch and any `_transformer_ids` from transformer +stages are not stripped when a plugin stamps its own +`context["pipeline_id"]`. Each key names a different component in +the chain; when a single owner is needed, consumers apply the +lifecycle-position precedence of OVOS-CONTEXT-1 §5.2. + +`Message.context["pipeline_id"]` is **self-attribution** — who +emitted. A `data.pipeline_id` field on a topic's payload is a +**subject** — the plugin a query or description concerns. A +consumer reading attribution reads `context`; a consumer reading a +subject reads `data`. #### Orchestrator-side enforcement @@ -388,6 +342,22 @@ or session` is applied immediately after a non-null match, before the post-match-pre-dispatch window where intent transformers and CONTEXT-1's decay tick run. +### 4.3 The capture map + +`Match.captures` is a `{string: string}` mapping (the same shape +OVOS-INTENT-3 §7 defines for template / keyword intent slots). + +For skill-owned matches against intents the plugin previously +consumed from OVOS-INTENT-4 registrations, the capture map keys +are the slot names (template intents) or vocabulary names (keyword +intents) of the matched intent. + +For plugin-owned matches, the capture map is whatever the plugin +chooses to surface. It **MAY** be empty. + +The orchestrator does not interpret the capture map; it forwards +it to the dispatched handler. + ### 4.4 Match-phase timeout The `match` operation is logically synchronous from the orchestrator's @@ -411,24 +381,6 @@ specification fixes only that the discipline **SHOULD** exist. --- -### 4.3 The capture map - -`Match.captures` is a `{string: string}` mapping (the same shape -OVOS-INTENT-3 §7 defines for template / keyword intent slots). - -For skill-owned matches against intents the plugin previously -consumed from OVOS-INTENT-4 registrations, the capture map keys -are the slot names (template intents) or vocabulary names (keyword -intents) of the matched intent. - -For plugin-owned matches, the capture map is whatever the plugin -chooses to surface. It **MAY** be empty. - -The orchestrator does not interpret the capture map; it forwards -it to the dispatched handler. - ---- - ## 5. Session fields owned by this specification This specification claims four session fields per OVOS-SESSION-1 @@ -440,9 +392,6 @@ and follow the deployment-default-fallback absence rule of OVOS-SESSION-1 §2.5: an omitted, empty, or absent field resolves at consumption to the deployment-configured default. -§5.5 fixes how the positive and negative fields compose when both -are set; §5.6 is an informative note on layer-2 authorization use. - ### 5.1 `session.pipeline` An ordered array of `pipeline_id` strings expressing the **session @@ -870,35 +819,12 @@ dispatch-handler subscription. §7.0's table only enumerates handler-owner shapes (the targets of dispatch); pure-matcher plugins live alongside both rows. -A **plain skill** may also handle a reserved-intent-name -dispatch topic when a companion specification defines one. For -example, a skill that defines a `converse` method (OVOS-CONVERSE-1 -§9.3) subscribes to `:converse` via framework -convention, not via OVOS-INTENT-4 registration — the reserved -name is not registrable (§7.3). The plain-skill row above lists -INTENT-4-registered intents as the *normal* path; reserved -intent_names handled by framework convention extend it without -changing the dispatch shape. - The pipeline-plugin-with-handlers case is normative: **if a pipeline plugin emits matches whose `owner_id` equals its own `pipeline_id`, that identifier is also its `skill_id`** for the purposes of OVOS-INTENT-3, OVOS-INTENT-4, and every other spec -that addresses skills by id. They are not two identifiers that -happen to coincide; they are the same identifier under two -roles. - -**Passive index registration.** A pipeline plugin with bundled -handlers **SHOULD** publish the set of `intent_name` values it -owns through the per-pipeline introspection topic -`ovos.pipeline..intents.list` (§10). Observers and -introspection tools rely on this index to enumerate every -handler in the deployment; without it, plugin-owned handlers -are invisible to deployment-wide tooling that walks -OVOS-INTENT-4 only. This is **not** OVOS-INTENT-4 registration — -the plugin's matches do not flow through any external matcher — -it is a one-way declaration of "these are the intent_names I -dispatch on." +that addresses skills by id. They are the same identifier under +two roles, not two identifiers that happen to coincide. From the dispatch path's perspective the two shapes are indistinguishable. `:` carries no @@ -1012,6 +938,13 @@ listing); the per-name semantics are owned by the reserving specification. Other specifications MAY reserve further names by adding rows to this table in their own PR. +A plain skill (§7.0) subscribes to a reserved-name dispatch topic +via framework convention rather than OVOS-INTENT-4 registration — +the reserved name is not registrable. The normal skill path +(INTENT-4-registered intents) and the reserved-name path share the +same `:` dispatch shape; no dispatch +mechanics change. + ### 7.4 In-process equivalence When the handler-owning component (skill or plugin) runs in the @@ -1287,6 +1220,14 @@ other plugins) discover that set at runtime, this specification defines a pull-query / scatter-response pattern keyed on `pipeline_id`. +A pipeline plugin with bundled handlers **SHOULD** publish the set +of `intent_name` values it owns through the query topic below. +Observers and introspection tools rely on this index to enumerate +every handler in the deployment; without it, plugin-owned handlers +are invisible to deployment-wide tooling that walks OVOS-INTENT-4 +only. This is **not** OVOS-INTENT-4 registration — it is a +one-way declaration of "these are the intent_names I dispatch on." + ### 10.1 Query and response topics | Topic | Direction | Carries | @@ -1378,6 +1319,12 @@ from the hosting process. ## 11. Conformance +### A **deployment** **SHOULD**: + +- load at least one pipeline plugin that consumes OVOS-INTENT-4 + registrations when skills emitting keyword or template intents are + present; without such a plugin those intents never match. + ### An **orchestrator** **MUST**: - subscribe to the utterance-layer entry topic @@ -1421,14 +1368,6 @@ specification. The orchestrator owns the handler-lifecycle trio callable; the spec binds the orchestrator that invokes it, not the handler itself. -### Non-goals - -The following are explicitly outside this specification: plugin -loading and discovery; any pre-pipeline utterance transformation -or cancellation chain; ASR n-best ranking semantics within -plugins; per-plugin behavioural specs; the `session` object's -wire shape and field set (owned by OVOS-SESSION-1). - --- ## See also From cae134e865625b9d886843178ec4490877d3a128 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:29:57 +0100 Subject: [PATCH 40/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20rewrite=20ident?= =?UTF-8?q?ity=20stamping=20to=20two=20clear=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop all derivation/routing/authorship framing — that belongs in MSG-1. The section now states exactly what is needed: - orchestrator stamps context["pipeline_id"] before each match call - orchestrator stamps context["skill_id"] = owner_id on every dispatch Both fields flow automatically from there; no plugin author policy required. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 78 +++++++++++++---------------------------------------- 1 file changed, 18 insertions(+), 60 deletions(-) diff --git a/pipeline.md b/pipeline.md index cf70d78..bffae20 100644 --- a/pipeline.md +++ b/pipeline.md @@ -159,66 +159,24 @@ multiple matching modes (for example, a strict mode and a permissive mode). The orchestrator treats each `pipeline_id` as a distinct stage. -### 3.1 Plugin self-identification on emission - -A pipeline plugin **MUST** set `Message.context["pipeline_id"]` on -**every Message it emits to the bus**, to its own `pipeline_id`. -This is the plugin-side analogue of the skill rule in -OVOS-INTENT-4 §3.1: it makes plugin-originated traffic -attributable to its emitter without parsing topic names or `data` -payloads. - -The rule binds independent of the topic. It applies to bus events -a plugin emits on its own initiative (background telemetry, -diagnostics, plugin-defined topics), and — crucially for downstream -specs — to events on shared topics owned by other specifications. -For example, OVOS-CONTEXT-1 §5.2 reads `context["pipeline_id"]` to -attribute a plugin-emitted `ovos.context.set` to its owning -plugin; without this rule that attribution has no wire-level -source. - -**Stamp rule.** A plugin **MUST** set -`Message.context["pipeline_id"]` to its own identity on every -Message it **originates** — constructs and emits fresh. When the -plugin emits a derived Message via `Message.forward`, -`Message.reply`, or `Message.response` (OVOS-MSG-1 §5), these are -**routing-metadata** derivations that adjust addressing but are -not new originations; the plugin **MUST NOT** overwrite the -inherited `context["pipeline_id"]` in any of these cases — -preserving upstream attribution is the point of the derivation. -Only a fresh construction is an origination that requires stamping. - -**Coexistence with other identity keys.** MSG-1 derivation rules -preserve inherited context keys — `context["skill_id"]` from an -upstream dispatch and any `_transformer_ids` from transformer -stages are not stripped when a plugin stamps its own -`context["pipeline_id"]`. Each key names a different component in -the chain; when a single owner is needed, consumers apply the -lifecycle-position precedence of OVOS-CONTEXT-1 §5.2. - -`Message.context["pipeline_id"]` is **self-attribution** — who -emitted. A `data.pipeline_id` field on a topic's payload is a -**subject** — the plugin a query or description concerns. A -consumer reading attribution reads `context`; a consumer reading a -subject reads `data`. - -#### Orchestrator-side enforcement - -The orchestrator (or any component that loads pipeline plugins) -**SHOULD** intercept / decorate the plugin's emit pathway at load -time so non-compliant plugin code cannot emit a Message that lacks -or misstates `context["pipeline_id"]`. This places the discipline -on the plugin-loading infrastructure rather than on every plugin -author, mirroring the skill-loader enforcement of OVOS-INTENT-4 -§3.1. - -A consumer that needs to attribute a plugin-emitted Message -**MUST** read `context["pipeline_id"]` — it **MUST NOT** infer the -plugin from `source`, `data` fields, or topic name. A Message -without `context["pipeline_id"]` arriving on a topic that requires -plugin attribution (per the topic's owning spec) is malformed at -that topic's layer; the topic's spec defines the rejection -behaviour. +### 3.1 Identity stamping + +Two identity fields travel in `Message.context` through the pipeline: + +- **`context["pipeline_id"]`** — set by the orchestrator to the + `pipeline_id` of the plugin currently executing `match`. The + orchestrator **MUST** stamp this field before each `match` call + so that any Message the plugin emits during matching is + attributable to it. Downstream consumers (e.g. OVOS-CONTEXT-1 + §5.2) read this field to attribute plugin-emitted events. +- **`context["skill_id"]`** — set by the orchestrator to + `Match.owner_id` on every dispatch (§7.1). Every Message a + handler emits during its execution carries this value, satisfying + OVOS-INTENT-4 §3.1 by construction. + +The orchestrator **SHOULD** enforce both fields at load time so +non-compliant plugin or skill code cannot emit a Message with a +missing or incorrect identity key. --- From fe95383e5c120085f4945ecad865e136d37115b6 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:32:38 +0100 Subject: [PATCH 41/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20simplify=20to?= =?UTF-8?q?=20two=20sentences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orchestrator stamps pipeline_id on match selection. Handler identity (skill_id) is INTENT-4's rule, applies to bundled-handler plugins identically to plain skills — fully polymorphic, nothing to add here. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/pipeline.md b/pipeline.md index bffae20..2c925e7 100644 --- a/pipeline.md +++ b/pipeline.md @@ -159,24 +159,18 @@ multiple matching modes (for example, a strict mode and a permissive mode). The orchestrator treats each `pipeline_id` as a distinct stage. -### 3.1 Identity stamping - -Two identity fields travel in `Message.context` through the pipeline: - -- **`context["pipeline_id"]`** — set by the orchestrator to the - `pipeline_id` of the plugin currently executing `match`. The - orchestrator **MUST** stamp this field before each `match` call - so that any Message the plugin emits during matching is - attributable to it. Downstream consumers (e.g. OVOS-CONTEXT-1 - §5.2) read this field to attribute plugin-emitted events. -- **`context["skill_id"]`** — set by the orchestrator to - `Match.owner_id` on every dispatch (§7.1). Every Message a - handler emits during its execution carries this value, satisfying - OVOS-INTENT-4 §3.1 by construction. - -The orchestrator **SHOULD** enforce both fields at load time so -non-compliant plugin or skill code cannot emit a Message with a -missing or incorrect identity key. +### 3.1 Pipeline attribution + +When the orchestrator selects a match it **MUST** stamp +`Message.context["pipeline_id"]` with the `pipeline_id` of the +plugin that produced it. This makes the match attributable to its +engine; downstream consumers (e.g. OVOS-CONTEXT-1 §5.2) read this +field without inspecting topics or payloads. + +Handler identity (`context["skill_id"]`) is governed by +OVOS-INTENT-4 §3.1 and applies to pipeline plugins with bundled +handlers identically to plain skills — the dispatch shape is fully +polymorphic (§7.0). --- From 06b04b0ae6af16c1a8f20deb2e6070bb181f17fe Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:34:10 +0100 Subject: [PATCH 42/57] =?UTF-8?q?PIPELINE-1=20=C2=A73.1:=20pipeline=5Fid?= =?UTF-8?q?=20stamped=20at=20dispatch,=20propagates=20via=20MSG-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify that pipeline_id first appears on the dispatch Message (§7.1), not at match time. MSG-1 derivation semantics carry it through handler emissions with no further action required by the plugin or handler. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pipeline.md b/pipeline.md index 2c925e7..0f0aa3d 100644 --- a/pipeline.md +++ b/pipeline.md @@ -161,16 +161,13 @@ distinct stage. ### 3.1 Pipeline attribution -When the orchestrator selects a match it **MUST** stamp -`Message.context["pipeline_id"]` with the `pipeline_id` of the -plugin that produced it. This makes the match attributable to its -engine; downstream consumers (e.g. OVOS-CONTEXT-1 §5.2) read this -field without inspecting topics or payloads. - -Handler identity (`context["skill_id"]`) is governed by -OVOS-INTENT-4 §3.1 and applies to pipeline plugins with bundled -handlers identically to plain skills — the dispatch shape is fully -polymorphic (§7.0). +The orchestrator stamps `context["pipeline_id"]` on the dispatch +Message (§7.1) — this is the first point at which the matching +plugin's identity appears on the wire. From there it propagates +through all Messages the handler emits via MSG-1 derivation +semantics, making every downstream Message attributable to the +plugin that produced the match without any further action by the +plugin or the handler. --- From bbb5126ce0696c705be212d971aaeacdb100af11 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:36:43 +0100 Subject: [PATCH 43/57] PIPELINE-1: pre-merge fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §1: conformance cross-ref corrected to §11 (was §10) - §9 intro: drop hardcoded "five" event count - §7.0: remove named project references from pure-matcher paragraph (golden rule — describe by role, not by implementation name) - §7.1: replace "via forward (MSG-1 §5.1)" with "MSG-1 derivation semantics" — routing mechanics belong in MSG-1, not here; drop redundant skill_id drift paragraph (INTENT-4 §3.1 owns it) - §9.6: replace explicit forward/reply prescription with "derives from the dispatch Message per MSG-1 §5 derivation semantics" Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 61 +++++++++++++++++++++-------------------------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/pipeline.md b/pipeline.md index 0f0aa3d..fe3b28c 100644 --- a/pipeline.md +++ b/pipeline.md @@ -51,7 +51,7 @@ This specification defines: `ovos.intent.matched`, `complete_intent_failure`, `ovos.utterance.handled`, and the natural-language response topic `ovos.utterance.speak` (§9.6); -- **conformance** (§10). +- **conformance** (§11). It does **not** define: @@ -757,16 +757,15 @@ The spec recognises two handler-owner shapes: The examples in the second row are plugins whose own `match` emits matches addressed back to itself. A different shape — a -**pure-matcher** pipeline plugin (Padatious / Adapt for skill -intents; the converse plugin of OVOS-CONVERSE-1 for the -reserved intent_names `converse` / `response`) — is also a -pipeline plugin per §3 but is **not a handler-owner**: its -`match` returns matches whose `owner_id` is some *other* -component's identity. Pure-matcher plugins have only a -`pipeline_id`; they have no `skill_id` and they own no -dispatch-handler subscription. §7.0's table only enumerates -handler-owner shapes (the targets of dispatch); pure-matcher -plugins live alongside both rows. +**pure-matcher** pipeline plugin — is also a pipeline plugin per §3 +but is **not a handler-owner**: its `match` returns matches whose +`owner_id` is some *other* component's identity (for example, a +template or keyword engine that matches against skill-registered +intents, or a converse engine routing to reserved intent_names per +§7.3). Pure-matcher plugins have only a `pipeline_id`; they have no +`skill_id` and they own no dispatch-handler subscription. §7.0's +table only enumerates handler-owner shapes (the targets of +dispatch); pure-matcher plugins live alongside both rows. The pipeline-plugin-with-handlers case is normative: **if a pipeline plugin emits matches whose `owner_id` equals its own @@ -794,29 +793,18 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): `skill_id` is the voice-application identity (§7.0) — every dispatched handler owns one, whether it is a plain skill or a pipeline plugin with bundled handlers (in which case - `skill_id == pipeline_id`). This stamping carries the - handler-owner's identity forward into every Message the - handler emits via `forward` (OVOS-MSG-1 §5.1), satisfying - OVOS-INTENT-4 §3.1 by construction. + `skill_id == pipeline_id`). MSG-1 derivation semantics carry + this value forward into every Message the handler emits, + satisfying OVOS-INTENT-4 §3.1 by construction. - **`context["pipeline_id"]` stamping.** When the dispatched `` corresponds to a loaded pipeline plugin (the pipeline-plugin-with-bundled-handlers case of §7.0), the orchestrator **MUST** additionally stamp `context["pipeline_id"] = `. For such a handler `context["skill_id"]` and `context["pipeline_id"]` carry the - same identifier; consumers reading either key get the same - value. For a plain skill (not loaded as a pipeline plugin), - the orchestrator **MUST NOT** stamp `context["pipeline_id"]`. - -Any Message the skill subsequently emits **MUST** carry -`context["skill_id"]` matching the `` of the dispatch -that invoked it (OVOS-INTENT-4 §3.1). Because the orchestrator -stamps the dispatch context and skills derive their emissions -from it via `forward` / `reply`, this match is automatic — a -skill that emits a Message whose `context["skill_id"]` differs -from the dispatch is non-conformant, and the orchestrator -**SHOULD** detect and log such drift if it is in a position to -do so (loader-side interception per OVOS-INTENT-4 §3.1). + same identifier. For a plain skill (not loaded as a pipeline + plugin), the orchestrator **MUST NOT** stamp + `context["pipeline_id"]`. The dispatch Message's `data`: @@ -990,7 +978,7 @@ same match. Re-dispatch is not defined by this specification. ## 9. Utterance-layer messages -This specification formalizes five utterance-layer bus events. +This specification formalizes the following utterance-layer bus events. All travel in standard OVOS-MSG-1 envelopes; routing follows the single-flip model of OVOS-MSG-1 §5.2. @@ -1141,14 +1129,13 @@ namespace as the entry topic. | `lang` | string | no | BCP-47 tag of the response language. When absent, the output stage resolves language from the session per OVOS-SESSION-1 §3.2. | **Derivation and session propagation.** A handler **MUST** derive each -`ovos.utterance.speak` emission via `Message.forward` or -`Message.reply` (OVOS-MSG-1 §5) from the dispatch Message (§7) it -received. This carries `context.session` and `context.skill_id` forward -automatically — downstream stages (dialog-transformer chain -OVOS-TRANSFORM-1 §3.5, the output-path specification) can read the -session and attribute the response without additional wire fields. A -`ovos.utterance.speak` Message that does not derive from a dispatch is -non-conformant. +`ovos.utterance.speak` emission from the dispatch Message (§7) it +received, per MSG-1 §5 derivation semantics. This carries +`context.session` and `context.skill_id` forward automatically — +downstream stages (dialog-transformer chain OVOS-TRANSFORM-1 §3.5, +the output-path specification) can read the session and attribute the +response without additional wire fields. An `ovos.utterance.speak` +Message that does not derive from a dispatch is non-conformant. **Multiplicity and ordering.** A handler **MAY** emit zero or more `ovos.utterance.speak` Messages. Zero is permitted — a handler that From fc90ee79bf2fa1eea12c150abd3438037ad601a9 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:41:06 +0100 Subject: [PATCH 44/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.0:=20drop=20two-shap?= =?UTF-8?q?es=20framing;=20dispatch=20is=20uniform?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "plain skill vs plugin-with-bundled-handlers" distinction was architectural noise — there is no difference. A match has an owner_id which is the skill_id of the handler. If that is the plugin itself, skill_id == pipeline_id. Same dispatch, same handler obligations, same INTENT-4 rules. Plugin just skipped the registration bus round-trip. - §7.0: rewritten to state the single rule plainly; table and "identifier polymorphism" heading removed - §7.1: pipeline_id stamp rule simplified — always stamped from the producing plugin; no conditional on handler-owner shape - §2: remove "two dispatch shapes" paragraph (no longer needed) Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 97 ++++++++++++++--------------------------------------- 1 file changed, 25 insertions(+), 72 deletions(-) diff --git a/pipeline.md b/pipeline.md index fe3b28c..174d9ff 100644 --- a/pipeline.md +++ b/pipeline.md @@ -119,21 +119,6 @@ discovers and instantiates them is a deployment concern. Each plugin exposes one operation to the orchestrator (§4) and is otherwise a black box. -From the orchestrator's perspective, "plugin" and "skill" are -indistinguishable as handler owners. Both are black-box third-party -components. The only difference is where the handler lives: - -- a skill's handler is reached via a `:` - dispatch topic — the skill registered the intent (OVOS-INTENT-4) - and owns the handler; -- a plugin's bundled handler is reached via a - `:` dispatch topic — the plugin matched - the utterance and owns the handler itself. - -From outside either case, the assistant responded. The user does -not know or care which component answered. - - --- ## 3. Pipeline plugins @@ -736,48 +721,23 @@ where `` is `Match.owner_id` and `` is `Match.intent_name`. Both segments are bound by OVOS-MSG-1 §2.1.1 — neither may contain `:` — so the single `:` split is unambiguous. -### 7.0 Identifier polymorphism — `` is one identifier - -`` names the **handler-owner** of the matched intent. -It is a single identifier string used identically on the -dispatch topic regardless of whether the handler-owner is a -plain skill or a pipeline plugin that owns its own handlers. - -Conceptually, `skill_id` is the **voice-application identity** — -the identity of whatever component the dispatch lands on. -`pipeline_id` is the **matching-engine identity** — the identity -of a component loaded as a pipeline plugin under §3. - -The spec recognises two handler-owner shapes: - -| Shape | How matches are produced | Identifier roles | -|-------|--------------------------|------------------| -| **Plain skill** | Some pipeline plugin (matching engine) consumes the skill's OVOS-INTENT-4 registrations and returns a `Match` whose `owner_id` is the skill's `skill_id`. | `skill_id` only; `pipeline_id` does not apply. | -| **Pipeline plugin with bundled handlers** (fallback, common-query, persona, OCP, …) | The plugin's own `match` returns a `Match` whose `owner_id` is the plugin's own identity, on an `intent_name` the plugin defines for itself. | `pipeline_id == skill_id` — both names refer to the same identifier. The plugin **MUST NOT** register its bundled-handler intents under OVOS-INTENT-4 (they are not skill registrations and would create a circular dependency through whichever matcher consumes the registry). | - -The examples in the second row are plugins whose own `match` -emits matches addressed back to itself. A different shape — a -**pure-matcher** pipeline plugin — is also a pipeline plugin per §3 -but is **not a handler-owner**: its `match` returns matches whose -`owner_id` is some *other* component's identity (for example, a -template or keyword engine that matches against skill-registered -intents, or a converse engine routing to reserved intent_names per -§7.3). Pure-matcher plugins have only a `pipeline_id`; they have no -`skill_id` and they own no dispatch-handler subscription. §7.0's -table only enumerates handler-owner shapes (the targets of -dispatch); pure-matcher plugins live alongside both rows. - -The pipeline-plugin-with-handlers case is normative: **if a -pipeline plugin emits matches whose `owner_id` equals its own -`pipeline_id`, that identifier is also its `skill_id`** for the -purposes of OVOS-INTENT-3, OVOS-INTENT-4, and every other spec -that addresses skills by id. They are the same identifier under -two roles, not two identifiers that happen to coincide. - -From the dispatch path's perspective the two shapes are -indistinguishable. `:` carries no -information about which shape `` is; the orchestrator -applies §7.1 stamping uniformly. +### 7.0 `` is a `skill_id` + +`Match.owner_id` is the `skill_id` of the component that will +handle the dispatch. The orchestrator does not distinguish between +a skill whose intents were registered via OVOS-INTENT-4 and a +pipeline plugin that matched itself — both are handler-owners +reached by the same `:` topic, and the +dispatched handler has the same obligations as any skill +(OVOS-INTENT-4 §3.1). + +A pipeline plugin that returns matches where `owner_id` equals its +own `pipeline_id` is simply a component whose `skill_id` and +`pipeline_id` happen to be the same identifier. It skips the +OVOS-INTENT-4 registration step because it consumes no external +intent registry — its `match` implementation decides directly +whether to claim the utterance. There is no architectural +difference; the dispatch path is identical. ### 7.1 Routing and payload @@ -790,21 +750,14 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): `source` is the orchestrator; - **`context["skill_id"]` stamping.** The orchestrator **MUST** stamp `context["skill_id"] = ` on every dispatch. - `skill_id` is the voice-application identity (§7.0) — every - dispatched handler owns one, whether it is a plain skill or a - pipeline plugin with bundled handlers (in which case - `skill_id == pipeline_id`). MSG-1 derivation semantics carry - this value forward into every Message the handler emits, - satisfying OVOS-INTENT-4 §3.1 by construction. -- **`context["pipeline_id"]` stamping.** When the dispatched - `` corresponds to a loaded pipeline plugin (the - pipeline-plugin-with-bundled-handlers case of §7.0), the - orchestrator **MUST** additionally stamp - `context["pipeline_id"] = `. For such a handler - `context["skill_id"]` and `context["pipeline_id"]` carry the - same identifier. For a plain skill (not loaded as a pipeline - plugin), the orchestrator **MUST NOT** stamp - `context["pipeline_id"]`. + MSG-1 derivation semantics carry this value forward into every + Message the handler emits, satisfying OVOS-INTENT-4 §3.1 by + construction. +- **`context["pipeline_id"]` stamping.** The orchestrator **MUST** + stamp `context["pipeline_id"]` on every dispatch with the + `pipeline_id` of the plugin that produced the match (§3.1). When + the match is self-addressed (`owner_id == pipeline_id`, §7.0), + both context keys carry the same identifier. The dispatch Message's `data`: From 89b87e51de2e2dc6408314f5a55959bbc8acc52e Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:45:13 +0100 Subject: [PATCH 45/57] PIPELINE-1: replace owner_id with skill_id throughout owner_id was an abstract alias for skill_id with no distinct meaning. Now that the two-shapes framing is gone, the abstraction serves no purpose. Every match, dispatch topic, payload field, and prose reference now uses skill_id directly, consistent with INTENT-3/4 and the rest of the spec set. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 84 ++++++++++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/pipeline.md b/pipeline.md index 174d9ff..264031f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -43,7 +43,7 @@ This specification defines: `session.blacklisted_intents` (negative filters); - the **utterance lifecycle** (§6) — entry, iteration, dispatch, terminal events; -- the **dispatch** topic shape (§7) — `:`; +- the **dispatch** topic shape (§7) — `:`; - the **handler-lifecycle trio** (§8) — `ovos.intent.handler.start` / `.complete` / `.error`; - the **utterance-layer bus events** (§9) — @@ -132,7 +132,7 @@ Constraints on `pipeline_id` strings: - Non-empty. - Bound by OVOS-MSG-1 §2.1.1: because `pipeline_id` appears as a - component in colon-separated topic shapes (`:` + component in colon-separated topic shapes (`:` in §7, per-pipeline introspection topics in §10), it **MUST NOT** contain `:`. The recommended form is ASCII letters / digits / `_` / `-` only. @@ -197,8 +197,8 @@ fields below. | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id` | string | yes | The handler-owner's identity — the `skill_id` of the dispatched handler. For a pipeline plugin with bundled handlers, this is the plugin's identity, which is **the same identifier** as its `pipeline_id` (§7.0). The dispatch topic shape `:` is uniform across both handler-owner shapes. | -| `intent_name` | string | yes | An opaque non-empty string that, together with `owner_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | +| `skill_id` | string | yes | The `skill_id` of the handler to invoke. For a pipeline plugin that matches itself, this equals its `pipeline_id` (§7.0). | +| `intent_name` | string | yes | An opaque non-empty string that, together with `skill_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | | `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | | `utterance` | string | yes | The specific candidate string from the input list that won the match. A plugin that does not track which candidate won **MUST** populate this with the first element of the input list as a fallback; the orchestrator forwards this value verbatim as `data.utterance` in the dispatch payload (§7.1) and **MUST NOT** substitute another value. | @@ -418,7 +418,7 @@ intents **MUST NOT** be matched for this session. The contract is **two-tier**: 1. A pipeline plugin **SHOULD NOT** return a `Match` whose - `owner_id` (§7.1) is a `skill_id` listed here. A plugin's + `skill_id` (§7.1) is a `skill_id` listed here. A plugin's internal handling of would-match-but-blacklisted candidates is **not specified** — it MAY skip the candidate before scoring, suppress its score below a match threshold, route to a @@ -427,7 +427,7 @@ The contract is **two-tier**: 2. A pipeline plugin that does not implement filtering is **not conformant** with this field. The orchestrator **MUST** therefore act as backstop: after a plugin returns a candidate `Match`, the - orchestrator **MUST** check `Match.owner_id` against + orchestrator **MUST** check `Match.skill_id` against `blacklisted_skills` and, if listed, **MUST** treat the match as if the plugin had declined — continue iteration to the next plugin per §6.2. No bus event is emitted for backstop filtering; @@ -439,14 +439,14 @@ field. ### 5.4 `session.blacklisted_intents` -An unordered array of fully-qualified `:` +An unordered array of fully-qualified `:` strings (the dispatch-topic shape of §7) whose specific intents **MUST NOT** be matched for this session. The contract is identical in shape to §5.3 (two-tier: plugin-SHOULD + orchestrator-MUST-backstop), with the comparison performed against the candidate `Match`'s dispatch identity -`:`. +`:`. The bare `intent_name` form is **not** accepted in this field. `intent_name` is only unique within an owner, so a bare entry would @@ -460,7 +460,7 @@ Entries are **language-agnostic.** OVOS-INTENT-4 §3.2 keys intent identity on the triple `(skill_id, intent_name, lang)`, so a single intent registered for `en-US` and `de-DE` is two separate registrations. A `blacklisted_intents` entry -`:` denies both — there is no per-language +`:` denies both — there is no per-language denylist. A deployment that needs language-scoped denial expresses it through a session whose `lang` already narrows the set of matchable registrations. @@ -591,12 +591,12 @@ ovos.utterance.handle ← entry (§9.1) │ │ engine-side context promotion (CONTEXT-1 §5.3) │ │ │ intent-transformer chain runs (TRANSFORM-1 │ │ │ §3.4) — may modify Match.captures, MUST NOT │ - │ │ change owner_id / intent_name │ + │ │ change skill_id / intent_name │ │ │ post-decay turns_remaining-- (CONTEXT-1 §4) │ │ └────────────────────────────────────────────────┘ │ │ ovos.intent.matched (§9.2) - │ dispatch on : (§7) + │ dispatch on : (§7) │ (handler runs; emits lifecycle trio §8) │ ovos.utterance.speak (×0..N) (§9.6) │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 @@ -714,24 +714,24 @@ orchestrator dispatches the matched handler by emitting a Message on the topic: ``` -: +: ``` -where `` is `Match.owner_id` and `` is +where `` is `Match.skill_id` and `` is `Match.intent_name`. Both segments are bound by OVOS-MSG-1 §2.1.1 — neither may contain `:` — so the single `:` split is unambiguous. -### 7.0 `` is a `skill_id` +### 7.0 `Match.skill_id` is the handler's identity -`Match.owner_id` is the `skill_id` of the component that will +`Match.skill_id` is the `skill_id` of the component that will handle the dispatch. The orchestrator does not distinguish between a skill whose intents were registered via OVOS-INTENT-4 and a -pipeline plugin that matched itself — both are handler-owners -reached by the same `:` topic, and the +pipeline plugin that matched itself — both are reached by the +same `:` dispatch topic, and the dispatched handler has the same obligations as any skill (OVOS-INTENT-4 §3.1). -A pipeline plugin that returns matches where `owner_id` equals its +A pipeline plugin that returns matches where `skill_id` equals its own `pipeline_id` is simply a component whose `skill_id` and `pipeline_id` happen to be the same identifier. It skips the OVOS-INTENT-4 registration step because it consumes no external @@ -749,21 +749,21 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): `reply`, so `destination` is the original utterance emitter and `source` is the orchestrator; - **`context["skill_id"]` stamping.** The orchestrator **MUST** - stamp `context["skill_id"] = ` on every dispatch. + stamp `context["skill_id"] = ` on every dispatch. MSG-1 derivation semantics carry this value forward into every Message the handler emits, satisfying OVOS-INTENT-4 §3.1 by construction. - **`context["pipeline_id"]` stamping.** The orchestrator **MUST** stamp `context["pipeline_id"]` on every dispatch with the `pipeline_id` of the plugin that produced the match (§3.1). When - the match is self-addressed (`owner_id == pipeline_id`, §7.0), + the match is self-addressed (`skill_id == pipeline_id`, §7.0), both context keys carry the same identifier. The dispatch Message's `data`: ```json { - "owner_id": "music.skill", + "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US", "utterance": "play the beatles", @@ -773,7 +773,7 @@ The dispatch Message's `data`: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id` | string | yes | The `Match.owner_id` — the topic's prefix, repeated for the handler's convenience. | +| `skill_id` | string | yes | The `Match.skill_id` — the topic's prefix, repeated for the handler's convenience. | | `intent_name` | string | yes | The `Match.intent_name` — the topic's suffix. | | `lang` | string | conditional | The language the utterance was recognized in. Populated from `Match.lang` when present. When `Match.lang` is absent, the orchestrator falls back to the entry-topic `Message.data.lang` (§9.1) if that field was present. If neither source provides a language, this field **MUST** be omitted. Handlers **MUST** treat this field as optional — its absence means no authoritative content language was determined for this match. | | `utterance` | string | yes | The candidate string that won the match. | @@ -782,7 +782,7 @@ The dispatch Message's `data`: ### 7.2 Subscription discipline Each handler subscribes to exactly its own -`:` topic. A skill subscribes to topics +`:` topic. A skill subscribes to topics under its own `skill_id`; a plugin that bundles its own handlers subscribes to topics under its own `pipeline_id`. Because each topic is unique to one handler, the bus delivers the dispatch @@ -805,7 +805,7 @@ pipeline plugin role. A reserved intent_name is one that: - a pipeline plugin **MAY** emit as the `intent_name` of a returned `Match` to signal "this match was produced by the role that reserves the name"; the dispatch then proceeds - normally per §7, addressed to `:`, + normally per §7, addressed to `:`, and the handler subscribed to that topic does whatever the reserving specification defines. @@ -820,8 +820,8 @@ Reservations currently in force: | Reserved intent_name | Reserving spec | Meaning of a Match bearing this name | |----------------------|----------------|--------------------------------------| -| `converse` | OVOS-CONVERSE-1 §4 | a converse plugin's claim that `` (an active handler) wants this utterance — the orchestrator dispatches `:converse` and the owner's converse handler runs | -| `response` | OVOS-CONVERSE-1 §5 | a converse plugin's signal that `` (the response-mode holder) is to receive the awaited utterance — the orchestrator dispatches `:response` and the owner's response handler runs | +| `converse` | OVOS-CONVERSE-1 §4 | a converse plugin's claim that `` (an active handler) wants this utterance — the orchestrator dispatches `:converse` and the owner's converse handler runs | +| `response` | OVOS-CONVERSE-1 §5 | a converse plugin's signal that `` (the response-mode holder) is to receive the awaited utterance — the orchestrator dispatches `:response` and the owner's response handler runs | This specification fixes only the registry mechanism (reservation listing); the per-name semantics are owned by the reserving @@ -832,16 +832,16 @@ A plain skill (§7.0) subscribes to a reserved-name dispatch topic via framework convention rather than OVOS-INTENT-4 registration — the reserved name is not registrable. The normal skill path (INTENT-4-registered intents) and the reserved-name path share the -same `:` dispatch shape; no dispatch +same `:` dispatch shape; no dispatch mechanics change. ### 7.4 In-process equivalence -When the handler-owning component (skill or plugin) runs in the +When the handler (skill or plugin) runs in the same process as the orchestrator, the orchestrator **MAY** invoke the handler directly without serializing the dispatch Message over a transport — provided every external observer sees the -same `:` dispatch and the same +same `:` dispatch and the same handler-lifecycle trio (§8) it would have seen for an out-of-process handler. This uniformity is what makes a deployment portable across in-process and out-of-process handler @@ -892,7 +892,7 @@ Each lifecycle message's `data`: ```json { - "owner_id": "music.skill", + "skill_id": "music.skill", "intent_name": "play_music" } ``` @@ -901,7 +901,7 @@ Each lifecycle message's `data`: ```json { - "owner_id": "music.skill", + "skill_id": "music.skill", "intent_name": "play_music", "exception": "RuntimeError: Spotify is not configured" } @@ -909,7 +909,7 @@ Each lifecycle message's `data`: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id` | string | yes | The handler-owning component's id (skill_id or pipeline_id). | +| `skill_id` | string | yes | The `skill_id` of the handler that was dispatched. | | `intent_name` | string | yes | The intent the handler was dispatched for. | | `exception` | string | `error` only | Human-readable description of the failure raised by the handler. | @@ -979,7 +979,7 @@ plugin's id: ```json { - "owner_id": "music.skill", + "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US", "utterance": "play the beatles", @@ -990,7 +990,7 @@ plugin's id: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `owner_id`, `intent_name`, `lang`, `utterance`, `captures` | as §7.1 | yes | Same fields as the dispatch payload. | +| `skill_id`, `intent_name`, `lang`, `utterance`, `captures` | as §7.1 | yes | Same fields as the dispatch payload. | | `pipeline_id` | string | yes | The `pipeline_id` of the plugin that produced the match. | `ovos.intent.matched` is a **notification**, not a dispatch. @@ -1028,7 +1028,7 @@ ran and raised." ### 9.4 The dispatch topic -`:` — see §7. +`:` — see §7. ### 9.5 `ovos.utterance.handled` @@ -1131,7 +1131,7 @@ loaded plugin emits one query per `pipeline_id` it cares about and aggregates the responses itself. The `pipeline_id` in the topic is the same identifier carried by -`session.pipeline` (§5) and by `Match.owner_id` when the plugin +`session.pipeline` (§5) and by `Match.skill_id` when the plugin owns its own handler (§7); a consumer that has already observed a `pipeline_id` from any of these sources can query it directly. @@ -1145,12 +1145,12 @@ The plugin **MUST** reply with the currently-loaded intent set: "intents": [ { "intent_name": "play_music", - "owner_id": "music.skill", + "skill_id": "music.skill", "lang": "en-US" }, { "intent_name": "stop_music", - "owner_id": "music.skill", + "skill_id": "music.skill", "lang": "en-US" } ] @@ -1162,7 +1162,7 @@ The plugin **MUST** reply with the currently-loaded intent set: | `pipeline_id` | string | yes | The responding plugin's id. | | `intents` | array | yes | Currently-loaded intents (possibly empty). | | `intents[].intent_name` | string | yes | Intent identifier. | -| `intents[].owner_id` | string | yes | The owning component (`skill_id` or `pipeline_id` when plugin-owned). | +| `intents[].skill_id` | string | yes | The `skill_id` of the handler. For a self-matching plugin, equals its `pipeline_id`. | | `intents[].lang` | string | yes | The language the intent is registered for. | A plugin **MAY** include additional per-intent fields (engine @@ -1174,7 +1174,7 @@ metadata, confidence thresholds, sample templates) but consumers The request payload **MAY** carry filters: ```json -{ "lang": "en-US", "owner_id": "music.skill" } +{ "lang": "en-US", "skill_id": "music.skill" } ``` When a filter is present, the plugin **SHOULD** restrict its @@ -1228,7 +1228,7 @@ from the hosting process. - emit `complete_intent_failure` when no plugin claimed (§9.3); - emit `ovos.intent.matched` (§9.2) on every successful claim, before the dispatch; -- dispatch on `:` per §7; +- dispatch on `:` per §7; - handle a plugin exception by logging and continuing to the next plugin (§6.2), not by failing the utterance; - emit the handler-lifecycle trio (§8) wrapping every handler @@ -1240,7 +1240,7 @@ from the hosting process. - expose a `match(utterance, lang, session) → Match | None` operation (§4); -- when claiming, return a `Match` with `owner_id` and +- when claiming, return a `Match` with `skill_id` and `intent_name` per §4 — never a partial or speculative claim; - bear a `pipeline_id` distinct from any other loaded plugin's id (§3); From da4c7d77c62bdd86a69bca489411c3cdc9df8fe9 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 18:55:49 +0100 Subject: [PATCH 46/57] PIPELINE-1: reframe as foundational spec; drop INTENT-4 from builds-on INTENT-4 is an optional layer built on top of PIPELINE-1, not a dependency of it. Move INTENT-4 to a "See also" note. Reframe the opening paragraph so the spec presents itself as the NL entry/exit boundary rather than a companion to the intent specs. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pipeline.md b/pipeline.md index 264031f..9f65b4f 100644 --- a/pipeline.md +++ b/pipeline.md @@ -8,22 +8,25 @@ the assistant is done with it — and the **pipeline plugin** abstraction the **orchestrator** runs to decide what to do with each utterance. -It is the orchestrator-side companion to OVOS-INTENT-3 and -OVOS-INTENT-4. Those specifications define what an intent *is* and -how a skill puts an intent on the bus; this one defines what the -orchestrator does with utterances and the contract every pipeline -plugin conforms to. +This is the **foundational bus specification for voice assistant +input/output**: it defines the natural-language entry point +(`ovos.utterance.handle`, §9.1), the pipeline plugin abstraction +the orchestrator iterates, and the natural-language exit point +(`ovos.utterance.speak`, §9.6). Intent registration and skill +dispatch are an optional layer built on top of this mechanism. -It builds on three companion specifications: +It builds on two companion specifications: - the *Bus Message Specification* (OVOS-MSG-1) — the envelope, routing keys, session carrier, and derivations every Message defined here travels in; - the *Intent Definition Specification* (OVOS-INTENT-3) — defines - the *orchestrator* and the intent / handler model; -- the *Intent and Entity Registration Bus Contract* (OVOS-INTENT-4) - — the wire format pipeline plugins consume to learn what intents - to match (when they choose to). + the *orchestrator* and the intent / handler model. + +See also: OVOS-INTENT-4 (*Intent and Entity Registration Bus +Contract*) — the wire format pipeline plugins MAY consume to learn +what intents skills have registered. Consumption is plugin- +discretionary; this specification does not require it. The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT** and **MAY** are used as in RFC 2119. From 30d771d26647d0150b28178458a1614a61873048 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:13:08 +0100 Subject: [PATCH 47/57] =?UTF-8?q?PIPELINE-1:=20rename=20complete=5Fintent?= =?UTF-8?q?=5Ffailure=20=E2=86=92=20ovos.intent.unmatched?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows MSG-1 dot-namespaced topic convention. Symmetric with ovos.intent.matched. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pipeline.md b/pipeline.md index 9f65b4f..8b63b4d 100644 --- a/pipeline.md +++ b/pipeline.md @@ -51,7 +51,7 @@ This specification defines: `ovos.intent.handler.start` / `.complete` / `.error`; - the **utterance-layer bus events** (§9) — the utterance entry topic `ovos.utterance.handle` (§9.1), - `ovos.intent.matched`, `complete_intent_failure`, + `ovos.intent.matched`, `ovos.intent.unmatched`, `ovos.utterance.handled`, and the natural-language response topic `ovos.utterance.speak` (§9.6); - **conformance** (§11). @@ -377,7 +377,7 @@ session (OVOS-SESSION-1 §3.1). The default-session pipeline is owned and maintained by the orchestrator and represents what the deployment runs when no preference is expressed. If the default session itself has no `pipeline` configured, the utterance proceeds -to no-match (`complete_intent_failure`, §9.3). +to no-match (`ovos.intent.unmatched`, §9.3). Different sessions may carry different `pipeline`. This is how a session origin expresses different preferences for different @@ -518,7 +518,7 @@ The intended separation of concerns is sharp: If every requested `pipeline_id` is dropped by availability or policy, the effective pipeline is empty and the utterance proceeds -directly to no-match (`complete_intent_failure`, §9.3). The +directly to no-match (`ovos.intent.unmatched`, §9.3). The orchestrator **MUST NOT** silently fall back to the default-session pipeline in this case — falling back would let a policy-rejected preference pull in a different ordering the origin never asked for @@ -608,7 +608,7 @@ ovos.utterance.handle ← entry (§9.1) │ break │ └─ if no plugin matched (or all matches filtered): - complete_intent_failure (§9.3) + ovos.intent.unmatched (§9.3) ovos.utterance.handled (§9.5) ``` @@ -646,7 +646,7 @@ For each utterance, the orchestrator **MUST**: (OVOS-TRANSFORM-1 §3.2, §3.3) before pipeline iteration begins; - if the utterance-transformer chain returns an **empty utterance list**, skip pipeline iteration entirely and proceed - directly to `complete_intent_failure` (§9.3) — `match()` is + directly to `ovos.intent.unmatched` (§9.3) — `match()` is contractually defined over a non-empty list (§4) and the orchestrator **MUST NOT** invoke any plugin with an empty list. (If the empty list arrived together with cancellation @@ -656,7 +656,7 @@ For each utterance, the orchestrator **MUST**: - for each `pipeline_id`, call `match` on the corresponding loaded plugin (skipping unknown identifiers, §5); - stop at the **first plugin** that returns a non-`None` `Match`; -- if no plugin returns a `Match`, emit `complete_intent_failure` +- if no plugin returns a `Match`, emit `ovos.intent.unmatched` (§9.3). A plugin that raises an exception during `match` is treated as if @@ -699,7 +699,7 @@ followed by the universal end-marker `ovos.utterance.handled`: | Outcome | Sequence of utterance-layer events | |---------|------------------------------------| | Matched by a plugin | `ovos.intent.matched` → dispatch + (handler trio §8) → `ovos.utterance.speak` ×0..N → `ovos.utterance.handled` | -| No plugin matched | `complete_intent_failure` → `ovos.utterance.handled` | +| No plugin matched | `ovos.intent.unmatched` → `ovos.utterance.handled` | | Cancelled by a transformer | `ovos.utterance.cancelled` → `ovos.utterance.handled` (see OVOS-TRANSFORM-1 §8.2) | If a dispatched handler emits `ovos.intent.handler.error` (§8) @@ -1001,7 +1001,7 @@ Consumers **MUST NOT** treat receipt as permission or instruction to run a handler — handler invocation happens via the dispatch topic (§7). -### 9.3 `complete_intent_failure` +### 9.3 `ovos.intent.unmatched` Emitted by the orchestrator when pipeline iteration completed with no plugin claiming the utterance. Broadcast. @@ -1025,7 +1025,7 @@ This message **MUST** be followed immediately by `ovos.utterance.handled` (§9.5). This is the **intent-layer failure** signal. It is distinct from -a handler-layer error (§8): `complete_intent_failure` means "no +a handler-layer error (§8): `ovos.intent.unmatched` means "no plugin claimed"; `ovos.intent.handler.error` means "a handler ran and raised." @@ -1228,7 +1228,7 @@ from the hosting process. - iterate `session.pipeline` in order (§6.2) and stop at the first plugin returning a non-`None` `Match`; - skip unknown `pipeline_id`s without failing the utterance (§5); -- emit `complete_intent_failure` when no plugin claimed (§9.3); +- emit `ovos.intent.unmatched` when no plugin claimed (§9.3); - emit `ovos.intent.matched` (§9.2) on every successful claim, before the dispatch; - dispatch on `:` per §7; From 76f46fa407f3cb9891e00b55fad4730f5808e128 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:17:59 +0100 Subject: [PATCH 48/57] =?UTF-8?q?PIPELINE-1=20=C2=A74.4:=20add=20latency?= =?UTF-8?q?=20discipline=20=E2=80=94=20defer=20long-running=20work=20to=20?= =?UTF-8?q?handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugins SHOULD return from match immediately and defer expensive work (model inference, network calls) to the handler phase. Canonical example: an LLM plugin can match instantly and generate in the handler. Orchestrator SHOULD surface match-phase duration as an observable metric. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/pipeline.md b/pipeline.md index 8b63b4d..40bf637 100644 --- a/pipeline.md +++ b/pipeline.md @@ -295,7 +295,7 @@ chooses to surface. It **MAY** be empty. The orchestrator does not interpret the capture map; it forwards it to the dispatched handler. -### 4.4 Match-phase timeout +### 4.4 Match-phase timeout and latency discipline The `match` operation is logically synchronous from the orchestrator's perspective — the orchestrator calls `match` and waits for the return @@ -316,6 +316,24 @@ The timeout bound, the recovery policy, and whether a timed-out plugin triggers the §6.2 circuit-breaker are deployer-configurable. This specification fixes only that the discipline **SHOULD** exist. +**Latency discipline.** In voice-assistant deployments, match-phase +latency directly determines response latency — the pipeline is +sequential and the user is waiting. Plugins **SHOULD** therefore return +from `match` as quickly as possible and defer all long-running work to +the handler phase. A plugin that can determine it will claim an +utterance without fully processing it **SHOULD** return a `Match` +immediately and begin expensive processing (model inference, network +calls, disambiguation) inside the handler, not inside `match`. + +A language-model plugin is the canonical example: it typically knows it +will consume any utterance that reaches it and can return a `Match` +immediately; the actual generation belongs in the handler. The match +phase is a routing decision, not a processing phase. + +The orchestrator **SHOULD** surface match-phase duration as an +observable metric so deployers can identify plugins that violate this +discipline. + --- ## 5. Session fields owned by this specification From 728974ba9ef6a8d35e0cb1e2b92e242094c114c2 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:37:01 +0100 Subject: [PATCH 49/57] =?UTF-8?q?PIPELINE-1:=20captures=E2=86=92slots,=20u?= =?UTF-8?q?tterance=E2=86=92utterances=20param,=20drop=203=20non-normative?= =?UTF-8?q?=20paragraphs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Match.captures → Match.slots throughout (§4.1, §4.3, §7.1, §9.2, §6.1) - match(utterance,…) → match(utterances,…) in signature, §4 inputs, §11 - Drop naming-convention explanation paragraphs from §9.1 and §9.6 - Drop §4.2 "flow diagram reflects this" cross-ref (diagram already shows it) Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 44 +++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/pipeline.md b/pipeline.md index 40bf637..9260b10 100644 --- a/pipeline.md +++ b/pipeline.md @@ -164,12 +164,12 @@ plugin or the handler. A plugin exposes one operation to the orchestrator: ``` -match(utterance, lang, session) → Match | None +match(utterances, lang, session) → Match | None ``` Inputs: -- `utterance` — a **non-empty list of candidate strings**. The +- `utterances` — a **non-empty list of candidate strings**. The list typically originates from the entry topic (§9.1) and may have been modified by the utterance-transformer chain (OVOS-TRANSFORM-1 §3.2) before reaching the plugin. A @@ -203,7 +203,7 @@ fields below. | `skill_id` | string | yes | The `skill_id` of the handler to invoke. For a pipeline plugin that matches itself, this equals its `pipeline_id` (§7.0). | | `intent_name` | string | yes | An opaque non-empty string that, together with `skill_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | | `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | -| `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | +| `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | | `utterance` | string | yes | The specific candidate string from the input list that won the match. A plugin that does not track which candidate won **MUST** populate this with the first element of the input list as a fallback; the orchestrator forwards this value verbatim as `data.utterance` in the dispatch payload (§7.1) and **MUST NOT** substitute another value. | | `updated_session` | object | no | A replacement `session` snapshot the plugin produced during `match` (§4.2). When present, the orchestrator MUST use this snapshot — in place of the inbound utterance's session — for the dispatch and every downstream stage. When absent, the inbound session is carried unchanged. This is the **only** mechanism by which a plugin's match-phase session mutations reach downstream consumers; in-place mutations of the inbound session object are not visible past the plugin boundary. | @@ -274,25 +274,21 @@ session-mutation pathway** of OVOS-MSG-1 (which CONTEXT-1 §5.3 and CONVERSE-1 §3.2 build on) is governed by those specs and is unaffected by §4.2. -The §6 flow diagram reflects this: `session = match.updated_session -or session` is applied immediately after a non-null match, -before the post-match-pre-dispatch window where intent -transformers and CONTEXT-1's decay tick run. -### 4.3 The capture map +### 4.3 The slot map -`Match.captures` is a `{string: string}` mapping (the same shape +`Match.slots` is a `{string: string}` mapping (the same shape OVOS-INTENT-3 §7 defines for template / keyword intent slots). For skill-owned matches against intents the plugin previously -consumed from OVOS-INTENT-4 registrations, the capture map keys +consumed from OVOS-INTENT-4 registrations, the slot map keys are the slot names (template intents) or vocabulary names (keyword intents) of the matched intent. -For plugin-owned matches, the capture map is whatever the plugin +For plugin-owned matches, the slot map is whatever the plugin chooses to surface. It **MAY** be empty. -The orchestrator does not interpret the capture map; it forwards +The orchestrator does not interpret the slot map; it forwards it to the dispatched handler. ### 4.4 Match-phase timeout and latency discipline @@ -599,7 +595,7 @@ ovos.utterance.handle ← entry (§9.1) │ ├─ for pipeline_id in effective pipeline: │ plugin = loaded_plugins[pipeline_id] # skip if not loaded - │ match = plugin.match(utterance, lang, session) + │ match = plugin.match(utterances, lang, session) │ if match is None: │ continue # any plugin-side updated_session is discarded │ @@ -611,7 +607,7 @@ ovos.utterance.handle ← entry (§9.1) │ ┌── post-match-pre-dispatch window ──────────────┐ │ │ engine-side context promotion (CONTEXT-1 §5.3) │ │ │ intent-transformer chain runs (TRANSFORM-1 │ - │ │ §3.4) — may modify Match.captures, MUST NOT │ + │ │ §3.4) — may modify Match.slots, MUST NOT │ │ │ change skill_id / intent_name │ │ │ post-decay turns_remaining-- (CONTEXT-1 §4) │ │ └────────────────────────────────────────────────┘ @@ -788,7 +784,7 @@ The dispatch Message's `data`: "intent_name": "play_music", "lang": "en-US", "utterance": "play the beatles", - "captures": { "query": "the beatles" } + "slots": { "query": "the beatles" } } ``` @@ -798,7 +794,7 @@ The dispatch Message's `data`: | `intent_name` | string | yes | The `Match.intent_name` — the topic's suffix. | | `lang` | string | conditional | The language the utterance was recognized in. Populated from `Match.lang` when present. When `Match.lang` is absent, the orchestrator falls back to the entry-topic `Message.data.lang` (§9.1) if that field was present. If neither source provides a language, this field **MUST** be omitted. Handlers **MUST** treat this field as optional — its absence means no authoritative content language was determined for this match. | | `utterance` | string | yes | The candidate string that won the match. | -| `captures` | object (string→string) | yes | The capture map (§4.3). MAY be empty. | +| `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | ### 7.2 Subscription discipline @@ -964,12 +960,6 @@ that wants to feed an utterance into the assistant — a listener, a chat bridge, a CLI, a test harness, a remote-peer client. Receiving on this topic kicks off the lifecycle of §6. -The topic name follows the naming conventions of OVOS-MSG-1 §2.1: -imperative-mood verb (`handle` — a request for the assistant to -handle this utterance), dot-separated hierarchy, no `:` (which is -reserved for component-pair dispatch topics), and pairs with the -past-tense terminal event `ovos.utterance.handled` (§9.5) by -shared root verb. Payload shape: @@ -1004,14 +994,14 @@ plugin's id: "intent_name": "play_music", "lang": "en-US", "utterance": "play the beatles", - "captures": { "query": "the beatles" }, + "slots": { "query": "the beatles" }, "pipeline_id": "template-high" } ``` | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `skill_id`, `intent_name`, `lang`, `utterance`, `captures` | as §7.1 | yes | Same fields as the dispatch payload. | +| `skill_id`, `intent_name`, `lang`, `utterance`, `slots` | as §7.1 | yes | Same fields as the dispatch payload. | | `pipeline_id` | string | yes | The `pipeline_id` of the plugin that produced the match. | `ovos.intent.matched` is a **notification**, not a dispatch. @@ -1083,10 +1073,6 @@ A deployment with no audio output (a text-only chat bridge, a test harness) receives the same `ovos.utterance.speak` Message as an audio-capable deployment. -The topic name follows the naming conventions of OVOS-MSG-1 §2.1: -imperative-mood verb (`speak` — a request for the assistant to speak -this response), dot-separated hierarchy, same `ovos.utterance.*` -namespace as the entry topic. **Payload:** @@ -1259,7 +1245,7 @@ from the hosting process. ### A **pipeline plugin** **MUST**: -- expose a `match(utterance, lang, session) → Match | None` operation +- expose a `match(utterances, lang, session) → Match | None` operation (§4); - when claiming, return a `Match` with `skill_id` and `intent_name` per §4 — never a partial or speculative claim; From 718e63cb688c47e18f7a3d528a30c1e47686a70b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:38:02 +0100 Subject: [PATCH 50/57] =?UTF-8?q?PIPELINE-1=20=C2=A76.1:=20dialog/TTS=20tr?= =?UTF-8?q?ansformers=20run=20in=20output=20layer=20after=20.handled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They are not part of the pipeline lifecycle; move them outside the flow diagram's break point and correct the prose to say they run just before TTS rendering in the output layer, not inside the dispatch path. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/pipeline.md b/pipeline.md index 9260b10..afd01a2 100644 --- a/pipeline.md +++ b/pipeline.md @@ -616,10 +616,11 @@ ovos.utterance.handle ← entry (§9.1) │ dispatch on : (§7) │ (handler runs; emits lifecycle trio §8) │ ovos.utterance.speak (×0..N) (§9.6) - │ dialog-transformer chain runs ← TRANSFORM-1 §3.5 - │ (output-path delivery; tts-transformer chain — §3.6) │ ovos.utterance.handled (§9.5) │ break + │ [output layer — outside this spec's scope] + │ (dialog-transformer chain ← TRANSFORM-1 §3.5) + │ (tts-transformer chain ← TRANSFORM-1 §3.6) │ └─ if no plugin matched (or all matches filtered): ovos.intent.unmatched (§9.3) @@ -635,20 +636,15 @@ and before iteration, against the candidate utterance list. The **post-match-pre-dispatch window** (highlighted) is where CONTEXT-1 §5.3 sanctions engine-side `session.intent_context` mutation and where TRANSFORM-1 §3.4 inserts the intent-transformer -chain over the chosen `Match`. The **dialog** and **TTS** -transformer chains run on handler-emitted natural-language responses and -on the rendered audio respectively, off the dispatch return-path. -**`ovos.utterance.handled` is emitted at handler completion** — -immediately after `ovos.intent.handler.complete` (or `.error`) — and -does **not** gate on any downstream output delivery. A handler's -obligation to this specification ends when it returns; it emits zero or -more `ovos.utterance.speak` Messages (§9.6) without knowing or caring -whether the deployment has any audio-output capability at all. Audio -output is fully decoupled from the pipeline: a chat-only deployment -receives the same utterance lifecycle and the same end-marker as an -audio deployment. The dialog and TTS transformer chains are shown in the -flow diagram in their logical causal position; they are not a -synchronization barrier for `ovos.utterance.handled`. +chain over the chosen `Match`. **`ovos.utterance.handled` is emitted at handler completion** — +immediately after `ovos.intent.handler.complete` (or `.error`). +The **dialog-transformer** and **TTS-transformer** chains +(TRANSFORM-1 §3.5 / §3.6) run in the output layer after +`ovos.utterance.handled`, just before TTS rendering; they are +outside this specification's scope and are not a synchronization +barrier for the end-marker. Audio output is fully decoupled from +the pipeline: a chat-only deployment receives the same utterance +lifecycle and the same end-marker as an audio deployment. Pseudocode is informative; normative rules are in §§4–9. @@ -1092,9 +1088,9 @@ audio-capable deployment. `ovos.utterance.speak` emission from the dispatch Message (§7) it received, per MSG-1 §5 derivation semantics. This carries `context.session` and `context.skill_id` forward automatically — -downstream stages (dialog-transformer chain OVOS-TRANSFORM-1 §3.5, -the output-path specification) can read the session and attribute the -response without additional wire fields. An `ovos.utterance.speak` +the output layer (dialog-transformer chain OVOS-TRANSFORM-1 §3.5, +TTS, delivery) can read the session and attribute the response +without additional wire fields. An `ovos.utterance.speak` Message that does not derive from a dispatch is non-conformant. **Multiplicity and ordering.** A handler **MAY** emit zero or more From d85e92f64b4accb1be72362815dfa77d780985b9 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:44:36 +0100 Subject: [PATCH 51/57] =?UTF-8?q?PIPELINE-1=20=C2=A76.2:=20trim=20circuit-?= =?UTF-8?q?breaker;=20drop=20ovos.pipeline.dropped=20topic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapsed the 10-line circuit-breaker block to 4 lines — the SHOULD discipline is preserved, the over-specified event topic and payload are dropped. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/pipeline.md b/pipeline.md index afd01a2..927de1e 100644 --- a/pipeline.md +++ b/pipeline.md @@ -308,9 +308,8 @@ timed-out call is discarded (there is no `Match` to carry an `updated_session`; the inbound session is unchanged). No bus event is emitted for the timeout at this stage. -The timeout bound, the recovery policy, and whether a timed-out plugin -triggers the §6.2 circuit-breaker are deployer-configurable. This -specification fixes only that the discipline **SHOULD** exist. +The timeout bound and whether it counts toward the §6.2 circuit-breaker +are deployer-configurable. **Latency discipline.** In voice-assistant deployments, match-phase latency directly determines response latency — the pipeline is @@ -674,23 +673,11 @@ it returned `None`. The orchestrator **MUST** continue to the next plugin and **SHOULD** log the exception. A single plugin's bug does not fail the whole utterance. -**Repeated-exception circuit-breaker.** Persistent plugin failure -is a deployment concern, not an utterance-level concern, but a -defective plugin that raises on every invocation degrades every -utterance until the deployment intervenes. An orchestrator -**SHOULD** track per-plugin exception rates within a session (or -across a configurable window) and **SHOULD** drop a plugin from -the session's effective pipeline (§5.5) after a deployer-tunable -threshold of consecutive exceptions — typically three. A dropped -plugin behaves as if absent for the remainder of the session; -recovery is a deployment concern (process restart, plugin reload). -The threshold, the recovery policy, and whether the drop is -per-session or process-wide are deployer-configurable; this -specification fixes only that the discipline **SHOULD** exist. -The orchestrator **MAY** broadcast an `ovos.pipeline.dropped` -diagnostic event (payload `{pipeline_id, reason, exception_count}`) -to make the drop observable; this event is informative and -**MUST NOT** be relied upon for normative control flow. +**Repeated-exception circuit-breaker.** An orchestrator **SHOULD** +drop a plugin from the effective pipeline after a deployer-tunable +consecutive-exception threshold. A dropped plugin behaves as if +absent; recovery is a deployment concern. The threshold and scope +(per-session or process-wide) are deployer-configurable. ### 6.3 Plugins do not see each other's matches From 1688c9d38ca8fcbb6abf94b67b5cd3935d1006c0 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:46:19 +0100 Subject: [PATCH 52/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.1:=20drop=20skill=5F?= =?UTF-8?q?id=20and=20intent=5Fname=20from=20dispatch=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are encoded in the topic (:); repeating them in data is redundant. A handler that needs them splits the topic on ':'. §9.2 ovos.intent.matched keeps them (broadcast, no topic encoding). Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pipeline.md b/pipeline.md index 927de1e..f3cd867 100644 --- a/pipeline.md +++ b/pipeline.md @@ -763,8 +763,6 @@ The dispatch Message's `data`: ```json { - "skill_id": "music.skill", - "intent_name": "play_music", "lang": "en-US", "utterance": "play the beatles", "slots": { "query": "the beatles" } @@ -773,12 +771,12 @@ The dispatch Message's `data`: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `skill_id` | string | yes | The `Match.skill_id` — the topic's prefix, repeated for the handler's convenience. | -| `intent_name` | string | yes | The `Match.intent_name` — the topic's suffix. | | `lang` | string | conditional | The language the utterance was recognized in. Populated from `Match.lang` when present. When `Match.lang` is absent, the orchestrator falls back to the entry-topic `Message.data.lang` (§9.1) if that field was present. If neither source provides a language, this field **MUST** be omitted. Handlers **MUST** treat this field as optional — its absence means no authoritative content language was determined for this match. | | `utterance` | string | yes | The candidate string that won the match. | | `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | +`skill_id` and `intent_name` are not repeated in the payload — they are the topic's `:` prefix and suffix. A handler that needs them splits the topic on `:`. + ### 7.2 Subscription discipline Each handler subscribes to exactly its own @@ -968,8 +966,7 @@ Emitted by the orchestrator after a plugin's `match` returns non-`None`, before the dispatch (§7) goes out. Broadcast (no `destination`). -Payload mirrors the dispatch payload (§7.1) plus the matching -plugin's id: +Payload: ```json { @@ -984,7 +981,9 @@ plugin's id: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `skill_id`, `intent_name`, `lang`, `utterance`, `slots` | as §7.1 | yes | Same fields as the dispatch payload. | +| `skill_id` | string | yes | The handler's `skill_id`. | +| `intent_name` | string | yes | The matched intent name. | +| `lang`, `utterance`, `slots` | as §7.1 | — | Same semantics as the dispatch payload. | | `pipeline_id` | string | yes | The `pipeline_id` of the plugin that produced the match. | `ovos.intent.matched` is a **notification**, not a dispatch. From b34d9508a3ff2a13035a447f4e4153bc285a011f Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:47:23 +0100 Subject: [PATCH 53/57] =?UTF-8?q?PIPELINE-1=20=C2=A77.1:=20mandate=20lang?= =?UTF-8?q?=20in=20dispatch=20payload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By dispatch time the pipeline must have resolved a content language. lang is now required in the dispatch data; a match with no lang and no entry-topic lang is treated as declined. Match.lang updated to reflect the same obligation. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipeline.md b/pipeline.md index f3cd867..67311a5 100644 --- a/pipeline.md +++ b/pipeline.md @@ -202,7 +202,7 @@ fields below. |-------|------|----------|---------| | `skill_id` | string | yes | The `skill_id` of the handler to invoke. For a pipeline plugin that matches itself, this equals its `pipeline_id` (§7.0). | | `intent_name` | string | yes | An opaque non-empty string that, together with `skill_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | -| `lang` | string | no | The BCP-47 language tag the match was performed against. When the plugin received a non-`None` `lang` parameter (§4), this is typically that value (the plugin matched in the language the caller declared). A plugin that determined the language by other means (a multilingual matcher, a content-language-detecting matcher, a hard-coded engine) **MAY** set `lang` to whatever value reflects the language of the match. Absent when the plugin does not commit to a language — for example, a fallback plugin matching on language-independent rules. Downstream stages (intent-transformer chain per OVOS-TRANSFORM-1 §3.4, handlers under dispatch) treat `Match.lang` as authoritative for the match's language. | +| `lang` | string | no | The BCP-47 language tag the match was performed against. A plugin that determined the language itself (multilingual matcher, hard-coded engine) **MUST** set this. A plugin that matched in the language the entry-topic declared **MAY** omit it and rely on the orchestrator's fallback to the entry-topic `lang` (§7.1). If neither the plugin nor the entry-topic provides a language, the orchestrator treats the match as declined (§7.1). | | `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | | `utterance` | string | yes | The specific candidate string from the input list that won the match. A plugin that does not track which candidate won **MUST** populate this with the first element of the input list as a fallback; the orchestrator forwards this value verbatim as `data.utterance` in the dispatch payload (§7.1) and **MUST NOT** substitute another value. | | `updated_session` | object | no | A replacement `session` snapshot the plugin produced during `match` (§4.2). When present, the orchestrator MUST use this snapshot — in place of the inbound utterance's session — for the dispatch and every downstream stage. When absent, the inbound session is carried unchanged. This is the **only** mechanism by which a plugin's match-phase session mutations reach downstream consumers; in-place mutations of the inbound session object are not visible past the plugin boundary. | @@ -771,7 +771,7 @@ The dispatch Message's `data`: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `lang` | string | conditional | The language the utterance was recognized in. Populated from `Match.lang` when present. When `Match.lang` is absent, the orchestrator falls back to the entry-topic `Message.data.lang` (§9.1) if that field was present. If neither source provides a language, this field **MUST** be omitted. Handlers **MUST** treat this field as optional — its absence means no authoritative content language was determined for this match. | +| `lang` | string | yes | The content language of the match. The orchestrator **MUST** populate this from `Match.lang` when present, falling back to the entry-topic `Message.data.lang` (§9.1). A plugin that returns a `Match` without a `lang` and whose entry-topic carried no `lang` is a pipeline configuration error — the orchestrator **MUST NOT** dispatch; it **MUST** treat the match as if the plugin declined and continue iteration. | | `utterance` | string | yes | The candidate string that won the match. | | `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | From 45f1f5dde465f66f5ea27f75bae7c54eccce286b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 19:49:17 +0100 Subject: [PATCH 54/57] =?UTF-8?q?PIPELINE-1:=20mandate=20Match.lang=20?= =?UTF-8?q?=E2=80=94=20plugin=20owns=20language=20resolution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match.lang is now required. Language resolution is the plugin's explicit responsibility: use the entry-topic lang hint, session signals, or any other policy, then declare the result. A Match without lang is malformed and treated as declined. Dispatch lang is taken directly from Match.lang with no orchestrator fallback. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pipeline.md b/pipeline.md index 67311a5..0a0556e 100644 --- a/pipeline.md +++ b/pipeline.md @@ -178,18 +178,14 @@ Inputs: language. A plugin is free to consider all candidates, only the first, or any subset; the orchestrator does not prescribe how candidates are weighted. -- `lang` — the **optional** BCP-47 content-language tag of the - utterance, sourced by the orchestrator from `Message.data.lang` - of the entry-topic Message (§9.1). **Present only when the - producer authoritatively knew the content language; absent - otherwise.** The orchestrator **MUST NOT** synthesize a value - when `Message.data.lang` is absent — in particular, it - **MUST NOT** fall back to `session.lang` or to any per-utterance - language signal of OVOS-SESSION-1 §3.2. A plugin that needs a - language and receives `lang: None` **MAY** consult `session` - for OVOS-SESSION-1 §3.2 signals or apply its own resolution - policy — the choice is the plugin's. A plugin that does not - need the language ignores the parameter. +- `lang` — the **optional** BCP-47 content-language hint sourced + from `Message.data.lang` of the entry-topic (§9.1). Present only + when the producer authoritatively knew the content language; + absent otherwise. The orchestrator **MUST NOT** synthesize a + value. The plugin uses this as input to its own language + resolution — consulting `session` (OVOS-SESSION-1 §3.2) or + applying any other policy — and **MUST** declare the resolved + language in `Match.lang`. - `session` — the session carrier from `context.session` of the utterance Message (OVOS-MSG-1 §4, OVOS-SESSION-1). @@ -202,7 +198,7 @@ fields below. |-------|------|----------|---------| | `skill_id` | string | yes | The `skill_id` of the handler to invoke. For a pipeline plugin that matches itself, this equals its `pipeline_id` (§7.0). | | `intent_name` | string | yes | An opaque non-empty string that, together with `skill_id`, names the handler to invoke. For skill-owned matches this is the intent name the skill registered. For plugin-owned matches this is whatever label the plugin chose for this response. | -| `lang` | string | no | The BCP-47 language tag the match was performed against. A plugin that determined the language itself (multilingual matcher, hard-coded engine) **MUST** set this. A plugin that matched in the language the entry-topic declared **MAY** omit it and rely on the orchestrator's fallback to the entry-topic `lang` (§7.1). If neither the plugin nor the entry-topic provides a language, the orchestrator treats the match as declined (§7.1). | +| `lang` | string | yes | The BCP-47 language tag the match was performed against. The plugin **MUST** set this — it is the plugin's explicit responsibility to declare what language its match is in. A plugin that received a `lang` parameter and matched in that language returns it here; a plugin that determined the language by other means (multilingual matcher, hard-coded engine, content-language detection) sets it to whatever language the match was performed in. | | `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | | `utterance` | string | yes | The specific candidate string from the input list that won the match. A plugin that does not track which candidate won **MUST** populate this with the first element of the input list as a fallback; the orchestrator forwards this value verbatim as `data.utterance` in the dispatch payload (§7.1) and **MUST NOT** substitute another value. | | `updated_session` | object | no | A replacement `session` snapshot the plugin produced during `match` (§4.2). When present, the orchestrator MUST use this snapshot — in place of the inbound utterance's session — for the dispatch and every downstream stage. When absent, the inbound session is carried unchanged. This is the **only** mechanism by which a plugin's match-phase session mutations reach downstream consumers; in-place mutations of the inbound session object are not visible past the plugin boundary. | @@ -771,7 +767,7 @@ The dispatch Message's `data`: | Field | Type | Required | Meaning | |-------|------|----------|---------| -| `lang` | string | yes | The content language of the match. The orchestrator **MUST** populate this from `Match.lang` when present, falling back to the entry-topic `Message.data.lang` (§9.1). A plugin that returns a `Match` without a `lang` and whose entry-topic carried no `lang` is a pipeline configuration error — the orchestrator **MUST NOT** dispatch; it **MUST** treat the match as if the plugin declined and continue iteration. | +| `lang` | string | yes | The content language of the match, taken directly from `Match.lang`. A `Match` with no `lang` is malformed; the orchestrator **MUST** treat it as if the plugin declined and continue iteration. | | `utterance` | string | yes | The candidate string that won the match. | | `slots` | object (string→string) | yes | The slot map (§4.3). MAY be empty. | From a48e07548f08fd674cbc641d52e16fbab5b69019 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 20:00:13 +0100 Subject: [PATCH 55/57] =?UTF-8?q?PIPELINE-1:=20drop=20=C2=A77.4=20In-proce?= =?UTF-8?q?ss=20equivalence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pipeline.md b/pipeline.md index 0a0556e..6405865 100644 --- a/pipeline.md +++ b/pipeline.md @@ -829,18 +829,6 @@ the reserved name is not registrable. The normal skill path same `:` dispatch shape; no dispatch mechanics change. -### 7.4 In-process equivalence - -When the handler (skill or plugin) runs in the -same process as the orchestrator, the orchestrator **MAY** invoke -the handler directly without serializing the dispatch Message -over a transport — provided every external observer sees the -same `:` dispatch and the same -handler-lifecycle trio (§8) it would have seen for an -out-of-process handler. This uniformity is what makes a -deployment portable across in-process and out-of-process handler -arrangements. - --- ## 8. Handler-lifecycle messages From 720a34567f73d55c720c1aa48a4e60f3a8475998 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 20:12:53 +0100 Subject: [PATCH 56/57] =?UTF-8?q?PIPELINE-1=20=C2=A76.5:=20long-running=20?= =?UTF-8?q?handlers=20and=20nested=20utterance=20lifecycles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the get_response nesting pattern: inner utterance lifecycle runs to completion (including its own .handled) while the outer handler is blocked. Mandates orchestrator concurrency (deadlock otherwise). Clarifies that the per-entry .handled invariant applies independently to each ovos.utterance.handle. Cross-references CONVERSE-1 §5 for the response routing mechanism. Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/pipeline.md b/pipeline.md index 6405865..ec2ec16 100644 --- a/pipeline.md +++ b/pipeline.md @@ -701,6 +701,61 @@ instead of `.complete`, the orchestrator still emits terminates with `ovos.utterance.handled`" invariant holds across all paths. +### 6.5 Long-running handlers and nested utterance lifecycles + +Handlers are long-running by design. A handler **MAY** block +for an unbounded duration — for example, to run a voice game, +a multi-step interaction, or any flow that asks the user one or +more follow-up questions. This is not a timeout condition and +**MUST NOT** be treated as one. + +When a handler asks the user a question and waits for the reply +(the `get_response` pattern defined by OVOS-CONVERSE-1 §5), the +following happens on the bus: + +``` +ovos.utterance.handle (original utterance) + ovos.intent.handler.start (outer handler) + ovos.utterance.speak (handler's question to the user) + [outer handler blocks] + ovos.utterance.handle (user's reply) + ovos.intent.matched + ovos.intent.handler.start (inner :response dispatch) + ovos.intent.handler.complete + ovos.utterance.handled (user's reply — inner lifecycle ends) + [outer handler unblocks, continues] + ovos.intent.handler.complete (outer handler) +ovos.utterance.handled (original utterance — outer lifecycle ends) +``` + +The inner utterance is a complete, independent lifecycle: +it enters on `ovos.utterance.handle`, is matched and dispatched +by the converse plugin on `:response` +(OVOS-CONVERSE-1 §5), and terminates with its own +`ovos.utterance.handled`. The outer lifecycle's +`ovos.utterance.handled` does not fire until the outer handler +returns, which may be after arbitrarily many inner lifecycles. + +**The "exactly one `ovos.utterance.handled` per +`ovos.utterance.handle`" invariant (§6.4) applies independently +to each entry message.** It says nothing about ordering between +concurrent or nested lifecycles; interleaved handler trios and +end-markers are conformant and expected. + +**The orchestrator MUST remain able to accept and process new +`ovos.utterance.handle` messages while a handler is running.** +An orchestrator that blocks the utterance-entry subscription for +the duration of a handler invocation will deadlock the first time +any handler calls `get_response`. Concurrent utterance processing +is a structural requirement, not an optimisation. + +The session is the correlation key for nested lifecycles: the +inner utterance carries the same `session_id` with +`session.response_mode` populated (OVOS-CONVERSE-1 §5), which +is what the converse plugin reads to route the reply to the +waiting handler. No additional correlation field is defined by +this specification. + --- ## 7. Dispatch From b7579a8b4af502e344a1f617846e337e4edb4326 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 26 May 2026 20:14:45 +0100 Subject: [PATCH 57/57] PIPELINE-1: pre-merge polish pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §2: fix sentence run-on - §4.2/§9.1/§9.6: remove double blank lines - §6.1: drop "(highlighted)" — not meaningful in markdown - §11 plugin MUST: add lang to the required Match fields - §11 orchestrator MUST: add §6.5 concurrency obligation - §10.1: fix confusing Match.skill_id parenthetical → pipeline_id Co-Authored-By: Claude Sonnet 4.6 --- pipeline.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pipeline.md b/pipeline.md index ec2ec16..2865fe8 100644 --- a/pipeline.md +++ b/pipeline.md @@ -88,9 +88,8 @@ It does **not** define: The **orchestrator** (OVOS-INTENT-3 §6.1) is the logical role that consumes the utterance-layer entry topic `ovos.utterance.handle` -(§9.1), iterates plugins per session, emits -dispatch and terminal events, and guarantees the universal -end-marker `ovos.utterance.handled`. +(§9.1), iterates plugins per session, emits dispatch and terminal events, +and guarantees the universal end-marker `ovos.utterance.handled`. The orchestrator is distinct from the **messagebus** (the transport layer) and from any individual plugin. @@ -270,7 +269,6 @@ session-mutation pathway** of OVOS-MSG-1 (which CONTEXT-1 §5.3 and CONVERSE-1 §3.2 build on) is governed by those specs and is unaffected by §4.2. - ### 4.3 The slot map `Match.slots` is a `{string: string}` mapping (the same shape @@ -628,7 +626,7 @@ specification's iteration loop. The **audio-transformer chain** the entry topic is emitted and is therefore not visible here. The **utterance** and **metadata** transformer chains run after entry and before iteration, against the candidate utterance list. The -**post-match-pre-dispatch window** (highlighted) is where +**post-match-pre-dispatch window** is where CONTEXT-1 §5.3 sanctions engine-side `session.intent_context` mutation and where TRANSFORM-1 §3.4 inserts the intent-transformer chain over the chosen `Match`. **`ovos.utterance.handled` is emitted at handler completion** — @@ -980,7 +978,6 @@ that wants to feed an utterance into the assistant — a listener, a chat bridge, a CLI, a test harness, a remote-peer client. Receiving on this topic kicks off the lifecycle of §6. - Payload shape: ```json @@ -1094,7 +1091,6 @@ A deployment with no audio output (a text-only chat bridge, a test harness) receives the same `ovos.utterance.speak` Message as an audio-capable deployment. - **Payload:** ```json @@ -1159,8 +1155,7 @@ loaded plugin emits one query per `pipeline_id` it cares about and aggregates the responses itself. The `pipeline_id` in the topic is the same identifier carried by -`session.pipeline` (§5) and by `Match.skill_id` when the plugin -owns its own handler (§7); a consumer that has already observed +`session.pipeline` (§5) and by `context["pipeline_id"]` on any observed dispatch (§3.1); a consumer that has already observed a `pipeline_id` from any of these sources can query it directly. ### 10.2 Response payload @@ -1262,14 +1257,16 @@ from the hosting process. - emit the handler-lifecycle trio (§8) wrapping every handler invocation: `start` before the call, then exactly one of `complete` (on normal return) or `error` (on exception or - timeout, §8.3) after. + timeout, §8.3) after; +- remain able to accept and process new `ovos.utterance.handle` + messages while a handler is running (§6.5). ### A **pipeline plugin** **MUST**: - expose a `match(utterances, lang, session) → Match | None` operation (§4); -- when claiming, return a `Match` with `skill_id` and - `intent_name` per §4 — never a partial or speculative claim; +- when claiming, return a `Match` with `skill_id`, `intent_name`, + and `lang` per §4 — never a partial or speculative claim; - bear a `pipeline_id` distinct from any other loaded plugin's id (§3); - **respond** to every `ovos.pipeline..intents.list`