@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 +15 -1
- package/dist/validation.js +11 -0
- package/guide.md +21 -2
- package/package.json +1 -1
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") {
|
package/dist/validation.js
CHANGED
|
@@ -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
|
|
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).
|
|
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.
|