@xema/omni-protocol 0.1.58 → 0.1.59

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/testing.js CHANGED
@@ -1053,7 +1053,21 @@ async function driveOneCall(drive) {
1053
1053
  }, cursor);
1054
1054
  if (mediaEnded !== undefined)
1055
1055
  cursor = mediaEnded.at;
1056
- await updated(t => t.phase === "completing", "the task completing after its media ended");
1056
+ if (await updated(t => t.phase === "completing", "the task completing after its media ended") !== undefined && offers("hold")) {
1057
+ // The other direction of step 4: the same control, still declared, on a call that is over.
1058
+ // A host holds it back (command.phase.handling), and an adapter that receives it anyway
1059
+ // must refuse it -- so the drive sends it past the validator and expects failed.
1060
+ let answer;
1061
+ try {
1062
+ answer = await drive.connection.execute({ taskId, command: { type: "hold" } });
1063
+ }
1064
+ catch (error) {
1065
+ refuse("drive.command.rejected", "drive.command.hold", `execute rejected rather than answered: ${String(error)}`);
1066
+ }
1067
+ if (isRecord(answer) && answer.status !== "failed") {
1068
+ refuse("drive.command.handling", "drive.command.hold", `the provider applied hold on a completing task: a control on the contact belongs to in-progress or paused, and the adapter is the second gate`);
1069
+ }
1070
+ }
1057
1071
  }
1058
1072
  }
1059
1073
  if (typeof session?.close === "function") {
@@ -1831,6 +1831,10 @@ export function validateTaskCommand(command, task, path = "command") {
1831
1831
  return true;
1832
1832
  };
1833
1833
  const inPhase = (rule, ...phases) => into.require(phases.includes(String(task.phase)), rule, path, `${String(type)} belongs to ${phases.join(" or ")}, and the task is ${String(task.phase)}`);
1834
+ // A control on the contact acts on a contact being handled. Before in-progress there is nothing
1835
+ // to act on yet, and in completing the handling has ended: a call with nobody on it, a
1836
+ // conversation closed. What a capability offers, the phase decides whether there is anything to use it on.
1837
+ const handling = () => inPhase("command.phase.handling", "in-progress", "paused");
1834
1838
  const onCall = Array.isArray(task.onCall) ? task.onCall.filter(isPlainObject) : [];
1835
1839
  switch (type) {
1836
1840
  case "answer":
@@ -1846,23 +1850,28 @@ export function validateTaskCommand(command, task, path = "command") {
1846
1850
  break;
1847
1851
  case "mute":
1848
1852
  offered("mute");
1853
+ handling();
1849
1854
  break;
1850
1855
  case "hold":
1851
1856
  case "resume":
1852
1857
  case "pause":
1853
1858
  offered("hold");
1859
+ handling();
1854
1860
  break;
1855
1861
  case "end-call":
1856
1862
  offered("endCall");
1863
+ handling();
1857
1864
  break;
1858
1865
  case "recording":
1859
1866
  offered("recording");
1867
+ handling();
1860
1868
  break;
1861
1869
  case "connect-back":
1862
1870
  offered("connectBack");
1863
1871
  inPhase("command.phase.completing", "completing");
1864
1872
  break;
1865
1873
  case "transfer":
1874
+ handling();
1866
1875
  if (command.action === "cold")
1867
1876
  offered("coldTransfer");
1868
1877
  else if (command.action === "warm")
@@ -1872,6 +1881,7 @@ export function validateTaskCommand(command, task, path = "command") {
1872
1881
  }
1873
1882
  break;
1874
1883
  case "lead-assist":
1884
+ handling();
1875
1885
  if (command.action === "request" || command.action === "cancel") {
1876
1886
  offered("leadAssist");
1877
1887
  if (command.action === "cancel") {
@@ -1883,6 +1893,7 @@ export function validateTaskCommand(command, task, path = "command") {
1883
1893
  }
1884
1894
  break;
1885
1895
  case "conference":
1896
+ handling();
1886
1897
  if (offered("conference") && command.action === "remove") {
1887
1898
  const others = onCall.filter(entry => entry.role !== "agent");
1888
1899
  if (command.party === true) {
package/guide.md CHANGED
@@ -2762,6 +2762,11 @@ const taskCapabilities = {
2762
2762
  } satisfies Pick<Task<"voice">, "channel" | "capabilities" | "browsers">;
2763
2763
  ```
2764
2764
 
2765
+ A capability says the control may be offered; the task's phase says whether there is anything to use
2766
+ it on. Every control below that acts on the call or the conversation -- everything but `decline`,
2767
+ `connectBack` and `dispositions` -- is offered only while the task is `in-progress` or `paused`.
2768
+ See **Which commands need a capability**.
2769
+
2765
2770
  ### Voice capabilities
2766
2771
 
2767
2772
  | Capability | Omni UI | Contract |
@@ -4009,6 +4014,17 @@ declared:
4009
4014
  | `lead-assist` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
4010
4015
  | Everything else | Its own named capability. |
4011
4016
 
4017
+ **A control on the contact belongs to the handling phases**, `in-progress` and `paused`: `mute`,
4018
+ `hold`, `resume` and `pause`, `end-call`, `recording`, every `transfer` and `conference` action, and
4019
+ every `lead-assist` action. Each acts on the call or the conversation, and only while there is one.
4020
+ Before `in-progress` nothing has been placed or opened; in `completing` the handling has ended -- a
4021
+ call with nobody on it, a conversation closed -- and a wrap-up that still shows Transfer shows it
4022
+ for nothing. The capability stays declared, because it is a property of the task and the task is
4023
+ still open; the phase says there is nothing to use it on. Omni shows none of these controls outside
4024
+ the two phases, and `validateTaskCommand` refuses each of them there (`command.phase.handling`).
4025
+ The commands with a phase of their own -- `answer`, `accept`, `decline` and `reject` in `pending`,
4026
+ `call` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
4027
+
4012
4028
  `validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
4013
4029
  capability it needs, the phase it belongs to, and the state that has to stand. The task it wants
4014
4030
  is the one the provider published, not a host's own mapping of it: a host that keeps only its
@@ -4377,7 +4393,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
4377
4393
  | `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
4378
4394
  | `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
4379
4395
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
4380
- | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
4396
+ | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.handling` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
4381
4397
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
4382
4398
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
4383
4399
 
@@ -4448,7 +4464,10 @@ complete it with a disposition where the agent completes -- and holds every step
4448
4464
  host holds a provider to. Each command is validated against the task as published
4449
4465
  (`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
4450
4466
  violation (`drive.command.failed`), and an event the provider owes and never sends is one too
4451
- (`drive.timeout`, after `driveTimeoutMs`, 5000 by default). A task the agent completes is
4467
+ (`drive.timeout`, after `driveTimeoutMs`, 5000 by default). Once the call has ended and the task is
4468
+ `completing`, the drive sends `hold` once more, past the validator that would hold it back, and
4469
+ expects `failed`: the adapter is the second gate on a control on the contact, and one that applies
4470
+ it on a call that is over is named (`drive.command.handling`). A task the agent completes is
4452
4471
  completed from wherever it stands once the drive has nothing left to do on it -- from `completing`
4453
4472
  after an `end-call`, or from `in-progress` where there is no call to end, which is every chat and
4454
4473
  email and a voice task offering no `endCall` -- so a conversation reaches its end as a call does.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.58",
3
+ "version": "0.1.59",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",