@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 CHANGED
@@ -619,15 +619,15 @@ export interface ScheduledActivity {
619
619
  party?: Contact;
620
620
  attributes?: Attribute[];
621
621
  }
622
- export interface DispositionCode {
622
+ export interface OutcomeCode {
623
623
  id: string;
624
624
  label: string;
625
625
  group?: string;
626
626
  }
627
- export interface DispositionRules {
627
+ export interface OutcomeRules {
628
628
  required?: boolean;
629
629
  notes?: "required" | "optional" | "none";
630
- codes?: DispositionCode[];
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
- dispositions?: true | DispositionRules;
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 DispositionPayload {
1058
- disposition?: string;
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
- } & DispositionPayload);
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
- } & DispositionPayload);
1167
+ } & OutcomePayload);
1168
1168
  export type EmailTaskCommand = {
1169
1169
  type: "accept";
1170
1170
  } | {
1171
1171
  type: "decline";
1172
1172
  } | ({
1173
1173
  type: "complete";
1174
- } & DispositionPayload);
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.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
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.dispositions",
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.dispositions))
72
- seen.add("task.dispositions");
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 dispositions = isRecord(latestTask().capabilities) ? latestTask().capabilities.dispositions : undefined;
1797
- if (isRecord(dispositions) && dispositions.required === true && Array.isArray(dispositions.codes) && isRecord(dispositions.codes[0])) {
1798
- command.disposition = dispositions.codes[0].id;
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
@@ -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, dispositions: true, custom: true, decline: true, hold: 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, dispositions: true, custom: true, decline: true, hold: true }),
105
- email: membersOf({ browsers: true, dispositions: true, custom: true, decline: 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 validateDispositions(value, path, into) {
610
+ function validateOutcomes(value, path, into) {
611
611
  if (value === true)
612
612
  return;
613
613
  if (!isPlainObject(value)) {
614
- into.add("task.dispositions.shape", path, "must be true or a disposition policy");
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.dispositions.required", `${path}.required`, "required must be a boolean when present");
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.dispositions.required.codes", `${path}.codes`, "a required disposition policy must publish at least one code");
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.dispositions.notes", `${path}.notes`);
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.dispositions.codes", `${path}.codes`, "codes must be an array when present");
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.disposition.shape", at, "each disposition code must be an object");
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.disposition.id", `${at}.id`, "a disposition code needs an id")) {
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.disposition.unique", `${at}.id`, `duplicate disposition code: ${code.id}`);
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.disposition.label", `${at}.label`, "a disposition code needs a label");
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 !== "dispositions" && name !== "custom"));
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, dispositions, custom controls -- is content, not a control.
1263
+ // queue provides -- browsers, outcomes, custom controls -- is content, not a control.
1264
1264
  if (isLocked(declared)) {
1265
- if (into.require(name !== "browsers" && name !== "dispositions" && name !== "custom", "task.capability.locked.unexpected", `${path}.capabilities.${name}`, `${name} is what the queue provides, not a control anyone locks`)) {
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 "dispositions":
1275
- validateDispositions(declared, `${path}.capabilities.dispositions`, into);
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("disposition", "notes");
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.disposition !== undefined)
2363
- into.filled(command.disposition, "command.complete.disposition", `${path}.disposition`, "a disposition must not be empty when present");
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 dispositions capability published: a code from its
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 dispositions = capabilities.dispositions;
2515
- if (dispositions === undefined) {
2516
- into.require(command.disposition === undefined, "command.complete.disposition.unexpected", `${path}.disposition`, "the task declares no dispositions; complete travels with no code");
2517
- into.require(command.notes === undefined, "command.complete.notes.unexpected", `${path}.notes`, "the task declares no dispositions; complete travels with no notes");
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 (dispositions === true) {
2520
- into.require(command.disposition === undefined, "command.complete.disposition.unexpected", `${path}.disposition`, "a bare dispositions control publishes no code, so complete carries none");
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(dispositions)) {
2523
- if (Array.isArray(dispositions.codes) && command.disposition !== undefined) {
2524
- into.require(dispositions.codes.some(code => isPlainObject(code) && code.id === command.disposition), "command.complete.disposition.unknown", `${path}.disposition`, `${describeValue(command.disposition)} is not a code the task published`);
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 (dispositions.required === true) {
2527
- into.require(command.disposition !== undefined, "command.complete.disposition.required", `${path}.disposition`, "the task requires a disposition and complete carries none");
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 (dispositions.notes === "none") {
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 (dispositions.notes === "required") {
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 DispositionCode = { id: string; label: string; group?: string };
530
+ type OutcomeCode = { id: string; label: string; group?: string };
531
531
 
532
- type DispositionRules = {
532
+ type OutcomeRules = {
533
533
  required?: boolean;
534
534
  notes?: "required" | "optional" | "none";
535
- codes?: DispositionCode[];
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
- dispositions?: true | DispositionRules;
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 DispositionPayload = { disposition?: string; notes?: string };
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" } & DispositionPayload);
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" } & DispositionPayload);
825
+ | ({ type: "complete" } & OutcomePayload);
826
826
 
827
827
  type EmailTaskCommand =
828
828
  | { type: "accept" }
829
829
  | { type: "decline" }
830
- | ({ type: "complete" } & DispositionPayload);
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, dispositions, custom
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, disposition codes, a task's custom
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 `dispositions`, but rejects voice-only controls such as `hold` and `endCall` at compile
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 disposition it is -- in a campaign that is the commonest outcome there is, and it
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
- disposition, and complete the task.
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, dispositions: 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. `DispositionCode`, `DispositionRules`, `DispositionPayload`,
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 `dispositions` capability decides only whether a code travels with it. Likewise
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
- dispositions: true,
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 `dispositions` -- is offered only while the task is `in-progress` or `paused`.
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
- | `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
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
- `dispositions` alone, `true` remains valid and means "offer the control with nothing published";
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
- #### `dispositions`
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
- dispositions: {
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.disposition`. |
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 `dispositions: true` Omni shows a Complete control and sends `complete` with no code, because
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
- | `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
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
- | `dispositions` | Primary button: Complete | Omni may request task completion with a provider disposition and notes. |
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 disposition | The same, independently: **both have the completion window** |
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, dispositions: 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 a disposition only where one was published.** `disposition` is a
4472
- `DispositionCode.id` from the task's own `dispositions` capability, and `notes` obeys that
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 `dispositions` 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.disposition.unknown`), a code at all where it requires one (`.disposition.required`), notes as it said (`.notes.required`, `.notes.unexpected`), and neither where the task declares no dispositions (`.disposition.unexpected`). |
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 a disposition where the agent completes -- and holds every step to the rules a
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 disposition actually completed; publish the confirming task or host report before
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",