Skip to content

Latest commit

 

History

History
1200 lines (990 loc) · 54.3 KB

File metadata and controls

1200 lines (990 loc) · 54.3 KB

Intent and Entity Registration Bus Contract

Spec ID: OVOS-INTENT-4 · Version: 2 · Status: Draft

This document defines the bus messages a skill uses to declare its intents and entities. It is the wire format for intent registration — nothing else. Registrations are broadcast on the bus; pipeline plugins (OVOS-PIPELINE-1) consume what they want; the orchestrator passively indexes everything for introspection. The bus-level utterance lifecycle (matching, dispatch, handler lifecycle, terminal events) is owned by OVOS-PIPELINE-1.

It is the bus-level companion to OVOS-INTENT-3: where that specification defines what an intent is, this one defines how a skill puts that intent on the bus so a plugin can match against it.

It builds on three companion specifications:

  • the Bus Message Specification (OVOS-MSG-1) — the envelope every message defined here travels in, the destination routing key, the session carrier, and the forward / reply / response derivations;
  • the Intent Definition Specification (OVOS-INTENT-3) — the intent concept, identity model, the two definition methods, and the match result that this spec carries on the bus;
  • the Locale Resource Formats Specification (OVOS-INTENT-2) and the Sentence Template Grammar Specification (OVOS-INTENT-1) — the authoring file formats and template grammar a skill loader expands before emitting a registration payload (file paths never cross the bus; see §5.1).

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED and MAY are used as in RFC 2119.


1. Scope

This specification defines a small fixed set of registration topics and the orchestrator-provided introspection interface:

  • the two intent registration messages — one per definition method (OVOS-INTENT-3 §2);
  • the entity registration message — the .entity value-set hint (OVOS-INTENT-3 §5.2);
  • the deregistration messages, for one intent, one entity, or a whole skill;
  • the enable and disable messages — temporary suppression of a registered intent without losing its definition;
  • the introspection messages — list and describe registrations, served by the orchestrator's passive registration index (§10);
  • the session-scoped registration model (§11) — how every registration is automatically keyed to the registering session, enabling per-session skill sets and distributed satellite deployments.

It does not define:

  • the intent concept itself (OVOS-INTENT-3) or the handler reference, which never crosses the bus (§9);
  • how plugins implement registration storage, matching, or consumption — black box (OVOS-PIPELINE-1);
  • the utterance lifecycle, dispatch, handler-lifecycle trio, match-result notification, or ovos.intent.unmatched — all owned by OVOS-PIPELINE-1;
  • session lifecyclesession is carried opaquely per OVOS-MSG-1;
  • language fallback — what happens when no registration matches the utterance language exactly is out of scope for this specification.

2. Architectural model — registrations are broadcast

Registration messages defined here are broadcast on the bus. There is no central party that owns, validates, or routes them; whether any loaded pipeline plugin (OVOS-PIPELINE-1) consumes a given registration is a plugin concern, out of scope here. A registration no plugin consumes is silently dropped — the skill's intent will not match, and the deployment is responsible for diagnosing why (typically: wrong plugin loaded for the registration method).

