diff --git a/README.md b/README.md index bb40eb1..a63842a 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ below). Adoption is voluntary; conformance, once adopted, is not. | OVOS-TRANSFORM-1 | [Transformer Plugins](transformer.md) | 1 | [Draft — in review (PR #20)](https://github.com/OpenVoiceOS/architecture/pull/20) | | OVOS-CONTEXT-1 | [Intent Context](intent-context.md) | 1 | [Draft — in review (PR #18)](https://github.com/OpenVoiceOS/architecture/pull/18) | | OVOS-CONVERSE-1 | [Active Handlers and Interactive Response](converse.md) | 1 | [Draft — in review (PR #25)](https://github.com/OpenVoiceOS/architecture/pull/25) | +| OVOS-STOP-1 | [Stop Pipeline Plugin](ovos-stop-1.md) | 1 | [Draft — in review (PR #33)](https://github.com/OpenVoiceOS/architecture/pull/33) | Each spec carries its own scope statement, design rationale, and conformance section in its header. Open the document for the full @@ -119,8 +120,8 @@ picture — the tables above are an index. **Reading order by role:** - *Writing a skill?* INTENT-1 → INTENT-2 → INTENT-3. INTENT-4 only if you need the registration wire format. -- *Building a pipeline plugin?* PIPELINE-1, then SESSION-1 + SESSION-2, then the role spec (CONVERSE-1, CONTEXT-1, or TRANSFORM-1). -- *Building an orchestrator?* MSG-1 → SESSION-1 → SESSION-2 → PIPELINE-1, then INTENT-4, CONTEXT-1, CONVERSE-1, TRANSFORM-1. +- *Building a pipeline plugin?* PIPELINE-1, then SESSION-1 + SESSION-2, then the role spec (CONVERSE-1, CONTEXT-1, TRANSFORM-1, or STOP-1). +- *Building an orchestrator?* MSG-1 → SESSION-1 → SESSION-2 → PIPELINE-1, then INTENT-4, CONTEXT-1, CONVERSE-1, TRANSFORM-1, STOP-1. - *Surveying the architecture?* [appendix/overview.md §1](appendix/overview.md) for the three-stack narrative. For background — design rationale, comparisons with other systems, diff --git a/ovos-pipeline-1.md b/ovos-pipeline-1.md index 1a26c67..68e2ba4 100644 --- a/ovos-pipeline-1.md +++ b/ovos-pipeline-1.md @@ -807,6 +807,25 @@ The dispatch Message's `context` (OVOS-MSG-1 §4): `pipeline_id` of the plugin that produced the match (§3.1). When the match is self-addressed (`skill_id == pipeline_id`, §7.0), both context keys carry the same identifier. +- **`session.active_handlers` push.** The orchestrator **MUST** + push `{skill_id: , activated_at: }` onto `session.active_handlers`, + evicting any prior entry with the same `skill_id`. The list is + a recency record keyed by `activated_at` — consumers determine + "most recently activated" by comparing timestamps, not by list + position. The push is + **suppressed** only for dispatches on reserved intent_names + listed in §7.3 — a reserved-name dispatch represents a + continuation of an already-active skill's participation or its + termination, not a fresh activation. The orchestrator applies + the polymorphism rule (§7.0) uniformly and does not otherwise + distinguish skill from pipeline-plugin dispatches; suppression + is keyed strictly off the reserved-name registry. The push is + applied after `Match.updated_session` is committed: a plugin + that mutates `active_handlers` via `updated_session` (e.g., + STOP-1's global stop wiping the list) sees the stamp applied + on top, so the dispatched skill_id always lands at the head + unless the intent_name is reserved. The dispatch Message's `data`: @@ -859,9 +878,13 @@ pipeline plugin role. A reserved intent_name is one that: 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 +modification. Dispatches on reserved intent_names fire §7.1 +context stamping, §7.2 routing, and §8 handler-trio identically +to ordinary dispatches. The one exception is the +`session.active_handlers` push defined in §7.1, which is +suppressed on reserved-name dispatches — a reserved name +represents a continuation or termination of an already-active +skill's participation, not a fresh activation. The reserving specification gets exclusive use of the name across the deployment's skill set; it gets no other privilege. @@ -871,6 +894,7 @@ Reservations currently in force: |----------------------|----------------|--------------------------------------| | `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 | +| `stop` | OVOS-STOP-1 §4 | a stop plugin's claim that `` (an active handler) should cease activity — the orchestrator dispatches `:stop` and the owner's stop handler runs | This specification fixes only the registry mechanism (reservation listing); the per-name semantics are owned by the reserving diff --git a/ovos-session-1.md b/ovos-session-1.md index 32c9698..84ef948 100644 --- a/ovos-session-1.md +++ b/ovos-session-1.md @@ -192,7 +192,7 @@ session and persist across utterances. | `detected_lang` | string (BCP-47) | §3.2 (this spec) | | `pipeline` | array of string | OVOS-PIPELINE-1 §5 | | `intent_context` | object | OVOS-CONTEXT-1 §2 | -| `active_handlers` | array of object `{id, activated_at}` | OVOS-CONVERSE-1 §2.1 | +| `active_handlers` | array of object `{skill_id, activated_at, ...}` | OVOS-PIPELINE-1 §7.1 | | `response_mode` | object `{owner_id, expires_at}` | OVOS-CONVERSE-1 §2.2 | | `audio_transformers` | array of string | OVOS-TRANSFORM-1 §5 | | `utterance_transformers` | array of string | OVOS-TRANSFORM-1 §5 | diff --git a/ovos-stop-1.md b/ovos-stop-1.md new file mode 100644 index 0000000..8e9eaaf --- /dev/null +++ b/ovos-stop-1.md @@ -0,0 +1,358 @@ +# Stop Pipeline Plugin Specification + +**Spec ID:** OVOS-STOP-1 · **Version:** 1 · **Status:** Draft + +This specification defines the **stop pipeline plugin** — a pipeline +plugin that matches utterances expressing the user's intention to +interrupt the assistant's current activity — and the bus surface by +which it cascades a stop request across the recency-ordered list of +active handlers or broadcasts a global stop signal. + +The intent_name `stop` is reserved at OVOS-PIPELINE-1 §7.3. + +Dependencies: OVOS-MSG-1 (envelope and derivations), OVOS-PIPELINE-1 +(pipeline-plugin contract, dispatch shape, reserved-name registry, +`active_handlers` stamping), OVOS-SESSION-1 (session field registry), +OVOS-SESSION-2 (mutation boundaries), OVOS-CONVERSE-1 (`response_mode` +and `converse_handlers` field definitions). + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY** +are used as in RFC 2119. + +--- + +## 1. Scope + +This specification defines: the stop plugin role, the reserved +intent_name `stop`, the stoppability discovery and cascade algorithm, +the global broadcast, and the session-scoping obligations of stop +subscribers. + +It does **not** define: vocabulary file formats, matching algorithms, +confidence thresholds, audio capture control, handler-side framework +APIs, wake-word and barge-in policies, or post-stop in-flight +interaction teardown (a skill-side or orchestrator-side concern). + +--- + +## 2. Reserved intent_name + +| Reserved intent_name | Meaning | +|----------------------|---------| +| `stop` | Cease activity for the inbound `session_id`. Dispatched on `:stop` where the target is the most recently activated (highest `activated_at`) positive pong responder (§4). | + +Skills and other pipelines **MUST NOT** register `stop` under +OVOS-INTENT-4. A registration naming this intent_name is malformed per +OVOS-INTENT-4 §5.3 — consumers log at WARN and do not index. + +The intent_name `global_stop` is **not** reserved. The stop plugin +uses it for its own self-dispatch (`:global_stop`, §5), +namespaced under its own `pipeline_id`. + +--- + +## 3. The stop plugin role + +A stop plugin is an ordinary pipeline plugin (PIPELINE-1 §3) that +matches stop-command utterances and returns Matches under §2. It is +subject to the same denylist filtering, first-match-wins iteration, and +circuit-breaker rules as every other pipeline plugin. + +### 3.1 Pipeline identity and dispatch target + +A stop plugin MAY be loaded as multiple `pipeline_id` entries in +`session.pipeline` (e.g. separate confidence tiers). When multiple +entries are deployed, the implementation MUST ensure exactly one +`ovos.stop` broadcast is emitted per global stop event per session. + +`Match.skill_id` MUST equal: + +- for `intent_name: "stop"` — the most recently activated positive pong + responder selected per §4; +- for `intent_name: "global_stop"` — the `pipeline_id` whose handler + emits `ovos.stop`. + +A stop plugin MUST: + +- return `None` for any language for which it cannot resolve stop + vocabulary; +- read `session.active_handlers` to drive the §4 cascade; +- perform the ping-pong exchange (§4.2) inside `match`. + +### 3.2 Vocabulary + +A stop plugin SHOULD provide explicit "stop everything" vocabulary that +maps directly to `intent_name: "global_stop"` without running the §4 +cascade. Generic stop utterances run the cascade per §4. + +--- + +## 4. Generic stop — `intent_name: "stop"` + +### 4.1 Algorithm + +Inside `match`: + +1. Read `session.active_handlers`. If empty, return a `global_stop` + Match per §5. +2. Emit `ovos.stop.ping` and collect `ovos.stop.pong` responses within + a deployer-configured timeout (default: 0.5 s; SHOULD NOT exceed 1 s). +3. Identify positive responders: pongs where `can_handle: true` and + `skill_id` appears in `session.active_handlers`. Pongs from skills + not in `active_handlers` MUST be ignored; late pongs MAY be ignored. +4. If at least one positive responder exists, select the entry with the + highest `activated_at` in `session.active_handlers`. If two entries + share the same `activated_at`, select the entry appearing latest in + the list (most recently stamped). Construct + `updated_session` removing that `skill_id` from `active_handlers` + and clearing any `response_mode` entry it owns. Return + `Match(skill_id=, intent_name="stop", updated_session=...)`. +5. If no positive responder exists, return a `global_stop` Match per §5. + +### 4.2 Ping and pong shape + +**`ovos.stop.ping`** — broadcast. Payload MAY be empty. `session_id` +is carried in Message context per OVOS-MSG-1. + +**`ovos.stop.pong`** — shared reply topic. A handler MUST emit a +Message of type `ovos.stop.pong` derived via `reply` (OVOS-MSG-1 §5), +so that routing metadata is preserved and the pong reaches the stop +plugin regardless of where the skill is running (local or remote). +`source` and `destination` are layer-2 metadata and do not affect the +topic name. + +```json +{ "skill_id": "example.skill", "can_handle": true } +``` + +| Field | Type | Required | Meaning | +|-------|------|----------|---------| +| `skill_id` | string | yes | The `skill_id` of the responding handler. | +| `can_handle` | boolean | yes | Whether the handler has stoppable activity for the inbound `session_id`. | + +`can_handle: true` asserts that the handler has user-visible or +session-affecting activity in progress for the inbound `session_id` +**and** is prepared to cease it on receipt of `:stop`. + +A handler with no current activity MUST respond `can_handle: false` +or remain silent. A handler that does not subscribe to +`ovos.stop.ping`, or does not respond within the timeout, is treated +as `can_handle: false` and is ineligible as a stop target for that +ping round. If no handler declares stoppability, the cascade escalates +to `global_stop` per §4.1 step 5. + +### 4.3 Dispatch and stop handler obligations + +The orchestrator dispatches `:stop` per PIPELINE-1 §7, +firing the handler-lifecycle trio (`ovos.intent.handler.start`, +`.complete`, `.error`). + +The stop handler MUST: + +- cease the activity it declared stoppable, scoped to the inbound + `session_id`; +- not initiate a second stop sequence if a stop dispatch arrives while + already stopping — the duplicate MUST be treated as a no-op. + +The stop handler MUST NOT interrupt activity belonging to a different +`session_id`. + +`Match.updated_session` is committed before dispatch (PIPELINE-1 §4.2) +and is not rolled back if the stop handler emits the `.error` +lifecycle event. + +### 4.4 Self-pruning of `active_handlers` + +A handler that cannot be stopped SHOULD remove itself from +`session.active_handlers` by emitting any session-carrying Message +with the updated list, so that future ping rounds bypass it. + +--- + +## 5. Global stop — `intent_name: "global_stop"` + +### 5.1 Trigger conditions + +A `global_stop` Match is returned in three cases: + +- explicit "stop everything" vocabulary (§3.2); +- generic stop with empty `active_handlers` (§4.1 step 1); +- generic stop with no positive pong responders (§4.1 step 5). + +### 5.2 Match construction + +The `global_stop` Match MUST carry a fully-cleaned `updated_session`: + +``` +Match( + skill_id = , + intent_name = "global_stop", + updated_session = +) +``` + +All three fields are cleared atomically at match time via +`Match.updated_session` (PIPELINE-1 §4.2), before dispatch. + +PIPELINE-1 §7.1 stamps `` onto `active_handlers` +at dispatch time (the name `global_stop` is not reserved, so stamping +suppression does not apply). This is intentional: the stop plugin MAY +participate in converse after a global stop — for example, to handle a +follow-up clarification — provided it registers a converse handler. + +### 5.3 Broadcast + +The handler dispatched by `:global_stop` MUST emit +`ovos.stop`. Every component performing user-visible activity MUST +subscribe to `ovos.stop` and cease activity for the `session_id` +carried in Message context per OVOS-MSG-1. + +`ovos.stop` is not a dispatch topic — it does not follow the +`:` shape and does not fire the handler-lifecycle +trio. The namespace `ovos.stop.*` is reserved by this specification. + +--- + +## 6. Session interaction + +### 6.1 `response_mode` + +For `intent_name: "stop"`, a stop plugin MUST clear the +`session.response_mode` entry whose `owner_id` matches the dispatch +target, via `Match.updated_session`. If no such entry exists, the +field is left unchanged. For `intent_name: "global_stop"`, +`response_mode` is removed entirely as part of the §5.2 Match +construction. + +### 6.2 `active_handlers` + +`session.active_handlers` (OVOS-PIPELINE-1 §7.1) is the stop +cascade's recency input. It is distinct from `session.converse_handlers` +(OVOS-CONVERSE-1 §2.1), the converse plugin's eligibility list. + +A stop plugin MUST drain `active_handlers` via `Match.updated_session` +(committed pre-dispatch per PIPELINE-1 §4.2): + +- `stop` Match — remove the dispatch target entry only; +- `global_stop` Match — empty `active_handlers` entirely and empty + `converse_handlers` (OVOS-CONVERSE-1 §2.1) entirely. + +The stamping push (PIPELINE-1 §7.1) is suppressed for the reserved +intent_name `stop`, so the removal in `updated_session` is the final +state. It is not suppressed for `global_stop`. + +### 6.3 Denylists + +A stop plugin MUST honour `session.blacklisted_skills` and +`session.blacklisted_intents` (PIPELINE-1 §5): + +- `blacklisted_skills`: a handler whose `skill_id` appears in this list + MUST NOT be selected as a stop target; +- `blacklisted_intents`: applies to the dispatched intent_name (`"stop"` + or `"global_stop"`). A stop plugin MUST not resolve a intent_name + that appears in `blacklisted_intents`. A `stop` utterance that + would resolve to `global_stop` (§4.1 steps 1 or 5) is subject to the + `global_stop` entry, not the `stop` entry. This list does not affect + the ping broadcast. + +--- + +## 7. Pipeline positioning + +A deployment that includes the stop plugin SHOULD place the +highest-confidence stop stage **first** in `session.pipeline`, ahead +of the converse plugin and every intent-matching stage. Lower-confidence +stop stages MAY be interleaved with intent-matching stages. + +Typical ordering: + +``` +session.pipeline: [ + "stop_high", + "converse", + "intent_high", + "stop_medium", + "intent_medium", + "stop_low", + "intent_low" +] +``` + +--- + +## 8. Bus surface + +| Topic | Direction | Purpose | +|-------|-----------|---------| +| `ovos.stop.ping` | stop plugin → all | Stoppability query (broadcast) | +| `ovos.stop.pong` | skill → stop plugin | Stoppability response | +| `:stop` | orchestrator → skill | Skill-directed stop dispatch | +| `:global_stop` | orchestrator → stop handler | Global stop dispatch | +| `ovos.stop` | stop handler → all | Universal stop broadcast | + +Dispatch topics (`<…>:stop`, `<…>:global_stop`) fire the +handler-lifecycle trio. No other topic in this table does. + +--- + +## 9. Conformance + +### Stop pipeline plugin — MUST: + +- return `intent_name` of exactly `"stop"` or `"global_stop"` (§2, §3.1); +- set `Match.skill_id` per §3.1; +- return `None` when no stop vocabulary matches or `lang` is unsupported (§3.1); +- collect pong responses within a deployer-configured timeout (§4.1); +- ignore pongs from skills absent from `session.active_handlers` (§4.1); +- treat non-responding handlers as `can_handle: false` (§4.2); +- clear `session.response_mode` for the dispatch target via `Match.updated_session` (§6.1); +- drain `active_handlers` via `Match.updated_session` (§6.2); +- on `global_stop`, also empty `converse_handlers` via `Match.updated_session` (§6.2); +- return `global_stop` when `active_handlers` is empty or no positive pong responder exists (§4.1 steps 1, 5); +- honour `session.blacklisted_skills` and `session.blacklisted_intents` per §6.3; +- subscribe to `:global_stop` and emit `ovos.stop` (§5.3); +- emit exactly one `ovos.stop` broadcast per global stop event per session (§3.1). + +### Stop pipeline plugin — SHOULD: + +- configure the ping-pong timeout to not exceed 1 s (§4.1); +- provide explicit "stop everything" vocabulary mapping to `global_stop` without cascade (§3.2). + +### Deployment — SHOULD: + +- place the highest-confidence stop stage first in `session.pipeline` (§7); +- configure stop vocabulary for every supported language. + +### Skill — MUST: + +- subscribe to both `:stop` and `ovos.stop`; +- on `:stop`, cease stoppable activity for the inbound `session_id` (§4.3); +- on `ovos.stop`, cease all activity for the inbound `session_id`; +- treat a duplicate `:stop` or `ovos.stop` as a no-op (§4.3). + +### Skill — SHOULD: + +- subscribe to `ovos.stop.ping` and respond with a `reply`-derived + `ovos.stop.pong` carrying `can_handle` for the inbound `session_id` (§4.2); +- remove itself from `session.active_handlers` when it cannot be stopped (§4.4). + +### Non-skill component performing user-visible activity — MUST: + +- subscribe to `ovos.stop` and cease activity for the inbound `session_id`. + +### Orchestrator — MUST: + +- treat OVOS-INTENT-4 registrations naming `stop` as malformed — + log at WARN and decline to index (§2). + +--- + +## See also + +- OVOS-PIPELINE-1 — pipeline contract, dispatch, active_handlers +- OVOS-SESSION-1 — session field registry +- OVOS-SESSION-2 — mutation boundaries +- OVOS-MSG-1 — Message envelope and derivations