@xema/omni-protocol 0.1.32 → 0.1.34
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 +96 -16
- package/dist/index.js +18 -0
- package/dist/testing.d.ts +12 -8
- package/dist/testing.js +53 -14
- package/dist/validation.d.ts +3 -1
- package/dist/validation.js +130 -18
- package/guide.md +210 -68
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -35,6 +35,8 @@ are used precisely throughout and mean nothing looser here.
|
|
|
35
35
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
|
+
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a callback. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
39
|
+
| **On the call** | Who a voice task's audio joins right now, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in. |
|
|
38
40
|
|
|
39
41
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
40
42
|
one thing, so a bare "status" in conversation is always the authentication session's, and a
|
|
@@ -94,6 +96,7 @@ the semantic name in every contract field rather than repeating the primitive ty
|
|
|
94
96
|
type IsoTimestamp = string;
|
|
95
97
|
type UserId = string;
|
|
96
98
|
type TaskId = string;
|
|
99
|
+
type DialId = string;
|
|
97
100
|
type DurationSeconds = number;
|
|
98
101
|
```
|
|
99
102
|
|
|
@@ -162,6 +165,8 @@ type DialDestinations = "contacts-only" | "any-number";
|
|
|
162
165
|
|
|
163
166
|
type DialCapability = { destinations: DialDestinations };
|
|
164
167
|
|
|
168
|
+
type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled";
|
|
169
|
+
|
|
165
170
|
type IdleCapabilities<C extends Channel = Channel> = {
|
|
166
171
|
personalBrowser?: PersonalBrowserCapability;
|
|
167
172
|
calendar?: true;
|
|
@@ -178,6 +183,7 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
178
183
|
phaseLabels?: TaskPhaseLabels;
|
|
179
184
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
180
185
|
orgLevels?: LevelDeclaration[];
|
|
186
|
+
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
181
187
|
runningStepReports?: true;
|
|
182
188
|
};
|
|
183
189
|
```
|
|
@@ -414,11 +420,12 @@ type ScheduledActivity = {
|
|
|
414
420
|
};
|
|
415
421
|
|
|
416
422
|
type DialRequest = {
|
|
423
|
+
dialId: DialId;
|
|
417
424
|
destination: string;
|
|
418
425
|
};
|
|
419
426
|
|
|
420
427
|
type DialResult =
|
|
421
|
-
| { status: "
|
|
428
|
+
| { status: "dialling"; dialId: DialId }
|
|
422
429
|
| { status: "failed"; failure: ProtocolFailure };
|
|
423
430
|
```
|
|
424
431
|
|
|
@@ -570,6 +577,8 @@ type TaskHandlingHistory = {
|
|
|
570
577
|
type TaskHandlingStep = {
|
|
571
578
|
step: HandlingStep;
|
|
572
579
|
at: IsoTimestamp;
|
|
580
|
+
dialId?: DialId;
|
|
581
|
+
destination?: string;
|
|
573
582
|
seconds?: DurationSeconds;
|
|
574
583
|
by?: UserId;
|
|
575
584
|
};
|
|
@@ -578,11 +587,13 @@ type TaskCompletion =
|
|
|
578
587
|
| { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
|
|
579
588
|
| { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
|
|
580
589
|
|
|
581
|
-
type
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
590
|
+
type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
|
|
591
|
+
|
|
592
|
+
type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
593
|
+
| { role: "party" }
|
|
594
|
+
| { role: "agent"; userId: UserId }
|
|
595
|
+
| { role: "consulted" | "conferenced"; destination: string; dialId?: DialId; label?: string }
|
|
596
|
+
);
|
|
586
597
|
|
|
587
598
|
type TaskLead = {
|
|
588
599
|
stage: "requested" | "joined";
|
|
@@ -628,8 +639,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
628
639
|
handlingHistory?: TaskHandlingHistory;
|
|
629
640
|
} & TaskCompletion & (
|
|
630
641
|
C extends "voice"
|
|
631
|
-
? {
|
|
632
|
-
: {
|
|
642
|
+
? { onCall?: OnCall[]; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
|
|
643
|
+
: { onCall?: never; lead?: never; assisting?: never; media?: never }
|
|
633
644
|
);
|
|
634
645
|
|
|
635
646
|
type AcceptanceMode =
|
|
@@ -682,16 +693,17 @@ type VoiceTaskCommand =
|
|
|
682
693
|
| { type: "hold" }
|
|
683
694
|
| { type: "resume" }
|
|
684
695
|
| { type: "disconnect" }
|
|
685
|
-
| { type: "callback" }
|
|
686
|
-
| { type: "transfer"; destination: string; action?: never }
|
|
687
|
-
| { type: "transfer"; action: "consult"; destination: string }
|
|
696
|
+
| { type: "callback"; dialId: DialId }
|
|
697
|
+
| { type: "transfer"; dialId: DialId; destination: string; action?: never }
|
|
698
|
+
| { type: "transfer"; action: "consult"; dialId: DialId; destination: string }
|
|
688
699
|
| { type: "transfer"; action: "complete" }
|
|
689
700
|
| { type: "transfer"; action: "cancel" }
|
|
690
701
|
| { type: "lead"; action: "request"; note?: string }
|
|
691
702
|
| { type: "lead"; action: "cancel" }
|
|
692
703
|
| { type: "lead"; action: "take-over" }
|
|
693
704
|
| { type: "lead"; action: "leave" }
|
|
694
|
-
| { type: "conference";
|
|
705
|
+
| { type: "conference"; action: "add"; dialId: DialId; destination: string }
|
|
706
|
+
| { type: "conference"; action: "remove"; destination: string }
|
|
695
707
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
696
708
|
| ({ type: "complete" } & DispositionPayload);
|
|
697
709
|
|
|
@@ -720,6 +732,7 @@ type TaskCommandRequest<C extends Channel = Channel> = {
|
|
|
720
732
|
|
|
721
733
|
type TaskCommandResult =
|
|
722
734
|
| { status: "applied" }
|
|
735
|
+
| { status: "dialling"; dialId: DialId }
|
|
723
736
|
| { status: "failed"; failure: ProtocolFailure };
|
|
724
737
|
```
|
|
725
738
|
|
|
@@ -967,6 +980,7 @@ type ProviderEvent =
|
|
|
967
980
|
| { type: "task-media-started"; taskId: TaskId }
|
|
968
981
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
969
982
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
983
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destination?: string; reason?: string }
|
|
970
984
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
971
985
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
972
986
|
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
@@ -1296,11 +1310,12 @@ already committed stops an agent who is already stopped. A command whose second
|
|
|
1296
1310
|
cost — a dial places a second call — is not made safe by anyone, which is why it is never repeated
|
|
1297
1311
|
without a person deciding to.
|
|
1298
1312
|
|
|
1299
|
-
An agent dials. The provider places the call and the answer is lost. Omni shows the dial
|
|
1300
|
-
unknown, not failed. Within a moment the provider offers the resulting call through
|
|
1301
|
-
and the
|
|
1302
|
-
|
|
1303
|
-
|
|
1313
|
+
An agent dials. The provider places the call and the `dialling` answer is lost. Omni shows the dial
|
|
1314
|
+
as unknown, not failed. Within a moment the provider offers the resulting call through
|
|
1315
|
+
`task-offered` and reports the dial's `dial-outcome` under the `dialId` Omni sent, and the agent is
|
|
1316
|
+
on it; had nothing been placed, nothing arrives and the agent dials again. What Omni must not do is
|
|
1317
|
+
dial again on the agent's behalf — the one outcome worse than a lost answer is two phones ringing
|
|
1318
|
+
at the customer.
|
|
1304
1319
|
|
|
1305
1320
|
An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
|
|
1306
1321
|
adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
|
|
@@ -1377,6 +1392,7 @@ export default defineAdapter({
|
|
|
1377
1392
|
idleCapabilities: {
|
|
1378
1393
|
dial: { destinations: "any-number" },
|
|
1379
1394
|
},
|
|
1395
|
+
dialOutcomes: ["answered", "no-answer"],
|
|
1380
1396
|
},
|
|
1381
1397
|
createAuthenticationSession: context => createAcmeAuthentication(context),
|
|
1382
1398
|
connect: context => createConnection(context),
|
|
@@ -1401,6 +1417,7 @@ compile time.
|
|
|
1401
1417
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1402
1418
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1403
1419
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1420
|
+
| `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
|
|
1404
1421
|
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1405
1422
|
|
|
1406
1423
|
### Authentication methods
|
|
@@ -1471,9 +1488,9 @@ aggregated from every provider, but a `contacts-only` dialpad offers only the en
|
|
|
1471
1488
|
contributed. Any other reading lets a second provider put destinations into a directory the first
|
|
1472
1489
|
restricted precisely so it could control what was dialled.
|
|
1473
1490
|
|
|
1474
|
-
A declared dial capability requires `Connection.dial()
|
|
1475
|
-
|
|
1476
|
-
|
|
1491
|
+
A declared dial capability requires `Connection.dial()` and a `dialOutcomes` declaration, since a
|
|
1492
|
+
dialpad dials. Omni sends the destination as the agent selected or typed it, under a `dialId` of
|
|
1493
|
+
its own minting, and the provider holds it to the declared `destinations`.
|
|
1477
1494
|
|
|
1478
1495
|
#### Personal Browser (`personalBrowser`)
|
|
1479
1496
|
|
|
@@ -1898,7 +1915,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1898
1915
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
1899
1916
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
1900
1917
|
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. |
|
|
1901
|
-
| `dial(request)` | The manifest declares `idleCapabilities.dial`. |
|
|
1918
|
+
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
1902
1919
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
1903
1920
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
1904
1921
|
| `cancelBreak()` | The login declares `capabilities.breaks`. |
|
|
@@ -2112,7 +2129,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2112
2129
|
| `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. |
|
|
2113
2130
|
| `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
|
|
2114
2131
|
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2115
|
-
| `
|
|
2132
|
+
| `onCall` | Voice only. Who is on the call right now, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial brought in -- with the `destination` dialled, the `dialId` where a host placed it, and `held: true` on anyone parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2116
2133
|
| `lead` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Consulting a lead**. |
|
|
2117
2134
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead` `take-over` and `leave` issuable. See **Consulting a lead**. |
|
|
2118
2135
|
|
|
@@ -2315,9 +2332,16 @@ duration is the `held` entry's `seconds`, and a desk that restarts its counter o
|
|
|
2315
2332
|
counting the wrong thing.
|
|
2316
2333
|
|
|
2317
2334
|
`muted` is there because Omni performs the mute rather than the provider — see **Where a command
|
|
2318
|
-
executes** — so without a step the
|
|
2335
|
+
executes** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2319
2336
|
account of a period the agent could not be heard.
|
|
2320
2337
|
|
|
2338
|
+
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2339
|
+
each end a dial, so the entry carries the `destination` it went to and, where a host placed it, the
|
|
2340
|
+
`dialId` the host minted; no other step dialled, and either field on one is refused
|
|
2341
|
+
(`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2342
|
+
by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
|
|
2343
|
+
up in the record, not discarded.
|
|
2344
|
+
|
|
2321
2345
|
Four rules a provider has to keep:
|
|
2322
2346
|
|
|
2323
2347
|
- **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
|
|
@@ -2391,6 +2415,12 @@ did ask for them forwards upstream, and how often, is its own business. The step
|
|
|
2391
2415
|
`handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
|
|
2392
2416
|
`recordStep` is required of a connection whose tasks declare `mute`, and answers `recorded`.
|
|
2393
2417
|
|
|
2418
|
+
**A host-performed leg still open when the task's media ends, or the task ends, is ended by the
|
|
2419
|
+
host at that instant** — `ended: true`, `seconds` to the end, the same `at` — since a provider
|
|
2420
|
+
left to close it would be guessing at a host-performed duration. Both ends are read off one clock,
|
|
2421
|
+
the host's, and a leg shorter than a second is reported as the second it was, never as the nought
|
|
2422
|
+
the record refuses.
|
|
2423
|
+
|
|
2394
2424
|
### Browser capability
|
|
2395
2425
|
|
|
2396
2426
|
`browsers` is available to voice, chat, and email tasks. When declared, Omni renders the task's
|
|
@@ -2528,7 +2558,7 @@ const taskCapabilities = {
|
|
|
2528
2558
|
| `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
|
|
2529
2559
|
| `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
|
|
2530
2560
|
| `consultLead` | Secondary menu item: Consult lead | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.lead`. See **Consulting a lead**. |
|
|
2531
|
-
| `conference` | Secondary button: Conference | Omni may
|
|
2561
|
+
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it. |
|
|
2532
2562
|
| `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
|
|
2533
2563
|
| `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
|
|
2534
2564
|
|
|
@@ -2577,7 +2607,7 @@ capabilities: {
|
|
|
2577
2607
|
| Field | Contract |
|
|
2578
2608
|
| --- | --- |
|
|
2579
2609
|
| `destinations` | Directory Omni renders, with unique `id` values. |
|
|
2580
|
-
| `address` | The value Omni sends as `
|
|
2610
|
+
| `address` | The value Omni sends as `destination` on a `transfer` or a `conference` command. |
|
|
2581
2611
|
| `kind` | Where the contact is going. See below. |
|
|
2582
2612
|
| `allowManualEntry` | Whether the agent may type a destination that is not in the directory. A directory with no destinations must allow manual entry, or the control has nothing to offer. |
|
|
2583
2613
|
|
|
@@ -2601,9 +2631,10 @@ from `blindTransfer` (a hand-over with nobody consulted) and from `conference` (
|
|
|
2601
2631
|
call): a queue may offer any of the three without the others, and each is declared on its own.
|
|
2602
2632
|
|
|
2603
2633
|
```ts
|
|
2604
|
-
// 1. Consult.
|
|
2605
|
-
// `
|
|
2606
|
-
|
|
2634
|
+
// 1. Consult. A dial: the provider parks the customer and calls the destination, answers
|
|
2635
|
+
// `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
|
|
2636
|
+
// while the call to it stands.
|
|
2637
|
+
{ type: "transfer", action: "consult", dialId: "dial-7f2", destination: "+14155550111" }
|
|
2607
2638
|
|
|
2608
2639
|
// 2a. Hand the customer to the consulted destination and leave.
|
|
2609
2640
|
{ type: "transfer", action: "complete" }
|
|
@@ -2613,21 +2644,25 @@ call): a queue may offer any of the three without the others, and each is declar
|
|
|
2613
2644
|
```
|
|
2614
2645
|
|
|
2615
2646
|
`consult` is gated by the `consultTransfer` capability and takes a destination exactly as a blind
|
|
2616
|
-
transfer does, from the same kind of directory.
|
|
2617
|
-
|
|
2618
|
-
destination
|
|
2619
|
-
|
|
2620
|
-
|
|
2647
|
+
transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
|
|
2648
|
+
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
2649
|
+
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
2650
|
+
is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
|
|
2651
|
+
one they could mean, which is why a task carries at most one `consulted` entry
|
|
2652
|
+
(`task.onCall.consulted.single`). A consultation that could be started but not finished would
|
|
2653
|
+
strand the customer and the destination both, which is why all three are commands and a provider
|
|
2654
|
+
that offers `consultTransfer` implements all three.
|
|
2621
2655
|
|
|
2622
2656
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
2623
2657
|
dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
|
|
2624
2658
|
the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
|
|
2625
2659
|
`transferred` outcome naming the destination. `applied` on `cancel` says the destination is
|
|
2626
|
-
dropped; the task returns to `in-progress` with `
|
|
2660
|
+
dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
|
|
2627
2661
|
provider's report of both, as it does for every command.
|
|
2628
2662
|
|
|
2629
|
-
A destination that does not answer is a
|
|
2630
|
-
|
|
2663
|
+
A destination that does not answer is a dial that ended: the provider reports its `dial-outcome`,
|
|
2664
|
+
returns the task to `in-progress` with no `consulted` entry, and the agent is back with the
|
|
2665
|
+
customer.
|
|
2631
2666
|
|
|
2632
2667
|
### Chat capabilities
|
|
2633
2668
|
|
|
@@ -2690,14 +2725,112 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
|
|
|
2690
2725
|
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
2691
2726
|
declares `dial`.
|
|
2692
2727
|
|
|
2728
|
+
- `dialId` is the host's identity for this dial, minted before the request leaves.
|
|
2693
2729
|
- `destination` is the original number selected or entered by the agent.
|
|
2694
2730
|
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
2695
2731
|
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
2696
|
-
- `
|
|
2732
|
+
- `dialling` says the request was accepted and the call is being placed, and restates the
|
|
2733
|
+
`dialId`. It says nothing about whether anyone will answer.
|
|
2697
2734
|
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
2698
2735
|
|
|
2699
|
-
The resulting call is offered through the normal `task-offered` event
|
|
2700
|
-
does not manufacture a task
|
|
2736
|
+
The resulting call is offered through the normal `task-offered` event, and how the dial ended
|
|
2737
|
+
arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
|
|
2738
|
+
inside Omni.
|
|
2739
|
+
|
|
2740
|
+
## Every dial has an outcome
|
|
2741
|
+
|
|
2742
|
+
Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
|
|
2743
|
+
`conference` `add`, and `callback`. Each is accepted or refused at once, and each then ends later
|
|
2744
|
+
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
2745
|
+
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
2746
|
+
`dialling`, then says how it ended, once.
|
|
2747
|
+
|
|
2748
|
+
**The host mints the identity.** Every command that dials carries a `dialId` the host made, unique
|
|
2749
|
+
across the installation, and it travels four ways so no leg is a lookup:
|
|
2750
|
+
|
|
2751
|
+
```ts
|
|
2752
|
+
declare const connection: Connection<"voice">;
|
|
2753
|
+
declare const taskId: TaskId;
|
|
2754
|
+
|
|
2755
|
+
// 1. Out, minted by the host.
|
|
2756
|
+
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destination: "+14155550111" } });
|
|
2757
|
+
|
|
2758
|
+
// 2. Back on the result, restated, so the host compares and confirms.
|
|
2759
|
+
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
2760
|
+
|
|
2761
|
+
// 3. On the outcome, however late, against the task it belonged to.
|
|
2762
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destination: "+14155550111", outcome: "no-answer", reason: "No route to destination" };
|
|
2763
|
+
|
|
2764
|
+
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
2765
|
+
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destination: "+14155550111" };
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
The identity is a correlation handle between host and provider and **is never shown to the
|
|
2769
|
+
agent**. A host holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
2770
|
+
mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
|
|
2771
|
+
its `handlingHistory`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
2772
|
+
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
2773
|
+
|
|
2774
|
+
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
2775
|
+
`{ status: "dialling", dialId }`, restating the host's identity (`result.dialId`,
|
|
2776
|
+
`result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
|
|
2777
|
+
nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
|
|
2778
|
+
`dialId` the host sent for that reason.
|
|
2779
|
+
|
|
2780
|
+
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
2781
|
+
there was one, the destination where the provider knows it, and how the dial ended -- from a closed
|
|
2782
|
+
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`. `answered` is stated
|
|
2783
|
+
rather than read off somebody appearing on the call, because a host that infers success cannot tell
|
|
2784
|
+
"reached" from "still ringing". `cancelled` is the dialler calling the dial off before anyone
|
|
2785
|
+
answered -- the agent hanging up while a consult still rings -- which is not a `no-answer` the
|
|
2786
|
+
destination never gave. `reason` is the switch's own words, optional, and shown to the agent
|
|
2787
|
+
attributed to the switch rather than to Omni.
|
|
2788
|
+
|
|
2789
|
+
**`taskId` is the task the dial was placed on**, resolved from the dial's own identity and never from
|
|
2790
|
+
whatever task the agent is looking at when the outcome arrives. That task may already have ended,
|
|
2791
|
+
and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
2792
|
+
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
|
|
2793
|
+
call it belonged to and never against the next one.
|
|
2794
|
+
|
|
2795
|
+
**`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
|
|
2796
|
+
claims, and both are true at once when a leg answers and never joins. A desk shows `answered` as the
|
|
2797
|
+
dial's conclusion and says "on the call" only when the person appears on `Task.onCall`; the obvious
|
|
2798
|
+
reading of `answered` -- that they are on the line -- is the one it does not make.
|
|
2799
|
+
|
|
2800
|
+
**A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
|
|
2801
|
+
provider that cannot tell busy from unreachable must not pick one. The manifest declares
|
|
2802
|
+
`dialOutcomes`, and a `dial-outcome` carries only a declared member
|
|
2803
|
+
(`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
|
|
2804
|
+
success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
|
|
2805
|
+
a failure (`manifest.dialOutcomes.failure`). A provider that dials at all declares it: an idle
|
|
2806
|
+
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `callback`,
|
|
2807
|
+
`blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
|
|
2808
|
+
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
2809
|
+
error there.
|
|
2810
|
+
|
|
2811
|
+
**Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
|
|
2812
|
+
right now, replaced whole with the task like every other field, and present when the provider
|
|
2813
|
+
knows the room:
|
|
2814
|
+
|
|
2815
|
+
```ts
|
|
2816
|
+
const onCall: OnCall[] = [
|
|
2817
|
+
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
2818
|
+
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
2819
|
+
{ role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", held: true, since: "2026-08-21T09:15:30Z" },
|
|
2820
|
+
];
|
|
2821
|
+
```
|
|
2822
|
+
|
|
2823
|
+
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
2824
|
+
neither carries a destination. `consulted` and `conferenced` were, so both carry the `destination`
|
|
2825
|
+
and, where a host's dial brought them, its `dialId`; a person the platform added itself carries
|
|
2826
|
+
none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2827
|
+
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
2828
|
+
never saw. A `conference` `remove` names the `destination` of the `conferenced` entry to drop.
|
|
2829
|
+
|
|
2830
|
+
The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
|
|
2831
|
+
they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
|
|
2832
|
+
belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
|
|
2833
|
+
agent's.
|
|
2701
2834
|
|
|
2702
2835
|
## Breaks
|
|
2703
2836
|
|
|
@@ -2948,36 +3081,36 @@ Omni offers the aggregate Break control only when every provider currently holdi
|
|
|
2948
3081
|
declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
|
|
2949
3082
|
knowingly permit partial availability.
|
|
2950
3083
|
|
|
2951
|
-
Omni coordinates one attempt as follows:
|
|
3084
|
+
Omni coordinates one break attempt as follows:
|
|
2952
3085
|
|
|
2953
|
-
1. Freeze the
|
|
2954
|
-
receive work. A provider joining during the attempt is given no capacity until it
|
|
2955
|
-
A provider whose authentication is `expired` is not one the agent can receive work
|
|
2956
|
-
not
|
|
2957
|
-
is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
2958
|
-
until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
2959
|
-
and work continues. `
|
|
3086
|
+
1. Freeze the providers the attempt asks: every connected provider from which the agent can
|
|
3087
|
+
currently receive work. A provider joining during the attempt is given no capacity until it
|
|
3088
|
+
finishes. A provider whose authentication is `expired` is not one the agent can receive work
|
|
3089
|
+
from and is not asked: nothing is asked of it, the break proceeds without it, and when the
|
|
3090
|
+
login is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
3091
|
+
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
3092
|
+
available and work continues. `assertBreakAttemptProviders` holds a host to this set.
|
|
2960
3093
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
2961
|
-
3. Send one `requestBreak` to every
|
|
3094
|
+
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-decision` or
|
|
2962
3095
|
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
2963
3096
|
causes Omni to take the cancel path.
|
|
2964
|
-
4. If every
|
|
2965
|
-
and send `commitBreak()` to
|
|
3097
|
+
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
3098
|
+
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
2966
3099
|
and reports `starting-after-task` or `in-effect`. The **commit bound** — ten seconds from the
|
|
2967
|
-
decision, tunable per deployment — decides who is kept: a
|
|
3100
|
+
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
2968
3101
|
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
2969
|
-
begins without it. `in-effect` decides `on-break`: Omni enters it once every kept
|
|
2970
|
-
reports `in-effect`. A kept
|
|
3102
|
+
begins without it. `in-effect` decides `on-break`: Omni enters it once every kept provider
|
|
3103
|
+
reports `in-effect`. A kept provider reporting `starting-after-task` has applied the commit
|
|
2971
3104
|
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
2972
3105
|
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
2973
|
-
5. If any
|
|
3106
|
+
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
2974
3107
|
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
2975
|
-
`cancelling-break`. Send `cancelBreak()` to every
|
|
3108
|
+
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
2976
3109
|
`awaiting-decision` or `granted`. Work continues during cancellation because no stop was
|
|
2977
|
-
committed. Return to `working` only after no
|
|
3110
|
+
committed. Return to `working` only after no provider retains either state.
|
|
2978
3111
|
|
|
2979
|
-
Commit and cancel are mutually exclusive decisions for one attempt. Once Omni chooses commit
|
|
2980
|
-
never rolls that attempt back: it is reconciled by snapshot until every
|
|
3112
|
+
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
3113
|
+
it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
|
|
2981
3114
|
A provider that reports `granted` must therefore preserve the request across reconnects and must
|
|
2982
3115
|
honour a later commit or cancel. This
|
|
2983
3116
|
durable promise prevents a provider from failing the commit after another provider has already
|
|
@@ -2986,7 +3119,7 @@ stopped the agent.
|
|
|
2986
3119
|
#### Why the commit phase is bounded and the decision is not
|
|
2987
3120
|
|
|
2988
3121
|
Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
|
|
2989
|
-
durable and cannot be rolled back, so a
|
|
3122
|
+
durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
|
|
2990
3123
|
or is uninstalled between granting and committing would hold Omni in `committing-break` with no
|
|
2991
3124
|
exit: the providers that did commit have already stopped the agent, and the agent is neither
|
|
2992
3125
|
working nor on a break.
|
|
@@ -2996,7 +3129,7 @@ on one platform while another keeps routing work to them — and **a provider Om
|
|
|
2996
3129
|
routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
|
|
2997
3130
|
for it costs the agent their break.
|
|
2998
3131
|
|
|
2999
|
-
Setting a
|
|
3132
|
+
Setting a provider aside is not a rollback and not a cancel. The commit stands and the
|
|
3000
3133
|
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
3001
3134
|
snapshot before anything else, and the snapshot decides:
|
|
3002
3135
|
|
|
@@ -3011,7 +3144,7 @@ snapshot before anything else, and the snapshot decides:
|
|
|
3011
3144
|
The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
|
|
3012
3145
|
no capacity until it is reconciled.
|
|
3013
3146
|
|
|
3014
|
-
**If the agent has already ended the break elsewhere, the attempt is over** and the returning
|
|
3147
|
+
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
3015
3148
|
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
3016
3149
|
would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
|
|
3017
3150
|
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
@@ -3020,7 +3153,7 @@ Omni may tell the agent which platforms the break has not yet reached, as it alr
|
|
|
3020
3153
|
break cannot be paired across every provider.
|
|
3021
3154
|
|
|
3022
3155
|
The decision phase needs no such bound, because nothing has stopped. Work continues throughout
|
|
3023
|
-
`requesting-break`, so a
|
|
3156
|
+
`requesting-break`, so a provider that never answers costs the agent a wait rather than their
|
|
3024
3157
|
availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
|
|
3025
3158
|
because no stop was ever committed.
|
|
3026
3159
|
|
|
@@ -3436,7 +3569,7 @@ declared:
|
|
|
3436
3569
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
3437
3570
|
| `callback` | The `callback` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
3438
3571
|
| `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
|
|
3439
|
-
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- `
|
|
3572
|
+
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
3440
3573
|
| `lead` with `action: "request"` or `"cancel"` | The `consultLead` capability. `cancel` needs a request standing -- `Task.lead` with status `requested`. |
|
|
3441
3574
|
| `lead` 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`. |
|
|
3442
3575
|
| Everything else | Its own named capability. |
|
|
@@ -3627,11 +3760,11 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
|
|
|
3627
3760
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
3628
3761
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
3629
3762
|
|
|
3630
|
-
For a multi-provider attempt, "every provider" is the
|
|
3631
|
-
entered `requesting-break`. Omni commits only after every
|
|
3763
|
+
For a multi-provider break attempt, "every provider" is the set of providers frozen when the
|
|
3764
|
+
attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
|
|
3632
3765
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
3633
|
-
`on-break` once every kept
|
|
3634
|
-
setting aside a
|
|
3766
|
+
`on-break` once every kept provider reports `in-effect`; the commit bound decides who is kept,
|
|
3767
|
+
setting aside a provider that has not applied the commit rather than holding a break that has
|
|
3635
3768
|
already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
|
|
3636
3769
|
multi-provider break**.
|
|
3637
3770
|
|
|
@@ -3688,6 +3821,15 @@ The `task-media-ended` event and the `completing` phase are likewise non-termina
|
|
|
3688
3821
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
3689
3822
|
same envelope ID is harmless.
|
|
3690
3823
|
|
|
3824
|
+
### `dial-outcome`
|
|
3825
|
+
|
|
3826
|
+
How a dial the host placed ended, once, either way, named by the `dialId` the host sent: `answered`,
|
|
3827
|
+
or one of the declared ways of not reaching the destination, with the switch's `reason` where it
|
|
3828
|
+
gave one. `taskId` names the task the dial was on, where there was one, and that task may already
|
|
3829
|
+
have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
|
|
3830
|
+
audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
|
|
3831
|
+
dial has an outcome**.
|
|
3832
|
+
|
|
3691
3833
|
### `announcement`
|
|
3692
3834
|
|
|
3693
3835
|
Publishes an agent-facing message. `text` is always required and is the accessible fallback.
|
|
@@ -3874,7 +4016,7 @@ cannot be established from TypeScript structure alone.
|
|
|
3874
4016
|
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
|
|
3875
4017
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
|
|
3876
4018
|
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
|
|
3877
|
-
| `
|
|
4019
|
+
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
3878
4020
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
3879
4021
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
3880
4022
|
| `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
|