@xema/omni-protocol 0.1.24 → 0.1.26
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 +1 -1
- package/dist/index.d.ts +69 -63
- package/dist/index.js +5 -5
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +31 -23
- package/dist/validation.d.ts +10 -10
- package/dist/validation.js +93 -90
- package/guide.md +165 -132
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -27,7 +27,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
27
27
|
| **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
|
|
28
28
|
| **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. |
|
|
29
29
|
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*, and nothing about who carries it out — that is fixed per command, see **Where a command executes**. |
|
|
30
|
-
| **Login** | One authenticated sign-in to one provider, identified by `
|
|
30
|
+
| **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. |
|
|
31
31
|
| **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. |
|
|
32
32
|
| **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
|
|
33
33
|
| **Concurrent capacity** | How many tasks this provider may have allocated to the agent at once — an absolute ceiling, stated as `AgentCapacity.count` and standing until Omni restates it. The provider counts its own outstanding tasks against it. |
|
|
@@ -36,14 +36,15 @@ are used precisely throughout and mean nothing looser here.
|
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
one
|
|
41
|
-
|
|
39
|
+
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
40
|
+
one thing, so a bare "status" in conversation is always the authentication session's, and a
|
|
41
|
+
transport, a task, a break and a call each have a word of their own:
|
|
42
42
|
|
|
43
43
|
| Word | Belongs to | Values |
|
|
44
44
|
| --- | --- | --- |
|
|
45
45
|
| `phase` | A task | `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, `completing` |
|
|
46
|
-
| `
|
|
46
|
+
| `media` | A task's audio | `started`, `ended` |
|
|
47
|
+
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
47
48
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
48
49
|
| `approval` | A break request | `not-requested`, `awaiting-decision`, `granted`, `starting-after-task`, `in-effect` |
|
|
49
50
|
| `availability` | A roster member | `ready`, `on-task`, `on-break`, `signed-out` |
|
|
@@ -146,20 +147,20 @@ type Attribute = { key: string; value: string };
|
|
|
146
147
|
```ts
|
|
147
148
|
type AuthenticationMethod = "browser-sso" | "credentials";
|
|
148
149
|
|
|
149
|
-
type
|
|
150
|
+
type BrowserAccess = {
|
|
150
151
|
mode: "allow-all" | "block-all";
|
|
151
152
|
allowList?: string[];
|
|
152
153
|
blockList?: string[];
|
|
153
154
|
};
|
|
154
155
|
|
|
155
156
|
type PersonalBrowserCapability = {
|
|
156
|
-
access:
|
|
157
|
+
access: BrowserAccess;
|
|
157
158
|
accessPolicyScope?: "initial-url" | "all-navigation";
|
|
158
159
|
};
|
|
159
160
|
|
|
160
|
-
type
|
|
161
|
+
type DialDestinations = "contacts-only" | "any-number";
|
|
161
162
|
|
|
162
|
-
type DialCapability = {
|
|
163
|
+
type DialCapability = { destinations: DialDestinations };
|
|
163
164
|
|
|
164
165
|
type IdleCapabilities<C extends Channel = Channel> = {
|
|
165
166
|
personalBrowser?: PersonalBrowserCapability;
|
|
@@ -176,7 +177,7 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
176
177
|
idleCapabilities?: IdleCapabilities<C>;
|
|
177
178
|
phaseLabels?: TaskPhaseLabels;
|
|
178
179
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
179
|
-
|
|
180
|
+
orgLevels?: LevelDeclaration[];
|
|
180
181
|
};
|
|
181
182
|
```
|
|
182
183
|
|
|
@@ -215,7 +216,7 @@ type SecretStore = {
|
|
|
215
216
|
|
|
216
217
|
type AuthenticationContext = {
|
|
217
218
|
protocolVersion: number;
|
|
218
|
-
|
|
219
|
+
loginId: string;
|
|
219
220
|
secrets: SecretStore;
|
|
220
221
|
signal?: AbortSignal;
|
|
221
222
|
log?: (entry: unknown) => void;
|
|
@@ -227,7 +228,7 @@ type TeamCapabilities = {
|
|
|
227
228
|
policyControl?: true;
|
|
228
229
|
};
|
|
229
230
|
|
|
230
|
-
type
|
|
231
|
+
type UserCapabilities = {
|
|
231
232
|
breaks?: true;
|
|
232
233
|
preferences?: AgentPreference[];
|
|
233
234
|
team?: TeamCapabilities;
|
|
@@ -236,8 +237,8 @@ type SessionCapabilities = {
|
|
|
236
237
|
type AuthenticationState =
|
|
237
238
|
| { status: "signed-out" }
|
|
238
239
|
| { status: "authenticating" }
|
|
239
|
-
| { status: "authenticated"; identity: User; capabilities:
|
|
240
|
-
| { status: "refreshing"; identity: User; capabilities:
|
|
240
|
+
| { status: "authenticated"; identity: User; capabilities: UserCapabilities; expiresAt?: IsoTimestamp }
|
|
241
|
+
| { status: "refreshing"; identity: User; capabilities: UserCapabilities }
|
|
241
242
|
| { status: "expired"; identity?: User; failure?: AuthenticationFailure };
|
|
242
243
|
|
|
243
244
|
type AuthenticationFailure = {
|
|
@@ -262,7 +263,6 @@ type HostAudioOutput =
|
|
|
262
263
|
type UrlVisibility = "full" | "domain" | "hidden";
|
|
263
264
|
|
|
264
265
|
type HostReport = {
|
|
265
|
-
browsers: { urlVisibility: UrlVisibility };
|
|
266
266
|
online: boolean;
|
|
267
267
|
audio?: {
|
|
268
268
|
input: HostAudioInput;
|
|
@@ -277,14 +277,14 @@ type Host = {
|
|
|
277
277
|
|
|
278
278
|
type ConnectContext = {
|
|
279
279
|
protocolVersion: number;
|
|
280
|
-
|
|
280
|
+
loginId: string;
|
|
281
281
|
autoAcceptTasks?: boolean;
|
|
282
282
|
host: Host;
|
|
283
283
|
signal?: AbortSignal;
|
|
284
284
|
log?: (entry: unknown) => void;
|
|
285
285
|
};
|
|
286
286
|
|
|
287
|
-
type
|
|
287
|
+
type TransportStatus = "connecting" | "active" | "error";
|
|
288
288
|
|
|
289
289
|
type CredentialField = {
|
|
290
290
|
name: string;
|
|
@@ -311,7 +311,7 @@ type CompleteAuthenticationRequest =
|
|
|
311
311
|
| { flowId: string; method: "credentials"; values: Readonly<Record<string, string>> };
|
|
312
312
|
|
|
313
313
|
type CompleteAuthenticationResult =
|
|
314
|
-
| { status: "authenticated"; identity: User; capabilities:
|
|
314
|
+
| { status: "authenticated"; identity: User; capabilities: UserCapabilities; expiresAt?: IsoTimestamp }
|
|
315
315
|
| { status: "rejected"; failure: AuthenticationFailure };
|
|
316
316
|
|
|
317
317
|
type AuthenticationActionResult =
|
|
@@ -336,11 +336,11 @@ type AuthenticationSession = {
|
|
|
336
336
|
```ts
|
|
337
337
|
type PreferenceId = "hold" | "mute" | `skill:${string}`;
|
|
338
338
|
|
|
339
|
-
type SetBy =
|
|
339
|
+
type SetBy = Level | "provider";
|
|
340
340
|
|
|
341
341
|
type Resolved = {
|
|
342
342
|
setBy: SetBy;
|
|
343
|
-
lockedBy?:
|
|
343
|
+
lockedBy?: Level;
|
|
344
344
|
reason?: string;
|
|
345
345
|
};
|
|
346
346
|
|
|
@@ -359,10 +359,11 @@ type PreferenceResult =
|
|
|
359
359
|
| { status: "failed"; failure: ProtocolFailure };
|
|
360
360
|
|
|
361
361
|
type Snapshot = {
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
transport: TransportStatus;
|
|
363
|
+
loginId: string;
|
|
364
364
|
break: BreakState;
|
|
365
365
|
tasks: Task[];
|
|
366
|
+
taskCount: number;
|
|
366
367
|
contacts?: Contact[];
|
|
367
368
|
scheduledActivities?: ScheduledActivity[];
|
|
368
369
|
team?: TeamRoster;
|
|
@@ -410,7 +411,7 @@ type DialResult =
|
|
|
410
411
|
```ts
|
|
411
412
|
type DispositionCode = { id: string; label: string; group?: string };
|
|
412
413
|
|
|
413
|
-
type
|
|
414
|
+
type DispositionRules = {
|
|
414
415
|
required?: boolean;
|
|
415
416
|
notes?: "required" | "optional" | "hidden";
|
|
416
417
|
codes?: DispositionCode[];
|
|
@@ -431,7 +432,7 @@ type DestinationDirectory = {
|
|
|
431
432
|
type CustomCapability = {
|
|
432
433
|
id: string;
|
|
433
434
|
ui: {
|
|
434
|
-
|
|
435
|
+
control: "button" | "toggle" | "menu-item";
|
|
435
436
|
label: string;
|
|
436
437
|
placement: "primary" | "secondary" | "overflow";
|
|
437
438
|
render?: "inline" | "page";
|
|
@@ -441,7 +442,7 @@ type CustomCapability = {
|
|
|
441
442
|
|
|
442
443
|
type SharedTaskCapabilities = {
|
|
443
444
|
browsers?: true;
|
|
444
|
-
dispositions?: true |
|
|
445
|
+
dispositions?: true | DispositionRules;
|
|
445
446
|
custom?: CustomCapability[];
|
|
446
447
|
};
|
|
447
448
|
|
|
@@ -481,18 +482,22 @@ const BROWSER_ISOLATION_SCHEMES = {
|
|
|
481
482
|
type BrowserIsolationScheme =
|
|
482
483
|
(typeof BROWSER_ISOLATION_SCHEMES)[keyof typeof BROWSER_ISOLATION_SCHEMES];
|
|
483
484
|
|
|
484
|
-
type
|
|
485
|
+
type Browser = {
|
|
485
486
|
id: string;
|
|
486
487
|
name: string;
|
|
487
|
-
purpose: string;
|
|
488
488
|
url: string;
|
|
489
489
|
};
|
|
490
490
|
|
|
491
|
-
type TaskBrowser =
|
|
491
|
+
type TaskBrowser = Browser & {
|
|
492
|
+
purpose: string;
|
|
493
|
+
urlVisibility?: UrlVisibility;
|
|
494
|
+
} & (
|
|
492
495
|
| { reuse: false; isolationScheme?: never }
|
|
493
496
|
| { reuse: true; isolationScheme: BrowserIsolationScheme }
|
|
494
497
|
);
|
|
495
498
|
|
|
499
|
+
type PersonalBrowser = Browser;
|
|
500
|
+
|
|
496
501
|
type BrowserSessionKeyInput = {
|
|
497
502
|
providerId: string;
|
|
498
503
|
taskId: TaskId;
|
|
@@ -546,8 +551,8 @@ type TaskHandlingStep = {
|
|
|
546
551
|
};
|
|
547
552
|
|
|
548
553
|
type TaskCompletion =
|
|
549
|
-
| { completionMode: "agent-command";
|
|
550
|
-
| { completionMode: "provider-automatic";
|
|
554
|
+
| { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
|
|
555
|
+
| { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
|
|
551
556
|
|
|
552
557
|
type TaskConsultation = {
|
|
553
558
|
destination: string;
|
|
@@ -556,7 +561,7 @@ type TaskConsultation = {
|
|
|
556
561
|
};
|
|
557
562
|
|
|
558
563
|
type TaskLead = {
|
|
559
|
-
|
|
564
|
+
stage: "requested" | "joined";
|
|
560
565
|
leadId?: UserId;
|
|
561
566
|
note?: string;
|
|
562
567
|
since: IsoTimestamp;
|
|
@@ -568,21 +573,21 @@ type TaskAssisting = {
|
|
|
568
573
|
since: IsoTimestamp;
|
|
569
574
|
};
|
|
570
575
|
|
|
571
|
-
type
|
|
576
|
+
type Level = string;
|
|
572
577
|
|
|
573
|
-
type
|
|
574
|
-
id:
|
|
578
|
+
type LevelDeclaration = {
|
|
579
|
+
id: Level;
|
|
575
580
|
label: string;
|
|
576
581
|
};
|
|
577
582
|
|
|
578
583
|
type Locked = {
|
|
579
|
-
lockedBy:
|
|
584
|
+
lockedBy: Level;
|
|
580
585
|
reason?: string;
|
|
581
586
|
};
|
|
582
587
|
|
|
583
588
|
type Lockable<T> = T | Locked;
|
|
584
589
|
|
|
585
|
-
type TaskMediaState = "
|
|
590
|
+
type TaskMediaState = "started" | "ended";
|
|
586
591
|
|
|
587
592
|
type Task<C extends Channel = Channel> = {
|
|
588
593
|
id: TaskId;
|
|
@@ -704,33 +709,32 @@ what the queue allows: on for everyone, off for everyone, or left to the person.
|
|
|
704
709
|
your team is a **policy**; what you do to yourself is a **preference**. A person belongs to one
|
|
705
710
|
team and many queues, so a policy applies across every queue the person works.
|
|
706
711
|
|
|
707
|
-
**The provider resolves; the protocol carries the result and who decided.** The structure's
|
|
708
|
-
are the provider's ladder: each
|
|
712
|
+
**The provider resolves; the protocol carries the result and who decided.** The structure's levels
|
|
713
|
+
are the provider's ladder: each level states only what it sets, an enforcing policy at a level above
|
|
709
714
|
the person settles the value for everyone below it, and where nothing enforces the most specific
|
|
710
|
-
|
|
711
|
-
never describes the chain between them: which
|
|
712
|
-
know. A typical organisation has four, and they are the defaults — `
|
|
715
|
+
level that says anything wins. The protocol names a level by the id the manifest declares for it and
|
|
716
|
+
never describes the chain between them: which levels a person passes through is the structure's to
|
|
717
|
+
know. A typical organisation has four, and they are the defaults — `DEFAULT_LEVELS`: `org`, `site`,
|
|
713
718
|
`team`, `person`, each with the label a desk shows. A structure that differs states its whole
|
|
714
|
-
ladder in `Manifest.
|
|
715
|
-
leaves out does not exist — a structure with no site
|
|
716
|
-
`site` is refused on its wire. A declared
|
|
719
|
+
ladder in `Manifest.orgLevels`, `person` included: what the list carries is in force, and what it
|
|
720
|
+
leaves out does not exist — a structure with no site level declares `org`, `team`, `person`, and
|
|
721
|
+
`site` is refused on its wire. A declared level is one the provider's own store actually resolves
|
|
717
722
|
at: a label with no policy behind it decides nothing. A manifest that declares none has exactly
|
|
718
|
-
the four. `lockedBy` is any
|
|
719
|
-
`setBy` is any
|
|
720
|
-
anything and the provider's own configuration supplied the value"
|
|
721
|
-
Omni's provisioning file, which does not reach the wire. A host renders "who decided" from the
|
|
723
|
+
the four. `lockedBy` is any level in force except `person`, who never locks their own value;
|
|
724
|
+
`setBy` is any level in force, or `provider`, the protocol's word for "no level has said
|
|
725
|
+
anything and the provider's own configuration supplied the value". A host renders "who decided" from the
|
|
722
726
|
declared labels and needs no others, and validates every republished `authenticated` state
|
|
723
727
|
against them, not only the sign-in. What the wire carries is the resolution:
|
|
724
728
|
|
|
725
|
-
- **`lockedBy`** — a
|
|
726
|
-
their own value, and the queue is not a
|
|
727
|
-
- **`setBy`** — who stated the value as it stands: a
|
|
728
|
-
`
|
|
729
|
-
broad
|
|
729
|
+
- **`lockedBy`** — a level above the person made this value theirs to keep. A person never locks
|
|
730
|
+
their own value, and the queue is not a level: what the queue does not allow at all is absent.
|
|
731
|
+
- **`setBy`** — who stated the value as it stands: a level, the `person` themself, or
|
|
732
|
+
`provider` where no level has said anything. Provenance, not a lock: a value that came from a
|
|
733
|
+
broad level as a default is still the person's to change.
|
|
730
734
|
|
|
731
735
|
**On a task, a control the queue could allow may stand locked in its place.** `Task.capabilities`
|
|
732
736
|
is the effective set. What the queue does not allow is absent and nothing is shown. What the queue
|
|
733
|
-
allows and a
|
|
737
|
+
allows and a level above the person locked is present as `{ lockedBy, reason? }` where the control's
|
|
734
738
|
value would be — `mute: { lockedBy: "team", reason: "Nobody on this team mutes" }` — and Omni
|
|
735
739
|
renders that control disabled, saying who decided, so an agent who cannot press Mute knows whether
|
|
736
740
|
to ask their lead or their site. `lockedBy` is the discriminant: a value that carries it is the
|
|
@@ -746,20 +750,20 @@ controls — is content, and is never locked.
|
|
|
746
750
|
with `on`, `off`, or `agent`, for any task control, `dial`, or a skill — and only `hold`, `mute`
|
|
747
751
|
and skills may be `agent`; callback and new call are the team's, on or off, within what the queue
|
|
748
752
|
allows. The roster carries `policies` for such a login: every policy as it stands, who set it, and
|
|
749
|
-
`lockedBy` where a
|
|
753
|
+
`lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
|
|
750
754
|
change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
|
|
751
755
|
|
|
752
756
|
**What the team left to the person is the person's, and the provider keeps it.** The login's
|
|
753
757
|
`capabilities.preferences` lists every preference the person may hold — `hold`, `mute`, a skill —
|
|
754
758
|
with where it stands and who set it: `setBy: "team"` while they inherit the team's default,
|
|
755
|
-
`"person"` once they have set their own, `"
|
|
756
|
-
is hidden for want of a row, and a preference a
|
|
759
|
+
`"person"` once they have set their own, `"provider"` where no level has said anything. Nothing
|
|
760
|
+
is hidden for want of a row, and a preference a level above has since locked is listed with
|
|
757
761
|
`lockedBy`. `setPreference` is the person's act — `{ id, enabled }` to set their own, `{ id,
|
|
758
762
|
inherit: true }` to give it up and inherit again — answered `applied` and republished as a new
|
|
759
763
|
`authenticated` state when something changed, a state and not a flicker, as every republish of
|
|
760
764
|
`authenticated` is; and it is durable: the person's across sessions. A lead may also set a
|
|
761
765
|
person's preference from their own screen, which arrives the same way. A preference is keyed by
|
|
762
|
-
the capability's own name because it is the same capability at another
|
|
766
|
+
the capability's own name because it is the same capability at another level: effective in
|
|
763
767
|
`Task.capabilities`, set for the team in `policies`, left to the person in `preferences`. The
|
|
764
768
|
command `mute` acts on one call; the preference `mute` says whether the person wants the control
|
|
765
769
|
at all, and a host renders it in its settings, never as the button on a call.
|
|
@@ -793,7 +797,7 @@ type ImposedBreak =
|
|
|
793
797
|
|
|
794
798
|
type BreakState = {
|
|
795
799
|
approval: BreakApproval;
|
|
796
|
-
|
|
800
|
+
mayAsk: boolean;
|
|
797
801
|
refusedReason?: string;
|
|
798
802
|
decisionReason?: string;
|
|
799
803
|
retryAfterMs?: number;
|
|
@@ -921,12 +925,12 @@ type ProviderSummary = {
|
|
|
921
925
|
metrics?: SummaryMetric[];
|
|
922
926
|
};
|
|
923
927
|
|
|
924
|
-
type
|
|
928
|
+
type TransportRecovery = "reconnect" | "reauthenticate";
|
|
925
929
|
|
|
926
930
|
type ProviderEvent =
|
|
927
931
|
| { type: "snapshot"; reason: "reconnected" | "provider-requested"; snapshot: Snapshot }
|
|
928
|
-
| { type: "
|
|
929
|
-
| { type: "
|
|
932
|
+
| { type: "transport-status"; status: "connecting" | "active"; message?: string }
|
|
933
|
+
| { type: "transport-status"; status: "error"; recovery: TransportRecovery; message?: string }
|
|
930
934
|
| { type: "break-state"; break: BreakState }
|
|
931
935
|
| {
|
|
932
936
|
type: "task-offered";
|
|
@@ -936,7 +940,7 @@ type ProviderEvent =
|
|
|
936
940
|
preparationEndsAt?: IsoTimestamp;
|
|
937
941
|
}
|
|
938
942
|
| { type: "task-updated"; task: Task }
|
|
939
|
-
| { type: "task-media-
|
|
943
|
+
| { type: "task-media-started"; taskId: TaskId }
|
|
940
944
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
941
945
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
942
946
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
@@ -947,7 +951,7 @@ type ProviderEvent =
|
|
|
947
951
|
|
|
948
952
|
type ProviderEventEnvelope = {
|
|
949
953
|
id: string;
|
|
950
|
-
|
|
954
|
+
loginId: string;
|
|
951
955
|
occurredAt: IsoTimestamp;
|
|
952
956
|
event: ProviderEvent;
|
|
953
957
|
};
|
|
@@ -997,12 +1001,12 @@ const ALLOWED_BROWSER_URL_SCHEMES = ["http:", "https:"] as const;
|
|
|
997
1001
|
const IDLE_CAPABILITIES = ["dial", "personalBrowser", "calendar", "contacts"] as const;
|
|
998
1002
|
type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
|
|
999
1003
|
|
|
1000
|
-
const
|
|
1004
|
+
const DEFAULT_LEVELS = [
|
|
1001
1005
|
{ id: "org", label: "Your organisation" },
|
|
1002
1006
|
{ id: "site", label: "Your site" },
|
|
1003
1007
|
{ id: "team", label: "Your team" },
|
|
1004
1008
|
{ id: "person", label: "You" },
|
|
1005
|
-
] as const satisfies readonly
|
|
1009
|
+
] as const satisfies readonly LevelDeclaration[];
|
|
1006
1010
|
|
|
1007
1011
|
const IDLE_CAPABILITY_UI = {
|
|
1008
1012
|
dial: "Dialpad",
|
|
@@ -1223,6 +1227,17 @@ which field says so varies. See **Which commands need a capability**.
|
|
|
1223
1227
|
|
|
1224
1228
|
Snapshots establish and replace provider state when an agent signs in, reconnects, or resynchronises.
|
|
1225
1229
|
|
|
1230
|
+
**A snapshot is the provider's answer, and an adapter that got no answer publishes nothing.** A
|
|
1231
|
+
state read that answers unknown — a session not yet associated, a backend mid-failover — is not an
|
|
1232
|
+
empty state: the adapter keeps what it holds, stays `connecting`, and publishes a snapshot only
|
|
1233
|
+
once the provider has actually answered for this login, exactly as `connect()` may not resolve
|
|
1234
|
+
before it can provide a meaningful one. And emptiness is stated, never inferred: every snapshot
|
|
1235
|
+
carries `taskCount`, the provider's own count reconciled against `tasks.length`, so a snapshot
|
|
1236
|
+
with no work says `taskCount: 0` in so many words and a blank or half-built state — which lacks
|
|
1237
|
+
the count — can never pass as a confirmed empty. Absence of knowledge is never evidence of
|
|
1238
|
+
absence, and every place "empty" is allowed to carry both meanings will eventually clear
|
|
1239
|
+
somebody's live call.
|
|
1240
|
+
|
|
1226
1241
|
Events report completed transactions after that baseline. Nothing is missed while the connection
|
|
1227
1242
|
holds; when it drops, the reconnect snapshot re-establishes the baseline before any further event
|
|
1228
1243
|
is applied.
|
|
@@ -1235,7 +1250,7 @@ make a repeat safe. Omni does not retry; if the agent acts again it is a new com
|
|
|
1235
1250
|
|
|
1236
1251
|
On a persistent ordered transport there is only one way a command goes unsettled: the connection
|
|
1237
1252
|
went away underneath it. **An adapter that cannot settle a command has lost its transport, and
|
|
1238
|
-
says so** — `
|
|
1253
|
+
says so** — `transport-status` `connecting`, reconnect, snapshot — whichever channel the command
|
|
1239
1254
|
actually travelled on. An unsettled promise is therefore always followed by a snapshot, and that
|
|
1240
1255
|
snapshot is the answer; Omni waits for it rather than calling `snapshot()` itself. While the
|
|
1241
1256
|
transport is up, a result says the provider accepted the command, and the event that follows —
|
|
@@ -1244,7 +1259,7 @@ transport is up, a result says the provider accepted the command, and the event
|
|
|
1244
1259
|
**Classify on the rejection, never on a connection status published separately from it.** The
|
|
1245
1260
|
status is a report about the wire and races the rejection; the rejection is the event. A rejection
|
|
1246
1261
|
is the provider's answer only when it carries the provider's answer — a failure the provider
|
|
1247
|
-
named. Every other rejection is transport loss, whatever the last `
|
|
1262
|
+
named. Every other rejection is transport loss, whatever the last `transport-status` said.
|
|
1248
1263
|
|
|
1249
1264
|
A command therefore carries no key. The provider names its own records — a task, a lead request, a
|
|
1250
1265
|
member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
|
|
@@ -1334,7 +1349,7 @@ export default defineAdapter({
|
|
|
1334
1349
|
supportedProtocolVersions: [OMNI_PROTOCOL_VERSION],
|
|
1335
1350
|
authenticationMethods: ["browser-sso"],
|
|
1336
1351
|
idleCapabilities: {
|
|
1337
|
-
dial: {
|
|
1352
|
+
dial: { destinations: "any-number" },
|
|
1338
1353
|
},
|
|
1339
1354
|
},
|
|
1340
1355
|
createAuthenticationSession: context => createAcmeAuthentication(context),
|
|
@@ -1359,7 +1374,7 @@ compile time.
|
|
|
1359
1374
|
| `idleCapabilities` | Declares actions Omni may offer while the agent has no active task, such as voice dialing. Task controls do not belong here. |
|
|
1360
1375
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1361
1376
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1362
|
-
| `
|
|
1377
|
+
| `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**. |
|
|
1363
1378
|
|
|
1364
1379
|
### Authentication methods
|
|
1365
1380
|
|
|
@@ -1413,11 +1428,11 @@ a destination policy:
|
|
|
1413
1428
|
|
|
1414
1429
|
```ts
|
|
1415
1430
|
idleCapabilities: {
|
|
1416
|
-
dial: {
|
|
1431
|
+
dial: { destinations: "any-number" }
|
|
1417
1432
|
}
|
|
1418
1433
|
```
|
|
1419
1434
|
|
|
1420
|
-
`
|
|
1435
|
+
`destinations` is required and accepts one `DialDestinations`:
|
|
1421
1436
|
|
|
1422
1437
|
| Value | Contract |
|
|
1423
1438
|
| --- | --- |
|
|
@@ -1612,7 +1627,7 @@ Creates the provider-scoped authentication session.
|
|
|
1612
1627
|
| `AuthenticationContext` field | Contract |
|
|
1613
1628
|
| --- | --- |
|
|
1614
1629
|
| `protocolVersion` | The negotiated version, fixed for this login. |
|
|
1615
|
-
| `
|
|
1630
|
+
| `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. |
|
|
1616
1631
|
| `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
|
|
1617
1632
|
| `signal` | Optional cancellation signal. |
|
|
1618
1633
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -1646,11 +1661,11 @@ login fix, rendered apart from a provider Omni cannot reach. Commands meanwhile
|
|
|
1646
1661
|
`omni.not-authenticated`.
|
|
1647
1662
|
|
|
1648
1663
|
**Re-authentication restores the login; it does not replace it.** It runs on the session Omni
|
|
1649
|
-
kept, under the same `
|
|
1664
|
+
kept, under the same `loginId` — the old `flowId` died with the expiry, so the adapter issues a
|
|
1650
1665
|
new challenge — and when the state returns to `authenticated` the connection and everything on it
|
|
1651
1666
|
carry on: Omni does not call `connect()` again, since a second connection would be a second
|
|
1652
1667
|
session for one agent. What "signing in again replaces the login" describes is a new
|
|
1653
|
-
`AuthenticationSession` under a new `
|
|
1668
|
+
`AuthenticationSession` under a new `loginId`, after `signed-out`.
|
|
1654
1669
|
|
|
1655
1670
|
### What the login may do
|
|
1656
1671
|
|
|
@@ -1801,14 +1816,14 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1801
1816
|
- The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
|
|
1802
1817
|
- May be called again on the same login after that: once per `Connection`, not once per login.
|
|
1803
1818
|
Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
|
|
1804
|
-
calls `connect()` afresh — see **`
|
|
1819
|
+
calls `connect()` afresh — see **`transport-status`**.
|
|
1805
1820
|
|
|
1806
1821
|
### `ConnectContext`
|
|
1807
1822
|
|
|
1808
1823
|
| Field | Contract |
|
|
1809
1824
|
| --- | --- |
|
|
1810
1825
|
| `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
|
|
1811
|
-
| `
|
|
1826
|
+
| `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. |
|
|
1812
1827
|
| `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, `task-offered` carries an `acceptanceMode`; when `false`, every task requires agent acceptance. |
|
|
1813
1828
|
| `host` | The host'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 host reports, the adapter decides**. |
|
|
1814
1829
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
@@ -1833,10 +1848,11 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
1833
1848
|
|
|
1834
1849
|
| Field | Contract |
|
|
1835
1850
|
| --- | --- |
|
|
1836
|
-
| `
|
|
1837
|
-
| `
|
|
1838
|
-
| `break` | Complete break state, including approval,
|
|
1851
|
+
| `transport` | Current `TransportStatus` — whether this provider's transport can serve the login. Defined under **`transport-status`**. |
|
|
1852
|
+
| `loginId` | Identity of this login. It must match the connection context. |
|
|
1853
|
+
| `break` | Complete break state, including approval, whether the agent may ask, reasons, retry details, and any imposed break. |
|
|
1839
1854
|
| `tasks` | Complete set of tasks currently offered to or owned by this agent. |
|
|
1855
|
+
| `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. |
|
|
1840
1856
|
| `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
|
|
1841
1857
|
| `scheduledActivities` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
|
|
1842
1858
|
| `team` | Required `TeamRoster` when the login declares `capabilities.team`, `[]` when nobody is in it. Forbidden otherwise — the login is the permission. |
|
|
@@ -2057,10 +2073,10 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2057
2073
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2058
2074
|
| `contact` | Optional `Contact` for the person or entity on this task. Often a name and one address; a withheld caller ID may leave nothing to send at all. |
|
|
2059
2075
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
|
|
2060
|
-
| `media` | Voice only. The task's real-time audio as the provider holds it: `
|
|
2076
|
+
| `media` | 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-media-started`**. |
|
|
2061
2077
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2062
2078
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2063
|
-
| `
|
|
2079
|
+
| `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
2064
2080
|
| `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. |
|
|
2065
2081
|
| `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
|
|
2066
2082
|
| `consultation` | Voice only. Present while the agent is consulting a transfer destination: who is being consulted, and since when where the provider records it. Its presence is what makes `transfer` `complete` and `cancel` issuable. `label` is a name for the destination -- a person, a queue -- not a phrase; the host supplies the verb. See **Consult transfer**. |
|
|
@@ -2117,7 +2133,7 @@ command.
|
|
|
2117
2133
|
routed to the agent and accepted as `acceptanceMode` dictates, and its presence and phase follow
|
|
2118
2134
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2119
2135
|
consult, a conference leg joining or leaving, a transfer, a callback — the media follows
|
|
2120
|
-
separately, arriving on `task-media-
|
|
2136
|
+
separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
|
|
2121
2137
|
`task-media-ended`. Omni does not ring,
|
|
2122
2138
|
bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
|
|
2123
2139
|
are the adapter's and the platform's, transient, and decide neither when a task exists nor what
|
|
@@ -2129,7 +2145,7 @@ allowance starts on it and the callback control appears on it — and Omni follo
|
|
|
2129
2145
|
follows any other. What Omni never does is derive a task's state from its own media session: a
|
|
2130
2146
|
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
2131
2147
|
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
2132
|
-
`task-media-
|
|
2148
|
+
`task-media-started` and `task-media-ended` alternate on a task whose work has begun, media ends
|
|
2133
2149
|
only where it arrived, what follows the media ending is `completing` or `task-ended`, and every
|
|
2134
2150
|
task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
|
|
2135
2151
|
and `assertMediaFollowsTheTask` holds any sequence.
|
|
@@ -2140,10 +2156,10 @@ and `assertMediaFollowsTheTask` holds any sequence.
|
|
|
2140
2156
|
the task open until Omni sends the channel's `complete` command. With `provider-automatic`, the
|
|
2141
2157
|
provider may complete the task without receiving that command.
|
|
2142
2158
|
|
|
2143
|
-
`
|
|
2159
|
+
`wrapAllowance` is independent of that decision. It is fixed, and when it starts depends on
|
|
2144
2160
|
whether the channel carries real-time media:
|
|
2145
2161
|
|
|
2146
|
-
| Channel |
|
|
2162
|
+
| Channel | Wrap allowance starts at |
|
|
2147
2163
|
| --- | --- |
|
|
2148
2164
|
| Voice and any channel with real-time media | The `task-media-ended` event |
|
|
2149
2165
|
| Chat | When the conversation ends and the task enters `completing` |
|
|
@@ -2153,8 +2169,8 @@ whether the channel carries real-time media:
|
|
|
2153
2169
|
```ts
|
|
2154
2170
|
const emailCompletion = {
|
|
2155
2171
|
completionMode: "agent-command",
|
|
2156
|
-
|
|
2157
|
-
} satisfies Pick<Task<"email">, "completionMode" | "
|
|
2172
|
+
wrapAllowance: 120,
|
|
2173
|
+
} satisfies Pick<Task<"email">, "completionMode" | "wrapAllowance">;
|
|
2158
2174
|
```
|
|
2159
2175
|
|
|
2160
2176
|
In this example, the agent has two minutes after sending the email to add notes, select a
|
|
@@ -2164,7 +2180,7 @@ disposition, and complete the task.
|
|
|
2164
2180
|
without waiting for a command; with `agent-command`, it still waits for `complete`.
|
|
2165
2181
|
|
|
2166
2182
|
There is no value meaning "unlimited", because a number that is not a duration would be read as
|
|
2167
|
-
one. A provider that imposes no deadline says so by **omitting** `
|
|
2183
|
+
one. A provider that imposes no deadline says so by **omitting** `wrapAllowance`, which
|
|
2168
2184
|
only `agent-command` permits: the provider will not complete the task itself, so there is nothing
|
|
2169
2185
|
for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
|
|
2170
2186
|
claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
|
|
@@ -2173,7 +2189,7 @@ Under `provider-automatic` the field is required, because the provider is going
|
|
|
2173
2189
|
```ts
|
|
2174
2190
|
const untimedWrap = {
|
|
2175
2191
|
completionMode: "agent-command",
|
|
2176
|
-
} satisfies Pick<Task<"voice">, "completionMode" | "
|
|
2192
|
+
} satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
|
|
2177
2193
|
```
|
|
2178
2194
|
|
|
2179
2195
|
Here the customer has hung up, `task-media-ended` has been sent on time, the task is `completing`,
|
|
@@ -2189,7 +2205,7 @@ Omni issues `{ type: "callback" }`; it is issuable only in `completing`, and onl
|
|
|
2189
2205
|
capability is declared. The provider knows who the party is; the command carries no destination.
|
|
2190
2206
|
|
|
2191
2207
|
On `applied` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
2192
|
-
working again, and the
|
|
2208
|
+
working again, and the wrap allowance is **discarded, not paused**. From there the call is
|
|
2193
2209
|
reported as any call is -- `paused`, `in-progress`, and when its media ends, `task-media-ended`
|
|
2194
2210
|
again, which starts a fresh allowance from that instant. A party who does not answer is a call
|
|
2195
2211
|
whose media ended: the task returns to `completing` through the same event and the clock starts
|
|
@@ -2197,7 +2213,7 @@ again from there. At no point is an agent dialling against a deadline.
|
|
|
2197
2213
|
|
|
2198
2214
|
**The control exists only while there is a window to use it in.** Under `agent-command` the task
|
|
2199
2215
|
stays `completing` until the agent completes it, so the window is open for as long as they need.
|
|
2200
|
-
Under `provider-automatic` the window is the allowance -- and with `
|
|
2216
|
+
Under `provider-automatic` the window is the allowance -- and with `wrapAllowance: 0` there
|
|
2201
2217
|
is none: the provider disposes the task at provider end, and Omni does not offer Call back, whatever
|
|
2202
2218
|
the task declares. A capability names a control that can be used; on a task with no `completing`
|
|
2203
2219
|
window it cannot, and declaring it there changes nothing.
|
|
@@ -2208,8 +2224,8 @@ const callbackCapable = {
|
|
|
2208
2224
|
capabilities: { hold: true, callback: true, dispositions: true },
|
|
2209
2225
|
phase: "completing",
|
|
2210
2226
|
completionMode: "provider-automatic",
|
|
2211
|
-
|
|
2212
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "
|
|
2227
|
+
wrapAllowance: 30,
|
|
2228
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "completionMode" | "wrapAllowance">;
|
|
2213
2229
|
```
|
|
2214
2230
|
|
|
2215
2231
|
With ten seconds of the thirty left, the agent presses Call back: `execute({ command: { type:
|
|
@@ -2220,8 +2236,8 @@ from that instant.
|
|
|
2220
2236
|
```ts
|
|
2221
2237
|
const immediateProviderCompletion = {
|
|
2222
2238
|
completionMode: "provider-automatic",
|
|
2223
|
-
|
|
2224
|
-
} satisfies Pick<Task, "completionMode" | "
|
|
2239
|
+
wrapAllowance: 0,
|
|
2240
|
+
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
2225
2241
|
```
|
|
2226
2242
|
|
|
2227
2243
|
### How a task has been handled
|
|
@@ -2296,6 +2312,13 @@ capabilities: { browsers: true }
|
|
|
2296
2312
|
|
|
2297
2313
|
Tasks without browser definitions omit the capability and provide an empty `browsers` array.
|
|
2298
2314
|
|
|
2315
|
+
**A browser is one tab, and each workspace keeps its own.** `Browser` is what a tab is — an id, a
|
|
2316
|
+
name, a URL. The task workspace shows the task's `TaskBrowser` entries, one tab each, fixed at the
|
|
2317
|
+
task's definition: their count and their details, `urlVisibility` included, arrive with the task,
|
|
2318
|
+
and nothing adds a tab to a task later. The personal workspace is the agent's: as many
|
|
2319
|
+
`PersonalBrowser` tabs as they open, never on the wire, and no provider says anything about what
|
|
2320
|
+
they may see there.
|
|
2321
|
+
|
|
2299
2322
|
#### `TaskBrowser` and isolation
|
|
2300
2323
|
|
|
2301
2324
|
Each `TaskBrowser` defines one named browser in the task workspace.
|
|
@@ -2308,6 +2331,7 @@ Each `TaskBrowser` defines one named browser in the task workspace.
|
|
|
2308
2331
|
| `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
|
|
2309
2332
|
| `reuse` | Required. `false` creates a task-specific browser session. |
|
|
2310
2333
|
| `isolationScheme` | **Required when `reuse` is `true`**, and rejected when it is `false`. There is no default: see below. |
|
|
2334
|
+
| `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; Omni honours it tab by tab. |
|
|
2311
2335
|
|
|
2312
2336
|
##### Choosing a reuse scheme
|
|
2313
2337
|
|
|
@@ -2506,7 +2530,7 @@ commands and a provider that offers `consultTransfer` implements all three.
|
|
|
2506
2530
|
|
|
2507
2531
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
2508
2532
|
dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
|
|
2509
|
-
the provider reports `task-media-ended`, any
|
|
2533
|
+
the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
|
|
2510
2534
|
`transferred` outcome naming the destination. `applied` on `cancel` says the destination is
|
|
2511
2535
|
dropped; the task returns to `in-progress` with `consultation` gone. Omni waits for the
|
|
2512
2536
|
provider's report of both, as it does for every command.
|
|
@@ -2537,13 +2561,13 @@ Every task may publish additional provider-specific controls in `capabilities.cu
|
|
|
2537
2561
|
capabilities: {
|
|
2538
2562
|
hold: true,
|
|
2539
2563
|
custom: [
|
|
2540
|
-
{ id: "request-supervisor", ui: {
|
|
2541
|
-
{ id: "mark-vip", ui: {
|
|
2564
|
+
{ id: "request-supervisor", ui: { control: "button", label: "Request supervisor", placement: "secondary" } },
|
|
2565
|
+
{ id: "mark-vip", ui: { control: "toggle", label: "Mark as VIP", placement: "overflow" } },
|
|
2542
2566
|
],
|
|
2543
2567
|
}
|
|
2544
2568
|
```
|
|
2545
2569
|
|
|
2546
|
-
Custom capability IDs must be non-empty and unique within the task. `ui.
|
|
2570
|
+
Custom capability IDs must be non-empty and unique within the task. `ui.control` is `button`, `toggle`,
|
|
2547
2571
|
or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
|
|
2548
2572
|
control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
|
|
2549
2573
|
— a tab in the same work area as the task's browsers, beside them, and alone on a task that has
|
|
@@ -2576,7 +2600,7 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
|
|
|
2576
2600
|
declares `dial`.
|
|
2577
2601
|
|
|
2578
2602
|
- `destination` is the original number selected or entered by the agent.
|
|
2579
|
-
- The provider holds `destination` to its declared `
|
|
2603
|
+
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
2580
2604
|
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
2581
2605
|
- `dialled` confirms that outbound call creation completed.
|
|
2582
2606
|
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
@@ -2594,8 +2618,8 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
2594
2618
|
| Field | Contract |
|
|
2595
2619
|
| --- | --- |
|
|
2596
2620
|
| `approval` | Where the agent's current request stands. See the states below. |
|
|
2597
|
-
| `
|
|
2598
|
-
| `refusedReason` | Display-ready reason shown when `
|
|
2621
|
+
| `mayAsk` | Whether the agent may ask at all. Distinct from `approval`. |
|
|
2622
|
+
| `refusedReason` | Display-ready reason shown when `mayAsk` is false — a standing gate that applies to everyone. |
|
|
2599
2623
|
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
2600
2624
|
| `retryAfterMs` | How long until the agent may retry, when the provider can say. |
|
|
2601
2625
|
| `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. |
|
|
@@ -2622,7 +2646,7 @@ A provider reports `starting-after-task` only after Omni commits a `granted` req
|
|
|
2622
2646
|
work is still active. Omni does not send the request again, because asking again would not move
|
|
2623
2647
|
it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
|
|
2624
2648
|
|
|
2625
|
-
`
|
|
2649
|
+
`mayAsk: false` is what lets Omni withdraw the control rather than let an agent ask and be
|
|
2626
2650
|
refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
|
|
2627
2651
|
something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
|
|
2628
2652
|
|
|
@@ -2960,7 +2984,7 @@ A lead who also takes calls sees their team on the idle dashboard. `Snapshot.tea
|
|
|
2960
2984
|
| --- | --- |
|
|
2961
2985
|
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the roster says something else entirely — see **The login is the permission** below. |
|
|
2962
2986
|
| `requests` | The members currently asking this lead to join a call, each with the task and the note. Required when the login declares `team.consultControl`, `[]` when nobody is asking; omitted when it does not. See **Consulting a lead**. |
|
|
2963
|
-
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a
|
|
2987
|
+
| `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. Required when the login declares `team.policyControl`; omitted when it does not. See **Who decides what an agent may do**. |
|
|
2964
2988
|
|
|
2965
2989
|
| `TeamMember` field | Contract |
|
|
2966
2990
|
| --- | --- |
|
|
@@ -3021,7 +3045,7 @@ never an identifier from another provider, and Omni does not translate between t
|
|
|
3021
3045
|
from `describeUsers()`.
|
|
3022
3046
|
|
|
3023
3047
|
`suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
3024
|
-
approve them. A provider that suspends breaks must also publish `
|
|
3048
|
+
approve them. A provider that suspends breaks must also publish `mayAsk: false` to the team's
|
|
3025
3049
|
agents so they see it before asking. A `place` must likewise reach that member as an `imposed` break
|
|
3026
3050
|
on their own `BreakState`, or they are stopped from working with no way to see why.
|
|
3027
3051
|
|
|
@@ -3058,7 +3082,7 @@ executeTeamConsult({ command: { type: "decline", requestId: "req-7", reason: "In
|
|
|
3058
3082
|
**On `join` the provider bridges three parties and the lead is on a task of their own**, on the
|
|
3059
3083
|
same task id, arriving on the lead's connection as `task-offered` with `require-automatic-acceptance`
|
|
3060
3084
|
-- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
|
|
3061
|
-
moves to `lead: {
|
|
3085
|
+
moves to `lead: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
|
|
3062
3086
|
trigger it; but from then on it is an outstanding task the provider counts against the lead's
|
|
3063
3087
|
stated ceiling like any other, nothing more is allocated to the lead while it stands, and a
|
|
3064
3088
|
provider whose lead is already at the ceiling answers the join `failed`.
|
|
@@ -3095,7 +3119,7 @@ const consultLeadCapable = {
|
|
|
3095
3119
|
channel: "voice",
|
|
3096
3120
|
capabilities: { hold: true, consultLead: true, dispositions: true },
|
|
3097
3121
|
phase: "in-progress",
|
|
3098
|
-
lead: {
|
|
3122
|
+
lead: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
3099
3123
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "lead">;
|
|
3100
3124
|
```
|
|
3101
3125
|
|
|
@@ -3171,7 +3195,6 @@ what failed, and reports. It never decides for the adapter what a missing microp
|
|
|
3171
3195
|
| `online` | Whether the host 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 host does — so `false` is a reason not to go ready and `true` is not a reason to. |
|
|
3172
3196
|
| `audio` | Present on a voice connection, absent where there is no audio. |
|
|
3173
3197
|
| `audio.input` | `ready` with `localAudio` — the microphone as captured, the same stream `openMedia` receives — and `flowing`, false while the hardware or OS says no audio moves through it (a headset's own mute switch, which Omni's Mute control never touches). `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which a host 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. A host 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. |
|
|
3174
|
-
| `browsers.urlVisibility` | What Omni's own chrome shows of a task browser's URL: `hidden`, `domain`, or `full`. A statement about the chrome, not about what the page renders or a screenshot captures; task browsers only — the personal browser is the agent's own and nothing a provider sends appears in it. A provider reads it before deciding what URL it is willing to send: one that carries confidential data goes out under `hidden` and not under `full`. |
|
|
3175
3198
|
| `audio.output` | `ready`, 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. |
|
|
3176
3199
|
|
|
3177
3200
|
Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
|
|
@@ -3245,8 +3268,8 @@ carries as `audio.input.localAudio`, and absent while that input is `unavailable
|
|
|
3245
3268
|
bridges audio without a host-side input may ignore it; one that needs it and finds it absent
|
|
3246
3269
|
answers `unavailable` with a failure Omni shows the agent.
|
|
3247
3270
|
|
|
3248
|
-
**When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-
|
|
3249
|
-
and on a task arriving with `media: "
|
|
3271
|
+
**When to ask is the provider's word, not Omni's guess.** Omni opens media on `task-media-started`,
|
|
3272
|
+
and on a task arriving with `media: "started"` on a snapshot; it closes on `task-media-ended` and
|
|
3250
3273
|
when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
|
|
3251
3274
|
track that ends — moves the task or its audio.
|
|
3252
3275
|
|
|
@@ -3323,7 +3346,7 @@ Applies a `TaskCommandRequest` to one provider-local task.
|
|
|
3323
3346
|
- `applied` confirms the side effect completed. `failed` confirms it did **not**, with a typed
|
|
3324
3347
|
`ProtocolFailure`; a provider that will not and one that cannot report the same shape, and `code`
|
|
3325
3348
|
says which.
|
|
3326
|
-
- A command sent while `
|
|
3349
|
+
- A command sent while `transport-status` is not `active` answers `failed` with `omni.unavailable`.
|
|
3327
3350
|
Neither Omni nor the adapter queues it.
|
|
3328
3351
|
- **A command that asks for a state answers `applied` when that state holds, whoever brought it
|
|
3329
3352
|
about; a command that acts answers `failed` when it cannot act.** Declining a lead request
|
|
@@ -3361,7 +3384,7 @@ react rather than only display the message:
|
|
|
3361
3384
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
3362
3385
|
| `omni.destination-not-permitted` | The dial or transfer destination violates the provider's policy. |
|
|
3363
3386
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
3364
|
-
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `
|
|
3387
|
+
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
3365
3388
|
| `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
|
|
3366
3389
|
|
|
3367
3390
|
They are published as `OMNI_FAILURE_CODES`.
|
|
@@ -3373,14 +3396,14 @@ They are published as `OMNI_FAILURE_CODES`.
|
|
|
3373
3396
|
| Field | Contract |
|
|
3374
3397
|
| --- | --- |
|
|
3375
3398
|
| `id` | Required identifier for this event, unique within the login. Omni does not act on it; it exists so a host log line and an adapter log line can be matched when something has to be traced. |
|
|
3376
|
-
| `
|
|
3399
|
+
| `loginId` | Login session that produced the event. Omni rejects any other value, which only reaches it if an adapter kept an old connection emitting after a re-login. |
|
|
3377
3400
|
| `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
|
|
3378
3401
|
| `event` | Typed `ProviderEvent` payload. |
|
|
3379
3402
|
|
|
3380
3403
|
#### Provider instants are read against a provider clock
|
|
3381
3404
|
|
|
3382
3405
|
Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
|
|
3383
|
-
`preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `
|
|
3406
|
+
`preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `wrapAllowance` where
|
|
3384
3407
|
one is stated.
|
|
3385
3408
|
Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
|
|
3386
3409
|
damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
|
|
@@ -3402,7 +3425,10 @@ silently lose a message, so while the connection is up Omni has seen everything
|
|
|
3402
3425
|
Loss has exactly one shape: the connection went away. The adapter reports `connecting` or `error`,
|
|
3403
3426
|
reconnects, and emits a `snapshot` event carrying complete state. That snapshot is the repair —
|
|
3404
3427
|
whatever was missed while the connection was down is in it, and Omni replaces its provider view
|
|
3405
|
-
rather than reasoning about what it did not receive.
|
|
3428
|
+
rather than reasoning about what it did not receive. A repair is an answer like any other: a
|
|
3429
|
+
platform that has not yet answered for this login after a reconnect — a state read served empty by
|
|
3430
|
+
a backend that does not know the session yet — yields no snapshot, and the adapter stays
|
|
3431
|
+
`connecting` holding what it holds. See **Snapshots establish state; events report transactions**.
|
|
3406
3432
|
|
|
3407
3433
|
A snapshot must account for **everything the adapter has emitted before it resolves**, not merely
|
|
3408
3434
|
everything emitted when it was requested. Omni discards events buffered during the read on that
|
|
@@ -3411,8 +3437,8 @@ Omni apply state the snapshot already superseded.
|
|
|
3411
3437
|
|
|
3412
3438
|
#### Liveness
|
|
3413
3439
|
|
|
3414
|
-
`
|
|
3415
|
-
`
|
|
3440
|
+
`transport-status` is the only signal Omni has that a transport died. An adapter must emit
|
|
3441
|
+
`transport-status` with `connecting` or `error` as soon as it loses its transport, rather than
|
|
3416
3442
|
leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
|
|
3417
3443
|
healthy provider from a dead one.
|
|
3418
3444
|
|
|
@@ -3432,9 +3458,9 @@ snapshot. It carries what the login's capabilities call for — a roster for a l
|
|
|
3432
3458
|
snapshot — and nothing they do not; a capability is withdrawn by a republished `authenticated`,
|
|
3433
3459
|
never by an omission from a snapshot.
|
|
3434
3460
|
|
|
3435
|
-
### `
|
|
3461
|
+
### `transport-status`
|
|
3436
3462
|
|
|
3437
|
-
Updates `
|
|
3463
|
+
Updates `TransportStatus`, and carries an optional `message` that may explain an error but must be
|
|
3438
3464
|
safe for the agent to see.
|
|
3439
3465
|
|
|
3440
3466
|
| Value | Contract |
|
|
@@ -3448,7 +3474,7 @@ died; the host knows how to run a login. `recovery` joins the two:
|
|
|
3448
3474
|
|
|
3449
3475
|
- **`reconnect`** — the login is good and this connection is not: a backend restart, a session the
|
|
3450
3476
|
platform no longer recognises. Omni calls `disconnect()` on the dead connection and then
|
|
3451
|
-
`connect()` again on the same login — same `
|
|
3477
|
+
`connect()` again on the same login — same `loginId` — and the fresh connect snapshot
|
|
3452
3478
|
re-establishes state exactly as a reconnect snapshot does. `connect()` is once per
|
|
3453
3479
|
`Connection`, not once per login.
|
|
3454
3480
|
- **`reauthenticate`** — the session under the login died: a token rejected, a remote logout. Omni
|
|
@@ -3460,7 +3486,7 @@ has to decide when to stop. Omni owns giving up: after however long it chooses t
|
|
|
3460
3486
|
call `disconnect()` and either `connect()` afresh or surface the failure — so neither side waits
|
|
3461
3487
|
for the other to blink.
|
|
3462
3488
|
|
|
3463
|
-
|
|
3489
|
+
**`transport` is about the transport, nothing else.** It does not say whether the agent is available,
|
|
3464
3490
|
whether they are on a break, or how much work they can take: capacity travels on `setCapacity`,
|
|
3465
3491
|
availability on `BreakState`. Nor does it carry authentication — a session that expired reports
|
|
3466
3492
|
`expired` on `AuthenticationState` and fails actions with `omni.not-authenticated`, while
|
|
@@ -3475,7 +3501,7 @@ come — see **Liveness**.
|
|
|
3475
3501
|
|
|
3476
3502
|
Replaces this provider's complete `break` object. Its `approval` uses the canonical
|
|
3477
3503
|
`not-requested`, `awaiting-decision`, `granted`, `starting-after-task` and
|
|
3478
|
-
`in-effect` states defined under Breaks; the event also carries the corresponding
|
|
3504
|
+
`in-effect` states defined under Breaks; the event also carries the corresponding may-ask state,
|
|
3479
3505
|
reasons, retry details, and any imposed break.
|
|
3480
3506
|
|
|
3481
3507
|
Each state is also held to the one before it. A commit's states, `starting-after-task` and
|
|
@@ -3508,20 +3534,27 @@ snapshots until it ends.
|
|
|
3508
3534
|
Replaces the current representation of one provider-local task. It is a full task value, not a
|
|
3509
3535
|
partial patch.
|
|
3510
3536
|
|
|
3511
|
-
### `task-media-
|
|
3537
|
+
### `task-media-started`
|
|
3512
3538
|
|
|
3513
3539
|
The provider's word that the task's audio should now attach. Omni calls `openMedia` on it — and on
|
|
3514
|
-
a task carried with `media: "
|
|
3540
|
+
a task carried with `media: "started"`, which is how a reconnect snapshot reattaches audio an
|
|
3515
3541
|
earlier event brought — and renders the call as live from that word, never from its own senses. It
|
|
3516
|
-
names a task whose work has begun, and it alternates with `task-media-ended`: media that
|
|
3517
|
-
|
|
3542
|
+
names a task whose work has begun, and it alternates with `task-media-ended`: media that never
|
|
3543
|
+
started cannot end, so a live call whose provider says nothing about its audio is a provider in
|
|
3518
3544
|
breach, not a state a desk fills in from its own devices.
|
|
3519
3545
|
|
|
3546
|
+
The event is the transition and the task's `media` field is the state. A `task-updated` re-states
|
|
3547
|
+
the media its task already holds — republishing `started` on a hold is a statement, not a second
|
|
3548
|
+
arrival — but it does not move it: an update that itself flips the field is refused
|
|
3549
|
+
(`stream.taskUpdated.media`), and the pairing at the moment audio arrives is the phase change
|
|
3550
|
+
without the field, then the event. Releasing `ended` is the one move an update may make, since
|
|
3551
|
+
wrapped audio has nothing left to end.
|
|
3552
|
+
|
|
3520
3553
|
### `task-media-ended`
|
|
3521
3554
|
|
|
3522
3555
|
Signals that a task's real-time media ended. For voice and similar channels, this starts the fixed
|
|
3523
|
-
completion timer. It does not remove the task, and it ends only audio that `task-media-
|
|
3524
|
-
a task carried with `media: "
|
|
3556
|
+
completion timer. It does not remove the task, and it ends only audio that `task-media-started` — or
|
|
3557
|
+
a task carried with `media: "started"` — attached.
|
|
3525
3558
|
|
|
3526
3559
|
### `task-ended`
|
|
3527
3560
|
|
|
@@ -3609,8 +3642,8 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3609
3642
|
| Function | Validates |
|
|
3610
3643
|
| --- | --- |
|
|
3611
3644
|
| `validateManifest(manifest)` | Identity, protocol-version interoperability, authentication methods, and idle-capability shapes. |
|
|
3612
|
-
| `validateTask(task, { channel })` | Identity, channel agreement, phase,
|
|
3613
|
-
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
|
|
3645
|
+
| `validateTask(task, { channel })` | Identity, channel agreement, phase, wrap allowance, capability shapes, custom controls, and browsers. |
|
|
3646
|
+
| `validateSnapshot(snapshot, manifest)` | Status, break state, break reasons, team roster, the stated `taskCount` reconciled against the tasks carried, and every task, contact, and activity, including idle-capability gating both ways: a contribution the manifest never declared is refused, and one it declares is required, `[]` included. |
|
|
3614
3647
|
| `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
|
|
3615
3648
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
3616
3649
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
@@ -3704,7 +3737,7 @@ cannot be established from TypeScript structure alone.
|
|
|
3704
3737
|
| `stillHost(report?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a voice adapter. |
|
|
3705
3738
|
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
|
|
3706
3739
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
|
|
3707
|
-
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-
|
|
3740
|
+
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
|
|
3708
3741
|
| `assertBreakParticipants(candidates, participants)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
3709
3742
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
3710
3743
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|