@xema/omni-protocol 0.1.28 → 0.1.30
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/README.md +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +10 -1
- package/dist/validation.d.ts +3 -1
- package/dist/validation.js +23 -13
- package/guide.md +46 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ system.
|
|
|
15
15
|
| `src/index.ts` | The TypeScript declarations. |
|
|
16
16
|
| `src/validation.ts` | Runtime validators Omni applies to adapter output. |
|
|
17
17
|
| `src/testing.ts` | Conformance helpers an adapter runs against its own test state. |
|
|
18
|
+
| `tests/` | One test file per source module, plus the guards over the guide and the repository's own text. |
|
|
18
19
|
|
|
19
20
|
## Entry points
|
|
20
21
|
|
package/dist/index.d.ts
CHANGED
|
@@ -607,6 +607,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
607
607
|
/** The person or entity on the other end of this task. Who the task is with; `contacts` on the snapshot is the directory. */
|
|
608
608
|
party?: Contact;
|
|
609
609
|
phase: TaskPhase;
|
|
610
|
+
/**
|
|
611
|
+
* How this offer is accepted, stated on the pending task rather than the offer so a reconnect
|
|
612
|
+
* snapshot says it too: an offer the host never received is not accepted on the person's behalf
|
|
613
|
+
* for want of a word. Required while `pending` when Omni said it may auto-accept
|
|
614
|
+
* (`autoAcceptTasks: true`), forbidden when it said not, and absent past `pending`.
|
|
615
|
+
*/
|
|
616
|
+
acceptance?: AcceptanceMode;
|
|
610
617
|
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
611
618
|
reference?: string;
|
|
612
619
|
attributes?: TaskAttribute[];
|
|
@@ -1077,7 +1084,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1077
1084
|
} | {
|
|
1078
1085
|
type: "task-offered";
|
|
1079
1086
|
task: Task<C>;
|
|
1080
|
-
acceptanceMode?: AcceptanceMode;
|
|
1081
1087
|
allocationExpiresAt?: IsoTimestamp;
|
|
1082
1088
|
preparationEndsAt?: IsoTimestamp;
|
|
1083
1089
|
} | {
|
|
@@ -1102,6 +1108,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1102
1108
|
} | {
|
|
1103
1109
|
type: "queue-summary";
|
|
1104
1110
|
summary: QueueSummary;
|
|
1111
|
+
} | {
|
|
1112
|
+
type: "diagnostic";
|
|
1113
|
+
expected: string;
|
|
1114
|
+
observed: string;
|
|
1115
|
+
taskId?: TaskId;
|
|
1105
1116
|
} | {
|
|
1106
1117
|
type: "team-updated";
|
|
1107
1118
|
team: TeamRoster;
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and roster, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.consultation", "task.lead", "task.assisting", "task.media", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.consultation", "task.lead", "task.assisting", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
package/dist/testing.js
CHANGED
|
@@ -16,6 +16,7 @@ const STATE_SUBJECTS = [
|
|
|
16
16
|
"task.lead",
|
|
17
17
|
"task.assisting",
|
|
18
18
|
"task.media",
|
|
19
|
+
"task.acceptance",
|
|
19
20
|
"task.dispositions",
|
|
20
21
|
"task.destinations",
|
|
21
22
|
"task.custom",
|
|
@@ -32,7 +33,7 @@ const STATE_SUBJECTS = [
|
|
|
32
33
|
// `ProviderEvent` without a row here, or a row it lacks, is a compile error.
|
|
33
34
|
const EVENT_TYPES = {
|
|
34
35
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
35
|
-
"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,
|
|
36
37
|
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
37
38
|
};
|
|
38
39
|
const CONTRACT_SUBJECTS = [
|
|
@@ -61,6 +62,8 @@ function observeTask(value, seen) {
|
|
|
61
62
|
seen.add("task.assisting");
|
|
62
63
|
if (value.media !== undefined)
|
|
63
64
|
seen.add("task.media");
|
|
65
|
+
if (value.acceptance !== undefined)
|
|
66
|
+
seen.add("task.acceptance");
|
|
64
67
|
const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
|
|
65
68
|
if (isRecord(capabilities.dispositions))
|
|
66
69
|
seen.add("task.dispositions");
|
|
@@ -278,6 +281,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
278
281
|
unsubscribe = connection.subscribe(envelope => {
|
|
279
282
|
observeEvent(envelope, seen);
|
|
280
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
|
+
}
|
|
281
290
|
if (eventNamesUsers(envelope))
|
|
282
291
|
requireMethod(live, "describeUsers", "an event publishes a UserId");
|
|
283
292
|
// Cross-event rules apply once the stream has a beginning: the connect snapshot.
|
package/dist/validation.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface TaskValidationContext {
|
|
|
15
15
|
channel: string;
|
|
16
16
|
/** The level ids in force, from the manifest. The defaults when absent. */
|
|
17
17
|
levels?: readonly string[];
|
|
18
|
+
/** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether a pending task states its `acceptance`. */
|
|
19
|
+
autoAcceptTasks?: boolean;
|
|
18
20
|
}
|
|
19
21
|
export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
|
|
20
22
|
/**
|
|
@@ -36,7 +38,7 @@ export interface ReaderContext {
|
|
|
36
38
|
levels?: readonly string[];
|
|
37
39
|
/** The login's `loginId`. A snapshot or event naming another belongs to a login that is gone. */
|
|
38
40
|
loginId?: string;
|
|
39
|
-
/** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether
|
|
41
|
+
/** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether a pending task states its `acceptance`. */
|
|
40
42
|
autoAcceptTasks?: boolean;
|
|
41
43
|
}
|
|
42
44
|
export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -729,6 +729,20 @@ function validateTaskInto(task, context, path, into) {
|
|
|
729
729
|
into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
|
|
730
730
|
into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
|
|
731
731
|
into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`);
|
|
732
|
+
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
733
|
+
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
734
|
+
if (task.acceptance !== undefined) {
|
|
735
|
+
into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
|
|
736
|
+
if (task.phase !== "pending") {
|
|
737
|
+
into.add("task.acceptance.unexpected", `${path}.acceptance`, "acceptance is an offer's word; a task past pending has been accepted");
|
|
738
|
+
}
|
|
739
|
+
else if (context.autoAcceptTasks === false) {
|
|
740
|
+
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task requires agent acceptance and a pending task carries no acceptance");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
else if (task.phase === "pending" && context.autoAcceptTasks === true) {
|
|
744
|
+
into.add("task.acceptance.required", `${path}.acceptance`, "autoAcceptTasks is on, so a pending task states how it is accepted");
|
|
745
|
+
}
|
|
732
746
|
into.oneOf(task.completionMode, COMPLETION_MODES, "task.completionMode", `${path}.completionMode`);
|
|
733
747
|
// The allowance is coupled to the mode: a provider that will complete the task itself is going
|
|
734
748
|
// to act on the allowance, so it must state one; a provider waiting for `complete` may omit it
|
|
@@ -1009,7 +1023,7 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
|
|
|
1009
1023
|
const seen = new Set();
|
|
1010
1024
|
let assisting;
|
|
1011
1025
|
snapshot.tasks.forEach((task, index) => {
|
|
1012
|
-
validateTaskInto(task, { channel, levels }, `${path}.tasks[${index}]`, into);
|
|
1026
|
+
validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${path}.tasks[${index}]`, into);
|
|
1013
1027
|
// A lead assists one call at a time.
|
|
1014
1028
|
if (isPlainObject(task) && task.assisting !== undefined) {
|
|
1015
1029
|
if (assisting !== undefined)
|
|
@@ -1196,28 +1210,18 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1196
1210
|
validateBreakState(event.break, `${at}.break`, into);
|
|
1197
1211
|
break;
|
|
1198
1212
|
case "task-offered":
|
|
1199
|
-
validateTaskInto(event.task, { channel, levels }, `${at}.task`, into);
|
|
1213
|
+
validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
|
|
1200
1214
|
// An offer introduces work that is not yet under way; work in progress arrives only on a snapshot.
|
|
1201
1215
|
if (isPlainObject(event.task) && typeof event.task.phase === "string") {
|
|
1202
1216
|
into.require(OFFERABLE_PHASES.includes(event.task.phase), "event.taskOffered.phase", `${at}.task.phase`, `task-offered introduces a task as ${OFFERABLE_PHASES.join(", ")}, never as ${event.task.phase}`);
|
|
1203
1217
|
}
|
|
1204
|
-
if (event.acceptanceMode !== undefined) {
|
|
1205
|
-
into.oneOf(event.acceptanceMode, ACCEPTANCE_MODES, "event.taskOffered.acceptanceMode", `${at}.acceptanceMode`);
|
|
1206
|
-
}
|
|
1207
|
-
// The mode travels exactly when Omni said tasks may be auto-accepted.
|
|
1208
|
-
if (context.autoAcceptTasks === true) {
|
|
1209
|
-
into.require(event.acceptanceMode !== undefined, "event.taskOffered.acceptanceMode.required", `${at}.acceptanceMode`, "autoAcceptTasks is on, so task-offered carries an acceptanceMode");
|
|
1210
|
-
}
|
|
1211
|
-
else if (context.autoAcceptTasks === false) {
|
|
1212
|
-
into.require(event.acceptanceMode === undefined, "event.taskOffered.acceptanceMode.unexpected", `${at}.acceptanceMode`, "autoAcceptTasks is off, so every task requires agent acceptance and task-offered carries no acceptanceMode");
|
|
1213
|
-
}
|
|
1214
1218
|
for (const field of ["allocationExpiresAt", "preparationEndsAt"]) {
|
|
1215
1219
|
if (event[field] !== undefined)
|
|
1216
1220
|
into.timestamp(event[field], `event.taskOffered.${field}`, `${at}.${field}`);
|
|
1217
1221
|
}
|
|
1218
1222
|
break;
|
|
1219
1223
|
case "task-updated":
|
|
1220
|
-
validateTaskInto(event.task, { channel, levels }, `${at}.task`, into);
|
|
1224
|
+
validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
|
|
1221
1225
|
break;
|
|
1222
1226
|
case "task-media-started":
|
|
1223
1227
|
into.require(isTaskId(event.taskId), "event.taskMediaStarted.taskId", `${at}.taskId`, "a task id is required");
|
|
@@ -1238,6 +1242,12 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1238
1242
|
into.require(typeof event.html === "string", "event.announcement.html", `${at}.html`, "html must be a string when present");
|
|
1239
1243
|
}
|
|
1240
1244
|
break;
|
|
1245
|
+
case "diagnostic":
|
|
1246
|
+
into.filled(event.expected, "event.diagnostic.expected", `${at}.expected`, "a diagnostic states the rule that was broken, as a sentence");
|
|
1247
|
+
into.filled(event.observed, "event.diagnostic.observed", `${at}.observed`, "a diagnostic states what was observed instead");
|
|
1248
|
+
if (event.taskId !== undefined)
|
|
1249
|
+
into.require(isTaskId(event.taskId), "event.diagnostic.taskId", `${at}.taskId`, "taskId must be a task id when present");
|
|
1250
|
+
break;
|
|
1241
1251
|
case "queue-summary":
|
|
1242
1252
|
validateQueueSummary(event.summary, `${at}.summary`, into);
|
|
1243
1253
|
break;
|
package/guide.md
CHANGED
|
@@ -604,6 +604,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
604
604
|
browsers: TaskBrowser[];
|
|
605
605
|
party?: Contact;
|
|
606
606
|
phase: TaskPhase;
|
|
607
|
+
acceptance?: AcceptanceMode;
|
|
607
608
|
reference?: string;
|
|
608
609
|
attributes?: TaskAttribute[];
|
|
609
610
|
handlingHistory?: TaskHandlingStep[];
|
|
@@ -941,7 +942,6 @@ type ProviderEvent =
|
|
|
941
942
|
| {
|
|
942
943
|
type: "task-offered";
|
|
943
944
|
task: Task;
|
|
944
|
-
acceptanceMode?: AcceptanceMode;
|
|
945
945
|
allocationExpiresAt?: IsoTimestamp;
|
|
946
946
|
preparationEndsAt?: IsoTimestamp;
|
|
947
947
|
}
|
|
@@ -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[] };
|
|
@@ -1830,7 +1831,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1830
1831
|
| --- | --- |
|
|
1831
1832
|
| `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
|
|
1832
1833
|
| `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
|
|
1833
|
-
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`,
|
|
1834
|
+
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
1834
1835
|
| `host` | The host's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The host reports, the adapter decides**. |
|
|
1835
1836
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
1836
1837
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -1974,13 +1975,15 @@ deliver again once it has gone quiet. Hold the capacity and deliver when work ar
|
|
|
1974
1975
|
**4. A provider offers a task.** The provider emits `task-offered` within the stated capacity.
|
|
1975
1976
|
|
|
1976
1977
|
**5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
|
|
1977
|
-
agent acceptance. When it is `true`, the
|
|
1978
|
+
agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
|
|
1978
1979
|
intent.
|
|
1979
1980
|
|
|
1980
1981
|
### Acceptance modes
|
|
1981
1982
|
|
|
1982
1983
|
During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
|
|
1983
|
-
provider
|
|
1984
|
+
provider states `acceptance` on each pending task — on the task rather than the offer, so a
|
|
1985
|
+
reconnect snapshot says it too and an offer the host never received is not accepted on the
|
|
1986
|
+
person's behalf for want of a word:
|
|
1984
1987
|
|
|
1985
1988
|
| Directive | Contract |
|
|
1986
1989
|
| --- | --- |
|
|
@@ -1988,7 +1991,7 @@ provider includes an acceptance directive with each allocation:
|
|
|
1988
1991
|
| `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. A host that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
|
|
1989
1992
|
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
1990
1993
|
|
|
1991
|
-
When Omni sent `autoAcceptTasks: false`, the provider omits `
|
|
1994
|
+
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task
|
|
1992
1995
|
requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
1993
1996
|
provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
|
|
1994
1997
|
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
@@ -1998,19 +2001,19 @@ press is Omni's doing, not the provider's.
|
|
|
1998
2001
|
gone ready is telling the deployment they are working. Requiring a press before every contact is
|
|
1999
2002
|
the exception a provisioning file asks for, not the state it falls into when a flag is missing.
|
|
2000
2003
|
|
|
2001
|
-
Nothing is given away by that default. `
|
|
2004
|
+
Nothing is given away by that default. `acceptance` is the provider's own control and outranks
|
|
2002
2005
|
it: `consent` puts the decision back in the agent's hands for any task where it
|
|
2003
2006
|
belongs, whatever the host was configured with.
|
|
2004
2007
|
|
|
2005
2008
|
An automatically accepted task still arrives through `task-offered`.
|
|
2006
2009
|
|
|
2007
|
-
Agent-initiated work arrives through `task-offered` with
|
|
2008
|
-
`
|
|
2010
|
+
Agent-initiated work arrives through `task-offered` with the task's
|
|
2011
|
+
`acceptance: "automatic"`.
|
|
2009
2012
|
|
|
2010
2013
|
### Pending
|
|
2011
2014
|
|
|
2012
2015
|
A task in the `pending` phase has been **offered to the agent and not yet accepted**. Omni applies
|
|
2013
|
-
`autoAcceptTasks` and the
|
|
2016
|
+
`autoAcceptTasks` and the task's `acceptance` to decide whether acceptance is
|
|
2014
2017
|
automatic or requires the agent. A provider that requires automatic acceptance still emits
|
|
2015
2018
|
`task-offered`; it does not introduce new work as `in-progress`.
|
|
2016
2019
|
|
|
@@ -2019,8 +2022,7 @@ declare const task: Task;
|
|
|
2019
2022
|
|
|
2020
2023
|
const allocation = {
|
|
2021
2024
|
type: "task-offered",
|
|
2022
|
-
task,
|
|
2023
|
-
acceptanceMode: "consent",
|
|
2025
|
+
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2024
2026
|
allocationExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2025
2027
|
preparationEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2026
2028
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
@@ -2083,6 +2085,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2083
2085
|
| `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. |
|
|
2084
2086
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
|
|
2085
2087
|
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. |
|
|
2088
|
+
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2086
2089
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2087
2090
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2088
2091
|
| `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
@@ -2134,12 +2137,12 @@ The canonical task transitions are:
|
|
|
2134
2137
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2135
2138
|
|
|
2136
2139
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2137
|
-
|
|
2140
|
+
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2138
2141
|
subsequent transitions to `preparing` or `in-progress`; Omni does not infer them from the acceptance
|
|
2139
2142
|
command.
|
|
2140
2143
|
|
|
2141
2144
|
**A task is never its audio.** A voice task is the allocation: the call is offered when it is
|
|
2142
|
-
routed to the agent and accepted as `
|
|
2145
|
+
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2143
2146
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2144
2147
|
consult, a conference leg joining or leaving, a transfer, a callback — the media follows
|
|
2145
2148
|
separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
|
|
@@ -3464,7 +3467,8 @@ Omni apply state the snapshot already superseded.
|
|
|
3464
3467
|
`transport-status` is the only signal Omni has that a transport died. An adapter must emit
|
|
3465
3468
|
`transport-status` with `connecting` or `error` as soon as it loses its transport, rather than
|
|
3466
3469
|
leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
|
|
3467
|
-
healthy provider from a dead one.
|
|
3470
|
+
healthy provider from a dead one. And what Omni is told, the agent is shown — see **The status is
|
|
3471
|
+
seen, not merely known** under `transport-status`.
|
|
3468
3472
|
|
|
3469
3473
|
#### Requesting a resync
|
|
3470
3474
|
|
|
@@ -3521,6 +3525,12 @@ authentication state, not here.
|
|
|
3521
3525
|
adapter that leaves a stale `active` in place is telling Omni to keep waiting for work that cannot
|
|
3522
3526
|
come — see **Liveness**.
|
|
3523
3527
|
|
|
3528
|
+
**The status is seen, not merely known.** A host renders the transport's status where the agent
|
|
3529
|
+
works — `connecting` from the moment it is reported, `error` with what revives it — and never a
|
|
3530
|
+
healthy workspace over a transport that cannot serve it: an agent talking on a desk whose transport
|
|
3531
|
+
died was an incident, and it happened because the desk knew and did not say. The same obligation
|
|
3532
|
+
covers a `diagnostic`: shown where the agent works, and counted.
|
|
3533
|
+
|
|
3524
3534
|
### `break-state`
|
|
3525
3535
|
|
|
3526
3536
|
Replaces this provider's complete `break` object. Its `approval` uses the canonical
|
|
@@ -3548,8 +3558,8 @@ multi-provider break**.
|
|
|
3548
3558
|
### `task-offered`
|
|
3549
3559
|
|
|
3550
3560
|
Offers a task to Omni without a separate offer acknowledgement. An offer does not accept
|
|
3551
|
-
the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the
|
|
3552
|
-
`
|
|
3561
|
+
the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the task's
|
|
3562
|
+
`acceptance`. `task-offered` must not introduce a task as `in-progress`; only a reconnect or
|
|
3553
3563
|
resync snapshot may report work already in progress. The provider should include the task in later
|
|
3554
3564
|
snapshots until it ends.
|
|
3555
3565
|
|
|
@@ -3604,6 +3614,26 @@ Publishes an agent-facing message. `text` is always required and is the accessib
|
|
|
3604
3614
|
Optional HTML is sanitized by Omni. `announcedAt` and optional `expiresAt` are RFC-3339 times with
|
|
3605
3615
|
explicit timezones.
|
|
3606
3616
|
|
|
3617
|
+
### `diagnostic`
|
|
3618
|
+
|
|
3619
|
+
The provider's report that something its platform answered broke a rule the adapter relies on — a
|
|
3620
|
+
state read that contradicted itself, a `task-ended` naming a task nobody held, a party arriving
|
|
3621
|
+
without a name. `expected` is the rule as a sentence, `observed` is what came instead, and
|
|
3622
|
+
`taskId` names the task where there is one. **Informational, never behavioural**: the provider has
|
|
3623
|
+
already done the safe thing before it speaks, and nothing in the task or break machinery reacts to
|
|
3624
|
+
a diagnostic. It exists so that the loudness reaches a person — a host renders each one where the
|
|
3625
|
+
agent works and keeps a count an operator can read, because a healthy workspace over a shouting
|
|
3626
|
+
console is the silence problem one layer up. One event per occurrence; the host counts, the
|
|
3627
|
+
provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
|
|
3628
|
+
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
3629
|
+
fails loudly rather than passing with a note.
|
|
3630
|
+
|
|
3631
|
+
| Field | Contract |
|
|
3632
|
+
| --- | --- |
|
|
3633
|
+
| `expected` | Required. The rule that was broken, as a sentence a person can read. |
|
|
3634
|
+
| `observed` | Required. What the platform answered instead. |
|
|
3635
|
+
| `taskId` | Optional. The task concerned, where there is one. |
|
|
3636
|
+
|
|
3607
3637
|
### `queue-summary`
|
|
3608
3638
|
|
|
3609
3639
|
Publishes the provider's current dashboard contribution. Omni combines only the latest summary from
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xema/omni-protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.30",
|
|
4
4
|
"description": "The Omni protocol: the contract every provider adapter implements",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
32
32
|
"build": "pnpm run clean && tsc -p tsconfig.json",
|
|
33
|
-
"test": "tsc -p tsconfig.
|
|
33
|
+
"test": "tsc -p tsconfig.test.json && vitest run"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^22.20.1",
|