The orchestrator (OVOS-INTENT-3 §6.1) maintains the manifest (§10): a passive index built from observed registrations, observability-only. In processing registrations it does not gate matching, influence consumption, or block re-registration. Other specifications MAY consult the manifest read-only during the utterance lifecycle (for example, OVOS-PIPELINE-1 §6.2's required_slots backstop); such consultation does not make the manifest a gate on registration processing. Plugins are observably pluggable — adding or removing one is a deployment concern; bus traffic and the manifest are unaffected.

Registrations are fire-and-forget: there is no .response reply, no acknowledgement, no error event. A producer that needs to verify a registration landed queries the manifest (§10); manifest presence is the only signal this specification defines.

Manifest presence is not matchability. The orchestrator indexes every non-reserved registration it observes, verbatim, without validating the payload — reserved intent_name values (§3.2) are the single exclusion. A structurally malformed registration therefore still appears in the manifest even though every consuming plugin rejects it under §5.3 / §6.3 / §7.2. A producer reading its own entry back learns that the broadcast reached the orchestrator, not that any plugin will ever match it. The plugin-side WARN log (§5.3) is the signal for that half.


3. Identity

3.1 Skills self-identify on every emission

A skill MUST set Message.context["skill_id"] to its own skill_id (OVOS-INTENT-3 §3) on every Message it originates or mutates before placing it on the bus. This covers fresh emissions (registration messages of §§5–8, ad-hoc skill-defined topics, etc.) and any Message whose context, data, or session the skill modifies before emission.

For a skill handler running under dispatch, conformance is structural: the orchestrator stamps context["skill_id"] on the dispatch Message (OVOS-PIPELINE-1 §7.1), and all Messages the handler derives from it via the OVOS-MSG-1 §5 derivation semantics inherit that value automatically. No extra stamp step is needed on the dispatch path.

Message.context["skill_id"] is the authoritative attribution key for skill-originated bus traffic — observers MUST NOT infer the originating skill from topic names or data fields. A Message arriving without context["skill_id"] is either not skill-originated or is from a non-conformant skill.

Enforcement

On the dispatch path enforcement is structural — the orchestrator stamps context["skill_id"] per OVOS-PIPELINE-1 §7.1 and MSG-1 derivation propagates it to handler-derived Messages. For emissions outside the dispatch path, the component that loads skills SHOULD intercept the emit pathway so non-conformant handler code cannot escape. A Message whose context["skill_id"] disagrees with the <skill_id> of the dispatch it derives from is malformed; the orchestrator MUST log the drift at WARN.

3.2 Identity carried by every registration message

Every registration message carries the identity of what is being registered, inside the Message's data (OVOS-MSG-1 §2.2). The identity fields restate OVOS-INTENT-3 §3 at the bus layer.

For an intent:

Field Type Required Source
skill_id string yes INTENT-3 §3 — assistant-unique.
intent_name string yes INTENT-3 §3 — unique within the skill.
lang string yes BCP-47, compared case-insensitively (INTENT-2 §2). The language of the resource being registered — distinct from session.lang.

The triple (skill_id, intent_name, lang) identifies an intent (INTENT-3 §3). For manifest indexing and replacement (§8.1), the registration key is the quintuple (session_id, skill_id, intent_name, lang, method)method being keyword (§5) or template (§6), and session_id read from the Message context, never from data. The session_id component is owned by §11.1, which also defines the "default" scope; the rest of this section reads as if session_id were fixed. Registering a quintuple that matches an existing entry replaces that entry only; the other-method registration for the same triple is untouched. Replacement is also per-language: other languages of the same (skill_id, intent_name) are unaffected.

Every message of §§5–8 acts on the payload skill_id. The payload skill_id names the target — the skill whose registration is created, removed, suppressed or re-armed. context.skill_id names the source that emitted the message (§3.1) and is provenance only. A consumer — plugin or orchestrator — MUST act on the payload value, MUST NOT substitute context.skill_id for it, and MUST NOT treat a difference between the two as grounds for rejection. A consumer SHOULD log source and target at DEBUG when they differ.

Source and target coincide whenever a skill registers its own intents, which is what §12 requires of a skill. They differ legitimately: an administrative script, a provisioning tool, or a conflict-resolving skill registers, retracts, suppresses or re-arms on another skill's behalf without being that skill. Enable and disable (§8.5) are the same shape as the rest, not an exception to it.

Because the payload carries the identity that acts, a message of §§5–8 is complete without context.skill_id, and a consumer MUST NOT treat its absence as malformed. A source that is not a skill has no skill_id of its own to declare; §3.1 binds skills, not every emitter.

Which sources may act on which targets is, deployment-wide, an unsolved trust problem — a hardening decision this specification cannot settle. Unguarded, ovos.skill.deregister (§8.4) is a remote uninstall. An orchestrator MAY therefore enforce a deployment policy that blocks cross-skill messages (source ≠ target) — dropping the message and logging the refusal at WARN with both identities and the topic. The policy's shape (allowlist, config flag, anything else) is deployment-defined and out of scope; absent one, cross-skill action is honoured as specified above.

The target session needs no field of its own: a control message affects the scope of the session_id its context carries, like every message of §§5–8 (§11.1). A controller managing another session's registrations declares that session on the message — the ordinary per-message session declaration of OVOS-SESSION-2, not a mechanism of this specification. What bounds an external participant to its own scope is the bridge (OVOS-BRIDGE-1), not this message shape.

A single intent MAY be registered under both methods — they are two training-data representations of the same handler. Different pipeline plugins consume different methods; a match from either dispatches to the same <skill_id>:<intent_name> topic. The wire contract makes no claim about which representation should "win" when both produce a match — that is a pipeline policy concern (OVOS-PIPELINE-1). Producers MAY ship divergent suppression vocabularies between the two methods (different excluded for keyword vs different blacklist for template); each plugin honours only its own method's suppression.

For an entity, intent_name is replaced by entity_name (same uniqueness rule: unique within the skill). Entity registrations have no method axis.

Other specifications MAY reserve specific intent_name values; the authoritative registry is OVOS-PIPELINE-1 §7.3. A registration naming a reserved intent_name is malformed — the orchestrator and every consuming plugin treat it under the malformed-payload rules of §5.3 / §6.3 (log at WARN, do not index).


4. Topics

Topics defined by this specification are lowercase, dot-separated, and namespaced under ovos.intent., ovos.entity., ovos.skill. and ovos.skills.. The singular ovos.skill. names a message about one skill — ovos.skill.loaded announces one, ovos.skill.deregister withdraws one — and the plural ovos.skills. names a query over the skills as a group, which is ovos.skills.list. All registration topics are broadcast — any component (typically pipeline plugins) may subscribe. The orchestrator also subscribes to all of them passively, to maintain the introspection index of §10.

Topic Direction Purpose §
ovos.intent.register.keyword skill → bus (broadcast) Register a keyword intent (INTENT-3 §4). §5
ovos.intent.register.template skill → bus (broadcast) Register a template intent (INTENT-3 §5). §6
ovos.intent.deregister skill → bus (broadcast) Remove one intent. §8
ovos.intent.enable skill → bus (broadcast) Re-arm a previously disabled intent. §8.5
ovos.intent.disable skill → bus (broadcast) Suppress an intent without removing its definition. §8.5
ovos.entity.register skill → bus (broadcast) Register an .entity value-set hint (INTENT-3 §5.2). §7
ovos.entity.deregister skill → bus (broadcast) Remove one entity. §8
ovos.skill.deregister skill → bus (broadcast) Remove all intents and entities for one skill_id. §8
ovos.skill.loaded skill → bus (broadcast) Announce that a skill is loaded and which capabilities it exposes. §8.6
ovos.intent.list observer → orchestrator Query registered intents (introspection; served by the orchestrator). §10
ovos.intent.describe observer → orchestrator Query one registered intent (introspection; served by the orchestrator). §10
ovos.skills.list observer → orchestrator Query loaded skills and their capabilities (introspection; served by the orchestrator). §10.3

Match notification, dispatch, and handler-lifecycle topics live in OVOS-PIPELINE-1 §§7–9, not here.


5. Keyword intent registration

Topic: ovos.intent.register.keyword.

A keyword intent is defined by keyword constraints over vocabularies (INTENT-3 §4). This message carries the constraints and the vocabularies in one payload.

5.1 Vocabulary descriptor

A vocabulary descriptor is a JSON object identifying one vocabulary (INTENT-3 §4.1):

{ "name": "set", "samples": ["set", "change", "adjust"] }

name is the vocabulary name (INTENT-3 §4.1) — this is the key under which the vocabulary's captured phrase appears in the match result (OVOS-PIPELINE-1 Match.slots; INTENT-3 §4.3). samples entries are slot-free OVOS-INTENT-1 templates (INTENT-1 §1.1) and MUST contain at least one entry.

Locale resource files (.voc, .intent, .entity, .blacklist; OVOS-INTENT-2) are a producer-side authoring convenience: a skill loader reads them and inlines their expanded content into the registration payload. File paths never appear on the wire.

5.2 Payload shape

{
  "skill_id": "lighting.skill",
  "intent_name": "set_brightness",
  "lang": "en-US",
  "required": [
    { "name": "set", "samples": ["set", "change", "adjust"] },
    { "name": "brightness", "samples": ["brightness", "light level"] }
  ],
  "optional": [],
  "one_of": [
    [
      { "name": "up",   "samples": ["up",   "higher", "brighter"] },
      { "name": "down", "samples": ["down", "lower",  "dimmer"]  }
    ]
  ],
  "excluded": [
    { "name": "question", "samples": ["what is", "how"] }
  ]
}

Field reference:

Field Type Required Meaning (per INTENT-3 §4.2)
required array of vocabulary descriptors no (absent = []) Every required vocabulary MUST occur in the utterance.
optional array of vocabulary descriptors no (absent = []) Captured if it occurs; absence does not prevent a match.
one_of array of arrays of vocabulary descriptors no (absent = []) Each inner array is one group; at least one member of each group MUST occur.
excluded array of vocabulary descriptors no (absent = []) If any of these occurs, the intent MUST NOT match.

Empty arrays are permitted, and an absent list-valued key is equivalent to an empty list — a consumer MUST NOT treat a payload as malformed merely because a list-valued key is omitted. Requiring empty keys on the wire would add nothing a consumer can rely on (the §5.3 validity rules operate on the resolved values either way) while turning every producer omission into a spurious rejection.

5.3 Constraint validity

The constraint rules of INTENT-3 §4.2 are restated here as bus-layer malformed-payload rules:

  • The intent_name MUST NOT be one reserved by another spec (§3.2; the authoritative registry is OVOS-PIPELINE-1 §7.3).
  • The combined required and one_of lists MUST NOT both be empty — an intent with only optional and excluded has nothing that must be present and is malformed (INTENT-3 §4.2).
  • A vocabulary MUST NOT appear under more than one role within a single registration (INTENT-3 §4.2). Vocabulary identity for this check is by name.
  • Two descriptors that share a name within the same role are not malformed: a consumer MUST merge them into one vocabulary whose samples is the union of theirs. The union is what the cross-role uniqueness check above sees, and what the match result reports under that name.
  • Every vocabulary descriptor MUST carry a non-empty samples array (§5.1).
  • A vocabulary descriptor's samples MUST include at least one template that expands to a non-empty sample (OVOS-INTENT-1 §3.6). A descriptor that yields zero non-empty samples is malformed.

An absent required, optional, one_of, or excluded key is read as an empty list (§5.2); the validity rules above apply to the resolved values.

Unknown payload fields are not malformed: a consumer MUST ignore fields it does not recognise and MUST NOT reject a registration because of them. This is what lets companion specifications (e.g. OVOS-CONTEXT-1's requires_context / excludes_context) ride on the registration payload as additional fields; the orchestrator's manifest preserves them (§10.2).

A consuming plugin MUST NOT index a registration that violates these rules. The rejecting plugin MUST log the rejection at WARN, including skill_id, intent_name, lang, the rejecting topic, and a one-line reason — this is the only debugging signal a producer receives, since the bus is fire-and-forget (§2). The topic is part of the actionable signal because the same (skill_id, intent_name, lang) may be valid as keyword and malformed as template (or vice versa, §3.2). Structured logging is RECOMMENDED.

Within a vocabulary descriptor, an individual sample that is not parsable as OVOS-INTENT-1 §3 grammar, or that expands to zero non-empty samples, does not malform the registration. A consuming plugin MUST NOT reject the registration on its account: it MUST skip the offending sample, MUST log each skipped sample at WARN with the fields above plus the sample itself, and MUST index the remaining valid samples. Only a descriptor in which no sample expands to a non-empty sample is malformed (the zero-yield rule above), and only then is the registration rejected.

5.4 No intent-suppression .blacklist

The intent-suppression .blacklist (INTENT-2 §4.3, INTENT-3 §5.5) is not used with keyword intents; it is a template-intent artifact and travels on the §6 payload's blacklist field. The excluded role (§5.2) is the keyword-intent suppression mechanism (INTENT-3 §4.2).

The slot-value-exclusion role of a .blacklist paired with an .entity (INTENT-2 §4.3) is a different role again; it travels on the §6 payload's slot_blacklist field.


6. Template intent registration

Topic: ovos.intent.register.template.

A template intent is defined by example sentence templates (INTENT-3 §5, INTENT-1 §3).

6.1 Payload shape

{
  "skill_id": "music.skill",
  "intent_name": "play_music",
  "lang": "en-US",
  "samples": [
    "(play|put on) {query}",
    "(play|put on) {query} (on|using) {engine}",
    "i want to listen to {query}",
    "play {query} for {duration:length}"
  ],
  "blacklist": ["trailer", "music video"],
  "required_slots": ["query"],
  "slot_blacklist": {"query": ["it", "that"]},
  "slot_types": {"length": "duration"}
}

Field reference:

Field Type Required Meaning
samples array of strings yes OVOS-INTENT-1 templates with named slots (INTENT-1 §3, §5).
blacklist array of strings no Slot-free phrases (INTENT-2 §4.3) whose occurrence suppresses the match (INTENT-3 §5.5).
required_slots array of strings no Slot names the engine MUST extract for a match to be valid (INTENT-3 §5.3).
slot_types object, slot name → type name no The typed-slot declarations of the templates in samples, derived from their {type:name} placeholders (OVOS-INTENT-1 §3.4, §5.6).
slot_blacklist object, slot name → array of strings no Per-slot exclusion sets. Membership and the resulting bind-or-leave-unresolved behavior are defined by INTENT-2 §4.3; this field carries that exclusion set for the named slot.

As in §5.2, an absent list-valued key (blacklist, required_slots) is equivalent to an empty list; an absent slot_blacklist is equivalent to an empty object — no exclusions; samples is the one list a producer must supply, and it must be non-empty (§6.3).

Every slot name in this payload is the bare name — the name without any type prefix. required_slots lists bare names, slot_blacklist and slot_types are keyed by bare names, and a template's {duration:length} placeholder declares the slot length. slot_types is redundant with the samples it was derived from, and carrying it saves a consumer from parsing the templates to learn what a slot expects. A consumer that does not implement typed slots MUST ignore it and read the templates by the degrade rule of OVOS-INTENT-1 §3.4; this is the §6.3 unknown-field tolerance applied to a named field. An absent slot_types is equivalent to an empty object.

A producer derives required_slots from the intent's paired .required resource (INTENT-2 §4.5). Every name it carries MUST be a slot declared by a template in samples. A name no template declares malforms the registration (§6.3).

6.2 Slot sets

Templates in samples MAY declare different sets of named slots; the engine extracts only the slots declared by the template that best matches (INTENT-1 §5.5, INTENT-3 §5.1). A consuming plugin MUST accept registrations with differing slot sets across templates.

6.3 Malformed payloads

A consuming plugin MUST NOT index a template registration in which:

  • the intent_name is reserved by another spec (§3.2);
  • samples is missing or empty;
  • no template in samples is both parsable as OVOS-INTENT-1 §3 grammar and expands to at least one non-empty sample (OVOS-INTENT-1 §3.6);
  • required_slots names a slot that is not declared by any valid template in samples (INTENT-3 §5.3).

An individual template that is not parsable as OVOS-INTENT-1 §3 grammar, or that expands to zero non-empty samples, does not malform the registration by itself. A consuming plugin MUST NOT reject the registration on its account: it MUST skip that template, MUST log each skipped template at WARN with the §5.3 fields plus the template itself, and MUST index the remaining valid templates. The registration is rejected only when no valid template remains (third bullet above).

The §5.3 WARN-log rule applies: the rejecting plugin MUST log the rejection with skill_id, intent_name, lang, and a one-line reason. The §5.3 unknown-field rule also applies: unknown payload fields MUST be ignored, not treated as malformed.


7. Entity registration

Topic: ovos.entity.register.

An entity is an optional value-set hint for a template-intent slot (INTENT-3 §5.2, INTENT-1 §5.4, INTENT-2 §4.3). Registering an entity is never a precondition for an intent that references the slot name; a slot with no entity still fills normally.

7.1 Payload shape

{
  "skill_id": "music.skill",
  "entity_name": "engine",
  "lang": "en-US",
  "samples": ["spotify", "youtube music", "the radio"]
}

Field reference:

Field Type Required Meaning
entity_name string yes Unique within the skill. By convention matches the slot name a template intent references.
samples array of strings yes Slot-free value-set entries (INTENT-1 §5.4).

7.2 Malformed payloads

A consuming plugin MUST NOT index an entity registration whose samples is missing or empty, or in which no entry yields a non-empty value. The §5.3 WARN-log rule applies: the rejecting plugin MUST log the rejection with skill_id, entity_name, lang, and a one-line reason.

An individual entry that is not parsable as OVOS-INTENT-1 §3 grammar, or that yields no non-empty value, does not malform the registration by itself. A consuming plugin MUST NOT reject the registration on its account: it MUST skip the offending entry, MUST log each skipped entry at WARN with the §5.3 fields plus the entry itself, and MUST index the remaining valid entries.


8. Deregistration, enable, disable, and replacement

8.1 Replacement is implicit

Registering an intent whose (session_id, skill_id, intent_name, lang, method) quintuple matches an existing registration replaces it (INTENT-3 §6.1) — no prior deregister needed. Replacement preserves enabled/disabled state (§8.5); a producer that wants to reset that state deregisters first. The same rule applies to entities, keyed on the quadruple (session_id, skill_id, entity_name, lang) — entities have no method axis. The session_id is read from context.session.session_id (§11.1) — never from Message.data.

8.2 ovos.intent.deregister

Removes one intent. Payload:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US" }

If lang is omitted, every language registered for that (skill_id, intent_name) pair is removed. Deregistration targets the (skill_id, intent_name, lang) triple and removes all methods under it — both the keyword and template registrations of the same intent (§3.2), if both exist. There is no per-method deregistration; a skill that wants to remove only one method re-registers the other.

8.3 ovos.entity.deregister

Removes one entity. Payload:

{ "skill_id": "music.skill", "entity_name": "engine", "lang": "en-US" }

If lang is omitted, every language registered for that (skill_id, entity_name) pair is removed.

8.4 ovos.skill.deregister

Removes everything owned by a skill — every intent and every entity registered under that skill_id. Payload:

{ "skill_id": "music.skill" }

The removal is scoped to the session_id read from context.session.session_id of the Message (§11.1) — never from Message.data. A deregistration arriving under the default session removes the "default"-scoped registrations; one arriving under a satellite's session removes only that session's registrations (§11.3).

This message is broadcast, like every other registration message (§2) — it is not addressed to the orchestrator. It is emitted when a skill is unloaded (INTENT-3 §6.1), by the skill itself or by whatever component unloads it, and every subscriber that holds registrations for that (session_id, skill_id) pair drops them independently. A bridge SHOULD emit ovos.skill.deregister carrying the satellite's session in context for every skill the satellite registered when the satellite disconnects (OVOS-BRIDGE-1 §3).

Deregistering an intent, entity, or skill that is not currently registered is a no-op: registrations are fire-and-forget, every plugin processes the message independently, and any plugin without a matching record simply has nothing to remove. This makes the shutdown sequence — where every plugin the skill ever talked to receives every deregistration — naturally idempotent.

Races between a deregistration and an in-flight match (a match emitted before the deregister was processed, dispatched after) are the responsibility of the utterance lifecycle owner — see OVOS-PIPELINE-1.

8.5 ovos.intent.enable and ovos.intent.disable

A registered intent is, by default, enabled — eligible for matching. A skill MAY temporarily disable an intent without removing it; the orchestrator retains the definition in the manifest but marks it disabled, and plugins exclude it from match candidacy until it is re-enabled. The bus-level surface (rather than skill-side gating) lets external tooling — admin UIs, A/B experiments, conflict resolution — suppress an intent without modifying skill code. Both topics share the same payload as ovos.intent.deregister (§8.2), and lang semantics:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US" }

Here skill_id is the target of the operation, not the sender, as in every message of §§5–8 (§3.2): context.skill_id — the source — MAY name a different skill. The target session is the context session, as for every message here (§3.2, §11.3): a controller reaches another session's scope by declaring that session on the message, not through any payload field.

If lang is omitted, every language for that (skill_id, intent_name) is affected. Like deregistration, enable/disable target the triple and apply to all methods of the intent — there is no per-method enable/disable. A producer that wants to retain only one method deregisters the triple (§8.2, removes both methods) and re-registers just the desired one.

Enabling an already-enabled intent, or disabling an already-disabled intent, is a no-op. Enabling or disabling an intent that is not currently registered is likewise a no-op — like deregistration (§8.4), each consumer processes the message independently and one without a matching record has nothing to change. Re-registration (§8.1) preserves enabled/disabled state unless the producer deregisters first. Whether a consuming plugin's disabled-state record survives a plugin reload is out of scope: a reloaded plugin that needs the current enabled/disabled state recovers it by querying the manifest (§10.1), whose enabled field reflects the latest state.

Enabled/disabled state does not survive an orchestrator restart. The manifest is rebuilt from observed broadcasts (§2, §10), and ovos.intent.disable is not re-emitted by the re-registering skill — so a restarted orchestrator indexes every re-emitted registration in its default enabled state. The party that disabled an intent is responsible for re-asserting the suppression: it SHOULD re-emit ovos.intent.disable after it observes the deployment's readiness announcement (§10), for every intent it currently holds disabled. A disabling party that does not track its own suppressions cannot recover them; the manifest is not durable state.

Enable and disable are scoped to the session the message carries (§11.1): the effect of ovos.intent.disable and ovos.intent.enable is bound to the session_id read from context.session.session_id, and a consuming plugin MUST NOT alter matching for any session other than those the change reaches through §11.2. Which sessions it reaches follows the scope of the registration it targets. A disable under the "default" session acts on the "default" registration, which every session inherits (§11.2), so it is device-wide: every session that matches through the inherited entry stops matching it. A disable under a specific session_id acts on that session's view only — it suppresses the intent for that session, whether the entry it targets is the session's own or the inherited "default" one, and no other session is affected. Enabling an intent re-arms exactly what a prior disable on the same session suppressed.


8.6 ovos.skill.loaded

A skill announces itself once it is able to receive dispatches:

{ "skill_id": "music.skill", "capabilities": ["converse", "fallback"] }
Field Type Required Meaning
skill_id string yes The skill (INTENT-3 §3).
capabilities array of string no The roles the skill takes part in beyond intent dispatch (below). Absent or empty means intents only.

The announcement is broadcast and session-keyed like every message here (§11.1): the session_id read from context.session.session_id is the scope the skill is loaded under. Re-announcement replaces the prior entry for the same (session_id, skill_id), so a skill SHOULD re-emit it with its registrations on the readiness announcement (§10). ovos.skill.deregister (§8.4) is the matching unload: it removes the announcement together with the skill's registrations, so a skill is loaded in a scope while an announcement stands there that no deregistration has withdrawn.

A capability names a role another specification defines for a skill; declaring it says the skill answers on that role's bus surface. This specification registers:

Capability Meaning Defined by
fallback Registers as a fallback handler and answers the willingness poll. OVOS-FALLBACK-1 §3.1, §6.1
common_query Answers the wants-to-answer poll and the full-answer request. OVOS-COMMON-QUERY-1 §11
converse Accepts converse dispatch while an active handler. OVOS-CONVERSE-1 §4

Other specifications MAY register further names by adding rows here. A consumer MUST ignore a name it does not know. A capability is a declaration, not a registration: fallback says a FALLBACK-1 registration is coming, it does not replace one.

The announcement replaces the pre-spec load notice whose payload carried a filesystem path; a listing derived from it reports what a skill can do, never where it lives.

9. The handler reference is not on the bus

Per INTENT-3 §6.1, the handler reference — the code object that runs when the intent matches — never crosses the bus; it is held locally by the skill process. This specification puts only the intent definition (§§5–7) on the wire; the dispatch Message that invokes the handler is defined in OVOS-PIPELINE-1. Together they let a skill in a different process from the orchestrator host its handlers across the bus without serializing them — the contract that makes local and remote skills indistinguishable from outside.


10. Introspection — the orchestrator-owned manifest

Registration broadcasts of §5–§8 are load-time announcements; a consumer that subscribed after the skill loaded has missed them (the bus is async with no catch-up channel). The orchestrator-owned manifest is this specification's answer — the orchestrator indexes every registration it observes and serves queries against it. Skills have no introspection obligation; they emit and move on.

Pull-query is the source of truth. A consumer that needs accurate state MUST issue ovos.intent.list / ovos.intent.describe and MUST NOT rely on having heard the original broadcast. For compiled-plugin state — which intents a particular matcher actually has loaded — query OVOS-PIPELINE-1 §10 instead; the surfaces are distinct (declared vs compiled).

Under a split orchestrator (OVOS-PIPELINE-1 §2), each process answers from its own slice; consumers aggregate.

Cold-start recovery. The asymmetry cuts both ways: a skill that registered before the orchestrator (or a consuming plugin) started has emitted into the void, and because registrations are fire-and-forget (§2) nothing tells it so — the manifest stays permanently empty for that skill. A skill therefore SHOULD re-emit its full registration set when it observes the deployment's readiness announcement — the broadcast by which the orchestrator signals it is up and consuming (the topic is deployment-defined and not owned by this specification). Re-emission is safe by construction: replacement is implicit (§8.1), so a duplicate registration is idempotent.

Re-emission MUST carry the same session context as the original registration. The registration key includes session_id (§11.1), so a re-emission under a different session does not replace the original entry — it creates a second one, under a scope whose owner never registered it and cannot deregister it (§11.3). Idempotence holds only within one session scope.

For a satellite's session-scoped registrations this makes the bridge, not the satellite alone, part of the recovery path: relaying the readiness announcement to the satellite and re-establishing the satellite's registrations is defined in OVOS-BRIDGE-1 §4.4.

Three read-only topics:

10.1 ovos.intent.list

Lists registered intents. Request payload:

{ "skill_id": "music.skill", "lang": "en-US", "session_id": "satellite-abc" }

All fields are optional filters: omitting skill_id returns every skill's intents; omitting lang returns every language; omitting session_id returns intents from all sessions (global view). When session_id is provided the response returns the effective pool for that session: "default" intents plus session-specific intents (§11.2), not the raw index for that session alone. An intent registered under both methods (§3.2) appears as two entries distinguished by method.

Response (ovos.intent.list.response):

{
  "ok": true,
  "intents": [
    {
      "skill_id": "music.skill",
      "intent_name": "play_music",
      "lang": "en-US",
      "method": "template",
      "enabled": true,
      "session_id": "default"
    }
  ]
}

Each entry carries skill_id, intent_name, lang, a method of "keyword" or "template" (INTENT-3 §2), an enabled boolean (§8.5), and the session_id under which the intent was registered. Reserved-name registrations are malformed (§3.2) and do not appear in the manifest.

10.2 ovos.intent.describe

Returns stored registration payloads. Request payload:

{ "skill_id": "music.skill", "intent_name": "play_music", "lang": "en-US", "method": "template", "session_id": "satellite-abc" }

skill_id is required: it is what bounds the reply, so a describe query can never return more than one skill's registrations. A request without skill_id is an error.

intent_name, lang, method, and session_id are optional filters. Omitting intent_name returns every intent of the skill; omitting lang returns every language, each folded per the case-insensitive BCP-47 comparison of §3.2 (INTENT-2 §2); omitting method returns every registered method; omitting session_id returns definitions from every session: the same (skill_id, intent_name, lang, method) may exist under "default" and under one or more sessions (§11.2), and each is a separate entry.

A single query naming skill_id alone therefore returns every registration of that skill, across every intent, language, method, and session:

{ "skill_id": "music.skill" }

This is the shape a client uses to render "what can I ask this device" in one round trip per skill, instead of one request per intent per language.

Response (ovos.intent.describe.response):

  • On success, { "ok": true, "definitions": [ { "skill_id": "...", "intent_name": "...", "lang": "...", "method": "...", "session_id": "...", "definition": {...} }, ... ] } where each definition is the §5 or §6 payload as it was broadcast. Each entry is self-identifying via its skill_id, intent_name, lang, method, and session_id fields; consumers MUST key on those, not on array position. The array carries one entry per registered (session_id, intent_name, lang, method) combination that passes the filters. When more than one entry is returned, ordering them by session_id with "default" first, then by intent_name, then by lang, then by method in the order keyword, template, is RECOMMENDED for stable output. A query that names one intent_name and one lang has a single value at each of those positions, so its entries order by session_id and then method.
  • On an unknown skill, or on filters that match nothing, { "ok": false, "error": "..." }.

The reply size is bounded by one skill's registrations, which is why skill_id stays required and why ovos.intent.list (§10.1) stays a listing without definitions: a client walks the skills from §10.1 and asks §10.2 once per skill.

10.3 ovos.skills.list

Lists loaded skills and their capabilities. Request payload:

{ "session_id": "satellite-abc" }

session_id is an optional filter. When present the response carries the skills loaded in the effective scope of that session: those announced under "default" plus those announced under that session_id (§11.2). When absent the response carries every announced skill under every session. The filter names the session asked about; the request's own context.session does not scope the answer, so a controller can ask about a scope it does not hold.

Response (ovos.skills.list.response), via the response derivation (OVOS-MSG-1 §5.3):

{
  "ok": true,
  "skills": [
    {
      "skill_id": "music.skill",
      "session_id": "default",
      "capabilities": ["converse"],
      "intents": 12
    }
  ]
}

Each entry carries the skill_id, the session_id it was announced under (§8.6), the capabilities it declared, and intents, the number of registrations the manifest holds for it in that scope (§10.1), which is 0 for a skill that announced and registered nothing. An empty deployment answers "skills": [], never silence. Ordering by session_id with "default" first, then by skill_id, is RECOMMENDED.

This listing is the one place the bus reports which skills are loaded. A fallback handler has no identifier of its own the way an intent has (skill_id, intent_name), and a common-query or converse participant registers nothing at all, so what can be reported truthfully is the skill and the roles it declared. A client that wants the registered intents of a skill it found here walks §10.1 and §10.2; the fallback registry itself (priorities, stage ranges) belongs to the fallback pipeline plugin and is not served here.

The orchestrator MAY restrict access to introspection topics; authorization is out of scope.


11. Session-scoped registration

11.1 Every registration is session-keyed

The orchestrator keys every registration by the session_id it reads from context.session.session_id — the context field of the bus Message envelope, never from Message.data. This is a strict requirement: session_id in data would allow a producer to register intents under an arbitrary session it does not own. Reading from context means the session_id is set by the session the producer is running under, not by anything the producer chooses to assert in its payload.

No change to the registration message shape is required: session is an ordinary context field of the OVOS-MSG-1 envelope. A Message whose context carries no session, or a session with no session_id, is not malformed — OVOS-MSG-1 §4 only SHOULDs the session onto a Message. Such a registration is keyed under "default", exactly as if the producer had run under the default session. This is the same treatment the local device gets, and it keeps a minimal producer (a shell script, a test harness) able to register without constructing a session.

The full registration key becomes the quintuple (session_id, skill_id, intent_name, lang, method); the prior quadruple (skill_id, intent_name, lang, method) is the special case where session_id == "default".

Skills running on the local device register under "default" because the local device uses the default session (OVOS-SESSION-2 §5). Skills running on a remote satellite register under whatever session_id the satellite's session carries. No new message, no new field, no coordination protocol.

11.2 Inheritance — "default" is the global scope

The effective intent pool for a session X is:

pool(X) = { intents registered under "default" }
        ∪ { intents registered under session_id == X }
        − { entries excluded by session X's blacklisted_skills
            and blacklisted_intents }

Every session implicitly inherits the full "default" set. Session-scoped registrations extend the pool — they never narrow it. Narrowing is the job of the blacklisted_skills and blacklisted_intents session fields (OVOS-PIPELINE-1 §5, OVOS-SESSION-1 §3). The blacklisted_pipelines field is not part of this formula: it removes pipeline plugins from the session's pipeline, not entries from the intent pool.

If the same (skill_id, intent_name, lang, method) exists in both "default" and session X, both index entries are retained and both appear in the matching pool. The existing first-match-wins iteration (OVOS-PIPELINE-1 §6) determines which is used; the blacklist is the explicit suppression mechanism if the satellite wants to shadow a default intent.

11.3 Deregistration and session teardown

ovos.intent.deregister removes every entry matching (session_id, skill_id, intent_name, lang) across both methods — the key minus its method component, since there is no per-method deregistration (§8.2). ovos.entity.deregister removes the entry matching (session_id, skill_id, entity_name, lang); entities have no method axis. When lang is omitted, lang drops out of the match too and every language under that session and skill is removed (§8.2, §8.3).

As for every message in this specification, the session_id is read from context.session.session_id (§11.1) — never from Message.data. A deregistration arriving under the default session removes the "default"-scoped entries only — it does not remove session-scoped registrations with the same (skill_id, intent_name, lang).

ovos.intent.enable and ovos.intent.disable (§8.5) are scoped the same way: they change the enabled state of the entries matching (session_id, skill_id, intent_name, lang) across both methods, with session_id read from context.session.session_id. Disabling an intent under a satellite's session does not disable the "default" registration of the same intent for any other session, and a party holding the default session cannot disable a satellite's session-scoped entry.

ovos.skill.deregister (§8.4) removes all registrations for that skill scoped to the session read from context.session.session_id. A bridge SHOULD emit ovos.skill.deregister carrying the satellite's session in context for each satellite skill when the satellite disconnects, to clean up the satellite's session-scoped registrations from the orchestrator's index.

11.4 Pipeline plugin visibility

A pipeline plugin that wishes to support session-scoped matching SHOULD receive the effective pool for the current session's session_id when performing a match, i.e. the union described in §11.2. Plugins that do not implement session-scoped matching continue to operate against the "default" pool only and remain conformant; they simply cannot match session-specific intents.

How the orchestrator communicates the effective pool to a plugin is an implementation concern outside this specification. The normative requirement is that the pool delivered to a matching plugin for session X MUST include all entries satisfying §11.2.

11.5 Dispatch routing for session-scoped skills

When the orchestrator dispatches a session-scoped intent — one registered under a non-default session_id — the dispatch Message is a .reply() of the inbound utterance, which sets context.destination to the originating participant's source. A bridge conformant with OVOS-BRIDGE-1 §3.2 will route that dispatch back to the satellite that owns the session. No special routing protocol is needed; the existing destination-based routing (OVOS-MSG-1 §3, OVOS-BRIDGE-1 §3.2) handles it transparently.


12. Conformance

A skill (producer of registration messages) MUST:

  • emit each registration through the topic that matches its definition method (§5 for keyword, §6 for template); a single intent MAY be registered under both methods if the skill has training data of both kinds (§3.2);
  • include the identity fields of §3.2 in every registration's data;
  • set Message.context["skill_id"] to its own skill_id on every Message it emits, per §3.1;
  • name itself in the payload skill_id of every message of §§5–8 it emits for its own registrations, including deregistration, enable, and disable (§3.2);
  • conform every registration's payload to §5 (keyword), §6 (template), or §7 (entity), respectively;
  • emit ovos.skill.loaded once it can receive dispatches, naming every capability it takes part in (§8.6);
  • emit ovos.intent.deregister / ovos.entity.deregister / ovos.skill.deregister to retract its registrations, paired with the local release of the handler (§9, INTENT-3 §6.1);
  • conform its underlying templates, vocabularies, and entities to OVOS-INTENT-1 and OVOS-INTENT-2;
  • name slots by their bare names throughout a template payload, and when it sends slot_types, derive it from the {type:name} placeholders of the same samples (§6.1).

A consuming plugin that does not implement typed slots MUST ignore slot_types and MUST NOT treat its presence as malforming the registration (§6.1, §6.3).

A skill SHOULD query the manifest (§10) to confirm a registration landed; there is no acknowledgement. A skill SHOULD re-emit its registrations on observing the deployment's readiness announcement (§10) — cold-start recovery for a late-starting orchestrator or consumer — carrying the same session context as the original registration (§10). A party holding intents disabled SHOULD re-emit ovos.intent.disable at the same point (§8.5).

A pipeline plugin (consumer) MAY:

  • subscribe to any subset of the registration topics and consume what fits its matching strategy — a plugin that consumes none and matches by internal rules (e.g. an LLM persona) is also conformant; a plugin that indexes registrations MUST also honour the §8 retraction and enable/disable topics for what it indexed — ovos.intent.deregister, ovos.entity.deregister, ovos.skill.deregister, and ovos.intent.enable / ovos.intent.disable — so a skill's registrations cannot outlive its unload or drift out of step with its declared enabled state in that plugin's index.

A plugin MUST NOT index a malformed registration (§§5.3, 6.3, 7.2 — including registrations whose intent_name is reserved, §3.2) and MUST log every such rejection at WARN with skill_id, intent_name/entity_name, lang, the rejecting topic, and a one-line reason — fire-and-forget means this log is the producer's only debugging signal. An individual malformed template, sample, or entity entry within an otherwise valid registration is skipped and logged, never grounds for rejecting the registration (§§5.3, 6.3, 7.2). Matching behaviour beyond that is OVOS-PIPELINE-1's concern.

A plugin MUST act on the payload skill_id of every message of §§5–8, never on context.skill_id, and MUST NOT reject a message because the two differ (§3.2).

The orchestrator MUST:

  • subscribe to every registration topic (§§5–8) and maintain the manifest — a passive index built from observed broadcasts;
  • key every manifest entry by the quintuple (session_id, skill_id, intent_name, lang, method), reading session_id from context.session.session_id of the registration message (§11.1);
  • serve ovos.intent.list and ovos.intent.describe queries against the manifest, returning the shape of §10.1 / §10.2; when the query includes a session_id, return the effective pool for that session per §11.2;
  • treat a re-registration with the same quintuple as replacement of the prior manifest entry (§8.1); other session_ids, languages, and methods for the same intent are unaffected;
  • honour ovos.intent.enable / ovos.intent.disable in the manifest (§8.5), scoped to the session_id read from context.session.session_id and applied across both methods (§11.3) — the enabled field of §10.1 reflects the latest state, which does not survive an orchestrator restart (§8.5);
  • index every non-reserved registration verbatim, without validating the payload — manifest presence records that the broadcast was observed, not that any plugin will match it (§2);
  • key every manifest entry by the payload skill_id, whether or not it matches context.skill_id (§3.2);
  • index every ovos.skill.loaded announcement by (session_id, skill_id) and answer ovos.skills.list from that index and the manifest (§8.6, §10.3);
  • on receiving ovos.skill.deregister, remove all manifest entries for the (session_id, skill_id) pair, with session_id read from context.session.session_id (§8.4, §11.1, §11.3);
  • NOT validate, reject, route, or gate any registration message beyond the reserved-intent_name exclusion of §3.2. The orchestrator is a passive listener for the manifest, not a routing party.

The orchestrator's other responsibilities — matching, dispatch, handler lifecycle, utterance lifecycle — live in OVOS-PIPELINE-1.


See also

  • Bus Message Specification (OVOS-MSG-1) — the envelope every message here travels in, the shared identifier-component rule (§2.1.1) bounding skill_id / intent_name, the destination and session keys used throughout, and the forward / reply / response derivations.
  • Session Specification (OVOS-SESSION-1) — the wire shape of session carried on every registration broadcast.
  • Utterance Lifecycle and Pipeline Specification (OVOS-PIPELINE-1) — the orchestrator's contract: pipeline-plugin model, utterance lifecycle, match-result notification, dispatch, handler-lifecycle trio, terminal events. This spec sits next to PIPELINE-1; together they cover the full skill ↔ orchestrator ↔ plugin path.
  • Intent Definition Specification (OVOS-INTENT-3) — the intent concept, identity, definition methods, and match result that this specification carries on the bus.
  • Locale Resource Formats Specification (OVOS-INTENT-2) — the authoring file formats a skill loader expands into inline samples before emitting a registration.
  • Sentence Template Grammar Specification (OVOS-INTENT-1) — the grammar of the samples strings carried in every registration payload.
  • Bridge Specification (OVOS-BRIDGE-1) — satellite skill registration, relay of the readiness announcement, and disconnect cleanup for session-scoped registrations (§4.4).
  • Fallback Specification (OVOS-FALLBACK-1) — the same payload-acts, context-is-provenance identity rule applied to fallback registration (§3.1).