@xema/omni-protocol 0.1.31 → 0.1.32
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 +52 -1
- package/dist/testing.js +19 -2
- package/dist/validation.d.ts +7 -1
- package/dist/validation.js +51 -4
- package/guide.md +80 -11
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
113
113
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
114
114
|
/** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_LEVELS`. */
|
|
115
115
|
orgLevels?: LevelDeclaration[];
|
|
116
|
+
/**
|
|
117
|
+
* The provider takes running reports of a host-performed step -- `recordStep` with `seconds`
|
|
118
|
+
* so far and no `ended`. Absent, the host sends exactly two reports per leg, when it began and
|
|
119
|
+
* when it ended, and a running report is refused: what a provider never asked for never crosses.
|
|
120
|
+
*/
|
|
121
|
+
runningStepReports?: true;
|
|
116
122
|
}
|
|
117
123
|
export interface SecretStore {
|
|
118
124
|
get(key: string): Promise<string | undefined>;
|
|
@@ -493,6 +499,24 @@ export type TaskAttribute = TaskAttributeBase & ({
|
|
|
493
499
|
at: IsoTimestamp;
|
|
494
500
|
});
|
|
495
501
|
export type HandlingStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered";
|
|
502
|
+
/**
|
|
503
|
+
* The call record: the steps that brought the task here, one entry per occurrence and oldest
|
|
504
|
+
* first, and what they add up to before this agent -- stated by the provider from its own record,
|
|
505
|
+
* never summed by a desk from instants. Each total is present when the provider knows it and
|
|
506
|
+
* absent when it does not; a plausible nought is the fallback the no-fallbacks rule forbids. The
|
|
507
|
+
* record rides on the task and is replaced with it, so a late entry corrects the sums.
|
|
508
|
+
*/
|
|
509
|
+
export interface TaskHandlingHistory {
|
|
510
|
+
steps: TaskHandlingStep[];
|
|
511
|
+
/** Seconds others spent handling it before this agent. */
|
|
512
|
+
handleSeconds?: DurationSeconds;
|
|
513
|
+
/** Seconds the caller spent on hold at others' hands. */
|
|
514
|
+
holdSeconds?: DurationSeconds;
|
|
515
|
+
/** Seconds waiting before anyone answered. */
|
|
516
|
+
queueSeconds?: DurationSeconds;
|
|
517
|
+
/** How many times the task changed hands. */
|
|
518
|
+
transfers?: number;
|
|
519
|
+
}
|
|
496
520
|
export interface TaskHandlingStep {
|
|
497
521
|
step: HandlingStep;
|
|
498
522
|
at: IsoTimestamp;
|
|
@@ -617,7 +641,7 @@ export type Task<C extends Channel = Channel> = {
|
|
|
617
641
|
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
618
642
|
reference?: string;
|
|
619
643
|
attributes?: TaskAttribute[];
|
|
620
|
-
handlingHistory?:
|
|
644
|
+
handlingHistory?: TaskHandlingHistory;
|
|
621
645
|
} & TaskCompletion & (C extends "voice" ? {
|
|
622
646
|
consultation?: TaskConsultation;
|
|
623
647
|
lead?: TaskLead;
|
|
@@ -1025,6 +1049,31 @@ export type SetPreferenceRequest = {
|
|
|
1025
1049
|
id: PreferenceId;
|
|
1026
1050
|
inherit: true;
|
|
1027
1051
|
};
|
|
1052
|
+
/**
|
|
1053
|
+
* The host's report of a handling leg it performed itself -- a mute, which Omni does rather than
|
|
1054
|
+
* the provider -- so the provider's record has an account of it. Keyed by `step` and `at`: the
|
|
1055
|
+
* same entry is reported when it begins, as often as the host cares to while it runs, and once
|
|
1056
|
+
* more with `ended`, when `seconds` is the final duration. The host is the authority for the
|
|
1057
|
+
* legs it performs, so `seconds` may say how long so far at any time; the end is stated, never
|
|
1058
|
+
* inferred from a number's presence. What the adapter forwards upstream, and how often, is its own.
|
|
1059
|
+
*/
|
|
1060
|
+
export type HandlingReport = {
|
|
1061
|
+
taskId: TaskId;
|
|
1062
|
+
step: HandlingStep;
|
|
1063
|
+
at: IsoTimestamp;
|
|
1064
|
+
} & ({
|
|
1065
|
+
ended: true;
|
|
1066
|
+
seconds: DurationSeconds;
|
|
1067
|
+
} | {
|
|
1068
|
+
ended?: never;
|
|
1069
|
+
seconds?: DurationSeconds;
|
|
1070
|
+
});
|
|
1071
|
+
export type HandlingReportResult = {
|
|
1072
|
+
status: "recorded";
|
|
1073
|
+
} | {
|
|
1074
|
+
status: "failed";
|
|
1075
|
+
failure: ProtocolFailure;
|
|
1076
|
+
};
|
|
1028
1077
|
export type PreferenceResult = {
|
|
1029
1078
|
status: "applied";
|
|
1030
1079
|
} | {
|
|
@@ -1168,6 +1217,8 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1168
1217
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1169
1218
|
/** Required when the login declares `capabilities.preferences`: the person's own choice, kept by the provider and republished as `authenticated`. */
|
|
1170
1219
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1220
|
+
/** Records a handling leg the host performed. Required of a connection whose tasks may declare `mute`. */
|
|
1221
|
+
recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
|
|
1171
1222
|
}
|
|
1172
1223
|
export interface Adapter<C extends Channel = Channel> {
|
|
1173
1224
|
manifest: Manifest<C>;
|
package/dist/testing.js
CHANGED
|
@@ -52,7 +52,7 @@ function observeTask(value, seen) {
|
|
|
52
52
|
seen.add("task.browsers");
|
|
53
53
|
if (some(value.attributes))
|
|
54
54
|
seen.add("task.attributes");
|
|
55
|
-
if (some(value.handlingHistory))
|
|
55
|
+
if (isRecord(value.handlingHistory) && some(value.handlingHistory.steps))
|
|
56
56
|
seen.add("task.handlingHistory");
|
|
57
57
|
if (value.consultation !== undefined)
|
|
58
58
|
seen.add("task.consultation");
|
|
@@ -289,6 +289,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
289
289
|
}
|
|
290
290
|
if (eventNamesUsers(envelope))
|
|
291
291
|
requireMethod(live, "describeUsers", "an event publishes a UserId");
|
|
292
|
+
if (eventDeclaresMute(envelope))
|
|
293
|
+
requireMethod(live, "recordStep", "a task declares mute, which the host performs and must have somewhere to record");
|
|
292
294
|
// Cross-event rules apply once the stream has a beginning: the connect snapshot.
|
|
293
295
|
if (seeded)
|
|
294
296
|
violations.push(...stream.apply(envelope), ...breaks.apply(envelope));
|
|
@@ -308,6 +310,9 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
308
310
|
requireCapabilityMethods(live, current().capabilities);
|
|
309
311
|
if (publishesUserIds(snapshot))
|
|
310
312
|
requireMethod(live, "describeUsers", "the snapshot publishes a UserId");
|
|
313
|
+
if (isRecord(snapshot) && Array.isArray(snapshot.tasks) && snapshot.tasks.some(taskDeclaresMute)) {
|
|
314
|
+
requireMethod(live, "recordStep", "a task declares mute, which the host performs and must have somewhere to record");
|
|
315
|
+
}
|
|
311
316
|
// Capacity is stated, not requested: nothing may be allocated until it is, so a connection
|
|
312
317
|
// that will not accept one is a connection nothing can be given to.
|
|
313
318
|
// The guide's obligation on a voice adapter: consult the host before declaring the agent
|
|
@@ -398,9 +403,21 @@ function publishesUserIds(snapshot) {
|
|
|
398
403
|
return snapshot.tasks.some(taskNamesUsers);
|
|
399
404
|
}
|
|
400
405
|
const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
|
|
401
|
-
const taskNamesUsers = (task) => isRecord(task) && ((Array.isArray(task.handlingHistory) && task.handlingHistory.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
406
|
+
const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.handlingHistory) && Array.isArray(task.handlingHistory.steps) && task.handlingHistory.steps.some(step => isRecord(step) && step.by !== undefined)) ||
|
|
402
407
|
(isRecord(task.lead) && task.lead.leadId !== undefined) ||
|
|
403
408
|
isRecord(task.assisting));
|
|
409
|
+
/** Mute is the leg the host performs, so a task that allows it obliges the provider to take the record. */
|
|
410
|
+
const taskDeclaresMute = (task) => isRecord(task) && isRecord(task.capabilities) && task.capabilities.mute !== undefined;
|
|
411
|
+
function eventDeclaresMute(envelope) {
|
|
412
|
+
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
413
|
+
if (!isRecord(event))
|
|
414
|
+
return false;
|
|
415
|
+
if (event.type === "task-offered" || event.type === "task-updated")
|
|
416
|
+
return taskDeclaresMute(event.task);
|
|
417
|
+
if (event.type === "snapshot" && isRecord(event.snapshot) && Array.isArray(event.snapshot.tasks))
|
|
418
|
+
return event.snapshot.tasks.some(taskDeclaresMute);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
404
421
|
/** Whether an event publishes a `UserId`, on a roster, a task, or the snapshot a reconnect carries. */
|
|
405
422
|
function eventNamesUsers(envelope) {
|
|
406
423
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
package/dist/validation.d.ts
CHANGED
|
@@ -49,9 +49,15 @@ export declare function validateEventEnvelope(envelope: unknown, manifest: unkno
|
|
|
49
49
|
* promise withheld is an absent key, never a false one, exactly as a capability is.
|
|
50
50
|
*/
|
|
51
51
|
export declare function validateHostGuarantees(guarantees: unknown, path?: string): ProtocolViolation[];
|
|
52
|
+
/**
|
|
53
|
+
* What the host reports of a leg it performed, as an adapter may validate it before forwarding:
|
|
54
|
+
* a task, a step, when it began, how long so far if the host says, and an explicit end that
|
|
55
|
+
* carries the final duration.
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateHandlingReport(report: unknown, path?: string, manifest?: unknown): ProtocolViolation[];
|
|
52
58
|
export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
|
|
53
59
|
/** The connection methods whose results `validateResult` knows. */
|
|
54
|
-
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia" | "setPreference" | "executeTeamPolicy";
|
|
60
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
|
|
55
61
|
/**
|
|
56
62
|
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
57
63
|
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|
package/dist/validation.js
CHANGED
|
@@ -312,6 +312,9 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
|
+
if (manifest.runningStepReports !== undefined) {
|
|
316
|
+
into.require(manifest.runningStepReports === true, "manifest.runningStepReports", `${path}.runningStepReports`, "runningStepReports is declared by presence, as true; a provider that takes begin and end only omits it");
|
|
317
|
+
}
|
|
315
318
|
if (manifest.orgLevels !== undefined) {
|
|
316
319
|
if (!Array.isArray(manifest.orgLevels)) {
|
|
317
320
|
into.add("manifest.orgLevels.shape", `${path}.orgLevels`, "orgLevels must be an array when present");
|
|
@@ -628,13 +631,27 @@ function validateTaskAttributes(value, path, into) {
|
|
|
628
631
|
function validateHandlingHistory(value, path, into) {
|
|
629
632
|
if (value === undefined)
|
|
630
633
|
return;
|
|
631
|
-
if (!
|
|
632
|
-
into.add("task.handlingHistory.shape", path, "handlingHistory must be an
|
|
634
|
+
if (!isPlainObject(value)) {
|
|
635
|
+
into.add("task.handlingHistory.shape", path, "handlingHistory must be an object with its steps when present");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
// What the record adds up to before this agent: each total present when the provider knows it
|
|
639
|
+
// and absent when it does not, never a plausible nought.
|
|
640
|
+
for (const field of ["handleSeconds", "holdSeconds", "queueSeconds"]) {
|
|
641
|
+
if (value[field] !== undefined) {
|
|
642
|
+
into.require(isDurationSeconds(value[field]), `task.handlingHistory.${field}`, `${path}.${field}`, `${field} must be a whole number of seconds, zero or more, or omitted when unknown`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (value.transfers !== undefined) {
|
|
646
|
+
into.require(typeof value.transfers === "number" && Number.isInteger(value.transfers) && value.transfers >= 0, "task.handlingHistory.transfers", `${path}.transfers`, "transfers must be a whole number, zero or more, or omitted when unknown");
|
|
647
|
+
}
|
|
648
|
+
if (!Array.isArray(value.steps)) {
|
|
649
|
+
into.add("task.handlingHistory.steps.shape", `${path}.steps`, "handlingHistory carries its steps as an array, empty when the task has had none");
|
|
633
650
|
return;
|
|
634
651
|
}
|
|
635
652
|
let previous;
|
|
636
|
-
value.forEach((entry, index) => {
|
|
637
|
-
const at = `${path}[${index}]`;
|
|
653
|
+
value.steps.forEach((entry, index) => {
|
|
654
|
+
const at = `${path}.steps[${index}]`;
|
|
638
655
|
if (!isPlainObject(entry)) {
|
|
639
656
|
into.add("task.handlingHistory.entry", at, "each handling step must be an object");
|
|
640
657
|
return;
|
|
@@ -1400,6 +1417,35 @@ export function validateHostGuarantees(guarantees, path = "host.guarantees") {
|
|
|
1400
1417
|
}
|
|
1401
1418
|
return into.violations;
|
|
1402
1419
|
}
|
|
1420
|
+
/**
|
|
1421
|
+
* What the host reports of a leg it performed, as an adapter may validate it before forwarding:
|
|
1422
|
+
* a task, a step, when it began, how long so far if the host says, and an explicit end that
|
|
1423
|
+
* carries the final duration.
|
|
1424
|
+
*/
|
|
1425
|
+
export function validateHandlingReport(report, path = "handlingReport", manifest) {
|
|
1426
|
+
const into = new Collector();
|
|
1427
|
+
if (!isPlainObject(report)) {
|
|
1428
|
+
into.add("handlingReport.shape", path, "a handling report must be an object");
|
|
1429
|
+
return into.violations;
|
|
1430
|
+
}
|
|
1431
|
+
into.require(isTaskId(report.taskId), "handlingReport.taskId", `${path}.taskId`, "a report names the task");
|
|
1432
|
+
into.oneOf(report.step, HANDLING_STEPS, "handlingReport.step", `${path}.step`);
|
|
1433
|
+
into.timestamp(report.at, "handlingReport.at", `${path}.at`);
|
|
1434
|
+
if (report.seconds !== undefined) {
|
|
1435
|
+
into.require(isDurationSeconds(report.seconds) && report.seconds > 0, "handlingReport.seconds", `${path}.seconds`, "seconds must be a positive whole number; omit it rather than report nought");
|
|
1436
|
+
}
|
|
1437
|
+
if (report.ended !== undefined) {
|
|
1438
|
+
if (into.require(report.ended === true, "handlingReport.ended", `${path}.ended`, "ended is declared by presence, as true; a running leg omits it")) {
|
|
1439
|
+
into.require(report.seconds !== undefined, "handlingReport.ended.seconds", `${path}.seconds`, "an ended leg states its final duration");
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
else if (report.seconds !== undefined && manifest !== undefined) {
|
|
1443
|
+
// A running report crosses only to a provider that asked for one: begin and end are the whole
|
|
1444
|
+
// of what the rest receive.
|
|
1445
|
+
into.require(isPlainObject(manifest) && manifest.runningStepReports === true, "handlingReport.running.unexpected", `${path}.seconds`, "this provider takes begin and end only; a running report was never asked for");
|
|
1446
|
+
}
|
|
1447
|
+
return into.violations;
|
|
1448
|
+
}
|
|
1403
1449
|
export function validateHostReport(report, path = "host") {
|
|
1404
1450
|
const into = new Collector();
|
|
1405
1451
|
if (!isPlainObject(report)) {
|
|
@@ -1465,6 +1511,7 @@ const RESULT_STATUSES = {
|
|
|
1465
1511
|
executeTeamConsult: { success: "applied", failure: "failed" },
|
|
1466
1512
|
openMedia: { success: "opened", failure: "unavailable" },
|
|
1467
1513
|
setPreference: { success: "applied", failure: "failed" },
|
|
1514
|
+
recordStep: { success: "recorded", failure: "failed" },
|
|
1468
1515
|
executeTeamPolicy: { success: "applied", failure: "failed" },
|
|
1469
1516
|
};
|
|
1470
1517
|
/**
|
package/guide.md
CHANGED
|
@@ -178,6 +178,7 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
178
178
|
phaseLabels?: TaskPhaseLabels;
|
|
179
179
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
180
180
|
orgLevels?: LevelDeclaration[];
|
|
181
|
+
runningStepReports?: true;
|
|
181
182
|
};
|
|
182
183
|
```
|
|
183
184
|
|
|
@@ -360,6 +361,15 @@ type SetPreferenceRequest =
|
|
|
360
361
|
| { id: PreferenceId; enabled: boolean }
|
|
361
362
|
| { id: PreferenceId; inherit: true };
|
|
362
363
|
|
|
364
|
+
type HandlingReport = { taskId: TaskId; step: HandlingStep; at: IsoTimestamp } & (
|
|
365
|
+
| { ended: true; seconds: DurationSeconds }
|
|
366
|
+
| { ended?: never; seconds?: DurationSeconds }
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
type HandlingReportResult =
|
|
370
|
+
| { status: "recorded" }
|
|
371
|
+
| { status: "failed"; failure: ProtocolFailure };
|
|
372
|
+
|
|
363
373
|
type PreferenceResult =
|
|
364
374
|
| { status: "applied" }
|
|
365
375
|
| { status: "failed"; failure: ProtocolFailure };
|
|
@@ -549,6 +559,14 @@ type HandlingStep =
|
|
|
549
559
|
| "conferenced"
|
|
550
560
|
| "unanswered";
|
|
551
561
|
|
|
562
|
+
type TaskHandlingHistory = {
|
|
563
|
+
steps: TaskHandlingStep[];
|
|
564
|
+
handleSeconds?: DurationSeconds;
|
|
565
|
+
holdSeconds?: DurationSeconds;
|
|
566
|
+
queueSeconds?: DurationSeconds;
|
|
567
|
+
transfers?: number;
|
|
568
|
+
};
|
|
569
|
+
|
|
552
570
|
type TaskHandlingStep = {
|
|
553
571
|
step: HandlingStep;
|
|
554
572
|
at: IsoTimestamp;
|
|
@@ -607,7 +625,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
607
625
|
acceptance?: AcceptanceMode;
|
|
608
626
|
reference?: string;
|
|
609
627
|
attributes?: TaskAttribute[];
|
|
610
|
-
handlingHistory?:
|
|
628
|
+
handlingHistory?: TaskHandlingHistory;
|
|
611
629
|
} & TaskCompletion & (
|
|
612
630
|
C extends "voice"
|
|
613
631
|
? { consultation?: TaskConsultation; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
|
|
@@ -990,6 +1008,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
990
1008
|
executeTeamConsult?(request: TeamConsultCommandRequest): Promise<TeamCommandResult>;
|
|
991
1009
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
992
1010
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1011
|
+
recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
|
|
993
1012
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
994
1013
|
};
|
|
995
1014
|
|
|
@@ -1382,6 +1401,7 @@ compile time.
|
|
|
1382
1401
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1383
1402
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1384
1403
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1404
|
+
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1385
1405
|
|
|
1386
1406
|
### Authentication methods
|
|
1387
1407
|
|
|
@@ -1886,6 +1906,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1886
1906
|
| `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
|
|
1887
1907
|
| `executeTeamConsult(command)` | The login declares `capabilities.team.consultControl`. |
|
|
1888
1908
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
1909
|
+
| `recordStep(report)` | A task declares `mute`: the host performs that leg, and the provider's record has to have somewhere to take it. See **The host records what it performs**. |
|
|
1889
1910
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
1890
1911
|
| `openMedia(request)` | The manifest channel is `voice`. Every voice task's audio lands in Omni, so there is no voice adapter that does not implement it. |
|
|
1891
1912
|
|
|
@@ -2090,7 +2111,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2090
2111
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2091
2112
|
| `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. |
|
|
2092
2113
|
| `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. |
|
|
2093
|
-
| `handlingHistory` |
|
|
2114
|
+
| `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**. |
|
|
2094
2115
|
| `consultation` | Voice only. Present while the agent is consulting a transfer destination: who is being consulted, and since when where the provider records it. Its presence is what makes `transfer` `complete` and `cancel` issuable. `label` is a name for the destination -- a person, a queue -- not a phrase; the host supplies the verb. See **Consult transfer**. |
|
|
2095
2116
|
| `lead` | 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 **Consulting a lead**. |
|
|
2096
2117
|
| `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` `take-over` and `leave` issuable. See **Consulting a lead**. |
|
|
@@ -2254,15 +2275,23 @@ const immediateProviderCompletion = {
|
|
|
2254
2275
|
|
|
2255
2276
|
### How a task has been handled
|
|
2256
2277
|
|
|
2257
|
-
`Task.handlingHistory` is the
|
|
2278
|
+
`Task.handlingHistory` is the call record: the steps that brought the task to the agent, oldest
|
|
2279
|
+
first, and what they add up to before this agent:
|
|
2258
2280
|
|
|
2259
2281
|
```ts
|
|
2260
|
-
handlingHistory:
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2282
|
+
handlingHistory: {
|
|
2283
|
+
steps: [
|
|
2284
|
+
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2285
|
+
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
2286
|
+
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
2287
|
+
{ step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
|
|
2288
|
+
{ step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
|
|
2289
|
+
],
|
|
2290
|
+
handleSeconds: 312, // handled by others before this agent
|
|
2291
|
+
holdSeconds: 35, // holds others put the caller on
|
|
2292
|
+
queueSeconds: 41, // waiting before anyone answered
|
|
2293
|
+
transfers: 1, // hands the call has changed
|
|
2294
|
+
}
|
|
2266
2295
|
```
|
|
2267
2296
|
|
|
2268
2297
|
**This is not an archive.** It is live data about a task that is still open: it travels with the task
|
|
@@ -2321,8 +2350,46 @@ A host must render the two differently. Showing an unattributed `answered` the s
|
|
|
2321
2350
|
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2322
2351
|
*"not recorded"* in the place the name would go.
|
|
2323
2352
|
|
|
2324
|
-
Omit `handlingHistory` entirely when the provider cannot observe the steps.
|
|
2325
|
-
claim — it says the task has had none.
|
|
2353
|
+
Omit `handlingHistory` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2354
|
+
different claim — it says the task has had none.
|
|
2355
|
+
|
|
2356
|
+
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
2357
|
+
carrying what others already used of it, and the agent reads that before they say hello: the
|
|
2358
|
+
totals are the provider's own — never a desk summing instants that may be rounded or skewed, for
|
|
2359
|
+
the same reason `seconds` is reported and not derived — and each is present when the provider
|
|
2360
|
+
knows it and absent when it does not, never a plausible nought. A fresh call from the queue has
|
|
2361
|
+
`steps` with no prior `answered` and no totals to state, and a desk shows nothing rather than
|
|
2362
|
+
zeros. The record is restated with the task on every `task-updated` and snapshot, so an entry
|
|
2363
|
+
that arrives late corrects the sums. These four are what every platform reports; more will be
|
|
2364
|
+
added here as a need is shown, not invented ahead of one.
|
|
2365
|
+
|
|
2366
|
+
#### The host records what it performs
|
|
2367
|
+
|
|
2368
|
+
`muted` is the one leg Omni performs rather than the provider, and a record kept by the provider
|
|
2369
|
+
would have a hole exactly there. So the host reports it, through `recordStep`, and the provider
|
|
2370
|
+
writes it into its record as it writes every other leg:
|
|
2371
|
+
|
|
2372
|
+
```ts
|
|
2373
|
+
declare const connection: Connection<"voice">;
|
|
2374
|
+
declare const taskId: TaskId;
|
|
2375
|
+
declare const at: IsoTimestamp;
|
|
2376
|
+
|
|
2377
|
+
void connection.recordStep?.({ taskId, step: "muted", at }); // the moment the agent mutes
|
|
2378
|
+
void connection.recordStep?.({ taskId, step: "muted", at, seconds: 15 }); // still running, if the host chooses to say
|
|
2379
|
+
void connection.recordStep?.({ taskId, step: "muted", at, seconds: 42, ended: true }); // the moment they unmute
|
|
2380
|
+
```
|
|
2381
|
+
|
|
2382
|
+
The entry is keyed by `step` and `at`, so every report about one leg names the same instant. The
|
|
2383
|
+
host is the authority for the legs it performs, so it may say how long so far — `seconds` is
|
|
2384
|
+
elapsed while the leg runs and final once it has ended — and **the end is stated, never
|
|
2385
|
+
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
2386
|
+
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
2387
|
+
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
2388
|
+
leg, when it began and when it ended, and `validateHandlingReport(report, path, manifest)` refuses
|
|
2389
|
+
a running one it was never asked for (`handlingReport.running.unexpected`). What a provider that
|
|
2390
|
+
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
2391
|
+
`handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
|
|
2392
|
+
`recordStep` is required of a connection whose tasks declare `mute`, and answers `recorded`.
|
|
2326
2393
|
|
|
2327
2394
|
### Browser capability
|
|
2328
2395
|
|
|
@@ -2718,6 +2785,7 @@ everywhere; success is not.
|
|
|
2718
2785
|
| Method | Succeeded |
|
|
2719
2786
|
| --- | --- |
|
|
2720
2787
|
| `setCapacity` | `applied` |
|
|
2788
|
+
| `recordStep` | `recorded` |
|
|
2721
2789
|
| `requestBreak` | `requested` |
|
|
2722
2790
|
| `commitBreak` | `committed` |
|
|
2723
2791
|
| `cancelBreak` | `cancelled` |
|
|
@@ -3714,6 +3782,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3714
3782
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
3715
3783
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
3716
3784
|
| `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
|
|
3785
|
+
| `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
3717
3786
|
| `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
3718
3787
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
3719
3788
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|