@xema/omni-protocol 0.1.56 → 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -744,6 +744,13 @@ export interface Locked {
744
744
  * is the discriminant: a value that carries it is the lock, so no `T` may carry that key.
745
745
  */
746
746
  export type Lockable<T> = T | Locked;
747
+ /**
748
+ * Who chose a task's capabilities. `undetermined` is a fault the provider states on the task rather
749
+ * than hides: the platform's terms could not be read, and the set published is what the provider
750
+ * will honour, not what the platform permits.
751
+ */
752
+ export type CapabilitySource = "queue" | "ungoverned" | "undetermined";
753
+ export declare const CAPABILITY_SOURCES: readonly ["queue", "ungoverned", "undetermined"];
747
754
  export type Task<C extends Channel = Channel> = {
748
755
  id: TaskId;
749
756
  title: string;
@@ -751,6 +758,13 @@ export type Task<C extends Channel = Channel> = {
751
758
  /** The provider's own name for a category of work. Finer-grained than a channel. */
752
759
  taskType: string;
753
760
  capabilities: TaskCapabilities<C>;
761
+ /**
762
+ * Where the capabilities came from before the provider put them on the task, so a host can tell
763
+ * a fact from a fault: `queue` when somebody chose these terms, `ungoverned` when nothing handed
764
+ * the work over, `undetermined` when a queue was named and its terms could not be read -- the
765
+ * provider then publishes what it will honour and says so here, where the controls are drawn.
766
+ */
767
+ capabilitySource: CapabilitySource;
754
768
  browsers: TaskBrowser[];
755
769
  /** The person or entity on the other end of this task. Who the task is with; `contacts` on the snapshot is the directory. */
756
770
  party?: Contact;
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ export const DEFAULT_LEVELS = [
68
68
  export function effectiveLevels(declared) {
69
69
  return [...(declared ?? DEFAULT_LEVELS)];
70
70
  }
71
+ export const CAPABILITY_SOURCES = ["queue", "ungoverned", "undetermined"];
71
72
  // ---------------------------------------------------------------------------
72
73
  // Task commands.
73
74
  // ---------------------------------------------------------------------------
package/dist/testing.js CHANGED
@@ -301,6 +301,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
301
301
  violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
302
302
  message: `the provider reported a diagnostic: expected ${String(envelope.event.expected)}; observed ${String(envelope.event.observed)}` });
303
303
  }
304
+ violations.push(...undeterminedTasks(eventTasks(envelope), "event"));
304
305
  if (eventNamesUsers(envelope))
305
306
  requireMethod(live, "describeUsers", "an event publishes a UserId");
306
307
  if (eventDeclaresMute(envelope))
@@ -320,6 +321,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
320
321
  const snapshot = await connection.snapshot();
321
322
  observeSnapshot(snapshot, seen);
322
323
  violations.push(...validateSnapshot(snapshot, adapter.manifest, "snapshot", reader()));
324
+ violations.push(...undeterminedTasks(Array.isArray(snapshot?.tasks) ? snapshot.tasks : [], "snapshot.tasks"));
323
325
  stream.seed(snapshot);
324
326
  breaks.seed(snapshot);
325
327
  seeded = true;
@@ -526,6 +528,28 @@ export function assertAuthenticationRestoreAndExpiry(states) {
526
528
  throw new Error("Refreshing state must occur before expiry");
527
529
  }
528
530
  }
