@xema/omni-protocol 0.1.29 → 0.1.31
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 +5 -0
- package/dist/testing.js +7 -1
- package/dist/validation.js +16 -1
- package/guide.md +41 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1108,6 +1108,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1108
1108
|
} | {
|
|
1109
1109
|
type: "queue-summary";
|
|
1110
1110
|
summary: QueueSummary;
|
|
1111
|
+
} | {
|
|
1112
|
+
type: "diagnostic";
|
|
1113
|
+
expected: string;
|
|
1114
|
+
observed: string;
|
|
1115
|
+
taskId?: TaskId;
|
|
1111
1116
|
} | {
|
|
1112
1117
|
type: "team-updated";
|
|
1113
1118
|
team: TeamRoster;
|
package/dist/testing.js
CHANGED
|
@@ -33,7 +33,7 @@ const STATE_SUBJECTS = [
|
|
|
33
33
|
// `ProviderEvent` without a row here, or a row it lacks, is a compile error.
|
|
34
34
|
const EVENT_TYPES = {
|
|
35
35
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
36
|
-
"task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true,
|
|
36
|
+
"task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true, diagnostic: true,
|
|
37
37
|
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
38
38
|
};
|
|
39
39
|
const CONTRACT_SUBJECTS = [
|
|
@@ -281,6 +281,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
281
281
|
unsubscribe = connection.subscribe(envelope => {
|
|
282
282
|
observeEvent(envelope, seen);
|
|
283
283
|
violations.push(...validateEventEnvelope(envelope, adapter.manifest, "event", reader()));
|
|
284
|
+
// A diagnostic is informational to a host and a failure to a conformance run: the platform
|
|
285
|
+
// under test broke a rule the adapter relies on, and a green result must not paper over it.
|
|
286
|
+
if (isRecord(envelope?.event) && envelope.event.type === "diagnostic") {
|
|
287
|
+
violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
|
|
288
|
+
message: `the provider reported a diagnostic: expected ${String(envelope.event.expected)}; observed ${String(envelope.event.observed)}` });
|
|
289
|
+
}
|
|
284
290
|
if (eventNamesUsers(envelope))
|
|
285
291
|
requireMethod(live, "describeUsers", "an event publishes a UserId");
|
|
286
292
|
// Cross-event rules apply once the stream has a beginning: the connect snapshot.
|
package/dist/validation.js
CHANGED
|
@@ -632,6 +632,7 @@ function validateHandlingHistory(value, path, into) {
|
|
|
632
632
|
into.add("task.handlingHistory.shape", path, "handlingHistory must be an array when present");
|
|
633
633
|
return;
|
|
634
634
|
}
|
|
635
|
+
let previous;
|
|
635
636
|
value.forEach((entry, index) => {
|
|
636
637
|
const at = `${path}[${index}]`;
|
|
637
638
|
if (!isPlainObject(entry)) {
|
|
@@ -639,7 +640,15 @@ function validateHandlingHistory(value, path, into) {
|
|
|
639
640
|
return;
|
|
640
641
|
}
|
|
641
642
|
into.oneOf(entry.step, HANDLING_STEPS, "task.handlingHistory.step", `${at}.step`);
|
|
642
|
-
into.timestamp(entry.at, "task.handlingHistory.at", `${at}.at`)
|
|
643
|
+
if (into.timestamp(entry.at, "task.handlingHistory.at", `${at}.at`)) {
|
|
644
|
+
// The record is one entry per occurrence, oldest first: a second hold is a second entry
|
|
645
|
+
// after the first, never a revision of it or an entry filed out of its turn.
|
|
646
|
+
const instant = Date.parse(entry.at);
|
|
647
|
+
if (previous !== undefined && instant < previous) {
|
|
648
|
+
into.add("task.handlingHistory.order", `${at}.at`, "handling steps are oldest first; this entry is earlier than the one before it");
|
|
649
|
+
}
|
|
650
|
+
previous = instant;
|
|
651
|
+
}
|
|
643
652
|
if (entry.seconds !== undefined) {
|
|
644
653
|
// Omitted while a leg is still running. Nought is a claim that it took no time.
|
|
645
654
|
into.require(isDurationSeconds(entry.seconds) && entry.seconds > 0, "task.handlingHistory.seconds", `${at}.seconds`, "seconds must be a positive whole number; omit it while the step is still running");
|
|
@@ -1242,6 +1251,12 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1242
1251
|
into.require(typeof event.html === "string", "event.announcement.html", `${at}.html`, "html must be a string when present");
|
|
1243
1252
|
}
|
|
1244
1253
|
break;
|
|
1254
|
+
case "diagnostic":
|
|
1255
|
+
into.filled(event.expected, "event.diagnostic.expected", `${at}.expected`, "a diagnostic states the rule that was broken, as a sentence");
|
|
1256
|
+
into.filled(event.observed, "event.diagnostic.observed", `${at}.observed`, "a diagnostic states what was observed instead");
|
|
1257
|
+
if (event.taskId !== undefined)
|
|
1258
|
+
into.require(isTaskId(event.taskId), "event.diagnostic.taskId", `${at}.taskId`, "taskId must be a task id when present");
|
|
1259
|
+
break;
|
|
1245
1260
|
case "queue-summary":
|
|
1246
1261
|
validateQueueSummary(event.summary, `${at}.summary`, into);
|
|
1247
1262
|
break;
|
package/guide.md
CHANGED
|
@@ -951,6 +951,7 @@ type ProviderEvent =
|
|
|
951
951
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
952
952
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
953
953
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
954
|
+
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
954
955
|
| { type: "team-updated"; team: TeamRoster }
|
|
955
956
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
956
957
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
@@ -2259,6 +2260,8 @@ const immediateProviderCompletion = {
|
|
|
2259
2260
|
handlingHistory: [
|
|
2260
2261
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2261
2262
|
{ step: "answered", at: "2026-08-21T00:59:41Z", by: "a-17" },
|
|
2263
|
+
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
2264
|
+
{ step: "held", at: "2026-08-21T01:06:48Z", by: "a-17" },
|
|
2262
2265
|
]
|
|
2263
2266
|
```
|
|
2264
2267
|
|
|
@@ -2272,6 +2275,16 @@ It rides in the snapshot and is replaced whole like everything else there.
|
|
|
2272
2275
|
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
2273
2276
|
`unanswered`, and each is defined on `TaskHandlingStep`.
|
|
2274
2277
|
|
|
2278
|
+
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2279
|
+
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
2280
|
+
entry after the first, never a revision of it. The same goes for every step: two mutes are two
|
|
2281
|
+
`muted` entries. Order is enforced: an entry earlier than the one before it is refused
|
|
2282
|
+
(`task.handlingHistory.order`). **Handle time is anchored, not restarted.** It runs from the
|
|
2283
|
+
`answered` step's `at` — from the task's first `in-progress` where the provider reports no
|
|
2284
|
+
history — until the task's media ends, and a hold neither pauses nor resets it: the hold's own
|
|
2285
|
+
duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
|
|
2286
|
+
counting the wrong thing.
|
|
2287
|
+
|
|
2275
2288
|
`muted` is there because Omni performs the mute rather than the provider — see **Where a command
|
|
2276
2289
|
executes** — so without a step the one participant that keeps the task's record would have no
|
|
2277
2290
|
account of a period the agent could not be heard.
|
|
@@ -3466,7 +3479,8 @@ Omni apply state the snapshot already superseded.
|
|
|
3466
3479
|
`transport-status` is the only signal Omni has that a transport died. An adapter must emit
|
|
3467
3480
|
`transport-status` with `connecting` or `error` as soon as it loses its transport, rather than
|
|
3468
3481
|
leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
|
|
3469
|
-
healthy provider from a dead one.
|
|
3482
|
+
healthy provider from a dead one. And what Omni is told, the agent is shown — see **The status is
|
|
3483
|
+
seen, not merely known** under `transport-status`.
|
|
3470
3484
|
|
|
3471
3485
|
#### Requesting a resync
|
|
3472
3486
|
|
|
@@ -3523,6 +3537,12 @@ authentication state, not here.
|
|
|
3523
3537
|
adapter that leaves a stale `active` in place is telling Omni to keep waiting for work that cannot
|
|
3524
3538
|
come — see **Liveness**.
|
|
3525
3539
|
|
|
3540
|
+
**The status is seen, not merely known.** A host renders the transport's status where the agent
|
|
3541
|
+
works — `connecting` from the moment it is reported, `error` with what revives it — and never a
|
|
3542
|
+
healthy workspace over a transport that cannot serve it: an agent talking on a desk whose transport
|
|
3543
|
+
died was an incident, and it happened because the desk knew and did not say. The same obligation
|
|
3544
|
+
covers a `diagnostic`: shown where the agent works, and counted.
|
|
3545
|
+
|
|
3526
3546
|
### `break-state`
|
|
3527
3547
|
|
|
3528
3548
|
Replaces this provider's complete `break` object. Its `approval` uses the canonical
|
|
@@ -3606,6 +3626,26 @@ Publishes an agent-facing message. `text` is always required and is the accessib
|
|
|
3606
3626
|
Optional HTML is sanitized by Omni. `announcedAt` and optional `expiresAt` are RFC-3339 times with
|
|
3607
3627
|
explicit timezones.
|
|
3608
3628
|
|
|
3629
|
+
### `diagnostic`
|
|
3630
|
+
|
|
3631
|
+
The provider's report that something its platform answered broke a rule the adapter relies on — a
|
|
3632
|
+
state read that contradicted itself, a `task-ended` naming a task nobody held, a party arriving
|
|
3633
|
+
without a name. `expected` is the rule as a sentence, `observed` is what came instead, and
|
|
3634
|
+
`taskId` names the task where there is one. **Informational, never behavioural**: the provider has
|
|
3635
|
+
already done the safe thing before it speaks, and nothing in the task or break machinery reacts to
|
|
3636
|
+
a diagnostic. It exists so that the loudness reaches a person — a host renders each one where the
|
|
3637
|
+
agent works and keeps a count an operator can read, because a healthy workspace over a shouting
|
|
3638
|
+
console is the silence problem one layer up. One event per occurrence; the host counts, the
|
|
3639
|
+
provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
|
|
3640
|
+
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
3641
|
+
fails loudly rather than passing with a note.
|
|
3642
|
+
|
|
3643
|
+
| Field | Contract |
|
|
3644
|
+
| --- | --- |
|
|
3645
|
+
| `expected` | Required. The rule that was broken, as a sentence a person can read. |
|
|
3646
|
+
| `observed` | Required. What the platform answered instead. |
|
|
3647
|
+
| `taskId` | Optional. The task concerned, where there is one. |
|
|
3648
|
+
|
|
3609
3649
|
### `queue-summary`
|
|
3610
3650
|
|
|
3611
3651
|
Publishes the provider's current dashboard contribution. Omni combines only the latest summary from
|