@xema/omni-protocol 0.1.68 → 0.1.70
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 +21 -3
- package/dist/testing.js +43 -2
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +10 -0
- package/guide.md +58 -14
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1397,11 +1397,15 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1397
1397
|
/**
|
|
1398
1398
|
* How many tasks this provider may have allocated to the agent at once.
|
|
1399
1399
|
*
|
|
1400
|
-
* An absolute ceiling
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1400
|
+
* An absolute ceiling standing until Omni restates it. The provider counts its own outstanding
|
|
1401
|
+
* tasks against it and needs no new signal when one ends. The agent is one person on several
|
|
1402
|
+
* providers, and the host divides their capacity among them: `0` is host-stopped -- the agent's
|
|
1403
|
+
* capacity is elsewhere for now, this provider allocates nothing and shows the member as
|
|
1404
|
+
* `on-task` -- and is not a break, which is the agent not working at all. The host restates a
|
|
1405
|
+
* count of one or more when this provider has the agent's capacity again.
|
|
1403
1406
|
*/
|
|
1404
1407
|
export interface AgentCapacity {
|
|
1408
|
+
/** A whole number, zero or more; zero is host-stopped. */
|
|
1405
1409
|
count: number;
|
|
1406
1410
|
}
|
|
1407
1411
|
export interface SummaryMetric {
|
|
@@ -1507,10 +1511,24 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1507
1511
|
}
|
|
1508
1512
|
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
|
|
1509
1513
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1514
|
+
/**
|
|
1515
|
+
* What the host refused, told to the adapter that published it: a snapshot the host would not
|
|
1516
|
+
* replace its state with, or an event it dropped, and every rule it broke. A refusal the provider
|
|
1517
|
+
* never hears of is a desk frozen at its last good state for a shift with nothing wrong on the
|
|
1518
|
+
* provider's side; this is how both sides see it.
|
|
1519
|
+
*/
|
|
1520
|
+
export interface Refusal {
|
|
1521
|
+
artefact: "snapshot" | "event";
|
|
1522
|
+
/** The envelope refused, where the artefact is an event. */
|
|
1523
|
+
envelopeId?: string;
|
|
1524
|
+
violations: ProtocolViolation[];
|
|
1525
|
+
}
|
|
1510
1526
|
export interface Connection<C extends Channel = Channel> {
|
|
1511
1527
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
|
1512
1528
|
/** Delivery order must match the order the provider observes changes. Never replay. */
|
|
1513
1529
|
subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
|
|
1530
|
+
/** Told what the host refused, snapshot or event, with every rule broken. Always; a refusal is visible on both sides. */
|
|
1531
|
+
refused(report: Refusal): void;
|
|
1514
1532
|
/** Nothing may be allocated until a capacity is stated, so every connection receives it. */
|
|
1515
1533
|
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
1516
1534
|
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
package/dist/testing.js
CHANGED
|
@@ -292,6 +292,19 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
292
292
|
const connected = { ...context, host, store: watched.store };
|
|
293
293
|
connection = await adapter.connect(connected);
|
|
294
294
|
const live = connection;
|
|
295
|
+
// A refusal is visible on both sides: what the host would not take, the adapter is told, with the rules.
|
|
296
|
+
requireMethod(live, "refused", "every connection is told what the host refused");
|
|
297
|
+
const tellRefused = (report) => {
|
|
298
|
+
if (typeof live.refused !== "function")
|
|
299
|
+
return;
|
|
300
|
+
ruleEvaluated("connection.refused.rejected");
|
|
301
|
+
try {
|
|
302
|
+
live.refused(report);
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
violations.push({ rule: "connection.refused.rejected", path: "connection.refused", message: `told of a refusal, the adapter threw: ${String(error)}` });
|
|
306
|
+
}
|
|
307
|
+
};
|
|
295
308
|
// Dial is declared by presence: the capability object carries a destination policy rather
|
|
296
309
|
// than an `enabled` flag, so its presence is the declaration.
|
|
297
310
|
if (adapter.manifest.idleCapabilities?.dial !== undefined)
|
|
@@ -310,7 +323,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
310
323
|
const waiters = new Set();
|
|
311
324
|
unsubscribe = connection.subscribe(envelope => {
|
|
312
325
|
observeEvent(envelope, seen);
|
|
313
|
-
|
|
326
|
+
const broken = validateEventEnvelope(envelope, adapter.manifest, "event", reader());
|
|
327
|
+
violations.push(...broken);
|
|
328
|
+
if (broken.length > 0)
|
|
329
|
+
tellRefused({ artefact: "event", envelopeId: typeof envelope?.id === "string" ? envelope.id : undefined, violations: broken });
|
|
314
330
|
// A diagnostic is informational to a host and a failure to a conformance run: the platform
|
|
315
331
|
// under test broke a rule the adapter relies on, and a green result must not paper over it.
|
|
316
332
|
// A reload in life has no first connection: the page is gone. The drive keeps its first
|
|
@@ -361,7 +377,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
361
377
|
});
|
|
362
378
|
const snapshot = await connection.snapshot();
|
|
363
379
|
observeSnapshot(snapshot, seen);
|
|
364
|
-
|
|
380
|
+
{
|
|
381
|
+
const broken = validateSnapshot(snapshot, adapter.manifest, "snapshot", reader());
|
|
382
|
+
violations.push(...broken);
|
|
383
|
+
if (broken.length > 0)
|
|
384
|
+
tellRefused({ artefact: "snapshot", violations: broken });
|
|
385
|
+
}
|
|
365
386
|
violations.push(...undeterminedTasks(Array.isArray(snapshot?.tasks) ? snapshot.tasks : [], "snapshot.tasks"));
|
|
366
387
|
stream.seed(snapshot);
|
|
367
388
|
breaks.seed(snapshot);
|
|
@@ -431,6 +452,26 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
431
452
|
context: connected, secrets: authenticationSecrets, reader, rebuild: options.rebuild, held: watched.held, reloading,
|
|
432
453
|
}));
|
|
433
454
|
}
|
|
455
|
+
// Capacity supersedes rather than accumulates, so a decrease is as ordinary as an increase: the
|
|
456
|
+
// host raises it, lowers it, and takes it away, and the provider takes each as the ceiling it is.
|
|
457
|
+
// A provider whose ceiling can only rise passes a zero it special-cases and still cannot go from
|
|
458
|
+
// five to three, so the exercise moves the axis both ways. Zero is host-stopped: the agent's
|
|
459
|
+
// capacity is elsewhere, and the provider allocates nothing and refuses nothing for it. Any offer
|
|
460
|
+
// after a lower count is caught against it (stream.taskOffered.overCapacity).
|
|
461
|
+
ruleEvaluated("connection.setCapacity.lowered", "connection.setCapacity.zero");
|
|
462
|
+
for (const count of [2, 1, 0]) {
|
|
463
|
+
capacityStated = count;
|
|
464
|
+
const restated = await live.setCapacity({ count });
|
|
465
|
+
const shape = validateResult(restated, "setCapacity", "connection.setCapacity");
|
|
466
|
+
violations.push(...shape);
|
|
467
|
+
if (shape.length === 0 && restated.status === "failed") {
|
|
468
|
+
violations.push({ rule: count === 0 ? "connection.setCapacity.zero" : "connection.setCapacity.lowered", path: "connection.setCapacity",
|
|
469
|
+
message: count === 0
|
|
470
|
+
? `the provider would not take a capacity of zero: ${restated.failure.code}; zero is host-stopped, the agent's capacity being elsewhere, and a provider allocates nothing and refuses nothing for it`
|
|
471
|
+
: `the provider would not take a capacity of ${count} after a higher one: ${restated.failure.code}; capacity supersedes, and a decrease is as ordinary as an increase` });
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
434
475
|
}
|
|
435
476
|
finally {
|
|
436
477
|
let clean = true;
|
package/dist/validation.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export declare function validateEventEnvelope(envelope: unknown, manifest: unkno
|
|
|
64
64
|
* phone the microphone is the phone's, and off voice there is none. Neither the value nor its
|
|
65
65
|
* absence is inferred from what kind of application the host is.
|
|
66
66
|
*/
|
|
67
|
+
/** What the host states as capacity: a whole number, zero or more, zero being host-stopped. */
|
|
68
|
+
export declare function validateCapacity(capacity: unknown, path?: string): ProtocolViolation[];
|
|
67
69
|
/** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
|
|
68
70
|
export declare function validateLoginStore(store: unknown, path?: string): ProtocolViolation[];
|
|
69
71
|
export declare function validateHostMute(mute: unknown, softphone: boolean, path?: string): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -1700,6 +1700,16 @@ const HOST_MUTES = membersOf({ stream: true, station: true });
|
|
|
1700
1700
|
* phone the microphone is the phone's, and off voice there is none. Neither the value nor its
|
|
1701
1701
|
* absence is inferred from what kind of application the host is.
|
|
1702
1702
|
*/
|
|
1703
|
+
/** What the host states as capacity: a whole number, zero or more, zero being host-stopped. */
|
|
1704
|
+
export function validateCapacity(capacity, path = "capacity") {
|
|
1705
|
+
const into = new Collector();
|
|
1706
|
+
if (!isPlainObject(capacity)) {
|
|
1707
|
+
into.add("capacity.shape", path, "a capacity is an object carrying count");
|
|
1708
|
+
return into.violations;
|
|
1709
|
+
}
|
|
1710
|
+
into.require(typeof capacity.count === "number" && Number.isInteger(capacity.count) && capacity.count >= 0, "capacity.count", `${path}.count`, "count is a whole number, zero or more: how many tasks this provider may allocate at once, zero while the agent's capacity is elsewhere");
|
|
1711
|
+
return into.violations;
|
|
1712
|
+
}
|
|
1703
1713
|
/** The login's store, which the host provides on every connection: three functions, and nothing else is a store. */
|
|
1704
1714
|
export function validateLoginStore(store, path = "store") {
|
|
1705
1715
|
const into = new Collector();
|
package/guide.md
CHANGED
|
@@ -425,7 +425,7 @@ type Snapshot = {
|
|
|
425
425
|
};
|
|
426
426
|
|
|
427
427
|
type AgentCapacity = {
|
|
428
|
-
count: number; // absolute ceiling,
|
|
428
|
+
count: number; // absolute ceiling, zero or more; zero is host-stopped
|
|
429
429
|
};
|
|
430
430
|
|
|
431
431
|
type CapacityResult =
|
|
@@ -1066,9 +1066,16 @@ adapter compiled against the browser lib.
|
|
|
1066
1066
|
### Adapter and connection
|
|
1067
1067
|
|
|
1068
1068
|
```ts
|
|
1069
|
+
type Refusal = {
|
|
1070
|
+
artefact: "snapshot" | "event";
|
|
1071
|
+
envelopeId?: string;
|
|
1072
|
+
violations: ProtocolViolation[];
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1069
1075
|
type Connection<C extends Channel = Channel> = {
|
|
1070
1076
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
|
1071
1077
|
subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
|
|
1078
|
+
refused(report: Refusal): void;
|
|
1072
1079
|
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
1073
1080
|
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
|
1074
1081
|
disconnect(): Promise<void>;
|
|
@@ -2028,6 +2035,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2028
2035
|
| `snapshot()` | Always. |
|
|
2029
2036
|
| `subscribe(listener)` | Always. |
|
|
2030
2037
|
| `disconnect()` | Always. |
|
|
2038
|
+
| `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
|
|
2031
2039
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
2032
2040
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2033
2041
|
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone` where the provider knows it. |
|
|
@@ -2358,9 +2366,10 @@ Reaching the instant is not itself a transition: the provider reports what it di
|
|
|
2358
2366
|
|
|
2359
2367
|
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
2360
2368
|
spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the host refuses
|
|
2361
|
-
the update (`task.preview.deadline.unexpected`)
|
|
2362
|
-
|
|
2363
|
-
wait for, so it carries neither
|
|
2369
|
+
the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
|
|
2370
|
+
provider is told through `refused` exactly which rule, so the state that looked right on its side
|
|
2371
|
+
is named on its side. The task past preview has no deadline to wait for, so it carries neither
|
|
2372
|
+
field.
|
|
2364
2373
|
|
|
2365
2374
|
**A task is never its audio.** A voice task is the allocation: the call is offered when it is
|
|
2366
2375
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
@@ -3361,9 +3370,10 @@ hold applied. A `held` result would repeat the discriminant that travelled with
|
|
|
3361
3370
|
|
|
3362
3371
|
States how many tasks this provider may have allocated to the agent **at once**.
|
|
3363
3372
|
|
|
3364
|
-
`count` is an absolute ceiling, not an increment
|
|
3365
|
-
property of the agent, not of the moment: it is
|
|
3366
|
-
when
|
|
3373
|
+
`count` is an absolute ceiling, not an increment, a whole number of zero or more
|
|
3374
|
+
(`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
|
|
3375
|
+
stated when the agent is set up and restated only when it genuinely changes, which is a
|
|
3376
|
+
provisioning change rather than a task starting or ending -- with one exception below.
|
|
3367
3377
|
|
|
3368
3378
|
**The provider counts its own outstanding tasks against it.** Allocate while you hold fewer than
|
|
3369
3379
|
`count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
|
|
@@ -3371,9 +3381,21 @@ room again and need no new signal to know it. Omni does not re-state capacity as
|
|
|
3371
3381
|
go, and a provider that waits for it will stall.
|
|
3372
3382
|
|
|
3373
3383
|
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
3374
|
-
concern — Omni set `count` knowing it
|
|
3375
|
-
|
|
3376
|
-
|
|
3384
|
+
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3385
|
+
providers, and the host divides their capacity among them rather than telling each the whole. A
|
|
3386
|
+
provider that has none of it for now is told **`count: 0`, host-stopped**: allocate nothing, show
|
|
3387
|
+
the member as `on-task` on the roster, since the agent is working, and take the next count as any
|
|
3388
|
+
other when the host has capacity for this provider again. Zero is not a refusal to state, and a
|
|
3389
|
+
provider that answers it `failed` is named (`connection.setCapacity.zero`); it is the one restatement
|
|
3390
|
+
that follows work rather than provisioning.
|
|
3391
|
+
|
|
3392
|
+
Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
|
|
3393
|
+
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
3394
|
+
it was ever told, or returns early on a small one -- cannot be told to take less work, and a host
|
|
3395
|
+
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
3396
|
+
both ways after the drive, two then one then nought, and names a provider that will not come down
|
|
3397
|
+
(`connection.setCapacity.lowered`, `connection.setCapacity.zero`); an offer after a lower count is
|
|
3398
|
+
caught against it (`stream.taskOffered.overCapacity`).
|
|
3377
3399
|
|
|
3378
3400
|
**Capacity gates what the provider allocates, not what the agent starts.** A call placed from the
|
|
3379
3401
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
@@ -3965,10 +3987,12 @@ anything to become available.
|
|
|
3965
3987
|
| Omni commits a break | `commitBreak`. The break stops allocation, not the ceiling. |
|
|
3966
3988
|
| Agent returns from break | `endBreak` |
|
|
3967
3989
|
|
|
3968
|
-
**
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3990
|
+
**A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
|
|
3991
|
+
agent this provider may carry; a break says they are not working at all. A provider told
|
|
3992
|
+
`count: 0` shows an agent whose capacity is elsewhere, `on-task`; a provider told a break shows
|
|
3993
|
+
`on-break`. Collapsing the two would leave a provider unable to tell an agent working on another
|
|
3994
|
+
provider from an agent who has gone to lunch, and only one of those needs a reason, a decision and
|
|
3995
|
+
a return.
|
|
3972
3996
|
|
|
3973
3997
|
### How the agent hears the call
|
|
3974
3998
|
|
|
@@ -4578,6 +4602,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
4578
4602
|
| `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
4579
4603
|
| `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
4580
4604
|
| `validateLoginStore(store)` | The login's store the host hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
4605
|
+
| `validateCapacity(capacity)` | What the host states as capacity: a whole number of zero or more (`capacity.count`), zero being host-stopped. The harness states one on connect and two, one and zero after the drive, and names a provider that will not come down (`connection.setCapacity.lowered`) or take zero (`connection.setCapacity.zero`). |
|
|
4581
4606
|
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|
|
4582
4607
|
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.handling` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
|
|
4583
4608
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
@@ -4605,6 +4630,25 @@ and may be compiled against a different protocol version, so its output is untru
|
|
|
4605
4630
|
Validating a snapshot before it replaces provider state is what stops a malformed task from
|
|
4606
4631
|
reaching the workspace.
|
|
4607
4632
|
|
|
4633
|
+
### What the host does with what it refuses
|
|
4634
|
+
|
|
4635
|
+
A refusal has an aftermath on the desk and a report to the provider, and both are stated.
|
|
4636
|
+
|
|
4637
|
+
- **A refused snapshot replaces nothing.** The host keeps the last state it took from this
|
|
4638
|
+
provider whole, and shows the provider as faulted -- the transport as it stands, and the words
|
|
4639
|
+
"last update refused" with the rule -- so the agent knows the view is standing still rather than
|
|
4640
|
+
believing it current. It does not adopt the good half of a snapshot: a task the agent is on would
|
|
4641
|
+
vanish from the desk while the call is up.
|
|
4642
|
+
- **A refused event is dropped and counted.** The state the host holds does not move for it. The
|
|
4643
|
+
provider is told, and may republish a corrected state or raise a `diagnostic`; the host retries
|
|
4644
|
+
nothing.
|
|
4645
|
+
- **The provider is told, every time.** `refused(report)` carries the artefact, the envelope id for
|
|
4646
|
+
an event, and every violation with its rule and path. An adapter logs it at error and treats it as
|
|
4647
|
+
its own defect until shown otherwise: what the host refused never reached the agent, and a
|
|
4648
|
+
provider that hears nothing runs a whole shift beside a frozen desk with nothing wrong on its
|
|
4649
|
+
side. The harness tells an adapter under test exactly as a host does
|
|
4650
|
+
(`connection.refused.required`, `connection.refused.rejected`).
|
|
4651
|
+
|
|
4608
4652
|
## Conformance helpers
|
|
4609
4653
|
|
|
4610
4654
|
### `exerciseAdapter(adapter, context, options?)`
|