@xema/omni-protocol 0.1.75 → 0.1.76
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 -2
- package/dist/testing.js +12 -1
- package/dist/validation.js +23 -1
- package/guide.md +34 -13
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -904,11 +904,14 @@ export type TaskOutcome = {
|
|
|
904
904
|
} | {
|
|
905
905
|
type: "transferred";
|
|
906
906
|
destinationId?: string;
|
|
907
|
-
}
|
|
907
|
+
}
|
|
908
|
+
/** Who called the work off: the agent declining, the provider withdrawing or re-routing, the party abandoning. */
|
|
909
|
+
| {
|
|
908
910
|
type: "cancelled";
|
|
911
|
+
by: "agent" | "provider" | "party";
|
|
909
912
|
reason?: string;
|
|
910
913
|
}
|
|
911
|
-
/** Only the phases in which somebody is still being waited on can expire
|
|
914
|
+
/** Only the phases in which somebody is still being waited on can expire; an offer that lapses at `allocationExpiresAt` names `pending`. */
|
|
912
915
|
| {
|
|
913
916
|
type: "expired";
|
|
914
917
|
phase: "pending" | "confirmed" | "preview";
|
package/dist/testing.js
CHANGED
|
@@ -1043,7 +1043,9 @@ export class TaskStream {
|
|
|
1043
1043
|
stages.set(entry.dialId, entry.stage);
|
|
1044
1044
|
}
|
|
1045
1045
|
}
|
|
1046
|
-
return {
|
|
1046
|
+
return { channel: String(isRecord(task) ? task.channel : undefined), completionMode: String(isRecord(task) ? task.completionMode : undefined),
|
|
1047
|
+
wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
|
|
1048
|
+
phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), allocation: String(isRecord(task) ? task.allocationId : undefined) };
|
|
1047
1049
|
}
|
|
1048
1050
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
1049
1051
|
seed(snapshot) {
|
|
@@ -1231,6 +1233,15 @@ export class TaskStream {
|
|
|
1231
1233
|
// A late ending for a life that is over must not end the life that is open under the same id.
|
|
1232
1234
|
if (!this.namesTheOpenLife(event, known, at, refuse))
|
|
1233
1235
|
break;
|
|
1236
|
+
// Off voice there is no media event, so the update that moves a task to completing is the
|
|
1237
|
+
// provider's word that handling ended and the moment the wrap allowance starts. A provider
|
|
1238
|
+
// that completes the task itself with an allowance to run has to have started the clock:
|
|
1239
|
+
// completed from in-progress, the allowance it stated was never given.
|
|
1240
|
+
ruleEvaluated("stream.taskEnded.unwrapped");
|
|
1241
|
+
if (known.channel !== "voice" && known.completionMode === "provider-automatic" && known.wrapAllowance !== undefined && known.wrapAllowance > 0
|
|
1242
|
+
&& known.phase !== "completing" && isRecord(event.outcome) && event.outcome.type === "completed") {
|
|
1243
|
+
refuse("stream.taskEnded.unwrapped", `${at}.outcome`, `${id} was completed from ${known.phase} with a wrap allowance of ${known.wrapAllowance}s under provider-automatic: off voice, completing is the provider's word that handling ended and the allowance's start, and it was never published`);
|
|
1244
|
+
}
|
|
1234
1245
|
this.endedAllocations.add(known.allocation);
|
|
1235
1246
|
this.tasks.delete(id);
|
|
1236
1247
|
break;
|
package/dist/validation.js
CHANGED
|
@@ -89,6 +89,7 @@ const PHONES = membersOf({ softphone: true, deskPhone: true });
|
|
|
89
89
|
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
90
90
|
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, barge: true });
|
|
91
91
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
92
|
+
const CANCELLED_BY = membersOf({ agent: true, provider: true, party: true });
|
|
92
93
|
const EXPIRABLE_PHASES = membersOf({
|
|
93
94
|
pending: true, confirmed: true, preview: true,
|
|
94
95
|
});
|
|
@@ -994,6 +995,12 @@ function validateMonitoring(value, channel, path, into) {
|
|
|
994
995
|
into.oneOf(value.mode, MONITOR_MODES, "task.monitoring.mode", `${path}.mode`);
|
|
995
996
|
into.timestamp(value.since, "task.monitoring.since", `${path}.since`);
|
|
996
997
|
}
|
|
998
|
+
/** Whether a task is the agent's own doing: a dial the host placed on its onCall, a lead's join, a lead's monitor. */
|
|
999
|
+
function originatedByTheAgent(task) {
|
|
1000
|
+
if (Array.isArray(task.onCall) && task.onCall.some(entry => isPlainObject(entry) && typeof entry.dialId === "string"))
|
|
1001
|
+
return true;
|
|
1002
|
+
return task.assisting !== undefined || task.monitoring !== undefined;
|
|
1003
|
+
}
|
|
997
1004
|
export function validateTask(task, context, path = "task") {
|
|
998
1005
|
const into = new Collector();
|
|
999
1006
|
validateTaskInto(task, context, path, into);
|
|
@@ -1032,15 +1039,27 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1032
1039
|
}
|
|
1033
1040
|
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
1034
1041
|
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
1042
|
+
// Work the agent originated -- a dial or connect-back, carrying the host's dialId on onCall; a
|
|
1043
|
+
// lead's join, carrying assisting; a monitor, carrying monitoring -- was accepted by the command
|
|
1044
|
+
// that created it, and says so whatever the provisioning: the desk shows no Accept for a call
|
|
1045
|
+
// the agent placed. The provisioning governs work the queue routes, and nothing else.
|
|
1046
|
+
const originated = task.phase === "pending" && originatedByTheAgent(task);
|
|
1047
|
+
ruleEvaluated("task.acceptance.originated");
|
|
1035
1048
|
if (task.acceptance !== undefined) {
|
|
1036
1049
|
into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
|
|
1037
1050
|
if (task.phase !== "pending") {
|
|
1038
1051
|
into.add("task.acceptance.unexpected", `${path}.acceptance`, "acceptance is an offer's word; a task past pending has been accepted");
|
|
1039
1052
|
}
|
|
1053
|
+
else if (originated) {
|
|
1054
|
+
into.require(task.acceptance === "automatic", "task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a monitor -- was accepted by the command that created it, and says automatic whatever the provisioning");
|
|
1055
|
+
}
|
|
1040
1056
|
else if (context.autoAcceptTasks === false) {
|
|
1041
|
-
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task requires agent acceptance and a pending task carries no acceptance");
|
|
1057
|
+
into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task the queue routes requires agent acceptance and a pending task carries no acceptance");
|
|
1042
1058
|
}
|
|
1043
1059
|
}
|
|
1060
|
+
else if (originated) {
|
|
1061
|
+
into.add("task.acceptance.originated", `${path}.acceptance`, "work the agent originated -- a dial, a connect-back, a join, a monitor -- says acceptance: automatic whatever the provisioning: the command that created it accepted it");
|
|
1062
|
+
}
|
|
1044
1063
|
else if (task.phase === "pending" && context.autoAcceptTasks === true) {
|
|
1045
1064
|
into.add("task.acceptance.required", `${path}.acceptance`, "autoAcceptTasks is on, so a pending task states how it is accepted");
|
|
1046
1065
|
}
|
|
@@ -1469,6 +1488,9 @@ function validateTaskOutcome(value, path, into) {
|
|
|
1469
1488
|
}
|
|
1470
1489
|
break;
|
|
1471
1490
|
case "cancelled":
|
|
1491
|
+
// One word covered the agent declining, the provider withdrawing and the party abandoning the
|
|
1492
|
+
// ring; a record that cannot tell them apart is not a record. by says who, as completed does.
|
|
1493
|
+
into.oneOf(value.by, CANCELLED_BY, "event.taskEnded.outcome.cancelled.by", `${path}.by`);
|
|
1472
1494
|
if (value.reason !== undefined) {
|
|
1473
1495
|
into.filled(value.reason, "event.taskEnded.outcome.cancelled", `${path}.reason`, "a reason must not be empty when present");
|
|
1474
1496
|
}
|
package/guide.md
CHANGED
|
@@ -702,7 +702,7 @@ type AcceptanceMode =
|
|
|
702
702
|
type TaskOutcome =
|
|
703
703
|
| { type: "completed"; by: "agent" | "provider" }
|
|
704
704
|
| { type: "transferred"; destinationId?: string }
|
|
705
|
-
| { type: "cancelled"; reason?: string }
|
|
705
|
+
| { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
|
|
706
706
|
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
707
707
|
| { type: "left" }
|
|
708
708
|
| { type: "failed"; failure: ProtocolFailure };
|
|
@@ -2170,8 +2170,8 @@ person's behalf for want of a word:
|
|
|
2170
2170
|
| `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. |
|
|
2171
2171
|
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
2172
2172
|
|
|
2173
|
-
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task
|
|
2174
|
-
requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
2173
|
+
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task the
|
|
2174
|
+
queue routes requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
2175
2175
|
provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
|
|
2176
2176
|
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
2177
2177
|
press is Omni's doing, not the provider's.
|
|
@@ -2185,8 +2185,13 @@ belongs, whatever the host was configured with.
|
|
|
2185
2185
|
|
|
2186
2186
|
An automatically accepted task still arrives through `task-offered`.
|
|
2187
2187
|
|
|
2188
|
-
|
|
2189
|
-
`
|
|
2188
|
+
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2189
|
+
agent's own act — a dialpad call or a connect-back, recognisable by the host's `dialId` on
|
|
2190
|
+
`onCall`; a lead's join, carrying `assisting`; a lead's monitor, carrying `monitoring` — arrives
|
|
2191
|
+
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2192
|
+
validator holds it there under either provisioning and under none (`task.acceptance.originated`):
|
|
2193
|
+
the desk shows no **Accept** for a call the agent placed. The provisioning governs work the queue
|
|
2194
|
+
routes to the agent, and nothing else.
|
|
2190
2195
|
|
|
2191
2196
|
### Pending
|
|
2192
2197
|
|
|
@@ -2220,14 +2225,20 @@ on a desk phone — and is not in question per call. See **How the agent hears t
|
|
|
2220
2225
|
Automatic acceptance still begins with `task-offered`.
|
|
2221
2226
|
|
|
2222
2227
|
`allocationExpiresAt` is the deadline after which the offer lapses. Where present, Omni counts
|
|
2223
|
-
down and stops offering **Accept** once it passes
|
|
2228
|
+
down and stops offering **Accept** once it passes; the provider ends the lapsed offer with
|
|
2229
|
+
`task-ended` and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.**
|
|
2224
2230
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
2225
2231
|
to end, and a computed value would have Omni withdraw **Accept** from a task still pending.
|
|
2226
2232
|
|
|
2227
2233
|
A preview's deadline is not on the offer. It travels on the task, as `previewEndsAt` with
|
|
2228
2234
|
`atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
|
|
2229
2235
|
|
|
2230
|
-
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome
|
|
2236
|
+
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome, `by:
|
|
2237
|
+
"provider"`. **`cancelled` says who called the work off**, as `completed` says who completed it:
|
|
2238
|
+
`agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
|
|
2239
|
+
the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
|
|
2240
|
+
required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on before
|
|
2241
|
+
`allocationExpiresAt` is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
|
|
2231
2242
|
|
|
2232
2243
|
### Tasks already in progress
|
|
2233
2244
|
|
|
@@ -2309,7 +2320,8 @@ The canonical task transitions are:
|
|
|
2309
2320
|
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2310
2321
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2311
2322
|
| `preview` | The deadline `expires` | Removed by `task-ended` with `expired` outcome |
|
|
2312
|
-
| `pending` | Provider withdraws the allocation | Removed by `task-ended` with `cancelled` outcome |
|
|
2323
|
+
| `pending` | Provider withdraws the allocation, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2324
|
+
| `pending` | The offer lapses at `allocationExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2313
2325
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2314
2326
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2315
2327
|
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
@@ -2420,9 +2432,18 @@ whether the channel carries real-time media:
|
|
|
2420
2432
|
| Channel | Wrap allowance starts at |
|
|
2421
2433
|
| --- | --- |
|
|
2422
2434
|
| Voice and any channel with real-time media | The `task-media-ended` event |
|
|
2423
|
-
| Chat |
|
|
2424
|
-
| Email |
|
|
2425
|
-
| Other non-media channels | The
|
|
2435
|
+
| Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side, the allowance running from that publication's `occurredAt` |
|
|
2436
|
+
| Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message, the allowance running from that publication's `occurredAt` |
|
|
2437
|
+
| Other non-media channels | The `task-updated` that moves the task to `completing` |
|
|
2438
|
+
|
|
2439
|
+
**Off voice, `completing` is the provider's word that handling ended.** There is no media event to
|
|
2440
|
+
carry it, so the phase does; a host starts the wrap clock at that publication and nowhere else. It
|
|
2441
|
+
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2442
|
+
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
2443
|
+
required, because the clock has to start somewhere: a chat or email task the provider completes
|
|
2444
|
+
from `in-progress` with an allowance to run gave the agent none of it, and the stream refuses the
|
|
2445
|
+
ending (`stream.taskEnded.unwrapped`). Under `agent-command` the agent's `complete` is the end,
|
|
2446
|
+
from `in-progress` or from `completing` alike.
|
|
2426
2447
|
|
|
2427
2448
|
```ts
|
|
2428
2449
|
const emailCompletion = {
|
|
@@ -4268,8 +4289,8 @@ beside the mapped one is what the full check costs a host; an adapter has it for
|
|
|
4268
4289
|
validator holds a command only to a task that stands: a task handed in that is not one the wire
|
|
4269
4290
|
published is named (`command.task`) rather than checked against.
|
|
4270
4291
|
|
|
4271
|
-
Declining
|
|
4272
|
-
|
|
4292
|
+
Declining a pending offer ends it without accepting or completing it. The provider confirms the
|
|
4293
|
+
end with `task-ended` and a `cancelled` outcome, `by: "agent"`.
|
|
4273
4294
|
|
|
4274
4295
|
### `execute(request)`
|
|
4275
4296
|
|