@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 +14 -0
- package/dist/index.js +1 -0
- package/dist/testing.js +31 -1
- package/dist/validation.js +4 -0
- package/guide.md +50 -8
- package/package.json +1 -1
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") {
|
package/dist/validation.js
CHANGED
|
@@ -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: {}`
|
|
2699
|
-
permits nothing on this task
|
|
2700
|
-
|
|
2701
|
-
the
|
|
2702
|
-
|
|
2703
|
-
|
|
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
|
| --- | --- |
|