@xema/omni-protocol 0.1.43 → 0.1.45
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 +3 -3
- package/dist/index.d.ts +48 -32
- package/dist/index.js +6 -6
- package/dist/validation.js +41 -29
- package/guide.md +111 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,12 +99,12 @@ const task = {
|
|
|
99
99
|
|
|
100
100
|
**Switch on `command.action` with no `default`.** A `default` turns a future action into whatever
|
|
101
101
|
the default does, silently; naming every arm means dropping one fails the build — indirectly, as a
|
|
102
|
-
missing return — and keeps the narrowing that lets you read `
|
|
102
|
+
missing return — and keeps the narrowing that lets you read `destinationId` without a cast.
|
|
103
103
|
|
|
104
104
|
```ts
|
|
105
105
|
switch (command.action) {
|
|
106
|
-
case "cold": return coldTransfer(command.
|
|
107
|
-
case "warm": return warmTransfer(command.
|
|
106
|
+
case "cold": return coldTransfer(command.destinationId);
|
|
107
|
+
case "warm": return warmTransfer(command.destinationId);
|
|
108
108
|
case "complete": return completeWarmTransfer();
|
|
109
109
|
case "cancel": return cancelWarmTransfer();
|
|
110
110
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -409,16 +409,18 @@ export interface DispositionRules {
|
|
|
409
409
|
notes?: "required" | "optional" | "none";
|
|
410
410
|
codes?: DispositionCode[];
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* One item the queue configured for the agent to send a contact to: a button or a menu item. The
|
|
414
|
+
* protocol does not say what it does -- a queue, a menu, a line -- the provider executes it when a
|
|
415
|
+
* command names its `id`. Never a named agent: who takes a contact next is the queue's decision.
|
|
416
|
+
*/
|
|
412
417
|
export interface Destination {
|
|
413
418
|
id: string;
|
|
414
419
|
label: string;
|
|
415
|
-
address: string;
|
|
416
|
-
/** `agent` here is a routing target, not the person signed in. */
|
|
417
|
-
kind: "queue" | "agent" | "external";
|
|
418
420
|
}
|
|
421
|
+
/** The items a transfer or conference control offers. Nothing is typed; the agent picks one. */
|
|
419
422
|
export interface DestinationDirectory {
|
|
420
|
-
destinations
|
|
421
|
-
allowManualEntry: boolean;
|
|
423
|
+
destinations: Destination[];
|
|
422
424
|
}
|
|
423
425
|
export interface CustomCapability {
|
|
424
426
|
id: string;
|
|
@@ -451,12 +453,12 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
451
453
|
agentDisconnect?: Lockable<true>;
|
|
452
454
|
/** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
|
|
453
455
|
connectBack?: Lockable<true>;
|
|
454
|
-
coldTransfer?: Lockable<
|
|
456
|
+
coldTransfer?: Lockable<DestinationDirectory>;
|
|
455
457
|
/** Park the customer and call a destination first; then `complete` or `cancel`. */
|
|
456
|
-
warmTransfer?: Lockable<
|
|
458
|
+
warmTransfer?: Lockable<DestinationDirectory>;
|
|
457
459
|
/** Ask a lead to join this call, with a note. The lead's decision arrives on `Task.leadAssist`. */
|
|
458
460
|
leadAssist?: Lockable<true>;
|
|
459
|
-
conference?: Lockable<
|
|
461
|
+
conference?: Lockable<DestinationDirectory>;
|
|
460
462
|
recording?: Lockable<true>;
|
|
461
463
|
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
462
464
|
decline?: Lockable<true>;
|
|
@@ -512,10 +514,15 @@ export type TaskPhase =
|
|
|
512
514
|
"pending"
|
|
513
515
|
/** Accepted, and not yet started. */
|
|
514
516
|
| "confirmed"
|
|
515
|
-
/**
|
|
516
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Voice only: the customer's record is on the agent's screen and no call has gone out. The agent
|
|
519
|
+
* presses Call, or `previewEndsAt` arrives and `atDeadline` says what the system does instead.
|
|
520
|
+
*/
|
|
521
|
+
| "preview" | "in-progress" | "paused"
|
|
517
522
|
/** The work is over and the agent is finishing up. */
|
|
518
523
|
| "completing";
|
|
524
|
+
/** What the system does when a preview's deadline passes with no Call pressed. */
|
|
525
|
+
export type PreviewDeadline = "calls" | "expires";
|
|
519
526
|
/** Who ends the task: the agent issuing `complete`, or the provider deciding it is over. */
|
|
520
527
|
export type CompletionMode = "agent-command" | "provider-automatic";
|
|
521
528
|
export interface TaskAttributeBase {
|
|
@@ -556,10 +563,10 @@ export interface TaskHandlingStep {
|
|
|
556
563
|
at: IsoTimestamp;
|
|
557
564
|
/**
|
|
558
565
|
* On a step that dialled -- `transferred`, `conferenced`, `unanswered` -- the host's identity for
|
|
559
|
-
* that dial when a host placed it, and the
|
|
566
|
+
* that dial when a host placed it, and the directory item it went to. Neither belongs on any other step.
|
|
560
567
|
*/
|
|
561
568
|
dialId?: DialId;
|
|
562
|
-
|
|
569
|
+
destinationId?: string;
|
|
563
570
|
/**
|
|
564
571
|
* Reported, never derived. An entry may be written while its leg is still running, so there is
|
|
565
572
|
* no end to subtract from -- and omitted is the honest report of that, where nought would
|
|
@@ -611,7 +618,7 @@ export type OnCall = {
|
|
|
611
618
|
userId: UserId;
|
|
612
619
|
} | {
|
|
613
620
|
role: "consulted" | "conferenced";
|
|
614
|
-
|
|
621
|
+
destinationId: string;
|
|
615
622
|
stage: OnCallStage;
|
|
616
623
|
dialId?: DialId;
|
|
617
624
|
label?: string;
|
|
@@ -710,6 +717,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
710
717
|
* (`autoAcceptTasks: true`), forbidden when it said not, and absent past `pending`.
|
|
711
718
|
*/
|
|
712
719
|
acceptance?: AcceptanceMode;
|
|
720
|
+
/**
|
|
721
|
+
* In `preview` only, and together: when the system stops waiting for the agent to press Call,
|
|
722
|
+
* and what it does then -- `calls` places the call itself, `expires` takes the record back and
|
|
723
|
+
* the task ends `expired`. Absent, the agent has as long as they need.
|
|
724
|
+
*/
|
|
725
|
+
previewEndsAt?: IsoTimestamp;
|
|
726
|
+
atDeadline?: PreviewDeadline;
|
|
713
727
|
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
714
728
|
reference?: string;
|
|
715
729
|
attributes?: TaskAttribute[];
|
|
@@ -739,7 +753,7 @@ export type TaskOutcome = {
|
|
|
739
753
|
by: "agent" | "provider";
|
|
740
754
|
} | {
|
|
741
755
|
type: "transferred";
|
|
742
|
-
|
|
756
|
+
destinationId?: string;
|
|
743
757
|
} | {
|
|
744
758
|
type: "cancelled";
|
|
745
759
|
reason?: string;
|
|
@@ -747,7 +761,7 @@ export type TaskOutcome = {
|
|
|
747
761
|
/** Only the phases in which somebody is still being waited on can expire. */
|
|
748
762
|
| {
|
|
749
763
|
type: "expired";
|
|
750
|
-
phase: "pending" | "confirmed" | "
|
|
764
|
+
phase: "pending" | "confirmed" | "preview";
|
|
751
765
|
}
|
|
752
766
|
/** This agent left a call that continues without them: a lead who joined and dropped. */
|
|
753
767
|
| {
|
|
@@ -757,7 +771,7 @@ export type TaskOutcome = {
|
|
|
757
771
|
failure: ProtocolFailure;
|
|
758
772
|
};
|
|
759
773
|
export declare const TASK_COMMAND_NAMES: {
|
|
760
|
-
readonly voice: readonly ["answer", "decline", "
|
|
774
|
+
readonly voice: readonly ["answer", "decline", "call", "mute", "hold", "resume", "disconnect", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
761
775
|
readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
|
|
762
776
|
readonly email: readonly ["accept", "decline", "complete"];
|
|
763
777
|
};
|
|
@@ -770,8 +784,11 @@ export type VoiceTaskCommand = {
|
|
|
770
784
|
type: "answer";
|
|
771
785
|
} | {
|
|
772
786
|
type: "decline";
|
|
773
|
-
}
|
|
774
|
-
|
|
787
|
+
}
|
|
788
|
+
/** In `preview`: place the call to the party whose record the agent has read. A dial, gated by the phase alone. */
|
|
789
|
+
| {
|
|
790
|
+
type: "call";
|
|
791
|
+
dialId: DialId;
|
|
775
792
|
} | {
|
|
776
793
|
type: "mute";
|
|
777
794
|
muted: boolean;
|
|
@@ -787,19 +804,19 @@ export type VoiceTaskCommand = {
|
|
|
787
804
|
type: "connect-back";
|
|
788
805
|
dialId: DialId;
|
|
789
806
|
}
|
|
790
|
-
/** Cold: hand the customer to `
|
|
807
|
+
/** Cold: hand the customer to the directory item `destinationId` with nobody spoken to first. Gated by `coldTransfer`. */
|
|
791
808
|
| {
|
|
792
809
|
type: "transfer";
|
|
793
810
|
action: "cold";
|
|
794
811
|
dialId: DialId;
|
|
795
|
-
|
|
812
|
+
destinationId: string;
|
|
796
813
|
}
|
|
797
|
-
/** Warm: park the customer and call `
|
|
814
|
+
/** Warm: park the customer and call the directory item `destinationId` first. Gated by `warmTransfer`. */
|
|
798
815
|
| {
|
|
799
816
|
type: "transfer";
|
|
800
817
|
action: "warm";
|
|
801
818
|
dialId: DialId;
|
|
802
|
-
|
|
819
|
+
destinationId: string;
|
|
803
820
|
}
|
|
804
821
|
/** Hand the customer to the consulted destination and leave. Needs a `consulted` entry on `Task.onCall`. */
|
|
805
822
|
| {
|
|
@@ -832,18 +849,18 @@ export type VoiceTaskCommand = {
|
|
|
832
849
|
type: "lead-assist";
|
|
833
850
|
action: "leave";
|
|
834
851
|
}
|
|
835
|
-
/** Dial `
|
|
852
|
+
/** Dial the directory item `destinationId` into the call. Gated by `conference`. */
|
|
836
853
|
| {
|
|
837
854
|
type: "conference";
|
|
838
855
|
action: "add";
|
|
839
856
|
dialId: DialId;
|
|
840
|
-
|
|
857
|
+
destinationId: string;
|
|
841
858
|
}
|
|
842
|
-
/** Drop the `conferenced` entry on `Task.onCall` with this
|
|
859
|
+
/** Drop the `conferenced` entry on `Task.onCall` with this `destinationId`; while it still rings, this calls the dial off. */
|
|
843
860
|
| {
|
|
844
861
|
type: "conference";
|
|
845
862
|
action: "remove";
|
|
846
|
-
|
|
863
|
+
destinationId: string;
|
|
847
864
|
} | {
|
|
848
865
|
type: "recording";
|
|
849
866
|
action: "start" | "pause" | "resume" | "stop";
|
|
@@ -1255,7 +1272,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1255
1272
|
type: "task-offered";
|
|
1256
1273
|
task: Task<C>;
|
|
1257
1274
|
allocationExpiresAt?: IsoTimestamp;
|
|
1258
|
-
preparationEndsAt?: IsoTimestamp;
|
|
1259
1275
|
} | {
|
|
1260
1276
|
type: "task-updated";
|
|
1261
1277
|
task: Task<C>;
|
|
@@ -1282,7 +1298,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1282
1298
|
dialId: DialId;
|
|
1283
1299
|
outcome: DialOutcome;
|
|
1284
1300
|
taskId?: TaskId;
|
|
1285
|
-
|
|
1301
|
+
destinationId?: string;
|
|
1286
1302
|
reason?: string;
|
|
1287
1303
|
} | {
|
|
1288
1304
|
type: "announcement";
|
|
@@ -1372,7 +1388,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1372
1388
|
readonly voice: {
|
|
1373
1389
|
readonly pending: "Offered";
|
|
1374
1390
|
readonly confirmed: "Accepted";
|
|
1375
|
-
readonly
|
|
1391
|
+
readonly preview: "Preview";
|
|
1376
1392
|
readonly "in-progress": "On Call";
|
|
1377
1393
|
readonly paused: "On Hold";
|
|
1378
1394
|
readonly completing: "After Call Work";
|
|
@@ -1380,7 +1396,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1380
1396
|
readonly chat: {
|
|
1381
1397
|
readonly pending: "Incoming Chat";
|
|
1382
1398
|
readonly confirmed: "Accepted";
|
|
1383
|
-
readonly
|
|
1399
|
+
readonly preview: "Preview";
|
|
1384
1400
|
readonly "in-progress": "In Chat";
|
|
1385
1401
|
readonly paused: "Paused";
|
|
1386
1402
|
readonly completing: "Wrap-up";
|
|
@@ -1388,7 +1404,7 @@ export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
|
1388
1404
|
readonly email: {
|
|
1389
1405
|
readonly pending: "Assigned";
|
|
1390
1406
|
readonly confirmed: "Accepted";
|
|
1391
|
-
readonly
|
|
1407
|
+
readonly preview: "Preview";
|
|
1392
1408
|
readonly "in-progress": "Working";
|
|
1393
1409
|
readonly paused: "Paused";
|
|
1394
1410
|
readonly completing: "Completing";
|
|
@@ -1429,7 +1445,7 @@ export declare const taskKey: (providerId: string, taskId: TaskId) => string;
|
|
|
1429
1445
|
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
1430
1446
|
/** Every handling step somebody takes part in. `queued` is the one nobody does. */
|
|
1431
1447
|
export declare const HANDLING_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
1432
|
-
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `
|
|
1448
|
+
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
|
1433
1449
|
export declare const HANDLING_STEPS_THAT_DIAL: readonly ["transferred", "conferenced", "unanswered"];
|
|
1434
1450
|
export declare const handlingStepDials: (step: HandlingStep) => boolean;
|
|
1435
1451
|
/** Whether an absent `by` means "could not attribute" rather than "nobody was involved". */
|
package/dist/index.js
CHANGED
|
@@ -71,14 +71,14 @@ export function effectiveLevels(declared) {
|
|
|
71
71
|
// Task commands.
|
|
72
72
|
// ---------------------------------------------------------------------------
|
|
73
73
|
export const TASK_COMMAND_NAMES = {
|
|
74
|
-
voice: ["answer", "decline", "
|
|
74
|
+
voice: ["answer", "decline", "call", "mute", "hold", "resume", "disconnect",
|
|
75
75
|
"connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
|
|
76
76
|
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
77
77
|
email: ["accept", "decline", "complete"],
|
|
78
78
|
};
|
|
79
79
|
/** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
|
|
80
80
|
export function commandDialId(command) {
|
|
81
|
-
if (command.type === "connect-back")
|
|
81
|
+
if (command.type === "connect-back" || command.type === "call")
|
|
82
82
|
return command.dialId;
|
|
83
83
|
if (command.type === "transfer" && (command.action === "cold" || command.action === "warm"))
|
|
84
84
|
return command.dialId;
|
|
@@ -117,15 +117,15 @@ export function defineAdapter(adapter) {
|
|
|
117
117
|
// ---------------------------------------------------------------------------
|
|
118
118
|
export const DEFAULT_TASK_PHASE_LABELS = {
|
|
119
119
|
voice: {
|
|
120
|
-
pending: "Offered", confirmed: "Accepted",
|
|
120
|
+
pending: "Offered", confirmed: "Accepted", preview: "Preview",
|
|
121
121
|
"in-progress": "On Call", paused: "On Hold", completing: "After Call Work",
|
|
122
122
|
},
|
|
123
123
|
chat: {
|
|
124
|
-
pending: "Incoming Chat", confirmed: "Accepted",
|
|
124
|
+
pending: "Incoming Chat", confirmed: "Accepted", preview: "Preview",
|
|
125
125
|
"in-progress": "In Chat", paused: "Paused", completing: "Wrap-up",
|
|
126
126
|
},
|
|
127
127
|
email: {
|
|
128
|
-
pending: "Assigned", confirmed: "Accepted",
|
|
128
|
+
pending: "Assigned", confirmed: "Accepted", preview: "Preview",
|
|
129
129
|
"in-progress": "Working", paused: "Paused", completing: "Completing",
|
|
130
130
|
},
|
|
131
131
|
};
|
|
@@ -157,7 +157,7 @@ export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)
|
|
|
157
157
|
export const HANDLING_STEPS_WITH_A_PERSON = [
|
|
158
158
|
"offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
|
|
159
159
|
];
|
|
160
|
-
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `
|
|
160
|
+
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
|
161
161
|
export const HANDLING_STEPS_THAT_DIAL = ["transferred", "conferenced", "unanswered"];
|
|
162
162
|
export const handlingStepDials = (step) => HANDLING_STEPS_THAT_DIAL.includes(step);
|
|
163
163
|
// Pinned both ways: a step added to `HandlingStep` has to be placed here, and a step listed here
|
package/dist/validation.js
CHANGED
|
@@ -38,7 +38,7 @@ const membersOf = (members) => Object.keys(members);
|
|
|
38
38
|
const CHANNELS = membersOf({ voice: true, chat: true, email: true });
|
|
39
39
|
const TRANSPORT_RECOVERIES = membersOf({ reconnect: true, reauthenticate: true });
|
|
40
40
|
const TASK_PHASES = membersOf({
|
|
41
|
-
pending: true, confirmed: true,
|
|
41
|
+
pending: true, confirmed: true, preview: true, "in-progress": true, paused: true, completing: true,
|
|
42
42
|
});
|
|
43
43
|
const COMPLETION_MODES = membersOf({ "agent-command": true, "provider-automatic": true });
|
|
44
44
|
const ACCEPTANCE_MODES = membersOf({
|
|
@@ -58,7 +58,6 @@ const TEAM_AVAILABILITIES = membersOf({
|
|
|
58
58
|
const HANDLING_STEPS = membersOf({
|
|
59
59
|
queued: true, offered: true, answered: true, held: true, muted: true, transferred: true, conferenced: true, unanswered: true,
|
|
60
60
|
});
|
|
61
|
-
const DESTINATION_KINDS = membersOf({ queue: true, agent: true, external: true });
|
|
62
61
|
const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
|
|
63
62
|
const CUSTOM_UI_PLACEMENTS = membersOf({ primary: true, secondary: true, overflow: true });
|
|
64
63
|
const NOTES_RULES = membersOf({ required: true, optional: true, none: true });
|
|
@@ -82,13 +81,14 @@ const MEMBER_BREAKS = membersOf({
|
|
|
82
81
|
"awaiting-decision": true, granted: true, "starting-after-task": true,
|
|
83
82
|
});
|
|
84
83
|
const OFFERABLE_PHASES = membersOf({
|
|
85
|
-
pending: true, confirmed: true,
|
|
84
|
+
pending: true, confirmed: true, preview: true,
|
|
86
85
|
});
|
|
86
|
+
const PREVIEW_DEADLINES = membersOf({ calls: true, expires: true });
|
|
87
87
|
const TEAM_CAPABILITIES = membersOf({ breakControl: true, leadAssistControl: true, policyControl: true, monitorControl: true });
|
|
88
88
|
const MONITOR_MODES = membersOf({ monitor: true, whisper: true, barge: true });
|
|
89
89
|
const COMPLETED_BY = membersOf({ agent: true, provider: true });
|
|
90
90
|
const EXPIRABLE_PHASES = membersOf({
|
|
91
|
-
pending: true, confirmed: true,
|
|
91
|
+
pending: true, confirmed: true, preview: true,
|
|
92
92
|
});
|
|
93
93
|
const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
|
|
94
94
|
/** The capabilities each channel arm of `TaskCapabilities` declares, keyed off the type itself. */
|
|
@@ -417,23 +417,18 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
417
417
|
// Task.
|
|
418
418
|
// ---------------------------------------------------------------------------
|
|
419
419
|
function validateDestinationDirectory(value, path, into) {
|
|
420
|
-
if (value === true)
|
|
421
|
-
return;
|
|
422
420
|
if (!isPlainObject(value)) {
|
|
423
|
-
into.add("task.destinations.shape", path, "
|
|
421
|
+
into.add("task.destinations.shape", path, "a transfer or conference control carries the directory it offers: { destinations: [...] }");
|
|
424
422
|
return;
|
|
425
423
|
}
|
|
426
|
-
into.require(typeof value.allowManualEntry === "boolean", "task.destinations.allowManualEntry", `${path}.allowManualEntry`, "a directory must say whether manual entry is allowed");
|
|
427
|
-
// A directory with nothing in it and no typing is a control with nothing to offer.
|
|
428
|
-
if (value.allowManualEntry === false && !(Array.isArray(value.destinations) && value.destinations.length > 0)) {
|
|
429
|
-
into.add("task.destinations.offer", path, "a directory with no destinations must allow manual entry, or the control has nothing to offer");
|
|
430
|
-
}
|
|
431
|
-
if (value.destinations === undefined)
|
|
432
|
-
return;
|
|
433
424
|
if (!Array.isArray(value.destinations)) {
|
|
434
|
-
into.add("task.destinations.list", `${path}.destinations`, "destinations must be an array
|
|
425
|
+
into.add("task.destinations.list", `${path}.destinations`, "destinations must be an array");
|
|
435
426
|
return;
|
|
436
427
|
}
|
|
428
|
+
// Nothing is typed, so a directory with nothing in it is a control with nothing to offer.
|
|
429
|
+
if (value.destinations.length === 0) {
|
|
430
|
+
into.add("task.destinations.offer", path, "a directory offers at least one item; with none, omit the capability");
|
|
431
|
+
}
|
|
437
432
|
const seen = new Set();
|
|
438
433
|
value.destinations.forEach((destination, index) => {
|
|
439
434
|
const at = `${path}.destinations[${index}]`;
|
|
@@ -447,8 +442,6 @@ function validateDestinationDirectory(value, path, into) {
|
|
|
447
442
|
seen.add(destination.id);
|
|
448
443
|
}
|
|
449
444
|
into.filled(destination.label, "task.destination.label", `${at}.label`, "a destination needs a label");
|
|
450
|
-
into.filled(destination.address, "task.destination.address", `${at}.address`, "a destination needs an address");
|
|
451
|
-
into.oneOf(destination.kind, DESTINATION_KINDS, "task.destination.kind", `${at}.kind`);
|
|
452
445
|
});
|
|
453
446
|
}
|
|
454
447
|
function validateDispositions(value, path, into) {
|
|
@@ -723,7 +716,7 @@ function validateHandlingHistory(value, path, into) {
|
|
|
723
716
|
}
|
|
724
717
|
// A step that dialled says where to and, when a host placed it, which dial; no other step dialled.
|
|
725
718
|
const dialled = typeof entry.step === "string" && HANDLING_STEPS_THAT_DIAL.includes(entry.step);
|
|
726
|
-
for (const field of ["dialId", "
|
|
719
|
+
for (const field of ["dialId", "destinationId"]) {
|
|
727
720
|
if (entry[field] === undefined)
|
|
728
721
|
continue;
|
|
729
722
|
if (into.filled(entry[field], `task.handlingHistory.${field}`, `${at}.${field}`, `${field} must not be empty when present`)) {
|
|
@@ -779,7 +772,7 @@ function validateOnCall(value, channel, path, into) {
|
|
|
779
772
|
into.require(entry.userId === undefined, "task.onCall.userId.unexpected", `${at}.userId`, `a ${role} is not named by a user id`);
|
|
780
773
|
}
|
|
781
774
|
if (role === "consulted" || role === "conferenced") {
|
|
782
|
-
into.filled(entry.
|
|
775
|
+
into.filled(entry.destinationId, "task.onCall.destinationId", `${at}.destinationId`, `a ${role} entry names the directory item that was dialled`);
|
|
783
776
|
// Stated, so a snapshot says who is present without anyone having seen the dial's outcome.
|
|
784
777
|
if (into.oneOf(entry.stage, ON_CALL_STAGES, "task.onCall.stage", `${at}.stage`)) {
|
|
785
778
|
into.require(!(entry.stage === "ringing" && entry.held === true), "task.onCall.held.ringing", `${at}.held`, "nobody ringing is held; held belongs to somebody who has joined");
|
|
@@ -790,7 +783,7 @@ function validateOnCall(value, channel, path, into) {
|
|
|
790
783
|
into.filled(entry.label, "task.onCall.label", `${at}.label`, "a label must not be empty when present");
|
|
791
784
|
}
|
|
792
785
|
else {
|
|
793
|
-
for (const field of ["
|
|
786
|
+
for (const field of ["destinationId", "dialId", "label", "stage"]) {
|
|
794
787
|
into.require(entry[field] === undefined, `task.onCall.${field}.unexpected`, `${at}.${field}`, `a ${role} was dialled from nowhere; ${field} belongs on consulted or conferenced`);
|
|
795
788
|
}
|
|
796
789
|
}
|
|
@@ -864,7 +857,28 @@ function validateTaskInto(task, context, path, into) {
|
|
|
864
857
|
into.require(isTaskId(task.id), "task.id", `${path}.id`, "a task needs a non-empty id");
|
|
865
858
|
into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
|
|
866
859
|
into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
|
|
867
|
-
into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`)
|
|
860
|
+
if (into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`) && task.phase === "preview") {
|
|
861
|
+
// A preview is a record waiting for the agent to press Call: only voice has a call to place,
|
|
862
|
+
// and pressing Call is a dial, so the manifest has to have said how a dial ends.
|
|
863
|
+
into.require(context.channel === "voice", "task.phase.channel", `${path}.phase`, `a ${context.channel} task has no call to preview`);
|
|
864
|
+
into.require(context.dialOutcomesDeclared !== false, "task.preview.dialOutcomes.required", `${path}.phase`, "a preview ends in the agent pressing Call, which dials, and the manifest declares no dialOutcomes to say how a dial ends");
|
|
865
|
+
}
|
|
866
|
+
// The deadline and what happens at it travel together, and only while the record is being previewed.
|
|
867
|
+
if (task.previewEndsAt !== undefined || task.atDeadline !== undefined) {
|
|
868
|
+
into.require(task.phase === "preview", "task.preview.deadline.unexpected", `${path}.previewEndsAt`, "previewEndsAt and atDeadline belong to a task in preview; past it there is nothing to wait for");
|
|
869
|
+
if (task.previewEndsAt === undefined) {
|
|
870
|
+
into.add("task.preview.previewEndsAt.required", `${path}.previewEndsAt`, "atDeadline says what happens at a deadline, so there has to be one");
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
into.timestamp(task.previewEndsAt, "task.preview.previewEndsAt", `${path}.previewEndsAt`);
|
|
874
|
+
}
|
|
875
|
+
if (task.atDeadline === undefined) {
|
|
876
|
+
into.add("task.preview.atDeadline.required", `${path}.atDeadline`, "a preview with a deadline says what the system does at it: calls, or expires -- an agent counting down has to know which");
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
into.oneOf(task.atDeadline, PREVIEW_DEADLINES, "task.preview.atDeadline", `${path}.atDeadline`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
868
882
|
// Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
|
|
869
883
|
// travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
|
|
870
884
|
if (task.acceptance !== undefined) {
|
|
@@ -1275,8 +1289,8 @@ function validateTaskOutcome(value, path, into) {
|
|
|
1275
1289
|
into.oneOf(value.by, COMPLETED_BY, "event.taskEnded.outcome.completed", `${path}.by`);
|
|
1276
1290
|
break;
|
|
1277
1291
|
case "transferred":
|
|
1278
|
-
if (value.
|
|
1279
|
-
into.filled(value.
|
|
1292
|
+
if (value.destinationId !== undefined) {
|
|
1293
|
+
into.filled(value.destinationId, "event.taskEnded.outcome.transferred", `${path}.destinationId`, "a destinationId must not be empty when present");
|
|
1280
1294
|
}
|
|
1281
1295
|
break;
|
|
1282
1296
|
case "cancelled":
|
|
@@ -1386,10 +1400,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1386
1400
|
if (isPlainObject(event.task) && typeof event.task.phase === "string") {
|
|
1387
1401
|
into.require(OFFERABLE_PHASES.includes(event.task.phase), "event.taskOffered.phase", `${at}.task.phase`, `task-offered introduces a task as ${OFFERABLE_PHASES.join(", ")}, never as ${event.task.phase}`);
|
|
1388
1402
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
into.timestamp(event[field], `event.taskOffered.${field}`, `${at}.${field}`);
|
|
1392
|
-
}
|
|
1403
|
+
if (event.allocationExpiresAt !== undefined)
|
|
1404
|
+
into.timestamp(event.allocationExpiresAt, "event.taskOffered.allocationExpiresAt", `${at}.allocationExpiresAt`);
|
|
1393
1405
|
break;
|
|
1394
1406
|
case "task-updated":
|
|
1395
1407
|
validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks, dialOutcomesDeclared: manifestDials(manifest) }, `${at}.task`, into);
|
|
@@ -1413,8 +1425,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1413
1425
|
}
|
|
1414
1426
|
if (event.taskId !== undefined)
|
|
1415
1427
|
into.require(isTaskId(event.taskId), "event.dialOutcome.taskId", `${at}.taskId`, "taskId must be a task id when present");
|
|
1416
|
-
if (event.
|
|
1417
|
-
into.filled(event.
|
|
1428
|
+
if (event.destinationId !== undefined)
|
|
1429
|
+
into.filled(event.destinationId, "event.dialOutcome.destinationId", `${at}.destinationId`, "a destinationId must not be empty when present");
|
|
1418
1430
|
if (event.reason !== undefined)
|
|
1419
1431
|
into.filled(event.reason, "event.dialOutcome.reason", `${at}.reason`, "a reason must not be empty when present");
|
|
1420
1432
|
break;
|
package/guide.md
CHANGED
|
@@ -20,7 +20,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
20
20
|
| **Host** | Omni, named that way where the contrast with a provider is the point — *the host carries the audio*, *host-side input*. |
|
|
21
21
|
| **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
|
|
22
22
|
| **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
|
|
23
|
-
| **Agent** | The person signed in and taking work.
|
|
23
|
+
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; a transfer never names one, since a contact goes back to a queue. |
|
|
24
24
|
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamRoster` to them and to nobody else: **the login is the permission**. |
|
|
25
25
|
| **Provisioning** | Omni-side policy about this agent, configured outside the protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and provisioning disagree, the stricter wins. |
|
|
26
26
|
| **Task** | One unit of assigned work — a call, a chat, a mail. |
|
|
@@ -45,7 +45,7 @@ transport, a task, a break and a call each have a word of their own:
|
|
|
45
45
|
|
|
46
46
|
| Word | Belongs to | Values |
|
|
47
47
|
| --- | --- | --- |
|
|
48
|
-
| `phase` | A task | `pending`, `confirmed`, `
|
|
48
|
+
| `phase` | A task | `pending`, `confirmed`, `preview`, `in-progress`, `paused`, `completing` |
|
|
49
49
|
| `media` | A task's audio | `started`, `ended` |
|
|
50
50
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
51
51
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
@@ -447,13 +447,10 @@ type DispositionRules = {
|
|
|
447
447
|
type Destination = {
|
|
448
448
|
id: string;
|
|
449
449
|
label: string;
|
|
450
|
-
address: string;
|
|
451
|
-
kind: "queue" | "agent" | "external";
|
|
452
450
|
};
|
|
453
451
|
|
|
454
452
|
type DestinationDirectory = {
|
|
455
|
-
destinations
|
|
456
|
-
allowManualEntry: boolean;
|
|
453
|
+
destinations: Destination[];
|
|
457
454
|
};
|
|
458
455
|
|
|
459
456
|
type CustomCapability = {
|
|
@@ -481,10 +478,10 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
481
478
|
hold?: Lockable<true>;
|
|
482
479
|
agentDisconnect?: Lockable<true>;
|
|
483
480
|
connectBack?: Lockable<true>;
|
|
484
|
-
coldTransfer?: Lockable<
|
|
485
|
-
warmTransfer?: Lockable<
|
|
481
|
+
coldTransfer?: Lockable<DestinationDirectory>;
|
|
482
|
+
warmTransfer?: Lockable<DestinationDirectory>;
|
|
486
483
|
leadAssist?: Lockable<true>;
|
|
487
|
-
conference?: Lockable<
|
|
484
|
+
conference?: Lockable<DestinationDirectory>;
|
|
488
485
|
recording?: Lockable<true>;
|
|
489
486
|
}
|
|
490
487
|
: C extends "chat"
|
|
@@ -542,7 +539,7 @@ default — see **Choosing an isolation scheme**.
|
|
|
542
539
|
type TaskPhase =
|
|
543
540
|
| "pending"
|
|
544
541
|
| "confirmed"
|
|
545
|
-
| "
|
|
542
|
+
| "preview"
|
|
546
543
|
| "in-progress"
|
|
547
544
|
| "paused"
|
|
548
545
|
| "completing";
|
|
@@ -582,7 +579,7 @@ type TaskHandlingStep = {
|
|
|
582
579
|
step: HandlingStep;
|
|
583
580
|
at: IsoTimestamp;
|
|
584
581
|
dialId?: DialId;
|
|
585
|
-
|
|
582
|
+
destinationId?: string;
|
|
586
583
|
seconds?: DurationSeconds;
|
|
587
584
|
by?: UserId;
|
|
588
585
|
};
|
|
@@ -598,7 +595,7 @@ type OnCallStage = "ringing" | "joined";
|
|
|
598
595
|
type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
599
596
|
| { role: "party" }
|
|
600
597
|
| { role: "agent"; userId: UserId }
|
|
601
|
-
| { role: "consulted" | "conferenced";
|
|
598
|
+
| { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
|
|
602
599
|
);
|
|
603
600
|
|
|
604
601
|
type TaskLeadAssist = {
|
|
@@ -647,6 +644,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
647
644
|
party?: Contact;
|
|
648
645
|
phase: TaskPhase;
|
|
649
646
|
acceptance?: AcceptanceMode;
|
|
647
|
+
previewEndsAt?: IsoTimestamp;
|
|
648
|
+
atDeadline?: PreviewDeadline;
|
|
650
649
|
reference?: string;
|
|
651
650
|
attributes?: TaskAttribute[];
|
|
652
651
|
handlingHistory?: TaskHandlingHistory;
|
|
@@ -656,6 +655,8 @@ type Task<C extends Channel = Channel> = {
|
|
|
656
655
|
: { onCall?: never; leadAssist?: never; assisting?: never; monitoring?: never; media?: never }
|
|
657
656
|
);
|
|
658
657
|
|
|
658
|
+
type PreviewDeadline = "calls" | "expires";
|
|
659
|
+
|
|
659
660
|
type AcceptanceMode =
|
|
660
661
|
| "no-preference"
|
|
661
662
|
| "consent"
|
|
@@ -663,9 +664,9 @@ type AcceptanceMode =
|
|
|
663
664
|
|
|
664
665
|
type TaskOutcome =
|
|
665
666
|
| { type: "completed"; by: "agent" | "provider" }
|
|
666
|
-
| { type: "transferred";
|
|
667
|
+
| { type: "transferred"; destinationId?: string }
|
|
667
668
|
| { type: "cancelled"; reason?: string }
|
|
668
|
-
| { type: "expired"; phase: "pending" | "confirmed" | "
|
|
669
|
+
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
669
670
|
| { type: "left" }
|
|
670
671
|
| { type: "failed"; failure: ProtocolFailure };
|
|
671
672
|
```
|
|
@@ -677,7 +678,7 @@ const TASK_COMMAND_NAMES = {
|
|
|
677
678
|
voice: [
|
|
678
679
|
"answer",
|
|
679
680
|
"decline",
|
|
680
|
-
"
|
|
681
|
+
"call",
|
|
681
682
|
"mute",
|
|
682
683
|
"hold",
|
|
683
684
|
"resume",
|
|
@@ -701,22 +702,22 @@ type DispositionPayload = { disposition?: string; notes?: string };
|
|
|
701
702
|
type VoiceTaskCommand =
|
|
702
703
|
| { type: "answer" }
|
|
703
704
|
| { type: "decline" }
|
|
704
|
-
| { type: "
|
|
705
|
+
| { type: "call"; dialId: DialId }
|
|
705
706
|
| { type: "mute"; muted: boolean }
|
|
706
707
|
| { type: "hold" }
|
|
707
708
|
| { type: "resume" }
|
|
708
709
|
| { type: "disconnect" }
|
|
709
710
|
| { type: "connect-back"; dialId: DialId }
|
|
710
|
-
| { type: "transfer"; action: "cold"; dialId: DialId;
|
|
711
|
-
| { type: "transfer"; action: "warm"; dialId: DialId;
|
|
711
|
+
| { type: "transfer"; action: "cold"; dialId: DialId; destinationId: string }
|
|
712
|
+
| { type: "transfer"; action: "warm"; dialId: DialId; destinationId: string }
|
|
712
713
|
| { type: "transfer"; action: "complete" }
|
|
713
714
|
| { type: "transfer"; action: "cancel" }
|
|
714
715
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
715
716
|
| { type: "lead-assist"; action: "cancel" }
|
|
716
717
|
| { type: "lead-assist"; action: "take-over" }
|
|
717
718
|
| { type: "lead-assist"; action: "leave" }
|
|
718
|
-
| { type: "conference"; action: "add"; dialId: DialId;
|
|
719
|
-
| { type: "conference"; action: "remove";
|
|
719
|
+
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
720
|
+
| { type: "conference"; action: "remove"; destinationId: string }
|
|
720
721
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
721
722
|
| ({ type: "complete" } & DispositionPayload);
|
|
722
723
|
|
|
@@ -997,13 +998,12 @@ type ProviderEvent =
|
|
|
997
998
|
type: "task-offered";
|
|
998
999
|
task: Task;
|
|
999
1000
|
allocationExpiresAt?: IsoTimestamp;
|
|
1000
|
-
preparationEndsAt?: IsoTimestamp;
|
|
1001
1001
|
}
|
|
1002
1002
|
| { type: "task-updated"; task: Task }
|
|
1003
1003
|
| { type: "task-media-started"; taskId: TaskId }
|
|
1004
1004
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
1005
1005
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
1006
|
-
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId;
|
|
1006
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destinationId?: string; reason?: string }
|
|
1007
1007
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1008
1008
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1009
1009
|
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
@@ -1163,7 +1163,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1163
1163
|
voice: {
|
|
1164
1164
|
pending: "Offered",
|
|
1165
1165
|
confirmed: "Accepted",
|
|
1166
|
-
|
|
1166
|
+
preview: "Preview",
|
|
1167
1167
|
"in-progress": "On Call",
|
|
1168
1168
|
paused: "On Hold",
|
|
1169
1169
|
completing: "After Call Work",
|
|
@@ -1171,7 +1171,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1171
1171
|
chat: {
|
|
1172
1172
|
pending: "Incoming Chat",
|
|
1173
1173
|
confirmed: "Accepted",
|
|
1174
|
-
|
|
1174
|
+
preview: "Preview",
|
|
1175
1175
|
"in-progress": "In Chat",
|
|
1176
1176
|
paused: "Paused",
|
|
1177
1177
|
completing: "Wrap-up",
|
|
@@ -1179,7 +1179,7 @@ const DEFAULT_TASK_PHASE_LABELS = {
|
|
|
1179
1179
|
email: {
|
|
1180
1180
|
pending: "Assigned",
|
|
1181
1181
|
confirmed: "Accepted",
|
|
1182
|
-
|
|
1182
|
+
preview: "Preview",
|
|
1183
1183
|
"in-progress": "Working",
|
|
1184
1184
|
paused: "Paused",
|
|
1185
1185
|
completing: "Completing",
|
|
@@ -1283,7 +1283,7 @@ only where the corresponding capability is declared; an absent capability means
|
|
|
1283
1283
|
there is no separate permission flag.
|
|
1284
1284
|
|
|
1285
1285
|
The commands every task has are authorized by other fields the provider declared — a task that was
|
|
1286
|
-
offered can be accepted, one in `
|
|
1286
|
+
offered can be accepted, one in `preview` can be called, one whose `completionMode` is
|
|
1287
1287
|
`agent-command` can be completed. Nothing is issuable that the provider did not publish; only
|
|
1288
1288
|
which field says so varies. See **Which commands need a capability**.
|
|
1289
1289
|
|
|
@@ -2092,7 +2092,6 @@ const allocation = {
|
|
|
2092
2092
|
type: "task-offered",
|
|
2093
2093
|
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2094
2094
|
allocationExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2095
|
-
preparationEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2096
2095
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2097
2096
|
```
|
|
2098
2097
|
|
|
@@ -2114,10 +2113,8 @@ down and stops offering **Accept** once it passes. **Omit it unless the provider
|
|
|
2114
2113
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
2115
2114
|
to end, and a computed value would have Omni withdraw **Accept** from a task still pending.
|
|
2116
2115
|
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
before the agent must start the contact. Reaching the timestamp does not imply a transition; the
|
|
2120
|
-
provider reports what happens next through an event.
|
|
2116
|
+
A preview's deadline is not on the offer. It travels on the task, as `previewEndsAt` with
|
|
2117
|
+
`atDeadline`, so a snapshot carries it too -- see **Preview: the agent presses Call**.
|
|
2121
2118
|
|
|
2122
2119
|
A provider may withdraw a pending task by emitting `task-ended` with a `cancelled` outcome.
|
|
2123
2120
|
|
|
@@ -2151,15 +2148,17 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2151
2148
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2152
2149
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2153
2150
|
| `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. |
|
|
2154
|
-
| `phase` | Current canonical task phase: `pending`, `confirmed`, `
|
|
2151
|
+
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2155
2152
|
| `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`**. |
|
|
2156
2153
|
| `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**. |
|
|
2154
|
+
| `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2155
|
+
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
|
|
2157
2156
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2158
2157
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2159
2158
|
| `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. |
|
|
2160
2159
|
| `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. |
|
|
2161
2160
|
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2162
|
-
| `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, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `
|
|
2161
|
+
| `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, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where a host placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2163
2162
|
| `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**. |
|
|
2164
2163
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead-assist` `take-over` and `leave` issuable. See **Lead assist**. |
|
|
2165
2164
|
| `monitoring` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Monitoring a call**. |
|
|
@@ -2192,9 +2191,11 @@ The canonical task transitions are:
|
|
|
2192
2191
|
| --- | --- | --- |
|
|
2193
2192
|
| No task | Provider allocates a task | `pending` |
|
|
2194
2193
|
| `pending` | Task is accepted | `confirmed` |
|
|
2195
|
-
| `confirmed` |
|
|
2196
|
-
| `confirmed` | Work begins
|
|
2197
|
-
| `
|
|
2194
|
+
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2195
|
+
| `confirmed` | Work begins | `in-progress` |
|
|
2196
|
+
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2197
|
+
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2198
|
+
| `preview` | The deadline `expires` | Removed by `task-ended` with `expired` outcome |
|
|
2198
2199
|
| `pending` | Provider withdraws the allocation | Removed by `task-ended` with `cancelled` outcome |
|
|
2199
2200
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2200
2201
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
@@ -2207,9 +2208,47 @@ The canonical task transitions are:
|
|
|
2207
2208
|
|
|
2208
2209
|
Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2209
2210
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2210
|
-
subsequent transitions to `
|
|
2211
|
+
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
2211
2212
|
command.
|
|
2212
2213
|
|
|
2214
|
+
#### Preview: the agent presses Call
|
|
2215
|
+
|
|
2216
|
+
An outbound campaign that lets the agent see who they are about to call is a **preview**: the
|
|
2217
|
+
record arrives as a task, the task sits in `preview` with the party on it, and no call has gone
|
|
2218
|
+
out. The agent reads and presses Call. That is the whole phase, and it exists on voice alone --
|
|
2219
|
+
chat and email have no call to place, and their reading is ordinary work in `in-progress`.
|
|
2220
|
+
|
|
2221
|
+
```ts
|
|
2222
|
+
const previewed = {
|
|
2223
|
+
channel: "voice",
|
|
2224
|
+
capabilities: {},
|
|
2225
|
+
phase: "preview",
|
|
2226
|
+
party: { name: "Maya Rao", number: "+919876543210" },
|
|
2227
|
+
previewEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2228
|
+
atDeadline: "calls",
|
|
2229
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsAt" | "atDeadline">;
|
|
2230
|
+
|
|
2231
|
+
// The agent presses Call. It is a dial like any other.
|
|
2232
|
+
const pressed: TaskCommand<"voice"> = { type: "call", dialId: "dial-7f2" };
|
|
2233
|
+
```
|
|
2234
|
+
|
|
2235
|
+
**Call is a dial.** It carries the host's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2236
|
+
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
2237
|
+
agent is there to be called. On `answered` the task is `in-progress` and its media follows on
|
|
2238
|
+
`task-media-started`. On any other outcome the task goes to `completing`, so the agent records the
|
|
2239
|
+
no-answer as the disposition it is -- in a campaign that is the commonest outcome there is, and it
|
|
2240
|
+
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2241
|
+
(`task.preview.dialOutcomes.required`).
|
|
2242
|
+
|
|
2243
|
+
**The deadline says what the system will do.** A provider that will not wait forever states
|
|
2244
|
+
`previewEndsAt`, and with it `atDeadline`: `calls` means the system places the call itself when the
|
|
2245
|
+
time runs out, which is progressive dialling; `expires` means the record is taken back and the task
|
|
2246
|
+
ends with an `expired` outcome naming `preview`. The two travel together
|
|
2247
|
+
(`task.preview.atDeadline.required`, `task.preview.previewEndsAt.required`), only while the task is in
|
|
2248
|
+
`preview` (`task.preview.deadline.unexpected`), and Omni's countdown reads "calling in 12s" or
|
|
2249
|
+
"expires in 12s" from the provider's word. With neither, the agent has as long as they need.
|
|
2250
|
+
Reaching the instant is not itself a transition: the provider reports what it did, as an event.
|
|
2251
|
+
|
|
2213
2252
|
**A task is never its audio.** A voice task is the allocation: the call is offered when it is
|
|
2214
2253
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2215
2254
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
@@ -2372,7 +2411,7 @@ executes** — so without a step the provider, which alone keeps the task's reco
|
|
|
2372
2411
|
account of a period the agent could not be heard.
|
|
2373
2412
|
|
|
2374
2413
|
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2375
|
-
each end a dial, so the entry carries the `
|
|
2414
|
+
each end a dial, so the entry carries the `destinationId` it went to and, where a host placed it, the
|
|
2376
2415
|
`dialId` the host minted; no other step dialled, and either field on one is refused
|
|
2377
2416
|
(`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2378
2417
|
by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
|
|
@@ -2632,9 +2671,9 @@ the provider published none.
|
|
|
2632
2671
|
```ts
|
|
2633
2672
|
capabilities: {
|
|
2634
2673
|
coldTransfer: {
|
|
2635
|
-
allowManualEntry: false,
|
|
2636
2674
|
destinations: [
|
|
2637
|
-
{ id: "tier2", label: "Tier 2 support"
|
|
2675
|
+
{ id: "tier2", label: "Tier 2 support" },
|
|
2676
|
+
{ id: "main-menu", label: "Back to the main menu" },
|
|
2638
2677
|
],
|
|
2639
2678
|
},
|
|
2640
2679
|
}
|
|
@@ -2642,22 +2681,23 @@ capabilities: {
|
|
|
2642
2681
|
|
|
2643
2682
|
| Field | Contract |
|
|
2644
2683
|
| --- | --- |
|
|
2645
|
-
| `destinations` |
|
|
2646
|
-
| `
|
|
2647
|
-
| `
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2684
|
+
| `destinations` | The items the control offers, at least one, with unique `id` values. Each is a button or a menu item on the desk. |
|
|
2685
|
+
| `id` | What Omni sends as `destinationId` on a `transfer` or `conference` command, and what the provider executes. |
|
|
2686
|
+
| `label` | What the agent reads. |
|
|
2687
|
+
|
|
2688
|
+
**The protocol does not say what an item does.** A queue, an IVR menu, an outside line, a
|
|
2689
|
+
skill group -- that is the queue's configuration and the provider's business. The agent sees a
|
|
2690
|
+
list of labels, picks one, and Omni sends its `id`; the provider executes whatever the item is. No
|
|
2691
|
+
kind travels, nothing is typed, and a control with an empty directory has nothing to offer and is
|
|
2692
|
+
refused (`task.destinations.offer`). A control declared as bare `true` is refused for the same
|
|
2693
|
+
reason (`task.destinations.shape`): once nothing is typed, the directory is the control.
|
|
2694
|
+
|
|
2695
|
+
**An agent never transfers to a named agent**, warm or cold. A contact that needs another pair of
|
|
2696
|
+
hands goes back to a queue or a menu the queue configured, and who takes it next is the queue's
|
|
2697
|
+
decision: routing by skill, availability and fairness is what the queue is for, and a hand-off to a
|
|
2698
|
+
chosen colleague bypasses all three. The desk shows no agent picker and no dial pad on a transfer.
|
|
2699
|
+
The one way a named person ends up with an agent's call is a lead taking it over through lead
|
|
2700
|
+
assist, which is the lead's act, not a transfer the agent chose.
|
|
2661
2701
|
|
|
2662
2702
|
#### Warm transfer
|
|
2663
2703
|
|
|
@@ -2671,7 +2711,7 @@ offer any of the three without the others, and each is declared on its own.
|
|
|
2671
2711
|
// 1. Warm. A dial: the provider parks the customer and calls the destination, answers
|
|
2672
2712
|
// `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
|
|
2673
2713
|
// while the call to it stands.
|
|
2674
|
-
{ type: "transfer", action: "warm", dialId: "dial-7f2",
|
|
2714
|
+
{ type: "transfer", action: "warm", dialId: "dial-7f2", destinationId: "tier2" }
|
|
2675
2715
|
|
|
2676
2716
|
// 2a. Hand the customer to the consulted destination and leave.
|
|
2677
2717
|
{ type: "transfer", action: "complete" }
|
|
@@ -2680,7 +2720,7 @@ offer any of the three without the others, and each is declared on its own.
|
|
|
2680
2720
|
{ type: "transfer", action: "cancel" }
|
|
2681
2721
|
```
|
|
2682
2722
|
|
|
2683
|
-
`warm` is gated by the `warmTransfer` capability and
|
|
2723
|
+
`warm` is gated by the `warmTransfer` capability and names a directory item exactly as a cold
|
|
2684
2724
|
transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
|
|
2685
2725
|
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
2686
2726
|
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
@@ -2790,16 +2830,16 @@ declare const connection: Connection<"voice">;
|
|
|
2790
2830
|
declare const taskId: TaskId;
|
|
2791
2831
|
|
|
2792
2832
|
// 1. Out, minted by the host.
|
|
2793
|
-
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2",
|
|
2833
|
+
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
2794
2834
|
|
|
2795
2835
|
// 2. Back on the result, restated, so the host compares and confirms.
|
|
2796
2836
|
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
2797
2837
|
|
|
2798
2838
|
// 3. On the outcome, however late, against the task it belonged to.
|
|
2799
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId,
|
|
2839
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
2800
2840
|
|
|
2801
2841
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
2802
|
-
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2",
|
|
2842
|
+
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
2803
2843
|
```
|
|
2804
2844
|
|
|
2805
2845
|
The identity is a correlation handle between host and provider and **is never shown to the
|
|
@@ -2815,7 +2855,7 @@ nothing, is refused (`result.status`). `validateResult(result, method, path, dia
|
|
|
2815
2855
|
`dialId` the host sent for that reason.
|
|
2816
2856
|
|
|
2817
2857
|
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
2818
|
-
there was one, the
|
|
2858
|
+
there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
|
|
2819
2859
|
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
|
|
2820
2860
|
`answered` is stated rather than read off somebody appearing on the call, because a host that infers
|
|
2821
2861
|
success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
|
|
@@ -2868,15 +2908,15 @@ knows the room:
|
|
|
2868
2908
|
const onCall: OnCall[] = [
|
|
2869
2909
|
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
2870
2910
|
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
2871
|
-
{ role: "conferenced",
|
|
2872
|
-
{ role: "conferenced",
|
|
2911
|
+
{ role: "conferenced", destinationId: "tier2", dialId: "dial-7f2", stage: "joined", held: true, since: "2026-08-21T09:15:30Z" },
|
|
2912
|
+
{ role: "conferenced", destinationId: "main-menu", dialId: "dial-3c9", stage: "ringing", since: "2026-08-21T09:16:02Z" },
|
|
2873
2913
|
];
|
|
2874
2914
|
```
|
|
2875
2915
|
|
|
2876
2916
|
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
2877
|
-
neither carries a destination. `consulted` and `conferenced` were, so both carry the
|
|
2878
|
-
and, where a host's dial brought them, its `dialId`; a person
|
|
2879
|
-
none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2917
|
+
neither carries a destination. `consulted` and `conferenced` were, so both carry the
|
|
2918
|
+
`destinationId` of the directory item and, where a host's dial brought them, its `dialId`; a person
|
|
2919
|
+
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2880
2920
|
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
2881
2921
|
never saw.
|
|
2882
2922
|
|
|
@@ -2885,7 +2925,7 @@ what makes `transfer` `cancel` issuable and what `conference` `remove` names, an
|
|
|
2885
2925
|
agent cannot call off while it rings is a parked customer with no way back -- which is the very case
|
|
2886
2926
|
`cancelled` exists for. Consult and conference behave alike here: the `consulted` or `conferenced`
|
|
2887
2927
|
entry appears with its `dialId` when the dial is placed, and a `conference` `remove` naming a
|
|
2888
|
-
still-ringing `
|
|
2928
|
+
still-ringing `destinationId` calls that dial off, with `cancelled` as its outcome.
|
|
2889
2929
|
|
|
2890
2930
|
**The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
|
|
2891
2931
|
answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
|
|
@@ -3707,7 +3747,7 @@ declared:
|
|
|
3707
3747
|
| --- | --- |
|
|
3708
3748
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
3709
3749
|
| `decline` | The `decline` capability on any channel, **and** Omni provisioning permitting it. One word for refusing an offer, whatever the channel. |
|
|
3710
|
-
| `
|
|
3750
|
+
| `call` | 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`. |
|
|
3711
3751
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
3712
3752
|
| `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. |
|
|
3713
3753
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
@@ -3764,7 +3804,7 @@ react rather than only display the message:
|
|
|
3764
3804
|
| `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. |
|
|
3765
3805
|
| `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. |
|
|
3766
3806
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
3767
|
-
| `omni.destination-not-permitted` | The
|
|
3807
|
+
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
3768
3808
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
3769
3809
|
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
3770
3810
|
| `omni.break-already-committed` | Cancellation lost the commit/cancel race; Omni must finish commit recovery. |
|
|
@@ -3785,7 +3825,7 @@ They are published as `OMNI_FAILURE_CODES`.
|
|
|
3785
3825
|
#### Provider instants are read against a provider clock
|
|
3786
3826
|
|
|
3787
3827
|
Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
|
|
3788
|
-
`
|
|
3828
|
+
`previewEndsAt`, and the wrap deadline of `task-media-ended` plus `wrapAllowance` where
|
|
3789
3829
|
one is stated.
|
|
3790
3830
|
Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
|
|
3791
3831
|
damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
|