@xema/omni-protocol 0.1.84 → 0.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -9
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +6 -6
- package/dist/validation.js +35 -35
- package/guide.md +53 -34
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -619,15 +619,15 @@ export interface ScheduledActivity {
|
|
|
619
619
|
party?: Contact;
|
|
620
620
|
attributes?: Attribute[];
|
|
621
621
|
}
|
|
622
|
-
export interface
|
|
622
|
+
export interface OutcomeCode {
|
|
623
623
|
id: string;
|
|
624
624
|
label: string;
|
|
625
625
|
group?: string;
|
|
626
626
|
}
|
|
627
|
-
export interface
|
|
627
|
+
export interface OutcomeRules {
|
|
628
628
|
required?: boolean;
|
|
629
629
|
notes?: "required" | "optional" | "none";
|
|
630
|
-
codes?:
|
|
630
|
+
codes?: OutcomeCode[];
|
|
631
631
|
}
|
|
632
632
|
/**
|
|
633
633
|
* One item the queue configured for the agent to send a contact to: a button or a menu item. The
|
|
@@ -658,7 +658,7 @@ export interface CustomCapability {
|
|
|
658
658
|
}
|
|
659
659
|
export interface SharedTaskCapabilities {
|
|
660
660
|
browsers?: true;
|
|
661
|
-
|
|
661
|
+
outcomes?: true | OutcomeRules;
|
|
662
662
|
custom?: CustomCapability[];
|
|
663
663
|
}
|
|
664
664
|
/**
|
|
@@ -1054,8 +1054,8 @@ export declare const TASK_COMMAND_NAMES: {
|
|
|
1054
1054
|
readonly email: readonly ["accept", "decline", "complete"];
|
|
1055
1055
|
};
|
|
1056
1056
|
export type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> = (typeof TASK_COMMAND_NAMES)[C][number];
|
|
1057
|
-
export interface
|
|
1058
|
-
|
|
1057
|
+
export interface OutcomePayload {
|
|
1058
|
+
outcome?: string;
|
|
1059
1059
|
notes?: string;
|
|
1060
1060
|
}
|
|
1061
1061
|
export type VoiceTaskCommand = {
|
|
@@ -1153,7 +1153,7 @@ export type VoiceTaskCommand = {
|
|
|
1153
1153
|
source: "provider";
|
|
1154
1154
|
}) | ({
|
|
1155
1155
|
type: "complete";
|
|
1156
|
-
} &
|
|
1156
|
+
} & OutcomePayload);
|
|
1157
1157
|
export type ChatTaskCommand = {
|
|
1158
1158
|
type: "accept";
|
|
1159
1159
|
} | {
|
|
@@ -1164,14 +1164,14 @@ export type ChatTaskCommand = {
|
|
|
1164
1164
|
type: "resume";
|
|
1165
1165
|
} | ({
|
|
1166
1166
|
type: "complete";
|
|
1167
|
-
} &
|
|
1167
|
+
} & OutcomePayload);
|
|
1168
1168
|
export type EmailTaskCommand = {
|
|
1169
1169
|
type: "accept";
|
|
1170
1170
|
} | {
|
|
1171
1171
|
type: "decline";
|
|
1172
1172
|
} | ({
|
|
1173
1173
|
type: "complete";
|
|
1174
|
-
} &
|
|
1174
|
+
} & OutcomePayload);
|
|
1175
1175
|
export interface CustomTaskCommand {
|
|
1176
1176
|
type: "custom";
|
|
1177
1177
|
name: string;
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and roster, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.monitoring", "task.media", "task.acceptance", "task.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.interactionHistory", "task.onCall", "task.leadAssist", "task.assisting", "task.monitoring", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
package/dist/testing.js
CHANGED
|
@@ -18,7 +18,7 @@ const STATE_SUBJECTS = [
|
|
|
18
18
|
"task.monitoring",
|
|
19
19
|
"task.media",
|
|
20
20
|
"task.acceptance",
|
|
21
|
-
"task.
|
|
21
|
+
"task.outcomes",
|
|
22
22
|
"task.destinations",
|
|
23
23
|
"task.custom",
|
|
24
24
|
"task.locked",
|
|
@@ -68,8 +68,8 @@ function observeTask(value, seen) {
|
|
|
68
68
|
if (value.acceptance !== undefined)
|
|
69
69
|
seen.add("task.acceptance");
|
|
70
70
|
const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
|
|
71
|
-
if (isRecord(capabilities.
|
|
72
|
-
seen.add("task.
|
|
71
|
+
if (isRecord(capabilities.outcomes))
|
|
72
|
+
seen.add("task.outcomes");
|
|
73
73
|
for (const directory of ["coldTransfer", "warmTransfer", "conference"]) {
|
|
74
74
|
const declared = capabilities[directory];
|
|
75
75
|
if (isRecord(declared) && some(declared.destinations))
|
|
@@ -1793,9 +1793,9 @@ async function driveOneCall(drive) {
|
|
|
1793
1793
|
const completable = latestTask().phase === "completing" || latestTask().phase === "in-progress" || latestTask().phase === "paused";
|
|
1794
1794
|
if (latestTask().completionMode === "agent-command" && completable) {
|
|
1795
1795
|
const command = { type: "complete" };
|
|
1796
|
-
const
|
|
1797
|
-
if (isRecord(
|
|
1798
|
-
command.
|
|
1796
|
+
const outcomes = isRecord(latestTask().capabilities) ? latestTask().capabilities.outcomes : undefined;
|
|
1797
|
+
if (isRecord(outcomes) && outcomes.required === true && Array.isArray(outcomes.codes) && isRecord(outcomes.codes[0])) {
|
|
1798
|
+
command.outcome = outcomes.codes[0].id;
|
|
1799
1799
|
}
|
|
1800
1800
|
if (await send(command) !== undefined) {
|
|
1801
1801
|
// applied says the provider has completed the task, and its ending follows within the bound
|
package/dist/validation.js
CHANGED
|
@@ -98,11 +98,11 @@ const ISOLATION_SCHEME_VALUES = Object.values(BROWSER_ISOLATION_SCHEMES);
|
|
|
98
98
|
/** The capabilities each channel arm of `TaskCapabilities` declares, keyed off the type itself. */
|
|
99
99
|
const TASK_CAPABILITIES = {
|
|
100
100
|
voice: membersOf({
|
|
101
|
-
browsers: true,
|
|
101
|
+
browsers: true, outcomes: true, custom: true, decline: true, hold: true,
|
|
102
102
|
endCall: true, connectBack: true, coldTransfer: true, warmTransfer: true, leadAssist: true, conference: true, recording: true,
|
|
103
103
|
}),
|
|
104
|
-
chat: membersOf({ browsers: true,
|
|
105
|
-
email: membersOf({ browsers: true,
|
|
104
|
+
chat: membersOf({ browsers: true, outcomes: true, custom: true, decline: true, hold: true }),
|
|
105
|
+
email: membersOf({ browsers: true, outcomes: true, custom: true, decline: true }),
|
|
106
106
|
};
|
|
107
107
|
// The published list and the type's keys are the same set, or one of them is wrong.
|
|
108
108
|
true;
|
|
@@ -607,41 +607,41 @@ function validateDestinationDirectory(value, path, into) {
|
|
|
607
607
|
into.filled(destination.label, "task.destination.label", `${at}.label`, "a destination needs a label");
|
|
608
608
|
});
|
|
609
609
|
}
|
|
610
|
-
function
|
|
610
|
+
function validateOutcomes(value, path, into) {
|
|
611
611
|
if (value === true)
|
|
612
612
|
return;
|
|
613
613
|
if (!isPlainObject(value)) {
|
|
614
|
-
into.add("task.
|
|
614
|
+
into.add("task.outcomes.shape", path, "must be true or an outcome policy");
|
|
615
615
|
return;
|
|
616
616
|
}
|
|
617
617
|
if (value.required !== undefined) {
|
|
618
|
-
into.require(typeof value.required === "boolean", "task.
|
|
618
|
+
into.require(typeof value.required === "boolean", "task.outcomes.required", `${path}.required`, "required must be a boolean when present");
|
|
619
619
|
}
|
|
620
620
|
// A code must be collected before completion, so there must be one to collect.
|
|
621
621
|
if (value.required === true && !(Array.isArray(value.codes) && value.codes.length > 0)) {
|
|
622
|
-
into.add("task.
|
|
622
|
+
into.add("task.outcomes.required.codes", `${path}.codes`, "a required outcome policy must publish at least one code");
|
|
623
623
|
}
|
|
624
624
|
if (value.notes !== undefined)
|
|
625
|
-
into.oneOf(value.notes, NOTES_RULES, "task.
|
|
625
|
+
into.oneOf(value.notes, NOTES_RULES, "task.outcomes.notes", `${path}.notes`);
|
|
626
626
|
if (value.codes === undefined)
|
|
627
627
|
return;
|
|
628
628
|
if (!Array.isArray(value.codes)) {
|
|
629
|
-
into.add("task.
|
|
629
|
+
into.add("task.outcomes.codes", `${path}.codes`, "codes must be an array when present");
|
|
630
630
|
return;
|
|
631
631
|
}
|
|
632
632
|
const seen = new Set();
|
|
633
633
|
value.codes.forEach((code, index) => {
|
|
634
634
|
const at = `${path}.codes[${index}]`;
|
|
635
635
|
if (!isPlainObject(code)) {
|
|
636
|
-
into.add("task.
|
|
636
|
+
into.add("task.outcome.shape", at, "each outcome code must be an object");
|
|
637
637
|
return;
|
|
638
638
|
}
|
|
639
|
-
if (into.filled(code.id, "task.
|
|
639
|
+
if (into.filled(code.id, "task.outcome.id", `${at}.id`, "an outcome code needs an id")) {
|
|
640
640
|
if (seen.has(code.id))
|
|
641
|
-
into.add("task.
|
|
641
|
+
into.add("task.outcome.unique", `${at}.id`, `duplicate outcome code: ${code.id}`);
|
|
642
642
|
seen.add(code.id);
|
|
643
643
|
}
|
|
644
|
-
into.filled(code.label, "task.
|
|
644
|
+
into.filled(code.label, "task.outcome.label", `${at}.label`, "an outcome code needs a label");
|
|
645
645
|
});
|
|
646
646
|
}
|
|
647
647
|
function validateCustomCapabilities(value, path, into) {
|
|
@@ -697,7 +697,7 @@ const DEFAULT_LEVEL_IDS = DEFAULT_LEVELS.map(level => level.id);
|
|
|
697
697
|
const POLICY_SETTINGS = membersOf({ on: true, off: true, person: true });
|
|
698
698
|
const POLICY_KEYS = new Set([
|
|
699
699
|
...TASK_CAPABILITIES.voice, ...TASK_CAPABILITIES.chat, ...TASK_CAPABILITIES.email, "dial",
|
|
700
|
-
].filter(name => name !== "browsers" && name !== "
|
|
700
|
+
].filter(name => name !== "browsers" && name !== "outcomes" && name !== "custom"));
|
|
701
701
|
const PERSON_SETTABLE = /^(hold|skill:.+)$/;
|
|
702
702
|
const isLocked = (value) => isPlainObject(value) && value.lockedBy !== undefined;
|
|
703
703
|
/** The level ids in force: the manifest's, or the defaults when the caller holds no manifest. */
|
|
@@ -1260,9 +1260,9 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1260
1260
|
into.add("task.capability.dialOutcomes.required", `${path}.capabilities.${name}`, `${name} dials, and the manifest declares no dialOutcomes to say how a dial ends`);
|
|
1261
1261
|
}
|
|
1262
1262
|
// A control the queue could allow may stand locked in its place, saying whose. What the
|
|
1263
|
-
// queue provides -- browsers,
|
|
1263
|
+
// queue provides -- browsers, outcomes, custom controls -- is content, not a control.
|
|
1264
1264
|
if (isLocked(declared)) {
|
|
1265
|
-
if (into.require(name !== "browsers" && name !== "
|
|
1265
|
+
if (into.require(name !== "browsers" && name !== "outcomes" && name !== "custom", "task.capability.locked.unexpected", `${path}.capabilities.${name}`, `${name} is what the queue provides, not a control anyone locks`)) {
|
|
1266
1266
|
validateLockedInto(declared, "task.capability.locked", `${path}.capabilities.${name}`, context.levels, into);
|
|
1267
1267
|
}
|
|
1268
1268
|
continue;
|
|
@@ -1271,8 +1271,8 @@ function validateTaskInto(task, context, path, into) {
|
|
|
1271
1271
|
case "recording":
|
|
1272
1272
|
into.violations.push(...validateRecordingPolicy(declared, `${path}.capabilities.recording`));
|
|
1273
1273
|
break;
|
|
1274
|
-
case "
|
|
1275
|
-
|
|
1274
|
+
case "outcomes":
|
|
1275
|
+
validateOutcomes(declared, `${path}.capabilities.outcomes`, into);
|
|
1276
1276
|
break;
|
|
1277
1277
|
case "custom":
|
|
1278
1278
|
validateCustomCapabilities(declared, `${path}.capabilities.custom`, into);
|
|
@@ -2298,7 +2298,7 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2298
2298
|
fields.push("dialId");
|
|
2299
2299
|
break;
|
|
2300
2300
|
case "complete":
|
|
2301
|
-
fields.push("
|
|
2301
|
+
fields.push("outcome", "notes");
|
|
2302
2302
|
break;
|
|
2303
2303
|
case "transfer":
|
|
2304
2304
|
fields.push("action");
|
|
@@ -2359,8 +2359,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2359
2359
|
into.violations.push(...validateRecordingCommandShape(command, "provider", path));
|
|
2360
2360
|
break;
|
|
2361
2361
|
case "complete":
|
|
2362
|
-
if (command.
|
|
2363
|
-
into.filled(command.
|
|
2362
|
+
if (command.outcome !== undefined)
|
|
2363
|
+
into.filled(command.outcome, "command.complete.outcome", `${path}.outcome`, "an outcome must not be empty when present");
|
|
2364
2364
|
if (command.notes !== undefined)
|
|
2365
2365
|
into.require(typeof command.notes === "string", "command.complete.notes", `${path}.notes`, "notes must be a string when present");
|
|
2366
2366
|
break;
|
|
@@ -2509,27 +2509,27 @@ export function validateTaskCommand(command, task, path = "command", taskContext
|
|
|
2509
2509
|
}
|
|
2510
2510
|
case "complete": {
|
|
2511
2511
|
into.require(task.completionMode === "agent-command", "command.complete.mode", path, "complete belongs to agent-command; a provider-automatic task completes itself");
|
|
2512
|
-
// What travels with complete is what the
|
|
2512
|
+
// What travels with complete is what the outcomes capability published: a code from its
|
|
2513
2513
|
// list where it has one, a code at all where it requires one, notes as it said, nothing where it published nothing.
|
|
2514
|
-
const
|
|
2515
|
-
if (
|
|
2516
|
-
into.require(command.
|
|
2517
|
-
into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task declares no
|
|
2514
|
+
const outcomes = capabilities.outcomes;
|
|
2515
|
+
if (outcomes === undefined) {
|
|
2516
|
+
into.require(command.outcome === undefined, "command.complete.outcome.unexpected", `${path}.outcome`, "the task declares no outcomes; complete travels with no code");
|
|
2517
|
+
into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task declares no outcomes; complete travels with no notes");
|
|
2518
2518
|
}
|
|
2519
|
-
else if (
|
|
2520
|
-
into.require(command.
|
|
2519
|
+
else if (outcomes === true) {
|
|
2520
|
+
into.require(command.outcome === undefined, "command.complete.outcome.unexpected", `${path}.outcome`, "a bare outcomes control publishes no code, so complete carries none");
|
|
2521
2521
|
}
|
|
2522
|
-
else if (isPlainObject(
|
|
2523
|
-
if (Array.isArray(
|
|
2524
|
-
into.require(
|
|
2522
|
+
else if (isPlainObject(outcomes)) {
|
|
2523
|
+
if (Array.isArray(outcomes.codes) && command.outcome !== undefined) {
|
|
2524
|
+
into.require(outcomes.codes.some(code => isPlainObject(code) && code.id === command.outcome), "command.complete.outcome.unknown", `${path}.outcome`, `${describeValue(command.outcome)} is not a code the task published`);
|
|
2525
2525
|
}
|
|
2526
|
-
if (
|
|
2527
|
-
into.require(command.
|
|
2526
|
+
if (outcomes.required === true) {
|
|
2527
|
+
into.require(command.outcome !== undefined, "command.complete.outcome.required", `${path}.outcome`, "the task requires an outcome and complete carries none");
|
|
2528
2528
|
}
|
|
2529
|
-
if (
|
|
2529
|
+
if (outcomes.notes === "none") {
|
|
2530
2530
|
into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task takes no notes");
|
|
2531
2531
|
}
|
|
2532
|
-
else if (
|
|
2532
|
+
else if (outcomes.notes === "required") {
|
|
2533
2533
|
into.require(typeof command.notes === "string" && command.notes.length > 0, "command.complete.notes.required", `${path}.notes`, "the task requires notes and complete carries none");
|
|
2534
2534
|
}
|
|
2535
2535
|
}
|
package/guide.md
CHANGED
|
@@ -527,12 +527,12 @@ type DialResult =
|
|
|
527
527
|
### Task capabilities
|
|
528
528
|
|
|
529
529
|
```ts
|
|
530
|
-
type
|
|
530
|
+
type OutcomeCode = { id: string; label: string; group?: string };
|
|
531
531
|
|
|
532
|
-
type
|
|
532
|
+
type OutcomeRules = {
|
|
533
533
|
required?: boolean;
|
|
534
534
|
notes?: "required" | "optional" | "none";
|
|
535
|
-
codes?:
|
|
535
|
+
codes?: OutcomeCode[];
|
|
536
536
|
};
|
|
537
537
|
|
|
538
538
|
type Destination = {
|
|
@@ -557,7 +557,7 @@ type CustomCapability = {
|
|
|
557
557
|
|
|
558
558
|
type SharedTaskCapabilities = {
|
|
559
559
|
browsers?: true;
|
|
560
|
-
|
|
560
|
+
outcomes?: true | OutcomeRules;
|
|
561
561
|
custom?: CustomCapability[];
|
|
562
562
|
};
|
|
563
563
|
|
|
@@ -793,7 +793,7 @@ const TASK_COMMAND_NAMES = {
|
|
|
793
793
|
type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> =
|
|
794
794
|
(typeof TASK_COMMAND_NAMES)[C][number];
|
|
795
795
|
|
|
796
|
-
type
|
|
796
|
+
type OutcomePayload = { outcome?: string; notes?: string };
|
|
797
797
|
|
|
798
798
|
type VoiceTaskCommand =
|
|
799
799
|
| { type: "answer" }
|
|
@@ -815,19 +815,19 @@ type VoiceTaskCommand =
|
|
|
815
815
|
| { type: "conference"; action: "remove"; destinationId: string; party?: never }
|
|
816
816
|
| { type: "conference"; action: "remove"; party: true; destinationId?: never }
|
|
817
817
|
| (RecordingCommand & { source: "provider" })
|
|
818
|
-
| ({ type: "complete" } &
|
|
818
|
+
| ({ type: "complete" } & OutcomePayload);
|
|
819
819
|
|
|
820
820
|
type ChatTaskCommand =
|
|
821
821
|
| { type: "accept" }
|
|
822
822
|
| { type: "decline" }
|
|
823
823
|
| { type: "pause" }
|
|
824
824
|
| { type: "resume" }
|
|
825
|
-
| ({ type: "complete" } &
|
|
825
|
+
| ({ type: "complete" } & OutcomePayload);
|
|
826
826
|
|
|
827
827
|
type EmailTaskCommand =
|
|
828
828
|
| { type: "accept" }
|
|
829
829
|
| { type: "decline" }
|
|
830
|
-
| ({ type: "complete" } &
|
|
830
|
+
| ({ type: "complete" } & OutcomePayload);
|
|
831
831
|
|
|
832
832
|
type CustomTaskCommand = { type: "custom"; name: string; [key: string]: unknown };
|
|
833
833
|
|
|
@@ -898,7 +898,7 @@ The adapter is the one that knows the value, so it is the one held to it: given
|
|
|
898
898
|
queue locked, the validator refuses any other field carrying one, digits compared as digits so no
|
|
899
899
|
formatting hides them (`task.locked.leak`), and a conformance run whose tasks lock a party's number
|
|
900
900
|
or email states those values in `lockedValues`, since a run that cannot ask the question is not a
|
|
901
|
-
pass. A host never has the value and never asks. A name is not locked. What the queue provides rather than permits — browsers,
|
|
901
|
+
pass. A host never has the value and never asks. A name is not locked. What the queue provides rather than permits — browsers, outcomes, custom
|
|
902
902
|
controls — is content, and is never locked.
|
|
903
903
|
|
|
904
904
|
**A lead sets the team's policy from their roster.** A login that declares
|
|
@@ -1499,7 +1499,7 @@ normative definition; matching names alone do not establish shared meaning.
|
|
|
1499
1499
|
|
|
1500
1500
|
### 10. Order on the wire is display order
|
|
1501
1501
|
|
|
1502
|
-
A list the provider publishes — a destination directory,
|
|
1502
|
+
A list the provider publishes — a destination directory, outcome codes, a task's custom
|
|
1503
1503
|
controls, its browsers — is in the order the provider wants it shown, and a host keeps that order.
|
|
1504
1504
|
A reader will find meaning in position unless something says there is none, so the provider
|
|
1505
1505
|
decides: where it has an intention, an administrator's sequence, it lists in that order; where it
|
|
@@ -2368,7 +2368,7 @@ into a new session.
|
|
|
2368
2368
|
`task-offered` introduces a new task and does not imply acceptance.
|
|
2369
2369
|
|
|
2370
2370
|
`Task<C>` is channel-discriminated. For example, `Task<"email">` accepts
|
|
2371
|
-
`browsers` and `
|
|
2371
|
+
`browsers` and `outcomes`, but rejects voice-only controls such as `hold` and `endCall` at compile
|
|
2372
2372
|
time. Runtime conformance checks also require the task channel to match its provider manifest.
|
|
2373
2373
|
|
|
2374
2374
|
| Field | Contract |
|
|
@@ -2493,7 +2493,7 @@ ring-back is audio the agent hears -- so `task-media-started` arrives on the `pr
|
|
|
2493
2493
|
its party ringing. On `answered` the task is `in-progress`; on any other outcome the media ends,
|
|
2494
2494
|
`task-media-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
2495
2495
|
`completing`, so the agent records the
|
|
2496
|
-
no-answer as the
|
|
2496
|
+
no-answer as the outcome it is -- in a campaign that is the commonest outcome there is, and it
|
|
2497
2497
|
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2498
2498
|
(`task.preview.dialOutcomes.required`).
|
|
2499
2499
|
|
|
@@ -2631,7 +2631,7 @@ const emailCompletion = {
|
|
|
2631
2631
|
```
|
|
2632
2632
|
|
|
2633
2633
|
In this example, the agent has two minutes after sending the email to add notes, select a
|
|
2634
|
-
|
|
2634
|
+
outcome, and complete the task.
|
|
2635
2635
|
|
|
2636
2636
|
`0` means completion may happen immediately. With `provider-automatic`, the provider may complete
|
|
2637
2637
|
without waiting for a command; with `agent-command`, it still waits for `complete`.
|
|
@@ -2694,7 +2694,7 @@ window it cannot, and declaring it there changes nothing.
|
|
|
2694
2694
|
```ts
|
|
2695
2695
|
const connectBackCapable = {
|
|
2696
2696
|
channel: "voice",
|
|
2697
|
-
capabilities: { hold: true, connectBack: true,
|
|
2697
|
+
capabilities: { hold: true, connectBack: true, outcomes: true },
|
|
2698
2698
|
phase: "completing",
|
|
2699
2699
|
completionMode: "provider-automatic",
|
|
2700
2700
|
wrapAllowance: 30,
|
|
@@ -2737,8 +2737,7 @@ Migration from the earlier spelling:
|
|
|
2737
2737
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2738
2738
|
History/report rule names now use `interactionHistory`/`interactionReport`; phase and conformance
|
|
2739
2739
|
rules use `interaction` and final-task rules use `completion`. Legacy fields are rejected even
|
|
2740
|
-
when the new field is also supplied.
|
|
2741
|
-
`dispositions`, and `disposition` still describe outcome codes and are unchanged.
|
|
2740
|
+
when the new field is also supplied.
|
|
2742
2741
|
|
|
2743
2742
|
|
|
2744
2743
|
`Task.interactionHistory` is the call record: the steps that brought the task to the agent, oldest
|
|
@@ -3079,7 +3078,7 @@ source is one more published fact about them, and the one that lets a host tell
|
|
|
3079
3078
|
|
|
3080
3079
|
**Completing the task is not a capability, and no capability set withholds it.** `complete` is
|
|
3081
3080
|
governed by `completionMode` alone: under `agent-command` it is always available, whatever the set
|
|
3082
|
-
says, and the `
|
|
3081
|
+
says, and the `outcomes` capability decides only whether a code travels with it. Likewise
|
|
3083
3082
|
Answer on a pending task and Call on a preview are the phase's controls, not the set's. The
|
|
3084
3083
|
capability set governs what the agent may do *with* the task; completing it is never on the
|
|
3085
3084
|
list.
|
|
@@ -3173,7 +3172,7 @@ const taskCapabilities = {
|
|
|
3173
3172
|
channel: "voice",
|
|
3174
3173
|
capabilities: {
|
|
3175
3174
|
hold: true,
|
|
3176
|
-
|
|
3175
|
+
outcomes: true,
|
|
3177
3176
|
},
|
|
3178
3177
|
browsers: [],
|
|
3179
3178
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "browsers">;
|
|
@@ -3181,7 +3180,7 @@ const taskCapabilities = {
|
|
|
3181
3180
|
|
|
3182
3181
|
A capability says the control may be offered; the task's phase says whether there is anything to use
|
|
3183
3182
|
it on. Every control below that acts on the call or the conversation -- everything but `decline`,
|
|
3184
|
-
`connectBack` and `
|
|
3183
|
+
`connectBack` and `outcomes` -- is offered only while the task is `in-progress` or `paused`.
|
|
3185
3184
|
See **Which commands need a capability**.
|
|
3186
3185
|
|
|
3187
3186
|
### Voice capabilities
|
|
@@ -3197,20 +3196,40 @@ See **Which commands need a capability**.
|
|
|
3197
3196
|
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
3198
3197
|
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and remove one person from it -- a conferenced entry, one still ringing included, which calls the dial off, or the party, leaving the agent with the colleague. See **Ending a call, and removing one person from it**. |
|
|
3199
3198
|
| `recording` | Overflow menu item: Recording | Per-task provider and host policies expose only their permitted recording actions; each has independent state and routing. |
|
|
3200
|
-
| `
|
|
3199
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3201
3200
|
|
|
3202
3201
|
### Publishing codes and destinations
|
|
3203
3202
|
|
|
3204
3203
|
Four capabilities accept an object when the provider wants Omni to render real choices. For
|
|
3205
|
-
`
|
|
3204
|
+
`outcomes` alone, `true` remains valid and means "offer the control with nothing published";
|
|
3206
3205
|
the three directory controls -- `coldTransfer`, `warmTransfer`, `conference` -- carry their
|
|
3207
3206
|
directory or are refused, since once nothing is typed the directory is the control.
|
|
3208
3207
|
|
|
3209
|
-
#### `
|
|
3208
|
+
#### `outcomes`
|
|
3209
|
+
|
|
3210
|
+
An outcome describes what happened; completion finishes the task. Outcome codes are selected
|
|
3211
|
+
from the task's own published list. They are distinct from `DialOutcome`, which reports the
|
|
3212
|
+
result of a dial attempt.
|
|
3213
|
+
|
|
3214
|
+
Migration requires providers and hosts to adopt the renamed types and fields together:
|
|
3215
|
+
|
|
3216
|
+
| Former API | Current API |
|
|
3217
|
+
| --- | --- |
|
|
3218
|
+
| DispositionCode | `OutcomeCode` |
|
|
3219
|
+
| DispositionRules | `OutcomeRules` |
|
|
3220
|
+
| DispositionPayload | `OutcomePayload` |
|
|
3221
|
+
| Task.capabilities.dispositions | `Task.capabilities.outcomes` |
|
|
3222
|
+
| complete.disposition | `complete.outcome` |
|
|
3223
|
+
|
|
3224
|
+
Validation and conformance diagnostics use `task.outcomes`, `task.outcome`, and
|
|
3225
|
+
`command.complete.outcome` in place of the former names. Update retained task snapshots and
|
|
3226
|
+
command producers as well as imports. Old capability and command fields are rejected, including
|
|
3227
|
+
payloads containing both names; there are no compatibility aliases. Code IDs, notes, required
|
|
3228
|
+
selection rules, and completion behavior are unchanged.
|
|
3210
3229
|
|
|
3211
3230
|
```ts
|
|
3212
3231
|
capabilities: {
|
|
3213
|
-
|
|
3232
|
+
outcomes: {
|
|
3214
3233
|
required: true,
|
|
3215
3234
|
notes: "optional",
|
|
3216
3235
|
codes: [
|
|
@@ -3225,9 +3244,9 @@ capabilities: {
|
|
|
3225
3244
|
| --- | --- |
|
|
3226
3245
|
| `required` | When `true`, Omni must collect a code before issuing `complete`. A required policy must publish at least one code. |
|
|
3227
3246
|
| `notes` | `required`, `optional`, or `none`; controls the free-text field beside the code. |
|
|
3228
|
-
| `codes` | Codes Omni offers. `id` values are non-empty and unique; Omni sends the chosen `id` as `TaskCommand.complete.
|
|
3247
|
+
| `codes` | Codes Omni offers. `id` values are non-empty and unique; Omni sends the chosen `id` as `TaskCommand.complete.outcome`. |
|
|
3229
3248
|
|
|
3230
|
-
With `
|
|
3249
|
+
With `outcomes: true` Omni shows a Complete control and sends `complete` with no code, because
|
|
3231
3250
|
the provider published none.
|
|
3232
3251
|
|
|
3233
3252
|
#### `coldTransfer`, `warmTransfer` and `conference`
|
|
@@ -3360,14 +3379,14 @@ customer.
|
|
|
3360
3379
|
| --- | --- | --- |
|
|
3361
3380
|
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when provisioning also permits declining. |
|
|
3362
3381
|
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
3363
|
-
| `
|
|
3382
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3364
3383
|
|
|
3365
3384
|
### Email capabilities
|
|
3366
3385
|
|
|
3367
3386
|
| Capability | Omni UI | Contract |
|
|
3368
3387
|
| --- | --- | --- |
|
|
3369
3388
|
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when provisioning also permits declining. |
|
|
3370
|
-
| `
|
|
3389
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3371
3390
|
|
|
3372
3391
|
### Custom capabilities
|
|
3373
3392
|
|
|
@@ -4138,7 +4157,7 @@ choice that is no command at all:
|
|
|
4138
4157
|
| --- | --- | --- |
|
|
4139
4158
|
| `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
4140
4159
|
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
4141
|
-
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own
|
|
4160
|
+
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own outcome | The same, independently: **both have the completion window** |
|
|
4142
4161
|
|
|
4143
4162
|
`left` is the one outcome that ends a task without ending the call: this agent left a call that
|
|
4144
4163
|
continues without them. It reads as neither a completion nor a cancellation, because it is
|
|
@@ -4147,7 +4166,7 @@ neither.
|
|
|
4147
4166
|
```ts
|
|
4148
4167
|
const leadAssistCapable = {
|
|
4149
4168
|
channel: "voice",
|
|
4150
|
-
capabilities: { hold: true, leadAssist: true,
|
|
4169
|
+
capabilities: { hold: true, leadAssist: true, outcomes: true },
|
|
4151
4170
|
phase: "in-progress",
|
|
4152
4171
|
leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
4153
4172
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
|
|
@@ -4468,8 +4487,8 @@ with a stale view: a flip against a state the provider has already changed turns
|
|
|
4468
4487
|
then off again. A custom `toggle` control therefore carries its own boolean. `hold` and `resume`,
|
|
4469
4488
|
`pause` and `resume` need no flag, being pairs rather than toggles.
|
|
4470
4489
|
|
|
4471
|
-
**`complete` sends
|
|
4472
|
-
`
|
|
4490
|
+
**`complete` sends an outcome only where one was published.** `outcome` is a
|
|
4491
|
+
`OutcomeCode.id` from the task's own `outcomes` capability, and `notes` obeys that
|
|
4473
4492
|
capability's `notes` setting. A task publishing no codes still receives `complete`, with neither.
|
|
4474
4493
|
|
|
4475
4494
|
### Where a command executes
|
|
@@ -4574,7 +4593,7 @@ declared:
|
|
|
4574
4593
|
| `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
|
|
4575
4594
|
| `decline` | The `decline` capability on any channel, **and** Omni provisioning permitting it. One word for refusing an offer, whatever the channel. |
|
|
4576
4595
|
| `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`. |
|
|
4577
|
-
| `complete` | `completionMode: "agent-command"`. The `
|
|
4596
|
+
| `complete` | `completionMode: "agent-command"`. The `outcomes` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. What travels is what the capability published: a code from its list where it has one (`command.complete.outcome.unknown`), a code at all where it requires one (`.outcome.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no outcomes (`.outcome.unexpected`). |
|
|
4578
4597
|
| `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. |
|
|
4579
4598
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
4580
4599
|
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
@@ -5198,7 +5217,7 @@ outcome -- stays in `notExercised` for every adapter, and a rule about a live ca
|
|
|
5198
5217
|
in each adopter's own tests. `{ drive: true }` closes that: the exercise takes the first task the
|
|
5199
5218
|
provider offers through one ordinary lifecycle -- accept it, wait for its media and open it on a
|
|
5200
5219
|
softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
|
|
5201
|
-
complete it with
|
|
5220
|
+
complete it with an outcome where the agent completes -- and holds every step to the rules a
|
|
5202
5221
|
host holds a provider to. Each command is validated against the task as published
|
|
5203
5222
|
(`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
|
|
5204
5223
|
violation (`drive.command.failed`), and an event the provider owes and never sends is one too
|
|
@@ -5405,7 +5424,7 @@ same operation; reuse with different contents is refused. It never silently retr
|
|
|
5405
5424
|
operation. Implementations without reliable request correlation must refuse controls.
|
|
5406
5425
|
|
|
5407
5426
|
Keep command progress in host UI separately from authoritative state. Applied means the requested
|
|
5408
|
-
transition and
|
|
5427
|
+
recording transition and any retain/discard effect actually completed; publish the confirming task or host report before
|
|
5409
5428
|
resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
|
|
5410
5429
|
show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
|
|
5411
5430
|
updates remain full current task/host views, not replayed provider events. These current-state fields
|