@xema/omni-protocol 0.1.55 → 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.d.ts +15 -1
- package/dist/testing.js +80 -2
- package/dist/validation.js +4 -0
- package/guide.md +82 -1
- 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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
|
|
1
|
+
import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
|
|
2
2
|
import { type ProtocolViolation } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
|
|
4
4
|
/**
|
|
@@ -84,6 +84,20 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
|
|
|
84
84
|
code: string;
|
|
85
85
|
};
|
|
86
86
|
}): void;
|
|
87
|
+
/**
|
|
88
|
+
* A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
|
|
89
|
+
* mid-task republishes the task with the set as it now stands, and a command under the withdrawn
|
|
90
|
+
* capability is refused from then on. `tasks` is what the provider published for one task, first
|
|
91
|
+
* to last -- the offer, then each republish -- every one validated against the manifest, all the
|
|
92
|
+
* same id, with at least one capability offered by the first absent from the last. A locked
|
|
93
|
+
* control is present without permission, not withdrawn: it is still drawn, so it does not count.
|
|
94
|
+
* `command` is the command a host issues under a withdrawn capability: it must be clean against
|
|
95
|
+
* the first task and refused against the last for want of that capability and for nothing else.
|
|
96
|
+
* That pairing is what proves the refusal comes from the withdrawal rather than from a command
|
|
97
|
+
* that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
|
|
98
|
+
* answer to the same command.
|
|
99
|
+
*/
|
|
100
|
+
export declare function assertTaskCapabilityWithdrawal(tasks: readonly Task[], manifest: Manifest, command: Record<string, unknown>): void;
|
|
87
101
|
/**
|
|
88
102
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
89
103
|
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
|
|
2
|
-
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
2
|
+
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
@@ -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
|
|
@@ -575,6 +599,54 @@ export function assertCommandRefusedAfterWithdrawal(result) {
|
|
|
575
599
|
throw new Error(`A command after its capability was withdrawn fails with omni.capability-not-enabled, not ${result.failure?.code}`);
|
|
576
600
|
}
|
|
577
601
|
}
|
|
602
|
+
/**
|
|
603
|
+
* A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
|
|
604
|
+
* mid-task republishes the task with the set as it now stands, and a command under the withdrawn
|
|
605
|
+
* capability is refused from then on. `tasks` is what the provider published for one task, first
|
|
606
|
+
* to last -- the offer, then each republish -- every one validated against the manifest, all the
|
|
607
|
+
* same id, with at least one capability offered by the first absent from the last. A locked
|
|
608
|
+
* control is present without permission, not withdrawn: it is still drawn, so it does not count.
|
|
609
|
+
* `command` is the command a host issues under a withdrawn capability: it must be clean against
|
|
610
|
+
* the first task and refused against the last for want of that capability and for nothing else.
|
|
611
|
+
* That pairing is what proves the refusal comes from the withdrawal rather than from a command
|
|
612
|
+
* that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
|
|
613
|
+
* answer to the same command.
|
|
614
|
+
*/
|
|
615
|
+
export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
|
|
616
|
+
const first = tasks[0];
|
|
617
|
+
const last = tasks.at(-1);
|
|
618
|
+
if (tasks.length < 2 || first === undefined || last === undefined) {
|
|
619
|
+
throw new Error("Task capability withdrawal needs the task as offered and at least one republish of it");
|
|
620
|
+
}
|
|
621
|
+
const context = {
|
|
622
|
+
channel: manifest.channel,
|
|
623
|
+
levels: effectiveLevels(manifest.orgLevels).map(level => level.id),
|
|
624
|
+
dialOutcomesDeclared: manifest.dialOutcomes !== undefined,
|
|
625
|
+
};
|
|
626
|
+
tasks.forEach((task, index) => {
|
|
627
|
+
assertNoViolations(validateTask(task, context, `tasks[${index}]`), `Task capability withdrawal: tasks[${index}] must be a task the wire could carry`);
|
|
628
|
+
if (task.id !== first.id) {
|
|
629
|
+
throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.id}, and the offer was ${first.id}`);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
const withdrawn = Object.keys(first.capabilities).filter(name => last.capabilities[name] === undefined);
|
|
633
|
+
if (withdrawn.length === 0) {
|
|
634
|
+
throw new Error("Task capability withdrawal must end with at least one capability the offer declared withdrawn; a locked control is present without permission, not withdrawn");
|
|
635
|
+
}
|
|
636
|
+
const before = validateTaskCommand(command, first, "command");
|
|
637
|
+
if (before.length > 0) {
|
|
638
|
+
throw new Error(`Task capability withdrawal: the command must be issuable against the task as offered, or its later refusal proves nothing (${before.map(v => v.rule).join(", ")})`);
|
|
639
|
+
}
|
|
640
|
+
const after = validateTaskCommand(command, last, "command");
|
|
641
|
+
const forWant = after.filter(v => withdrawn.some(name => v.rule === `command.capability.${name}`));
|
|
642
|
+
if (forWant.length === 0) {
|
|
643
|
+
throw new Error(`Task capability withdrawal: the command must be refused against the republished task for want of ${withdrawn.join(" or ")}; it was ${after.length === 0 ? "accepted" : `refused for ${after.map(v => v.rule).join(", ")}`}`);
|
|
644
|
+
}
|
|
645
|
+
const other = after.filter(v => !forWant.includes(v));
|
|
646
|
+
if (other.length > 0) {
|
|
647
|
+
throw new Error(`Task capability withdrawal: the republished task refuses the command for more than the withdrawal (${other.map(v => v.rule).join(", ")}); change one thing at a time`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
578
650
|
/**
|
|
579
651
|
* Throws unless the run reached every subject named: the paired assertion beside a clean result,
|
|
580
652
|
* so a fixture that never produced a roster cannot pass a test that meant to check one.
|
|
@@ -687,7 +759,7 @@ export class TaskStream {
|
|
|
687
759
|
stages.set(entry.dialId, entry.stage);
|
|
688
760
|
}
|
|
689
761
|
}
|
|
690
|
-
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 };
|
|
691
763
|
}
|
|
692
764
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
693
765
|
seed(snapshot) {
|
|
@@ -729,6 +801,12 @@ export class TaskStream {
|
|
|
729
801
|
refuse("stream.taskUpdated.unknown", `${at}.task.id`, `${id} was never offered or carried on a snapshot`);
|
|
730
802
|
break;
|
|
731
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
|
+
}
|
|
732
810
|
if (known.media === "ended") {
|
|
733
811
|
const phase = isRecord(event.task) ? String(event.task.phase) : "";
|
|
734
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. |
|
|
@@ -2664,6 +2668,80 @@ browser that used it.
|
|
|
2664
2668
|
Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
|
|
2665
2669
|
must not show or issue hold for that task even if another task from the same provider supports it.
|
|
2666
2670
|
|
|
2671
|
+
**A capability is a property of the task, and of nothing the task names.** A provider derives it
|
|
2672
|
+
from no other source -- not the queue the task came from, not the login, not its own configuration
|
|
2673
|
+
-- because each of those answers a question about this task from somewhere that does not know
|
|
2674
|
+
about this task, and a client with two sources and a rule for choosing between them is the shape
|
|
2675
|
+
that produces two consumers disagreeing about one fact. The task is the one source, and the
|
|
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.
|
|
2689
|
+
|
|
2690
|
+
**A permission that changes while the task is open is republished on the task at the moment it
|
|
2691
|
+
changes.** An agent on a billing dispute may refund up to their own limit and no further. The
|
|
2692
|
+
customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
|
|
2693
|
+
republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
|
|
2694
|
+
is republished without it. Nothing about the agent changed -- who is on the call did -- and the
|
|
2695
|
+
task said so both times, at the moment it became true. A call moves queues the same way: a caller
|
|
2696
|
+
identified as a priority customer is transferred to the priority queue, and the task arrives under
|
|
2697
|
+
that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
|
|
2698
|
+
restated on the task at the hand-over, not inferred from the queue it left. A capability stated
|
|
2699
|
+
once at offer or answer and never corrected is a fact with a shelf life and no expiry, and a host
|
|
2700
|
+
draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
|
|
2701
|
+
the task, with its capabilities as they now stand, and a host treats the last statement as current
|
|
2702
|
+
rather than re-deriving anything. This is the same shape as a room left full after the call ends
|
|
2703
|
+
-- a field describing the present, carried past the moment it stopped being true -- carried past
|
|
2704
|
+
this time not by a spread but by nobody sending the correction. A provider whose platform can
|
|
2705
|
+
change a permission mid-task and does not republish is in breach, however conformant its offer was.
|
|
2706
|
+
A capability set can shrink as well as grow, and a host that has only ever seen it grow meets a
|
|
2707
|
+
control that was there a moment ago and is gone; a command that arrives after its capability was
|
|
2708
|
+
withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
|
|
2709
|
+
provider gets right first: a control gained late is a nicety, a control withdrawn and not
|
|
2710
|
+
republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
|
|
2711
|
+
direction: the task as offered and as republished, and one command that was issuable under the
|
|
2712
|
+
first and is refused under the last for want of the capability withdrawn -- and nothing else.
|
|
2713
|
+
|
|
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.
|
|
2744
|
+
|
|
2667
2745
|
```ts
|
|
2668
2746
|
const taskCapabilities = {
|
|
2669
2747
|
channel: "voice",
|
|
@@ -4209,7 +4287,9 @@ agent works and keeps a count an operator can read, because a healthy workspace
|
|
|
4209
4287
|
console is the silence problem one layer up. One event per occurrence; the host counts, the
|
|
4210
4288
|
provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
|
|
4211
4289
|
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
4212
|
-
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**.
|
|
4213
4293
|
|
|
4214
4294
|
| Field | Contract |
|
|
4215
4295
|
| --- | --- |
|
|
@@ -4401,6 +4481,7 @@ cannot be established from TypeScript structure alone.
|
|
|
4401
4481
|
| Helper | Contract checked |
|
|
4402
4482
|
| --- | --- |
|
|
4403
4483
|
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no roster for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
|
|
4484
|
+
| `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
|
|
4404
4485
|
| `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
|
|
4405
4486
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
4406
4487
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|