531
+ /** The tasks an envelope carries: the one a task event names, or a snapshot event's list. */
532
+ function eventTasks(envelope) {
533
+ if (!isRecord(envelope) || !isRecord(envelope.event))
534
+ return [];
535
+ const event = envelope.event;
536
+ if (event.type === "task-offered" || event.type === "task-updated")
537
+ return [event.task];
538
+ if (event.type === "snapshot" && isRecord(event.snapshot) && Array.isArray(event.snapshot.tasks))
539
+ return event.snapshot.tasks;
540
+ return [];
541
+ }
542
+ /**
543
+ * A task published under `undetermined` terms is a fact to a host and a failure to a conformance
544
+ * run, as a diagnostic is: the platform under test could not say what it permits, and a green
545
+ * result must not paper over it.
546
+ */
547
+ function undeterminedTasks(tasks, path) {
548
+ return tasks.flatMap((task, index) => isRecord(task) && task.capabilitySource === "undetermined"
549
+ ? [{ rule: "capabilitySource.undetermined", path: `${path}[${index}].capabilitySource`,
550
+ message: `task ${String(task.id)} was published under terms the provider could not determine` }]
551
+ : []);
552
+ }
529
553
  /**
530
554
  * Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
531
555
  * with the new set, and the next snapshot agrees with it. `states` is what the authentication
@@ -735,7 +759,7 @@ export class TaskStream {
735
759
  stages.set(entry.dialId, entry.stage);
736
760
  }
737
761
  }
738
- return { phase: String(isRecord(task) ? task.phase : undefined), media, stages };
762
+ return { phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages };
739
763
  }
740
764
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
741
765
  seed(snapshot) {
@@ -777,6 +801,12 @@ export class TaskStream {
777
801
  refuse("stream.taskUpdated.unknown", `${at}.task.id`, `${id} was never offered or carried on a snapshot`);
778
802
  break;
779
803
  }
804
+ // Terms once read stay read. A re-read that fails is not a new fact about the task, so the
805
+ // last statement stands and the failure is a diagnostic; undetermined is a place a task
806
+ // starts from, never one it returns to.
807
+ if ((known.source === "queue" || known.source === "ungoverned") && isRecord(event.task) && event.task.capabilitySource === "undetermined") {
808
+ refuse("stream.taskUpdated.capabilitySource", `${at}.task.capabilitySource`, `${id} was published under ${known.source} terms and now says undetermined: terms once read stay read, and a re-read that fails is a diagnostic, not a republish`);
809
+ }
780
810
  if (known.media === "ended") {
781
811
  const phase = isRecord(event.task) ? String(event.task.phase) : "";
782
812
  if (phase !== "completing") {
@@ -41,6 +41,7 @@ const TASK_PHASES = membersOf({
41
41
  pending: true, confirmed: true, preview: true, "in-progress": true, paused: true, completing: true,
42
42
  });
43
43
  const COMPLETION_MODES = membersOf({ "agent-command": true, "provider-automatic": true });
44
+ const CAPABILITY_SOURCES = membersOf({ queue: true, ungoverned: true, undetermined: true });
44
45
  const ACCEPTANCE_MODES = membersOf({
45
46
  "no-preference": true, "consent": true, "automatic": true,
46
47
  });
@@ -970,6 +971,9 @@ function validateTaskInto(task, context, path, into) {
970
971
  if (task.assisting !== undefined && task.monitoring !== undefined) {
971
972
  into.add("task.monitoring.assisting", `${path}.monitoring`, "a task is a joined call or a monitored one, never both");
972
973
  }
974
+ // Where the terms came from is stated with them: a host cannot tell "the platform permits
975
+ // nothing" from "the terms could not be read" from the set alone, and the provider knows which.
976
+ into.oneOf(task.capabilitySource, CAPABILITY_SOURCES, "task.capabilitySource", `${path}.capabilitySource`);
973
977
  const capabilities = task.capabilities;
974
978
  if (!isPlainObject(capabilities)) {
975
979
  into.add("task.capabilities.shape", `${path}.capabilities`, "a task needs a capabilities object");
package/guide.md CHANGED
@@ -644,12 +644,15 @@ type Lockable<T> = T | Locked;
644
644
 
645
645
  type TaskMediaState = "started" | "ended";
646
646
 
647
+ type CapabilitySource = "queue" | "ungoverned" | "undetermined";
648
+
647
649
  type Task<C extends Channel = Channel> = {
648
650
  id: TaskId;
649
651
  title: string;
650
652
  channel: C;
651
653
  taskType: string;
652
654
  capabilities: TaskCapabilities<C>;
655
+ capabilitySource: CapabilitySource;
653
656
  browsers: TaskBrowser[];
654
657
  party?: Contact;
655
658
  phase: TaskPhase;
@@ -2194,6 +2197,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2194
2197
  | `channel` | Channel handling this task. It must equal the source provider's manifest channel. |
2195
2198
  | `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
2196
2199
  | `capabilities` | Controls and workspace features available for this specific task. |
2200
+ | `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `ungoverned` when nothing handed the work over -- an agent's own outbound -- and `undetermined` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. A host shows `undetermined` where the agent works. See **Task capabilities**. |
2197
2201
  | `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
2198
2202
  | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
2199
2203
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
@@ -2669,7 +2673,19 @@ from no other source -- not the queue the task came from, not the login, not its
2669
2673
  -- because each of those answers a question about this task from somewhere that does not know
2670
2674
  about this task, and a client with two sources and a rule for choosing between them is the shape
2671
2675
  that produces two consumers disagreeing about one fact. The task is the one source, and the
2672
- provider puts on the task what the platform permits for it.
2676
+ provider puts on the task what the platform permits for it -- and says, in `capabilitySource`, who
2677
+ chose those terms. `queue` says somebody configured them. `ungoverned` says nothing handed the work
2678
+ over: an agent's own outbound call has no queue behind it, and the provider states what it permits
2679
+ for such a call. Neither changes where a host reads the capabilities from, which is the task; the
2680
+ source is one more published fact about them, and the one that lets a host tell a fact from a fault
2681
+ (see below).
2682
+
2683
+ **Completing the task is not a capability, and no capability set withholds it.** `complete` is
2684
+ governed by `completionMode` alone: under `agent-command` it is always available, whatever the set
2685
+ says, and the `dispositions` capability decides only whether a code travels with it. Likewise
2686
+ Answer on a pending task and Call on a preview are the phase's controls, not the set's. The
2687
+ capability set governs what the agent may do *with* the task; disposing of it is never on the
2688
+ list.
2673
2689
 
2674
2690
  **A permission that changes while the task is open is republished on the task at the moment it
2675
2691
  changes.** An agent on a billing dispute may refund up to their own limit and no further. The
@@ -2695,12 +2711,36 @@ republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal
2695
2711
  direction: the task as offered and as republished, and one command that was issuable under the
2696
2712
  first and is refused under the last for want of the capability withdrawn -- and nothing else.
2697
2713
 
2698
- **An empty capability set is a statement, not a shrug.** `capabilities: {}` says the platform
2699
- permits nothing on this task. A provider that has not yet learned what the platform permits -- a
2700
- queue's configuration that has not reached it -- knows nothing of the kind, and must not publish
2701
- the task as if it did: it holds the task until it knows, or reports the gap as a `diagnostic`,
2702
- because "no queue governs this call" is a fact and "the configuration has not arrived" is a fault,
2703
- and a host cannot tell them apart from a value that carries neither.
2714
+ **An empty capability set is a statement, not a shrug.** `capabilities: {}` under `queue` or
2715
+ `ungoverned` says the platform permits nothing capability-gated on this task, and a host draws
2716
+ nothing beyond what the phase and `completionMode` require. A provider that has not yet learned
2717
+ what the platform permits -- a queue's configuration that has not reached it -- knows nothing of
2718
+ the kind, and must not publish the task as if it did, neither as `{}` nor as every control it has.
2719
+ It publishes the task under `capabilitySource: "undetermined"`, with the capabilities it will
2720
+ honour until it knows, and a host shows that where the agent works, beside the controls it draws
2721
+ from them: "no queue governs this call" is a fact, "the configuration has not arrived" is a fault,
2722
+ and the set alone cannot tell them apart, so the provider says which. What a provider honours
2723
+ under undetermined terms is its own call -- a floor that would rather an agent briefly hold a
2724
+ control the platform might not have granted than lose hold or hang-up mid-call over a slow
2725
+ configuration read publishes those -- and the protocol chooses no default set for it. The fault is
2726
+ also reported as a `diagnostic` naming the task, one per occurrence, so an operator counts it. When
2727
+ the terms arrive, the task is republished under `queue` with the set as it now stands: the same
2728
+ republish as any other permission that changed while the task was open. The move goes one way.
2729
+ Terms once read stay read: a re-read that fails mid-task is not a new fact about the task, so the
2730
+ last statement stands and the failure is a `diagnostic`, and a task that was published under
2731
+ `queue` or `ungoverned` never returns to `undetermined` (`stream.taskUpdated.capabilitySource`).
2732
+
2733
+ What the agent is told differs by source, and only one source tells them anything. Under `queue`
2734
+ and `ungoverned` the agent sees controls and nothing about where they came from: both are facts,
2735
+ and an agent working a call has no use for the name of the rule behind its buttons. Under
2736
+ `undetermined` the agent is told, beside the controls, that these are what the provider will honour
2737
+ until the queue's terms arrive, and that the controls may change when they do -- a statement about
2738
+ the buttons in front of them now, not about the provider, because that is what changes when the
2739
+ republish lands. The two words are close in English and far apart on the desk: `ungoverned` is
2740
+ silence, `undetermined` is a notice. `exerciseAdapter` treats a
2741
+ task published under `undetermined` as a violation (`capabilitySource.undetermined`), as it treats
2742
+ a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
2743
+ rather than passing with a note.
2704
2744
 
2705
2745
  ```ts
2706
2746
  const taskCapabilities = {
@@ -4247,7 +4287,9 @@ agent works and keeps a count an operator can read, because a healthy workspace
4247
4287
  console is the silence problem one layer up. One event per occurrence; the host counts, the
4248
4288
  provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
4249
4289
  violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
4250
- fails loudly rather than passing with a note.
4290
+ fails loudly rather than passing with a note. A task published under `capabilitySource:
4291
+ "undetermined"` is the same kind of fact, stated on the task instead, and fails a run the same way
4292
+ (`capabilitySource.undetermined`); see **Task capabilities**.
4251
4293
 
4252
4294
  | Field | Contract |
4253
4295
  | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",