@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 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 { 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) };
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;
@@ -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
- Agent-initiated work arrives through `task-offered` with the task's
2189
- `acceptance: "automatic"`.
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. **Omit it unless the provider can observe it.**
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 | When the conversation ends and the task enters `completing` |
2424
- | Email | After the message is sent and the task enters `completing` |
2425
- | Other non-media channels | The moment the task enters `completing` |
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 or rejecting a pending offer ends it without accepting or completing it. The provider
4272
- confirms the end with `task-ended` and a `cancelled` outcome.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",