From d8bd1d9398d84a25a597320ee54073b5138cc0aa Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 4 Aug 2026 14:06:28 +0100 Subject: [PATCH] =?UTF-8?q?spec:=20PERSONA-1=20=E2=80=94=20consistency=20a?= =?UTF-8?q?udit=20(claim=20rule,=20mandatory=20speak,=20converse,=20OOB,?= =?UTF-8?q?=20positioning)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit pass over persona.md. Twenty findings; nineteen addressed here, one deferred. Contradictions resolved: - Section 12 vs 7.1 route 3: the conformance MUST to return None when persona_id is absent contradicted route 3, which claims in exactly that case. The obligation is now scoped to invocation under the plugin's main pipeline_id, with a parallel MUST for invocation under a fallback_pipeline_id. Sections 3 and 4 restate the same split. - Section 7.2 vs route 1: "MUST NOT use intent-matching to decide whether to claim" contradicted route 1, where the embedded persona commands are ordinary intents by design. The prohibition is now scoped to route 2. - Four claim-rule formulations: section 7.2 is now the single normative statement; sections 3, 4, 7.1 and 12 cite it. Silent-turn and failure contracts: - Section 8.1 — a claimed dispatch MUST speak. A persona claim is exclusive and sits before fallback, so a handler that emitted nothing produced a silent turn with no error anywhere. Every dispatch from a claiming match now MUST emit at least one ovos.utterance.speak, including a degraded or error utterance on backend failure, refusal, or generation timeout. The timeout value is deployment-defined. Stop is exempt. - The handler-lifecycle trio stays orchestrator-owned (PIPELINE-1 section 8): a handler that swallows a failure and returns normally produces .complete, so a failing handler SHOULD speak and then propagate, letting the orchestrator emit ovos.intent.handler.error. - Section 8.5 — the out-of-band interface answers on every failure mode, not only on an unsupported persona_id. Adds a deployment-defined request/generation budget and a correlation rule: the topic is a broadcast, so a caller MUST tolerate N replies (typically one success and N-1 errors), correlating by persona_id and utterance. Corrected cross-references: - Section 8.3 cited "CONVERSE-1's active-handler check" — the wrong list. Rewritten against session.converse_handlers (CONVERSE-1 section 2.1), stamped at dispatch (CONVERSE-1 section 3.1), with the persona's pipeline_id entering it as an ordinary eligible owner via PIPELINE-1 section 7.0 self-matching. No persona-specific rule is involved. - Section 6 stop clear: stop-1.md on dev contains no persona_id; STOP-1 section 6.2 drains active_handlers, converse_handlers and response_mode only. The clear is restated as deployment guidance ("a deployment SHOULD configure its stop plugin to clear persona_id on global stop") and names STOP-1 section 6.2 as the normative home. No cross-PR dependency. Unenforceable obligations demoted: - Section 5 unknown-persona summon MUST — no registry and no orchestrator role exists to enforce it. Replaced by a note describing the actual behaviour: route-2 fall-through, a session that behaves like no-persona mode. - Section 9 collision WARN — nothing holds a deployment-wide view. Restated as provisioning-time tooling guidance over ovos.persona.list. Underspecified surfaces defined: - Section 9 register/deregister: payload {persona_id}; duplicate register is a no-op; a deregistered identity keeps serving live sessions until dismissed but accepts no new summons. No response topic is added (scope guard) — rejection is observable only via ovos.persona.list. - Section 8.5: whether an out-of-band exchange enters section 8.4 history is plugin-defined and MUST be documented by the plugin. - Section 11: activation/dismissal events scoped to plugin-observed transitions (self-summon, self-release); external summon/dismiss emit nothing. Advisory, best-effort, absence proves nothing. Pipeline positioning (section 10): - Common query added to the ordering (persona stages SHOULD follow it, per COMMON-QUERY-1 section 12), and the MAY-place-earlier licence narrowed to domain-specialised personas that are not general question-answering. - Persona stages are OPTIONAL. A deployment with no persona stage at all is fully conformant. The section 10 example ordering keeps fallback_low as the final stage, after persona and persona_fallback: ... common_query, persona, persona_fallback, fallback_low. A persona-fallback stage may shadow fallback_low in normal operation, but the always-answer guarantee is carried by OVOS-FALLBACK-1 section 8.1's catch-all skill in fallback_low, not by route 3 — fallback_low SHOULD remain the final stage so the deployment still answers when no persona stage is loaded, the persona backend fails, or the persona-fallback stage declines. This corrects an earlier audit pass on this branch that dropped fallback_low from the example and framed route 3 as replacing the catch-all; that inverted the design per owner directive. Editorial: - Builds-on list said "four" and listed four; it depends on eight (adds SESSION-2, STOP-1, INTENT-2, TRANSFORM-1). - Section 12 had two identical SHOULD headings; merged. - CHANGELOG restructured into entry 1 (initial draft, rewritten — the old text described a spec that does not exist: an ovos.persona.capabilities topic, a MAY-return-None match, and conformance roles that were removed) and entry 2 (the actual changes, including this fallback_low correction). - Section 11 keeps the answer / list.response naming asymmetry, now with a one-line note saying so deliberately. Deferred: - GLOSSARY entries (persona, summon, dismiss, no-persona mode, persona-fallback, persona_id) belong to the GLOSSARY PR, not this file. - fallback.md section 8.2's example already ends with fallback_low and is consistent with the ordering restored here; no changes needed there. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 61 +++++++-- persona.md | 352 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 336 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6377ce08..43b577da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,14 +47,59 @@ an entry here. clients dismiss by removing the field from the state they carry. - §3 — `persona_id` character-set constraint relaxed to a RECOMMENDED ASCII letters/digits/`_`/`-` convention. -- Initial draft. Defines persona as a scoped match+handler layer with - its own pipeline position, `persona_id` session field, summon and - dismiss bus messages, match contract (MAY return `None` for pass- - through), handler contract, no-persona mode, multiple-persona - coexistence rules, `fallback_pipeline_id` for escalation, dialog- - transformer compatibility, capability-discovery via - `ovos.persona.capabilities`, and conformance roles (Persona Plugin, - Orchestrator, Skill). +- Consistency audit. §7.2 becomes the single normative statement of + the claim rule; §3, §4 and §12 restate it by citation. The route-2 + return-`None` obligation is scoped to invocation under the main + `pipeline_id`, with a parallel obligation stated for + `fallback_pipeline_id` invocation (route 3), resolving the conflict + with route 3's claim; the §7.2 prohibition on intent-matching is + likewise scoped to route 2, which route 1's ordinary intents + otherwise contradicted. §8.1 — a claimed dispatch MUST produce at + least one `ovos.utterance.speak`, including a degraded or error + utterance on backend failure or generation timeout, since a persona + claim is exclusive and an empty dispatch is a silent turn; the + handler-lifecycle trio stays orchestrator-owned and a failing + handler SHOULD propagate so `ovos.intent.handler.error` is emitted. + §8.3 rewritten against the real converse mechanism: + `session.converse_handlers` (CONVERSE-1 §2.1) stamped at dispatch + (CONVERSE-1 §3.1), with the persona's `pipeline_id` entering it as + an ordinary eligible owner by PIPELINE-1 §7.0 self-matching. §8.5 — + the out-of-band interface MUST reply on every failure mode, its + time budget is deployment-defined, and callers MUST tolerate one + reply per loaded plugin, correlating by `persona_id`; whether an + out-of-band exchange enters §8.4 history is plugin-defined and MUST + be documented. §9 — register/deregister payload and semantics + defined (duplicate register is a no-op; a deregistered identity + keeps serving live sessions until dismissed but accepts no new + summons; no response topic, so rejection is observable only via + `ovos.persona.list`). §10 — common query added to the ordering + (persona stages SHOULD follow it, per COMMON-QUERY-1 §12), the + MAY-place-earlier licence narrowed to domain-specialised + non-general personas, and the ordering example restored to keep + `fallback_low` as the final stage, after `persona_fallback` — persona + stages are OPTIONAL, and the every-utterance guarantee stays with + OVOS-FALLBACK-1 §8.1's catch-all in `fallback_low`, not with route + 3, which may only shadow it in normal operation. §11 — activation + events scoped to plugin-observed + transitions only; external summon/dismiss emit nothing. §5 and §9 + demote unenforceable obligations (unknown-persona summon, collision + WARN) to notes, there being no registry or orchestrator role to + enforce them. §6 — the stop-plugin `persona_id` clear restated as + deployment guidance, naming STOP-1 §6.2 as the normative home. + Builds-on list corrected to eight specifications. + +### 1 + +- Initial draft. Defines a persona as a complete conversational agent + hosted by a pipeline plugin, the `persona_id` session field (§3), + no-persona mode (§4), summon and dismiss (§5, §6), the three-route + match contract (§7) — embedded persona commands, the + active-persona catch-all, and the optional persona-fallback stage — + the handler contract with conversation history, out-of-band query + (`ovos.persona.query` / `.answer`), persona discovery + (`ovos.persona.list`) and stop awareness (§8), multiple-persona + coexistence and runtime registration (§9), pipeline positioning + (§10), the bus surface (§11), and conformance (§12). ## OVOS-CONTEXT-1 — Intent Context ### 2 diff --git a/persona.md b/persona.md index e43ed066..4e4e413a 100644 --- a/persona.md +++ b/persona.md @@ -12,7 +12,7 @@ pipeline-positioning constraints that let the orchestrator enforce deterministic skills-first behaviour with personas acting as a fallback layer. -It builds on four companion specifications: +It builds on eight companion specifications: - the *Utterance Lifecycle and Pipeline Specification* (OVOS-PIPELINE-1) — the pipeline-plugin contract, the `Match` @@ -23,9 +23,20 @@ It builds on four companion specifications: defined here travels in; - the *Session Carrier Wire Shape Specification* (OVOS-SESSION-1) — the session field registry and the omission rule; +- the *Session Lifecycle and State Ownership Specification* + (OVOS-SESSION-2) — the merge semantics, the handler-boundary + mutation rules, and the SHOULD-project / MAY-internal state + pathways; - the *Active Handlers and Interactive Response Specification* (OVOS-CONVERSE-1) — the conversation cycle that routes follow-up - utterances to the persona plugin during multi-turn interactions. + utterances to the persona plugin during multi-turn interactions; +- the *Stop Pipeline Plugin Specification* (OVOS-STOP-1) — the stop + cascade a persona handler must obey during generation; +- the *Locale Resource Formats Specification* (OVOS-INTENT-2) — the + locale resource format the embedded persona commands are expressed + in; +- the *Transformer Plugins Specification* (OVOS-TRANSFORM-1) — the + dialog-transformer chain that shapes persona output. The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY**, and **RECOMMENDED** are used as in RFC 2119. @@ -134,10 +145,11 @@ within a single session is not in scope. **Semantics:** -- When `persona_id` is **absent** (not set), no persona is active. - Persona stages MUST return `None` for all utterances except - those matching an embedded persona command (§7.1 route 1) or - handled by a persona-fallback stage (§7.1 route 3). +- When `persona_id` is **absent** (not set), no persona is active. A + persona stage invoked under its main `pipeline_id` returns `None` + for every utterance that does not match an embedded persona command + (§7.1 route 1); a stage invoked under a `fallback_pipeline_id` + claims it instead (§7.1 route 3). §7.2 is the normative statement. - When `persona_id` is **present and non-empty**, the corresponding persona is active. Persona stages whose supported identities include this value MUST claim utterances that reach them (§7). @@ -161,8 +173,11 @@ Per OVOS-SESSION-1 §3.4, a producer that intends no active persona **No-persona mode** is the pipeline state in which no persona is active (`persona_id` is absent from the session). In this mode: -- persona stages **MUST** decline every utterance that does not - match an embedded persona command (§7.1 route 1); +- a persona stage invoked under its main `pipeline_id` **MUST** + decline every utterance that does not match an embedded persona + command (§7.1 route 1). A stage invoked under a + `fallback_pipeline_id` is exempt: it applies route 3 and claims + the utterance (§7.1 route 3, §9); - the pipeline operates as a purely deterministic, skill-driven system — only intent-matching and fallback stages handle utterances. @@ -208,10 +223,17 @@ effect of summon is to set `persona_id` in the session. - a **session sync** (`ovos.session.sync`) from any component; - the **orchestrator** as a policy decision. -**Unique identity:** A summon MUST reference an existing -`persona_id`. A summon that names an unknown persona has no effect: -the orchestrator or summoning component SHOULD log at WARN and leave -`persona_id` unchanged. +**Unknown identities.** No component validates `persona_id` against a +deployment-wide registry — there is none; identity discovery is +per-plugin (§8.7). A `persona_id` that no loaded plugin supports is +therefore not rejected: it is simply set on the session and every +persona stage declines it under route 2 (§7.1), so the utterance falls +through to the persona-fallback stage if one is configured, and +otherwise to the fallback stages. The observable effect of summoning an +unknown persona is a session that behaves like no-persona mode while +carrying a `persona_id`. A summoning component that wants stricter +behaviour SHOULD verify the identity against `ovos.persona.list` +(§8.7) before setting the field. --- @@ -227,10 +249,13 @@ session by: - the **persona plugin itself** — detecting a release intent during `match` (§7.1 route 1) and clearing `persona_id` via `Match.updated_session`; -- the **stop cascade** (OVOS-STOP-1) — clearing `persona_id` as - part of the escape-hatch behaviour. The stop plugin SHOULD clear - `persona_id` so that "stop" returns the session to - deterministic mode; +- the **stop cascade** (OVOS-STOP-1) — a deployment **SHOULD** + configure its stop plugin to clear `persona_id` on a global stop, + so that "stop" returns the session to deterministic mode. This is + deployment guidance, not an obligation this specification places on + a stop plugin: OVOS-STOP-1 §6.2 is the normative home for the + session fields a stop plugin drains, and it does not currently list + `persona_id`; - a **pipeline plugin** via handler-side session mutation; - a **session sync** (`ovos.session.sync`) from any component. @@ -314,18 +339,24 @@ order: ### 7.2 Active-persona catch-all -When route 2 above applies (no embedded persona command detected, -and `persona_id` is present and supported), the plugin **MUST** -claim every utterance that reaches it, subject only to its -supported-identity check. This is the defining behavioural -characteristic of a persona: an active persona consumes everything -that reaches its pipeline stage. +This is the normative claim rule for route 2; §4, §7.1 and §12 +restate it by citation and add nothing to it. + +When route 2 applies (no embedded persona command was detected, and +`session.persona_id` is present and supported by this plugin), the +plugin **MUST** claim every utterance that reaches it. This is the +defining behavioural characteristic of a persona: an active persona +consumes everything that reaches its pipeline stage. The plugin **MAY** apply lightweight gate logic before claiming -(language detection, minimum utterance length, blacklist), but it -**MUST NOT** use confidence thresholds or intent-matching to decide -whether to claim — those belong to the deterministic pipeline, not -to an active persona. +(language detection, minimum utterance length, blacklist). While +deciding a **route 2** claim it **MUST NOT** use confidence +thresholds or intent-matching — those belong to the deterministic +pipeline, not to an active persona. The prohibition is scoped to +route 2 only: route 1 is intent matching by construction (the +embedded persona commands are ordinary intents, §7.1), and route 3 +claims on the absence of an active persona rather than on any +score. ### 7.3 Latency discipline @@ -375,9 +406,42 @@ standard dispatch payload (PIPELINE-1 §7.1): `lang`, `utterance`, `slots`. The handler generates a natural-language response and emits it via `ovos.utterance.speak` (PIPELINE-1 §9.6). -A handler **MAY** emit zero, one, or multiple `ovos.utterance.speak` -Messages. Multiple emissions are conveyed in order and the output stage -**SHOULD** preserve that order. +A claimed utterance **MUST** produce speech. A persona claim is +exclusive — the stage consumes the utterance, and no later stage, +including fallback, ever sees it — so a dispatch that emits nothing +leaves the user with silence and no error. For every dispatch +resulting from a claiming match (§7.1 routes 1–3), the handler +**MUST** emit at least one `ovos.utterance.speak`. A handler **MAY** +emit more than one; multiple emissions are conveyed in order and the +output stage **SHOULD** preserve that order. + +The obligation holds on failure paths as well. When generation cannot +produce an ordinary answer — a backend error, an exhausted retry, a +refused request, or a generation timeout — the handler **MUST** emit +a degraded or error utterance that tells the user the request could +not be answered. + +The handler-lifecycle trio stays with the orchestrator: the handler +emits none of it (PIPELINE-1 §8), and the terminal event is +`ovos.intent.handler.error` only when the handler raises +(PIPELINE-1 §8.1). A persona handler that catches a generation +failure, speaks a degraded utterance, and returns normally therefore +produces `ovos.intent.handler.complete`, which records a successful +dispatch. So that a failed generation is observable on the bus and +not merely audible, a handler that could not produce the answer it +was asked for **SHOULD** speak the degraded utterance and then +propagate the failure, letting the orchestrator emit +`ovos.intent.handler.error` with the exception. Speaking a degraded +answer never substitutes for that signal, and the signal never +substitutes for speaking. + +The generation timeout itself is deployment-defined; this +specification fixes only what the handler owes the user when the +timeout expires. + +Stop is not a failure. A generation that ceases because a stop signal +arrived for its session (§8.6) is exempt from the speech obligation — +the user asked for silence. ### 8.2 Handler-side session mutation @@ -400,9 +464,23 @@ the audio output service reopens the microphone after speech. Multi-turn interactions are handled through multiple consecutive dispatches: the handler emits its prompt and returns; on the next -utterance, the converse plugin (OVOS-CONVERSE-1) routes -`:converse` to the persona because -`skill_id == pipeline_id` in CONVERSE-1's active-handler check. +utterance the converse plugin (OVOS-CONVERSE-1) polls the persona and, +if the persona claims the turn, dispatches `:converse` +to it. + +Eligibility for that poll comes from the dispatch itself. The +orchestrator stamps every dispatch target onto +`session.converse_handlers` (CONVERSE-1 §3.1) — the converse +plugin's eligibility list (CONVERSE-1 §2.1), which is distinct from +`session.active_handlers` (PIPELINE-1 §7.1). A persona plugin +self-matches, so `Match.skill_id` is its own `pipeline_id` +(PIPELINE-1 §7.0); the stamp therefore enters the persona's +`pipeline_id` into `converse_handlers` as an ordinary eligible owner, +with no persona-specific rule involved. Stamping is not suppressed +for the reserved `converse` intent_name (CONVERSE-1 §3.1), so a +persona that handles a converse turn stays eligible for the next +poll. Eligibility decays by the CONVERSE-1 §3.2 TTL prune and is +cleared by a global stop (STOP-1 §6.2). A persona plugin that supports multi-turn **SHOULD** subscribe to `:converse` to receive follow-up utterances. @@ -444,10 +522,38 @@ Session context for history continuity is read from The plugin generates a response for the specified `persona_id` using the `reply()` derivation (OVOS-MSG-1 §5) to route back to the caller. -If `persona_id` is not supported by this plugin, the plugin **MUST** -still reply on `ovos.persona.answer` — echoing `persona_id` and -`utterance`, omitting `response`, and setting the `error` field — -rather than silently drop the request. + +**Every request is answered.** A plugin that subscribes to +`ovos.persona.query` **MUST** reply on `ovos.persona.answer` for +every request it receives, on every outcome — a supported +`persona_id` answered normally, an unsupported `persona_id`, a +backend or generation failure, a refusal, a malformed payload, or a +generation that exceeded the plugin's time budget. A failure reply +echoes `persona_id` and `utterance`, omits `response`, and sets the +`error` field. Silently dropping a request is never conformant: the +caller cannot distinguish a dropped request from a slow one. + +**Request timeout.** The window a caller waits for an answer, and the +budget after which a plugin gives up generating and replies with +`error`, are both deployment-defined. This specification fixes only +that the plugin replies rather than falls silent; deployments +**SHOULD** document the budget they configure, and a plugin +**SHOULD** set its own generation budget below the caller-side +window so that its `error` reply arrives before the caller stops +waiting. + +**Correlation across plugins.** `ovos.persona.query` is a broadcast +topic — every loaded persona plugin receives every query, and each +one that subscribes replies. A caller in a deployment with N persona +plugins therefore **MUST** tolerate receiving multiple +`ovos.persona.answer` replies to a single query, typically one +success and N−1 `error` replies for the unsupported identity. The +correlation key is `persona_id`: a caller matches replies to its +request by the echoed `persona_id` and `utterance`, accepts the reply +carrying `response`, and treats the `error` replies from +non-supporting plugins as expected traffic rather than as failures. +A caller that receives only `error` replies learns that no loaded +plugin serves that identity. The response payload: @@ -469,8 +575,16 @@ state or triggering the full utterance lifecycle. A persona plugin that implemen defined in §7–§8.4. The out-of-band query **MUST NOT** mutate `session.persona_id` or -change the active persona state. It is a stateless query within -the context provided. +change the active persona state. + +Whether an out-of-band exchange is appended to the conversation +history of the session named in `context.session.session_id` (§8.4) +is **plugin-defined**. Both behaviours are conformant: a plugin that +appends gives the user continuity between an out-of-band lookup and +the next spoken turn, and a plugin that does not keeps out-of-band +lookups from polluting the spoken conversation. A plugin **MUST** +document which of the two it does, because a caller cannot observe +the difference from the reply. ### 8.6 Stop awareness @@ -542,9 +656,14 @@ among them by setting `session.persona_id`. **Identity namespace:** `persona_id` values SHOULD be unique within a deployment. When two plugins both claim the same `persona_id`, the first one in pipeline order claims every utterance for that identity; -the second never matches. Deployments SHOULD avoid this; if detected -at runtime (e.g. via `ovos.persona.list` responses), the orchestrator -SHOULD log at WARN. +the second never matches. Deployments SHOULD avoid this. + +A collision is not detected anywhere at runtime: no component holds a +deployment-wide view of loaded identities, and the orchestrator does +not read `ovos.persona.list` responses. A deployment that wants the +collision surfaced builds it into its own tooling — comparing the +`ovos.persona.list` responses of all loaded plugins (§8.7) at +provisioning time is the practical place to do it. **Capability-based routing.** A skill or UI that wants to select among multiple loaded personas SHOULD query `ovos.persona.list`, collect the @@ -592,10 +711,32 @@ The payload for `ovos.persona.deregister`: { "persona_id": "" } ``` -These topics are **MAY** — a deployment that does not need runtime -persona management can omit them. When present, the plugin validates -the `persona_id` namespace uniqueness rules above and rejects -duplicate or unknown registrations. +Both payloads carry exactly one field, `persona_id` (string, +required); the persona's configuration — system prompt, solver +wiring, model — is a deployment concern (§1) and reaches the plugin +by whatever means the deployment already uses, not over these topics. + +Semantics when a plugin implements them: + +- **Register.** The plugin adds `persona_id` to its supported set. + Registering an identity the plugin already supports is a **no-op**, + not an error: the request is idempotent. +- **Deregister of an inactive identity.** The plugin removes + `persona_id` from its supported set. Subsequent utterances naming + it are declined under route 2 (§7.1). +- **Deregister of an identity in use.** Sessions whose + `session.persona_id` equals the deregistered identity are **not** + interrupted. The plugin **MUST** keep serving those sessions until + each is dismissed (§6), and **MUST NOT** accept new summons of that + identity — it drops out of `ovos.persona.list` (§8.7) immediately, + and a session that starts carrying it after deregistration is + declined. Deregistration retires an identity; it does not cut off a + conversation in progress. + +There is no response topic for either request, and this specification +adds none. A rejected or ignored request is therefore observable only +indirectly: the caller queries `ovos.persona.list` (§8.7) and +compares the supported set against what it asked for. --- @@ -606,13 +747,26 @@ after deterministic intent-matching stages and after the stop stage, so that skills handle their intents first and the escape hatch can interrupt an active persona. +Persona stages **SHOULD** also be placed **after** a common query +stage, if the deployment has one. OVOS-COMMON-QUERY-1 §12 states the +same ordering from its side: deterministic question-answering by +skills is preferred over a persona's generated reply, and a persona +placed before common query would consume every question before the +contest ran. + +Persona stages are **OPTIONAL**. A deployment with no persona stage +at all — no `persona`, no `persona_fallback` — is fully conformant; +the pipeline in that case is purely the deterministic, skill-driven +system described in §4. + A persona plugin's main `pipeline_id` (active-persona catch-all, §7.1 route 2) SHOULD appear after skill stages. Its optional -`fallback_pipeline_id` (persona-fallback, §7.1 route 3) SHOULD -appear after all skill stages and at or near the end of the pipeline, -before any last-resort fallback. +`fallback_pipeline_id` (persona-fallback, §7.1 route 3), when +present, SHOULD appear after all skill stages and near the end of +the pipeline, but it MUST NOT be the final stage: it sits before +`fallback_low`, not in its place. -A typical ordering with both positions: +A typical ordering with both persona positions present: ``` session.pipeline: [ @@ -620,21 +774,44 @@ session.pipeline: [ "converse", # active-handler poll "skill_high", # deterministic registered intents "skill_medium", + "common_query", # deterministic question-answering "persona", # active-persona catch-all (route 2) "persona_fallback", # persona-fallback catch-all (route 3) - "fallback_low" # last-resort fallback + "fallback_low" # last-resort catch-all (OVOS-FALLBACK-1 §8.1) ] ``` `persona` and `persona_fallback` are different pipeline_id values registered by the same plugin. A deployment that does not use the persona-fallback feature simply omits `persona_fallback` from the -pipeline. - -A deployment **MAY** place a persona stage earlier when the persona -is specialised for a domain that should pre-empt general-purpose -matchers. Multiple persona stages at different pipeline positions -are conformant. +pipeline; a deployment that uses no persona stage at all omits both. + +**`fallback_low` remains the final stage.** A persona-fallback stage +claims every utterance that reaches it when no persona is active +(§7.1 route 3), so in normal operation — with a persona plugin +loaded and its fallback stage healthy — `persona_fallback` shadows +`fallback_low` in practice. That is a runtime consequence of +ordering, not a change of who is responsible for the guarantee. The +guarantee that every utterance receives a response is assigned by +OVOS-FALLBACK-1 §8.1 to a bottom-of-pool catch-all fallback skill in +`fallback_low`, and route 3 does **not** carry or replace that +guarantee. `fallback_low` SHOULD remain the final stage in every +deployment so the always-answer guarantee still holds when: no +persona stage is loaded, the persona backend or generation fails, +or a `persona_fallback` stage declines (returns `None`, §7.1 route +3, e.g. an unsupported `persona_id`). Fallback stages above the +persona-fallback position (`fallback_high`, `fallback_medium`) +remain reachable and useful regardless. + +A deployment **MAY** place a persona stage earlier than this, but +only where the persona is specialised for a narrow domain and is +**not** a general question-answering agent — a persona that +pre-empts general-purpose matchers must be one that declines +everything outside its domain in practice. A general-purpose persona +placed before the skill or common query stages consumes every +utterance and disables the deterministic pipeline entirely, which is +what §4 and this section exist to prevent. Multiple persona stages +at different pipeline positions are conformant. --- @@ -656,8 +833,23 @@ are conformant. `ovos.persona.activated` payload: `{ "persona_id": "...", "session_id": "..." }`. `ovos.persona.dismissed` payload: `{ "persona_id": "...", "session_id": "..." }`. -These are advisory signals emitted on a best-effort basis; consumers -**MUST NOT** rely on them for correctness. Session state is authoritative. + +**Emission scope.** A persona plugin **MAY** emit these on the +transitions it performs itself — a self-summon or self-release +matched under §7.1 route 1. It has no visibility into the others: an +external summon or dismiss (§5, §6) changes `persona_id` outside the +plugin, and the plugin learns of it only when the next utterance +arrives, if one ever does. Those transitions produce **no** event. +The signals are therefore partial by construction, advisory, and +best-effort; consumers **MUST NOT** rely on them for correctness or +treat their absence as evidence that no transition occurred. Session +state is authoritative — a consumer that needs every transition reads +`persona_id` from the session instead. + +The reply topics are named `ovos.persona.answer` and +`ovos.persona.list.response`. The asymmetry is deliberate: `answer` is +the persona's own vocabulary for what it produces, and renaming either +one now would break deployed subscribers for no behavioural gain. All dispatch topics follow the PIPELINE-1 §7 topic shape and fire the handler-lifecycle trio (PIPELINE-1 §8). The persona handler emits @@ -683,8 +875,13 @@ listing the intent names it dispatches on. `session.persona_id`, and handle each according to its type — set or clear `persona_id` for summon/release, leave it unchanged for one-off queries (§7.1 route 1); -- after the summon/release check, read `session.persona_id` and - return `None` when the field is absent or empty (§7.1); +- when invoked under its **main** `pipeline_id`, after the + summon/release check, read `session.persona_id` and return `None` + when the field is absent or empty (§7.1 route 2); +- when invoked under a registered `fallback_pipeline_id`, claim the + utterance when `session.persona_id` is absent or empty, and return + `None` when it names an identity this plugin does not support + (§7.1 route 3); - return `None` when `session.persona_id` is set to a value it does not support (§7.1); - claim every utterance that reaches it when `session.persona_id` is @@ -693,10 +890,16 @@ listing the intent names it dispatches on. - set `Match.lang` to the resolved language of the match; - subscribe to `:` to receive its own dispatch; +- emit at least one `ovos.utterance.speak` for every dispatch + resulting from a claiming match, including a degraded or error + utterance when generation fails or times out (§8.1); - derive each `ovos.utterance.speak` emission from the dispatch Message per OVOS-MSG-1 §5 derivation semantics (PIPELINE-1 §9.6); - cease generation and return promptly on stop signals for its session - (§8.6). + (§8.6); +- reply on `ovos.persona.answer` to every `ovos.persona.query` it + receives, on every outcome, if it implements the out-of-band + interface (§8.5). ### A persona pipeline plugin **SHOULD**: @@ -711,13 +914,15 @@ listing the intent names it dispatches on. - respond to `ovos.persona.list` with its supported `persona_id` values (§8.7); - project summary state into a session-resident field registered per - OVOS-SESSION-1 §2.2 for resumption safety (§8.4). - -### A persona pipeline plugin **SHOULD**: - + OVOS-SESSION-1 §2.2 for resumption safety (§8.4); - include `tags` per persona in its `ovos.persona.list` response so that routing skills and UIs can make informed summon decisions (§8.7, - §9). + §9); +- speak the degraded utterance and then propagate a generation + failure, so the orchestrator emits `ovos.intent.handler.error` + (§8.1); +- document whether an out-of-band query enters the session's + conversation history (§8.5). ### A persona pipeline plugin **MAY**: @@ -737,8 +942,15 @@ listing the intent names it dispatches on. - position persona stages after deterministic skills and after the stop stage in `session.pipeline` (§10); -- position the persona-fallback stage (`fallback_pipeline_id`) after - all skill stages and before last-resort fallback (§10); +- position persona stages after a common query stage, if one is + present (§10); +- position the persona-fallback stage (`fallback_pipeline_id`), when + present, after all skill stages and near the end of the pipeline, + but before `fallback_low`, which SHOULD remain the final stage so + the always-answer guarantee (OVOS-FALLBACK-1 §8.1) holds even when + no persona stage is loaded, the persona backend fails, or the + persona-fallback stage declines (§10); +- document the out-of-band query time budget it configures (§8.5); - ensure `persona_id` values do not overlap across loaded persona plugins (§9); - designate at most one persona-fallback stage in the active pipeline @@ -760,7 +972,9 @@ listing the intent names it dispatches on. (OVOS-SESSION-2) — the SHOULD-project / MAY-internal state pathways and the mutation boundaries. - *Stop Pipeline Plugin Specification* (OVOS-STOP-1) — the stop - cascade that clears `persona_id` on dismiss (§6). + cascade a persona handler obeys during generation (§8.6), and the + §6.2 drain rules a deployment may extend to clear `persona_id` + (§6). - *Active Handlers and Interactive Response Specification* (OVOS-CONVERSE-1) — the conversation cycle that routes follow-up utterances to the persona plugin via `:converse`