@xema/omni-protocol 0.1.28 → 0.1.29
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 +7 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +3 -0
- package/dist/validation.d.ts +3 -1
- package/dist/validation.js +17 -13
- package/guide.md +17 -15
- 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
|
} | {
|
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",
|
|
@@ -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");
|
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");
|
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
|
}
|
|
@@ -1830,7 +1830,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1830
1830
|
| --- | --- |
|
|
1831
1831
|
| `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
|
|
1832
1832
|
| `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`,
|
|
1833
|
+
| `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
1834
|
| `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
1835
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
1836
1836
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -1974,13 +1974,15 @@ deliver again once it has gone quiet. Hold the capacity and deliver when work ar
|
|
|
1974
1974
|
**4. A provider offers a task.** The provider emits `task-offered` within the stated capacity.
|
|
1975
1975
|
|
|
1976
1976
|
**5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
|
|
1977
|
-
agent acceptance. When it is `true`, the
|
|
1977
|
+
agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
|
|
1978
1978
|
intent.
|
|
1979
1979
|
|
|
1980
1980
|
### Acceptance modes
|
|
1981
1981
|
|
|
1982
1982
|
During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
|
|
1983
|
-
provider
|
|
1983
|
+
provider states `acceptance` on each pending task — on the task rather than the offer, so a
|
|
1984
|
+
reconnect snapshot says it too and an offer the host never received is not accepted on the
|
|
1985
|
+
person's behalf for want of a word:
|
|
1984
1986
|
|
|
1985
1987
|
| Directive | Contract |
|
|
1986
1988
|
| --- | --- |
|
|
@@ -1988,7 +1990,7 @@ provider includes an acceptance directive with each allocation:
|
|
|
1988
1990
|
| `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
1991
|
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
1990
1992
|
|
|
1991
|
-
When Omni sent `autoAcceptTasks: false`, the provider omits `
|
|
1993
|
+
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task
|
|
1992
1994
|
requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
1993
1995
|
provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
|
|
1994
1996
|
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
@@ -1998,19 +2000,19 @@ press is Omni's doing, not the provider's.
|
|
|
1998
2000
|
gone ready is telling the deployment they are working. Requiring a press before every contact is
|
|
1999
2001
|
the exception a provisioning file asks for, not the state it falls into when a flag is missing.
|
|
2000
2002
|
|
|
2001
|
-
Nothing is given away by that default. `
|
|
2003
|
+
Nothing is given away by that default. `acceptance` is the provider's own control and outranks
|
|
2002
2004
|
it: `consent` puts the decision back in the agent's hands for any task where it
|
|
2003
2005
|
belongs, whatever the host was configured with.
|
|
2004
2006
|
|
|
2005
2007
|
An automatically accepted task still arrives through `task-offered`.
|
|
2006
2008
|
|
|
2007
|
-
Agent-initiated work arrives through `task-offered` with
|
|
2008
|
-
`
|
|
2009
|
+
Agent-initiated work arrives through `task-offered` with the task's
|
|
2010
|
+
`acceptance: "automatic"`.
|
|
2009
2011
|
|
|
2010
2012
|
### Pending
|
|
2011
2013
|
|
|
2012
2014
|
A task in the `pending` phase has been **offered to the agent and not yet accepted**. Omni applies
|
|
2013
|
-
`autoAcceptTasks` and the
|
|
2015
|
+
`autoAcceptTasks` and the task's `acceptance` to decide whether acceptance is
|
|
2014
2016
|
automatic or requires the agent. A provider that requires automatic acceptance still emits
|
|
2015
2017
|
`task-offered`; it does not introduce new work as `in-progress`.
|
|
2016
2018
|
|
|
@@ -2019,8 +2021,7 @@ declare const task: Task;
|
|
|
2019
2021
|
|
|
2020
2022
|
const allocation = {
|
|
2021
2023
|
type: "task-offered",
|
|
2022
|
-
task,
|
|
2023
|
-
acceptanceMode: "consent",
|
|
2024
|
+
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2024
2025
|
allocationExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2025
2026
|
preparationEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2026
2027
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
@@ -2083,6 +2084,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2083
2084
|
| `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
2085
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
|
|
2085
2086
|
| `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`**. |
|
|
2087
|
+
| `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
2088
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2087
2089
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2088
2090
|
| `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 +2136,12 @@ The canonical task transitions are:
|
|
|
2134
2136
|
| Any phase | Provider emits `task-ended` | Removed |
|
|
2135
2137
|
|
|
2136
2138
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2137
|
-
|
|
2139
|
+
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2138
2140
|
subsequent transitions to `preparing` or `in-progress`; Omni does not infer them from the acceptance
|
|
2139
2141
|
command.
|
|
2140
2142
|
|
|
2141
2143
|
**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 `
|
|
2144
|
+
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2143
2145
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2144
2146
|
consult, a conference leg joining or leaving, a transfer, a callback — the media follows
|
|
2145
2147
|
separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
|
|
@@ -3548,8 +3550,8 @@ multi-provider break**.
|
|
|
3548
3550
|
### `task-offered`
|
|
3549
3551
|
|
|
3550
3552
|
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
|
-
`
|
|
3553
|
+
the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the task's
|
|
3554
|
+
`acceptance`. `task-offered` must not introduce a task as `in-progress`; only a reconnect or
|
|
3553
3555
|
resync snapshot may report work already in progress. The provider should include the task in later
|
|
3554
3556
|
snapshots until it ends.
|
|
3555
3557
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xema/omni-protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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",
|