@xema/omni-protocol 0.1.99 → 0.1.100
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 +33 -33
- package/dist/index.js +3 -3
- package/dist/testing.d.ts +12 -12
- package/dist/testing.js +107 -107
- package/dist/validation.d.ts +1 -1
- package/dist/validation.js +69 -69
- package/guide.md +186 -180
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "r
|
|
|
106
106
|
export declare const DIAL_OUTCOMES: readonly ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled", "unexplained"];
|
|
107
107
|
/**
|
|
108
108
|
* How the agent hears the call: on a `softphone`, where the host owns the audio and opens it
|
|
109
|
-
* through `
|
|
109
|
+
* through `openAudio`, or on a `deskPhone`, a handset the platform rings, where the host opens
|
|
110
110
|
* nothing and only shows the call. A voice manifest lists the phones its platform supports, and
|
|
111
111
|
* the host picks one for the login at authentication.
|
|
112
112
|
*/
|
|
@@ -433,7 +433,7 @@ export interface RecordingActions {
|
|
|
433
433
|
export interface TaskRecordingPolicy {
|
|
434
434
|
provider?: RecordingActions;
|
|
435
435
|
host?: RecordingActions & {
|
|
436
|
-
|
|
436
|
+
storageId: string;
|
|
437
437
|
};
|
|
438
438
|
}
|
|
439
439
|
/** Current evidence only. Expiry/disconnect means unknown, never a historical stop. */
|
|
@@ -478,7 +478,7 @@ export interface HostRecording {
|
|
|
478
478
|
announcesToCaller?: true;
|
|
479
479
|
actions: RecordingAction[];
|
|
480
480
|
/** Explicitly provisioned destinations. A task chooses one; no upload/storage fallback. */
|
|
481
|
-
|
|
481
|
+
storageIds: string[];
|
|
482
482
|
execute(request: HostRecordingRequest): Promise<RecordingCommandResult>;
|
|
483
483
|
}
|
|
484
484
|
/** Recording applies or fails; it never dials. Rejection without result means unknown. */
|
|
@@ -729,9 +729,9 @@ export type TaskPhase =
|
|
|
729
729
|
/** What the system does when a preview's deadline passes with no Call pressed. */
|
|
730
730
|
export type PreviewDeadline =
|
|
731
731
|
/** Provider initiates dialing when preparation ends; never a host timer command. */
|
|
732
|
-
"
|
|
732
|
+
"provider-dials"
|
|
733
733
|
/** Host initiates the ordinary call command when preparation ends. */
|
|
734
|
-
| "host-
|
|
734
|
+
| "host-dials"
|
|
735
735
|
/** Preparation target has elapsed; remain in preview until the agent presses Call. */
|
|
736
736
|
| "waits";
|
|
737
737
|
/** Who ends the task: the agent issuing `complete`, or the provider deciding it is over. */
|
|
@@ -917,12 +917,12 @@ export interface Locked {
|
|
|
917
917
|
*/
|
|
918
918
|
export type Lockable<T> = T | Locked;
|
|
919
919
|
/**
|
|
920
|
-
* Who chose a task's capabilities. `
|
|
920
|
+
* Who chose a task's capabilities. `not-yet-read` is a fault the provider states on the task rather
|
|
921
921
|
* than hides: the platform's terms could not be read, and the set published is what the provider
|
|
922
922
|
* will honour, not what the platform permits.
|
|
923
923
|
*/
|
|
924
|
-
export type CapabilitySource = "queue" | "
|
|
925
|
-
export declare const CAPABILITY_SOURCES: readonly ["queue", "
|
|
924
|
+
export type CapabilitySource = "queue" | "nobody" | "not-yet-read";
|
|
925
|
+
export declare const CAPABILITY_SOURCES: readonly ["queue", "nobody", "not-yet-read"];
|
|
926
926
|
export type Task<C extends Channel = Channel> = {
|
|
927
927
|
/** The assignment this task is the record of, and its one identity. See `AssignmentId`. */
|
|
928
928
|
assignmentId: AssignmentId;
|
|
@@ -933,8 +933,8 @@ export type Task<C extends Channel = Channel> = {
|
|
|
933
933
|
capabilities: TaskCapabilities<C>;
|
|
934
934
|
/**
|
|
935
935
|
* Where the capabilities came from before the provider put them on the task, so a host can tell
|
|
936
|
-
* a fact from a fault: `queue` when somebody chose these terms, `
|
|
937
|
-
* the work over, `
|
|
936
|
+
* a fact from a fault: `queue` when somebody chose these terms, `nobody` when nothing handed
|
|
937
|
+
* the work over, `not-yet-read` when a queue was named and its terms could not be read -- the
|
|
938
938
|
* provider then publishes what it will honour and says so here, where the controls are drawn.
|
|
939
939
|
*/
|
|
940
940
|
capabilitySource: CapabilitySource;
|
|
@@ -953,7 +953,7 @@ export type Task<C extends Channel = Channel> = {
|
|
|
953
953
|
acceptance?: AcceptanceMode;
|
|
954
954
|
/**
|
|
955
955
|
* In `preview` only, and together: the preparation target and behavior when it elapses,
|
|
956
|
-
* and who acts then -- `
|
|
956
|
+
* and who acts then -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the
|
|
957
957
|
* host issue Call, and `waits` keeps waiting for the agent without dialing or ending the task.
|
|
958
958
|
* Absent, the agent has as long as they need, without a preparation countdown.
|
|
959
959
|
*/
|
|
@@ -970,13 +970,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
970
970
|
onCall?: OnCall[];
|
|
971
971
|
leadAssist?: TaskLeadAssist;
|
|
972
972
|
takenOver?: TaskTakenOver;
|
|
973
|
-
|
|
973
|
+
audio?: TaskAudioState;
|
|
974
974
|
} : {
|
|
975
975
|
recording?: never;
|
|
976
976
|
onCall?: never;
|
|
977
977
|
leadAssist?: never;
|
|
978
978
|
takenOver?: never;
|
|
979
|
-
|
|
979
|
+
audio?: never;
|
|
980
980
|
});
|
|
981
981
|
/**
|
|
982
982
|
* What the provider wants of Omni's acceptance policy for one offer. On routed work, present only
|
|
@@ -1014,7 +1014,7 @@ export type TaskOutcome = {
|
|
|
1014
1014
|
failure: ProtocolFailure;
|
|
1015
1015
|
};
|
|
1016
1016
|
export declare const TASK_COMMAND_NAMES: {
|
|
1017
|
-
readonly voice: readonly ["answer", "decline", "
|
|
1017
|
+
readonly voice: readonly ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
|
|
1018
1018
|
readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
|
|
1019
1019
|
readonly email: readonly ["accept", "decline", "complete"];
|
|
1020
1020
|
};
|
|
@@ -1030,7 +1030,7 @@ export type VoiceTaskCommand = {
|
|
|
1030
1030
|
}
|
|
1031
1031
|
/** In `preview`: place the call to the party whose record the agent has read. A dial, gated by the phase alone. */
|
|
1032
1032
|
| {
|
|
1033
|
-
type: "
|
|
1033
|
+
type: "dial";
|
|
1034
1034
|
dialId: DialId;
|
|
1035
1035
|
} | {
|
|
1036
1036
|
type: "hold";
|
|
@@ -1256,8 +1256,8 @@ export type BreakEndResult = {
|
|
|
1256
1256
|
status: "failed";
|
|
1257
1257
|
failure: ProtocolFailure;
|
|
1258
1258
|
};
|
|
1259
|
-
/** `
|
|
1260
|
-
export type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "
|
|
1259
|
+
/** `elsewhere`: signed in here, and the host holds this agent's capacity for another provider (`count: 0`, host-stopped); not receiving this provider's work. */
|
|
1260
|
+
export type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "elsewhere" | "signed-out";
|
|
1261
1261
|
export interface TeamMember {
|
|
1262
1262
|
id: UserId;
|
|
1263
1263
|
availability: TeamMemberAvailability;
|
|
@@ -1302,12 +1302,12 @@ export type MemberTask<C extends Channel = Channel> = {
|
|
|
1302
1302
|
onCall?: OnCall[];
|
|
1303
1303
|
leadAssist?: TaskLeadAssist;
|
|
1304
1304
|
takenOver?: TaskTakenOver;
|
|
1305
|
-
|
|
1305
|
+
audio?: TaskAudioState;
|
|
1306
1306
|
} : {
|
|
1307
1307
|
onCall?: never;
|
|
1308
1308
|
leadAssist?: never;
|
|
1309
1309
|
takenOver?: never;
|
|
1310
|
-
|
|
1310
|
+
audio?: never;
|
|
1311
1311
|
});
|
|
1312
1312
|
/** The lead on this member's call, in the mode they are heard, since when. */
|
|
1313
1313
|
export interface MemberListening {
|
|
@@ -1425,20 +1425,20 @@ export type TeamCommandResult = {
|
|
|
1425
1425
|
* `ended` once primary interaction's audio ended, and the field omitted while none should be. The
|
|
1426
1426
|
* provider's word -- a desk attaches and renders audio from it, never from its own senses.
|
|
1427
1427
|
*/
|
|
1428
|
-
export type
|
|
1429
|
-
export interface
|
|
1428
|
+
export type TaskAudioState = "started" | "ended";
|
|
1429
|
+
export interface CallAudio {
|
|
1430
1430
|
remoteAudio: MediaStream;
|
|
1431
1431
|
setMuted(muted: boolean): void;
|
|
1432
1432
|
close(): void;
|
|
1433
1433
|
}
|
|
1434
|
-
export interface
|
|
1434
|
+
export interface OpenAudioRequest {
|
|
1435
1435
|
assignmentId: AssignmentId;
|
|
1436
1436
|
/** The agent's microphone as Omni captured it, `HostReport.audio.input.localAudio`; absent while that input is `unavailable`. */
|
|
1437
1437
|
localAudio?: MediaStream;
|
|
1438
1438
|
}
|
|
1439
|
-
export type
|
|
1439
|
+
export type OpenAudioResult = {
|
|
1440
1440
|
status: "opened";
|
|
1441
|
-
|
|
1441
|
+
audio: CallAudio;
|
|
1442
1442
|
} | {
|
|
1443
1443
|
status: "unavailable";
|
|
1444
1444
|
failure: ProtocolFailure;
|
|
@@ -1530,7 +1530,7 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1530
1530
|
/** The provider's own count of those tasks, stated rather than inferred: it must equal `tasks.length`, so a blank or unanswered state can never pass as a confirmed empty. */
|
|
1531
1531
|
taskCount: number;
|
|
1532
1532
|
contacts?: Contact[];
|
|
1533
|
-
|
|
1533
|
+
calendar?: ScheduledActivity[];
|
|
1534
1534
|
team?: TeamMembers;
|
|
1535
1535
|
}
|
|
1536
1536
|
/**
|
|
@@ -1540,7 +1540,7 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1540
1540
|
* tasks against it and needs no new signal when one ends. The agent is one person on several
|
|
1541
1541
|
* providers, and the host divides their capacity among them: `0` is host-stopped -- the agent's
|
|
1542
1542
|
* capacity is elsewhere for now, this provider assigns nothing and shows the member as
|
|
1543
|
-
* `
|
|
1543
|
+
* `elsewhere` -- and is not a break, which is the agent not working at all. The host restates a
|
|
1544
1544
|
* count of one or more when this provider has the agent's capacity again.
|
|
1545
1545
|
*/
|
|
1546
1546
|
export interface AgentCapacity {
|
|
@@ -1583,23 +1583,23 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1583
1583
|
type: "task-updated";
|
|
1584
1584
|
task: Task<C>;
|
|
1585
1585
|
} | {
|
|
1586
|
-
type: "task-
|
|
1586
|
+
type: "task-audio-started";
|
|
1587
1587
|
assignmentId: AssignmentId;
|
|
1588
1588
|
} | {
|
|
1589
|
-
type: "task-
|
|
1589
|
+
type: "task-audio-ended";
|
|
1590
1590
|
assignmentId: AssignmentId;
|
|
1591
1591
|
}
|
|
1592
1592
|
/**
|
|
1593
1593
|
* The lead's channel is in a member's call -- listening, coaching or joined -- and its audio should
|
|
1594
|
-
* attach: on a softphone the application opens
|
|
1594
|
+
* attach: on a softphone the application opens audio on the member's assignment as it does for a
|
|
1595
1595
|
* task. Voice, to a login that declares `lead`, never to the member.
|
|
1596
1596
|
*/
|
|
1597
1597
|
| {
|
|
1598
|
-
type: "team-
|
|
1598
|
+
type: "team-audio-started";
|
|
1599
1599
|
memberId: UserId;
|
|
1600
1600
|
assignmentId: AssignmentId;
|
|
1601
1601
|
} | {
|
|
1602
|
-
type: "team-
|
|
1602
|
+
type: "team-audio-ended";
|
|
1603
1603
|
memberId: UserId;
|
|
1604
1604
|
assignmentId: AssignmentId;
|
|
1605
1605
|
} | {
|
|
@@ -1643,7 +1643,7 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1643
1643
|
contacts: Contact[];
|
|
1644
1644
|
} | {
|
|
1645
1645
|
type: "calendar-updated";
|
|
1646
|
-
|
|
1646
|
+
calendar: ScheduledActivity[];
|
|
1647
1647
|
};
|
|
1648
1648
|
/**
|
|
1649
1649
|
* The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
|
|
@@ -1718,7 +1718,7 @@ export interface Connection<C extends Channel = Channel> {
|
|
|
1718
1718
|
/** Required when the login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. */
|
|
1719
1719
|
executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
|
|
1720
1720
|
/** Required of a voice adapter whose manifest lists `softphone`: on one, the call's audio lands in Omni. */
|
|
1721
|
-
|
|
1721
|
+
openAudio?(request: OpenAudioRequest): Promise<OpenAudioResult>;
|
|
1722
1722
|
/** Required when the login declares `capabilities.preferences`: the person's own choice, kept by the provider and republished as `authenticated`. */
|
|
1723
1723
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1724
1724
|
/** Records an interaction leg the host performed. Required of a softphone login's connection: the host mutes its microphone on any call, and the record is the provider's. */
|
package/dist/index.js
CHANGED
|
@@ -71,19 +71,19 @@ export const DEFAULT_LEVELS = [
|
|
|
71
71
|
export function effectiveLevels(declared) {
|
|
72
72
|
return [...(declared ?? DEFAULT_LEVELS)];
|
|
73
73
|
}
|
|
74
|
-
export const CAPABILITY_SOURCES = ["queue", "
|
|
74
|
+
export const CAPABILITY_SOURCES = ["queue", "nobody", "not-yet-read"];
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
// Task commands.
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
78
78
|
export const TASK_COMMAND_NAMES = {
|
|
79
|
-
voice: ["answer", "decline", "
|
|
79
|
+
voice: ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call",
|
|
80
80
|
"connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
|
|
81
81
|
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
82
82
|
email: ["accept", "decline", "complete"],
|
|
83
83
|
};
|
|
84
84
|
/** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
|
|
85
85
|
export function commandDialId(command) {
|
|
86
|
-
if (command.type === "connect-back" || command.type === "
|
|
86
|
+
if (command.type === "connect-back" || command.type === "dial")
|
|
87
87
|
return command.dialId;
|
|
88
88
|
if (command.type === "transfer" && (command.action === "cold" || command.action === "warm"))
|
|
89
89
|
return command.dialId;
|
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 team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "contacts", "calendar", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
|
@@ -41,7 +41,7 @@ export interface ExerciseAdapterOptions {
|
|
|
41
41
|
collectOnly?: boolean;
|
|
42
42
|
/**
|
|
43
43
|
* Drive one ordinary lifecycle on the first task the provider offers -- accept it, open its
|
|
44
|
-
*
|
|
44
|
+
* audio on a softphone, hold and resume where offered, end the call where offered, complete it
|
|
45
45
|
* where the agent completes -- so the rules about a live call are reached rather than listed
|
|
46
46
|
* under `notExercised`. Off by default, since it issues commands against whatever platform the
|
|
47
47
|
* adapter is connected to: turn it on against a test backend.
|
|
@@ -177,9 +177,9 @@ export declare class TaskStream {
|
|
|
177
177
|
* Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
|
|
178
178
|
* state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
|
|
179
179
|
* get to forget it. A task it carries that the stream already held may not go backwards, lose an
|
|
180
|
-
* entry of its record, return to
|
|
181
|
-
* held up:
|
|
182
|
-
* paused without its
|
|
180
|
+
* entry of its record, return to not-yet-read terms, or be at work without the audio the stream
|
|
181
|
+
* held up: audio ends on task-audio-ended and the call moves on, so a task still in-progress or
|
|
182
|
+
* paused without its audio is a snapshot that lost state, not a call that ended.
|
|
183
183
|
*/
|
|
184
184
|
resync(snapshot: unknown, at: string): ProtocolViolation[];
|
|
185
185
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
@@ -205,12 +205,12 @@ export declare class BreakStream {
|
|
|
205
205
|
*/
|
|
206
206
|
export declare function assertBreakFollowsItsRequests(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
|
|
207
207
|
/**
|
|
208
|
-
* The
|
|
209
|
-
* with the snapshot it began from -- every task is introduced once, `task-
|
|
210
|
-
* `task-
|
|
211
|
-
* follows the
|
|
208
|
+
* The audio follows the task and never decides it. Given a provider's stream -- optionally seeded
|
|
209
|
+
* with the snapshot it began from -- every task is introduced once, `task-audio-started` and
|
|
210
|
+
* `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what
|
|
211
|
+
* follows the audio ending is `completing` or `task-ended`.
|
|
212
212
|
*/
|
|
213
|
-
export declare function
|
|
213
|
+
export declare function assertAudioFollowsTheTask(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
|
|
214
214
|
/** A login store that lives in memory for the test: what most adapter tests hand `exerciseAdapter` as `context.store`. */
|
|
215
215
|
export declare function memoryStore(): LoginStore;
|
|
216
216
|
/**
|
|
@@ -252,14 +252,14 @@ export interface BreakOnTaskStep {
|
|
|
252
252
|
*/
|
|
253
253
|
export declare function assertBreakBeginsAfterTask(steps: readonly BreakOnTaskStep[]): void;
|
|
254
254
|
/**
|
|
255
|
-
* Validates the deadline derived from
|
|
255
|
+
* Validates the deadline derived from audio end and the task's fixed wrap allowance.
|
|
256
256
|
*
|
|
257
257
|
* A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
|
|
258
258
|
* host counting down what the provider left open is the violation, and so is a host counting
|
|
259
259
|
* nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
|
|
260
260
|
* implementation; pass 0 to demand an exact match.
|
|
261
261
|
*/
|
|
262
|
-
export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "wrapAllowance">,
|
|
262
|
+
export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "wrapAllowance">, audioEndedAt: string, observedDeadline: string | undefined, toleranceMs?: number): void;
|
|
263
263
|
/** One browser in one task of one provider. `providerId` is `Manifest.id`, never `displayName`. */
|
|
264
264
|
export type BrowserIsolationScenario = BrowserSessionKeyInput;
|
|
265
265
|
/** Validates whether two task-browser definitions should share one browser session. */
|