@xema/omni-protocol 0.1.106 → 0.1.108
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/README.md +2 -1
- package/dist/index.d.ts +54 -0
- package/dist/index.js +2 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +12 -2
- package/dist/validation.d.ts +8 -1
- package/dist/validation.js +94 -0
- package/guide/agent.md +1091 -0
- package/guide/breaks.md +540 -0
- package/guide/chat.md +12 -0
- package/guide/email.md +11 -0
- package/guide/lead.md +334 -0
- package/guide/phone.md +339 -0
- package/guide/voice.md +602 -0
- package/guide.md +74 -2866
- package/package.json +2 -1
package/guide.md
CHANGED
|
@@ -11,6 +11,23 @@ their contract type or use `satisfies` so the relationship between the example a
|
|
|
11
11
|
visible and compiler-checkable. Use another language only when the artifact itself is not
|
|
12
12
|
TypeScript.
|
|
13
13
|
|
|
14
|
+
## The guide in files
|
|
15
|
+
|
|
16
|
+
This file holds what every reader needs and what the others rely on: the terms, the shapes, the
|
|
17
|
+
contract rules, how an adapter is declared, signed in, connected and heard from, and how a
|
|
18
|
+
conformance run holds it to all of that. The rest is by role and by channel, one file each, and
|
|
19
|
+
every cross-reference names the file it points into.
|
|
20
|
+
|
|
21
|
+
| File | |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `guide/agent.md` | **The agent's work.** What an agent does on any channel: the assignment from offer to wrap, the task and its record, the controls a task offers, the commands the desk sends. |
|
|
24
|
+
| `guide/voice.md` | **Voice.** What is true of a call and of nothing else: preview, connecting back, ending a call, conference, every dial and its outcome, recording. |
|
|
25
|
+
| `guide/phone.md` | **The phone.** How the agent hears the call: softphone and desk phone, the station and its headset, opening the audio, the microphone and the record. |
|
|
26
|
+
| `guide/chat.md` | **Chat.** What is true of a chat and of nothing else. |
|
|
27
|
+
| `guide/email.md` | **Email.** What is true of an email and of nothing else. |
|
|
28
|
+
| `guide/breaks.md` | **Breaks.** Asking for a break, a break forced on the agent, capacity, and one break across several providers. |
|
|
29
|
+
| `guide/lead.md` | **Team leads.** The lead's own surface: the team member list, the lead's commands, lead assist, listening to a call. |
|
|
30
|
+
|
|
14
31
|
## Terms
|
|
15
32
|
|
|
16
33
|
**Define a vocabulary; do not merely list it** applies to this document's own prose. These words
|
|
@@ -28,10 +45,10 @@ are used precisely throughout and mean nothing looser here.
|
|
|
28
45
|
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
|
|
29
46
|
| **Assignment** | The call routed to this agent, from the offer until the task ends. It is the one thing that crosses: the provider assigns, names the assignment with `assignmentId`, and that is the only name the desk ever uses back. A call that comes back is a new assignment on the same call, and a declined or lapsed offer is an assignment that became nothing more. What the platform calls its own record, and whether it reuses that name, is the adapter's business and unknown to the desk. |
|
|
30
47
|
| **Interaction** | The agent's time on the call, from answer until their audio ends: the `in-progress` and `paused` phases. An assignment produces at most one interaction, and a call that returns produces a new assignment and so a new interaction. |
|
|
31
|
-
| **Task** | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
|
|
48
|
+
| **Task** in `guide/agent.md` | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
|
|
32
49
|
| **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
|
|
33
50
|
| **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
|
|
34
|
-
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes
|
|
51
|
+
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes** in `guide/agent.md`. |
|
|
35
52
|
| **Login** | One authenticated sign-in to one provider, identified by `loginId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `loginId` survives. |
|
|
36
53
|
| **Transport** | The adapter's connection to its platform: a WebSocket or SignalR connection, required to be persistent and ordered. Which one and how it reconnects are the adapter's business; losing it does not end a login. |
|
|
37
54
|
| **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
|
|
@@ -40,8 +57,8 @@ are used precisely throughout and mean nothing looser here.
|
|
|
40
57
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
41
58
|
| **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. |
|
|
42
59
|
| **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, team member list. |
|
|
43
|
-
| **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a preview's Call, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome
|
|
44
|
-
| **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; `listen` and `coach` leave no trace on the member's task, while a lead heard by everyone is on its `onCall` as an `agent`. Every lead sees every lead on the call. A lead who wants the call takes it over. See **Listening to a call
|
|
60
|
+
| **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a preview's Call, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome** in `guide/voice.md`. |
|
|
61
|
+
| **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; `listen` and `coach` leave no trace on the member's task, while a lead heard by everyone is on its `onCall` as an `agent`. Every lead sees every lead on the call. A lead who wants the call takes it over. See **Listening to a call** in `guide/lead.md`. |
|
|
45
62
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone conferenced in from the moment their dial is placed. |
|
|
46
63
|
|
|
47
64
|
These fields describe state within their containing objects. Both authentication and break
|
|
@@ -182,6 +199,24 @@ type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected
|
|
|
182
199
|
|
|
183
200
|
type Phone = "softphone" | "deskPhone";
|
|
184
201
|
|
|
202
|
+
type PhoneStatus = "ready" | "unregistered" | "do-not-disturb" | "off-hook";
|
|
203
|
+
|
|
204
|
+
type PhoneChannelState = "ringing" | "active" | "held";
|
|
205
|
+
|
|
206
|
+
type PhoneChannel = {
|
|
207
|
+
state: PhoneChannelState;
|
|
208
|
+
since: IsoTimestamp;
|
|
209
|
+
assignmentId?: AssignmentId;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
type PhoneState = {
|
|
213
|
+
phone: Phone;
|
|
214
|
+
status: PhoneStatus;
|
|
215
|
+
muted?: true;
|
|
216
|
+
since?: IsoTimestamp;
|
|
217
|
+
channels: PhoneChannel[];
|
|
218
|
+
};
|
|
219
|
+
|
|
185
220
|
type IdleCapabilities<C extends Channel = Channel> = {
|
|
186
221
|
personalBrowser?: PersonalBrowserCapability;
|
|
187
222
|
calendar?: true;
|
|
@@ -202,6 +237,7 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
202
237
|
orgLevels?: LevelDeclaration[];
|
|
203
238
|
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
204
239
|
phones?: C extends "voice" ? Phone[] : never;
|
|
240
|
+
phoneStatus?: C extends "voice" ? true : never;
|
|
205
241
|
runningStepReports?: true;
|
|
206
242
|
settleMs: number;
|
|
207
243
|
};
|
|
@@ -481,6 +517,7 @@ type Snapshot = {
|
|
|
481
517
|
contacts?: Contact[];
|
|
482
518
|
calendar?: ScheduledActivity[];
|
|
483
519
|
shift?: Shift;
|
|
520
|
+
phone?: PhoneState;
|
|
484
521
|
team?: TeamMembers;
|
|
485
522
|
};
|
|
486
523
|
|
|
@@ -917,7 +954,7 @@ the capability's own name because it is the same capability at another level: ef
|
|
|
917
954
|
command `hold` acts on one call; the preference `hold` says whether the person wants the control
|
|
918
955
|
at all, and an agent application renders it in its settings, never as the button on a call. Mute is in none of
|
|
919
956
|
these ladders: the microphone is the agent application's, and nobody on the provider's side allows, locks or
|
|
920
|
-
keeps a choice about it. See **The station is the agent application's
|
|
957
|
+
keeps a choice about it. See **The station is the agent application's** in `guide/phone.md`.
|
|
921
958
|
|
|
922
959
|
## Breaks
|
|
923
960
|
|
|
@@ -989,6 +1026,7 @@ type TeamMember = {
|
|
|
989
1026
|
listening?: MemberListening[];
|
|
990
1027
|
shift?: Shift;
|
|
991
1028
|
request?: MemberRequest;
|
|
1029
|
+
phone?: PhoneState;
|
|
992
1030
|
};
|
|
993
1031
|
|
|
994
1032
|
type MemberRequest = {
|
|
@@ -1145,7 +1183,8 @@ type ProviderEvent =
|
|
|
1145
1183
|
| { type: "team-policies-updated"; policies: TeamPolicies }
|
|
1146
1184
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1147
1185
|
| { type: "calendar-updated"; calendar: ScheduledActivity[] }
|
|
1148
|
-
| { type: "shift-updated"; shift: Shift }
|
|
1186
|
+
| { type: "shift-updated"; shift: Shift }
|
|
1187
|
+
| { type: "phone-updated"; phone: PhoneState };
|
|
1149
1188
|
|
|
1150
1189
|
type ProviderEventEnvelope = {
|
|
1151
1190
|
id: string;
|
|
@@ -1444,7 +1483,7 @@ The commands every task has are authorized by other fields the provider declared
|
|
|
1444
1483
|
offered can be accepted, one in `preview` can be called, any task under `agent-command` and one
|
|
1445
1484
|
with a wrap under `provider-automatic` can be completed.
|
|
1446
1485
|
Nothing is issuable that the provider did not publish; only
|
|
1447
|
-
which field says so varies. See **Which commands need a capability
|
|
1486
|
+
which field says so varies. See **Which commands need a capability** in `guide/agent.md`.
|
|
1448
1487
|
|
|
1449
1488
|
### 5. Snapshots establish state; events report transactions
|
|
1450
1489
|
|
|
@@ -1636,11 +1675,12 @@ compile time.
|
|
|
1636
1675
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1637
1676
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1638
1677
|
| `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**. |
|
|
1639
|
-
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call
|
|
1640
|
-
| `
|
|
1678
|
+
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call** in `guide/phone.md`. |
|
|
1679
|
+
| `phoneStatus` | Voice only, declared by presence as `true`: the platform sees the phone itself -- its registration, its do-not-disturb, its hook, its calls -- and publishes it as `Snapshot.phone` and `phone-updated`. A platform that cannot see the phone omits it, and then publishes no phone (`manifest.phoneStatus`). See **The phone's own view** in `guide/phone.md`. |
|
|
1680
|
+
| `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 conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome** in `guide/voice.md`. |
|
|
1641
1681
|
| `timeCheck` | `true`: implements `checkTime` and states `providerTime` on every snapshot. Required of a provider that publishes an instant the desk renders as a running duration -- a member's `since`, a `listening[].since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts from the provider's clock (`manifest.timeCheck.required`); agent application polling is independently opt-in. See **Every screen counts from the provider's clock**. |
|
|
1642
1682
|
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
|
|
1643
|
-
| `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs
|
|
1683
|
+
| `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs** in `guide/agent.md`. |
|
|
1644
1684
|
| `settleMs` | Required. How long after `applied` the wire shows what the provider did: the `task-ended` after a `complete`, the `calendar-updated` after a `schedule`. A positive whole number of milliseconds (`manifest.settleMs`). Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1645
1685
|
|
|
1646
1686
|
### Authentication methods
|
|
@@ -1773,7 +1813,7 @@ in the agent's view would show that it had happened.
|
|
|
1773
1813
|
`calendar` is available to voice, chat, and email providers. It contributes a calendar to the idle
|
|
1774
1814
|
dashboard for callbacks and other scheduled activities associated with that provider. Calendar is
|
|
1775
1815
|
the provider's: Omni displays activities and, where a task offers `schedule`, asks the provider to
|
|
1776
|
-
create one -- see **Scheduling a follow-up** -- but cannot reschedule, cancel, or complete them.
|
|
1816
|
+
create one -- see **Scheduling a follow-up** in `guide/agent.md` -- but cannot reschedule, cancel, or complete them.
|
|
1777
1817
|
|
|
1778
1818
|
```ts
|
|
1779
1819
|
idleCapabilities: { calendar: true }
|
|
@@ -1896,7 +1936,7 @@ Creates the provider-scoped authentication session.
|
|
|
1896
1936
|
| `protocolVersion` | The negotiated version, fixed for this login. |
|
|
1897
1937
|
| `loginId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.loginId`, and how an adapter ties a connection back to the session that authenticated it. |
|
|
1898
1938
|
| `timeZone` | The zone the agent's day is reckoned in, as an IANA name from the agent application's clock, stated before any identity exists so the first `authenticated` state already carries it. The same value Omni later passes as `ConnectContext.timeZone`. See **The agent's day**. |
|
|
1899
|
-
| `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call
|
|
1939
|
+
| `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call** in `guide/phone.md`. |
|
|
1900
1940
|
| `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
|
|
1901
1941
|
| `signal` | Optional cancellation signal. |
|
|
1902
1942
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -1950,7 +1990,7 @@ to infer them from what arrives later.
|
|
|
1950
1990
|
| Field | Contract |
|
|
1951
1991
|
| --- | --- |
|
|
1952
1992
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1953
|
-
| `lead` | This login leads a team. The flag alone turns on the team feature in the agent application, and every lead act comes with it; there is no per-action permission beside it. While the lead has the feature on, the provider publishes a `TeamMembers` object to them on every snapshot — `members: []` when nobody is in it — and to nobody else. Requires `executeTeam`. See **Team leads
|
|
1993
|
+
| `lead` | This login leads a team. The flag alone turns on the team feature in the agent application, and every lead act comes with it; there is no per-action permission beside it. While the lead has the feature on, the provider publishes a `TeamMembers` object to them on every snapshot — `members: []` when nobody is in it — and to nobody else. Requires `executeTeam`. See **Team leads** in `guide/lead.md`. |
|
|
1954
1994
|
| `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
|
|
1955
1995
|
|
|
1956
1996
|
A session action is available only when both the capability and Omni local policy permit it.
|
|
@@ -2103,9 +2143,9 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2103
2143
|
| `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
|
|
2104
2144
|
| `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2105
2145
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2106
|
-
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's
|
|
2146
|
+
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's** in `guide/phone.md`. |
|
|
2107
2147
|
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2108
|
-
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides
|
|
2148
|
+
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides** in `guide/phone.md`. |
|
|
2109
2149
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
2110
2150
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
2111
2151
|
|
|
@@ -2166,13 +2206,14 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2166
2206
|
| `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
|
|
2167
2207
|
| `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
|
|
2168
2208
|
| `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
|
|
2209
|
+
| `phone` | The phone as the platform sees it -- the device, whether it can take a call, its own mute where observed -- from a provider whose manifest declares `phoneStatus`; required there (`snapshot.phone.required`) and forbidden otherwise (`snapshot.phone.unexpected`). Replaced whole by `phone-updated`. See **The phone's own view** in `guide/phone.md`. |
|
|
2169
2210
|
| `shift` | The agent's own day so far, as the provider counts it -- the same `Shift` their lead sees on the team member list, the same numbers from the same count. Replaced whole by `shift-updated`. Omitted only where the provider cannot say. See **The agent's day is on the wire**. |
|
|
2170
2211
|
| `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
|
|
2171
2212
|
|
|
2172
2213
|
## Live connection
|
|
2173
2214
|
|
|
2174
2215
|
`Connection` is what `connect()` returns. Its methods are documented in the sections that follow
|
|
2175
|
-
and under **Breaks
|
|
2216
|
+
and under **Breaks** in `guide/breaks.md`, **Team leads** in `guide/lead.md`, **Real-time audio** in `guide/phone.md` and **Task commands** in `guide/agent.md`; this is the whole
|
|
2176
2217
|
surface in one place, and what obliges an adapter to implement each one.
|
|
2177
2218
|
|
|
2178
2219
|
| Method | Implement it when |
|
|
@@ -2183,21 +2224,21 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2183
2224
|
| `disconnect()` | Always. |
|
|
2184
2225
|
| `refused(report)` | Always. The agent application tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the agent application does with what it refuses**. |
|
|
2185
2226
|
| `setCapacity(capacity)` | Always. Nothing may be assigned until a capacity is stated, so there is no connection that does not receive it. |
|
|
2186
|
-
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability
|
|
2227
|
+
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability** in `guide/agent.md`. |
|
|
2187
2228
|
| `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by` where a person forced it, a team member list, or `history[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2188
2229
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2189
2230
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2190
2231
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
2191
2232
|
| `cancelBreak()` | The login declares `capabilities.breaks`. |
|
|
2192
2233
|
| `endBreak()` | The login declares `capabilities.breaks`. |
|
|
2193
|
-
| `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands
|
|
2234
|
+
| `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands** in `guide/lead.md`. |
|
|
2194
2235
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
2195
|
-
| `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the agent application mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The agent application records what it performs
|
|
2236
|
+
| `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the agent application mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The agent application records what it performs** in `guide/agent.md`. |
|
|
2196
2237
|
| `openAudio(request)` | The manifest lists `softphone` among its `phones`. On a softphone the call's audio lands in Omni, so the adapter has to open it; a platform of desk phones alone never does. |
|
|
2197
2238
|
|
|
2198
2239
|
**The four break methods stand or fall together.** Declaring `capabilities.breaks` at login and then
|
|
2199
2240
|
implementing `requestBreak` without `commitBreak` leaves an agent granted a break that can never
|
|
2200
|
-
start, and the two-phase coordination in **Coordinating a multi-provider break** has no way to
|
|
2241
|
+
start, and the two-phase coordination in **Coordinating a multi-provider break** in `guide/breaks.md` has no way to
|
|
2201
2242
|
report that: `granted` is a promise to honour a later commit.
|
|
2202
2243
|
|
|
2203
2244
|
### `Connection.snapshot()`
|
|
@@ -2267,2699 +2308,6 @@ Stops the connection and releases adapter-owned resources.
|
|
|
2267
2308
|
- Must remove event handlers and release audio resources owned by the adapter.
|
|
2268
2309
|
- Does not imply that active tasks were completed or removed.
|
|
2269
2310
|
|
|
2270
|
-
## Task assignment lifecycle
|
|
2271
|
-
|
|
2272
|
-
The task-assignment lifecycle has five ordered stages:
|
|
2273
|
-
|
|
2274
|
-
**1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
|
|
2275
|
-
until the agent application has stated a capacity, and ready once it has. A provider assigns nothing before a
|
|
2276
|
-
capacity is stated, and needs no other word for it.
|
|
2277
|
-
|
|
2278
|
-
**2. The agent becomes ready.** The agent application states the capacity when its local policy says: at once,
|
|
2279
|
-
or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
|
|
2280
|
-
break does not imply readiness. After that, an agent stops taking work on this provider only by a
|
|
2281
|
-
break under `capabilities.breaks`, or by the agent application restating `count: 0` -- which is how
|
|
2282
|
-
a forced break on another provider reaches this one; a login that declares no breaks has no
|
|
2283
|
-
not-ready of its own, and `count: 0` is the agent application's division of capacity, not the
|
|
2284
|
-
agent's choice.
|
|
2285
|
-
|
|
2286
|
-
**3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
|
|
2287
|
-
much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
|
|
2288
|
-
capacity every time it changes, in either direction, and does not ask again while that value holds.
|
|
2289
|
-
Silence keeps the last one in force, so a provider that waits to be asked a second time will never
|
|
2290
|
-
deliver again once it has gone quiet. Hold the capacity and deliver when work arrives.
|
|
2291
|
-
|
|
2292
|
-
**4. A provider offers a task.** The provider emits `task-offered` within the stated capacity, and
|
|
2293
|
-
the task is `pending`: an offer introduces work nobody has accepted, so it is the only phase an
|
|
2294
|
-
offer introduces (`event.taskOffered.phase`). `confirmed`, `preview` and the rest are reached from
|
|
2295
|
-
it, or carried on a snapshot.
|
|
2296
|
-
|
|
2297
|
-
**5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
|
|
2298
|
-
agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
|
|
2299
|
-
intent.
|
|
2300
|
-
|
|
2301
|
-
### Acceptance modes
|
|
2302
|
-
|
|
2303
|
-
During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
|
|
2304
|
-
provider states `acceptance` on each pending task — on the task rather than the offer, so a
|
|
2305
|
-
reconnect snapshot says it too and an offer the agent application never received is not accepted on the
|
|
2306
|
-
person's behalf for want of a word:
|
|
2307
|
-
|
|
2308
|
-
| Directive | Contract |
|
|
2309
|
-
| --- | --- |
|
|
2310
|
-
| `no-preference` | The provider leaves acceptance to Omni; with `autoAcceptTasks: true`, Omni accepts automatically. |
|
|
2311
|
-
| `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. An agent application that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
|
|
2312
|
-
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
2313
|
-
|
|
2314
|
-
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task the
|
|
2315
|
-
queue routes requires agent acceptance. The two are never confused on the wire: `consent` is always the
|
|
2316
|
-
provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
|
|
2317
|
-
no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
|
|
2318
|
-
press is Omni's doing, not the provider's.
|
|
2319
|
-
|
|
2320
|
-
**The value is stated, never assumed.** `autoAcceptTasks` is required of every connection: a
|
|
2321
|
-
agent application says which policy the deployment provisioned, and a validator that is not told checks
|
|
2322
|
-
neither rule rather than guess the permissive one (`task.acceptance.required` and
|
|
2323
|
-
`.unexpected` fire only against a stated value). `acceptance` is the provider's own control and
|
|
2324
|
-
outranks the policy: `consent` puts the decision back in the agent's hands for any task where it
|
|
2325
|
-
belongs, whatever the agent application was configured with.
|
|
2326
|
-
|
|
2327
|
-
An automatically accepted task still arrives through `task-offered`.
|
|
2328
|
-
|
|
2329
|
-
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2330
|
-
agent's own act — a dialpad call or a connect-back, recognisable by the agent application's `dialId` on
|
|
2331
|
-
`onCall`; a call a lead took over, carrying `takenOver` — arrives
|
|
2332
|
-
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2333
|
-
validator holds it there under either local policy and under none (`task.acceptance.originated`):
|
|
2334
|
-
the desk shows no **Accept** for a call the agent placed. The local policy governs work the queue
|
|
2335
|
-
routes to the agent, and nothing else.
|
|
2336
|
-
|
|
2337
|
-
### Pending
|
|
2338
|
-
|
|
2339
|
-
A task in the `pending` phase has been **offered to the agent and not yet accepted**. Omni applies
|
|
2340
|
-
`autoAcceptTasks` and the task's `acceptance` to decide whether acceptance is
|
|
2341
|
-
automatic or requires the agent. A provider that requires automatic acceptance still emits
|
|
2342
|
-
`task-offered`; it does not introduce new work as `in-progress`.
|
|
2343
|
-
|
|
2344
|
-
```ts
|
|
2345
|
-
declare const task: Task;
|
|
2346
|
-
|
|
2347
|
-
const assignment = {
|
|
2348
|
-
type: "task-offered",
|
|
2349
|
-
task: { ...task, phase: "pending", acceptance: "consent", expiresInSeconds: 30 },
|
|
2350
|
-
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2351
|
-
```
|
|
2352
|
-
|
|
2353
|
-
The rule the phase exists to express: **nothing is acquired on the agent's behalf while a task
|
|
2354
|
-
is pending.** An agent application that carries audio must not open the microphone until the task is
|
|
2355
|
-
accepted. Omni does not open the task's browsers either — a task that rings out costs nothing.
|
|
2356
|
-
|
|
2357
|
-
When consent is required, Omni offers the agent an **Accept** control. The call is the
|
|
2358
|
-
medium the task arrives on, not a separate decision.
|
|
2359
|
-
|
|
2360
|
-
**Once a task is accepted, the call that comes with it is answered.** Omni has no discretion
|
|
2361
|
-
there and the provider is not consulted twice: one decision about the work, and the medium
|
|
2362
|
-
follows it. Where the audio lands was settled at sign-in — in Omni on a softphone, on the handset
|
|
2363
|
-
on a desk phone — and is not in question per call. See **How the agent hears the call**.
|
|
2364
|
-
|
|
2365
|
-
Automatic acceptance still begins with `task-offered`.
|
|
2366
|
-
|
|
2367
|
-
`expiresInSeconds` is how long the offer has left, in whole seconds from the publication that
|
|
2368
|
-
carries it. It rides on the pending task, as `acceptance` does, so a reconnect snapshot restates
|
|
2369
|
-
it from what is actually left; it belongs to `pending` alone and is gone once the task is accepted
|
|
2370
|
-
(`task.pending.expiresInSeconds`, `.unexpected`). Where present, Omni counts down from receipt and
|
|
2371
|
-
stops offering **Accept** once it runs out; the provider ends the lapsed offer with `task-ended`
|
|
2372
|
-
and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.** A provider that reports only elapsed
|
|
2373
|
-
ring time after the fact cannot say how long an offer has left, and a computed value would have
|
|
2374
|
-
Omni withdraw **Accept** from a task still pending.
|
|
2375
|
-
|
|
2376
|
-
**A deadline is seconds left, never an instant.** The agent application and the provider keep
|
|
2377
|
-
different clocks, and a countdown that compares one against the other is a countdown that runs
|
|
2378
|
-
fast or slow by their skew. Seconds from the publication that carries them are the provider's own
|
|
2379
|
-
arithmetic, counted down on the desk from the moment they arrive, and the transport delay is the
|
|
2380
|
-
only error. Restated on every publication that carries them, so a snapshot after a reconnect
|
|
2381
|
-
starts the countdown afresh from what is actually left (`task.pending.expiresInSeconds`,
|
|
2382
|
-
`task.preview.previewEndsInSeconds`: a whole number, zero or more).
|
|
2383
|
-
|
|
2384
|
-
A preview's deadline is not on the offer. It travels on the task, as `previewEndsInSeconds` with
|
|
2385
|
-
`atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
|
|
2386
|
-
|
|
2387
|
-
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome, `by:
|
|
2388
|
-
"provider"`. **`cancelled` says who called the work off**, as `completed` says who completed it:
|
|
2389
|
-
`agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
|
|
2390
|
-
the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
|
|
2391
|
-
required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on while
|
|
2392
|
-
`expiresInSeconds` ran is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
|
|
2393
|
-
|
|
2394
|
-
### Tasks already in progress
|
|
2395
|
-
|
|
2396
|
-
The provider does not introduce new work as already `in-progress`.
|
|
2397
|
-
|
|
2398
|
-
A task appears already `in-progress` only in a snapshot taken after a reconnect or a resync,
|
|
2399
|
-
reporting work that began earlier in this login and never stopped. A fresh login has none to
|
|
2400
|
-
report: nothing has been assigned yet, and work the agent was working elsewhere is not carried
|
|
2401
|
-
into a new session.
|
|
2402
|
-
|
|
2403
|
-
## Tasks
|
|
2404
|
-
|
|
2405
|
-
### `Task`
|
|
2406
|
-
|
|
2407
|
-
`Task` is the provider-owned description of one task presented to Omni.
|
|
2408
|
-
|
|
2409
|
-
`task-offered` introduces a new task and does not imply acceptance.
|
|
2410
|
-
|
|
2411
|
-
`Task<C>` is channel-discriminated. For example, `Task<"email">` accepts
|
|
2412
|
-
`browsers` and `outcomes`, but rejects voice-only controls such as `hold` and `endCall` at compile
|
|
2413
|
-
time. Runtime conformance checks also require the task channel to match its provider manifest.
|
|
2414
|
-
|
|
2415
|
-
| Field | Contract |
|
|
2416
|
-
| --- | --- |
|
|
2417
|
-
| `assignmentId` | Required `AssignmentId`: the assignment this task is the record of, and its one identity. Issued by the provider, unique within the provider, never reused. Every event, command and report that names a task names it by this, so a late dial outcome or a late history report for one customer never lands on the next. The stream refuses an assignment introduced twice (`stream.taskOffered.duplicate`) and an audio or ending event naming one that has ended or that nobody has seen (`stream.assignment.ended`, `.unknown`); a `dial-outcome` may name an ended assignment, since a dial placed late routinely outlives its call, and the agent application routes it there. See **A task's life on the wire**. |
|
|
2418
|
-
| `title` | Agent-facing task title. |
|
|
2419
|
-
| `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
|
|
2420
|
-
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
2421
|
-
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2422
|
-
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `nobody` when nothing handed the work over -- an agent's own outbound -- and `not-yet-read` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. An agent application shows `not-yet-read` where the agent works. See **Task capabilities**. |
|
|
2423
|
-
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2424
|
-
| `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
|
|
2425
|
-
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2426
|
-
| `audio` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-audio-started`**. Audio names a task whose work has begun, or whose party the agent application is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.audio.beforeWork`), on a snapshot as on the event, since an agent application opens the microphone on it; with the party ringing by an agent application dial or provider-triggered preview dial, actual ring-back audio may precede answer. |
|
|
2427
|
-
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2428
|
-
| `expiresInSeconds` | In `pending` only: how long the offer has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the offer stands until the provider says otherwise. See **Pending**. |
|
|
2429
|
-
| `previewEndsInSeconds` | Voice only, in `preview`: how long the preparation has left, in whole seconds from this publication, restated on every publication that carries it. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2430
|
-
| `atDeadline` | Voice only, in `preview`, with `previewEndsInSeconds`: what the system does when it runs out -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
|
|
2431
|
-
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2432
|
-
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one, and takes the agent's `complete` as finishing early. A required outcome is the agent's to give, so it needs `agent-command` (`task.outcomes.required.mode`). |
|
|
2433
|
-
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time audio, it begins after `task-audio-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; stated, it is the expected wrap, shown and never acted on. `0` under `provider-automatic` is no wrap at all, so such a task publishes no `outcomes` (`task.outcomes.wrapAllowance`). |
|
|
2434
|
-
| `wrapEndsInSeconds` | In `completing`, wherever `wrapAllowance` is stated: how much of the wrap is left, in whole seconds from this publication, restated on every publication that carries it (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`). A reloaded desk and a lead's screen count down the same number. See **Completion timing**. |
|
|
2435
|
-
| `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. |
|
|
2436
|
-
| `history` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
|
|
2437
|
-
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2438
|
-
| `leadAssist` | 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 **Lead assist**. |
|
|
2439
|
-
| `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against their capacity like any other call. See **Lead assist**. |
|
|
2440
|
-
|
|
2441
|
-
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2442
|
-
|
|
2443
|
-
```ts
|
|
2444
|
-
const attributes: TaskAttribute[] = [
|
|
2445
|
-
{
|
|
2446
|
-
key: "related-contact",
|
|
2447
|
-
label: "Related contact",
|
|
2448
|
-
type: "contact",
|
|
2449
|
-
party: { name: "Asha Rao", number: "+919876543210" },
|
|
2450
|
-
},
|
|
2451
|
-
{
|
|
2452
|
-
key: "answered",
|
|
2453
|
-
label: "Answered",
|
|
2454
|
-
type: "timestamp",
|
|
2455
|
-
at: "2026-08-27T09:30:00.000Z",
|
|
2456
|
-
},
|
|
2457
|
-
];
|
|
2458
|
-
```
|
|
2459
|
-
|
|
2460
|
-
`key` is the stable machine identifier and must be unique within the task's `attributes` array.
|
|
2461
|
-
`label` is the optional agent-facing name.
|
|
2462
|
-
|
|
2463
|
-
The canonical task transitions are:
|
|
2464
|
-
|
|
2465
|
-
| From | Decision or event | To |
|
|
2466
|
-
| --- | --- | --- |
|
|
2467
|
-
| No task | Provider assigns a task | `pending` |
|
|
2468
|
-
| `pending` | Task is accepted | `confirmed` |
|
|
2469
|
-
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2470
|
-
| `confirmed` | Work begins | `in-progress` |
|
|
2471
|
-
| `preview` | Agent presses Call (`dial`) and the customer answers, or the deadline `provider-dials` and they answer | `in-progress` |
|
|
2472
|
-
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2473
|
-
| `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
|
|
2474
|
-
| `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2475
|
-
| `pending` | The offer's `expiresInSeconds` runs out | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2476
|
-
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2477
|
-
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2478
|
-
| `paused` | Provider or agent resumes the task | `in-progress` |
|
|
2479
|
-
| `in-progress` or `paused` | Contact interaction ends and follow-up work remains | `completing` |
|
|
2480
|
-
| `completing` | Agent connects back to the party (`connect-back`) | `in-progress` |
|
|
2481
|
-
| Any phase | Provider emits `task-ended` | Removed |
|
|
2482
|
-
|
|
2483
|
-
#### A task's life on the wire
|
|
2484
|
-
|
|
2485
|
-
A task has one name, the assignment it is the record of, and the provider issues it. Platforms
|
|
2486
|
-
reuse their own handles: a closed one is retired minutes later, a requeue takes seconds, and on
|
|
2487
|
-
one platform a call was offered twice, thirteen seconds apart, under one handle, through a close
|
|
2488
|
-
and a re-offer. Everything that names a task after the fact -- a dial outcome, an audio event, an
|
|
2489
|
-
ending, a history report, a command -- would land on whichever assignment is open when it arrives.
|
|
2490
|
-
So the assignment id is unique within the provider and never reused: where the platform's handle
|
|
2491
|
-
never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
|
|
2492
|
-
and keeps the mapping, and the desk never learns which. A late event finds the assignment it
|
|
2493
|
-
belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME`)
|
|
2494
|
-
lives one assignment, never the next customer's cookies. A dial the agent application placed was
|
|
2495
|
-
already safe, since its outcome is placed by the agent application's own `dialId`; where the
|
|
2496
|
-
assignment earns its place is everything the agent application does not mint --
|
|
2497
|
-
`task-audio-started`, `task-audio-ended`, `task-ended`, a `recordStep` naming a task -- any of which
|
|
2498
|
-
would otherwise find the next customer under a reused handle and act on them. A minted id is part
|
|
2499
|
-
of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
|
|
2500
|
-
`store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
|
|
2501
|
-
adapter comes back with the same assignment on the same task, or every late event it was minted to
|
|
2502
|
-
catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
|
|
2503
|
-
|
|
2504
|
-
Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2505
|
-
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2506
|
-
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
2507
|
-
command.
|
|
2508
|
-
|
|
2509
|
-
#### Preview: the agent presses Call
|
|
2510
|
-
|
|
2511
|
-
An outbound campaign that lets the agent see who they are about to call is a **preview**: the
|
|
2512
|
-
record arrives as a task and sits in `preview` with the party on it before a call goes
|
|
2513
|
-
out. The agent prepares; manual or declared automatic initiation may then begin dialing. It
|
|
2514
|
-
remains preview while ringing until an evidenced answer or non-answer outcome. The agent may press Call. That is the whole phase, and it exists on voice alone --
|
|
2515
|
-
chat and email have no call to place, and their reading is ordinary work in `in-progress`.
|
|
2516
|
-
|
|
2517
|
-
```ts
|
|
2518
|
-
const previewed = {
|
|
2519
|
-
channel: "voice",
|
|
2520
|
-
capabilities: {},
|
|
2521
|
-
phase: "preview",
|
|
2522
|
-
party: { name: "Maya Rao", number: "+919876543210" },
|
|
2523
|
-
previewEndsInSeconds: 120,
|
|
2524
|
-
atDeadline: "provider-dials",
|
|
2525
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsInSeconds" | "atDeadline">;
|
|
2526
|
-
|
|
2527
|
-
// The agent presses Call. It is a dial like any other.
|
|
2528
|
-
const pressed: TaskCommand<"voice"> = { type: "dial", dialId: "dial-7f2" };
|
|
2529
|
-
```
|
|
2530
|
-
|
|
2531
|
-
**Call is a dial.** It carries the agent application's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2532
|
-
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
2533
|
-
agent is there to be called. Its audio starts on `dialling`, as every agent application-placed dial's does --
|
|
2534
|
-
ring-back is audio the agent hears -- so `task-audio-started` arrives on the `preview` task with
|
|
2535
|
-
its party ringing. On `answered` the task is `in-progress`; on any other outcome the audio ends,
|
|
2536
|
-
`task-audio-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
2537
|
-
`completing`, so the agent records the
|
|
2538
|
-
no-answer as the outcome it is -- in a campaign that is the commonest outcome there is, and it
|
|
2539
|
-
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2540
|
-
(`task.preview.dialOutcomes.required`).
|
|
2541
|
-
|
|
2542
|
-
**Preparation and trigger ownership are per task.** Unlimited preparation omits both
|
|
2543
|
-
`previewEndsInSeconds` and `atDeadline`: only an agent pressing Call starts dialing. Fixed
|
|
2544
|
-
preparation carries the seconds the provider says are left, counted down on the desk from the
|
|
2545
|
-
publication that carried them and restated on every later one, and exactly one trigger owner:
|
|
2546
|
-
|
|
2547
|
-
| Declaration | At preparation end |
|
|
2548
|
-
| --- | --- |
|
|
2549
|
-
| `atDeadline: "provider-dials"` | The provider initiates dialing. The agent application never sends a timer-triggered Call. |
|
|
2550
|
-
| `atDeadline: "host-dials"` | The agent application sends the ordinary `dial` command with a fresh agent application `dialId`. The provider does not independently auto-dial this task. |
|
|
2551
|
-
| `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
|
|
2552
|
-
|
|
2553
|
-
The agent may press Call early in either fixed-preparation dialing mode, or at any time in
|
|
2554
|
-
`waits` mode. With `waits`, the countdown is a preparation target, not an expiry: when it reaches
|
|
2555
|
-
zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
|
|
2556
|
-
completion, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
|
|
2557
|
-
from unlimited preparation only by displaying a preparation target. The former `expires` deadline
|
|
2558
|
-
value is not supported; elapsed preparation must not withdraw the task. For `provider-dials` and `host-dials`, the deadline ends
|
|
2559
|
-
preparation and triggers initiation; it does not promise ringing, playable audio or customer
|
|
2560
|
-
answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
|
|
2561
|
-
prevented initiation must be reported rather than leaving a silently expired countdown.
|
|
2562
|
-
A source eligibility threshold that may never trigger an attempt is not this promise.
|
|
2563
|
-
|
|
2564
|
-
The two deadline fields travel together and only in preview. The countdown is the seconds the
|
|
2565
|
-
publication carried, counted from its receipt: the agent application infers no deadline where none
|
|
2566
|
-
was stated and reads no clock against another. Neither elapsed time nor submission changes the
|
|
2567
|
-
task phase: subsequent authoritative events state ringing, answer, audio and completion.
|
|
2568
|
-
|
|
2569
|
-
An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
|
|
2570
|
-
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
2571
|
-
assignment and whether an attempt already exists before dispatch. A new snapshot or policy update
|
|
2572
|
-
cancels an obsolete timer; login loss, disconnect, task end and assignment replacement fence its
|
|
2573
|
-
callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
|
|
2574
|
-
blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
|
|
2575
|
-
The provider atomically arbitrates any residual manual/timer race and does not accept a second
|
|
2576
|
-
command as ownership of an already-running independent attempt.
|
|
2577
|
-
|
|
2578
|
-
Agent application-triggered automatic dialing is an ordinary agent application dial: its result is `dialling` with the same
|
|
2579
|
-
agent application `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
|
|
2580
|
-
dial has no invented agent application ID or agent application dial outcome. It reports source-evidenced party ringing,
|
|
2581
|
-
actual audio and customer answer separately. In preview with `atDeadline: "provider-dials"`, a ringing
|
|
2582
|
-
party without an agent application ID may carry actual pre-answer audio; the deadline alone permits no audio.
|
|
2583
|
-
Audio is still introduced/ended by the corresponding events and must not imply customer answer.
|
|
2584
|
-
Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
|
|
2585
|
-
Unsupported source correlation/outcome/audio evidence remains an integration blocker.
|
|
2586
|
-
|
|
2587
|
-
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
2588
|
-
spreading the `preview` one carries `previewEndsInSeconds` and `atDeadline` with it, and the agent application refuses
|
|
2589
|
-
the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
|
|
2590
|
-
provider is told through `refused` exactly which rule, so the state that looked right on its side
|
|
2591
|
-
is named on its side. The task past preview has no deadline to wait for, so it carries neither
|
|
2592
|
-
field.
|
|
2593
|
-
|
|
2594
|
-
**Provider execution and agent application controls are distinct.** Answer, hold/resume, conference,
|
|
2595
|
-
lead assist and interaction completion are executed by the provider
|
|
2596
|
-
through the adapter. The task's applicable capability, phase, completion mode and command-specific
|
|
2597
|
-
prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is agent application-owned, never a provider
|
|
2598
|
-
TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
|
|
2599
|
-
recording contract; this distinction does not remove it or move provider call operations to the agent application.
|
|
2600
|
-
|
|
2601
|
-
A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
|
|
2602
|
-
own assignment, and two open at once carry two assignment ids. Restore them unchanged on
|
|
2603
|
-
reconnect. Commands, audio, endings and history reports name the exact assignment; never retarget
|
|
2604
|
-
an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
|
|
2605
|
-
request's assignment against the supplied published task and then its command prerequisites. The
|
|
2606
|
-
caller must select that task within the correct provider/login and recheck at the provider; the
|
|
2607
|
-
validator cannot prove source freshness or authorization.
|
|
2608
|
-
`task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
|
|
2609
|
-
portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
|
|
2610
|
-
and active interactions; task completion is not caller hangup.
|
|
2611
|
-
|
|
2612
|
-
**Voice describes the agent's interaction within the wider call.** The task defines that agent's
|
|
2613
|
-
workspace, tools, permissions, audio participation and completion work. The caller's channel may
|
|
2614
|
-
continue through IVR, queues, other agents, holds or conferences while this interaction ends or wraps.
|
|
2615
|
-
Completing this task ends this interaction responsibility; it is not evidence that the caller's channel
|
|
2616
|
-
or journey ended. Task audio describes this agent's attachment, not the lifetime of every party's
|
|
2617
|
-
connection. Commands retain their explicit targets and effects; task completion must not silently
|
|
2618
|
-
become a caller-disconnect operation.
|
|
2619
|
-
|
|
2620
|
-
**A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
|
|
2621
|
-
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2622
|
-
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2623
|
-
conference leg joining or leaving, a lead joining, a take-over, a connect-back — the audio follows
|
|
2624
|
-
separately, arriving on `task-audio-started`, attaching through `openAudio` and ending with
|
|
2625
|
-
`task-audio-ended`. Omni does not ring,
|
|
2626
|
-
bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
|
|
2627
|
-
are the adapter's and the platform's, transient, and decide neither when a task exists nor what
|
|
2628
|
-
phase it is in.
|
|
2629
|
-
|
|
2630
|
-
The line runs between the provider's word and Omni's own senses. `task-audio-ended` is the
|
|
2631
|
-
provider's report that primary interaction ended — a fact about the work, which is why the completion
|
|
2632
|
-
allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
|
|
2633
|
-
follows any other. What Omni never does is derive a task's state from its own audio session: a
|
|
2634
|
-
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
2635
|
-
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
2636
|
-
`task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
|
|
2637
|
-
only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
|
|
2638
|
-
task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
|
|
2639
|
-
and `assertAudioFollowsTheTask` holds any sequence.
|
|
2640
|
-
|
|
2641
|
-
#### Completion timing
|
|
2642
|
-
|
|
2643
|
-
`completionMode` determines how completion is triggered. With `agent-command`, the provider keeps
|
|
2644
|
-
the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
|
|
2645
|
-
provider may complete the task without receiving that command.
|
|
2646
|
-
|
|
2647
|
-
**The agent may finish early under either mode.** `complete` is issuable wherever there is a wrap
|
|
2648
|
-
to cut short: under `agent-command` it is the end, and under `provider-automatic` it says the agent
|
|
2649
|
-
is done before the allowance ran out, and the provider is free to end the task at once. Only a task
|
|
2650
|
-
with no wrap at all -- `provider-automatic` with `wrapAllowance: 0` -- has nothing to complete, and
|
|
2651
|
-
the command is refused there (`command.complete.wrapAllowance`). An ending the agent asked for says
|
|
2652
|
-
so, `completed` with `by: "agent"`, whichever mode the task was under.
|
|
2653
|
-
|
|
2654
|
-
**A required outcome is the agent's to give.** A task whose `outcomes` says `required: true` waits
|
|
2655
|
-
for a code the agent chooses, so it completes on the agent's command: `provider-automatic` cannot
|
|
2656
|
-
wait for it, and the pair is refused (`task.outcomes.required.mode`). Optional outcomes may ride on
|
|
2657
|
-
a task the provider completes itself; the agent gives one if in time. And a code is given in wrap,
|
|
2658
|
-
so a `provider-automatic` task with `wrapAllowance: 0` publishes no `outcomes` at all
|
|
2659
|
-
(`task.outcomes.wrapAllowance`).
|
|
2660
|
-
|
|
2661
|
-
**The allowance means one thing per mode.** Under `provider-automatic` it is what the provider
|
|
2662
|
-
acts on: the task ends when it runs out. Under `agent-command` it is the expected wrap, stated so
|
|
2663
|
-
the agent can see it: the desk counts it down and, past zero, shows how far over they are, and
|
|
2664
|
-
nothing acts on it -- the task waits for `complete` however long that takes, with its outcomes
|
|
2665
|
-
collected whenever the agent gives them. A provider that would end the task at a time is
|
|
2666
|
-
`provider-automatic`.
|
|
2667
|
-
|
|
2668
|
-
**What is left travels with the task.** The allowance is the length; `wrapEndsInSeconds` is how
|
|
2669
|
-
much of it remains, stated on the `completing` task and restated on every publication that
|
|
2670
|
-
carries it, as `previewEndsInSeconds` is on a preview. A desk that reloads mid-wrap finds the
|
|
2671
|
-
number on the snapshot; a lead's screen finds it on the member; neither starts a clock of its own,
|
|
2672
|
-
and both count down from receipt. Under `provider-automatic` the provider ends the task when it
|
|
2673
|
-
reaches zero; under `agent-command` it is the expected wrap left, `0` once overrun, and nothing
|
|
2674
|
-
acts on it. Off voice, where `completing` is the provider's word that interaction ended, the field
|
|
2675
|
-
arrives with that word, so the clock starts nowhere else.
|
|
2676
|
-
|
|
2677
|
-
`wrapAllowance` is fixed, and when it starts depends on whether the channel carries real-time
|
|
2678
|
-
audio:
|
|
2679
|
-
|
|
2680
|
-
| Channel | Wrap allowance starts at |
|
|
2681
|
-
| --- | --- |
|
|
2682
|
-
| Voice and any channel with real-time audio | The `task-audio-ended` event |
|
|
2683
|
-
| Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
|
|
2684
|
-
| Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message; what is left arrives on that publication as `wrapEndsInSeconds` and is counted down from receipt |
|
|
2685
|
-
| Other non-audio channels | The `task-updated` that moves the task to `completing` |
|
|
2686
|
-
|
|
2687
|
-
**Off voice, `completing` is the provider's word that interaction ended.** There is no audio event to
|
|
2688
|
-
carry it, so the phase does; an agent application starts the wrap clock at that publication and nowhere else. It
|
|
2689
|
-
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2690
|
-
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
2691
|
-
required, because the clock has to start somewhere: a chat or email task the provider completes
|
|
2692
|
-
from `in-progress` with an allowance to run gave the agent none of it, and the stream refuses the
|
|
2693
|
-
ending (`stream.taskEnded.unwrapped`); an ending the agent's own `complete` brought forward says
|
|
2694
|
-
`by: "agent"` and is not that. Under `agent-command` the agent's `complete` is the end, from
|
|
2695
|
-
`in-progress` or from `completing` alike.
|
|
2696
|
-
|
|
2697
|
-
```ts
|
|
2698
|
-
const emailCompletion = {
|
|
2699
|
-
completionMode: "agent-command",
|
|
2700
|
-
wrapAllowance: 120,
|
|
2701
|
-
} satisfies Pick<Task<"email">, "completionMode" | "wrapAllowance">;
|
|
2702
|
-
```
|
|
2703
|
-
|
|
2704
|
-
In this example, the agent has two minutes after sending the email to add notes, select a
|
|
2705
|
-
outcome, and complete the task.
|
|
2706
|
-
|
|
2707
|
-
`0` means no wrap under `provider-automatic`: the provider completes the task at the end of the
|
|
2708
|
-
interaction, and `complete` has nothing to cut short. Under `agent-command` it is an expected wrap
|
|
2709
|
-
of nothing -- the desk shows the overrun from the start -- and the task still waits for `complete`.
|
|
2710
|
-
|
|
2711
|
-
There is no value meaning "unlimited", because a number that is not a duration would be read as
|
|
2712
|
-
one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
|
|
2713
|
-
only `agent-command` permits: the provider will not complete the task itself, so there is nothing
|
|
2714
|
-
for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
|
|
2715
|
-
claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
|
|
2716
|
-
Under `provider-automatic` the field is required, because the provider is going to act on it.
|
|
2717
|
-
|
|
2718
|
-
```ts
|
|
2719
|
-
const untimedWrap = {
|
|
2720
|
-
completionMode: "agent-command",
|
|
2721
|
-
} satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
|
|
2722
|
-
```
|
|
2723
|
-
|
|
2724
|
-
Here the customer has hung up, `task-audio-ended` has been sent on time, the task is `completing`,
|
|
2725
|
-
and the agent takes as long as the work needs. Moving the task to `completing` late to avoid a
|
|
2726
|
-
deadline is not an alternative on an audio channel: the clock starts at a real event, and delaying
|
|
2727
|
-
that event would falsify the phase and everything timed from it.
|
|
2728
|
-
|
|
2729
|
-
#### Connecting back during completion
|
|
2730
|
-
|
|
2731
|
-
A task that declares `connectBack` lets the agent connect back to the party while the task is
|
|
2732
|
-
`completing` -- to finish what the call left unfinished, on the same task rather than a new one,
|
|
2733
|
-
whoever placed the call in the first place. Most such calls came in, so this is not a redial: the
|
|
2734
|
-
agent rings a party who rang them. Omni issues `{ type: "connect-back", dialId }`; it is issuable
|
|
2735
|
-
only in `completing`, and only where the capability is declared. The provider knows who the party
|
|
2736
|
-
is; the command carries no destination, and it is a dial like any other, so it carries the agent application's
|
|
2737
|
-
`dialId`, is answered `dialling`, and ends in one `dial-outcome`.
|
|
2738
|
-
|
|
2739
|
-
On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
2740
|
-
working again, and the wrap allowance is **discarded, not paused**. From there the call is
|
|
2741
|
-
reported as any call is -- `paused`, `in-progress`, and when its audio ends, `task-audio-ended`
|
|
2742
|
-
again, which starts a fresh allowance from that instant. A party who does not answer is a dial
|
|
2743
|
-
whose outcome says so and a call whose audio ended: the task returns to `completing` through the
|
|
2744
|
-
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2745
|
-
deadline.
|
|
2746
|
-
|
|
2747
|
-
**The room shows the party being dialled.** From the moment any agent application dial places the party -- a
|
|
2748
|
-
dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
|
|
2749
|
-
the agent application's `dialId`, exactly as a conferenced entry does from its dial; `joined` on
|
|
2750
|
-
the publication that follows the answered `dial-outcome` and never before it
|
|
2751
|
-
(`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
|
|
2752
|
-
and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). An agent application shows a
|
|
2753
|
-
call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
|
|
2754
|
-
call over, the platform dialling the party again without a command -- shows the same thing with no
|
|
2755
|
-
agent application `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
|
|
2756
|
-
with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
|
|
2757
|
-
tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
|
|
2758
|
-
|
|
2759
|
-
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
2760
|
-
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
2761
|
-
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
2762
|
-
is none: the provider completes the task at provider end, and Omni does not offer Connect back,
|
|
2763
|
-
whatever the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
2764
|
-
window it cannot, and declaring it there changes nothing.
|
|
2765
|
-
|
|
2766
|
-
```ts
|
|
2767
|
-
const connectBackCapable = {
|
|
2768
|
-
channel: "voice",
|
|
2769
|
-
capabilities: { hold: true, connectBack: true, outcomes: true },
|
|
2770
|
-
phase: "completing",
|
|
2771
|
-
completionMode: "provider-automatic",
|
|
2772
|
-
wrapAllowance: 30,
|
|
2773
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
|
|
2774
|
-
```
|
|
2775
|
-
|
|
2776
|
-
With ten seconds of the thirty left, the agent presses Connect back: `execute({ command: { type:
|
|
2777
|
-
"connect-back", dialId } })` returns `dialling`, the task is `in-progress`, and the thirty seconds
|
|
2778
|
-
are gone.
|
|
2779
|
-
The second call ends: `task-audio-ended`, the task is `completing`, and a new thirty seconds runs
|
|
2780
|
-
from that instant.
|
|
2781
|
-
|
|
2782
|
-
```ts
|
|
2783
|
-
const immediateProviderCompletion = {
|
|
2784
|
-
completionMode: "provider-automatic",
|
|
2785
|
-
wrapAllowance: 0,
|
|
2786
|
-
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
2787
|
-
```
|
|
2788
|
-
|
|
2789
|
-
### History
|
|
2790
|
-
|
|
2791
|
-
The record is the call's, not the task's: most of what it holds happened before this agent, and
|
|
2792
|
-
`queued` and `offered` are not interactions at all, so the field is `history` and its types are
|
|
2793
|
-
`TaskHistory`, `TaskHistoryStep`, `HistoryStep`, `HistoryReport` and `HistoryReportResult`.
|
|
2794
|
-
*Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
|
|
2795
|
-
the total of earlier agents' time is `interactionSeconds` and the phase rules are
|
|
2796
|
-
`command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
|
|
2797
|
-
are provided. The manifest's `settleMs` bounds final task completion, not entry into
|
|
2798
|
-
the `completing` wrap-up phase, and bounds an applied `schedule` the same way. The `recordStep` method and `complete` command keep their names.
|
|
2799
|
-
|
|
2800
|
-
Migration from the earlier spellings:
|
|
2801
|
-
|
|
2802
|
-
| Former API | Current API |
|
|
2803
|
-
| --- | --- |
|
|
2804
|
-
| Task.handlingHistory, Task.interactionHistory | `Task.history` |
|
|
2805
|
-
| TaskHandlingHistory, TaskInteractionHistory | `TaskHistory` |
|
|
2806
|
-
| TaskHandlingStep, HandlingStep, TaskInteractionStep, InteractionStep | `TaskHistoryStep`, `HistoryStep` |
|
|
2807
|
-
| HandlingReport, HandlingReportResult, validateHandlingReport, and their Interaction spellings | `HistoryReport`, `HistoryReportResult`, `validateHistoryReport` |
|
|
2808
|
-
| HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL, and their INTERACTION spellings | `HISTORY_STEPS_WITH_A_PERSON`, `HISTORY_STEPS_THAT_DIAL` |
|
|
2809
|
-
| handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
|
|
2810
|
-
| handleSeconds | `interactionSeconds` |
|
|
2811
|
-
| Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `expiresInSeconds` |
|
|
2812
|
-
| Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
|
|
2813
|
-
| taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
|
|
2814
|
-
| Manifest.disposalSettleMs, Manifest.completionSettleMs | `Manifest.settleMs` (`manifest.settleMs`, `manifest.settleMs.renamed`): the bound after `applied` for `complete` and `schedule` alike |
|
|
2815
|
-
| the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
|
|
2816
|
-
| media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio`; there is no team audio, the lead's follows `listening` on the member |
|
|
2817
|
-
| capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
|
|
2818
|
-
| Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
|
|
2819
|
-
| availability reserved | `elsewhere` |
|
|
2820
|
-
| recording destinationId, HostRecording.destinationIds | `storageId`, `storageIds` (`recording.storage`, `recording.host.storage`) |
|
|
2821
|
-
| coldTransfer, warmTransfer, the transfer command and its four actions, the consulted role, the transferred outcome | gone: help arrives by **Lead assist**; the `transferred` history step and the `transfers` total stay, since the platform's own routing still changes hands |
|
|
2822
|
-
| assignmentExpiresAt on task-offered, previewEndsAt on the task | `expiresInSeconds`, `previewEndsInSeconds`: seconds left from the publication, never an instant (`event.taskOffered.expiresInSeconds`, `task.preview.previewEndsInSeconds`) |
|
|
2823
|
-
| TeamMembers.requests, LeadRequest | `TeamMember.request` (`MemberRequest`): the ask rides on the member (`team.member.request.*`) |
|
|
2824
|
-
| team-updated on every change | `team-updated` whole once after the switch and on snapshots; `team-member-updated`, `team-member-removed`, `team-policies-updated` after (`stream.team.baseline`, `stream.teamMember.unknown`) |
|
|
2825
|
-
| RecordingCommand.requestId | gone: no request identity; a partial effect is a settled `failed` under `omni.recording-unsettled` |
|
|
2826
|
-
| command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
|
|
2827
|
-
| expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
|
|
2828
|
-
| the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
|
|
2829
|
-
| schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
|
|
2830
|
-
| a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
|
|
2831
|
-
| end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
|
|
2832
|
-
| ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
|
|
2833
|
-
| MemberShift; the day on the lead's list alone | `Shift`, on `Snapshot.shift` and `shift-updated` for the agent, on `TeamMember.shift` for the lead |
|
|
2834
|
-
| TeamMember.listening as this lead's one entry | `MemberListening[]`, every lead's, each with `leadId` (`team.member.listening.leadId`, `.unique`, `.empty`, `.entry`, `.heard`) |
|
|
2835
|
-
| a member's history trimmed on the lead's list | the record whole, as the member's desk holds it |
|
|
2836
|
-
| durations counted from the screen's clock | the provider's: `timeCheck` required of a provider publishing a running instant (`manifest.timeCheck.required`), `Snapshot.providerTime` with it (`snapshot.providerTime`, `.unexpected`) |
|
|
2837
|
-
| a wrap the desk timed from an event's receipt | `Task.wrapEndsInSeconds` in `completing`, restated (`task.completing.wrapEndsInSeconds`, `.required`, `.unexpected`) |
|
|
2838
|
-
| MemberTask.capabilities, capabilitySource, browsers | gone: the workspace never travels to a lead (`team.member.task.workspace`); the record and the completion terms do |
|
|
2839
|
-
| a member republished on a hold, a mute, a take-over | on every publication of the member's task to the member |
|
|
2840
|
-
| two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
|
|
2841
|
-
|
|
2842
|
-
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2843
|
-
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
2844
|
-
`assignment` (`task.assignmentId`, `stream.taskOffered.duplicate`, `stream.assignment.ended`);
|
|
2845
|
-
phase and conformance rules use `interaction` and final-task rules use `completion`. The former
|
|
2846
|
-
fields are refused even when the new field is also supplied (`task.history.renamed`,
|
|
2847
|
-
`task.assignmentId.renamed` for a task's `id` or `allocationId`, `event.assignmentId.renamed`,
|
|
2848
|
-
`command.request.assignmentId.renamed` and `historyReport.assignmentId.renamed` for a `taskId`).
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
`Task.history` is the call record: the steps that brought the task to the agent, oldest
|
|
2852
|
-
first, and what they add up to before this agent:
|
|
2853
|
-
|
|
2854
|
-
```ts
|
|
2855
|
-
history: {
|
|
2856
|
-
steps: [
|
|
2857
|
-
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2858
|
-
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
2859
|
-
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
2860
|
-
{ step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
|
|
2861
|
-
{ step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
|
|
2862
|
-
],
|
|
2863
|
-
interactionSeconds: 312, // handled by others before this agent
|
|
2864
|
-
holdSeconds: 35, // holds others put the caller on
|
|
2865
|
-
queueSeconds: 41, // waiting before anyone answered
|
|
2866
|
-
transfers: 1, // hands the call has changed
|
|
2867
|
-
}
|
|
2868
|
-
```
|
|
2869
|
-
|
|
2870
|
-
**This is not an archive.** It is live data about a task that is still open: it travels with the task
|
|
2871
|
-
to whoever holds it next and ends when the task does. Nothing stores it, nothing queries it, and
|
|
2872
|
-
there is no archive behind it. A provider that keeps a record of *completed* contacts is describing
|
|
2873
|
-
something else, which will arrive under its own name and must not be folded in here.
|
|
2874
|
-
|
|
2875
|
-
It rides in the snapshot and is replaced whole like everything else there.
|
|
2876
|
-
|
|
2877
|
-
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
2878
|
-
`unanswered`, and each is defined on `TaskHistoryStep`.
|
|
2879
|
-
|
|
2880
|
-
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2881
|
-
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
2882
|
-
entry after the first, never a revision of it. A leg that has ended states its duration, and the
|
|
2883
|
-
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2884
|
-
only while its audio is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2885
|
-
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2886
|
-
(`task.history.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2887
|
-
provider whose platform parks the caller, the agent application whose microphone it is — by restating the entry
|
|
2888
|
-
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2889
|
-
plausible nought one level down from a total. **A leg still open when the audio ends is closed by
|
|
2890
|
-
the provider**, at that instant, in the same publication that says the audio ended: the provider
|
|
2891
|
-
is the one that knows the instant, and the agent application can only learn of it afterwards. Agents end calls
|
|
2892
|
-
muted, so the agent application's leg is routinely open at audio end; the provider closes it with the duration
|
|
2893
|
-
from the leg's `at` to the audio's end, and the agent application's own closing report for that leg, which
|
|
2894
|
-
follows what it hears, is answered `recorded` and changes nothing, the entry standing as the
|
|
2895
|
-
provider closed it. One publisher of the closed leg, and no order between them to get right. There is no resumed step; a resume is the end of a
|
|
2896
|
-
hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
|
|
2897
|
-
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
2898
|
-
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
2899
|
-
(`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
|
|
2900
|
-
total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
|
|
2901
|
-
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
2902
|
-
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
2903
|
-
that interval. **The second is the record's grain.** A duration is a positive whole number of
|
|
2904
|
-
seconds, and nought is refused because it claims no time; a leg that happened is stated at the
|
|
2905
|
-
grain: rounded to the nearest second, and a leg that rounds to nought is stated as `1`, the error
|
|
2906
|
-
being under the contract's resolution, where dropping the entry would say the leg never happened.
|
|
2907
|
-
|
|
2908
|
-
**A record once read is not unread.** Every restatement of a task's record -- on `task-updated`,
|
|
2909
|
-
on a resync snapshot -- carries every entry the agent application has already read, by step and instant, and
|
|
2910
|
-
may add to them; it never has fewer, and never drops the record while the task is open. A fact
|
|
2911
|
-
stated to the agent application and then withdrawn without an event is a record contradicting itself, exactly
|
|
2912
|
-
as a task that lost the terms it had read would be, and the stream refuses it the same way
|
|
2913
|
-
(`stream.taskUpdated.history`, `stream.snapshot.history`). A provider whose
|
|
2914
|
-
platform cannot hold a leg the agent application reported keeps it in the login's `store` for the life of the
|
|
2915
|
-
task, so a reload of the agent application restates the same record; the entry is keyed by `step` and `at`, so
|
|
2916
|
-
a running hold restated with its final `seconds` is the same entry.
|
|
2917
|
-
|
|
2918
|
-
**The provider owns the timestamps in its record.** Agent application timestamps are advisory. The provider
|
|
2919
|
-
may retain an agent application instant it accepts or assign its own observation/receipt instant; it need not
|
|
2920
|
-
copy the agent application clock. A provider declaring `timestampAuthority: "provider"` uses its own timestamps
|
|
2921
|
-
for final records. Receipt time is an observation boundary, not a claim about when the physical
|
|
2922
|
-
agent application action happened. Records are ordered by their published provider-selected instants, with no
|
|
2923
|
-
rewriting of an already-published entry when a later agent application report arrives.
|
|
2924
|
-
|
|
2925
|
-
The incoming agent application `step` and `at`, scoped to the task and its current life, identify one reported
|
|
2926
|
-
leg for correlation only. The provider retains a binding from that key to its chosen history
|
|
2927
|
-
`at`. Every successful `recordStep` returns `{ status: "recorded", at }` with that same canonical
|
|
2928
|
-
history instant, even for subsequent duration/end reports. The agent application keeps sending the original
|
|
2929
|
-
report key, never the returned history timestamp. A changed agent application timestamp is not a correction
|
|
2930
|
-
to the same report: it names a different leg. Bindings survive reconnect/reload for the retained
|
|
2931
|
-
task lifetime. The agent application renders provider-published history unchanged. It never compares the
|
|
2932
|
-
provider instant back to its own clock, substitutes its own timestamp, or adjusts later messages
|
|
2933
|
-
to match an earlier agent application estimate. The acknowledgment identifies the provider record for
|
|
2934
|
-
correlation/conformance only; it is not an application-side timestamp reconciliation instruction. Conflicting reuse is refused visibly, not paired by arrival time or nearest instant.
|
|
2935
|
-
If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
|
|
2936
|
-
or combine them: report the representation conflict. No ambiguous event is silently accepted.
|
|
2937
|
-
|
|
2938
|
-
**Interaction time is anchored, not restarted.** It runs from the
|
|
2939
|
-
`answered` step's `at` — from the task's first `in-progress` where the provider reports no
|
|
2940
|
-
history — until the task's audio ends, and a hold neither pauses nor resets it: the hold's own
|
|
2941
|
-
duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
|
|
2942
|
-
counting the wrong thing.
|
|
2943
|
-
|
|
2944
|
-
`muted` is there because the mute is the agent application's and never the provider's — see **The station is
|
|
2945
|
-
the agent application's** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2946
|
-
account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the agent application
|
|
2947
|
-
reported it: `host` for the agent's own Mute, `station` for a headset or system mute the agent application
|
|
2948
|
-
observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
|
|
2949
|
-
|
|
2950
|
-
**A step that dialled says which dial and where.** `conferenced` and `unanswered` each end a dial,
|
|
2951
|
-
and `transferred` a hand-over on the platform's own routing, so the entry carries the `destinationId`
|
|
2952
|
-
it went to and, where an agent application placed the dial, the `dialId` the agent application minted; no other step dialled, and either field on one is refused
|
|
2953
|
-
(`task.history.dialId.unexpected`). This is how a dial made before a take-over is placeable
|
|
2954
|
-
by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
|
|
2955
|
-
up in the record, not discarded.
|
|
2956
|
-
|
|
2957
|
-
Four rules a provider has to keep:
|
|
2958
|
-
|
|
2959
|
-
- **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
|
|
2960
|
-
the next. An entry can be written while its leg is still running, so the arithmetic has no second
|
|
2961
|
-
operand, and a provider holding the authoritative number should not have it recomputed from
|
|
2962
|
-
instants that may be rounded or clock-skewed. And once stated, `seconds` is what every screen
|
|
2963
|
-
shows for that leg: a count a screen kept while the leg ran is replaced by it, never kept beside it.
|
|
2964
|
-
- **Omit `seconds` while it is unknown. Never send `0`.** A leg still talking is not a zero-second
|
|
2965
|
-
conversation, and on live data that is the ordinary case rather than an edge. A zero is rejected.
|
|
2966
|
-
- **`by` is a bare `UserId`, and not necessarily an agent.** A lead or a manager takes part
|
|
2967
|
-
during an interaction too — a call taken over, a call conferenced in — so the field names whoever it was,
|
|
2968
|
-
from the same directory `ForcedBreak.by` names a lead from. It comes from this provider's own directory, the same
|
|
2969
|
-
namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
|
|
2970
|
-
within a provider and never across one.
|
|
2971
|
-
- **A task carries no names.** Omni resolves what to display with `getUserDetails()`. Two people
|
|
2972
|
-
called Arun on one site is ordinary, and anything pairing entries on a display name pairs them
|
|
2973
|
-
wrongly; carrying the name here would also copy it into every task and leave it to go stale.
|
|
2974
|
-
|
|
2975
|
-
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
2976
|
-
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
2977
|
-
`HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
|
|
2978
|
-
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
2979
|
-
`muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
|
|
2980
|
-
(`task.history.muted.by`).
|
|
2981
|
-
|
|
2982
|
-
That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
|
|
2983
|
-
handset, or a device the provider cannot resolve to a person. **Report the step without `by`
|
|
2984
|
-
rather than dropping it.** A list missing a real handler looks complete and is wrong, which is
|
|
2985
|
-
worse than one saying plainly it could not attribute a leg — and far better than publishing
|
|
2986
|
-
nothing because a single leg could not be named.
|
|
2987
|
-
|
|
2988
|
-
An agent application must render the two differently. Showing an unattributed `answered` the same way as
|
|
2989
|
-
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2990
|
-
*"not recorded"* in the place the name would go.
|
|
2991
|
-
|
|
2992
|
-
Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2993
|
-
different claim — it says the task has had none.
|
|
2994
|
-
|
|
2995
|
-
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
2996
|
-
carrying what others already used of it, and the agent reads that before they say hello: the
|
|
2997
|
-
totals are the provider's own — never a desk summing instants that may be rounded or skewed, for
|
|
2998
|
-
the same reason `seconds` is reported and not derived — and each is present when the provider
|
|
2999
|
-
knows it and absent when it does not, never a plausible nought. A fresh call from the queue has
|
|
3000
|
-
`steps` with no prior `answered` and no totals to state, and a desk shows nothing rather than
|
|
3001
|
-
zeros. The record is restated with the task on every `task-updated` and snapshot, so an entry
|
|
3002
|
-
that arrives late corrects the sums. These four are what every platform reports; more will be
|
|
3003
|
-
added here as a need is shown, not invented ahead of one.
|
|
3004
|
-
|
|
3005
|
-
#### The agent application records what it performs
|
|
3006
|
-
|
|
3007
|
-
`muted` is the one leg the agent application performs rather than the provider, and a record kept by the
|
|
3008
|
-
provider would have a hole exactly there. So the agent application reports it, through `recordStep`, and the provider
|
|
3009
|
-
writes it into its record as it writes every other leg:
|
|
3010
|
-
|
|
3011
|
-
```ts
|
|
3012
|
-
declare const connection: Connection<"voice">;
|
|
3013
|
-
declare const assignmentId: AssignmentId;
|
|
3014
|
-
declare const at: IsoTimestamp;
|
|
3015
|
-
|
|
3016
|
-
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
|
|
3017
|
-
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the agent application chooses to say
|
|
3018
|
-
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
3019
|
-
```
|
|
3020
|
-
|
|
3021
|
-
Every report about one leg repeats its original `step` and agent application `at` as a correlation key;
|
|
3022
|
-
the published history uses the provider-selected `at` returned by `recordStep`. The agent application reports
|
|
3023
|
-
the action it performed and its measured duration, not an authoritative provider timestamp. It may say how long so far — `seconds` is
|
|
3024
|
-
elapsed while the leg runs and final once it has ended — and **the end is stated, never
|
|
3025
|
-
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
3026
|
-
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
3027
|
-
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
3028
|
-
leg, when it began and when it ended, and `validateHistoryReport(report, path, manifest)` refuses
|
|
3029
|
-
a running one it was never asked for (`historyReport.running.unexpected`). What a provider that
|
|
3030
|
-
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
3031
|
-
`history` when the *provider* publishes it: Omni never writes the record itself.
|
|
3032
|
-
`recordStep` is required of every softphone login's connection, since every call on a softphone
|
|
3033
|
-
can be muted by the agent application, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
|
|
3034
|
-
`mutedBy: "host"` or `"station"`, and no other report has the word (`historyReport.mutedBy`,
|
|
3035
|
-
`.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
|
|
3036
|
-
the agent application mutes nothing and records nothing.
|
|
3037
|
-
|
|
3038
|
-
**The record is a record.** The microphone is the agent application's, and nothing in the
|
|
3039
|
-
history moves it: a `muted` entry the provider publishes, or closes with a final `seconds`, is the
|
|
3040
|
-
provider's account of a leg the agent application reported, and the mute itself ends only when the
|
|
3041
|
-
agent, or the station, ends it. The agent application keeps its own mute state, reports each leg
|
|
3042
|
-
as it begins and ends, and takes the provider's published entry as the closed account of that leg:
|
|
3043
|
-
it does not reopen the leg, replace the provider's timestamp, or overwrite the provider's final
|
|
3044
|
-
duration with a later report. A later agent mute is a new leg, never a reopening of the ended one.
|
|
3045
|
-
|
|
3046
|
-
Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
|
|
3047
|
-
for the current report key; a final `seconds` closes that leg under the history contract.
|
|
3048
|
-
This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
|
|
3049
|
-
task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
|
|
3050
|
-
retain the current provider view and apply the matching closure once correlation is available;
|
|
3051
|
-
never guess a match from arrival order or nearest timestamp. Failure to release an agent application-controlled
|
|
3052
|
-
mute is reported visibly; local device reports still describe the actual device state.
|
|
3053
|
-
|
|
3054
|
-
At a provider-confirmed task/audio end, the agent application also stops the associated local mute and reports
|
|
3055
|
-
its observed ending where the report is still accepted. An agent application closing report repeats its original
|
|
3056
|
-
key and may include its measured duration, but cannot reverse an already confirmed provider end.
|
|
3057
|
-
The provider acknowledges a known closed leg without rewriting its final record; after the task
|
|
3058
|
-
has been completed, it may refuse the report as assignment-not-found. The agent application reads that response rather
|
|
3059
|
-
than retrying or recreating the task. An observed agent application end before a provider closure is still
|
|
3060
|
-
reported normally; the provider decides the final timestamp and publishes the record.
|
|
3061
|
-
|
|
3062
|
-
### Browser capability
|
|
3063
|
-
|
|
3064
|
-
`browsers` is available to voice, chat, and email tasks. When declared, Omni renders the task's
|
|
3065
|
-
`TaskBrowser` entries as named browsers in the task workspace. A task that supplies one or more
|
|
3066
|
-
browser definitions must declare:
|
|
3067
|
-
|
|
3068
|
-
```ts
|
|
3069
|
-
capabilities: { browsers: true }
|
|
3070
|
-
```
|
|
3071
|
-
|
|
3072
|
-
Tasks without browser definitions omit the capability and provide an empty `browsers` array.
|
|
3073
|
-
|
|
3074
|
-
**A browser is one tab, and each workspace keeps its own.** `Browser` is what a tab is — an id, a
|
|
3075
|
-
name, a URL. The task workspace shows the task's `TaskBrowser` entries, one tab each, fixed at the
|
|
3076
|
-
task's definition: their count and their details, `urlVisibility` included, arrive with the task,
|
|
3077
|
-
and nothing adds a tab to a task later. The personal workspace is the agent's: as many
|
|
3078
|
-
`PersonalBrowser` tabs as they open, never on the wire, and no provider says anything about what
|
|
3079
|
-
they may see there.
|
|
3080
|
-
|
|
3081
|
-
#### `TaskBrowser` and isolation
|
|
3082
|
-
|
|
3083
|
-
Each `TaskBrowser` defines one named browser in the task workspace.
|
|
3084
|
-
|
|
3085
|
-
| Field | Contract |
|
|
3086
|
-
| --- | --- |
|
|
3087
|
-
| `id` | Stable internal selection and update identity within the task. |
|
|
3088
|
-
| `name` | Agent-facing tab label, unique within the task, and an input to schemes containing `TAB_NAME`. |
|
|
3089
|
-
| `purpose` | Human-readable explanation of the browser's role. |
|
|
3090
|
-
| `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
|
|
3091
|
-
| `sharedSession` | Required. `false` creates a task-specific browser session. |
|
|
3092
|
-
| `isolationScheme` | **Required when `sharedSession` is `true`**, and rejected when it is `false`. There is no default: see below. |
|
|
3093
|
-
| `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; an agent application that declares `guarantees.browserUrlVisibility` honours it tab by tab, and a provider checks that guarantee before it sends such a URL at all. |
|
|
3094
|
-
|
|
3095
|
-
##### Choosing an isolation scheme
|
|
3096
|
-
|
|
3097
|
-
Every scheme is supported and the provider picks the one its deployment needs. There is no
|
|
3098
|
-
default, and a `sharedSession: true` browser that declares none is invalid — the type will not compile
|
|
3099
|
-
it and `validateSnapshot` reports `task.browser.isolationScheme.required`.
|
|
3100
|
-
|
|
3101
|
-
That is deliberate. Sharing a signed-in session decides **who else may see those credentials**,
|
|
3102
|
-
and it is not a decision to inherit from whichever value happened to be the default. `TAB_NAME`
|
|
3103
|
-
keys on the tab label alone, so two providers that each publish a browser named "CRM" share one
|
|
3104
|
-
signed-in session — legitimate where a deployment wants exactly that, and a silent credential
|
|
3105
|
-
leak where it does not. It remains available; it has to be asked for.
|
|
3106
|
-
|
|
3107
|
-
`browserSessionKey` fails closed: given a reusing browser with no scheme it returns `undefined`
|
|
3108
|
-
and the browser is isolated. The safe reading of an invalid declaration is "do not share", never
|
|
3109
|
-
"share with everyone named the same".
|
|
3110
|
-
|
|
3111
|
-
##### Permitted URL schemes
|
|
3112
|
-
|
|
3113
|
-
`TaskBrowser.url` is provider-supplied and is loaded inside Omni's managed browser, so it is
|
|
3114
|
-
restricted to the schemes in `ALLOWED_BROWSER_URL_SCHEMES` — currently `http:` and `https:`.
|
|
3115
|
-
`file:`, `chrome:`, `javascript:`, and every other scheme are rejected. Omni substitutes a blank
|
|
3116
|
-
page rather than following a disallowed URL, and `isAllowedBrowserUrl()` is the shared predicate.
|
|
3117
|
-
|
|
3118
|
-
##### Reuse and isolation
|
|
3119
|
-
|
|
3120
|
-
With `sharedSession: true`, definitions producing the same isolation key share one **storage profile**:
|
|
3121
|
-
cookies, local storage, session storage, permissions, and cached credentials. Different keys are
|
|
3122
|
-
isolated from one another.
|
|
3123
|
-
|
|
3124
|
-
Sharing a profile is not sharing a window. Two browsers in the same task keep their own tab, their
|
|
3125
|
-
own visible label, and their own navigation state and history even when their keys match — a
|
|
3126
|
-
scheme that omits `TAB_NAME`, such as `PROVIDER_NAME__TASK_TYPE_NAME`, deliberately places every
|
|
3127
|
-
named browser of that task type in one signed-in profile without merging them into one page.
|
|
3128
|
-
|
|
3129
|
-
`browserSessionKey()` derives the key. Every part is escaped before the `.` separator is applied,
|
|
3130
|
-
because `encodeURIComponent` leaves `.` untouched and a raw join would let one value forge
|
|
3131
|
-
another key: provider `Acme.Voice` with task type `Support` would otherwise produce the same key
|
|
3132
|
-
as provider `Acme` with task type `Voice.Support`, silently placing two providers in one cookie
|
|
3133
|
-
jar. Hosts that must flatten the key further — for a native window label or a partition name with
|
|
3134
|
-
a restricted charset — must keep the mapping injective, for example by appending a fingerprint of
|
|
3135
|
-
the exact key, since lowercasing or replacing punctuation reintroduces exactly this collision.
|
|
3136
|
-
|
|
3137
|
-
```ts
|
|
3138
|
-
browsers: [
|
|
3139
|
-
{
|
|
3140
|
-
id: "crm",
|
|
3141
|
-
name: "CRM",
|
|
3142
|
-
purpose: "Contact record",
|
|
3143
|
-
url: "https://crm.example.com/contact/42",
|
|
3144
|
-
sharedSession: true,
|
|
3145
|
-
isolationScheme: BROWSER_ISOLATION_SCHEMES.PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME,
|
|
3146
|
-
}
|
|
3147
|
-
]
|
|
3148
|
-
```
|
|
3149
|
-
|
|
3150
|
-
The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as follows:
|
|
3151
|
-
|
|
3152
|
-
| Enum member | Example session key |
|
|
3153
|
-
| --- | --- |
|
|
3154
|
-
| `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's assignment id, so one assignment, never the next customer's cookies |
|
|
3155
|
-
| `TAB_NAME` | `CRM` |
|
|
3156
|
-
| `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
|
|
3157
|
-
| `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
|
|
3158
|
-
| `PROVIDER_NAME__TASK_TYPE_NAME` | `mailflow.Support` |
|
|
3159
|
-
| `TASK_TYPE_NAME__TAB_NAME` | `Support.CRM` |
|
|
3160
|
-
|
|
3161
|
-
`TASK_TYPE_NAME` refers to the mandatory `Task.taskType`. The isolation scheme never changes
|
|
3162
|
-
the browser tab label. Serialized values are stable protocol values and must not be renamed or
|
|
3163
|
-
reused.
|
|
3164
|
-
|
|
3165
|
-
**`PROVIDER_NAME` is `manifest.id`, never `manifest.displayName`.** Only the id is required unique
|
|
3166
|
-
across an installation; two providers may legitimately share a display label, and keying a cookie
|
|
3167
|
-
jar on one would put them in the same signed-in session. The id is also stable across launches,
|
|
3168
|
-
where a display name may be re-worded — and a changed key silently signs the agent out of every
|
|
3169
|
-
browser that used it.
|
|
3170
|
-
|
|
3171
|
-
### Task capabilities
|
|
3172
|
-
|
|
3173
|
-
Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
|
|
3174
|
-
must not show or issue hold for that task even if another task from the same provider supports it.
|
|
3175
|
-
|
|
3176
|
-
**A capability is a property of the task, and of nothing the task names.** A provider derives it
|
|
3177
|
-
from no other source -- not the queue the task came from, not the login, not its own configuration
|
|
3178
|
-
-- because each of those answers a question about this task from somewhere that does not know
|
|
3179
|
-
about this task, and a client with two sources and a rule for choosing between them is the shape
|
|
3180
|
-
that produces two consumers disagreeing about one fact. The task is the one source, and the
|
|
3181
|
-
provider puts on the task what the platform permits for it -- and says, in `capabilitySource`, who
|
|
3182
|
-
chose those terms. `queue` says somebody configured them. `nobody` says nothing handed the work
|
|
3183
|
-
over: an agent's own outbound call has no queue behind it, and the provider states what it permits
|
|
3184
|
-
for such a call. Neither changes where an agent application reads the capabilities from, which is the task; the
|
|
3185
|
-
source is one more published fact about them, and the one that lets an agent application tell a fact from a fault
|
|
3186
|
-
(see below).
|
|
3187
|
-
|
|
3188
|
-
**Completing the task is not a capability, and no capability set withholds it.** `complete` is
|
|
3189
|
-
governed by `completionMode` alone: under `agent-command` it is always available, whatever the set
|
|
3190
|
-
says, and the `outcomes` capability decides only whether a code travels with it. Likewise
|
|
3191
|
-
Answer on a pending task and Call on a preview are the phase's controls, not the set's. The
|
|
3192
|
-
capability set governs what the agent may do *with* the task; completing it is never on the
|
|
3193
|
-
list.
|
|
3194
|
-
|
|
3195
|
-
**A permission that changes while the task is open is republished on the task at the moment it
|
|
3196
|
-
changes.** An agent on a billing dispute may refund up to their own limit and no further. The
|
|
3197
|
-
customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
|
|
3198
|
-
republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
|
|
3199
|
-
is republished without it. Nothing about the agent changed -- who is on the call did -- and the
|
|
3200
|
-
task said so both times, at the moment it became true. A call moves queues the same way: a caller
|
|
3201
|
-
identified as a priority customer is transferred to the priority queue, and the task arrives under
|
|
3202
|
-
that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
|
|
3203
|
-
restated on the task at the hand-over, not inferred from the queue it left. A capability stated
|
|
3204
|
-
once at offer or answer and never corrected is a fact with a shelf life and no expiry, and an agent application
|
|
3205
|
-
draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
|
|
3206
|
-
the task, with its capabilities as they now stand, and an agent application treats the last statement as current
|
|
3207
|
-
rather than re-deriving anything. This is the same shape as a room left full after the call ends
|
|
3208
|
-
-- a field describing the present, carried past the moment it stopped being true -- carried past
|
|
3209
|
-
this time not by a spread but by nobody sending the correction. A provider whose platform can
|
|
3210
|
-
change a permission mid-task and does not republish is in breach, however conformant its offer was.
|
|
3211
|
-
A capability set can shrink as well as grow, and an agent application that has only ever seen it grow meets a
|
|
3212
|
-
control that was there a moment ago and is gone; a command that arrives after its capability was
|
|
3213
|
-
withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
|
|
3214
|
-
provider gets right first: a control gained late is a nicety, a control withdrawn and not
|
|
3215
|
-
republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
|
|
3216
|
-
direction: the task as offered and as republished, and one command that was issuable under the
|
|
3217
|
-
first and is refused under the last for want of the capability withdrawn -- and nothing else.
|
|
3218
|
-
|
|
3219
|
-
**An empty capability set is a statement, not a shrug.** `capabilities: {}` under `queue` or
|
|
3220
|
-
`nobody` says the platform permits nothing capability-gated on this task, and an agent application draws
|
|
3221
|
-
nothing beyond what the phase and `completionMode` require. A provider that has not yet learned
|
|
3222
|
-
what the platform permits -- a queue's configuration that has not reached it -- knows nothing of
|
|
3223
|
-
the kind, and must not publish the task as if it did, neither as `{}` nor as every control it has.
|
|
3224
|
-
It publishes the task under `capabilitySource: "not-yet-read"`, with the capabilities it will
|
|
3225
|
-
honour until it knows, and an agent application shows that where the agent works, beside the controls it draws
|
|
3226
|
-
from them: "no queue governs this call" is a fact, "the configuration has not arrived" is a fault,
|
|
3227
|
-
and the set alone cannot tell them apart, so the provider says which. What a provider honours
|
|
3228
|
-
under not-yet-read terms is its own call -- a floor that would rather an agent briefly hold a
|
|
3229
|
-
control the platform might not have granted than lose hold or hang-up mid-call over a slow
|
|
3230
|
-
configuration read publishes those -- and the protocol chooses no default set for it. The fault is
|
|
3231
|
-
also reported as a `diagnostic` naming the task, one per occurrence, so an operator counts it. When
|
|
3232
|
-
the terms arrive, the task is republished under `queue` with the set as it now stands: the same
|
|
3233
|
-
republish as any other permission that changed while the task was open. The move goes one way.
|
|
3234
|
-
Terms once read stay read: a re-read that fails mid-task is not a new fact about the task, so the
|
|
3235
|
-
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
3236
|
-
`queue` or `nobody` never returns to `not-yet-read`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
3237
|
-
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
3238
|
-
entry the same two ways (`stream.taskUpdated.history`, `stream.snapshot.history`);
|
|
3239
|
-
a snapshot carrying a task still at work does not forget the audio the stream held up, since audio
|
|
3240
|
-
ends on `task-audio-ended` and the call moves on (`stream.snapshot.audio`); a voice task ends after
|
|
3241
|
-
its audio ends, never around it, whatever the outcome (`stream.taskEnded.audioOpen`); and a task does not go
|
|
3242
|
-
backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
|
|
3243
|
-
stream sees publications, not
|
|
3244
|
-
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
3245
|
-
with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
|
|
3246
|
-
the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
|
|
3247
|
-
`completing` except by the party being dialled again, a connect-back or a platform's callback,
|
|
3248
|
-
which the update itself shows: the party ringing, or joined by a dial whose answered outcome the
|
|
3249
|
-
stream saw in this life. A completing task republished as `in-progress` from a stale copy carries
|
|
3250
|
-
no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-audio-started` on a `completing` task
|
|
3251
|
-
is refused as it is on a pending one (`stream.taskAudioStarted.beforeWork`), since a connect-back
|
|
3252
|
-
returns the task to `in-progress` before any audio. And a task completes after its audio ends,
|
|
3253
|
-
never around it: an update moving a task to `completing` while the stream holds its audio as
|
|
3254
|
-
started is refused (`stream.taskUpdated.audioOpen`), whoever caused the ending, and a task stating
|
|
3255
|
-
`completing` with `audio: "started"` contradicts itself on any snapshot or update
|
|
3256
|
-
(`task.audio.completing`). The consequence the rule exists for is concrete: the customer's audio
|
|
3257
|
-
keeps playing through the agent's wrap-up.
|
|
3258
|
-
|
|
3259
|
-
What the agent is told differs by source, and only one source tells them anything. Under `queue`
|
|
3260
|
-
and `nobody` the agent sees controls and nothing about where they came from: both are facts,
|
|
3261
|
-
and an agent working a call has no use for the name of the rule behind its buttons. Under
|
|
3262
|
-
`not-yet-read` the agent is told, beside the controls, that these are what the provider will honour
|
|
3263
|
-
until the queue's terms arrive, and that the controls may change when they do -- a statement about
|
|
3264
|
-
the buttons in front of them now, not about the provider, because that is what changes when the
|
|
3265
|
-
republish lands. It stays for as long as the set is provisional, beside the controls it qualifies;
|
|
3266
|
-
a message that shows and clears has said nothing about the buttons still on the screen. The two
|
|
3267
|
-
words are close in English and far apart on the desk: `nobody` is silence, `not-yet-read` is a
|
|
3268
|
-
standing notice.
|
|
3269
|
-
|
|
3270
|
-
One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
|
|
3271
|
-
conformant adapter never shows an agent application `not-yet-read` under test, and a clean `exerciseAdapter`
|
|
3272
|
-
result says nothing about how the agent application renders it. That rendering is tested against a fixture --
|
|
3273
|
-
a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
|
|
3274
|
-
and never against an adapter, even in principle. `exerciseAdapter` treats a
|
|
3275
|
-
task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
|
|
3276
|
-
a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
|
|
3277
|
-
rather than passing with a note.
|
|
3278
|
-
|
|
3279
|
-
```ts
|
|
3280
|
-
const taskCapabilities = {
|
|
3281
|
-
channel: "voice",
|
|
3282
|
-
capabilities: {
|
|
3283
|
-
hold: true,
|
|
3284
|
-
outcomes: true,
|
|
3285
|
-
},
|
|
3286
|
-
browsers: [],
|
|
3287
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "browsers">;
|
|
3288
|
-
```
|
|
3289
|
-
|
|
3290
|
-
A capability says the control may be offered; the task's phase says whether there is anything to use
|
|
3291
|
-
it on. Every control below that acts on the call or the conversation -- everything but `decline`,
|
|
3292
|
-
`connectBack`, `schedule` and `outcomes` -- is offered only while the task is `in-progress` or `paused`.
|
|
3293
|
-
See **Which commands need a capability**.
|
|
3294
|
-
|
|
3295
|
-
### Voice capabilities
|
|
3296
|
-
|
|
3297
|
-
| Capability | Omni UI | Contract |
|
|
3298
|
-
| --- | --- | --- |
|
|
3299
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
|
|
3300
|
-
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
3301
|
-
| `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3302
|
-
| `terminateCall` | Primary button: Terminate call | The provider ends the whole call: every channel on it, the agent's, the caller's, any colleague's and anyone else's. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3303
|
-
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
|
|
3304
|
-
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the call or from its wrap: a time, and a note. See **Scheduling a follow-up**. |
|
|
3305
|
-
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
3306
|
-
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and remove one person from it -- a conferenced entry, one still ringing included, which calls the dial off, or the party, leaving the agent with the colleague. See **Ending a call, and removing one person from it**. |
|
|
3307
|
-
| `recording` | Overflow menu item: Recording | Per-task provider and agent application policies expose only their permitted recording actions; each has independent state and routing. |
|
|
3308
|
-
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3309
|
-
|
|
3310
|
-
### Publishing codes and destinations
|
|
3311
|
-
|
|
3312
|
-
Two capabilities accept an object when the provider wants Omni to render real choices. For
|
|
3313
|
-
`outcomes`, `true` remains valid and means "offer the control with nothing published"; the one
|
|
3314
|
-
directory control, `conference`, carries its directory or is refused, since once nothing is typed
|
|
3315
|
-
the directory is the control.
|
|
3316
|
-
|
|
3317
|
-
#### `outcomes`
|
|
3318
|
-
|
|
3319
|
-
An outcome describes what happened; completion finishes the task. Outcome codes are selected
|
|
3320
|
-
from the task's own published list. They are distinct from `DialOutcome`, which reports the
|
|
3321
|
-
result of a dial attempt.
|
|
3322
|
-
|
|
3323
|
-
Migration requires providers and hosts to adopt the renamed types and fields together:
|
|
3324
|
-
|
|
3325
|
-
| Former API | Current API |
|
|
3326
|
-
| --- | --- |
|
|
3327
|
-
| DispositionCode | `OutcomeCode` |
|
|
3328
|
-
| DispositionRules | `OutcomeRules` |
|
|
3329
|
-
| DispositionPayload | `OutcomePayload` |
|
|
3330
|
-
| Task.capabilities.dispositions | `Task.capabilities.outcomes` |
|
|
3331
|
-
| complete.disposition | `complete.outcome` |
|
|
3332
|
-
|
|
3333
|
-
Validation and conformance diagnostics use `task.outcomes`, `task.outcome`, and
|
|
3334
|
-
`command.complete.outcome` in place of the former names. Update retained task snapshots and
|
|
3335
|
-
command producers as well as imports. Old capability and command fields are rejected, including
|
|
3336
|
-
payloads containing both names; there are no compatibility aliases. Code IDs, notes, required
|
|
3337
|
-
selection rules, and completion behavior are unchanged.
|
|
3338
|
-
|
|
3339
|
-
```ts
|
|
3340
|
-
capabilities: {
|
|
3341
|
-
outcomes: {
|
|
3342
|
-
required: true,
|
|
3343
|
-
notes: "optional",
|
|
3344
|
-
codes: [
|
|
3345
|
-
{ id: "resolved", label: "Resolved" },
|
|
3346
|
-
{ id: "callback", label: "Callback needed", group: "Follow-up" },
|
|
3347
|
-
],
|
|
3348
|
-
},
|
|
3349
|
-
}
|
|
3350
|
-
```
|
|
3351
|
-
|
|
3352
|
-
| Field | Contract |
|
|
3353
|
-
| --- | --- |
|
|
3354
|
-
| `required` | When `true`, Omni must collect a code before issuing `complete`. A required policy must publish at least one code. |
|
|
3355
|
-
| `notes` | `required`, `optional`, or `none`; controls the free-text field beside the code. |
|
|
3356
|
-
| `codes` | Codes Omni offers. `id` values are non-empty and unique; Omni sends the chosen `id` as `TaskCommand.complete.outcome`. |
|
|
3357
|
-
|
|
3358
|
-
With `outcomes: true` Omni shows a Complete control and sends `complete` with no code, because
|
|
3359
|
-
the provider published none.
|
|
3360
|
-
|
|
3361
|
-
#### `conference`
|
|
3362
|
-
|
|
3363
|
-
```ts
|
|
3364
|
-
capabilities: {
|
|
3365
|
-
conference: {
|
|
3366
|
-
destinations: [
|
|
3367
|
-
{ id: "tier2", label: "Tier 2 support" },
|
|
3368
|
-
{ id: "main-menu", label: "Back to the main menu" },
|
|
3369
|
-
],
|
|
3370
|
-
},
|
|
3371
|
-
}
|
|
3372
|
-
```
|
|
3373
|
-
|
|
3374
|
-
| Field | Contract |
|
|
3375
|
-
| --- | --- |
|
|
3376
|
-
| `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
|
|
3377
|
-
| `id` | What Omni sends as `destinationId` on a `conference` command, and what the provider executes. |
|
|
3378
|
-
| `label` | What the agent reads. |
|
|
3379
|
-
|
|
3380
|
-
**The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
|
|
3381
|
-
skill group -- that is the queue's configuration and the provider's business. The agent sees a
|
|
3382
|
-
list of labels, picks one, and Omni sends its `id`; the provider executes whatever the item is. No
|
|
3383
|
-
kind travels, nothing is typed, and a control with an empty directory has nothing to offer and is
|
|
3384
|
-
refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
|
|
3385
|
-
reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
|
|
3386
|
-
|
|
3387
|
-
**The provider chooses the supported destinations.** A published item may route to an IVR,
|
|
3388
|
-
queue, named agent or another configured destination. No destination kind is inferred from its
|
|
3389
|
-
label. The agent application offers only the directory attached to this task's `conference` capability,
|
|
3390
|
-
and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
|
|
3391
|
-
The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
|
|
3392
|
-
end the caller journey; a later assignment to the same agent is a new interaction.
|
|
3393
|
-
|
|
3394
|
-
#### Ending a call, and removing one person from it
|
|
3395
|
-
|
|
3396
|
-
A call has everyone on `Task.onCall`, and three commands take people off it, all performed by the
|
|
3397
|
-
provider. Two of them end the agent's part; they differ in what happens to the caller.
|
|
3398
|
-
|
|
3399
|
-
```ts
|
|
3400
|
-
{ type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
|
|
3401
|
-
{ type: "terminate-call" } // every channel on the call ends
|
|
3402
|
-
{ type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
|
|
3403
|
-
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
3404
|
-
```
|
|
3405
|
-
|
|
3406
|
-
**`end-call` ends my part.** The agent's channel and every channel the agent added -- a
|
|
3407
|
-
conferenced colleague, a lead who joined -- end, and the caller continues on the path the
|
|
3408
|
-
provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
|
|
3409
|
-
|
|
3410
|
-
**`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
|
|
3411
|
-
the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
|
|
3412
|
-
line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
|
|
3413
|
-
different thing from putting my own side down.
|
|
3414
|
-
|
|
3415
|
-
Both are commands to the provider through `execute`, and nothing happens on the desk until the
|
|
3416
|
-
provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
|
|
3417
|
-
any wrap allowance runs, and the agent completes. Neither is `task-ended`, and `complete` is
|
|
3418
|
-
neither of them. Both capabilities are the queue's to grant and a level's to lock, and both may
|
|
3419
|
-
change while the task is open: the provider republishes the task at the moment a permission
|
|
3420
|
-
changes, and a command that arrives after its capability was withdrawn is refused, as **Task
|
|
3421
|
-
capabilities** sets out. The provider knows which channels are the agent's and which were added;
|
|
3422
|
-
the agent application sends the command and no channel list, and the provider rechecks ownership
|
|
3423
|
-
when it acts.
|
|
3424
|
-
|
|
3425
|
-
`conference` `remove` takes one person off, named as the room names them, and the call goes on
|
|
3426
|
-
for the rest; it is gated by `conference`, since it only means something with a third person on
|
|
3427
|
-
the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
|
|
3428
|
-
remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
|
|
3429
|
-
it `failed`.
|
|
3430
|
-
|
|
3431
|
-
**There is no transfer.** The agent hands the call to nobody, cold or warm: help arrives by
|
|
3432
|
-
**Lead assist**, where a lead joins the call or takes it over, and a call that changes hands does
|
|
3433
|
-
so on the platform's own routing -- an IVR return, a queue, a lead's take-over -- with the record
|
|
3434
|
-
saying so in a `transferred` step and the `transfers` total. Nothing the agent commands moves the
|
|
3435
|
-
caller to somebody else.
|
|
3436
|
-
|
|
3437
|
-
#### Scheduling a follow-up
|
|
3438
|
-
|
|
3439
|
-
An agent on a call promises to call back on Thursday, or in wrap writes up that the customer wants
|
|
3440
|
-
a callback once the refund lands. The follow-up goes on the calendar, and the platform owns the
|
|
3441
|
-
calendar: `schedule` asks the provider to put it there. A chat or an email agent promises a
|
|
3442
|
-
callback as often, so the control is on every channel.
|
|
3443
|
-
|
|
3444
|
-
```ts
|
|
3445
|
-
// From the call or the conversation, or from its wrap: the time, and a note where the agent wrote one.
|
|
3446
|
-
{ type: "schedule", at: "2026-08-28T10:00:00Z", note: "Call back about the refund" }
|
|
3447
|
-
```
|
|
3448
|
-
|
|
3449
|
-
`schedule` is gated by the `schedule` capability and issuable in `in-progress`, `paused` or
|
|
3450
|
-
`completing` (`command.phase.interaction`): a follow-up is promised on the call and written up in
|
|
3451
|
-
wrap alike, and the task names the party it is for. It lands on the calendar, so a task may offer
|
|
3452
|
-
it only under a manifest that declares the `calendar` idle capability
|
|
3453
|
-
(`task.capability.calendar.required`), as a control that dials needs `dialOutcomes`; a locked
|
|
3454
|
-
`schedule` is still a declared one. The command carries a time and a note and nothing else
|
|
3455
|
-
(`command.schedule.at`, `command.schedule.note`, `command.field`): what the platform makes of a
|
|
3456
|
-
follow-up -- a campaign record, a reminder, a scheduled dial -- is its own, and the agent sees it
|
|
3457
|
-
on the calendar.
|
|
3458
|
-
|
|
3459
|
-
**`applied` says the follow-up is on the calendar**, and the `calendar-updated` that shows it
|
|
3460
|
-
follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
|
|
3461
|
-
the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
|
|
3462
|
-
clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
|
|
3463
|
-
not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
|
|
3464
|
-
at a time it takes from the provider's own publication, and holds the provider to the same bound
|
|
3465
|
-
(`drive.schedule.unsettled`).
|
|
3466
|
-
|
|
3467
|
-
### Chat capabilities
|
|
3468
|
-
|
|
3469
|
-
| Capability | Omni UI | Contract |
|
|
3470
|
-
| --- | --- | --- |
|
|
3471
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
|
|
3472
|
-
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
3473
|
-
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the conversation or from its wrap. See **Scheduling a follow-up**. |
|
|
3474
|
-
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3475
|
-
|
|
3476
|
-
### Email capabilities
|
|
3477
|
-
|
|
3478
|
-
| Capability | Omni UI | Contract |
|
|
3479
|
-
| --- | --- | --- |
|
|
3480
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
|
|
3481
|
-
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the message or from its wrap. See **Scheduling a follow-up**. |
|
|
3482
|
-
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3483
|
-
|
|
3484
|
-
### Custom capabilities
|
|
3485
|
-
|
|
3486
|
-
Every task may publish additional provider-specific controls in `capabilities.custom`:
|
|
3487
|
-
|
|
3488
|
-
```ts
|
|
3489
|
-
capabilities: {
|
|
3490
|
-
hold: true,
|
|
3491
|
-
custom: [
|
|
3492
|
-
{ id: "request-supervisor", ui: { control: "button", label: "Request supervisor", placement: "secondary" } },
|
|
3493
|
-
{ id: "mark-vip", ui: { control: "toggle", label: "Mark as VIP", placement: "overflow" } },
|
|
3494
|
-
],
|
|
3495
|
-
}
|
|
3496
|
-
```
|
|
3497
|
-
|
|
3498
|
-
Custom capability IDs must be non-empty and unique within the task. `ui.control` is `button`, `toggle`,
|
|
3499
|
-
or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
|
|
3500
|
-
control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
|
|
3501
|
-
— a tab in the same work area as the task's browsers, beside them, and alone on a task that has
|
|
3502
|
-
none; stated on every control, never defaulted (`task.custom.ui.render`). `prompt.fields` are what the agent supplies before the action runs — a
|
|
3503
|
-
destination number, a reference — as `CredentialField`s Omni renders as a form; the values travel
|
|
3504
|
-
on the custom command under the fields' names, as strings, and Omni sends the command only once
|
|
3505
|
-
every `required` field has a value. The provider validates what arrives as it validates any
|
|
3506
|
-
command; a form is a declaration, not a contract for what the agent typed. Omni renders the
|
|
3507
|
-
control and invokes it with the shared custom task command:
|
|
3508
|
-
|
|
3509
|
-
```ts
|
|
3510
|
-
{
|
|
3511
|
-
type: "custom",
|
|
3512
|
-
name: "request-supervisor",
|
|
3513
|
-
}
|
|
3514
|
-
```
|
|
3515
|
-
|
|
3516
|
-
`name` is the `id` of the custom capability the agent used. There is no `assignmentId` here: like every
|
|
3517
|
-
other command it travels on the `TaskCommandRequest` around it. A `toggle` carries the state it
|
|
3518
|
-
wants — `{ type: "custom", name: "mark-vip", on: true }` — never a flip, for the reason under
|
|
3519
|
-
**Task commands**.
|
|
3520
|
-
|
|
3521
|
-
Custom capabilities must not redefine the meaning of a standard channel capability.
|
|
3522
|
-
|
|
3523
|
-
## Idle actions
|
|
3524
|
-
|
|
3525
|
-
### `dial(request)`
|
|
3526
|
-
|
|
3527
|
-
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
3528
|
-
declares `dial`.
|
|
3529
|
-
|
|
3530
|
-
- `dialId` is the agent application's identity for this dial, minted before the request leaves.
|
|
3531
|
-
- `destination` is the original number selected or entered by the agent.
|
|
3532
|
-
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
3533
|
-
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
3534
|
-
- `dialling` says the request was accepted and the call is being placed, and restates the
|
|
3535
|
-
`dialId`. It says nothing about whether anyone will answer.
|
|
3536
|
-
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
3537
|
-
|
|
3538
|
-
The resulting call is offered through the normal `task-offered` event, and how the dial ended
|
|
3539
|
-
arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
|
|
3540
|
-
inside Omni.
|
|
3541
|
-
|
|
3542
|
-
## Every dial has an outcome
|
|
3543
|
-
|
|
3544
|
-
Four commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a
|
|
3545
|
-
`conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
|
|
3546
|
-
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
3547
|
-
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
3548
|
-
`dialling`, then says how it ended, once.
|
|
3549
|
-
|
|
3550
|
-
**The agent application mints the identity.** Every command that dials carries a `dialId` the agent application made, unique
|
|
3551
|
-
across the installation, and it travels four ways so no leg is a lookup:
|
|
3552
|
-
|
|
3553
|
-
```ts
|
|
3554
|
-
declare const connection: Connection<"voice">;
|
|
3555
|
-
declare const assignmentId: AssignmentId;
|
|
3556
|
-
|
|
3557
|
-
// 1. Out, minted by the agent application.
|
|
3558
|
-
const result = await connection.execute({ assignmentId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
3559
|
-
|
|
3560
|
-
// 2. Back on the result, restated, so the agent application compares and confirms.
|
|
3561
|
-
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
3562
|
-
|
|
3563
|
-
// 3. On the outcome, however late, against the task it belonged to.
|
|
3564
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3565
|
-
|
|
3566
|
-
// 4. In the record, so a dial made before a take-over is placeable by whoever holds the task now.
|
|
3567
|
-
const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
3568
|
-
```
|
|
3569
|
-
|
|
3570
|
-
The identity is a correlation handle between agent application and provider and **is never shown to the
|
|
3571
|
-
agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
3572
|
-
mint -- a dial made by the previous agent before a take-over -- it finds on the task's `onCall` or in
|
|
3573
|
-
its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
3574
|
-
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
3575
|
-
|
|
3576
|
-
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
3577
|
-
`{ status: "dialling", dialId }`, restating the agent application's identity (`result.dialId`,
|
|
3578
|
-
`result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
|
|
3579
|
-
nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
|
|
3580
|
-
`dialId` the agent application sent for that reason.
|
|
3581
|
-
|
|
3582
|
-
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
3583
|
-
there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
|
|
3584
|
-
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
|
|
3585
|
-
`answered` is stated rather than read off somebody appearing on the call, because an agent application that infers
|
|
3586
|
-
success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
|
|
3587
|
-
before anyone answered -- the agent hanging up while a conferenced colleague still rings -- which is not a
|
|
3588
|
-
`no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
|
|
3589
|
-
those named here: a statement about the switch, sent instead of the nearest cause it did not give.
|
|
3590
|
-
`reason` is the switch's own words, optional, and shown to the agent attributed to the switch rather
|
|
3591
|
-
than to Omni. The two are different things and may travel together: a switch can say "could not
|
|
3592
|
-
create dialog" in words and still name no cause a code would carry, so `unexplained` with a
|
|
3593
|
-
`reason` is a dial the switch described but did not classify, and a desk renders the words without
|
|
3594
|
-
inventing the class.
|
|
3595
|
-
|
|
3596
|
-
**`assignmentId` is the task the dial was placed on**, resolved from the dial's own identity and never from
|
|
3597
|
-
whatever task the agent is looking at when the outcome arrives. That task may already have ended,
|
|
3598
|
-
and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
3599
|
-
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
|
|
3600
|
-
call it belonged to and never against the next one.
|
|
3601
|
-
|
|
3602
|
-
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
|
|
3603
|
-
places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
|
|
3604
|
-
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- an
|
|
3605
|
-
idle dialpad call, which has no task yet -- or whose entry has already left the room; an
|
|
3606
|
-
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
3607
|
-
|
|
3608
|
-
**`answered` says what happened to the dial; `onCall` says who is in the room.** They are two
|
|
3609
|
-
claims, and both are true at once when a leg answers and never joins. A desk shows `answered` as the
|
|
3610
|
-
dial's conclusion and says "on the call" only when the person appears on `Task.onCall`; the obvious
|
|
3611
|
-
reading of `answered` -- that they are on the line -- is the one it does not make.
|
|
3612
|
-
|
|
3613
|
-
**A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
|
|
3614
|
-
provider that cannot tell busy from unreachable must not pick one. The manifest declares
|
|
3615
|
-
`dialOutcomes`, and a `dial-outcome` carries only a declared member
|
|
3616
|
-
(`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
|
|
3617
|
-
success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
|
|
3618
|
-
a failure (`manifest.dialOutcomes.failure`). `unexplained` is declarable only beside a cause the
|
|
3619
|
-
provider does report -- `busy`, `no-answer`, `unreachable`, `rejected` or `cancelled`
|
|
3620
|
-
(`manifest.dialOutcomes.unexplained.alone`): a provider that reports causes may honestly say the
|
|
3621
|
-
switch gave none, while one that reports none would be sending the generic failure this set exists
|
|
3622
|
-
to refuse. A provider that dials at all declares it: an idle
|
|
3623
|
-
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `connectBack`
|
|
3624
|
-
or `conference` under a manifest without it is refused
|
|
3625
|
-
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
3626
|
-
error there.
|
|
3627
|
-
|
|
3628
|
-
**Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
|
|
3629
|
-
right now, replaced whole with the task like every other field, and present when the provider
|
|
3630
|
-
knows the room:
|
|
3631
|
-
|
|
3632
|
-
```ts
|
|
3633
|
-
const onCall: OnCall[] = [
|
|
3634
|
-
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
3635
|
-
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
3636
|
-
{ role: "conferenced", destinationId: "tier2", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
|
|
3637
|
-
{ role: "conferenced", destinationId: "main-menu", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
|
|
3638
|
-
];
|
|
3639
|
-
```
|
|
3640
|
-
|
|
3641
|
-
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
3642
|
-
neither carries a destination. A `conferenced` colleague was, so the entry carries the
|
|
3643
|
-
`destinationId` of the directory item and, where an agent application's dial brought them, its `dialId`; a person
|
|
3644
|
-
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so an agent application that attaches
|
|
3645
|
-
after a take-over reads the room from here rather than inferring it from a sequence of outcomes it
|
|
3646
|
-
never saw.
|
|
3647
|
-
|
|
3648
|
-
**`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
|
|
3649
|
-
and added channels remain connected, the remaining room is still valid. When this interaction's
|
|
3650
|
-
audio ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
|
|
3651
|
-
provider that never publishes the room. This does not assert that the caller, bridge or other
|
|
3652
|
-
agents' channels ended. A published room receives a final empty view for this interaction, and the
|
|
3653
|
-
task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
|
|
3654
|
-
|
|
3655
|
-
**A field that describes the present is cleared by the transition that ends it.** `onCall`,
|
|
3656
|
-
`previewEndsInSeconds` and `atDeadline` are three instances of one shape, and there will be more: each
|
|
3657
|
-
describes a state that is true now, and each is carried past the moment it stops being true by the
|
|
3658
|
-
most natural implementation there is, building the next task by spreading the last one. The
|
|
3659
|
-
provider's own state looks right, the claim on the wire is stale, and only the agent application sees it. An
|
|
3660
|
-
adapter that builds each transition from what is true after it, rather than from what was true
|
|
3661
|
-
before, needs none of these rules named. Two agents were
|
|
3662
|
-
once shown on calls for the better part of an hour while callers queued, because the last message
|
|
3663
|
-
describing the room live was never followed by one saying it had emptied, and every consumer that
|
|
3664
|
-
derived anything from it was wrong in a way that looked like its own bug.
|
|
3665
|
-
|
|
3666
|
-
**A dialled entry is listed from the moment the dial is placed, not from the answer.** The entry is
|
|
3667
|
-
what `conference` `remove` names, and a destination the agent cannot call off while it rings is a
|
|
3668
|
-
customer kept waiting with no way back -- which is the very case `cancelled` exists for. The
|
|
3669
|
-
`conferenced` entry appears with its `dialId` when the dial is placed, and a `conference` `remove`
|
|
3670
|
-
naming a still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
|
|
3671
|
-
|
|
3672
|
-
**The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
|
|
3673
|
-
answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
|
|
3674
|
-
pairing as `task-audio-started` and `audio`: an `answered` outcome and the task restated with the
|
|
3675
|
-
entry `joined` say one thing twice, and any other outcome removes the entry. The outcome comes
|
|
3676
|
-
first: a `task-updated` that itself moves an entry from `ringing` to `joined` before an `answered`
|
|
3677
|
-
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `audio` is.
|
|
3678
|
-
The rule keys on the entry's `dialId`, so an entry the platform added itself, with none, is one
|
|
3679
|
-
whose move the stream cannot hold to an outcome: a clean stream proves nothing about ordering there. It is stated rather
|
|
3680
|
-
than left to whoever placed the dial because that knowledge lives in one process: after a reload,
|
|
3681
|
-
on a second session, or on a supervisor's screen the snapshot is the only source, and a room that
|
|
3682
|
-
cannot tell ringing from joined says it contains someone who may never arrive. Nobody ringing is
|
|
3683
|
-
held (`task.onCall.held.ringing`). A colleague who never answers is a dial that ended: the
|
|
3684
|
-
provider restates the room without the entry, and the agent is back with the customer.
|
|
3685
|
-
|
|
3686
|
-
The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
|
|
3687
|
-
they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
|
|
3688
|
-
belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
|
|
3689
|
-
agent's.
|
|
3690
|
-
|
|
3691
|
-
## Breaks
|
|
3692
|
-
|
|
3693
|
-
Everything about an agent's breaks on one provider arrives as one object, `Snapshot.break`,
|
|
3694
|
-
replaced whole by a single `break-state` event. These facts are only meaningful together — an
|
|
3695
|
-
approval says nothing without knowing whether the agent chose the break, and a list of reasons says
|
|
3696
|
-
nothing while none are being accepted — so they are not published separately.
|
|
3697
|
-
|
|
3698
|
-
| Field | Contract |
|
|
3699
|
-
| --- | --- |
|
|
3700
|
-
| `status` | Where the agent’s break lifecycle stands. See the states below. |
|
|
3701
|
-
| `canRequestBreak` | Whether the agent may ask at all. Distinct from `status`. |
|
|
3702
|
-
| `requestUnavailableReason` | Display-ready reason shown when `canRequestBreak` is false — a standing gate that applies to everyone. |
|
|
3703
|
-
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
3704
|
-
| `retryRequestAfterMs` | Milliseconds until the agent may retry a break request, when the provider can say. |
|
|
3705
|
-
| `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
|
|
3706
|
-
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `on-break` or `starting-after-task` where the provider publishes `reasons`, a forced break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
|
|
3707
|
-
| `forced` | Set when the break was forced on the agent rather than requested. |
|
|
3708
|
-
|
|
3709
|
-
A request can be waiting for two unrelated things, and they are separate values because
|
|
3710
|
-
rendering one as the other tells an agent to wait for somebody who is never coming:
|
|
3711
|
-
|
|
3712
|
-
| `status` | Meaning |
|
|
3713
|
-
| --- | --- |
|
|
3714
|
-
| `not-requested` | No request outstanding. |
|
|
3715
|
-
| `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
|
|
3716
|
-
| `granted` | The request was granted, by a person or by an `automatically-approved` policy. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
|
|
3717
|
-
| `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `on-break`. |
|
|
3718
|
-
| `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a call a lead took over, which the provider assigns whatever break the lead is on -- see **Lead assist**. |
|
|
3719
|
-
|
|
3720
|
-
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
3721
|
-
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
3722
|
-
answer and ask again when they want to. `decisionReason` may carry the words attached to that
|
|
3723
|
-
decision, but `status` does not remain denied.
|
|
3724
|
-
|
|
3725
|
-
A provider reports `starting-after-task` only after Omni commits a `granted` request while
|
|
3726
|
-
work is still active. Omni does not send the request again, because asking again would not move
|
|
3727
|
-
it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
|
|
3728
|
-
|
|
3729
|
-
`canRequestBreak: false` is what lets Omni withdraw the control rather than let an agent ask and be
|
|
3730
|
-
refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
|
|
3731
|
-
something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
|
|
3732
|
-
|
|
3733
|
-
### Forced breaks
|
|
3734
|
-
|
|
3735
|
-
Migration: the former awaiting-decision value is now `awaiting-approval` in break status
|
|
3736
|
-
and the team member’s pending break state. The old value is rejected without an alias;
|
|
3737
|
-
the request can still be granted or denied, and ordering rules are unchanged.
|
|
3738
|
-
|
|
3739
|
-
Migration: the former BreakApproval type is now `BreakStatus`, matching `BreakState.status`.
|
|
3740
|
-
The former type is not exported as an alias; its values and behavior are unchanged.
|
|
3741
|
-
|
|
3742
|
-
Migration: `BreakState.status` replaces the former approval field. It covers the full break
|
|
3743
|
-
lifecycle. The old field is rejected even alongside status; related diagnostics now use status.
|
|
3744
|
-
The `BreakStatus` union and its values, `reasons`, `activeReasonId`, and the separate testing
|
|
3745
|
-
helper BreakOnTaskStep.approval are unchanged. Hosts and providers must update together.
|
|
3746
|
-
|
|
3747
|
-
Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
|
|
3748
|
-
with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
|
|
3749
|
-
of milliseconds; the former break field is rejected even alongside the new field.
|
|
3750
|
-
The separate authentication and general failure retryAfterMs fields are unchanged.
|
|
3751
|
-
|
|
3752
|
-
Migration: the former in-effect break approval value is now `on-break`. It means the
|
|
3753
|
-
agent is on the break, not merely granted permission. The old value is rejected without an
|
|
3754
|
-
alias. The task prerequisite, the take-over exception and ordering rules are unchanged;
|
|
3755
|
-
the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
|
|
3756
|
-
`on-break` and is unchanged.
|
|
3757
|
-
|
|
3758
|
-
Migration: the former refusedReason field is now `BreakState.requestUnavailableReason`.
|
|
3759
|
-
It explains why requests are unavailable when `canRequestBreak` is false; `decisionReason`
|
|
3760
|
-
continues to explain a particular approval or denial. The old field is rejected even alongside
|
|
3761
|
-
the new one, and related diagnostics use requestUnavailableReason.
|
|
3762
|
-
|
|
3763
|
-
Migration: the former mayAsk field is now `BreakState.canRequestBreak`. The old field is
|
|
3764
|
-
rejected even beside the new field. Validation diagnostics use canRequestBreak in place of
|
|
3765
|
-
mayAsk. Request eligibility remains distinct from approval; existing alwaysAvailable reason
|
|
3766
|
-
exceptions are unchanged. Hosts and providers must update together.
|
|
3767
|
-
|
|
3768
|
-
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
3769
|
-
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced`. The former agent-end prohibition
|
|
3770
|
-
has been removed: forced breaks also permit explicit agent resumption. Update hosts and providers together. The old field
|
|
3771
|
-
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
3772
|
-
The break policy value formerly named auto-approve is now `automatically-approved`.
|
|
3773
|
-
Requests are approved automatically; approval does not skip commitment or the prerequisites
|
|
3774
|
-
for starting a break. The former value is rejected without an alias.
|
|
3775
|
-
The break policy value formerly named suspended is now `requests-suspended`. The former
|
|
3776
|
-
value is rejected without an alias. It suspends new requests, not breaks already underway.
|
|
3777
|
-
The break policy value formerly named ask is now `approval-required`: requests require
|
|
3778
|
-
approval. The former value is rejected without an alias; automatically-approved and requests-suspended retain
|
|
3779
|
-
their existing behavior.
|
|
3780
|
-
The team break command formerly named policy is now `set-break-policy`. The former
|
|
3781
|
-
command is rejected without an alias; its policy field is unchanged. Hosts and providers must adopt the new name together.
|
|
3782
|
-
The team break command formerly named decide is now `decide-break-request`. The former
|
|
3783
|
-
spelling is rejected without an alias; the granted/denied decisions and pending-request
|
|
3784
|
-
prerequisite are unchanged.
|
|
3785
|
-
The team break command formerly named place is now `force-break`. Hosts and providers must
|
|
3786
|
-
adopt the new command together; place and the interim force spelling are rejected without aliases.
|
|
3787
|
-
The team break command formerly named release is now `end-forced-break`, and its diagnostic is
|
|
3788
|
-
`team.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
3789
|
-
The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
|
|
3790
|
-
|
|
3791
|
-
`ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
|
|
3792
|
-
`expectedEndsInSeconds`: whole seconds from the publication that carries it, restated on every
|
|
3793
|
-
publication, so every screen -- the agent's, reloaded or not, and every lead's -- counts down the
|
|
3794
|
-
same number from receipt (`break.forced.expectedEndsInSeconds`). The duration the lead asked for
|
|
3795
|
-
travels on the `force-break` command alone; the state says what is left, and drops the field once
|
|
3796
|
-
it has run out. The agent must explicitly resume when ready. The retired endsAutomatically and
|
|
3797
|
-
endsAt fields are rejected, including when an expectation is also provided. Who forced it is
|
|
3798
|
-
`by`, as **A forced break is the provider's act** below sets out.
|
|
3799
|
-
|
|
3800
|
-
Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
|
|
3801
|
-
a display name.
|
|
3802
|
-
|
|
3803
|
-
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
3804
|
-
break in progress or about to be; beside `granted` or `awaiting-approval` the agent application would read a
|
|
3805
|
-
request the agent never made and commit it (`break.forced.status`). Where the provider publishes
|
|
3806
|
-
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
3807
|
-
|
|
3808
|
-
For example:
|
|
3809
|
-
|
|
3810
|
-
```ts
|
|
3811
|
-
forced: {
|
|
3812
|
-
by: "manager-1042",
|
|
3813
|
-
expectedEndsInSeconds: 600
|
|
3814
|
-
}
|
|
3815
|
-
```
|
|
3816
|
-
|
|
3817
|
-
The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
|
|
3818
|
-
Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
|
|
3819
|
-
work. The expectation is whole seconds still to run, restated on every publication; omission
|
|
3820
|
-
means none was stated, or it has run out. Neither its reaching zero nor an overdue indication
|
|
3821
|
-
authorizes the agent application or provider to end the break, restore availability, or route
|
|
3822
|
-
work to the agent.
|
|
3823
|
-
|
|
3824
|
-
**A forced break is the provider's act.** A lead's `force-break` is a command to the provider,
|
|
3825
|
-
which forces the break on the member; `by` names the lead who asked it to. A break the platform
|
|
3826
|
-
imposed on its own -- a schedule, a compliance hold -- says `by: "provider"`, and is the platform's
|
|
3827
|
-
to lift: a lead's `end-forced-break` on it is refused before it is sent
|
|
3828
|
-
(`team.command.endForcedBreak.provider`), and a provider that receives one answers `failed` with
|
|
3829
|
-
`omni.break-forced-by-provider`. The desk offers End forced break on a member only where
|
|
3830
|
-
`forced.by` names a lead.
|
|
3831
|
-
|
|
3832
|
-
**A lead lifting the restriction does not resume the agent.** On an applied
|
|
3833
|
-
`end-forced-break`, the provider clears `BreakState.forced` while preserving the current
|
|
3834
|
-
`on-break` or `starting-after-task` status and `activeReasonId`. This command cannot publish
|
|
3835
|
-
`not-requested`, restore readiness, or start routing work. Ordinary progress from
|
|
3836
|
-
`starting-after-task` to `on-break` still happens when work finishes. The agent application follows the
|
|
3837
|
-
published state and waits for the agent's explicit Resume, even after reconnect. Historical
|
|
3838
|
-
attribution is not rewritten by clearing the current forced marker.
|
|
3839
|
-
|
|
3840
|
-
The agent may explicitly resume either before or after the lead lifts the restriction. A lead
|
|
3841
|
-
command, timer, late snapshot or reconnect is never a substitute for that choice. Providers
|
|
3842
|
-
must correlate resumption to the authenticated agent's action; a state-only ordering validator
|
|
3843
|
-
cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
|
|
3844
|
-
still applies, and work resumes only on confirmed provider state.
|
|
3845
|
-
|
|
3846
|
-
An agent application counts `expectedEndsInSeconds` down from receipt, on every publication that
|
|
3847
|
-
carries it, and shows nothing of it where the provider sends none. It invents no start and no
|
|
3848
|
-
return time, and nothing acts on the number reaching zero.
|
|
3849
|
-
|
|
3850
|
-
A break applies to the **agent**, not to one provider. When a provider forces one, Omni stops the
|
|
3851
|
-
agent everywhere else at once, and not by asking: it states `setCapacity({ count: 0 })` on every
|
|
3852
|
-
other usable provider holding capacity, which is taken and never refused, and which their team
|
|
3853
|
-
lists show as `elsewhere`. Nobody on those providers can deny it, and the agent cannot cancel it,
|
|
3854
|
-
because there is nothing to cancel: a break request on them would be the agent's own to withdraw
|
|
3855
|
-
and a lead's to refuse, and a forced break is neither. When the forcing provider ends the forced
|
|
3856
|
-
break and the agent resumes there, the agent application restates its capacity on the rest. A
|
|
3857
|
-
provider whose login is not usable is left alone, as a break attempt leaves it;
|
|
3858
|
-
`assertForcedBreakStopsTheRest` holds an agent application to this set. **The provider keeps its
|
|
3859
|
-
shift totals true.** A member held elsewhere is one the provider sees -- `elsewhere` is a state it
|
|
3860
|
-
publishes -- and the day's `shift` is its own account of the day, so the time the agent was
|
|
3861
|
-
stopped for a break forced on another provider is the provider's to count, as a break of a kind
|
|
3862
|
-
its platform has, or introduces, for exactly this. A `breakSeconds` that leaves it out is a total
|
|
3863
|
-
the provider knew to be short, and **Never report a value you cannot observe** cuts the other way
|
|
3864
|
-
here: the provider observed it.
|
|
3865
|
-
|
|
3866
|
-
`ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
|
|
3867
|
-
did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
|
|
3868
|
-
is owed for a person, never for `provider` (`break.forced.by`).
|
|
3869
|
-
|
|
3870
|
-
## Capacity and break actions
|
|
3871
|
-
|
|
3872
|
-
**Each method answers in its own words.** A result is read by a person far more often than it is
|
|
3873
|
-
branched on by code — in a log, a support ticket, a conformance failure — so it says what happened
|
|
3874
|
-
rather than that something happened. `failed` is shared, because failing is the same act
|
|
3875
|
-
everywhere; success is not.
|
|
3876
|
-
|
|
3877
|
-
| Method | Succeeded |
|
|
3878
|
-
| --- | --- |
|
|
3879
|
-
| `setCapacity` | `applied` |
|
|
3880
|
-
| `recordStep` | `recorded` |
|
|
3881
|
-
| `requestBreak` | `requested` |
|
|
3882
|
-
| `commitBreak` | `committed` |
|
|
3883
|
-
| `cancelBreak` | `cancelled` |
|
|
3884
|
-
| `endBreak` | `ended` |
|
|
3885
|
-
|
|
3886
|
-
`failed` carries a typed `ProtocolFailure` and means the provider did not take the action, whether
|
|
3887
|
-
it would not or could not.
|
|
3888
|
-
|
|
3889
|
-
**Succeeding is not the outcome.** `requested` says the provider holds the request, not that a
|
|
3890
|
-
break was granted; `ended` says the provider has the instruction, not that the agent is working
|
|
3891
|
-
again.
|
|
3892
|
-
Every break method reports its real result through `break-state`; `setCapacity` reports none at
|
|
3893
|
-
all, because capacity is a statement rather than a request.
|
|
3894
|
-
|
|
3895
|
-
`execute` keeps `applied` rather than a verb per command, because the command
|
|
3896
|
-
is in the request: `execute({ command: { type: "hold" } })` returning `applied` already says the
|
|
3897
|
-
hold applied. A `held` result would repeat the discriminant that travelled with it.
|
|
3898
|
-
|
|
3899
|
-
### `setCapacity(capacity)`
|
|
3900
|
-
|
|
3901
|
-
States how many tasks this provider may have assigned to the agent **at once**.
|
|
3902
|
-
|
|
3903
|
-
`count` is an absolute ceiling, not an increment, a whole number of zero or more
|
|
3904
|
-
(`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
|
|
3905
|
-
stated when the agent is set up and restated only when it genuinely changes, which is a
|
|
3906
|
-
local policy change rather than a task starting or ending -- with one exception below.
|
|
3907
|
-
|
|
3908
|
-
**The provider counts its own outstanding tasks against it.** Assign while you hold fewer than
|
|
3909
|
-
`count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
|
|
3910
|
-
room again and need no new signal to know it. Omni does not re-state capacity as tasks come and
|
|
3911
|
-
go, and a provider that waits for it will stall.
|
|
3912
|
-
|
|
3913
|
-
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
3914
|
-
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3915
|
-
providers, and the agent application divides their capacity among them rather than telling each the whole. A
|
|
3916
|
-
provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
|
|
3917
|
-
the member as `elsewhere` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
|
|
3918
|
-
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3919
|
-
count as any other when the agent application has capacity for this provider again. Zero is the one restatement
|
|
3920
|
-
that follows work rather than local policy.
|
|
3921
|
-
|
|
3922
|
-
**A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
|
|
3923
|
-
statement has no failure to report, and a `failed` arm gave four agent application-provider pairings four
|
|
3924
|
-
readings of which ceiling stood after it. A provider that cannot carry the count assigns within
|
|
3925
|
-
what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
|
|
3926
|
-
refusal.
|
|
3927
|
-
|
|
3928
|
-
Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
|
|
3929
|
-
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
3930
|
-
it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
|
|
3931
|
-
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
3932
|
-
both ways after the drive, two then one then nought, and an offer after a lower count is caught
|
|
3933
|
-
against it (`stream.taskOffered.overCapacity`).
|
|
3934
|
-
|
|
3935
|
-
**Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
|
|
3936
|
-
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
3937
|
-
it: the ceiling binds assignment, not the agent's own hand.
|
|
3938
|
-
|
|
3939
|
-
### Runtime break prerequisite checks
|
|
3940
|
-
|
|
3941
|
-
Use `validateBreakCommand(method, request, state, context)` from the validation entry point
|
|
3942
|
-
before any agent break dispatch. The context supplies current authentication and transport;
|
|
3943
|
-
all four methods require the live login's break capability and active transport. Pass the
|
|
3944
|
-
request object only to the request method, and undefined to the other three.
|
|
3945
|
-
|
|
3946
|
-
| Method | Required current state |
|
|
3947
|
-
| --- | --- |
|
|
3948
|
-
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
|
|
3949
|
-
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
|
|
3950
|
-
| `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3951
|
-
| `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
|
|
3952
|
-
|
|
3953
|
-
Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
|
|
3954
|
-
decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
|
|
3955
|
-
the live `lead` flag and active transport, a current team member list for any command naming a
|
|
3956
|
-
member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
|
|
3957
|
-
awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
|
|
3958
|
-
forced committed break; join and decline need the member's request on the list; coach, join-call
|
|
3959
|
-
and leave need the lead already on that member's call; an assignment named must be one the list
|
|
3960
|
-
shows the member holding. The context's target state must belong to the named member; that
|
|
3961
|
-
association and backend authorization are provider responsibilities.
|
|
3962
|
-
|
|
3963
|
-
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
3964
|
-
as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
|
|
3965
|
-
checks. Pending and completing tasks still count as work. Only a call a lead took over
|
|
3966
|
-
(`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
|
|
3967
|
-
|
|
3968
|
-
These validators report violations without dispatching, rewriting state or proving freshness.
|
|
3969
|
-
Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
|
|
3970
|
-
Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
|
|
3971
|
-
is not a grant, and no method response overwrites a newer state emission. Rejected promises
|
|
3972
|
-
remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
|
|
3973
|
-
durable mutually exclusive commit/cancel decision and reconciliation obligations under
|
|
3974
|
-
**Coordinating a multi-provider break** remain
|
|
3975
|
-
required; per-provider validation does not implement the multi-provider coordinator.
|
|
3976
|
-
|
|
3977
|
-
### `requestBreak(request)`
|
|
3978
|
-
|
|
3979
|
-
**Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
|
|
3980
|
-
A lead may likewise omit reason text from `force-break`. Do not require a text explanation
|
|
3981
|
-
just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
|
|
3982
|
-
it rather than sending an empty string. A selected `reasonId` identifies a published break
|
|
3983
|
-
choice and is separate from this optional text.
|
|
3984
|
-
|
|
3985
|
-
With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
|
|
3986
|
-
without published choices, `{}` is valid. A lead can similarly send a `force-break` command
|
|
3987
|
-
with its member ID and, where required, the selected reason ID, without reason text.
|
|
3988
|
-
|
|
3989
|
-
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
3990
|
-
offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
|
|
3991
|
-
provider reports `not-requested` directly, with `decisionReason` when one was supplied.
|
|
3992
|
-
|
|
3993
|
-
**An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
|
|
3994
|
-
workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
|
|
3995
|
-
any other and begins when the work ends: that is `starting-after-task`, and
|
|
3996
|
-
`assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
|
|
3997
|
-
|
|
3998
|
-
#### Break reasons
|
|
3999
|
-
|
|
4000
|
-
A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
|
|
4001
|
-
and Omni returns the agent's choice as `BreakRequest.reasonId`:
|
|
4002
|
-
|
|
4003
|
-
```ts
|
|
4004
|
-
break: {
|
|
4005
|
-
reasons: [
|
|
4006
|
-
{ id: "lunch", label: "Lunch" },
|
|
4007
|
-
{ id: "training", label: "Training", group: "Scheduled" },
|
|
4008
|
-
]
|
|
4009
|
-
}
|
|
4010
|
-
```
|
|
4011
|
-
|
|
4012
|
-
Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
|
|
4013
|
-
manifest because a provider may change the codes it offers during a shift; a `snapshot` event
|
|
4014
|
-
replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
|
|
4015
|
-
for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
|
|
4016
|
-
field.
|
|
4017
|
-
|
|
4018
|
-
#### One break across several providers
|
|
4019
|
-
|
|
4020
|
-
An agent connected to several providers takes **one** break, not one per platform. Omni gathers
|
|
4021
|
-
what every connected provider offers, shows the distinct breaks, and on a choice sends one request
|
|
4022
|
-
per provider: the same `reason`, and each provider's own `reasonId`.
|
|
4023
|
-
|
|
4024
|
-
To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
|
|
4025
|
-
|
|
4026
|
-
```ts
|
|
4027
|
-
break: {
|
|
4028
|
-
reasons: [
|
|
4029
|
-
{ id: "lunch", label: "Lunch", kind: "meal" },
|
|
4030
|
-
{ id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
|
|
4031
|
-
]
|
|
4032
|
-
}
|
|
4033
|
-
```
|
|
4034
|
-
|
|
4035
|
-
A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
|
|
4036
|
-
supports mapping them onto, and every member means something specific:
|
|
4037
|
-
|
|
4038
|
-
| Kind | What it means |
|
|
4039
|
-
| --- | --- |
|
|
4040
|
-
| `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
|
|
4041
|
-
| `meal` | A meal: lunch, dinner, whatever the shift calls it. |
|
|
4042
|
-
| `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
|
|
4043
|
-
| `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
|
|
4044
|
-
| `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
|
|
4045
|
-
| `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
|
|
4046
|
-
| `administrative` | Paperwork and follow-up not attached to a particular contact. |
|
|
4047
|
-
| `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
|
|
4048
|
-
| `personal` | Personal time the deployment does not classify further. |
|
|
4049
|
-
| `other` | None of the above. Matches nothing, including another provider's `other`. |
|
|
4050
|
-
|
|
4051
|
-
**The definitions are the point, not decoration.** Ten undefined strings would be the label
|
|
4052
|
-
problem one level up: two providers could both declare `technical`, one meaning a dead headset
|
|
4053
|
-
and the other scheduled maintenance, match on it, and nothing could tell the difference.
|
|
4054
|
-
|
|
4055
|
-
`training`, `coaching` and `meeting` can all fit one session, so take them in that order of
|
|
4056
|
-
specificity: about this agent's own work is `coaching`, else something they must know afterwards is
|
|
4057
|
-
`training`, else `meeting`.
|
|
4058
|
-
|
|
4059
|
-
**A break somebody forced on the agent is not automatically one of these.** Where the agent was
|
|
4060
|
-
stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
|
|
4061
|
-
omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
|
|
4062
|
-
agent", and `other` claims a classification that was never made.
|
|
4063
|
-
|
|
4064
|
-
Omni matches in this order:
|
|
4065
|
-
|
|
4066
|
-
1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
|
|
4067
|
-
label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
|
|
4068
|
-
and nobody has to word their codes the same way.
|
|
4069
|
-
2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
|
|
4070
|
-
counts as missing: two providers saying `"other"` have only said their break is not on the
|
|
4071
|
-
list, which is no evidence they mean the same thing.
|
|
4072
|
-
|
|
4073
|
-
Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
|
|
4074
|
-
platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
|
|
4075
|
-
that for next time.
|
|
4076
|
-
|
|
4077
|
-
Three rules for a provider:
|
|
4078
|
-
|
|
4079
|
-
- **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
|
|
4080
|
-
little precision. Getting it wrong sends an agent to lunch on one platform and to training on
|
|
4081
|
-
another, and nothing can detect that.
|
|
4082
|
-
- **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
|
|
4083
|
-
between them for the agent; saying they are the same kind is not saying they are interchangeable.
|
|
4084
|
-
- **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
|
|
4085
|
-
provider's wording they picked it from, sent unchanged to every provider in the break.
|
|
4086
|
-
|
|
4087
|
-
A provider that publishes no reason codes still receives the request, with `reason` set and
|
|
4088
|
-
`reasonId` omitted.
|
|
4089
|
-
|
|
4090
|
-
#### Coordinating a multi-provider break
|
|
4091
|
-
|
|
4092
|
-
Sending the requests is a two-phase operation, not independent best effort. A provider first grants
|
|
4093
|
-
permission for the agent to stop, then Omni either commits or cancels that permission.
|
|
4094
|
-
|
|
4095
|
-
`granted` is provider-visible state. It means only that this provider has granted the active
|
|
4096
|
-
request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
|
|
4097
|
-
or why Omni has not committed yet. While reporting `granted`, the provider remains available,
|
|
4098
|
-
continues to honour Omni's current capacity, and continues offering work normally.
|
|
4099
|
-
|
|
4100
|
-
Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
|
|
4101
|
-
`cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
|
|
4102
|
-
outstanding and offers **Cancel break request**, but does not tell the agent that their break has
|
|
4103
|
-
begun.
|
|
4104
|
-
|
|
4105
|
-
Omni offers the aggregate Break control only when every provider currently holding capacity
|
|
4106
|
-
declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
|
|
4107
|
-
knowingly permit partial availability.
|
|
4108
|
-
|
|
4109
|
-
Omni coordinates one break attempt as follows:
|
|
4110
|
-
|
|
4111
|
-
1. Freeze the providers the attempt asks: every connected provider from which the agent can
|
|
4112
|
-
currently receive work. A provider joining during the attempt is given no capacity until it
|
|
4113
|
-
finishes. A provider whose authentication is `expired` is not one the agent can receive work
|
|
4114
|
-
from and is not asked: nothing is asked of it, the break proceeds without it, and when the
|
|
4115
|
-
login is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
4116
|
-
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
4117
|
-
available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
|
|
4118
|
-
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
4119
|
-
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
|
|
4120
|
-
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
4121
|
-
causes Omni to take the cancel path.
|
|
4122
|
-
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
4123
|
-
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
4124
|
-
and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
|
|
4125
|
-
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
4126
|
-
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
4127
|
-
begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
|
|
4128
|
-
reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
|
|
4129
|
-
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
4130
|
-
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
4131
|
-
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
4132
|
-
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
4133
|
-
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
4134
|
-
`awaiting-approval` or `granted`. Work continues during cancellation because no stop was
|
|
4135
|
-
committed. Return to `working` only after no provider retains either state.
|
|
4136
|
-
|
|
4137
|
-
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
4138
|
-
it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
|
|
4139
|
-
A provider that reports `granted` must therefore preserve the request across reconnects and must
|
|
4140
|
-
honour a later commit or cancel. This
|
|
4141
|
-
durable promise prevents a provider from failing the commit after another provider has already
|
|
4142
|
-
stopped the agent.
|
|
4143
|
-
|
|
4144
|
-
#### Why the commit phase is bounded and the decision is not
|
|
4145
|
-
|
|
4146
|
-
Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
|
|
4147
|
-
durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
|
|
4148
|
-
or is uninstalled between granting and committing would hold Omni in `committing-break` with no
|
|
4149
|
-
exit: the providers that did commit have already stopped the agent, and the agent is neither
|
|
4150
|
-
working nor on a break.
|
|
4151
|
-
|
|
4152
|
-
Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
|
|
4153
|
-
on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
|
|
4154
|
-
routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
|
|
4155
|
-
for it costs the agent their break.
|
|
4156
|
-
|
|
4157
|
-
Setting a provider aside is not a rollback and not a cancel. The commit stands and the
|
|
4158
|
-
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
4159
|
-
snapshot before anything else, and the snapshot decides:
|
|
4160
|
-
|
|
4161
|
-
- `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
|
|
4162
|
-
- still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
|
|
4163
|
-
late behind it, the provider stops an agent who is already stopped; nothing happens, because a
|
|
4164
|
-
commit is a state to be in, not an act to be done.
|
|
4165
|
-
- `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
|
|
4166
|
-
against an agent who is already on break elsewhere. **A new login is this case too**: the grant
|
|
4167
|
-
belonged to the old session.
|
|
4168
|
-
|
|
4169
|
-
The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
|
|
4170
|
-
no capacity until it is reconciled.
|
|
4171
|
-
|
|
4172
|
-
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
4173
|
-
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
4174
|
-
would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
|
|
4175
|
-
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
4176
|
-
|
|
4177
|
-
Omni may tell the agent which platforms the break has not yet reached, as it already does when a
|
|
4178
|
-
break cannot be paired across every provider.
|
|
4179
|
-
|
|
4180
|
-
The decision phase needs no such bound, because nothing has stopped. Work continues throughout
|
|
4181
|
-
`requesting-break`, so a provider that never answers costs the agent a wait rather than their
|
|
4182
|
-
availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
|
|
4183
|
-
because no stop was ever committed.
|
|
4184
|
-
|
|
4185
|
-
If cancel races with a late approval, the provider remains on the cancel path. If commit has already
|
|
4186
|
-
won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
|
|
4187
|
-
returning the agent to `working`.
|
|
4188
|
-
|
|
4189
|
-
A forced break is not rolled back by this algorithm. If one appears during either the request or
|
|
4190
|
-
cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
|
|
4191
|
-
`setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
|
|
4192
|
-
is committed, because the agent has already stopped everywhere.
|
|
4193
|
-
|
|
4194
|
-
This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
|
|
4195
|
-
the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
|
|
4196
|
-
|
|
4197
|
-
#### Reporting the break the agent is on
|
|
4198
|
-
|
|
4199
|
-
`BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
|
|
4200
|
-
asked for, but only until the session ends — after a reload or reconnect, or where the provider put
|
|
4201
|
-
the agent on the break itself, the provider is the only one who knows.
|
|
4202
|
-
|
|
4203
|
-
Omit it when you cannot say, and when there is no break: reporting a reason alongside
|
|
4204
|
-
`status: "not-requested"` describes a break that is not happening, and is rejected.
|
|
4205
|
-
|
|
4206
|
-
### `cancelBreak()`
|
|
4207
|
-
|
|
4208
|
-
Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
|
|
4209
|
-
Cancellation releases the request but does not restore work because work never stopped. If
|
|
4210
|
-
commit already won, the provider returns `omni.break-already-committed`. The resulting state is
|
|
4211
|
-
reported through `break-state`.
|
|
4212
|
-
|
|
4213
|
-
### `commitBreak()`
|
|
4214
|
-
|
|
4215
|
-
Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
|
|
4216
|
-
business reason. On commit the provider stops offering new work and reports
|
|
4217
|
-
`starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
|
|
4218
|
-
Committing a break that is already in effect changes nothing and answers `committed`.
|
|
4219
|
-
|
|
4220
|
-
### `endBreak()`
|
|
4221
|
-
|
|
4222
|
-
Tells a provider that an agent already on a break wants to become available again. The provider
|
|
4223
|
-
reports the resulting state through `break-state` and provider status events.
|
|
4224
|
-
|
|
4225
|
-
It ends the break, which is the thing that started. Nothing about the connection was ever paused,
|
|
4226
|
-
so there is nothing on it to resume.
|
|
4227
|
-
|
|
4228
|
-
## Team leads
|
|
4229
|
-
|
|
4230
|
-
The team feature is the lead's own surface, beside the agent's and never mixed with it. The
|
|
4231
|
-
login's `lead` flag turns it on in the agent application; the lead retrieves their members and
|
|
4232
|
-
sees, for each, their assignments as they stand and the full history of each, and acts on them
|
|
4233
|
-
with the authority the flag carries. Nothing the lead does here is a task on their desk. A lead
|
|
4234
|
-
who also takes calls is an agent like any other on the agent side, and a lead who does not want
|
|
4235
|
-
the agent role for now places themself on a break of a working kind and keeps the team feature on.
|
|
4236
|
-
|
|
4237
|
-
The team reaches the lead whole once, on `team-updated` after the lead switches the feature on
|
|
4238
|
-
and on every snapshot while it is on, and from then on one member at a time: when a member is
|
|
4239
|
-
offered a call, answers, holds, mutes or is taken over, the provider publishes that member whole
|
|
4240
|
-
on `team-member-updated`, so the lead's screen moves when the member's does and a team of two
|
|
4241
|
-
hundred is never resent because one of them held. The agent application asks for nothing and
|
|
4242
|
-
computes nothing. See **`team-updated`** and its companions.
|
|
4243
|
-
|
|
4244
|
-
| Field | Contract |
|
|
4245
|
-
| --- | --- |
|
|
4246
|
-
| `members` | Every member of this lead's team, whatever their state, each with their open tasks. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
|
|
4247
|
-
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Present where the provider offers policy control; absent where it does not. See **Who decides what an agent may do**. |
|
|
4248
|
-
|
|
4249
|
-
| `TeamMember` field | Contract |
|
|
4250
|
-
| --- | --- |
|
|
4251
|
-
| `id` | Required `UserId`. A task carries no names and neither does a team member list: Omni resolves what to display with `getUserDetails()`. |
|
|
4252
|
-
| `availability` | Required. What the member is doing now. |
|
|
4253
|
-
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4254
|
-
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
4255
|
-
| `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, less the workspace. The assignment, title, task type, phase, party, room, audio and completion terms -- `completionMode`, `wrapAllowance`, `wrapEndsInSeconds` -- are what the lead reads, and the task's record -- `history`, steps and totals -- travels whole, exactly as the member's desk holds it, so the lead's screen and the member's show the same record. The workspace -- `capabilities`, `capabilitySource`, `browsers` and their URLs -- is the member's desk's alone and never travels (`team.member.task.workspace`). The party's number and email are shown to leads or locked as the provider decides for leads, apart from what it decides for the member -- see **What the queue locks is locked on the whole task**. `[]` when the member holds none; omitted only where the provider cannot see them. |
|
|
4256
|
-
| `listening` | Every lead on this member's call — listening, coaching or joined — published to every lead alike: each entry names the lead by `leadId`, which of the member's calls by `assignmentId`, the `mode` they are heard in, and since when. The entry naming the signed-in lead is what their own audio opens on. Absent while nobody is on the call, never `[]` (`team.member.listening.*`); a lead is on one member's call at a time (`team.listening.single`), and a lead who is heard is also in the task's `onCall` (`team.member.listening.heard`). |
|
|
4257
|
-
| `request` | Present while the member is asking a lead to join one of their calls: which assignment, the note they wrote, since when. The call named is one their `tasks` carry (`team.member.request.assignment`). Absent when they are not asking. See **Lead assist**. |
|
|
4258
|
-
| `shift` | The member's own history for the day, about the person rather than any one call: `signedInAt`, `signedOutAt` once it has happened, today's totals as the provider counts them — `talkSeconds`, `holdSeconds`, `breakSeconds`, `tasksHandled`, each present only when the provider knows it — and the day's sign-in, sign-out and break `events`, oldest first. The same `Shift` the member's own snapshot carries, the same numbers from the same count. Omitted where the provider cannot say. |
|
|
4259
|
-
|
|
4260
|
-
Each availability value means one thing:
|
|
4261
|
-
|
|
4262
|
-
| Value | Meaning |
|
|
4263
|
-
| --- | --- |
|
|
4264
|
-
| `ready` | Signed in, able to take work, none assigned. |
|
|
4265
|
-
| `on-task` | Working on at least one task. It says nothing about how many, and nothing about whether more will fit. |
|
|
4266
|
-
| `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
|
|
4267
|
-
| `elsewhere` | Signed in here, and the agent application holds this agent's capacity for another provider (`count: 0`, agent application-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
|
|
4268
|
-
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
4269
|
-
|
|
4270
|
-
**Publish the whole team once, then each member whole.** Team presence typically reaches an
|
|
4271
|
-
adapter over a best-effort channel with no ordering and no delivery guarantee, so the adapter
|
|
4272
|
-
reconciles against its own authoritative read before it publishes anything. What it publishes is
|
|
4273
|
-
the whole team on `team-updated` -- after the switch, and on every snapshot -- and after that one
|
|
4274
|
-
member at a time on `team-member-updated`, carrying the member whole: their availability, their
|
|
4275
|
-
tasks, their listening, their shift, their request, as they now stand. Applying the same member
|
|
4276
|
-
twice changes nothing, so a redelivery is harmless, and a member the team never carried is added
|
|
4277
|
-
by it, as a colleague signing in is. `team-member-removed` takes one off. Never a field of a
|
|
4278
|
-
member, never a task of theirs on its own: a hold on a member's desk is that member republished
|
|
4279
|
-
whole on the lead's, and nothing else on the lead's screen moves.
|
|
4280
|
-
|
|
4281
|
-
**Whatever the member's desk hears of a task, every lead hears of the member.** The trigger is
|
|
4282
|
-
not a list of changes but the publication itself: each `task-offered`, `task-updated`,
|
|
4283
|
-
`task-audio-started`, `task-audio-ended` and `task-ended` the provider sends to the member is
|
|
4284
|
-
matched by a `team-member-updated` to every lead with the feature on, carrying the member whole
|
|
4285
|
-
with the task as it now stands, or without it once it has ended. A held leg closing with its
|
|
4286
|
-
`seconds`, a room restated, a phase moving, the wrap left ticking on a restatement: each reaches
|
|
4287
|
-
the lead's copy in the same round it reaches the member's, and the two copies of the record never
|
|
4288
|
-
part for longer than the delay between them.
|
|
4289
|
-
|
|
4290
|
-
**The agent's day is on the wire.** The lead sees `shift` totals as the provider counts them, and
|
|
4291
|
-
the agent sees the same: `Snapshot.shift` and `shift-updated` carry the agent's own day in the same
|
|
4292
|
-
shape, from the same count, so "calls today: 12" is one number on both screens. The desk computes
|
|
4293
|
-
nothing of the day; `tasksHandled` moves when a task ends and `talkSeconds` when a leg closes, and
|
|
4294
|
-
the active call's running time is the screen's own until then, as **Every screen counts from the
|
|
4295
|
-
provider's clock** sets out.
|
|
4296
|
-
|
|
4297
|
-
**Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
|
|
4298
|
-
synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
|
|
4299
|
-
everybody — worse than showing nothing, because it looks like data. Send it only when the provider
|
|
4300
|
-
knows when the state actually began. It times the current `availability`, so it moves every time
|
|
4301
|
-
that value does. And it is counted from the provider's clock, never the screen's, so a provider
|
|
4302
|
-
that publishes it declares `timeCheck` -- see **Every screen counts from the provider's clock**.
|
|
4303
|
-
|
|
4304
|
-
**The login is the permission.** A team member list goes to a login that declares
|
|
4305
|
-
`capabilities.lead`, once the team feature is on, and to nobody else. Omni never decides who
|
|
4306
|
-
leads a team: the provider said so at sign-in, and the team member list agrees with it — owed,
|
|
4307
|
-
`members: []` included, once a lead has switched the feature on; absent for everybody else, which
|
|
4308
|
-
is the correct rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What
|
|
4309
|
-
the lead may do with it comes with the flag: there is no per-action permission on the login or on
|
|
4310
|
-
the list.
|
|
4311
|
-
|
|
4312
|
-
**The lead switches the feature on and off, and the provider is told.** A lead may work as an
|
|
4313
|
-
agent alone, as agent and lead, or as lead alone, and moves between them on the fly. The
|
|
4314
|
-
agent application says which with `{ type: "lead-features", enabled }`: while it is on, the
|
|
4315
|
-
provider sends team events scoped to the lead's team and the lead may act; while it is off, the
|
|
4316
|
-
lead is an ordinary agent, nothing of the team is owed or expected (`team.unexpected`,
|
|
4317
|
-
`event.team.features`), and no lead act is possible, since the lead learns their members'
|
|
4318
|
-
assignments only from the list.
|
|
4319
|
-
|
|
4320
|
-
**The provider assumes nothing.** The switch is not remembered across connections: the agent
|
|
4321
|
-
application sends `lead-features` on every connect, after it has stated capacity, and until it has
|
|
4322
|
-
the provider treats the feature as off. The connect snapshot therefore carries no team, whatever
|
|
4323
|
-
the login declares; the whole team arrives on `team-updated` once the switch is on, and every
|
|
4324
|
-
snapshot after that carries it. A conformance run switches the feature on for a lead and holds the
|
|
4325
|
-
provider to that first `team-updated` (`team.required`); a team on the connect snapshot is the
|
|
4326
|
-
provider assuming (`team.unexpected`).
|
|
4327
|
-
|
|
4328
|
-
**The team member list never carries the agent it is published to.** A lead does not report to
|
|
4329
|
-
themself: their own break request and their own ask for a lead go up to whoever leads them and
|
|
4330
|
-
appear on *that* person's team member list, as a member with a `request`, while the requester sees
|
|
4331
|
-
only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
|
|
4332
|
-
among their own members filters the signed-in identity out before publishing. **Being a lead is a
|
|
4333
|
-
role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
|
|
4334
|
-
with nobody in their team publishes `members: []`, an agent with no such role publishes nothing, and no
|
|
4335
|
-
member count can tell those two apart.
|
|
4336
|
-
|
|
4337
|
-
### Lead commands
|
|
4338
|
-
|
|
4339
|
-
One method, `executeTeam`, taking a discriminated command exactly as `execute` takes a
|
|
4340
|
-
`TaskCommand`. Every act on a member's call names the member, `memberId`, and may name the
|
|
4341
|
-
assignment, `assignmentId`, where the lead has it; the provider resolves the member's current
|
|
4342
|
-
assignment from the member alone.
|
|
4343
|
-
|
|
4344
|
-
| Command | Effect |
|
|
4345
|
-
| --- | --- |
|
|
4346
|
-
| `{ type: "lead-features", enabled }` | The team feature on or off. See **The lead switches the feature on and off** above. |
|
|
4347
|
-
| `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
|
|
4348
|
-
| `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
|
|
4349
|
-
| `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory; the resulting forced break publishes what is left of it as `expectedEndsInSeconds`, restated on every publication. |
|
|
4350
|
-
| `{ type: "end-forced-break", memberId }` | Asks the provider to lift a forced-break restriction a lead asked for, by clearing `BreakState.forced`; one the platform imposed is its own to lift (`team.command.endForcedBreak.provider`, `omni.break-forced-by-provider`). The committed break continues; only the agent resumes work. |
|
|
4351
|
-
| `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
|
|
4352
|
-
| `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
|
|
4353
|
-
| `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
|
|
4354
|
-
| `{ type: "coach", memberId, assignmentId? }` | The lead is heard by the member alone. Needs the lead on that member's call. |
|
|
4355
|
-
| `{ type: "join-call", memberId, assignmentId? }` | The lead is heard by everyone on the call. Needs the lead on that member's call. |
|
|
4356
|
-
| `{ type: "leave", memberId, assignmentId? }` | The lead's channel leaves the member's call, however it got there. The member's call goes on. |
|
|
4357
|
-
| `{ type: "take-over-call", memberId, assignmentId? }` | The provider moves the call to the lead as its new agent, from a joined call or a listened one alike. See **Lead assist**. |
|
|
4358
|
-
| `{ type: "set-policy", capability, setting }` | Sets the team's policy for one capability: `on`, `off`, or `person`. See **Who decides what an agent may do**. |
|
|
4359
|
-
|
|
4360
|
-
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4361
|
-
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4362
|
-
from `getUserDetails()`. `validateTeamCommand` holds each command to the team member list as it
|
|
4363
|
-
stands -- see **Runtime break prerequisite checks**.
|
|
4364
|
-
|
|
4365
|
-
`requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4366
|
-
approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
|
|
4367
|
-
agents so they see it before asking. A `force-break` must likewise reach that member as a `forced` break
|
|
4368
|
-
on their own `BreakState`, or they are stopped from working with no way to see why.
|
|
4369
|
-
|
|
4370
|
-
What happens when no lead is online — auto-approving, for instance — is the provider's decision and is
|
|
4371
|
-
never expressed here.
|
|
4372
|
-
|
|
4373
|
-
### Lead assist
|
|
4374
|
-
|
|
4375
|
-
An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
|
|
4376
|
-
asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
|
|
4377
|
-
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the
|
|
4378
|
-
task; the lead's side is the team member list, which is already the lead's view of the team. The
|
|
4379
|
-
flow, in order:
|
|
4380
|
-
|
|
4381
|
-
```ts
|
|
4382
|
-
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
4383
|
-
execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
4384
|
-
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
4385
|
-
|
|
4386
|
-
// 2. Every lead with the team feature on sees the request on the member.
|
|
4387
|
-
// team-member-updated: member: { id: "A-1", ..., request: { assignmentId: "alloc-42", note, since } }
|
|
4388
|
-
|
|
4389
|
-
// 3. A lead joins, or declines.
|
|
4390
|
-
executeTeam({ command: { type: "join", memberId: "A-1", assignmentId: "alloc-42" } })
|
|
4391
|
-
executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" } })
|
|
4392
|
-
```
|
|
4393
|
-
|
|
4394
|
-
**On `join` the provider bridges the lead's channel into the call.** The member's task moves to
|
|
4395
|
-
`leadAssist: { stage: "joined", leadId }` and its `onCall` gains the lead as an `agent`, since a
|
|
4396
|
-
lead who is heard is in the room; the member on the team member list carries
|
|
4397
|
-
`listening: [{ leadId, assignmentId, mode: "join-call", since }]`, and the lead's audio opens on
|
|
4398
|
-
it as **Listening to a call** describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
|
|
4399
|
-
work assigned to them.
|
|
4400
|
-
|
|
4401
|
-
**A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
|
|
4402
|
-
or a member's -- is answered `failed`; the request stands for another lead, or until it is
|
|
4403
|
-
withdrawn or declined. A join is work, so Omni offers Join only to a lead whose voice channel is
|
|
4404
|
-
free, and a provider answers a `join` from one whose channel is not `failed`.
|
|
4405
|
-
|
|
4406
|
-
**On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
|
|
4407
|
-
provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
|
|
4408
|
-
else changes; the agent is still on the call.
|
|
4409
|
-
|
|
4410
|
-
**On `leave`, the lead's channel drops and the call goes on**: the member's task loses its
|
|
4411
|
-
`leadAssist`, the member loses this lead's entry in `listening`, and the lead is free. Nothing ended for the member.
|
|
4412
|
-
|
|
4413
|
-
**On `take-over-call`, the provider moves the call from the member to the lead**, and the lead is
|
|
4414
|
-
its new agent. There are two paths to it -- the agent asked and the lead joined, or the lead was
|
|
4415
|
-
listening unasked -- and one act at the end of either. What follows is the same on both:
|
|
4416
|
-
|
|
4417
|
-
| | The member's task | The lead's desk |
|
|
4418
|
-
| --- | --- | --- |
|
|
4419
|
-
| Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). The member loses this lead's `listening` entry on the team member list no later than the offer, and the lead's listening audio closes. | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`, within the lead's capacity. |
|
|
4420
|
-
| The member completes | `task-ended` with `{ type: "taken-over", leadId }`, at the member's own completion. The lead is named by user id, since a lead is not a directory item. | The lead works the call as any agent would, and ends it as any call ends. |
|
|
4421
|
-
|
|
4422
|
-
**The taken-over call is offered whatever break the lead is on, and counted like any other.** A
|
|
4423
|
-
lead working as lead alone is on a break of a working kind, and the provider assigns the call
|
|
4424
|
-
regardless: it is the one task a break in effect may hold (`break.on-break.tasks` allows it, and
|
|
4425
|
-
nothing else). It is a call landing on the lead's desk, so it counts against their capacity as
|
|
4426
|
-
every call does, with no exemption: a lead at capacity cannot take over, and the provider answers
|
|
4427
|
-
`failed`. The other conditions are that the lead is not already on another call and their voice
|
|
4428
|
-
channel is free. A lead with the team feature off cannot take over, since they have no member's
|
|
4429
|
-
assignment to name.
|
|
4430
|
-
|
|
4431
|
-
```ts
|
|
4432
|
-
const leadAssistCapable = {
|
|
4433
|
-
channel: "voice",
|
|
4434
|
-
capabilities: { hold: true, leadAssist: true, outcomes: true },
|
|
4435
|
-
phase: "in-progress",
|
|
4436
|
-
leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
4437
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
|
|
4438
|
-
|
|
4439
|
-
const takenOver = {
|
|
4440
|
-
channel: "voice",
|
|
4441
|
-
capabilities: { hold: true, endCall: true, outcomes: true },
|
|
4442
|
-
phase: "in-progress",
|
|
4443
|
-
takenOver: { memberId: "A-1", since: "2026-08-21T09:06:00Z" },
|
|
4444
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "takenOver">;
|
|
4445
|
-
```
|
|
4446
|
-
|
|
4447
|
-
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
4448
|
-
`getUserDetails()`; names never travel on a task or a team member list.
|
|
4449
|
-
|
|
4450
|
-
### Listening to a call
|
|
4451
|
-
|
|
4452
|
-
A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
|
|
4453
|
-
when it goes wrong. It is a team act naming the member, and no task on the lead's desk carries it:
|
|
4454
|
-
the lead's view of the call is the member's task on the team member list, with its room and its
|
|
4455
|
-
history. The three modes determine who hears the lead:
|
|
4456
|
-
|
|
4457
|
-
| Mode | Who hears the lead |
|
|
4458
|
-
| --- | --- |
|
|
4459
|
-
| `listen` | Nobody. The lead hears both sides in silence. |
|
|
4460
|
-
| `coach` | The agent alone. The customer hears nothing. |
|
|
4461
|
-
| `join-call` | Everyone on the call. |
|
|
4462
|
-
|
|
4463
|
-
All three come with the `lead` flag; a centre reserves none of them per lead.
|
|
4464
|
-
|
|
4465
|
-
**There is no team audio.** Audio is the agent's or the lead's, and the lead's audio while
|
|
4466
|
-
listening follows the provider's published state exactly as a task's does: the entry naming this
|
|
4467
|
-
lead in `listening` on the member is the provider's word that the lead's channel is in the call.
|
|
4468
|
-
When it appears, the application opens `openAudio({ assignmentId })` on the lead's connection for
|
|
4469
|
-
the call it names, and plays the `CallAudio` returned; when it goes, the application closes it.
|
|
4470
|
-
Another lead's entry opens nothing: it is shown, and that is all. Nothing else says
|
|
4471
|
-
when the lead's audio is up, and a reload reopens it from the snapshot's team member list as a task
|
|
4472
|
-
carried with `audio: "started"` is reopened.
|
|
4473
|
-
|
|
4474
|
-
The flow, end to end:
|
|
4475
|
-
|
|
4476
|
-
```ts
|
|
4477
|
-
// 1. The lead, with the feature on, sees a member on-task with their tasks, and picks a call.
|
|
4478
|
-
// 2. The lead's channel joins it in silence. Conditions: the lead's voice channel is free --
|
|
4479
|
-
// not on a call, not listening elsewhere. Capacity is not involved: nothing is assigned.
|
|
4480
|
-
executeTeam({ command: { type: "listen", memberId: "A-1", assignmentId: "alloc-42" } })
|
|
4481
|
-
// 3. The provider publishes the member whole on team-member-updated, to every lead; the member carries
|
|
4482
|
-
// listening: [{ leadId: "L-9", assignmentId: "alloc-42", mode: "listen", since }]
|
|
4483
|
-
// -- L-9's application opens openAudio({ assignmentId: "alloc-42" }) on its own connection; every
|
|
4484
|
-
// other lead's shows L-9 on the call and opens nothing.
|
|
4485
|
-
// 4. The lead changes how they are heard; the member is published again with the entry's new mode. The audio stays open.
|
|
4486
|
-
// join-call is heard by everyone, so the member's task is also republished with L-9 on onCall as an agent.
|
|
4487
|
-
executeTeam({ command: { type: "coach", memberId: "A-1" } })
|
|
4488
|
-
executeTeam({ command: { type: "join-call", memberId: "A-1" } })
|
|
4489
|
-
// 5. The lead leaves: the provider publishes the member without L-9's entry, and the task without L-9 in the room; the application closes the audio.
|
|
4490
|
-
executeTeam({ command: { type: "leave", memberId: "A-1" } })
|
|
4491
|
-
// 6. The member's call ends: the same as leave, from the provider's side.
|
|
4492
|
-
// 7. The lead takes the call: the member loses this lead's listening entry no later than the offer, the application
|
|
4493
|
-
// closes the listening audio, and the lead's own call arrives as an ordinary assignment marked
|
|
4494
|
-
// takenOver, counted against capacity, with its audio following task-audio-started as any call's does.
|
|
4495
|
-
// At capacity the take-over is refused and the lead stays listening.
|
|
4496
|
-
executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
|
|
4497
|
-
// 8. A join on the member's request is the same flow entered at step 3 with mode: "join-call",
|
|
4498
|
-
// and the member's task shows leadAssist: joined.
|
|
4499
|
-
// 9. A reload of the lead's application: the switch is sent again, the team-updated that follows
|
|
4500
|
-
// still carries this lead's entry in listening on the member, so the audio is reopened.
|
|
4501
|
-
```
|
|
4502
|
-
|
|
4503
|
-
**`listening` on the member is the state**, one entry per lead on the call, restated on every
|
|
4504
|
-
change of mode, naming the lead (`team.member.listening.leadId`, once per member,
|
|
4505
|
-
`team.member.listening.unique`) and the call, so the application knows which audio is its own to
|
|
4506
|
-
open (`team.member.listening.assignmentId`, and one the member's tasks carry,
|
|
4507
|
-
`team.member.listening.assignment`). Every lead sees every entry: a silent lead is hidden from
|
|
4508
|
-
the agent, never from the other leads and managers watching the same member on other screens.
|
|
4509
|
-
A lead is on one member's call at a time (`team.listening.single`).
|
|
4510
|
-
|
|
4511
|
-
**A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
|
|
4512
|
-
channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
|
|
4513
|
-
includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
|
|
4514
|
-
listens through it.
|
|
4515
|
-
|
|
4516
|
-
**A silent lead reaches nobody but the leads.** In `listen` and `coach` the member's task carries
|
|
4517
|
-
no trace of the lead: not on `onCall`, not in the record. Whether coaching is announced to the
|
|
4518
|
-
agent is the platform's business and travels on the audio, not on this wire. In `join-call`, and
|
|
4519
|
-
on a `join` answering a request, the lead is heard by everyone and is in the room: the task's
|
|
4520
|
-
`onCall` carries them as an `agent` by user id, on the member's desk and on every lead's copy of
|
|
4521
|
-
the task alike, and `leave` takes the entry off as it takes the channel off
|
|
4522
|
-
(`team.member.listening.heard`).
|
|
4523
|
-
|
|
4524
|
-
### A member waiting for a break
|
|
4525
|
-
|
|
4526
|
-
A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
|
|
4527
|
-
availability of its own — it rides alongside one on `break`. That a request exists says nothing
|
|
4528
|
-
about whether anybody has to act on it, and the difference is a lead's entire action list:
|
|
4529
|
-
|
|
4530
|
-
| `break` | Means |
|
|
4531
|
-
| --- | --- |
|
|
4532
|
-
| `awaiting-approval` | Somebody has to decide. This is the lead's queue. |
|
|
4533
|
-
| `granted` | Decided yes, but Omni has not told the provider to stop yet. Work continues and nobody needs to decide. |
|
|
4534
|
-
| `starting-after-task` | Already granted; it begins when their current task ends. Nobody needs to act. |
|
|
4535
|
-
|
|
4536
|
-
Those three are the only values that appear here. `not-requested` is absence — omit `break`
|
|
4537
|
-
instead. `on-break` is `availability: "on-break"`, and a denial transitions to `not-requested`,
|
|
4538
|
-
so neither survives to be reported. It is otherwise the same `BreakStatus` the member's own
|
|
4539
|
-
break state uses, rather than a parallel vocabulary for the lead's view, so the two cannot drift
|
|
4540
|
-
apart.
|
|
4541
|
-
|
|
4542
|
-
Omni offers Approve and Deny only while a member is `awaiting-approval`, shows `granted` as agreed
|
|
4543
|
-
but not started, and shows `starting-after-task` as settled.
|
|
4544
|
-
|
|
4545
|
-
**Live status, not a record.** The provider derives it from what is true now — not stored, not
|
|
4546
|
-
historical, carrying no decision made earlier. Like the team member list it belongs to, it is published
|
|
4547
|
-
whole and replaced whole, and a provider that cannot say omits it.
|
|
4548
|
-
|
|
4549
|
-
**An agent is not waiting on one person.** Authority is held by several, everyone who holds it
|
|
4550
|
-
sees the request on their own console, and **any one of them settles it**. Omni offers the
|
|
4551
|
-
decision to every login that declares `lead` and has the team feature on — which is how the provider already
|
|
4552
|
-
says who may decide — and does not try to work out whose turn it is.
|
|
4553
|
-
|
|
4554
|
-
A request needing *more than one* approval is not something this contract describes. There is
|
|
4555
|
-
no partial state to report and no progress to display: a request is either still owed a
|
|
4556
|
-
decision or it is not.
|
|
4557
|
-
|
|
4558
|
-
## Real-time audio
|
|
4559
|
-
|
|
4560
|
-
Every voice provider has audio: `channel: "voice"` says audio exists. Where the agent hears it
|
|
4561
|
-
depends on the `phone` the agent application selected at authentication and connect from the manifest's
|
|
4562
|
-
`phones`. On a softphone, audio lands in Omni; on a desk phone, it lands on the handset and Omni
|
|
4563
|
-
opens no audio. See **How the agent hears the call**. Audio transitions are voice-only;
|
|
4564
|
-
chat and email publish none (`event.audio.channel`, `stream.taskAudio.channel`).
|
|
4565
|
-
|
|
4566
|
-
The provider owns signalling and the platform's endpoint configuration. The agent application selects the
|
|
4567
|
-
declared phone mode; it does not enumerate or reconfigure the platform's devices. The selected
|
|
4568
|
-
mode stays with the login rather than being inferred from a snapshot or a device failure.
|
|
4569
|
-
|
|
4570
|
-
Nor does the audio ever stand in for the task: a task's presence and
|
|
4571
|
-
phase follow the provider's reports about the work, and the audio — attaching, moving through a
|
|
4572
|
-
hold, a conference, a lead joining or a take-over, and ending — is transient beside it. See **A task is
|
|
4573
|
-
never its audio** under **Task assignment lifecycle**.
|
|
4574
|
-
|
|
4575
|
-
### The agent application reports, the adapter decides
|
|
4576
|
-
|
|
4577
|
-
The adapter runs inside Omni and sees nothing of the station for itself: the devices, the
|
|
4578
|
-
permissions, the audio element and the network are the agent application's, and only the agent application can tell. So
|
|
4579
|
-
the agent application reports, and the adapter asks. **An adapter consults `ConnectContext.host` before it
|
|
4580
|
-
declares the agent ready to its platform, and again whenever the report changes**, and it decides
|
|
4581
|
-
what any of it means and what to relay upstream — go not-ready, refuse calls, carry on because the
|
|
4582
|
-
platform's audio lands elsewhere. Omni facilitates and does not take responsibility: it captures
|
|
4583
|
-
the microphone once as the voice connection opens, so the permission prompt lands while the agent
|
|
4584
|
-
is signing in rather than over a contact, prompts, retries on the agent's request, tells the agent
|
|
4585
|
-
what failed, and reports. It never decides for the adapter what a missing microphone means.
|
|
4586
|
-
|
|
4587
|
-
**The agent application also guarantees, and a promise the provider cannot see is not one it can rely on.**
|
|
4588
|
-
Two of this contract's obligations fall on the agent application rather than the provider — honouring a task
|
|
4589
|
-
browser's `urlVisibility`, and taking a `consent` offer only on the person's own press — and more
|
|
4590
|
-
than one desk speaks this contract: a browser and a native application differ in what they can
|
|
4591
|
-
reach, and a provider never branches on which it is talking to, only on what it has declared —
|
|
4592
|
-
here, and in `host.mute` (see **The station is the agent application's**). So `ConnectContext.host.guarantees` says which promises the
|
|
4593
|
-
connected agent application makes, declared once per connection, presence being the guarantee exactly as it is
|
|
4594
|
-
the permission everywhere else:
|
|
4595
|
-
|
|
4596
|
-
| Guarantee | Contract |
|
|
4597
|
-
| --- | --- |
|
|
4598
|
-
| `browserUrlVisibility` | Every task browser's `urlVisibility` is honoured in this agent application's chrome, tab by tab. A provider that would send a caller's number in a URL checks this first and tokenises where the promise is absent. |
|
|
4599
|
-
| `personConsent` | A `consent` offer is accepted only by the person's own explicit act, never on their behalf. A provider whose work may only be taken by a human checks this first and does not offer it where the promise is absent. |
|
|
4600
|
-
|
|
4601
|
-
A guarantee the agent application does not make is an absent key, never `false` — `validateHostGuarantees`
|
|
4602
|
-
refuses a false one, as it refuses a name this contract does not list.
|
|
4603
|
-
|
|
4604
|
-
| Field | Contract |
|
|
4605
|
-
| --- | --- |
|
|
4606
|
-
| `online` | Whether the agent application has a network interface up. Not a claim that anything is reachable — the adapter knows whether it can reach its own platform far better than the agent application does — so `false` is a reason not to go ready and `true` is not a reason to. |
|
|
4607
|
-
| `audio` | Present on a voice connection, absent where there is no audio. |
|
|
4608
|
-
| `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openAudio` receives — and `flowing`, false while no audio moves through it, when `mutedBy` says who stopped it: `host`, the agent application's own Mute on an agent application that mutes the station, or `station`, a slider on the headset or the operating system, which the agent application observed and did not do. An adapter treats `host` as the agent's act on a call and `station` as a condition of the station. `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which an agent application that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. An agent application decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
|
|
4609
|
-
| `audio.output` | `available`, with `flowing` where the agent application can know whether audio reaches the speaker — a browser mostly cannot, and omits it; a native agent application reads the endpoint — false while it is silenced, when `mutedBy` says who did it, as on input. Or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
|
|
4610
|
-
|
|
4611
|
-
Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
|
|
4612
|
-
a network gone — and it publishes a state, not a flicker: a change that resolves within moments is
|
|
4613
|
-
not reported, so an adapter may act on what it reads without debouncing it again. Nothing in the
|
|
4614
|
-
report is a task fact or a capacity fact; it is what the station can do, and the adapter's platform
|
|
4615
|
-
is the one that knows whether an agent without a microphone, or without a speaker, can work.
|
|
4616
|
-
|
|
4617
|
-
The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
|
|
4618
|
-
publishing a state and not a flicker — are Omni's tests' to hold. `exerciseAdapter` holds the
|
|
4619
|
-
other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
|
|
4620
|
-
a softphone login and none elsewhere -- not on a desk phone, not off voice
|
|
4621
|
-
(`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
|
|
4622
|
-
/ `.unexpected`, `host.mute` for a third word) -- and refuses a voice adapter that never asked the
|
|
4623
|
-
agent application anything (`connection.host.consulted`), and one that subscribed to nothing will never hear of a
|
|
4624
|
-
change (`connection.host.subscribed`): the obligation has two halves, and each is held.
|
|
4625
|
-
|
|
4626
|
-
### Capacity around setup
|
|
4627
|
-
|
|
4628
|
-
Connecting is not the same as being able to take a call. A provider that treats a live connection
|
|
4629
|
-
as reachability opens a window where it believes the agent is available and the agent is not yet
|
|
4630
|
-
set up — the adapter's own registration incomplete, its credentials not yet renewed.
|
|
4631
|
-
|
|
4632
|
-
Nothing closes that window, because nothing opens it: **Omni states no capacity until the
|
|
4633
|
-
connection is established**, and **Work is pulled, never pushed** makes an assignment with none
|
|
4634
|
-
stated a violation. Capacity does not wait on the microphone: whether an agent without agent application audio
|
|
4635
|
-
can take calls is the platform's question, answered by the adapter from the agent application's report, not by
|
|
4636
|
-
Omni withholding capacity for every platform alike.
|
|
4637
|
-
|
|
4638
|
-
Capacity follows **automatically** once the connection is established; the agent does not press
|
|
4639
|
-
anything to become available.
|
|
4640
|
-
|
|
4641
|
-
| Situation | What Omni sends |
|
|
4642
|
-
| --- | --- |
|
|
4643
|
-
| Connecting | Nothing. No capacity has been stated, so nothing may be assigned. |
|
|
4644
|
-
| Connected and idle | `setCapacity({ count: n })`, with the agent application's report already available to consult. |
|
|
4645
|
-
| A task starts or ends | Nothing. The provider counts its own against the ceiling. |
|
|
4646
|
-
| The agent's provisioned capacity changes | `setCapacity({ count: n })` |
|
|
4647
|
-
| Agent asks for a break | `requestBreak`. Capacity is unchanged and work continues. |
|
|
4648
|
-
| Omni commits a break | `commitBreak`. The break stops assignment, not the ceiling. |
|
|
4649
|
-
| Agent returns from break | `endBreak` |
|
|
4650
|
-
|
|
4651
|
-
**A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
|
|
4652
|
-
agent this provider may carry; a break says they are not working at all. A provider told
|
|
4653
|
-
`count: 0` shows an agent whose capacity is elsewhere, `elsewhere`; a provider told a break shows
|
|
4654
|
-
`on-break`. Collapsing the two would leave a provider unable to tell an agent working on another
|
|
4655
|
-
provider from an agent who has gone to lunch, and only one of those needs a reason, a decision and
|
|
4656
|
-
a return.
|
|
4657
|
-
|
|
4658
|
-
### How the agent hears the call
|
|
4659
|
-
|
|
4660
|
-
A voice platform can put an agent on a **softphone**, where the call's audio lands in the agent application
|
|
4661
|
-
and Omni owns the microphone and the playback, or on a **desk phone**, a handset on the platform's
|
|
4662
|
-
switch that the platform rings, where the agent application owns no audio and only shows the call. Which of
|
|
4663
|
-
these a platform can do is a fact about the platform, and which one a login is on is a choice the
|
|
4664
|
-
agent application makes for that agent -- so the manifest declares and the agent application chooses.
|
|
4665
|
-
|
|
4666
|
-
```ts
|
|
4667
|
-
// Manifest: what the platform can do
|
|
4668
|
-
phones: ["softphone", "deskPhone"]
|
|
4669
|
-
// The agent application, at authentication and again at connect: which one this login is on
|
|
4670
|
-
{ phone: "softphone" }
|
|
4671
|
-
```
|
|
4672
|
-
|
|
4673
|
-
A voice manifest lists its `phones` and any other channel lists none (`manifest.phones.required`,
|
|
4674
|
-
`manifest.phones.channel`). The agent application's `phone` is required on a voice login, absent on any other,
|
|
4675
|
-
and one the manifest listed (`context.phone.required`, `.unexpected`, `.unsupported`). Everything
|
|
4676
|
-
about audio then follows the phone rather than the channel: on a softphone the agent application reports its
|
|
4677
|
-
audio, the adapter implements `openAudio`, and `task-audio-started` is the word to open it; on a
|
|
4678
|
-
desk phone the agent application reports no audio, opens nothing, and `task-audio-started` still marks the
|
|
4679
|
-
moment the call is live so the desk shows it, with the sound on the handset. A platform that lists
|
|
4680
|
-
`deskPhone` alone never implements `openAudio`; one that lists `softphone` always does.
|
|
4681
|
-
|
|
4682
|
-
**The mode comes from the agent application; the status comes from the provider.** The agent application knows which kind
|
|
4683
|
-
of station the agent signed in at, because the person chose it, and nothing else can know that.
|
|
4684
|
-
The provider knows whether that station can carry a call right now -- whether a handset is
|
|
4685
|
-
registered -- which an agent application in a browser cannot see. So the agent application's `phone` selects the branch, and
|
|
4686
|
-
the provider's own knowledge of the device decides readiness within it. A provider never overrides
|
|
4687
|
-
the agent application's declaration from its device record: treating a desk-phone login as a softphone because
|
|
4688
|
-
the handset is momentarily unregistered, and demanding a microphone of it, is the reading this
|
|
4689
|
-
sentence exists to refuse. What the provider does when the handset is not registered is what it
|
|
4690
|
-
does for any station that cannot take a call -- hold the agent not-ready and say why.
|
|
4691
|
-
|
|
4692
|
-
**On a desk-phone login the agent application never calls `openAudio`.** There is no stream to hand over and
|
|
4693
|
-
no audio to attach; an agent application that calls it anyway is in error, and an adapter that receives the call
|
|
4694
|
-
answers `unavailable` with a non-retryable failure, since waiting changes nothing about a station
|
|
4695
|
-
that is a telephone. The harness requires no `openAudio` of such an adapter and never calls it.
|
|
4696
|
-
|
|
4697
|
-
**Which handset a desk-phone login rings is the platform's configuration for that agent**, and
|
|
4698
|
-
this wire never asks the agent for it: an agent application declares `phone` and nothing more. What a platform
|
|
4699
|
-
asks on surfaces of its own is its own decision.
|
|
4700
|
-
|
|
4701
|
-
**A declared phone the platform does not permit for this agent is a login that cannot be
|
|
4702
|
-
established.** The manifest says what the platform can do; the agent's record, kept by an
|
|
4703
|
-
administrator, says what this agent is configured for, and the two can disagree with the agent application's
|
|
4704
|
-
declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
|
|
4705
|
-
declaration on a switch that allows one registration per endpoint would evict the handset: the
|
|
4706
|
-
agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
|
|
4707
|
-
refuses the login at authentication, with `omni.phone-not-permitted` and a message the agent application shows
|
|
4708
|
-
-- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
|
|
4709
|
-
agent application. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
|
|
4710
|
-
never retryable: trying again does not reconfigure the agent, and an agent application validating the answer
|
|
4711
|
-
(`validateAuthenticationResult`) refuses a phone refusal marked otherwise
|
|
4712
|
-
(`authentication.failure.phone.retryable`). And **a
|
|
4713
|
-
provider never makes a declared phone true by changing the platform's configuration**: the record
|
|
4714
|
-
is the administrator's, and a login is not a request to reconfigure an agent.
|
|
4715
|
-
|
|
4716
|
-
### Opening the audio
|
|
4717
|
-
|
|
4718
|
-
`openAudio` hands Omni the remote audio for one task. Every adapter whose manifest lists
|
|
4719
|
-
`softphone` implements it, because on a softphone the call's audio lands in Omni:
|
|
4720
|
-
|
|
4721
|
-
```ts
|
|
4722
|
-
openAudio({ assignmentId, localAudio }): Promise<OpenAudioResult>
|
|
4723
|
-
// { status: "opened", session } | { status: "unavailable", failure }
|
|
4724
|
-
```
|
|
4725
|
-
|
|
4726
|
-
The adapter speaks whatever its platform speaks — SIP over WebSocket, a vendor SDK, plain
|
|
4727
|
-
WebRTC — and **none of that appears in this contract**. Registration, signalling, credential
|
|
4728
|
-
renewal and reconnect are the adapter's, exactly as its authentication and transport already
|
|
4729
|
-
are. Omni owns what belongs to the agent application: the microphone, the output element, mute, and when a
|
|
4730
|
-
session ends — owning the microphone meaning capturing it, prompting, retrying and reporting how
|
|
4731
|
-
it stands, never deciding for the adapter what a missing one means (see **The agent application reports, the
|
|
4732
|
-
adapter decides**).
|
|
4733
|
-
|
|
4734
|
-
| Member | Contract |
|
|
4735
|
-
| --- | --- |
|
|
4736
|
-
| `remoteAudio` | `MediaStream` Omni plays. |
|
|
4737
|
-
| `setMuted(muted)` | Mutes the agent's microphone on this session. |
|
|
4738
|
-
| `close()` | Releases the session. Omni calls it when the task ends. |
|
|
4739
|
-
|
|
4740
|
-
`localAudio` is the agent's microphone as Omni captured it, the same stream the agent application's report
|
|
4741
|
-
carries as `audio.input.localAudio`, and absent while that input is `unavailable`. A provider that
|
|
4742
|
-
bridges audio without an application-side input may ignore it; one that needs it and finds it absent
|
|
4743
|
-
answers `unavailable` with a failure Omni shows the agent.
|
|
4744
|
-
|
|
4745
|
-
**When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens audio on `task-audio-started`,
|
|
4746
|
-
and on a task arriving with `audio: "started"` on a snapshot; it closes on `task-audio-ended` and
|
|
4747
|
-
when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
|
|
4748
|
-
track that ends — moves the task or its audio.
|
|
4749
|
-
|
|
4750
|
-
**A task-scoped session does not oblige one call per task.** A platform holding a nailed-up
|
|
4751
|
-
leg for a whole shift may return the same session for every task and release the underlying
|
|
4752
|
-
path only when the connection closes. A platform placing a call per contact returns a new one
|
|
4753
|
-
each time. Omni asks when it needs audio and closes when it is done; how that maps to the
|
|
4754
|
-
platform is the adapter's business.
|
|
4755
|
-
|
|
4756
|
-
## Task commands
|
|
4757
|
-
|
|
4758
|
-
Command names follow the channel's operational vocabulary, and each channel's command is a union
|
|
4759
|
-
discriminated by `type` — the same discriminant `executeTeam` and `custom` already use. The
|
|
4760
|
-
unions are declared under **Shapes**.
|
|
4761
|
-
|
|
4762
|
-
`assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
|
|
4763
|
-
|
|
4764
|
-
**A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot converge
|
|
4765
|
-
with a stale view: a flip against a state the provider has already changed turns something on and
|
|
4766
|
-
then off again. A custom `toggle` control therefore carries its own boolean. `hold` and `resume`,
|
|
4767
|
-
`pause` and `resume` need no flag, being pairs rather than toggles.
|
|
4768
|
-
|
|
4769
|
-
**`complete` sends an outcome only where one was published.** `outcome` is a
|
|
4770
|
-
`OutcomeCode.id` from the task's own `outcomes` capability, and `notes` obeys that
|
|
4771
|
-
capability's `notes` setting. A task publishing no codes still receives `complete`, with neither.
|
|
4772
|
-
|
|
4773
|
-
### Where a command executes
|
|
4774
|
-
|
|
4775
|
-
Every command reaches the provider through `execute`, with no branch at the call site, and every
|
|
4776
|
-
command asks the provider to **perform** something: `hold`, `conference`, `schedule`, `end-call`,
|
|
4777
|
-
Provider-targeted `recording` commands and the rest act on the platform's own call leg, its bridge, or its record of the
|
|
4778
|
-
task. Nothing has happened until the provider applies them, and `failed` means nothing happened.
|
|
4779
|
-
|
|
4780
|
-
**The provider performs every action; the agent application only offers it.** A control drawn on the agent application is
|
|
4781
|
-
an affordance, never the enforcement: the agent application asks, and the provider does or declines. A provider
|
|
4782
|
-
answers `failed` for a command whose capability it did not publish, and that is the provider
|
|
4783
|
-
honouring its own declaration, not the agent application deciding policy -- an agent application that refuses a command on
|
|
4784
|
-
its own reading of a queue's flag has decided something that was never its to decide. What a
|
|
4785
|
-
command means on the platform is the provider's to work out: `end-call` disconnects the caller channel, and
|
|
4786
|
-
which legs on which bridge that touches is a fact about the switch, never a choice the agent application makes.
|
|
4787
|
-
The one thing the agent application performs physically is the microphone, and that is not a command at all.
|
|
4788
|
-
|
|
4789
|
-
### The station is the agent application's
|
|
4790
|
-
|
|
4791
|
-
The microphone, the speaker, the headset and its buttons are the station's, and the station is the
|
|
4792
|
-
agent application's. So mute is not a capability a provider declares, not a control a queue allows or a team
|
|
4793
|
-
locks, not a preference the person keeps with the provider, and not a command: nothing about the
|
|
4794
|
-
station crosses to the provider except two things -- the station's condition, in the agent application report,
|
|
4795
|
-
and the record of when the agent could not be heard, through `recordStep`. An agent application offers Mute on
|
|
4796
|
-
every voice task with audio open, under its own local policy, in `in-progress` and `paused`
|
|
4797
|
-
alone, and performs it through `CallAudio.setMuted()`. A task carrying
|
|
4798
|
-
`capabilities.mute` is refused (`task.capability.unknown`), and so is a `mute` command
|
|
4799
|
-
(`command.type`), a `mute` preference (`preference.id`) and a `mute` policy (`team.policy.key`).
|
|
4800
|
-
|
|
4801
|
-
**Two mutes, and the agent application states which its Mute performs.** A *stream* mute stops the audio the
|
|
4802
|
-
agent application sends: the microphone keeps capturing, the party hears silence, nothing else on the machine
|
|
4803
|
-
changes, and every softphone agent application can do it. A *station* mute silences the microphone itself at
|
|
4804
|
-
the operating system: every application on the machine goes quiet, the system shows it, and a
|
|
4805
|
-
headset that follows the system follows it. Only a native agent application can do it. `Host.mute` says which
|
|
4806
|
-
this agent application does -- `stream` or `station` -- stated on a softphone login, where the agent application holds the
|
|
4807
|
-
microphone, and absent on a desk phone and off voice (`host.mute.required` / `.unexpected`). A
|
|
4808
|
-
browser says `stream`. A native agent application says whichever it does. Nothing on the wire says what kind of
|
|
4809
|
-
application the agent application is; a provider reads only what the agent application declared.
|
|
4810
|
-
|
|
4811
|
-
**A silenced device says who silenced it.** `audio.input.flowing: false` and
|
|
4812
|
-
`audio.output.flowing: false` carry `mutedBy`: `host` when the agent application's own Mute did it -- which is
|
|
4813
|
-
only ever true of an agent application whose mute is `station`, since a stream mute leaves the microphone
|
|
4814
|
-
flowing -- and `station` when a slider on the headset or the operating system did, which the agent application
|
|
4815
|
-
observed and did not do. Without the word, a station-muting agent application's own Mute would read as a fault
|
|
4816
|
-
and an adapter would take the agent out of ready in the middle of a call. A browser reads a station
|
|
4817
|
-
mute through the capture track's muted state, read-only, and cannot lift it; a native agent application reads
|
|
4818
|
-
the endpoint and can clear an operating-system mute. A hardware slider is nobody's to clear.
|
|
4819
|
-
|
|
4820
|
-
**The record carries the same word.** A `muted` interaction leg is a period the agent could not be
|
|
4821
|
-
heard, and the record exists so that period is not a hole. The agent application reports every such period
|
|
4822
|
-
through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
|
|
4823
|
-
`mutedBy` saying whose the silence was, and the provider writes the word into the entry
|
|
4824
|
-
(`task.history.mutedBy`). The report names no agent because the agent application has exactly one, and
|
|
4825
|
-
the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
|
|
4826
|
-
holds and not an inference, for a station mute as much as for the agent application's own. An unattributed
|
|
4827
|
-
`muted` entry is refused (`task.history.muted.by`); a shared handset's unattributed hold
|
|
4828
|
-
is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
|
|
4829
|
-
forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
|
|
4830
|
-
are. See **The agent application records what it performs**.
|
|
4831
|
-
|
|
4832
|
-
**Mute has a lifecycle, and none of it is inferred.** A call that starts while the station is
|
|
4833
|
-
already muted begins a `muted` leg the moment its audio starts, `mutedBy: "station"`. Audio that
|
|
4834
|
-
ends while any leg is open ends the leg at that instant, as every agent application-performed leg ends. And the
|
|
4835
|
-
agent application's own Mute starts off on every call, which is every `task-audio-started` -- a connect-back
|
|
4836
|
-
opens a second call on the same task with no offer -- so an agent is never muted by the call before.
|
|
4837
|
-
|
|
4838
|
-
**Every press on a headset is the agent's own press**, performed the agent application's way, and the lights
|
|
4839
|
-
follow the agent application's state. The `personConsent` guarantee is honoured by a hook-switch press exactly
|
|
4840
|
-
as by a click. What a call-control headset sends, and what becomes of it:
|
|
4841
|
-
|
|
4842
|
-
| Input | Where it comes from | What the agent application does | What crosses to the provider |
|
|
4843
|
-
| --- | --- | --- | --- |
|
|
4844
|
-
| Mute button | The headset, on the HID Telephony usage page: a Phone Mute report on the press, a Mute LED the agent application writes back | A press of the agent application's Mute, performed the agent application's way; the LED follows the agent application's state, so button, light and control are one state with one owner | The `muted` leg, `mutedBy: "host"` |
|
|
4845
|
-
| Hook switch | The headset, HID Telephony | A press of the agent application's Answer or End call, only where the task is in a phase that has one; the off-hook and ring lights follow the task | The existing `answer` and `end-call` commands |
|
|
4846
|
-
| Flash, redial, speed dial | Older headsets and desk-phone style devices | Flash is Hold and Resume where the task offers `hold`; redial and speed dial map to nothing on a desk that never redials, and are ignored | Nothing new |
|
|
4847
|
-
| Volume up and down | The headset's own amplifier, or the operating system through the consumer-control keys | Nothing: the device and the system handle it | Nothing, except that a speaker at nought is `audio.output.flowing: false` where the agent application can know it |
|
|
4848
|
-
| Microphone gain and level | The operating system, the headset's own boost | A level meter, so the agent can see they are heard | Nothing: a level is a flicker, not a state |
|
|
4849
|
-
| Hardware or operating-system mute | A slider on the headset, the system's input mute | Observes it, publishes `flowing: false, mutedBy: "station"`, records the leg during a call, and tells the agent which it is where it knows -- headset or system -- and what to do; an agent application whose mute is `station` clears a system mute itself, a browser can only say so | The report and the leg |
|
|
4850
|
-
| Device changed, unplugged, permission revoked | The operating system | Re-capture, prompt, report | Already `audio.input` and `audio.output` with `lost`, `denied`, `no-device` |
|
|
4851
|
-
|
|
4852
|
-
**What the agent application shows.** Beside Mute, a standing line while the station is muted, saying which and
|
|
4853
|
-
what to do, with a button that clears a system mute only on an agent application whose mute is `station`. The
|
|
4854
|
-
Mute control does not pretend: while the station is muted it shows the agent cannot be heard and
|
|
4855
|
-
that pressing it will not change that, and a press still toggles the agent application's own mute, so the two
|
|
4856
|
-
states stay separate and the agent is not unmuted by surprise when the slider moves back. Before a
|
|
4857
|
-
call, the same line in the ready state, so an agent does not learn of it from a silent customer;
|
|
4858
|
-
whether work is held back is the adapter's decision from the report, as it is for every station
|
|
4859
|
-
fact. Where a provider's own platform holds a mute of its own -- a bridge that silences a leg --
|
|
4860
|
-
that is a fact about the switch, reported as the provider sees fit, and never the agent application's Mute.
|
|
4861
|
-
|
|
4862
|
-
### Which commands need a capability
|
|
4863
|
-
|
|
4864
|
-
**Presence is the permission** gates the controls a provider chooses to offer. Four commands are
|
|
4865
|
-
not among them, because every task has them; each is authorized by a different field the provider
|
|
4866
|
-
declared:
|
|
4867
|
-
|
|
4868
|
-
| Command | What makes it available |
|
|
4869
|
-
| --- | --- |
|
|
4870
|
-
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
4871
|
-
| `end-call` | The `endCall` capability: ends my part, the caller continues. |
|
|
4872
|
-
| `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
|
|
4873
|
-
| `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
|
|
4874
|
-
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
4875
|
-
| `dial` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
|
|
4876
|
-
| `complete` | A wrap to cut short: any task but a `provider-automatic` one with `wrapAllowance: 0`, under either completion mode (`command.complete.wrapAllowance`). The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
|
|
4877
|
-
| `connect-back` | The `connectBack` 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. |
|
|
4878
|
-
| `schedule` | The `schedule` capability, on any channel, in `in-progress`, `paused` or `completing`: a follow-up is promised on the call and written up in wrap alike. The manifest declares `calendar` for it to land on. |
|
|
4879
|
-
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with `stage: "requested"`. |
|
|
4880
|
-
| `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
|
|
4881
|
-
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
|
|
4882
|
-
| Everything else | Its own named capability. |
|
|
4883
|
-
|
|
4884
|
-
**A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
|
|
4885
|
-
`hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
|
|
4886
|
-
every `lead-assist` action; `schedule` alone reaches into `completing`. These controls act within this agent's current interaction.
|
|
4887
|
-
In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
|
|
4888
|
-
an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
|
|
4889
|
-
of this interaction does not establish that the caller or bridge ended. The capability stays
|
|
4890
|
-
declared while the task remains open; the phase prevents this task from controlling an interaction
|
|
4891
|
-
it no longer owns. Omni shows these controls only in the two interaction phases, and
|
|
4892
|
-
`validateTaskCommand` refuses them outside those phases (`command.phase.interaction`).
|
|
4893
|
-
The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
|
|
4894
|
-
`dial` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
|
|
4895
|
-
|
|
4896
|
-
`validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
|
|
4897
|
-
capability it needs, the phase it belongs to, and the state that has to stand. The task it wants
|
|
4898
|
-
is the one the provider published, not an agent application's own mapping of it: an agent application that keeps only its
|
|
4899
|
-
mapped shape has nothing honest to pass, and then checks shape alone, which is still worth doing
|
|
4900
|
-
-- it names a command the wire never had -- but is not the table. Keeping the published task
|
|
4901
|
-
beside the mapped one is what the full check costs an agent application; an adapter has it for free. The
|
|
4902
|
-
validator holds a command only to a task that stands: a task handed in that is not one the wire
|
|
4903
|
-
published is named (`command.task`) rather than checked against.
|
|
4904
|
-
|
|
4905
|
-
Declining a pending offer ends it without accepting or completing it. The provider confirms the
|
|
4906
|
-
end with `task-ended` and a `cancelled` outcome, `by: "agent"`.
|
|
4907
|
-
|
|
4908
|
-
### `execute(request)`
|
|
4909
|
-
|
|
4910
|
-
Applies a `TaskCommandRequest` to one provider-local task.
|
|
4911
|
-
|
|
4912
|
-
- Omni serializes commands per task and sends the next only after the previous settled or was
|
|
4913
|
-
given up as unknown; it stops sending when `task-ended` arrives.
|
|
4914
|
-
- `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
|
|
4915
|
-
`ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
|
|
4916
|
-
says which.
|
|
4917
|
-
- A command sent while `transport-status` is not `active` answers `failed` with `omni.unavailable`.
|
|
4918
|
-
Neither Omni nor the adapter queues it.
|
|
4919
|
-
- **A command that asks for a state answers `applied` when that state holds, whoever brought it
|
|
4920
|
-
about; a command that acts answers `failed` when it cannot act.** Declining a lead request
|
|
4921
|
-
already gone is `applied`; joining one already gone is `failed`, since nobody joined. A lead
|
|
4922
|
-
deciding a member's break that another lead has already decided is `applied` when the decisions
|
|
4923
|
-
agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
|
|
4924
|
-
in effect is `committed` for the same reason.
|
|
4925
|
-
- **A result is untrusted for the same reason a snapshot is.** It comes from an adapter that may be
|
|
4926
|
-
compiled against another version, and Omni shows the agent what it says. Omni validates it at
|
|
4927
|
-
the boundary with `validateResult(result, "execute")` — a status the method does not answer, a
|
|
4928
|
-
`failed` without its failure, a success carrying one, or an `omni.` code this contract lacks is
|
|
4929
|
-
refused, and the command is treated as unsettled.
|
|
4930
|
-
- **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
|
|
4931
|
-
promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
|
|
4932
|
-
see **An unsettled result is unknown**. `failed` must never be returned for something the
|
|
4933
|
-
provider is unsure of, because Omni will show the agent it did not happen.
|
|
4934
|
-
|
|
4935
|
-
### `ProtocolFailure`
|
|
4936
|
-
|
|
4937
|
-
| Field | Contract |
|
|
4938
|
-
| --- | --- |
|
|
4939
|
-
| `code` | Required stable machine-readable value. See the reserved codes below. |
|
|
4940
|
-
| `message` | Required, and safe for logs or agent display. |
|
|
4941
|
-
| `retryable` | Whether repeating the action can succeed at all. |
|
|
4942
|
-
| `retryAfterMs` | Optional minimum suggested delay before a retry. A suggestion, not a guarantee. |
|
|
4943
|
-
|
|
4944
|
-
The `omni.` prefix is **reserved**. Adapters must not invent codes under it; every other value is
|
|
4945
|
-
provider-private and Omni treats it as opaque. Using a reserved code where it applies lets Omni
|
|
4946
|
-
react rather than only display the message:
|
|
4947
|
-
|
|
4948
|
-
| Code | Meaning |
|
|
4949
|
-
| --- | --- |
|
|
4950
|
-
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
4951
|
-
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
4952
|
-
| `omni.assignment-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
|
|
4953
|
-
| `omni.phone-not-permitted` | The agent application declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
|
|
4954
|
-
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
4955
|
-
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
4956
|
-
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
4957
|
-
| `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
|
|
4958
|
-
| `omni.recording-unsettled` | A recording command the provider could not settle either way: a settled `failed`, never a promise left unresolved. See **Independent task recording**. |
|
|
4959
|
-
| `omni.break-forced-by-provider` | A lead asked to lift a break the platform imposed; the platform lifts its own. See **Forced breaks**. |
|
|
4960
|
-
|
|
4961
|
-
They are published as `OMNI_FAILURE_CODES`.
|
|
4962
|
-
|
|
4963
2311
|
## Event delivery
|
|
4964
2312
|
|
|
4965
2313
|
### `ProviderEventEnvelope`
|
|
@@ -5075,7 +2423,7 @@ used for final records, and agent application-supplied timestamps are advisory.
|
|
|
5075
2423
|
of time-check support and the optional agent application estimate; neither changes who owns the final record.
|
|
5076
2424
|
Omission makes no promise that agent application timestamps will be trusted. No agent application-authoritative default
|
|
5077
2425
|
is inferred. Successful `recordStep` responses identify the provider-selected history instant;
|
|
5078
|
-
see **The agent application records what it performs** for correlation and retained bindings.
|
|
2426
|
+
see **The agent application records what it performs** in `guide/agent.md` for correlation and retained bindings.
|
|
5079
2427
|
|
|
5080
2428
|
A provider need not trust or adopt any agent application-supplied timestamp. It may timestamp an incoming
|
|
5081
2429
|
message using its own clock at receipt and use that for its own processing/accounting. The reason
|
|
@@ -5315,7 +2663,7 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
|
|
|
5315
2663
|
- selects another task or returns to the idle workspace.
|
|
5316
2664
|
|
|
5317
2665
|
A `taken-over` outcome arrives at the member's own completion, after the wrap that follows a
|
|
5318
|
-
take-over as it follows an `end-call` -- see **Lead assist
|
|
2666
|
+
take-over as it follows an `end-call` -- see **Lead assist** in `guide/lead.md`.
|
|
5319
2667
|
|
|
5320
2668
|
A successful `complete` command does not clear the task. Omni waits for `task-ended`,
|
|
5321
2669
|
and not for ever: `applied` to a `complete` says the
|
|
@@ -5360,7 +2708,7 @@ provider does not batch. `exerciseAdapter` treats a diagnostic delivered during
|
|
|
5360
2708
|
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
5361
2709
|
fails loudly rather than passing with a note. A task published under `capabilitySource:
|
|
5362
2710
|
"not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
|
|
5363
|
-
(`capabilitySource.notYetRead`); see **Task capabilities
|
|
2711
|
+
(`capabilitySource.notYetRead`); see **Task capabilities** in `guide/agent.md`.
|
|
5364
2712
|
|
|
5365
2713
|
| Field | Contract |
|
|
5366
2714
|
| --- | --- |
|
|
@@ -5396,7 +2744,7 @@ One member, whole, as they now stand: their availability, their tasks, their lis
|
|
|
5396
2744
|
shift, their request. It replaces the member of that `id` on the lead's list, or adds them where
|
|
5397
2745
|
the list did not carry them, so applying it twice changes nothing. It comes only after the whole
|
|
5398
2746
|
team has (`stream.team.baseline`), and it is what every publication of a member's task to the
|
|
5399
|
-
member publishes to the lead, as **Team leads** sets out: never fewer.
|
|
2747
|
+
member publishes to the lead, as **Team leads** in `guide/lead.md` sets out: never fewer.
|
|
5400
2748
|
|
|
5401
2749
|
### `team-member-removed`
|
|
5402
2750
|
|
|
@@ -5419,6 +2767,14 @@ the `contacts` idle capability.
|
|
|
5419
2767
|
Replaces this provider's complete scheduled-activity contribution. It is emitted only when the manifest
|
|
5420
2768
|
declares the `calendar` idle capability.
|
|
5421
2769
|
|
|
2770
|
+
### `phone-updated`
|
|
2771
|
+
|
|
2772
|
+
The phone as the platform now sees it, whole, replacing `Snapshot.phone`: a registration lost or
|
|
2773
|
+
back, do-not-disturb pressed or cleared, the handset lifted with no call or replaced, the desk
|
|
2774
|
+
phone's own mute. Emitted only by a provider whose manifest declares `phoneStatus`
|
|
2775
|
+
(`event.phone.capability`). The lead's member carries the same `PhoneState`, republished with the
|
|
2776
|
+
member. See **The phone's own view** in `guide/phone.md`.
|
|
2777
|
+
|
|
5422
2778
|
### `shift-updated`
|
|
5423
2779
|
|
|
5424
2780
|
The agent's own day, whole, replacing `Snapshot.shift`: emitted whenever a total moves -- a task
|
|
@@ -5674,7 +3030,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5674
3030
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
|
|
5675
3031
|
| `assertAudioFollowsTheTask(envelopes, snapshot?)` | The audio follows the task and never decides it: every task is introduced once, `task-audio-started` and `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what follows the audio ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no audio satisfies it by never testing it — pair it with the assertion that the audio end is present. |
|
|
5676
3032
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5677
|
-
| `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks
|
|
3033
|
+
| `assertForcedBreakStopsTheRest(forcedOn, candidates, stopped)` | A forced break on one provider stops the agent everywhere else: capacity zero stated on every other usable provider holding capacity, nothing on the forcing one, nothing on a dead login. See **Forced breaks** in `guide/breaks.md`. |
|
|
5678
3034
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
5679
3035
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
5680
3036
|
| `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|
|
@@ -5693,151 +3049,3 @@ belongs in this contract is the boundary. A provider says what a control **is**
|
|
|
5693
3049
|
capabilities and what its work is **called** through `phaseLabels` and `taskTypePresentation`; how
|
|
5694
3050
|
any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
|
|
5695
3051
|
override the agent's theme and font preferences.
|
|
5696
|
-
|
|
5697
|
-
## Independent task recording
|
|
5698
|
-
|
|
5699
|
-
Recording is voice-only. A task can arrive already recording, including while pending, and offer
|
|
5700
|
-
no recording controls. The provider publishes only its own current state on
|
|
5701
|
-
the provider field of the task’s `recording` state. The agent application publishes its own full scoped view in `HostReport.recordings`.
|
|
5702
|
-
A provider task update cannot replace agent application state. Neither a capability nor an accepted command
|
|
5703
|
-
establishes recording. Missing state, lost recorder observation, transport loss and expired
|
|
5704
|
-
observation mean unknown; they never mean stopped. Paused means a recording remains open without
|
|
5705
|
-
capturing. Task hold, task pause and recording pause are independent.
|
|
5706
|
-
|
|
5707
|
-
The per-task policy is `Task.capabilities.recording`, with independently optional provider and agent application
|
|
5708
|
-
action sets. The outer capability may be locked. Absence grants no permission, and a state may
|
|
5709
|
-
exist without any permission. A provider may offer only stop for a recording started automatically,
|
|
5710
|
-
or withdraw a control on a later task update. There is no global recording mode and no automatic
|
|
5711
|
-
start from a capability declaration. Agent application support is declared on `ConnectContext.host.recording`,
|
|
5712
|
-
not the provider-owned manifest. The task names one of the storage places the agent application provisioned, by `storageId`;
|
|
5713
|
-
an unknown storage place or an unsupported action is refused visibly, never redirected to provider
|
|
5714
|
-
recording or a default upload location. Initial agent application support requires voice softphone audio and
|
|
5715
|
-
capture of both local and remote audio; an agent application unable to capture either must refuse start/resume.
|
|
5716
|
-
This package declares that contract; it supplies no recorder, audio mixing, storage or upload.
|
|
5717
|
-
|
|
5718
|
-
| Action | Required current recording state | Confirmed outcome |
|
|
5719
|
-
| --- | --- | --- |
|
|
5720
|
-
| start | inactive | active with a new recording identity |
|
|
5721
|
-
| pause | active | paused, preserving recording identity and captured audio |
|
|
5722
|
-
| resume | paused | active with the same recording identity |
|
|
5723
|
-
| stop | active or paused | inactive; finish and retain captured audio |
|
|
5724
|
-
| cancel | active or paused | inactive; abandon and discard captured audio |
|
|
5725
|
-
|
|
5726
|
-
Stop finishes the recording and retains captured audio. Cancel abandons the recording and discards
|
|
5727
|
-
its captured audio; the task policy offers it with `cancel: true`. There is no configurable cancel
|
|
5728
|
-
effect. A recorder that cannot confirm completion must not offer Cancel; it can offer Stop instead.
|
|
5729
|
-
Cancel never means cancelling an in-flight start request or deleting arbitrary past recordings.
|
|
5730
|
-
Stop can be applied only after finalization/retention succeeds; Cancel only after both cessation
|
|
5731
|
-
and completion of this recording's audio succeed under the recorder's storage contract.
|
|
5732
|
-
Partial success -- capture stopped but the storage outcome unknown, a start the recorder cannot
|
|
5733
|
-
vouch for either way -- is a settled `failed`, under the code `omni.recording-unsettled`: the
|
|
5734
|
-
command did not do what it promised, and the state the provider publishes with it is whatever
|
|
5735
|
-
capture state is actually known. It is never `applied`, and never an unsettled promise the agent
|
|
5736
|
-
application would be left to resync, since the provider knows exactly what it could not settle.
|
|
5737
|
-
There is no request identity on a recording command: the outcome is the state, and what the
|
|
5738
|
-
agent sees is the state. Retention is not a promise of sample-perfect audio.
|
|
5739
|
-
|
|
5740
|
-
Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
|
|
5741
|
-
`HostRecording.execute`. Both include the assignment and observation identities; all
|
|
5742
|
-
non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
|
|
5743
|
-
task and recorder owner. They are never inferred from filenames or current agent identity. A
|
|
5744
|
-
recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
|
|
5745
|
-
commands always name the current assignment. A later start gets a different ID. Reconnect does not
|
|
5746
|
-
create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
|
|
5747
|
-
There is no automatic restart, hand-over to another recorder, or stop on task hold/disconnect.
|
|
5748
|
-
Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
|
|
5749
|
-
agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
|
|
5750
|
-
|
|
5751
|
-
Only start/resume require an in-progress or paused task with started audio. Pause/stop/cancel may
|
|
5752
|
-
also finish an independently observed recorder while the task is completing. A pending task may
|
|
5753
|
-
show active recording, but agent controls wait until interaction begins. The two recording paths may
|
|
5754
|
-
both be active. A command to either path has no implied effect on the other.
|
|
5755
|
-
|
|
5756
|
-
Each confirmed observation has a fresh opaque observation identity, a canonical UTC millisecond
|
|
5757
|
-
observation instant and an exclusive expiry. These times describe current evidence, never historical
|
|
5758
|
-
capture boundaries. A trusted observer-domain current time must satisfy observedAt <= now < validUntil.
|
|
5759
|
-
Use `effectiveRecordingState` with that explicitly trusted time; an unavailable clock yields unknown.
|
|
5760
|
-
Receipt, replay and task publication never extend freshness. Clock discontinuity invalidates evidence;
|
|
5761
|
-
consumers using the explicit recording-expiry clock-estimation exception must invalidate that
|
|
5762
|
-
estimate and use monotonic aging so clock rollback cannot
|
|
5763
|
-
revive expired evidence. The executor compares observation identity and recording identity against
|
|
5764
|
-
its latest state atomically before I/O. An intervening observation or assignment change refuses the
|
|
5765
|
-
stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
|
|
5766
|
-
identity or observation time publish unknown and offer no issuable controls.
|
|
5767
|
-
|
|
5768
|
-
Use `validateTask` and `validateTaskCommand` for published shape and static permissions.
|
|
5769
|
-
Immediately before dispatch, additionally use `validateRecordingRequest` against the full current
|
|
5770
|
-
task and explicit observer-domain time. For agent application commands also supply the current agent application declaration,
|
|
5771
|
-
full agent application report and softphone context. The same checks must run at the executing boundary;
|
|
5772
|
-
client validation alone is not authorization. The executor serializes operations on each recorder
|
|
5773
|
-
and compares `observationId` and `recordingId` against its latest evidence before acting: a
|
|
5774
|
-
command against superseded evidence is refused, and nothing is retried on the executor's own
|
|
5775
|
-
account.
|
|
5776
|
-
|
|
5777
|
-
Keep command progress in agent application UI separately from authoritative state. Applied means the requested
|
|
5778
|
-
recording transition and any retain/discard effect actually completed; publish the confirming task or agent application report before
|
|
5779
|
-
resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
|
|
5780
|
-
show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
|
|
5781
|
-
updates remain full current task/agent application views, not replayed provider events. These current-state fields
|
|
5782
|
-
create no interaction-history entries and infer no actors, durations or historical capture boundaries.
|
|
5783
|
-
|
|
5784
|
-
Migration is explicit: replace the old true recording capability with per-path action policies,
|
|
5785
|
-
replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
|
|
5786
|
-
development, hosts and adapters must align their recording contracts; negotiation does not detect
|
|
5787
|
-
differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
|
|
5788
|
-
This change does not publish a package or enable recording in any existing agent application/provider by itself.
|
|
5789
|
-
|
|
5790
|
-
`validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
|
|
5791
|
-
semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
|
|
5792
|
-
retention/completion or source truth from a status flag; those remain executor obligations.
|
|
5793
|
-
|
|
5794
|
-
The storage place named on the task is bound when start actually creates a recording. Existing recordings
|
|
5795
|
-
keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
|
|
5796
|
-
The executor rejects a mismatched storage place rather than moving or discarding another binding.
|
|
5797
|
-
Action permission does not authorize unattended invocation: agent application controls require the agent's explicit
|
|
5798
|
-
act, and provider authorization remains enforced at its authenticated command boundary.
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
Recording dispatch must receive the same known task-validation context as the published task.
|
|
5802
|
-
Pass organisation levels and other known restrictions through the optional fourth argument of
|
|
5803
|
-
`validateTaskCommand`, and through `taskContext` in `validateRecordingRequest`. This keeps a valid
|
|
5804
|
-
custom organisation lock from being rejected against the default ladder, and prevents known
|
|
5805
|
-
capability restrictions from disappearing at dispatch. The standalone `validateRecordingCommandState`
|
|
5806
|
-
requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
### Agent application recording announcements to the caller
|
|
5810
|
-
|
|
5811
|
-
`HostRecording.announcesToCaller`, when true, is a guarantee that the agent application delivers audible recording
|
|
5812
|
-
status messages to the remote party over the call's outgoing audio. It belongs inside the optional
|
|
5813
|
-
agent application recording capability: without that capability there is no such guarantee. Omission makes no
|
|
5814
|
-
promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
|
|
5815
|
-
or the general `HostGuarantees` object. A provider can inspect this agent application guarantee when deciding
|
|
5816
|
-
whether to permit agent application recording on a task.
|
|
5817
|
-
|
|
5818
|
-
The guarantee applies only to agent application-owned recordings. Providers implement announcements for their
|
|
5819
|
-
own recordings at their end; provider state updates must not cause the agent application to announce those
|
|
5820
|
-
recordings. Both recorders can operate independently on the same call.
|
|
5821
|
-
|
|
5822
|
-
On a confirmed agent application start, the remote party hears that recording started. On a confirmed stop or
|
|
5823
|
-
cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
|
|
5824
|
-
pause is not presented as a finished recording. Announcements follow actual capture transitions,
|
|
5825
|
-
not button clicks, accepted requests or task-policy changes. When attaching to an already-active
|
|
5826
|
-
agent application recording, announce that recording is active, without inventing its original start time.
|
|
5827
|
-
Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
|
|
5828
|
-
must never produce a stopped announcement.
|
|
5829
|
-
|
|
5830
|
-
A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
|
|
5831
|
-
microphone does not fulfil this guarantee. The agent application must deliver the message directly in the audio
|
|
5832
|
-
sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
|
|
5833
|
-
Only declare the guarantee when the agent application can provide that outgoing audio path. An announcement
|
|
5834
|
-
that cannot be delivered, including after the remote party has disconnected, is a visible failure;
|
|
5835
|
-
never claim delivery or replay the notice into a different call.
|
|
5836
|
-
|
|
5837
|
-
For a command under this guarantee, applied requires both the recording action and its announcement
|
|
5838
|
-
to succeed. If capture changes but announcement delivery fails, publish the actual recording state,
|
|
5839
|
-
report the announcement failure visibly and answer `failed` under `omni.recording-unsettled`. Do not claim no
|
|
5840
|
-
effect, automatically repeat the recording action or silently switch to an agent-only notice.
|
|
5841
|
-
|
|
5842
|
-
`validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
|
|
5843
|
-
delivery remains the agent application implementation's responsibility and must be exercised in its own tests.
|