@xema/omni-protocol 0.1.68 → 0.1.69
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/testing.js +23 -2
- package/guide.md +31 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1507,10 +1507,24 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1507
1507
|
}
|
|
1508
1508
|
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
1509
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1510
|
+
/**
|
|
1511
|
+
* What the host refused, told to the adapter that published it: a snapshot the host would not
|
|
1512
|
+
* replace its state with, or an event it dropped, and every rule it broke. A refusal the provider
|
|
1513
|
+
* never hears of is a desk frozen at its last good state for a shift with nothing wrong on the
|
|
1514
|
+
* provider's side; this is how both sides see it.
|
|
1515
|
+
*/
|
|
1516
|
+
export interface Refusal {
|
|
1517
|
+
artefact: "snapshot" | "event";
|
|
1518
|
+
/** The envelope refused, where the artefact is an event. */
|
|
1519
|
+
envelopeId?: string;
|
|
1520
|
+
violations: ProtocolViolation[];
|
|
1521
|
+
}
|
|
1510
1522
|
export interface Connection<C extends Channel = Channel> {
|
|
1511
1523
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
|
1512
1524
|
/** Delivery order must match the order the provider observes changes. Never replay. */
|
|
1513
1525
|
subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
|
|
1526
|
+
/** Told what the host refused, snapshot or event, with every rule broken. Always; a refusal is visible on both sides. */
|
|
1527
|
+
refused(report: Refusal): void;
|
|
1514
1528
|
/** Nothing may be allocated until a capacity is stated, so every connection receives it. */
|
|
1515
1529
|
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
1516
1530
|
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);
|
package/guide.md
CHANGED
|
@@ -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
|
|
@@ -4605,6 +4614,25 @@ and may be compiled against a different protocol version, so its output is untru
|
|
|
4605
4614
|
Validating a snapshot before it replaces provider state is what stops a malformed task from
|
|
4606
4615
|
reaching the workspace.
|
|
4607
4616
|
|
|
4617
|
+
### What the host does with what it refuses
|
|
4618
|
+
|
|
4619
|
+
A refusal has an aftermath on the desk and a report to the provider, and both are stated.
|
|
4620
|
+
|
|
4621
|
+
- **A refused snapshot replaces nothing.** The host keeps the last state it took from this
|
|
4622
|
+
provider whole, and shows the provider as faulted -- the transport as it stands, and the words
|
|
4623
|
+
"last update refused" with the rule -- so the agent knows the view is standing still rather than
|
|
4624
|
+
believing it current. It does not adopt the good half of a snapshot: a task the agent is on would
|
|
4625
|
+
vanish from the desk while the call is up.
|
|
4626
|
+
- **A refused event is dropped and counted.** The state the host holds does not move for it. The
|
|
4627
|
+
provider is told, and may republish a corrected state or raise a `diagnostic`; the host retries
|
|
4628
|
+
nothing.
|
|
4629
|
+
- **The provider is told, every time.** `refused(report)` carries the artefact, the envelope id for
|
|
4630
|
+
an event, and every violation with its rule and path. An adapter logs it at error and treats it as
|
|
4631
|
+
its own defect until shown otherwise: what the host refused never reached the agent, and a
|
|
4632
|
+
provider that hears nothing runs a whole shift beside a frozen desk with nothing wrong on its
|
|
4633
|
+
side. The harness tells an adapter under test exactly as a host does
|
|
4634
|
+
(`connection.refused.required`, `connection.refused.rejected`).
|
|
4635
|
+
|
|
4608
4636
|
## Conformance helpers
|
|
4609
4637
|
|
|
4610
4638
|
### `exerciseAdapter(adapter, context, options?)`
|