@xema/omni-protocol 0.1.98 → 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 +155 -133
- package/dist/index.js +7 -17
- package/dist/testing.d.ts +12 -12
- package/dist/testing.js +147 -133
- package/dist/validation.d.ts +20 -6
- package/dist/validation.js +238 -265
- package/guide.md +406 -449
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -23,7 +23,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
23
23
|
| **Provider** | One independently connected external system: a voice platform, a chat platform, a mail platform. |
|
|
24
24
|
| **Adapter** | The package implementing this contract for one provider. One adapter is one provider, so the words are often interchangeable; *provider* names the system, *adapter* the code speaking for it. |
|
|
25
25
|
| **Agent** | The person signed in and taking work. On `onCall`, `agent` is a person on the call by user id; transfer destinations may include an agent when the provider publishes that directory item. |
|
|
26
|
-
| **Lead** | An agent whose login declares `capabilities.
|
|
26
|
+
| **Lead** | An agent whose login declares `capabilities.lead`. The flag alone turns on the team feature in the agent application: the lead sees their members with their tasks and stats, and acts on them with the authority the flag carries. While the feature is on, the provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
|
|
27
27
|
| **Local policy** | Rules configured by the agent application about this agent, outside the provider protocol and never sent to a provider. It gates whether an offer may be rejected, whether the agent goes ready on login, and whether tasks are auto-accepted. Where a capability and local policy disagree, the stricter wins. |
|
|
28
28
|
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. The protocol never describes the IVR or the queue; it carries the call's history and details to whichever agent holds it now. |
|
|
29
29
|
| **Assignment** | The call routed to this agent, from the offer until the task ends. It is the one thing that crosses: the provider assigns, names the assignment with `assignmentId`, and that is the only name the desk ever uses back. A call that comes back is a new assignment on the same call, and a declined or lapsed offer is an assignment that became nothing more. What the platform calls its own record, and whether it reuses that name, is the adapter's business and unknown to the desk. |
|
|
@@ -41,7 +41,7 @@ are used precisely throughout and mean nothing looser here.
|
|
|
41
41
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
42
42
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, team member list. |
|
|
43
43
|
| **Dial** | One outbound call the agent application asks a provider to place — from the idle dialpad, a cold or warm transfer, a conference add, or a connect-back. Identified by the agent application's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
44
|
-
| **Listen** | A lead
|
|
44
|
+
| **Listen** | A lead's channel in a member's call, unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. It is a team act naming the member, never a task on the lead's desk; nothing of it reaches the member's task. A lead who wants the call takes it over. See **Listening to a call**. |
|
|
45
45
|
| **On the call** | Who a voice task's audio joins, or is bringing in, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in from the moment their dial is placed. |
|
|
46
46
|
|
|
47
47
|
These fields describe state within their containing objects. Both authentication and break
|
|
@@ -50,7 +50,7 @@ state use `status`; name the object when discussing them to avoid ambiguity.
|
|
|
50
50
|
| Word | Belongs to | Values |
|
|
51
51
|
| --- | --- | --- |
|
|
52
52
|
| `phase` | A task | `pending`, `confirmed`, `preview`, `in-progress`, `paused`, `completing` |
|
|
53
|
-
| `
|
|
53
|
+
| `audio` | A task's audio | `started`, `ended` |
|
|
54
54
|
| `transport` | A connection | `connecting`, `active`, `error` |
|
|
55
55
|
| `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
|
|
56
56
|
| `status` | The agent’s break lifecycle | `not-requested`, `awaiting-approval`, `granted`, `starting-after-task`, `on-break` |
|
|
@@ -252,17 +252,10 @@ type AuthenticationContext = {
|
|
|
252
252
|
|
|
253
253
|
type ListeningMode = "listen" | "coach" | "join-call";
|
|
254
254
|
|
|
255
|
-
type TeamCapabilities = {
|
|
256
|
-
breakControl?: true;
|
|
257
|
-
leadAssistControl?: true;
|
|
258
|
-
listeningControl?: ListeningMode[];
|
|
259
|
-
policyControl?: true;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
255
|
type UserCapabilities = {
|
|
263
256
|
breaks?: true;
|
|
264
257
|
preferences?: AgentPreference[];
|
|
265
|
-
|
|
258
|
+
lead?: true;
|
|
266
259
|
};
|
|
267
260
|
|
|
268
261
|
type AuthenticationState =
|
|
@@ -309,7 +302,7 @@ interface RecordingActions {
|
|
|
309
302
|
}
|
|
310
303
|
interface TaskRecordingPolicy {
|
|
311
304
|
provider?: RecordingActions;
|
|
312
|
-
host?: RecordingActions & {
|
|
305
|
+
host?: RecordingActions & { storageId: string };
|
|
313
306
|
}
|
|
314
307
|
type RecordingState =
|
|
315
308
|
| { status: "unknown"; observationId?: never; observedAt?: never; validUntil?: never; recordingId?: never }
|
|
@@ -333,7 +326,7 @@ interface HostRecordingReport {
|
|
|
333
326
|
interface HostRecording {
|
|
334
327
|
announcesToCaller?: true;
|
|
335
328
|
actions: RecordingAction[];
|
|
336
|
-
|
|
329
|
+
storageIds: string[];
|
|
337
330
|
execute(request: HostRecordingRequest): Promise<RecordingCommandResult>;
|
|
338
331
|
}
|
|
339
332
|
type RecordingCommandResult = Exclude<TaskCommandResult, { status: "dialling" }>;
|
|
@@ -486,7 +479,7 @@ type Snapshot = {
|
|
|
486
479
|
tasks: Task[];
|
|
487
480
|
taskCount: number;
|
|
488
481
|
contacts?: Contact[];
|
|
489
|
-
|
|
482
|
+
calendar?: ScheduledActivity[];
|
|
490
483
|
team?: TeamMembers;
|
|
491
484
|
};
|
|
492
485
|
|
|
@@ -569,6 +562,7 @@ type TaskCapabilities<C extends Channel = Channel> =
|
|
|
569
562
|
decline?: Lockable<true>;
|
|
570
563
|
hold?: Lockable<true>;
|
|
571
564
|
endCall?: Lockable<true>;
|
|
565
|
+
terminateCall?: Lockable<true>;
|
|
572
566
|
connectBack?: Lockable<true>;
|
|
573
567
|
coldTransfer?: Lockable<DestinationDirectory>;
|
|
574
568
|
warmTransfer?: Lockable<DestinationDirectory>;
|
|
@@ -697,17 +691,8 @@ type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
|
|
|
697
691
|
| { stage: "joined"; leadId: UserId }
|
|
698
692
|
);
|
|
699
693
|
|
|
700
|
-
type
|
|
694
|
+
type TaskTakenOver = {
|
|
701
695
|
memberId: UserId;
|
|
702
|
-
assignmentId: AssignmentId;
|
|
703
|
-
note?: string;
|
|
704
|
-
since: IsoTimestamp;
|
|
705
|
-
};
|
|
706
|
-
|
|
707
|
-
type TaskListening = {
|
|
708
|
-
memberId: UserId;
|
|
709
|
-
assignmentId: AssignmentId;
|
|
710
|
-
mode: ListeningMode;
|
|
711
696
|
since: IsoTimestamp;
|
|
712
697
|
};
|
|
713
698
|
|
|
@@ -725,9 +710,9 @@ type Locked = {
|
|
|
725
710
|
|
|
726
711
|
type Lockable<T> = T | Locked;
|
|
727
712
|
|
|
728
|
-
type
|
|
713
|
+
type TaskAudioState = "started" | "ended";
|
|
729
714
|
|
|
730
|
-
type CapabilitySource = "queue" | "
|
|
715
|
+
type CapabilitySource = "queue" | "nobody" | "not-yet-read";
|
|
731
716
|
|
|
732
717
|
type Task<C extends Channel = Channel> = {
|
|
733
718
|
assignmentId: AssignmentId;
|
|
@@ -747,11 +732,11 @@ type Task<C extends Channel = Channel> = {
|
|
|
747
732
|
history?: TaskHistory;
|
|
748
733
|
} & TaskCompletion & (
|
|
749
734
|
C extends "voice"
|
|
750
|
-
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist;
|
|
751
|
-
: { recording?: never; onCall?: never; leadAssist?: never;
|
|
735
|
+
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; takenOver?: TaskTakenOver; audio?: TaskAudioState }
|
|
736
|
+
: { recording?: never; onCall?: never; leadAssist?: never; takenOver?: never; audio?: never }
|
|
752
737
|
);
|
|
753
738
|
|
|
754
|
-
type PreviewDeadline = "
|
|
739
|
+
type PreviewDeadline = "provider-dials" | "host-dials" | "waits";
|
|
755
740
|
|
|
756
741
|
type AcceptanceMode =
|
|
757
742
|
| "no-preference"
|
|
@@ -764,7 +749,6 @@ type TaskOutcome =
|
|
|
764
749
|
| { type: "taken-over"; leadId: UserId }
|
|
765
750
|
| { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
|
|
766
751
|
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
767
|
-
| { type: "left" }
|
|
768
752
|
| { type: "failed"; failure: ProtocolFailure };
|
|
769
753
|
```
|
|
770
754
|
|
|
@@ -775,10 +759,11 @@ const TASK_COMMAND_NAMES = {
|
|
|
775
759
|
voice: [
|
|
776
760
|
"answer",
|
|
777
761
|
"decline",
|
|
778
|
-
"
|
|
762
|
+
"dial",
|
|
779
763
|
"hold",
|
|
780
764
|
"resume",
|
|
781
765
|
"end-call",
|
|
766
|
+
"terminate-call",
|
|
782
767
|
"connect-back",
|
|
783
768
|
"transfer",
|
|
784
769
|
"lead-assist",
|
|
@@ -798,10 +783,11 @@ type OutcomePayload = { outcome?: string; notes?: string };
|
|
|
798
783
|
type VoiceTaskCommand =
|
|
799
784
|
| { type: "answer" }
|
|
800
785
|
| { type: "decline" }
|
|
801
|
-
| { type: "
|
|
786
|
+
| { type: "dial"; dialId: DialId }
|
|
802
787
|
| { type: "hold" }
|
|
803
788
|
| { type: "resume" }
|
|
804
789
|
| { type: "end-call" }
|
|
790
|
+
| { type: "terminate-call" }
|
|
805
791
|
| { type: "connect-back"; dialId: DialId }
|
|
806
792
|
| { type: "transfer"; action: "cold"; dialId: DialId; destinationId: string }
|
|
807
793
|
| { type: "transfer"; action: "warm"; dialId: DialId; destinationId: string }
|
|
@@ -809,8 +795,6 @@ type VoiceTaskCommand =
|
|
|
809
795
|
| { type: "transfer"; action: "cancel" }
|
|
810
796
|
| { type: "lead-assist"; action: "request"; note?: string }
|
|
811
797
|
| { type: "lead-assist"; action: "cancel" }
|
|
812
|
-
| { type: "lead-assist"; action: "take-over-call" }
|
|
813
|
-
| { type: "lead-assist"; action: "leave" }
|
|
814
798
|
| { type: "conference"; action: "add"; dialId: DialId; destinationId: string }
|
|
815
799
|
| { type: "conference"; action: "remove"; destinationId: string; party?: never }
|
|
816
800
|
| { type: "conference"; action: "remove"; party: true; destinationId?: never }
|
|
@@ -901,12 +885,12 @@ pass. An agent application never has the value and never asks. A name is not loc
|
|
|
901
885
|
controls — is content, and is never locked.
|
|
902
886
|
|
|
903
887
|
**A lead sets the team's policy from their team member list.** A login that declares
|
|
904
|
-
`capabilities.
|
|
888
|
+
`capabilities.lead` may `executeTeam({ command: { type: "set-policy", capability, setting } })`, where the provider offers policy control and says so by publishing `policies`,
|
|
905
889
|
with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`
|
|
906
890
|
and skills may be `person`; connect back and new call are the team's, on or off, within what the queue
|
|
907
891
|
allows. The team member list carries `policies` for such a login: every policy as it stands, who set it, and
|
|
908
892
|
`lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
|
|
909
|
-
change — `
|
|
893
|
+
change — `set-policy` on it answers `failed` with `omni.capability-not-enabled`.
|
|
910
894
|
|
|
911
895
|
**What the team left to the person is the person's, and the provider keeps it.** The login's
|
|
912
896
|
`capabilities.preferences` lists every preference the person may hold — `hold`, a skill —
|
|
@@ -984,26 +968,51 @@ type BreakEndResult =
|
|
|
984
968
|
### Team
|
|
985
969
|
|
|
986
970
|
```ts
|
|
987
|
-
type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "
|
|
971
|
+
type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "elsewhere" | "signed-out";
|
|
988
972
|
|
|
989
973
|
type TeamMember = {
|
|
990
974
|
id: UserId;
|
|
991
975
|
availability: TeamMemberAvailability;
|
|
992
976
|
since?: IsoTimestamp;
|
|
993
977
|
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
978
|
+
tasks?: MemberTask[];
|
|
979
|
+
listening?: MemberListening;
|
|
994
980
|
};
|
|
995
981
|
|
|
996
982
|
type LeadRequest = {
|
|
997
|
-
id: string;
|
|
998
983
|
memberId: UserId;
|
|
999
984
|
assignmentId: AssignmentId;
|
|
1000
985
|
note?: string;
|
|
1001
986
|
since: IsoTimestamp;
|
|
1002
987
|
};
|
|
1003
988
|
|
|
989
|
+
type MemberTask<C extends Channel = Channel> = {
|
|
990
|
+
assignmentId: AssignmentId;
|
|
991
|
+
title: string;
|
|
992
|
+
channel: C;
|
|
993
|
+
taskType: string;
|
|
994
|
+
phase: TaskPhase;
|
|
995
|
+
party?: Contact;
|
|
996
|
+
reference?: string;
|
|
997
|
+
attributes?: TaskAttribute[];
|
|
998
|
+
history?: TaskHistory;
|
|
999
|
+
capabilities?: TaskCapabilities<C>;
|
|
1000
|
+
capabilitySource?: CapabilitySource;
|
|
1001
|
+
browsers?: TaskBrowser[];
|
|
1002
|
+
completionMode?: CompletionMode;
|
|
1003
|
+
wrapAllowance?: DurationSeconds;
|
|
1004
|
+
} & (C extends "voice"
|
|
1005
|
+
? { onCall?: OnCall[]; leadAssist?: TaskLeadAssist; takenOver?: TaskTakenOver; audio?: TaskAudioState }
|
|
1006
|
+
: { onCall?: never; leadAssist?: never; takenOver?: never; audio?: never });
|
|
1007
|
+
|
|
1008
|
+
type MemberListening = {
|
|
1009
|
+
mode: ListeningMode;
|
|
1010
|
+
since: IsoTimestamp;
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1004
1013
|
type TeamMembers = {
|
|
1005
1014
|
members: TeamMember[];
|
|
1006
|
-
requests
|
|
1015
|
+
requests: LeadRequest[];
|
|
1007
1016
|
policies?: TeamPolicies;
|
|
1008
1017
|
};
|
|
1009
1018
|
|
|
@@ -1022,15 +1031,18 @@ type TeamPolicy = Resolved & {
|
|
|
1022
1031
|
|
|
1023
1032
|
type TeamPolicies = Partial<Record<PolicyKey, TeamPolicy>>;
|
|
1024
1033
|
|
|
1025
|
-
type TeamPolicyCommand = { type: "set"; capability: PolicyKey; setting: TeamPolicySetting };
|
|
1034
|
+
type TeamPolicyCommand = { type: "set-policy"; capability: PolicyKey; setting: TeamPolicySetting };
|
|
1026
1035
|
|
|
1027
|
-
type
|
|
1028
|
-
command: TeamPolicyCommand;
|
|
1029
|
-
};
|
|
1036
|
+
type TeamFeatureCommand = { type: "lead-features"; enabled: boolean };
|
|
1030
1037
|
|
|
1031
|
-
type
|
|
1032
|
-
| { type: "join";
|
|
1033
|
-
| { type: "decline";
|
|
1038
|
+
type TeamCallCommand =
|
|
1039
|
+
| { type: "join"; memberId: UserId; assignmentId?: AssignmentId }
|
|
1040
|
+
| { type: "decline"; memberId: UserId; assignmentId?: AssignmentId; reason?: string }
|
|
1041
|
+
| { type: "listen"; memberId: UserId; assignmentId?: AssignmentId }
|
|
1042
|
+
| { type: "coach"; memberId: UserId; assignmentId?: AssignmentId }
|
|
1043
|
+
| { type: "join-call"; memberId: UserId; assignmentId?: AssignmentId }
|
|
1044
|
+
| { type: "leave"; memberId: UserId; assignmentId?: AssignmentId }
|
|
1045
|
+
| { type: "take-over-call"; memberId: UserId; assignmentId?: AssignmentId };
|
|
1034
1046
|
|
|
1035
1047
|
type TeamBreakCommand =
|
|
1036
1048
|
| { type: "decide-break-request"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
@@ -1038,22 +1050,10 @@ type TeamBreakCommand =
|
|
|
1038
1050
|
| { type: "force-break"; memberId: UserId; reasonId?: string; reason?: string; expectedDurationMs?: number }
|
|
1039
1051
|
| { type: "end-forced-break"; memberId: UserId };
|
|
1040
1052
|
|
|
1041
|
-
type
|
|
1042
|
-
command: TeamBreakCommand;
|
|
1043
|
-
};
|
|
1053
|
+
type TeamCommand = TeamFeatureCommand | TeamBreakCommand | TeamCallCommand | TeamPolicyCommand;
|
|
1044
1054
|
|
|
1045
|
-
type
|
|
1046
|
-
command:
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
type TeamListenCommand =
|
|
1050
|
-
| { type: "listen"; memberId: UserId }
|
|
1051
|
-
| { type: "coach" }
|
|
1052
|
-
| { type: "join-call" }
|
|
1053
|
-
| { type: "leave" };
|
|
1054
|
-
|
|
1055
|
-
type TeamListenCommandRequest = {
|
|
1056
|
-
command: TeamListenCommand;
|
|
1055
|
+
type TeamCommandRequest = {
|
|
1056
|
+
command: TeamCommand;
|
|
1057
1057
|
};
|
|
1058
1058
|
|
|
1059
1059
|
type TeamCommandResult =
|
|
@@ -1061,20 +1061,20 @@ type TeamCommandResult =
|
|
|
1061
1061
|
| { status: "failed"; failure: ProtocolFailure };
|
|
1062
1062
|
```
|
|
1063
1063
|
|
|
1064
|
-
###
|
|
1064
|
+
### Audio
|
|
1065
1065
|
|
|
1066
1066
|
```ts
|
|
1067
|
-
type
|
|
1067
|
+
type CallAudio = {
|
|
1068
1068
|
remoteAudio: MediaStream;
|
|
1069
1069
|
setMuted(muted: boolean): void;
|
|
1070
1070
|
close(): void;
|
|
1071
1071
|
};
|
|
1072
1072
|
|
|
1073
|
-
type
|
|
1074
|
-
| { status: "opened";
|
|
1073
|
+
type OpenAudioResult =
|
|
1074
|
+
| { status: "opened"; audio: CallAudio }
|
|
1075
1075
|
| { status: "unavailable"; failure: ProtocolFailure };
|
|
1076
1076
|
|
|
1077
|
-
type
|
|
1077
|
+
type OpenAudioRequest = {
|
|
1078
1078
|
assignmentId: AssignmentId;
|
|
1079
1079
|
localAudio?: MediaStream;
|
|
1080
1080
|
};
|
|
@@ -1106,8 +1106,10 @@ type ProviderEvent =
|
|
|
1106
1106
|
assignmentExpiresAt?: IsoTimestamp;
|
|
1107
1107
|
}
|
|
1108
1108
|
| { type: "task-updated"; task: Task }
|
|
1109
|
-
| { type: "task-
|
|
1110
|
-
| { type: "task-
|
|
1109
|
+
| { type: "task-audio-started"; assignmentId: AssignmentId }
|
|
1110
|
+
| { type: "task-audio-ended"; assignmentId: AssignmentId }
|
|
1111
|
+
| { type: "team-audio-started"; memberId: UserId; assignmentId: AssignmentId }
|
|
1112
|
+
| { type: "team-audio-ended"; memberId: UserId; assignmentId: AssignmentId }
|
|
1111
1113
|
| { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
|
|
1112
1114
|
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
|
|
1113
1115
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
@@ -1115,7 +1117,7 @@ type ProviderEvent =
|
|
|
1115
1117
|
| { type: "diagnostic"; expected: string; observed: string; assignmentId?: AssignmentId }
|
|
1116
1118
|
| { type: "team-updated"; team: TeamMembers }
|
|
1117
1119
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1118
|
-
| { type: "calendar-updated";
|
|
1120
|
+
| { type: "calendar-updated"; calendar: ScheduledActivity[] };
|
|
1119
1121
|
|
|
1120
1122
|
type ProviderEventEnvelope = {
|
|
1121
1123
|
id: string;
|
|
@@ -1169,13 +1171,10 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1169
1171
|
cancelBreak?(): Promise<BreakCancelResult>;
|
|
1170
1172
|
endBreak?(): Promise<BreakEndResult>;
|
|
1171
1173
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1175
|
-
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1174
|
+
executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
|
|
1175
|
+
openAudio?(request: OpenAudioRequest): Promise<OpenAudioResult>;
|
|
1176
1176
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1177
1177
|
recordStep?(report: HistoryReport): Promise<HistoryReportResult>;
|
|
1178
|
-
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1179
1178
|
};
|
|
1180
1179
|
|
|
1181
1180
|
type Adapter<C extends Channel = Channel> = {
|
|
@@ -1532,7 +1531,7 @@ Request/response polling does not have those properties and is not a transport f
|
|
|
1532
1531
|
|
|
1533
1532
|
Everything an adapter publishes about a task is derived from what the platform states, or kept in
|
|
1534
1533
|
the login's store where the platform cannot hold it. Per offer, the `assignmentId` comes from what
|
|
1535
|
-
the offer itself states; per call, the
|
|
1534
|
+
the offer itself states; per call, the audio follows `task-audio-started` and `task-audio-ended`,
|
|
1536
1535
|
never a flag set when the audio was opened; per session, the phone the agent holds is what the
|
|
1537
1536
|
platform restates on activation, not a mapping read once over HTTP. What the platform cannot hold
|
|
1538
1537
|
-- an agent application's muted leg -- goes in `ConnectContext.store`, keyed by the task and gone with it.
|
|
@@ -1540,11 +1539,11 @@ platform restates on activation, not a mapping read once over HTTP. What the pla
|
|
|
1540
1539
|
A fact read once over a connection and kept in the adapter object dies with the client. An agent application
|
|
1541
1540
|
reload is the first client dying and a second coming up for the same login, and the second
|
|
1542
1541
|
publishes the same truth about the same task as the first did, or the first was publishing
|
|
1543
|
-
something it made up: an assignment minted for the connection,
|
|
1542
|
+
something it made up: an assignment minted for the connection, audio remembered as a boolean, a
|
|
1544
1543
|
phone the second never read. Three pieces of exactly that state were found in one adapter in an
|
|
1545
1544
|
afternoon, and none of it was visible until the adapter was built twice. The drive's `rebuild` is the test of this
|
|
1546
1545
|
principle: given a way to build the adapter again, the run hands the login over and holds the
|
|
1547
|
-
second to what the first published (`drive.reload.snapshot`, `.history`, `.
|
|
1546
|
+
second to what the first published (`drive.reload.snapshot`, `.history`, `.openAudio`, and the
|
|
1548
1547
|
stream's own rules across the resync). An adapter that cannot be built twice against its platform
|
|
1549
1548
|
and publish the same task is not a conformant adapter yet, whatever a single run says.
|
|
1550
1549
|
|
|
@@ -1611,7 +1610,7 @@ compile time.
|
|
|
1611
1610
|
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; agent application polling is independently opt-in. |
|
|
1612
1611
|
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
|
|
1613
1612
|
| `runningStepReports` | The provider takes running reports of an agent application-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the agent application sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The agent application records what it performs**. |
|
|
1614
|
-
| `completionSettleMs` | Required. How long after an applied
|
|
1613
|
+
| `completionSettleMs` | Required. How long after an applied `complete` the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.completionSettleMs`). A warm transfer's `complete` is not final task completion: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1615
1614
|
|
|
1616
1615
|
### Authentication methods
|
|
1617
1616
|
|
|
@@ -1754,7 +1753,7 @@ contributions from active providers into one agent-facing calendar while retaini
|
|
|
1754
1753
|
provider identity for each activity.
|
|
1755
1754
|
|
|
1756
1755
|
When declared, the provider publishes its authoritative list through
|
|
1757
|
-
`Snapshot.
|
|
1756
|
+
`Snapshot.calendar` and replaces it with a `calendar-updated` event when it
|
|
1758
1757
|
changes.
|
|
1759
1758
|
|
|
1760
1759
|
| Field | Contract |
|
|
@@ -1912,20 +1911,15 @@ replaces the login" describes is a new `AuthenticationSession` under a new `logi
|
|
|
1912
1911
|
### What the login may do
|
|
1913
1912
|
|
|
1914
1913
|
`capabilities` declares provider actions available to this login rather than to one task: whether
|
|
1915
|
-
the agent may ask for a break, and whether they lead a team
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
them from what arrives later.
|
|
1914
|
+
the agent may ask for a break, and whether they lead a team. It is declared by presence, like every
|
|
1915
|
+
capability in this contract, and it travels with the identity because it is part of who the agent
|
|
1916
|
+
is on this provider: the provider knows the roles, and says so at sign-in rather than leaving Omni
|
|
1917
|
+
to infer them from what arrives later.
|
|
1920
1918
|
|
|
1921
1919
|
| Field | Contract |
|
|
1922
1920
|
| --- | --- |
|
|
1923
1921
|
| `breaks` | This login may request a break. Requires the four break methods on the connection. |
|
|
1924
|
-
| `
|
|
1925
|
-
| `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — force-break, end-forced-break, decide-break-request, set-break-policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-approval`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
|
|
1926
|
-
| `team.leadAssistControl` | This lead may join a member's call on request. Requires `executeTeamLeadAssist`. |
|
|
1927
|
-
| `team.listeningControl` | This lead may listen to a member's call unasked, in the listed modes and no others: `listen`, `coach`, `join-call`. The list always includes `listen`, since the other two begin from one. Requires `executeTeamListen`. See **Listening to a call**. |
|
|
1928
|
-
| `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the team member list carries `policies`. |
|
|
1922
|
+
| `lead` | This login leads a team. The flag alone turns on the team feature in the agent application, and every lead act comes with it; there is no per-action permission beside it. While the lead has the feature on, the provider publishes a `TeamMembers` object to them on every snapshot — `members: []` when nobody is in it — and to nobody else. Requires `executeTeam`. See **Team leads**. |
|
|
1929
1923
|
| `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
|
|
1930
1924
|
|
|
1931
1925
|
A session action is available only when both the capability and Omni local policy permit it.
|
|
@@ -2133,13 +2127,13 @@ a capability it agrees with the login: a lead's snapshot carries `team`, nobody
|
|
|
2133
2127
|
| `tasks` | Complete set of tasks currently offered to or owned by this agent. |
|
|
2134
2128
|
| `taskCount` | The provider's own count of those tasks, stated rather than inferred, and it must equal `tasks.length`. A snapshot with no work says `taskCount: 0` in so many words — a blank or unanswered state lacks the count and cannot pass as a confirmed empty. |
|
|
2135
2129
|
| `contacts` | Required complete contact contribution when the manifest declares `contacts`; `[]` clears it. Omitted only when it does not. |
|
|
2136
|
-
| `
|
|
2137
|
-
| `team` | Required `TeamMembers` when the login declares `capabilities.team
|
|
2130
|
+
| `calendar` | Required complete calendar contribution when the manifest declares `calendar`; `[]` clears it. Omitted only when it does not. |
|
|
2131
|
+
| `team` | Required `TeamMembers` when the login declares `capabilities.lead` and the lead has the team feature on, `members: []` when nobody is in it. Forbidden otherwise — the login is the permission, and a lead who turned the feature off gets nothing of the team. |
|
|
2138
2132
|
|
|
2139
2133
|
## Live connection
|
|
2140
2134
|
|
|
2141
2135
|
`Connection` is what `connect()` returns. Its methods are documented in the sections that follow
|
|
2142
|
-
and under **Breaks**, **Team leads**, **Real-time
|
|
2136
|
+
and under **Breaks**, **Team leads**, **Real-time audio** and **Task commands**; this is the whole
|
|
2143
2137
|
surface in one place, and what obliges an adapter to implement each one.
|
|
2144
2138
|
|
|
2145
2139
|
| Method | Implement it when |
|
|
@@ -2157,13 +2151,10 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2157
2151
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
2158
2152
|
| `cancelBreak()` | The login declares `capabilities.breaks`. |
|
|
2159
2153
|
| `endBreak()` | The login declares `capabilities.breaks`. |
|
|
2160
|
-
| `
|
|
2161
|
-
| `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
|
|
2162
|
-
| `executeTeamListen(command)` | The login declares `capabilities.team.listeningControl`. |
|
|
2154
|
+
| `executeTeam(request)` | The login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. See **Lead commands**. |
|
|
2163
2155
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
2164
2156
|
| `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the agent application mutes its own microphone on any call, and the provider's record has to have somewhere to take that leg; a desk phone's microphone is the phone's. See **The agent application records what it performs**. |
|
|
2165
|
-
| `
|
|
2166
|
-
| `openMedia(request)` | The manifest lists `softphone` among its `phones`. On a softphone the call's audio lands in Omni, so the adapter has to open it; a platform of desk phones alone never does. |
|
|
2157
|
+
| `openAudio(request)` | The manifest lists `softphone` among its `phones`. On a softphone the call's audio lands in Omni, so the adapter has to open it; a platform of desk phones alone never does. |
|
|
2167
2158
|
|
|
2168
2159
|
**The four break methods stand or fall together.** Declaring `capabilities.breaks` at login and then
|
|
2169
2160
|
implementing `requestBreak` without `commitBreak` leaves an agent granted a break that can never
|
|
@@ -2177,7 +2168,7 @@ Returns the provider's complete authoritative state at one point in time.
|
|
|
2177
2168
|
- Omni registers `subscribe()` before awaiting the initial snapshot. An event delivered while the
|
|
2178
2169
|
snapshot is read is held until it lands, and what happens to it then depends on what it is. A
|
|
2179
2170
|
snapshot restates state, so an event of a state-replacing kind -- `task-offered`, `task-updated`,
|
|
2180
|
-
`task-ended`, `task-
|
|
2171
|
+
`task-ended`, `task-audio-started`, `task-audio-ended`, `break-state`, `team-updated`,
|
|
2181
2172
|
`contacts-updated`, `calendar-updated`, `transport-status`, `snapshot` -- is dropped, because the
|
|
2182
2173
|
snapshot accounts for it. An event that reports a transaction no snapshot carries --
|
|
2183
2174
|
`dial-outcome`, `diagnostic`, `announcement`, `queue-summary` -- is applied after the snapshot,
|
|
@@ -2233,7 +2224,7 @@ Stops the connection and releases adapter-owned resources.
|
|
|
2233
2224
|
|
|
2234
2225
|
- Must be safe after partial startup and safe to call once during normal shutdown.
|
|
2235
2226
|
- Must stop automatic reconnect.
|
|
2236
|
-
- Must remove event handlers and release
|
|
2227
|
+
- Must remove event handlers and release audio resources owned by the adapter.
|
|
2237
2228
|
- Does not imply that active tasks were completed or removed.
|
|
2238
2229
|
|
|
2239
2230
|
## Task assignment lifecycle
|
|
@@ -2295,7 +2286,7 @@ An automatically accepted task still arrives through `task-offered`.
|
|
|
2295
2286
|
|
|
2296
2287
|
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2297
2288
|
agent's own act — a dialpad call or a connect-back, recognisable by the agent application's `dialId` on
|
|
2298
|
-
`onCall`; a
|
|
2289
|
+
`onCall`; a call a lead took over, carrying `takenOver` — arrives
|
|
2299
2290
|
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2300
2291
|
validator holds it there under either local policy and under none (`task.acceptance.originated`):
|
|
2301
2292
|
the desk shows no **Accept** for a call the agent placed. The local policy governs work the queue
|
|
@@ -2319,7 +2310,7 @@ const assignment = {
|
|
|
2319
2310
|
```
|
|
2320
2311
|
|
|
2321
2312
|
The rule the phase exists to express: **nothing is acquired on the agent's behalf while a task
|
|
2322
|
-
is pending.** An agent application that carries
|
|
2313
|
+
is pending.** An agent application that carries audio must not open the microphone until the task is
|
|
2323
2314
|
accepted. Omni does not open the task's browsers either — a task that rings out costs nothing.
|
|
2324
2315
|
|
|
2325
2316
|
When consent is required, Omni offers the agent an **Accept** control. The call is the
|
|
@@ -2371,28 +2362,27 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2371
2362
|
|
|
2372
2363
|
| Field | Contract |
|
|
2373
2364
|
| --- | --- |
|
|
2374
|
-
| `assignmentId` | Required `AssignmentId`: the assignment this task is the record of, and its one identity. Issued by the provider, unique within the provider, never reused. Every event, command and report that names a task names it by this, so a late dial outcome or a late history report for one customer never lands on the next. The stream refuses an assignment introduced twice (`stream.taskOffered.duplicate`) and
|
|
2365
|
+
| `assignmentId` | Required `AssignmentId`: the assignment this task is the record of, and its one identity. Issued by the provider, unique within the provider, never reused. Every event, command and report that names a task names it by this, so a late dial outcome or a late history report for one customer never lands on the next. The stream refuses an assignment introduced twice (`stream.taskOffered.duplicate`) and an audio or ending event naming one that has ended or that nobody has seen (`stream.assignment.ended`, `.unknown`); a `dial-outcome` may name an ended assignment, since a dial placed late routinely outlives its call, and the agent application routes it there. See **A task's life on the wire**. |
|
|
2375
2366
|
| `title` | Agent-facing task title. |
|
|
2376
2367
|
| `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
|
|
2377
2368
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
2378
2369
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2379
|
-
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `
|
|
2370
|
+
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `nobody` when nothing handed the work over -- an agent's own outbound -- and `not-yet-read` when a queue was named and its terms could not be read, in which case `capabilities` is what the provider will honour, not what the platform permits. An agent application shows `not-yet-read` where the agent works. See **Task capabilities**. |
|
|
2380
2371
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2381
2372
|
| `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
|
|
2382
2373
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2383
|
-
| `
|
|
2374
|
+
| `audio` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-audio-started`**. Audio names a task whose work has begun, or whose party the agent application is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.audio.beforeWork`), on a snapshot as on the event, since an agent application opens the microphone on it; with the party ringing by an agent application dial or provider-triggered preview dial, actual ring-back audio may precede answer. |
|
|
2384
2375
|
| `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
|
|
2385
2376
|
| `previewEndsAt` | Voice only, in `preview`: the preparation target instant. Absent, the agent has as long as they need without a preparation countdown. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2386
|
-
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `
|
|
2377
|
+
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `provider-dials` makes the provider initiate dialing, `host-dials` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
|
|
2387
2378
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2388
2379
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2389
|
-
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time
|
|
2380
|
+
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time audio, it begins after `task-audio-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. |
|
|
2390
2381
|
| `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. |
|
|
2391
2382
|
| `history` | The call record: `steps` — the ordered interaction history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `interactionSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **Interaction history**. |
|
|
2392
2383
|
| `onCall` | Voice only. Who is on the call, or being brought onto it, as the provider states it, replaced whole with the task: `party` is the customer -- carrying a `stage` while being dialled again on the same task, a connect-back with the agent application's `dialId` or a platform's callback without, ringing from the moment the dial is placed and joined on its answered outcome --, `agent` a person by user id, `consulted` and `conferenced` somebody a dial is bringing in, listed from the moment the dial is placed -- with the `destinationId` dialled, the `dialId` where an agent application placed it, the `stage` reached (`ringing` until answered, `joined` after), and `held: true` on anyone joined and parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the agent application supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2393
2384
|
| `leadAssist` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Lead assist**. |
|
|
2394
|
-
| `
|
|
2395
|
-
| `listening` | Voice only, on the lead's own task while they listen to a member's call: whose call, which call, and the `mode` they are heard in, restated on every change. Never on the member's task, and never together with `assisting`. See **Listening to a call**. |
|
|
2385
|
+
| `takenOver` | Voice only, on a task that reached this agent by a lead's take-over: which member the call was taken from, and when. It is an ordinary assignment with the call's history, offered whatever break the lead is on and counted against no capacity. See **Lead assist**. |
|
|
2396
2386
|
|
|
2397
2387
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
2398
2388
|
|
|
@@ -2424,7 +2414,7 @@ The canonical task transitions are:
|
|
|
2424
2414
|
| `pending` | Task is accepted | `confirmed` |
|
|
2425
2415
|
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2426
2416
|
| `confirmed` | Work begins | `in-progress` |
|
|
2427
|
-
| `preview` | Agent presses Call (`
|
|
2417
|
+
| `preview` | Agent presses Call (`dial`) and the customer answers, or the deadline `provider-dials` and they answer | `in-progress` |
|
|
2428
2418
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2429
2419
|
| `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
|
|
2430
2420
|
| `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
@@ -2443,7 +2433,7 @@ The canonical task transitions are:
|
|
|
2443
2433
|
A task has one name, the assignment it is the record of, and the provider issues it. Platforms
|
|
2444
2434
|
reuse their own handles: a closed one is retired minutes later, a requeue takes seconds, and on
|
|
2445
2435
|
one platform a call was offered twice, thirteen seconds apart, under one handle, through a close
|
|
2446
|
-
and a re-offer. Everything that names a task after the fact -- a dial outcome,
|
|
2436
|
+
and a re-offer. Everything that names a task after the fact -- a dial outcome, an audio event, an
|
|
2447
2437
|
ending, a history report, a command -- would land on whichever assignment is open when it arrives.
|
|
2448
2438
|
So the assignment id is unique within the provider and never reused: where the platform's handle
|
|
2449
2439
|
never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
|
|
@@ -2452,7 +2442,7 @@ belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__ASS
|
|
|
2452
2442
|
lives one assignment, never the next customer's cookies. A dial the agent application placed was
|
|
2453
2443
|
already safe, since its outcome is placed by the agent application's own `dialId`; where the
|
|
2454
2444
|
assignment earns its place is everything the agent application does not mint --
|
|
2455
|
-
`task-
|
|
2445
|
+
`task-audio-started`, `task-audio-ended`, `task-ended`, a `recordStep` naming a task -- any of which
|
|
2456
2446
|
would otherwise find the next customer under a reused handle and act on them. A minted id is part
|
|
2457
2447
|
of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
|
|
2458
2448
|
`store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
|
|
@@ -2479,19 +2469,19 @@ const previewed = {
|
|
|
2479
2469
|
phase: "preview",
|
|
2480
2470
|
party: { name: "Maya Rao", number: "+919876543210" },
|
|
2481
2471
|
previewEndsAt: "2026-08-25T10:40:37.000Z",
|
|
2482
|
-
atDeadline: "
|
|
2472
|
+
atDeadline: "provider-dials",
|
|
2483
2473
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "party" | "previewEndsAt" | "atDeadline">;
|
|
2484
2474
|
|
|
2485
2475
|
// The agent presses Call. It is a dial like any other.
|
|
2486
|
-
const pressed: TaskCommand<"voice"> = { type: "
|
|
2476
|
+
const pressed: TaskCommand<"voice"> = { type: "dial", dialId: "dial-7f2" };
|
|
2487
2477
|
```
|
|
2488
2478
|
|
|
2489
2479
|
**Call is a dial.** It carries the agent application's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2490
2480
|
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
2491
|
-
agent is there to be called. Its
|
|
2492
|
-
ring-back is audio the agent hears -- so `task-
|
|
2493
|
-
its party ringing. On `answered` the task is `in-progress`; on any other outcome the
|
|
2494
|
-
`task-
|
|
2481
|
+
agent is there to be called. Its audio starts on `dialling`, as every agent application-placed dial's does --
|
|
2482
|
+
ring-back is audio the agent hears -- so `task-audio-started` arrives on the `preview` task with
|
|
2483
|
+
its party ringing. On `answered` the task is `in-progress`; on any other outcome the audio ends,
|
|
2484
|
+
`task-audio-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
2495
2485
|
`completing`, so the agent records the
|
|
2496
2486
|
no-answer as the outcome it is -- in a campaign that is the commonest outcome there is, and it
|
|
2497
2487
|
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
@@ -2503,8 +2493,8 @@ carries the provider's authoritative preparation-end instant and exactly one tri
|
|
|
2503
2493
|
|
|
2504
2494
|
| Declaration | At preparation end |
|
|
2505
2495
|
| --- | --- |
|
|
2506
|
-
| `atDeadline: "
|
|
2507
|
-
| `atDeadline: "host-
|
|
2496
|
+
| `atDeadline: "provider-dials"` | The provider initiates dialing. The agent application never sends a timer-triggered Call. |
|
|
2497
|
+
| `atDeadline: "host-dials"` | The agent application sends the ordinary `dial` command with a fresh agent application `dialId`. The provider does not independently auto-dial this task. |
|
|
2508
2498
|
| `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
|
|
2509
2499
|
|
|
2510
2500
|
The agent may press Call early in either fixed-preparation dialing mode, or at any time in
|
|
@@ -2512,7 +2502,7 @@ The agent may press Call early in either fixed-preparation dialing mode, or at a
|
|
|
2512
2502
|
zero the UI says "Waiting for agent" and keeps Call available. There is no automatic task-ended,
|
|
2513
2503
|
completion, dialing or phase change. No repeating timer action occurs on later snapshots. This differs
|
|
2514
2504
|
from unlimited preparation only by displaying a preparation target. The former `expires` deadline
|
|
2515
|
-
value is not supported; elapsed preparation must not withdraw the task. For `
|
|
2505
|
+
value is not supported; elapsed preparation must not withdraw the task. For `provider-dials` and `host-dials`, the deadline ends
|
|
2516
2506
|
preparation and triggers initiation; it does not promise ringing, playable audio or customer
|
|
2517
2507
|
answer at that exact instant. Scheduling/dispatch delay must remain visible, and a failed or
|
|
2518
2508
|
prevented initiation must be reported rather than leaving a silently expired countdown.
|
|
@@ -2523,7 +2513,7 @@ time; clock estimation is allowed only under the explicit agent application-trig
|
|
|
2523
2513
|
Without usable provider-domain time
|
|
2524
2514
|
it cannot safely auto-trigger and must expose the uncertainty and reconcile. No zero-duration,
|
|
2525
2515
|
receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
|
|
2526
|
-
the task phase: subsequent authoritative events state ringing, answer,
|
|
2516
|
+
the task phase: subsequent authoritative events state ringing, answer, audio and completion.
|
|
2527
2517
|
|
|
2528
2518
|
An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
|
|
2529
2519
|
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
@@ -2537,11 +2527,11 @@ command as ownership of an already-running independent attempt.
|
|
|
2537
2527
|
Agent application-triggered automatic dialing is an ordinary agent application dial: its result is `dialling` with the same
|
|
2538
2528
|
agent application `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
|
|
2539
2529
|
dial has no invented agent application ID or agent application dial outcome. It reports source-evidenced party ringing,
|
|
2540
|
-
actual
|
|
2541
|
-
party without an agent application ID may carry actual pre-answer
|
|
2542
|
-
|
|
2530
|
+
actual audio and customer answer separately. In preview with `atDeadline: "provider-dials"`, a ringing
|
|
2531
|
+
party without an agent application ID may carry actual pre-answer audio; the deadline alone permits no audio.
|
|
2532
|
+
Audio is still introduced/ended by the corresponding events and must not imply customer answer.
|
|
2543
2533
|
Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
|
|
2544
|
-
Unsupported source correlation/outcome/
|
|
2534
|
+
Unsupported source correlation/outcome/audio evidence remains an integration blocker.
|
|
2545
2535
|
|
|
2546
2536
|
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
2547
2537
|
spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the agent application refuses
|
|
@@ -2559,7 +2549,7 @@ recording contract; this distinction does not remove it or move provider call op
|
|
|
2559
2549
|
|
|
2560
2550
|
A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
|
|
2561
2551
|
own assignment, and two open at once carry two assignment ids. Restore them unchanged on
|
|
2562
|
-
reconnect. Commands,
|
|
2552
|
+
reconnect. Commands, audio, endings and history reports name the exact assignment; never retarget
|
|
2563
2553
|
an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
|
|
2564
2554
|
request's assignment against the supplied published task and then its command prerequisites. The
|
|
2565
2555
|
caller must select that task within the correct provider/login and recheck at the provider; the
|
|
@@ -2569,33 +2559,33 @@ portion must not be cleared because this task ended. Snapshot counts include all
|
|
|
2569
2559
|
and active interactions; task completion is not caller hangup.
|
|
2570
2560
|
|
|
2571
2561
|
**Voice describes the agent's interaction within the wider call.** The task defines that agent's
|
|
2572
|
-
workspace, tools, permissions,
|
|
2562
|
+
workspace, tools, permissions, audio participation and completion work. The caller's channel may
|
|
2573
2563
|
continue through IVR, queues, other agents, holds or conferences while this interaction ends or wraps.
|
|
2574
2564
|
Completing this task ends this interaction responsibility; it is not evidence that the caller's channel
|
|
2575
|
-
or journey ended. Task
|
|
2565
|
+
or journey ended. Task audio describes this agent's attachment, not the lifetime of every party's
|
|
2576
2566
|
connection. Commands retain their explicit targets and effects; task completion must not silently
|
|
2577
2567
|
become a caller-disconnect operation.
|
|
2578
2568
|
|
|
2579
2569
|
**A task is never its audio.** A voice task represents the agent’s interaction: the call is offered when it is
|
|
2580
2570
|
routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
|
|
2581
2571
|
the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
|
|
2582
|
-
consult, a conference leg joining or leaving, a transfer, a connect-back — the
|
|
2583
|
-
separately, arriving on `task-
|
|
2584
|
-
`task-
|
|
2572
|
+
consult, a conference leg joining or leaving, a transfer, a connect-back — the audio follows
|
|
2573
|
+
separately, arriving on `task-audio-started`, attaching through `openAudio` and ending with
|
|
2574
|
+
`task-audio-ended`. Omni does not ring,
|
|
2585
2575
|
bridge, or hold a line. How the phone rings, whether it rings at all, and where legs join and leave
|
|
2586
2576
|
are the adapter's and the platform's, transient, and decide neither when a task exists nor what
|
|
2587
2577
|
phase it is in.
|
|
2588
2578
|
|
|
2589
|
-
The line runs between the provider's word and Omni's own senses. `task-
|
|
2579
|
+
The line runs between the provider's word and Omni's own senses. `task-audio-ended` is the
|
|
2590
2580
|
provider's report that primary interaction ended — a fact about the work, which is why the completion
|
|
2591
2581
|
allowance starts on it and the Connect back control appears on it — and Omni follows that report as it
|
|
2592
|
-
follows any other. What Omni never does is derive a task's state from its own
|
|
2582
|
+
follows any other. What Omni never does is derive a task's state from its own audio session: a
|
|
2593
2583
|
stream that drops, a track that ends, a transport that disconnects, a microphone that fails, an
|
|
2594
2584
|
endpoint re-registering change nothing about the task until the provider says so. Structurally:
|
|
2595
|
-
`task-
|
|
2596
|
-
only where it arrived, what follows the
|
|
2585
|
+
`task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
|
|
2586
|
+
only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
|
|
2597
2587
|
task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
|
|
2598
|
-
and `
|
|
2588
|
+
and `assertAudioFollowsTheTask` holds any sequence.
|
|
2599
2589
|
|
|
2600
2590
|
#### Completion timing
|
|
2601
2591
|
|
|
@@ -2604,16 +2594,16 @@ the task open until Omni sends the channel's `complete` command. With `provider-
|
|
|
2604
2594
|
provider may complete the task without receiving that command.
|
|
2605
2595
|
|
|
2606
2596
|
`wrapAllowance` is independent of that decision. It is fixed, and when it starts depends on
|
|
2607
|
-
whether the channel carries real-time
|
|
2597
|
+
whether the channel carries real-time audio:
|
|
2608
2598
|
|
|
2609
2599
|
| Channel | Wrap allowance starts at |
|
|
2610
2600
|
| --- | --- |
|
|
2611
|
-
| Voice and any channel with real-time
|
|
2601
|
+
| Voice and any channel with real-time audio | The `task-audio-ended` event |
|
|
2612
2602
|
| Chat | The `task-updated` that moves the task to `completing`: the provider's word that the conversation is closed on its side, the allowance running from that publication's `occurredAt` |
|
|
2613
2603
|
| Email | The `task-updated` that moves the task to `completing`: the provider's word that the platform has accepted the outgoing message, the allowance running from that publication's `occurredAt` |
|
|
2614
|
-
| Other non-
|
|
2604
|
+
| Other non-audio channels | The `task-updated` that moves the task to `completing` |
|
|
2615
2605
|
|
|
2616
|
-
**Off voice, `completing` is the provider's word that interaction ended.** There is no
|
|
2606
|
+
**Off voice, `completing` is the provider's word that interaction ended.** There is no audio event to
|
|
2617
2607
|
carry it, so the phase does; an agent application starts the wrap clock at that publication and nowhere else. It
|
|
2618
2608
|
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2619
2609
|
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
@@ -2648,9 +2638,9 @@ const untimedWrap = {
|
|
|
2648
2638
|
} satisfies Pick<Task<"voice">, "completionMode" | "wrapAllowance">;
|
|
2649
2639
|
```
|
|
2650
2640
|
|
|
2651
|
-
Here the customer has hung up, `task-
|
|
2641
|
+
Here the customer has hung up, `task-audio-ended` has been sent on time, the task is `completing`,
|
|
2652
2642
|
and the agent takes as long as the work needs. Moving the task to `completing` late to avoid a
|
|
2653
|
-
deadline is not an alternative on
|
|
2643
|
+
deadline is not an alternative on an audio channel: the clock starts at a real event, and delaying
|
|
2654
2644
|
that event would falsify the phase and everything timed from it.
|
|
2655
2645
|
|
|
2656
2646
|
#### Connecting back during completion
|
|
@@ -2665,9 +2655,9 @@ is; the command carries no destination, and it is a dial like any other, so it c
|
|
|
2665
2655
|
|
|
2666
2656
|
On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
2667
2657
|
working again, and the wrap allowance is **discarded, not paused**. From there the call is
|
|
2668
|
-
reported as any call is -- `paused`, `in-progress`, and when its
|
|
2658
|
+
reported as any call is -- `paused`, `in-progress`, and when its audio ends, `task-audio-ended`
|
|
2669
2659
|
again, which starts a fresh allowance from that instant. A party who does not answer is a dial
|
|
2670
|
-
whose outcome says so and a call whose
|
|
2660
|
+
whose outcome says so and a call whose audio ended: the task returns to `completing` through the
|
|
2671
2661
|
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2672
2662
|
deadline.
|
|
2673
2663
|
|
|
@@ -2703,7 +2693,7 @@ const connectBackCapable = {
|
|
|
2703
2693
|
With ten seconds of the thirty left, the agent presses Connect back: `execute({ command: { type:
|
|
2704
2694
|
"connect-back", dialId } })` returns `dialling`, the task is `in-progress`, and the thirty seconds
|
|
2705
2695
|
are gone.
|
|
2706
|
-
The second call ends: `task-
|
|
2696
|
+
The second call ends: `task-audio-ended`, the task is `completing`, and a new thirty seconds runs
|
|
2707
2697
|
from that instant.
|
|
2708
2698
|
|
|
2709
2699
|
```ts
|
|
@@ -2739,6 +2729,12 @@ Migration from the earlier spellings:
|
|
|
2739
2729
|
| Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `assignmentKey(providerId, assignmentId)` scopes it |
|
|
2740
2730
|
| taskKey, omni.task-not-found, PROVIDER_NAME__TASK_ID__TAB_NAME (ProviderName.TaskId.TabName) | `assignmentKey`, `omni.assignment-not-found`, `PROVIDER_NAME__ASSIGNMENT_ID__TAB_NAME` (`ProviderName.AssignmentId.TabName`) |
|
|
2741
2731
|
| Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
|
|
2732
|
+
| the call command, atDeadline calls and host-calls | `dial`, `"provider-dials"`, `"host-dials"` |
|
|
2733
|
+
| media: task-media-started/-ended, team-media-*, Task.media, TaskMediaState, openMedia, OpenMediaRequest/Result, VoiceMediaSession and its session field | audio: `task-audio-started`, `task-audio-ended`, `team-audio-started`, `team-audio-ended`, `audio` on the task, `TaskAudioState`, `openAudio`, `OpenAudioRequest`, `OpenAudioResult`, `CallAudio` on the result's `audio` |
|
|
2734
|
+
| capabilitySource values ungoverned and undetermined | `nobody`, `not-yet-read` (`capabilitySource.notYetRead`) |
|
|
2735
|
+
| Snapshot.scheduledActivities, and scheduledActivities on calendar-updated | `calendar` in both |
|
|
2736
|
+
| availability reserved | `elsewhere` |
|
|
2737
|
+
| recording destinationId, HostRecording.destinationIds | `storageId`, `storageIds` (`recording.storage`, `recording.host.storage`) |
|
|
2742
2738
|
|
|
2743
2739
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2744
2740
|
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
@@ -2782,16 +2778,16 @@ Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conf
|
|
|
2782
2778
|
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
2783
2779
|
entry after the first, never a revision of it. A leg that has ended states its duration, and the
|
|
2784
2780
|
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2785
|
-
only while its
|
|
2781
|
+
only while its audio is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2786
2782
|
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2787
2783
|
(`task.history.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2788
2784
|
provider whose platform parks the caller, the agent application whose microphone it is — by restating the entry
|
|
2789
2785
|
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2790
|
-
plausible nought one level down from a total. **A leg still open when the
|
|
2791
|
-
the provider**, at that instant, in the same publication that says the
|
|
2786
|
+
plausible nought one level down from a total. **A leg still open when the audio ends is closed by
|
|
2787
|
+
the provider**, at that instant, in the same publication that says the audio ended: the provider
|
|
2792
2788
|
is the one that knows the instant, and the agent application can only learn of it afterwards. Agents end calls
|
|
2793
|
-
muted, so the agent application's leg is routinely open at
|
|
2794
|
-
from the leg's `at` to the
|
|
2789
|
+
muted, so the agent application's leg is routinely open at audio end; the provider closes it with the duration
|
|
2790
|
+
from the leg's `at` to the audio's end, and the agent application's own closing report for that leg, which
|
|
2795
2791
|
follows what it hears, is answered `recorded` and changes nothing, the entry standing as the
|
|
2796
2792
|
provider closed it. One publisher of the closed leg, and no order between them to get right. There is no resumed step; a resume is the end of a
|
|
2797
2793
|
hold, at the entry's `at` plus its `seconds`, and nothing is lost by not naming it twice. The same goes for every step: two mutes are two
|
|
@@ -2838,7 +2834,7 @@ or combine them: report the representation conflict. No ambiguous event is silen
|
|
|
2838
2834
|
|
|
2839
2835
|
**Handle time is anchored, not restarted.** It runs from the
|
|
2840
2836
|
`answered` step's `at` — from the task's first `in-progress` where the provider reports no
|
|
2841
|
-
history — until the task's
|
|
2837
|
+
history — until the task's audio ends, and a hold neither pauses nor resets it: the hold's own
|
|
2842
2838
|
duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
|
|
2843
2839
|
counting the wrong thing.
|
|
2844
2840
|
|
|
@@ -2938,7 +2934,7 @@ the agent application mutes nothing and records nothing.
|
|
|
2938
2934
|
**The provider's confirmed end is decisive.** The agent application keeps local state for interaction and
|
|
2939
2935
|
reporting, but follows the provider's authoritative state. When the provider publishes the end
|
|
2940
2936
|
of the current mute leg, the agent application ends that local mute and releases its agent application-controlled mute
|
|
2941
|
-
on the matching task's
|
|
2937
|
+
on the matching task's audio. It does not wait for its own timer, reopen the leg, replace the
|
|
2942
2938
|
provider timestamp, or overwrite the provider's final duration with a later agent application report.
|
|
2943
2939
|
A later agent mute is a new leg, never a reopening of the ended one.
|
|
2944
2940
|
|
|
@@ -2950,7 +2946,7 @@ retain the current provider view and apply the matching closure once correlation
|
|
|
2950
2946
|
never guess a match from arrival order or nearest timestamp. Failure to release an agent application-controlled
|
|
2951
2947
|
mute is reported visibly; local device reports still describe the actual device state.
|
|
2952
2948
|
|
|
2953
|
-
At a provider-confirmed task/
|
|
2949
|
+
At a provider-confirmed task/audio end, the agent application also stops the associated local mute and reports
|
|
2954
2950
|
its observed ending where the report is still accepted. An agent application closing report repeats its original
|
|
2955
2951
|
key and may include its measured duration, but cannot reverse an already confirmed provider end.
|
|
2956
2952
|
The provider acknowledges a known closed leg without rewriting its final record; after the task
|
|
@@ -3078,7 +3074,7 @@ from no other source -- not the queue the task came from, not the login, not its
|
|
|
3078
3074
|
about this task, and a client with two sources and a rule for choosing between them is the shape
|
|
3079
3075
|
that produces two consumers disagreeing about one fact. The task is the one source, and the
|
|
3080
3076
|
provider puts on the task what the platform permits for it -- and says, in `capabilitySource`, who
|
|
3081
|
-
chose those terms. `queue` says somebody configured them. `
|
|
3077
|
+
chose those terms. `queue` says somebody configured them. `nobody` says nothing handed the work
|
|
3082
3078
|
over: an agent's own outbound call has no queue behind it, and the provider states what it permits
|
|
3083
3079
|
for such a call. Neither changes where an agent application reads the capabilities from, which is the task; the
|
|
3084
3080
|
source is one more published fact about them, and the one that lets an agent application tell a fact from a fault
|
|
@@ -3116,15 +3112,15 @@ direction: the task as offered and as republished, and one command that was issu
|
|
|
3116
3112
|
first and is refused under the last for want of the capability withdrawn -- and nothing else.
|
|
3117
3113
|
|
|
3118
3114
|
**An empty capability set is a statement, not a shrug.** `capabilities: {}` under `queue` or
|
|
3119
|
-
`
|
|
3115
|
+
`nobody` says the platform permits nothing capability-gated on this task, and an agent application draws
|
|
3120
3116
|
nothing beyond what the phase and `completionMode` require. A provider that has not yet learned
|
|
3121
3117
|
what the platform permits -- a queue's configuration that has not reached it -- knows nothing of
|
|
3122
3118
|
the kind, and must not publish the task as if it did, neither as `{}` nor as every control it has.
|
|
3123
|
-
It publishes the task under `capabilitySource: "
|
|
3119
|
+
It publishes the task under `capabilitySource: "not-yet-read"`, with the capabilities it will
|
|
3124
3120
|
honour until it knows, and an agent application shows that where the agent works, beside the controls it draws
|
|
3125
3121
|
from them: "no queue governs this call" is a fact, "the configuration has not arrived" is a fault,
|
|
3126
3122
|
and the set alone cannot tell them apart, so the provider says which. What a provider honours
|
|
3127
|
-
under
|
|
3123
|
+
under not-yet-read terms is its own call -- a floor that would rather an agent briefly hold a
|
|
3128
3124
|
control the platform might not have granted than lose hold or hang-up mid-call over a slow
|
|
3129
3125
|
configuration read publishes those -- and the protocol chooses no default set for it. The fault is
|
|
3130
3126
|
also reported as a `diagnostic` naming the task, one per occurrence, so an operator counts it. When
|
|
@@ -3132,12 +3128,12 @@ the terms arrive, the task is republished under `queue` with the set as it now s
|
|
|
3132
3128
|
republish as any other permission that changed while the task was open. The move goes one way.
|
|
3133
3129
|
Terms once read stay read: a re-read that fails mid-task is not a new fact about the task, so the
|
|
3134
3130
|
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
3135
|
-
`queue` or `
|
|
3131
|
+
`queue` or `nobody` never returns to `not-yet-read`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
3136
3132
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
3137
3133
|
entry the same two ways (`stream.taskUpdated.history`, `stream.snapshot.history`);
|
|
3138
|
-
a snapshot carrying a task still at work does not forget the audio the stream held up, since
|
|
3139
|
-
ends on `task-
|
|
3140
|
-
its audio ends, never around it, whatever the outcome (`stream.taskEnded.
|
|
3134
|
+
a snapshot carrying a task still at work does not forget the audio the stream held up, since audio
|
|
3135
|
+
ends on `task-audio-ended` and the call moves on (`stream.snapshot.audio`); a voice task ends after
|
|
3136
|
+
its audio ends, never around it, whatever the outcome (`stream.taskEnded.audioOpen`); and a task does not go
|
|
3141
3137
|
backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
|
|
3142
3138
|
stream sees publications, not
|
|
3143
3139
|
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
@@ -3146,32 +3142,32 @@ the transition table -- back to `pending`, back to `confirmed` or `preview` once
|
|
|
3146
3142
|
`completing` except by the party being dialled again, a connect-back or a platform's callback,
|
|
3147
3143
|
which the update itself shows: the party ringing, or joined by a dial whose answered outcome the
|
|
3148
3144
|
stream saw in this life. A completing task republished as `in-progress` from a stale copy carries
|
|
3149
|
-
no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-
|
|
3150
|
-
is refused as it is on a pending one (`stream.
|
|
3151
|
-
returns the task to `in-progress` before any
|
|
3152
|
-
never around it: an update moving a task to `completing` while the stream holds its
|
|
3153
|
-
started is refused (`stream.taskUpdated.
|
|
3154
|
-
`completing` with `
|
|
3155
|
-
(`task.
|
|
3145
|
+
no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-audio-started` on a `completing` task
|
|
3146
|
+
is refused as it is on a pending one (`stream.taskAudioStarted.beforeWork`), since a connect-back
|
|
3147
|
+
returns the task to `in-progress` before any audio. And a task completes after its audio ends,
|
|
3148
|
+
never around it: an update moving a task to `completing` while the stream holds its audio as
|
|
3149
|
+
started is refused (`stream.taskUpdated.audioOpen`), whoever caused the ending, and a task stating
|
|
3150
|
+
`completing` with `audio: "started"` contradicts itself on any snapshot or update
|
|
3151
|
+
(`task.audio.completing`). The consequence the rule exists for is concrete: the customer's audio
|
|
3156
3152
|
keeps playing through the agent's wrap-up.
|
|
3157
3153
|
|
|
3158
3154
|
What the agent is told differs by source, and only one source tells them anything. Under `queue`
|
|
3159
|
-
and `
|
|
3155
|
+
and `nobody` the agent sees controls and nothing about where they came from: both are facts,
|
|
3160
3156
|
and an agent working a call has no use for the name of the rule behind its buttons. Under
|
|
3161
|
-
`
|
|
3157
|
+
`not-yet-read` the agent is told, beside the controls, that these are what the provider will honour
|
|
3162
3158
|
until the queue's terms arrive, and that the controls may change when they do -- a statement about
|
|
3163
3159
|
the buttons in front of them now, not about the provider, because that is what changes when the
|
|
3164
3160
|
republish lands. It stays for as long as the set is provisional, beside the controls it qualifies;
|
|
3165
3161
|
a message that shows and clears has said nothing about the buttons still on the screen. The two
|
|
3166
|
-
words are close in English and far apart on the desk: `
|
|
3162
|
+
words are close in English and far apart on the desk: `nobody` is silence, `not-yet-read` is a
|
|
3167
3163
|
standing notice.
|
|
3168
3164
|
|
|
3169
|
-
One consequence for whoever builds the agent application: because a conformance run fails on `
|
|
3170
|
-
conformant adapter never shows an agent application `
|
|
3165
|
+
One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
|
|
3166
|
+
conformant adapter never shows an agent application `not-yet-read` under test, and a clean `exerciseAdapter`
|
|
3171
3167
|
result says nothing about how the agent application renders it. That rendering is tested against a fixture --
|
|
3172
|
-
a task published under `
|
|
3168
|
+
a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
|
|
3173
3169
|
and never against an adapter, even in principle. `exerciseAdapter` treats a
|
|
3174
|
-
task published under `
|
|
3170
|
+
task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
|
|
3175
3171
|
a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
|
|
3176
3172
|
rather than passing with a note.
|
|
3177
3173
|
|
|
@@ -3197,7 +3193,8 @@ See **Which commands need a capability**.
|
|
|
3197
3193
|
| --- | --- | --- |
|
|
3198
3194
|
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
|
|
3199
3195
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
3200
|
-
| `endCall` | Primary button: End call | The provider ends the
|
|
3196
|
+
| `endCall` | Primary button: End call | The provider ends the agent's channel and every channel the agent added; the caller continues on the provider's path. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3197
|
+
| `terminateCall` | Primary button: Terminate call | The provider ends the caller channel. The task goes to its wrap. See **Ending a call, and removing one person from it**. |
|
|
3201
3198
|
| `connectBack` | Completing-task button: Connect back | Omni may have the provider connect the agent back to the task's party while the task is `completing`, returning it to `in-progress` on the same task. Not offered where there is no `completing` window: `provider-automatic` with a zero allowance completes the task at provider end. See **Connecting back during completion**. |
|
|
3202
3199
|
| `coldTransfer` | Secondary menu item: Cold transfer | Omni may hand the customer straight to a destination, with nobody spoken to first. |
|
|
3203
3200
|
| `warmTransfer` | Secondary menu item: Warm transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Warm transfer**. |
|
|
@@ -3292,50 +3289,41 @@ end the caller journey; a later assignment to the same agent is a new interactio
|
|
|
3292
3289
|
|
|
3293
3290
|
#### Ending a call, and removing one person from it
|
|
3294
3291
|
|
|
3295
|
-
A call has everyone on `Task.onCall`, and
|
|
3296
|
-
provider.
|
|
3292
|
+
A call has everyone on `Task.onCall`, and three commands take people off it, all performed by the
|
|
3293
|
+
provider. Two of them end the agent's part; they differ in what happens to the caller.
|
|
3297
3294
|
|
|
3298
3295
|
```ts
|
|
3299
|
-
{ type: "end-call" } //
|
|
3296
|
+
{ type: "end-call" } // the agent's channel and the channels the agent added end; the caller continues
|
|
3297
|
+
{ type: "terminate-call" } // the caller channel ends
|
|
3300
3298
|
{ type: "conference", action: "remove", party: true } // the customer leaves; the agent stays with the colleague
|
|
3301
3299
|
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
3302
3300
|
```
|
|
3303
3301
|
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
the
|
|
3325
|
-
the
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
caller/channel disconnect. Other agents' independent tasks end or wrap only on their own source
|
|
3330
|
-
transitions. Neither completion nor transfer itself ends the continuing caller journey.
|
|
3331
|
-
|
|
3332
|
-
`conference` `remove` remains a person-specific provider operation, gated by `conference`.
|
|
3333
|
-
It does not implicitly complete any other interaction. Its person/consultation prerequisites
|
|
3334
|
-
remain separate from the permission to disconnect the caller. A remove that would leave the
|
|
3335
|
-
agent alone is not a remove but an `end-call`, and the provider answers it `failed`.
|
|
3336
|
-
The consulted destination of a warm transfer is not removed this way:
|
|
3337
|
-
`transfer` `cancel` is its own step, which returns the agent to the parked customer, and a remove
|
|
3338
|
-
never promises that.
|
|
3302
|
+
**`end-call` ends my part.** The agent's channel and every channel the agent added -- a
|
|
3303
|
+
conferenced colleague, a consulted destination -- end, and the caller continues on the path the
|
|
3304
|
+
provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
|
|
3305
|
+
|
|
3306
|
+
**`terminate-call` ends the caller channel.** The call is over for the caller. Gated by
|
|
3307
|
+
`terminateCall`, a separate permission, since ending a customer's call is a different thing from
|
|
3308
|
+
putting my own side down.
|
|
3309
|
+
|
|
3310
|
+
Both are commands to the provider through `execute`, and nothing happens on the desk until the
|
|
3311
|
+
provider reports it: the agent's audio ends on `task-audio-ended`, the task moves to `completing`,
|
|
3312
|
+
any wrap allowance runs, and the agent completes. Neither is `task-ended`, and `complete` is
|
|
3313
|
+
neither of them. Both capabilities are the queue's to grant and a level's to lock, and both may
|
|
3314
|
+
change while the task is open: the provider republishes the task at the moment a permission
|
|
3315
|
+
changes, and a command that arrives after its capability was withdrawn is refused, as **Task
|
|
3316
|
+
capabilities** sets out. The provider knows which channels are the agent's and which were added;
|
|
3317
|
+
the agent application sends the command and no channel list, and the provider rechecks ownership
|
|
3318
|
+
when it acts.
|
|
3319
|
+
|
|
3320
|
+
`conference` `remove` takes one person off, named as the room names them, and the call goes on
|
|
3321
|
+
for the rest; it is gated by `conference`, since it only means something with a third person on
|
|
3322
|
+
the line. Removing the party leaves the agent with the colleague, a warm hand-over in reverse. A
|
|
3323
|
+
remove that would leave the agent alone is not a remove but an `end-call`, and a provider answers
|
|
3324
|
+
it `failed`. The consulted destination of a warm transfer is not removed this way: `transfer`
|
|
3325
|
+
`cancel` is its own step, which returns the agent to the parked customer, and a remove never
|
|
3326
|
+
promises that.
|
|
3339
3327
|
|
|
3340
3328
|
#### Warm transfer
|
|
3341
3329
|
|
|
@@ -3370,7 +3358,7 @@ that offers `warmTransfer` implements all three.
|
|
|
3370
3358
|
|
|
3371
3359
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
3372
3360
|
dropping the agent's leg. What follows is what follows any call the agent leaves: the agent's
|
|
3373
|
-
|
|
3361
|
+
audio ends and the provider reports `task-audio-ended`, the task moves to `completing`, any wrap
|
|
3374
3362
|
allowance runs, and the task ends `completed` as any call does. A warm complete is not a completion:
|
|
3375
3363
|
the agent has a wrap to do, so nothing is owed within `completionSettleMs`. `transferred` names a
|
|
3376
3364
|
cold transfer alone, where the agent had no wrap. `applied` on `cancel` says the destination is
|
|
@@ -3456,7 +3444,7 @@ inside Omni.
|
|
|
3456
3444
|
|
|
3457
3445
|
## Every dial has an outcome
|
|
3458
3446
|
|
|
3459
|
-
Five commands place a call: `dial` from the idle dialpad, `
|
|
3447
|
+
Five commands place a call: `dial()` from the idle dialpad, the `dial` command from preview, a cold or warm `transfer`, a
|
|
3460
3448
|
`conference` `add`, and `connect-back`. Each is accepted or refused at once, and each then ends later
|
|
3461
3449
|
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
3462
3450
|
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
@@ -3562,7 +3550,7 @@ never saw.
|
|
|
3562
3550
|
|
|
3563
3551
|
**`onCall` describes this agent's current interaction.** When the caller disconnects but the agent
|
|
3564
3552
|
and added channels remain connected, the remaining room is still valid. When this interaction's
|
|
3565
|
-
|
|
3553
|
+
audio ends or the task enters `completing`, clear its `onCall` view; absence is used only by a
|
|
3566
3554
|
provider that never publishes the room. This does not assert that the caller, bridge or other
|
|
3567
3555
|
agents' channels ended. A published room receives a final empty view for this interaction, and the
|
|
3568
3556
|
task may remain open for wrap. Completion is a separate task action, not the caller's disconnect.
|
|
@@ -3587,10 +3575,10 @@ still-ringing `destinationId` calls that dial off, with `cancelled` as its outco
|
|
|
3587
3575
|
|
|
3588
3576
|
**The entry says where it stands.** A dialled entry carries `stage`: `ringing` until its dial is
|
|
3589
3577
|
answered, `joined` after. The `dial-outcome` is the transition and the stage is the state, the same
|
|
3590
|
-
pairing as `task-
|
|
3578
|
+
pairing as `task-audio-started` and `audio`: an `answered` outcome and the task restated with the
|
|
3591
3579
|
entry `joined` say one thing twice, and any other outcome removes the entry. The outcome comes
|
|
3592
3580
|
first: a `task-updated` that itself moves an entry from `ringing` to `joined` before an `answered`
|
|
3593
|
-
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `
|
|
3581
|
+
outcome for its dial is refused (`stream.taskUpdated.stage`), as an update that moves `audio` is.
|
|
3594
3582
|
The rule keys on the entry's `dialId`, so an entry the platform added itself, with none, is one
|
|
3595
3583
|
whose move the stream cannot hold to an outcome: a clean stream proves nothing about ordering there. It is stated rather
|
|
3596
3584
|
than left to whoever placed the dial because that knowledge lives in one process: after a reload,
|
|
@@ -3632,7 +3620,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
3632
3620
|
| `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
|
|
3633
3621
|
| `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
|
|
3634
3622
|
| `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `on-break`. |
|
|
3635
|
-
| `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a lead
|
|
3623
|
+
| `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a call a lead took over, which the provider assigns whatever break the lead is on -- see **Lead assist**. |
|
|
3636
3624
|
|
|
3637
3625
|
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
3638
3626
|
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
@@ -3807,7 +3795,7 @@ Your own tasks are the only ones you count. What the agent holds at other provid
|
|
|
3807
3795
|
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3808
3796
|
providers, and the agent application divides their capacity among them rather than telling each the whole. A
|
|
3809
3797
|
provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
|
|
3810
|
-
the member as `
|
|
3798
|
+
the member as `elsewhere` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
|
|
3811
3799
|
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3812
3800
|
count as any other when the agent application has capacity for this provider again. Zero is the one restatement
|
|
3813
3801
|
that follows work rather than local policy.
|
|
@@ -3843,12 +3831,15 @@ request object only to the request method, and undefined to the other three.
|
|
|
3843
3831
|
| `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
3844
3832
|
| `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
|
|
3845
3833
|
|
|
3846
|
-
Use `
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3834
|
+
Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
|
|
3835
|
+
decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
|
|
3836
|
+
the live `lead` flag and active transport, a current team member list for any command naming a
|
|
3837
|
+
member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
|
|
3838
|
+
awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
|
|
3839
|
+
forced committed break; join and decline need the member's request on the list; coach, join-call
|
|
3840
|
+
and leave need the lead already on that member's call; an assignment named must be one the list
|
|
3841
|
+
shows the member holding. The context's target state must belong to the named member; that
|
|
3842
|
+
association and backend authorization are provider responsibilities.
|
|
3852
3843
|
|
|
3853
3844
|
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
3854
3845
|
as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
|
|
@@ -4116,22 +4107,23 @@ so there is nothing on it to resume.
|
|
|
4116
4107
|
|
|
4117
4108
|
## Team leads
|
|
4118
4109
|
|
|
4119
|
-
|
|
4120
|
-
`
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
bare array. `Snapshot.team`, the `team-updated` event and its `team` payload, login permissions,
|
|
4126
|
-
and `team.*` validation rules keep their names and semantics. An empty team has `members: []`;
|
|
4127
|
-
an absent `team` still means the login is not entitled to that contribution.
|
|
4110
|
+
The team feature is the lead's own surface, beside the agent's and never mixed with it. The
|
|
4111
|
+
login's `lead` flag turns it on in the agent application; the lead retrieves their members and
|
|
4112
|
+
sees, for each, their assignments as they stand and the full history of each, and acts on them
|
|
4113
|
+
with the authority the flag carries. Nothing the lead does here is a task on their desk. A lead
|
|
4114
|
+
who also takes calls is an agent like any other on the agent side, and a lead who does not want
|
|
4115
|
+
the agent role for now places themself on a break of a working kind and keeps the team feature on.
|
|
4128
4116
|
|
|
4117
|
+
`Snapshot.team` carries a `TeamMembers` object, replaced whole by `team-updated`, and the provider
|
|
4118
|
+
keeps it current: when a member is offered a call, answers, holds, mutes or is transferred, the
|
|
4119
|
+
list is republished, so the lead's screen moves when the member's does. The agent application asks
|
|
4120
|
+
for nothing and computes nothing.
|
|
4129
4121
|
|
|
4130
4122
|
| Field | Contract |
|
|
4131
4123
|
| --- | --- |
|
|
4132
|
-
| `members` | Every member of this lead's team, whatever their state. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
|
|
4133
|
-
| `requests` | The members currently asking
|
|
4134
|
-
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep.
|
|
4124
|
+
| `members` | Every member of this lead's team, whatever their state, each with their open tasks. `[]` says the lead has a team with nobody in it; omitting the team member list says something else entirely — see **The login is the permission** below. |
|
|
4125
|
+
| `requests` | The members currently asking a lead to join their call, each naming the member, the assignment and the note. Always carried, `[]` when nobody is asking. See **Lead assist**. |
|
|
4126
|
+
| `policies` | The team's policy per capability as it stands — the setting, who set it, and `lockedBy` where a level above the team made it theirs to keep. Present where the provider offers policy control; absent where it does not. See **Who decides what an agent may do**. |
|
|
4135
4127
|
|
|
4136
4128
|
| `TeamMember` field | Contract |
|
|
4137
4129
|
| --- | --- |
|
|
@@ -4139,6 +4131,8 @@ an absent `team` still means the login is not entitled to that contribution.
|
|
|
4139
4131
|
| `availability` | Required. What the member is doing now. |
|
|
4140
4132
|
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4141
4133
|
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
4134
|
+
| `tasks` | The member's open tasks as `MemberTask`s: the same task the member's desk holds, trimmed by the provider. The assignment, title, task type, phase, party, room, audio and the full history are what the lead reads; the workspace — controls, their source, browsers, completion terms — is the member's and travels only if the provider sends it. `[]` when the member holds none; omitted only where the provider cannot see them. |
|
|
4135
|
+
| `listening` | Present while this lead's channel is in this member's call — listening, coaching or joined — with the `mode` they are heard in and since when. One member's call at a time (`team.listening.single`). |
|
|
4142
4136
|
|
|
4143
4137
|
Each availability value means one thing:
|
|
4144
4138
|
|
|
@@ -4147,13 +4141,14 @@ Each availability value means one thing:
|
|
|
4147
4141
|
| `ready` | Signed in, able to take work, none assigned. |
|
|
4148
4142
|
| `on-task` | Working on at least one task. It says nothing about how many, and nothing about whether more will fit. |
|
|
4149
4143
|
| `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
|
|
4150
|
-
| `
|
|
4144
|
+
| `elsewhere` | Signed in here, and the agent application holds this agent's capacity for another provider (`count: 0`, agent application-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
|
|
4151
4145
|
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
4152
4146
|
|
|
4153
4147
|
**Always publish the complete team member list, never a change to it.** Team presence typically reaches an
|
|
4154
4148
|
adapter over a best-effort channel with no ordering and no delivery guarantee, so a stream of deltas
|
|
4155
4149
|
cannot be trusted to reconstruct the truth. The adapter reconciles against its own authoritative
|
|
4156
|
-
read and publishes the result.
|
|
4150
|
+
read and publishes the result. That includes the members' tasks: a hold on a member's desk is a
|
|
4151
|
+
republished list on the lead's.
|
|
4157
4152
|
|
|
4158
4153
|
**Omit `since` rather than inventing one.** Omni renders it as a duration, so a timestamp
|
|
4159
4154
|
synthesised from the adapter's own clock at seed time reads as "on task for 0 seconds" for
|
|
@@ -4161,11 +4156,20 @@ everybody — worse than showing nothing, because it looks like data. Send it on
|
|
|
4161
4156
|
knows when the state actually began. It times the current `availability`, so it moves every time
|
|
4162
4157
|
that value does.
|
|
4163
4158
|
|
|
4164
|
-
**The login is the permission.** A team member list goes to a login that declares
|
|
4165
|
-
every snapshot, and to nobody else. Omni never
|
|
4166
|
-
sign-in, and the team member list agrees with it
|
|
4167
|
-
|
|
4168
|
-
|
|
4159
|
+
**The login is the permission.** A team member list goes to a login that declares
|
|
4160
|
+
`capabilities.lead`, on every snapshot while the team feature is on, and to nobody else. Omni never
|
|
4161
|
+
decides who leads a team: the provider said so at sign-in, and the team member list agrees with it
|
|
4162
|
+
— present, `members: []` included, for a lead; absent for everybody else, which is the correct
|
|
4163
|
+
rendering for an agent who leads nobody (`team.required`, `team.unentitled`). What the lead may do
|
|
4164
|
+
with it comes with the flag: there is no per-action permission on the login or on the list.
|
|
4165
|
+
|
|
4166
|
+
**The lead switches the feature on and off, and the provider is told.** A lead may work as an
|
|
4167
|
+
agent alone, as agent and lead, or as lead alone, and moves between them on the fly. The
|
|
4168
|
+
agent application says which with `{ type: "lead-features", enabled }`: while it is on, the
|
|
4169
|
+
provider sends team events scoped to the lead's team and the lead may act; while it is off, the
|
|
4170
|
+
lead is an ordinary agent, no team member list is owed or expected (`team.unexpected`,
|
|
4171
|
+
`event.team.features`), and no lead act is possible, since the lead learns their members'
|
|
4172
|
+
assignments only from the list. It is on from sign-in for a lead until the lead turns it off.
|
|
4169
4173
|
|
|
4170
4174
|
**The team member list never carries the agent it is published to — not in `members`, and not in
|
|
4171
4175
|
`requests`.** A lead does not report to themself: their own break request and their own ask for a
|
|
@@ -4173,24 +4177,36 @@ lead go up to whoever leads them and appear on *that* person's team member list,
|
|
|
4173
4177
|
only their own `BreakState` and their task's `leadAssist` move. An adapter whose platform lists the lead
|
|
4174
4178
|
among their own members filters the signed-in identity out before publishing. **Being a lead is a
|
|
4175
4179
|
role the provider knows, never inferred from who is listed:** it is declared at sign-in, a lead
|
|
4176
|
-
with nobody in their team publishes `[]`, an agent with no such role publishes nothing, and no
|
|
4180
|
+
with nobody in their team publishes `members: []`, an agent with no such role publishes nothing, and no
|
|
4177
4181
|
member count can tell those two apart.
|
|
4178
4182
|
|
|
4179
4183
|
### Lead commands
|
|
4180
4184
|
|
|
4181
|
-
One method, `
|
|
4182
|
-
`TaskCommand
|
|
4185
|
+
One method, `executeTeam`, taking a discriminated command exactly as `execute` takes a
|
|
4186
|
+
`TaskCommand`. Every act on a member's call names the member, `memberId`, and may name the
|
|
4187
|
+
assignment, `assignmentId`, where the lead has it; the provider resolves the member's current
|
|
4188
|
+
assignment from the member alone.
|
|
4183
4189
|
|
|
4184
4190
|
| Command | Effect |
|
|
4185
4191
|
| --- | --- |
|
|
4186
|
-
| `{ type: "
|
|
4192
|
+
| `{ type: "lead-features", enabled }` | The team feature on or off. See **The lead switches the feature on and off** above. |
|
|
4193
|
+
| `{ type: "decide-break-request", memberId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
|
|
4187
4194
|
| `{ type: "set-break-policy", policy }` | `approval-required`, `automatically-approved`, or `requests-suspended`. |
|
|
4188
|
-
| `{ type: "force-break", memberId
|
|
4189
|
-
| `{ type: "end-forced-break", memberId
|
|
4195
|
+
| `{ type: "force-break", memberId, reasonId?, reason?, expectedDurationMs? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's forced break carries it as `activeReasonId`, so its kind is known. Optional `expectedDurationMs` is advisory and is published on the resulting forced break. |
|
|
4196
|
+
| `{ type: "end-forced-break", memberId }` | Lifts the forced-break restriction, whoever forced it, by clearing `BreakState.forced`. The committed break continues; only the agent resumes work. |
|
|
4197
|
+
| `{ type: "join", memberId, assignmentId? }` | Answers the member's request: the provider bridges the lead's channel into the call. See **Lead assist**. |
|
|
4198
|
+
| `{ type: "decline", memberId, assignmentId?, reason? }` | Refuses the member's request. |
|
|
4199
|
+
| `{ type: "listen", memberId, assignmentId? }` | The lead's channel joins the member's call unasked, in silence. See **Listening to a call**. |
|
|
4200
|
+
| `{ type: "coach", memberId, assignmentId? }` | The lead is heard by the member alone. Needs the lead on that member's call. |
|
|
4201
|
+
| `{ type: "join-call", memberId, assignmentId? }` | The lead is heard by everyone on the call. Needs the lead on that member's call. |
|
|
4202
|
+
| `{ type: "leave", memberId, assignmentId? }` | The lead's channel leaves the member's call, however it got there. The member's call goes on. |
|
|
4203
|
+
| `{ type: "take-over-call", memberId, assignmentId? }` | The provider moves the call to the lead as its new agent, from a joined call or a listened one alike. See **Lead assist**. |
|
|
4204
|
+
| `{ type: "set-policy", capability, setting }` | Sets the team's policy for one capability: `on`, `off`, or `person`. See **Who decides what an agent may do**. |
|
|
4190
4205
|
|
|
4191
4206
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4192
4207
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4193
|
-
from `getUserDetails()`.
|
|
4208
|
+
from `getUserDetails()`. `validateTeamCommand` holds each command to the team member list as it
|
|
4209
|
+
stands -- see **Runtime break prerequisite checks**.
|
|
4194
4210
|
|
|
4195
4211
|
`requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4196
4212
|
approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
|
|
@@ -4204,71 +4220,56 @@ never expressed here.
|
|
|
4204
4220
|
|
|
4205
4221
|
An agent on a call may ask a lead to join it -- a dispute that needs approval, a customer who
|
|
4206
4222
|
asks for a manager, a moment the agent wants a second pair of ears. A call centre calls this
|
|
4207
|
-
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the
|
|
4208
|
-
team,
|
|
4209
|
-
|
|
4210
|
-
```ts
|
|
4211
|
-
executeTeamLeadAssist({ command: TeamLeadAssistCommand }): Promise<TeamCommandResult>
|
|
4212
|
-
```
|
|
4213
|
-
|
|
4214
|
-
Required when the login declares `capabilities.team.leadAssistControl`, and gated by it exactly as
|
|
4215
|
-
`executeTeamBreak` is by `team.breakControl`. The flow, in order:
|
|
4223
|
+
assistance or escalation, and the name says who assists: the capability is `leadAssist` on the
|
|
4224
|
+
task; the lead's side is the team member list, which is already the lead's view of the team. The
|
|
4225
|
+
flow, in order:
|
|
4216
4226
|
|
|
4217
4227
|
```ts
|
|
4218
4228
|
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
4219
4229
|
execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
4220
4230
|
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
4221
4231
|
|
|
4222
|
-
// 2. Every lead
|
|
4223
|
-
// team-updated: requests: [{
|
|
4232
|
+
// 2. Every lead with the team feature on sees the request on their team member list.
|
|
4233
|
+
// team-updated: requests: [{ memberId: "A-1", assignmentId: "alloc-42", note, since }]
|
|
4224
4234
|
|
|
4225
4235
|
// 3. A lead joins, or declines.
|
|
4226
|
-
|
|
4227
|
-
|
|
4236
|
+
executeTeam({ command: { type: "join", memberId: "A-1", assignmentId: "alloc-42" } })
|
|
4237
|
+
executeTeam({ command: { type: "decline", memberId: "A-1", reason: "In a call" } })
|
|
4228
4238
|
```
|
|
4229
4239
|
|
|
4230
|
-
**On `join` the provider bridges
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
they joined -- is answered `failed`, whatever their ceiling; the request stands for another lead,
|
|
4241
|
-
or until it is withdrawn or declined.
|
|
4242
|
-
|
|
4243
|
-
**A lead on a break does not join.** A break is a reported state in which the agent is not working,
|
|
4244
|
-
and a join is work. Omni offers Join to a lead only while their own `BreakState.status` is neither
|
|
4245
|
-
`starting-after-task` nor `on-break` -- a committed break waiting for the lead's current work to
|
|
4246
|
-
finish is not given more -- and a provider answers a `join` from a lead on such a break `failed`.
|
|
4247
|
-
The request stands for another lead, as it does when this one is already on a call.
|
|
4240
|
+
**On `join` the provider bridges the lead's channel into the call.** The member's task moves to
|
|
4241
|
+
`leadAssist: { stage: "joined", leadId }`, the member on the team member list carries
|
|
4242
|
+
`listening: { mode: "join-call", since }`, and the lead's audio arrives as **Listening to a call**
|
|
4243
|
+
describes. Nothing is a task on the lead's desk: a join is the lead's act on the team surface, not
|
|
4244
|
+
work assigned to them.
|
|
4245
|
+
|
|
4246
|
+
**A lead is on one member's call at a time.** A `join` from a lead already on a call -- their own
|
|
4247
|
+
or a member's -- is answered `failed`; the request stands for another lead, or until it is
|
|
4248
|
+
withdrawn or declined. A join is work, so Omni offers Join only to a lead whose voice channel is
|
|
4249
|
+
free, and a provider answers a `join` from one whose channel is not `failed`.
|
|
4248
4250
|
|
|
4249
4251
|
**On `decline`, or a request the agent withdraws with `{ type: "lead-assist", action: "cancel" }`, the
|
|
4250
4252
|
provider clears `leadAssist` from the agent's task** and drops the request from every team member list. Nothing
|
|
4251
4253
|
else changes; the agent is still on the call.
|
|
4252
4254
|
|
|
4253
|
-
**
|
|
4254
|
-
|
|
4255
|
-
Migration: replace the former take-over action literal with `take-over-call` in hosts and
|
|
4256
|
-
providers together. The former spelling is rejected, with no compatibility alias.
|
|
4257
|
-
The `taken-over` task outcome and existing completion behavior remain unchanged.
|
|
4258
|
-
This action is distinct from `join-call`, which lets everyone hear a lead listening to a call.
|
|
4255
|
+
**On `leave`, the lead's channel drops and the call goes on**: the member's task loses its
|
|
4256
|
+
`leadAssist`, the member loses `listening`, and the lead is free. Nothing ended for the member.
|
|
4259
4257
|
|
|
4260
|
-
|
|
4261
|
-
|
|
4258
|
+
**On `take-over-call`, the provider moves the call from the member to the lead**, and the lead is
|
|
4259
|
+
its new agent. There are two paths to it -- the agent asked and the lead joined, or the lead was
|
|
4260
|
+
listening unasked -- and one act at the end of either. What follows is the same on both:
|
|
4262
4261
|
|
|
4263
|
-
|
|
|
4262
|
+
| | The member's task | The lead's desk |
|
|
4264
4263
|
| --- | --- | --- |
|
|
4265
|
-
|
|
|
4266
|
-
| `{ type: "
|
|
4267
|
-
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own outcome | The same, independently: **both have the completion window** |
|
|
4264
|
+
| Take-over | `task-audio-ended`, then `completing`: the member's interaction is over and their wrap runs, exactly as after `end-call`. The audio ends first, as before every voice ending (`stream.taskEnded.audioOpen`). | An ordinary assignment arrives: `task-offered` with `acceptance: "automatic"`, carrying the call's history and `takenOver: { memberId, since }`. |
|
|
4265
|
+
| The member completes | `task-ended` with `{ type: "taken-over", leadId }`, at the member's own completion. The lead is named by user id, since a lead is not a directory item. | The lead works the call as any agent would, and ends it as any call ends. |
|
|
4268
4266
|
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4267
|
+
**The taken-over call is offered whatever break the lead is on.** A lead working as lead alone is
|
|
4268
|
+
on a break of a working kind, and the provider assigns the call regardless: it is the one task a
|
|
4269
|
+
break in effect may hold (`break.on-break.tasks` allows it, and nothing else). It is the lead's own
|
|
4270
|
+
act, so it counts against no capacity, like the agent's own dial; the only conditions are that the
|
|
4271
|
+
lead is not already on another call and their voice channel is free. A lead with the team feature
|
|
4272
|
+
off cannot take over, since they have no member's assignment to name.
|
|
4272
4273
|
|
|
4273
4274
|
```ts
|
|
4274
4275
|
const leadAssistCapable = {
|
|
@@ -4277,6 +4278,13 @@ const leadAssistCapable = {
|
|
|
4277
4278
|
phase: "in-progress",
|
|
4278
4279
|
leadAssist: { stage: "joined", leadId: "L-9", note: "Refund dispute, needs approval", since: "2026-08-21T09:04:00Z" },
|
|
4279
4280
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "leadAssist">;
|
|
4281
|
+
|
|
4282
|
+
const takenOver = {
|
|
4283
|
+
channel: "voice",
|
|
4284
|
+
capabilities: { hold: true, endCall: true, outcomes: true },
|
|
4285
|
+
phase: "in-progress",
|
|
4286
|
+
takenOver: { memberId: "A-1", since: "2026-08-21T09:06:00Z" },
|
|
4287
|
+
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "takenOver">;
|
|
4280
4288
|
```
|
|
4281
4289
|
|
|
4282
4290
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
@@ -4284,44 +4292,10 @@ Lead and member alike are `UserId`s of this provider, so an adapter publishing t
|
|
|
4284
4292
|
|
|
4285
4293
|
### Listening to a call
|
|
4286
4294
|
|
|
4287
|
-
**Listen** lets the lead hear the call without being heard. The modes are `listen`, `coach`
|
|
4288
|
-
(agent-only lead audio), and `join-call` (lead audio heard by everyone).
|
|
4289
|
-
|
|
4290
|
-
| Former API | Current API |
|
|
4291
|
-
| --- | --- |
|
|
4292
|
-
| MonitorMode / MONITOR_MODES | `ListeningMode` / `LISTENING_MODES` |
|
|
4293
|
-
| TeamMonitorCommand / TeamMonitorCommandRequest | `TeamListenCommand` / `TeamListenCommandRequest` |
|
|
4294
|
-
| executeTeamMonitor | `executeTeamListen` |
|
|
4295
|
-
| team.monitorControl | `team.listeningControl` |
|
|
4296
|
-
| TaskMonitoring / task.monitoring | `TaskListening` / the task’s `listening` field |
|
|
4297
|
-
| MONITORING_BREAK_KINDS / breakKindAllowsMonitoring | `LISTENING_BREAK_KINDS` / `breakKindAllowsListening` |
|
|
4298
|
-
| monitor action and mode | `listen` action and mode |
|
|
4299
|
-
|
|
4300
|
-
Update command dispatch, allowed modes, task snapshots, imports and diagnostics together.
|
|
4301
|
-
The former names are not aliases. Old task and permission fields are rejected even alongside
|
|
4302
|
-
new fields. Validation rule segments now use `listening`, `listeningControl`, and `listen`.
|
|
4303
|
-
The existing login permissions, single-call limits, allowed break kinds and audio effects
|
|
4304
|
-
remain unchanged. Listening includes its coach/join-call modes and does not imply takeover.
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
**Coach** lets the lead speak privately to the agent; the caller cannot hear the lead.
|
|
4308
|
-
The command and listening mode are `coach`. Migration: replace the former whisper literal
|
|
4309
|
-
in `TeamListenCommand`, `ListeningMode`, `team.listeningControl`, and the task’s `listening.mode`.
|
|
4310
|
-
`LISTENING_MODES` exports `coach`; update command producers, permissions and retained snapshots
|
|
4311
|
-
together. No legacy alias is accepted. The lead must already be listening and permitted to
|
|
4312
|
-
coach. This mode does not join the audible caller conversation or take over the task.
|
|
4313
|
-
|
|
4314
|
-
The user-facing action is **Join call**. The command and listening mode are `join-call`.
|
|
4315
|
-
Migration: replace the former barge literal in `TeamListenCommand`, `ListeningMode`,
|
|
4316
|
-
`team.listeningControl`, and the task’s `listening.mode`. `LISTENING_MODES` exports the new literal;
|
|
4317
|
-
no legacy alias is accepted. Update commands, capability declarations and retained snapshots
|
|
4318
|
-
together. The lead must already be listening and permitted to use this mode. Everyone on the
|
|
4319
|
-
call hears the lead; this does not transfer ownership or invoke lead-assist takeover.
|
|
4320
|
-
|
|
4321
4295
|
A lead may listen to a member's call without being asked -- to coach, to check quality, to step in
|
|
4322
|
-
when it goes wrong.
|
|
4323
|
-
|
|
4324
|
-
three modes determine who hears the lead:
|
|
4296
|
+
when it goes wrong. It is a team act naming the member, and no task on the lead's desk carries it:
|
|
4297
|
+
the lead's view of the call is the member's task on the team member list, with its room and its
|
|
4298
|
+
history. The three modes determine who hears the lead:
|
|
4325
4299
|
|
|
4326
4300
|
| Mode | Who hears the lead |
|
|
4327
4301
|
| --- | --- |
|
|
@@ -4329,61 +4303,44 @@ three modes determine who hears the lead:
|
|
|
4329
4303
|
| `coach` | The agent alone. The customer hears nothing. |
|
|
4330
4304
|
| `join-call` | Everyone on the call. |
|
|
4331
4305
|
|
|
4332
|
-
|
|
4333
|
-
includes `listen`, because coach and join-call begin from one
|
|
4334
|
-
(`authentication.capability.team.listeningControl.listen`). A centre that lets every lead listen but
|
|
4335
|
-
reserves joining the call declares `["listen", "coach"]`, and Omni offers no Join call to that lead. The
|
|
4336
|
-
declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssist` is by
|
|
4337
|
-
`team.leadAssistControl`.
|
|
4306
|
+
All three come with the `lead` flag; a centre reserves none of them per lead. The flow:
|
|
4338
4307
|
|
|
4339
4308
|
```ts
|
|
4340
4309
|
// 1. The lead picks a member from the team member list and starts silent.
|
|
4341
|
-
|
|
4310
|
+
executeTeam({ command: { type: "listen", memberId: "A-1" } })
|
|
4311
|
+
// team-updated: the member carries listening: { mode: "listen", since }
|
|
4312
|
+
// team-audio-started: { memberId: "A-1", assignmentId: "alloc-42" } -- the lead's audio attaches
|
|
4342
4313
|
|
|
4343
|
-
// 2. The lead
|
|
4344
|
-
|
|
4345
|
-
|
|
4314
|
+
// 2. The lead changes how they are heard; the provider restates the member with the new mode.
|
|
4315
|
+
executeTeam({ command: { type: "coach", memberId: "A-1" } })
|
|
4316
|
+
executeTeam({ command: { type: "join-call", memberId: "A-1" } })
|
|
4346
4317
|
|
|
4347
|
-
// 3. The lead
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
// 4. The lead leaves; their task ends with `{ type: "left" }`. The member's call goes on.
|
|
4352
|
-
executeTeamListen({ command: { type: "leave" } })
|
|
4318
|
+
// 3. The lead leaves, or takes the call.
|
|
4319
|
+
executeTeam({ command: { type: "leave", memberId: "A-1" } })
|
|
4320
|
+
executeTeam({ command: { type: "take-over-call", memberId: "A-1" } })
|
|
4353
4321
|
```
|
|
4354
4322
|
|
|
4355
|
-
**The lead's
|
|
4356
|
-
|
|
4357
|
-
the
|
|
4358
|
-
|
|
4359
|
-
a
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
(`snapshot.listening.break`). The kind decides, not the reason's name: a centre that wants leads
|
|
4372
|
-
listening during a break declares that break `coaching`, `administrative` or `training`.
|
|
4323
|
+
**The lead's audio arrives through team audio.** While the lead's channel is in a member's call
|
|
4324
|
+
-- listening, coaching or joined on request -- the lead's connection receives `team-audio-started`
|
|
4325
|
+
naming the member and the member's assignment, and on a softphone the agent application opens
|
|
4326
|
+
`openAudio` on that assignment as it does for a task; `team-audio-ended` closes it. Voice only, and
|
|
4327
|
+
to a login that declares `lead` (`event.teamAudio.*`). The member's own audio events are the
|
|
4328
|
+
member's and never reach the lead.
|
|
4329
|
+
|
|
4330
|
+
**`listening` on the member is the state**, restated on every change of mode, and a lead is on
|
|
4331
|
+
one member's call at a time (`team.listening.single`). The member's call ending ends the lead's
|
|
4332
|
+
listening with it: the provider republishes the member without `listening` and sends
|
|
4333
|
+
`team-audio-ended`.
|
|
4334
|
+
|
|
4335
|
+
**A lead listens while holding no call of their own.** Omni offers Listen to a lead whose voice
|
|
4336
|
+
channel is free, and a provider answers a `listen` from one whose channel is not `failed`. That
|
|
4337
|
+
includes a lead on a break: a lead working as lead alone is on a break of a working kind, and
|
|
4338
|
+
listens through it.
|
|
4373
4339
|
|
|
4374
4340
|
**Nothing reaches the member.** The member's task carries no trace of a listening lead in any
|
|
4375
4341
|
mode: not on `onCall`, not in the record. Whether coaching is announced to the agent is the
|
|
4376
4342
|
platform's business and travels on the audio, not on this wire.
|
|
4377
4343
|
|
|
4378
|
-
```ts
|
|
4379
|
-
const listeningLead = {
|
|
4380
|
-
channel: "voice",
|
|
4381
|
-
capabilities: {},
|
|
4382
|
-
phase: "in-progress",
|
|
4383
|
-
listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
|
|
4384
|
-
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
|
|
4385
|
-
```
|
|
4386
|
-
|
|
4387
4344
|
### A member waiting for a break
|
|
4388
4345
|
|
|
4389
4346
|
A member who has asked for a break **keeps working** until Omni commits it, so asking is not an
|
|
@@ -4411,27 +4368,27 @@ whole and replaced whole, and a provider that cannot say omits it.
|
|
|
4411
4368
|
|
|
4412
4369
|
**An agent is not waiting on one person.** Authority is held by several, everyone who holds it
|
|
4413
4370
|
sees the request on their own console, and **any one of them settles it**. Omni offers the
|
|
4414
|
-
decision to every login that declares `
|
|
4371
|
+
decision to every login that declares `lead` and has the team feature on — which is how the provider already
|
|
4415
4372
|
says who may decide — and does not try to work out whose turn it is.
|
|
4416
4373
|
|
|
4417
4374
|
A request needing *more than one* approval is not something this contract describes. There is
|
|
4418
4375
|
no partial state to report and no progress to display: a request is either still owed a
|
|
4419
4376
|
decision or it is not.
|
|
4420
4377
|
|
|
4421
|
-
## Real-time
|
|
4378
|
+
## Real-time audio
|
|
4422
4379
|
|
|
4423
|
-
Every voice provider has
|
|
4380
|
+
Every voice provider has audio: `channel: "voice"` says audio exists. Where the agent hears it
|
|
4424
4381
|
depends on the `phone` the agent application selected at authentication and connect from the manifest's
|
|
4425
4382
|
`phones`. On a softphone, audio lands in Omni; on a desk phone, it lands on the handset and Omni
|
|
4426
|
-
opens no audio. See **How the agent hears the call**.
|
|
4427
|
-
chat and email publish none (`event.
|
|
4383
|
+
opens no audio. See **How the agent hears the call**. Audio transitions are voice-only;
|
|
4384
|
+
chat and email publish none (`event.audio.channel`, `stream.taskAudio.channel`).
|
|
4428
4385
|
|
|
4429
4386
|
The provider owns signalling and the platform's endpoint configuration. The agent application selects the
|
|
4430
4387
|
declared phone mode; it does not enumerate or reconfigure the platform's devices. The selected
|
|
4431
4388
|
mode stays with the login rather than being inferred from a snapshot or a device failure.
|
|
4432
4389
|
|
|
4433
4390
|
Nor does the audio ever stand in for the task: a task's presence and
|
|
4434
|
-
phase follow the provider's reports about the work, and the
|
|
4391
|
+
phase follow the provider's reports about the work, and the audio — attaching, moving through a
|
|
4435
4392
|
hold, a consult, a conference or a transfer, and ending — is transient beside it. See **A task is
|
|
4436
4393
|
never its audio** under **Task assignment lifecycle**.
|
|
4437
4394
|
|
|
@@ -4468,7 +4425,7 @@ refuses a false one, as it refuses a name this contract does not list.
|
|
|
4468
4425
|
| --- | --- |
|
|
4469
4426
|
| `online` | Whether the agent application has a network interface up. Not a claim that anything is reachable — the adapter knows whether it can reach its own platform far better than the agent application does — so `false` is a reason not to go ready and `true` is not a reason to. |
|
|
4470
4427
|
| `audio` | Present on a voice connection, absent where there is no audio. |
|
|
4471
|
-
| `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `
|
|
4428
|
+
| `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openAudio` receives — and `flowing`, false while no audio moves through it, when `mutedBy` says who stopped it: `host`, the agent application's own Mute on an agent application that mutes the station, or `station`, a slider on the headset or the operating system, which the agent application observed and did not do. An adapter treats `host` as the agent's act on a call and `station` as a condition of the station. `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which an agent application that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. An agent application decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
|
|
4472
4429
|
| `audio.output` | `available`, with `flowing` where the agent application can know whether audio reaches the speaker — a browser mostly cannot, and omits it; a native agent application reads the endpoint — false while it is silenced, when `mutedBy` says who did it, as on input. Or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
|
|
4473
4430
|
|
|
4474
4431
|
Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
|
|
@@ -4537,10 +4494,10 @@ A voice manifest lists its `phones` and any other channel lists none (`manifest.
|
|
|
4537
4494
|
`manifest.phones.channel`). The agent application's `phone` is required on a voice login, absent on any other,
|
|
4538
4495
|
and one the manifest listed (`context.phone.required`, `.unexpected`, `.unsupported`). Everything
|
|
4539
4496
|
about audio then follows the phone rather than the channel: on a softphone the agent application reports its
|
|
4540
|
-
audio, the adapter implements `
|
|
4541
|
-
desk phone the agent application reports no audio, opens nothing, and `task-
|
|
4497
|
+
audio, the adapter implements `openAudio`, and `task-audio-started` is the word to open it; on a
|
|
4498
|
+
desk phone the agent application reports no audio, opens nothing, and `task-audio-started` still marks the
|
|
4542
4499
|
moment the call is live so the desk shows it, with the sound on the handset. A platform that lists
|
|
4543
|
-
`deskPhone` alone never implements `
|
|
4500
|
+
`deskPhone` alone never implements `openAudio`; one that lists `softphone` always does.
|
|
4544
4501
|
|
|
4545
4502
|
**The mode comes from the agent application; the status comes from the provider.** The agent application knows which kind
|
|
4546
4503
|
of station the agent signed in at, because the person chose it, and nothing else can know that.
|
|
@@ -4552,10 +4509,10 @@ the handset is momentarily unregistered, and demanding a microphone of it, is th
|
|
|
4552
4509
|
sentence exists to refuse. What the provider does when the handset is not registered is what it
|
|
4553
4510
|
does for any station that cannot take a call -- hold the agent not-ready and say why.
|
|
4554
4511
|
|
|
4555
|
-
**On a desk-phone login the agent application never calls `
|
|
4512
|
+
**On a desk-phone login the agent application never calls `openAudio`.** There is no stream to hand over and
|
|
4556
4513
|
no audio to attach; an agent application that calls it anyway is in error, and an adapter that receives the call
|
|
4557
4514
|
answers `unavailable` with a non-retryable failure, since waiting changes nothing about a station
|
|
4558
|
-
that is a telephone. The harness requires no `
|
|
4515
|
+
that is a telephone. The harness requires no `openAudio` of such an adapter and never calls it.
|
|
4559
4516
|
|
|
4560
4517
|
**Which handset a desk-phone login rings is the platform's configuration for that agent**, and
|
|
4561
4518
|
this wire never asks the agent for it: an agent application declares `phone` and nothing more. What a platform
|
|
@@ -4578,11 +4535,11 @@ is the administrator's, and a login is not a request to reconfigure an agent.
|
|
|
4578
4535
|
|
|
4579
4536
|
### Opening the audio
|
|
4580
4537
|
|
|
4581
|
-
`
|
|
4538
|
+
`openAudio` hands Omni the remote audio for one task. Every adapter whose manifest lists
|
|
4582
4539
|
`softphone` implements it, because on a softphone the call's audio lands in Omni:
|
|
4583
4540
|
|
|
4584
4541
|
```ts
|
|
4585
|
-
|
|
4542
|
+
openAudio({ assignmentId, localAudio }): Promise<OpenAudioResult>
|
|
4586
4543
|
// { status: "opened", session } | { status: "unavailable", failure }
|
|
4587
4544
|
```
|
|
4588
4545
|
|
|
@@ -4605,8 +4562,8 @@ carries as `audio.input.localAudio`, and absent while that input is `unavailable
|
|
|
4605
4562
|
bridges audio without an application-side input may ignore it; one that needs it and finds it absent
|
|
4606
4563
|
answers `unavailable` with a failure Omni shows the agent.
|
|
4607
4564
|
|
|
4608
|
-
**When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens
|
|
4609
|
-
and on a task arriving with `
|
|
4565
|
+
**When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens audio on `task-audio-started`,
|
|
4566
|
+
and on a task arriving with `audio: "started"` on a snapshot; it closes on `task-audio-ended` and
|
|
4610
4567
|
when the task ends. Between those words, nothing Omni's own senses report — a stream that drops, a
|
|
4611
4568
|
track that ends — moves the task or its audio.
|
|
4612
4569
|
|
|
@@ -4619,7 +4576,7 @@ platform is the adapter's business.
|
|
|
4619
4576
|
## Task commands
|
|
4620
4577
|
|
|
4621
4578
|
Command names follow the channel's operational vocabulary, and each channel's command is a union
|
|
4622
|
-
discriminated by `type` — the same discriminant `
|
|
4579
|
+
discriminated by `type` — the same discriminant `executeTeam` and `custom` already use. The
|
|
4623
4580
|
unions are declared under **Shapes**.
|
|
4624
4581
|
|
|
4625
4582
|
`assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
|
|
@@ -4656,8 +4613,8 @@ agent application's. So mute is not a capability a provider declares, not a cont
|
|
|
4656
4613
|
locks, not a preference the person keeps with the provider, and not a command: nothing about the
|
|
4657
4614
|
station crosses to the provider except two things -- the station's condition, in the agent application report,
|
|
4658
4615
|
and the record of when the agent could not be heard, through `recordStep`. An agent application offers Mute on
|
|
4659
|
-
every voice task with
|
|
4660
|
-
alone, and performs it through `
|
|
4616
|
+
every voice task with audio open, under its own local policy, in `in-progress` and `paused`
|
|
4617
|
+
alone, and performs it through `CallAudio.setMuted()`. A task carrying
|
|
4661
4618
|
`capabilities.mute` is refused (`task.capability.unknown`), and so is a `mute` command
|
|
4662
4619
|
(`command.type`), a `mute` preference (`preference.id`) and a `mute` policy (`team.policy.key`).
|
|
4663
4620
|
|
|
@@ -4693,9 +4650,9 @@ forty seconds" and "the agent's headset was muted for forty seconds" as the diff
|
|
|
4693
4650
|
are. See **The agent application records what it performs**.
|
|
4694
4651
|
|
|
4695
4652
|
**Mute has a lifecycle, and none of it is inferred.** A call that starts while the station is
|
|
4696
|
-
already muted begins a `muted` leg the moment its
|
|
4653
|
+
already muted begins a `muted` leg the moment its audio starts, `mutedBy: "station"`. Audio that
|
|
4697
4654
|
ends while any leg is open ends the leg at that instant, as every agent application-performed leg ends. And the
|
|
4698
|
-
agent application's own Mute starts off on every call, which is every `task-
|
|
4655
|
+
agent application's own Mute starts off on every call, which is every `task-audio-started` -- a connect-back
|
|
4699
4656
|
opens a second call on the same task with no offer -- so an agent is never muted by the call before.
|
|
4700
4657
|
|
|
4701
4658
|
**Every press on a headset is the agent's own press**, performed the agent application's way, and the lights
|
|
@@ -4731,22 +4688,22 @@ declared:
|
|
|
4731
4688
|
| Command | What makes it available |
|
|
4732
4689
|
| --- | --- |
|
|
4733
4690
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
4734
|
-
| `end-call` | The `endCall` capability. |
|
|
4691
|
+
| `end-call` | The `endCall` capability: ends my part, the caller continues. |
|
|
4692
|
+
| `terminate-call` | The `terminateCall` capability: ends the caller channel. |
|
|
4735
4693
|
| `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`. |
|
|
4736
4694
|
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
4737
|
-
| `
|
|
4695
|
+
| `dial` | 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`. |
|
|
4738
4696
|
| `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`). |
|
|
4739
4697
|
| `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. |
|
|
4740
4698
|
| `transfer` with `action: "warm"` | The `warmTransfer` capability. `action: "cold"` is gated by `coldTransfer`; the two are declared and offered separately. |
|
|
4741
4699
|
| `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`. |
|
|
4742
4700
|
| `lead-assist` with `action: "request"` or `"cancel"` | The `leadAssist` capability. `cancel` needs a request standing -- `Task.leadAssist` with status `requested`. |
|
|
4743
|
-
| `lead-assist` with `action: "take-over-call"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
4744
4701
|
| `transfer` with a destination, `conference` with `action: "add"` | Its capability, and a `destinationId` the directory offered: the id Omni sends is the id the provider published (`command.destination.unknown`). |
|
|
4745
4702
|
| `custom` | A control the task published under `capabilities.custom`, by its `id` (`command.capability.custom`), carrying a non-empty string for each `required` prompt field and strings for any optional fields supplied (`command.custom.prompt`). A toggle carries its target `on` boolean (`command.custom.on`). |
|
|
4746
4703
|
| Everything else | Its own named capability. |
|
|
4747
4704
|
|
|
4748
4705
|
**A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
|
|
4749
|
-
`hold`, `resume` and `pause`, `end-call`, every `transfer` and `conference` action, and
|
|
4706
|
+
`hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `transfer` and `conference` action, and
|
|
4750
4707
|
every `lead-assist` action. These controls act within this agent's current interaction.
|
|
4751
4708
|
In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
|
|
4752
4709
|
an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
|
|
@@ -4755,7 +4712,7 @@ declared while the task remains open; the phase prevents this task from controll
|
|
|
4755
4712
|
it no longer owns. Omni shows these controls only in the two interaction phases, and
|
|
4756
4713
|
`validateTaskCommand` refuses them outside those phases (`command.phase.interaction`).
|
|
4757
4714
|
The commands with a phase of their own -- `answer`, `accept` and `decline` in `pending`,
|
|
4758
|
-
`
|
|
4715
|
+
`dial` in `preview`, `connect-back` in `completing`, `complete` in any -- are not among them.
|
|
4759
4716
|
|
|
4760
4717
|
`validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
|
|
4761
4718
|
capability it needs, the phase it belongs to, and the state that has to stand. The task it wants
|
|
@@ -5104,30 +5061,30 @@ snapshots until it ends.
|
|
|
5104
5061
|
Replaces the current representation of one provider-local task. It is a full task value, not a
|
|
5105
5062
|
partial patch.
|
|
5106
5063
|
|
|
5107
|
-
### `task-
|
|
5064
|
+
### `task-audio-started`
|
|
5108
5065
|
|
|
5109
|
-
The provider's word that the task's audio should now attach. Omni calls `
|
|
5110
|
-
a task carried with `
|
|
5066
|
+
The provider's word that the task's audio should now attach. Omni calls `openAudio` on it — and on
|
|
5067
|
+
a task carried with `audio: "started"`, which is how a reconnect snapshot reattaches audio an
|
|
5111
5068
|
earlier event brought — and renders the call as live from that word, never from its own senses. It
|
|
5112
|
-
precedes `
|
|
5069
|
+
precedes `openAudio` and is never a reply to it: a provider whose audio state comes from the
|
|
5113
5070
|
platform, a station going in use the moment a call is answered, sends it then, before any agent application has
|
|
5114
|
-
opened anything, and `
|
|
5115
|
-
names a task whose work has begun, and it alternates with `task-
|
|
5071
|
+
opened anything, and `openAudio` has its own answer for what the agent application did. It
|
|
5072
|
+
names a task whose work has begun, and it alternates with `task-audio-ended`: audio that never
|
|
5116
5073
|
started cannot end, so a live call whose provider says nothing about its audio is a provider in
|
|
5117
5074
|
breach, not a state a desk fills in from its own devices.
|
|
5118
5075
|
|
|
5119
|
-
The event is the transition and the task's `
|
|
5120
|
-
the
|
|
5076
|
+
The event is the transition and the task's `audio` field is the state. A `task-updated` re-states
|
|
5077
|
+
the audio its task already holds — republishing `started` on a hold is a statement, not a second
|
|
5121
5078
|
arrival — but it does not move it: an update that itself flips the field is refused
|
|
5122
|
-
(`stream.taskUpdated.
|
|
5079
|
+
(`stream.taskUpdated.audio`), and the pairing at the moment audio arrives is the phase change
|
|
5123
5080
|
without the field, then the event. Releasing `ended` is the one move an update may make, since
|
|
5124
5081
|
wrapped audio has nothing left to end.
|
|
5125
5082
|
|
|
5126
|
-
### `task-
|
|
5083
|
+
### `task-audio-ended`
|
|
5127
5084
|
|
|
5128
|
-
Signals that a task's real-time
|
|
5129
|
-
completion timer. It does not remove the task, and it ends only audio that `task-
|
|
5130
|
-
a task carried with `
|
|
5085
|
+
Signals that a task's real-time audio ended. For voice and similar channels, this starts the fixed
|
|
5086
|
+
completion timer. It does not remove the task, and it ends only audio that `task-audio-started` — or
|
|
5087
|
+
a task carried with `audio: "started"` — attached.
|
|
5131
5088
|
|
|
5132
5089
|
### `task-ended`
|
|
5133
5090
|
|
|
@@ -5135,22 +5092,22 @@ Every outcome ends the task for this agent. On `task-ended`, Omni:
|
|
|
5135
5092
|
|
|
5136
5093
|
- removes the task from its current provider view;
|
|
5137
5094
|
- clears the task workspace when it is selected;
|
|
5138
|
-
- stops task timers and
|
|
5095
|
+
- stops task timers and audio;
|
|
5139
5096
|
- releases task-scoped resources; and
|
|
5140
5097
|
- selects another task or returns to the idle workspace.
|
|
5141
5098
|
|
|
5142
|
-
A `
|
|
5143
|
-
|
|
5099
|
+
A `taken-over` outcome arrives at the member's own completion, after the wrap that follows a
|
|
5100
|
+
take-over as it follows an `end-call` -- see **Lead assist**.
|
|
5144
5101
|
|
|
5145
5102
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
|
|
5146
|
-
and not for ever: `applied` to a
|
|
5103
|
+
and not for ever: `applied` to a `complete` says the
|
|
5147
5104
|
provider has completed the task, and its `task-ended` follows within the
|
|
5148
5105
|
manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
|
|
5149
5106
|
performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
|
|
5150
5107
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
5151
5108
|
the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
|
|
5152
5109
|
task, since the ending was owed and lost. The drive holds a provider to the same bound
|
|
5153
|
-
(`drive.completion.unsettled`). The `task-
|
|
5110
|
+
(`drive.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
|
|
5154
5111
|
non-terminal. A replacement
|
|
5155
5112
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
5156
5113
|
same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
|
|
@@ -5184,8 +5141,8 @@ console is the silence problem one layer up. One event per occurrence; the agent
|
|
|
5184
5141
|
provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
|
|
5185
5142
|
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
5186
5143
|
fails loudly rather than passing with a note. A task published under `capabilitySource:
|
|
5187
|
-
"
|
|
5188
|
-
(`capabilitySource.
|
|
5144
|
+
"not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
|
|
5145
|
+
(`capabilitySource.notYetRead`); see **Task capabilities**.
|
|
5189
5146
|
|
|
5190
5147
|
| Field | Contract |
|
|
5191
5148
|
| --- | --- |
|
|
@@ -5211,7 +5168,7 @@ each connected provider.
|
|
|
5211
5168
|
Replaces this provider's complete `TeamMembers`. It is emitted only for an agent the provider
|
|
5212
5169
|
publishes a team member list to, and it carries the whole team every time — never a change to it, for the
|
|
5213
5170
|
reason set out under **Team leads**. A lead's snapshot always carries the team member list; it goes only when
|
|
5214
|
-
a republished `authenticated` no longer declares `capabilities.team
|
|
5171
|
+
a republished `authenticated` no longer declares `capabilities.lead`, or the lead turns the team feature off (`event.team.features`).
|
|
5215
5172
|
|
|
5216
5173
|
### `contacts-updated`
|
|
5217
5174
|
|
|
@@ -5285,9 +5242,9 @@ Some rules need to know who is reading. `validateTeamMembers`, `validateSnapshot
|
|
|
5285
5242
|
`AuthenticationState.identity.id` and their login's `capabilities`. Given `self`, a team member list that
|
|
5286
5243
|
carries that agent reports `team.member.self` or `team.request.self`. Given `capabilities`, a lead's
|
|
5287
5244
|
snapshot without a team member list reports `team.required`, a team member list published to a login that does not lead
|
|
5288
|
-
reports `team.unentitled`,
|
|
5289
|
-
`
|
|
5290
|
-
`team.
|
|
5245
|
+
reports `team.unentitled`, and a `team-updated` to such a login reports `event.team.capability`; given
|
|
5246
|
+
`leadFeatures: false`, a team member list reaching a lead who turned the feature off reports
|
|
5247
|
+
`team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
|
|
5291
5248
|
`exerciseAdapter` always passes both.
|
|
5292
5249
|
|
|
5293
5250
|
`assertNoViolations(violations)` throws `ProtocolConformanceError` — which carries the full
|
|
@@ -5355,10 +5312,10 @@ Nothing there is a violation: an adapter with no team has nothing to exercise. B
|
|
|
5355
5312
|
no tasks exercises no task rule, and a pass over it reads as coverage it is not.
|
|
5356
5313
|
|
|
5357
5314
|
**A static run never answers a call.** It states a capacity and disconnects, so everything
|
|
5358
|
-
downstream of `answer` -- the room, the stage,
|
|
5315
|
+
downstream of `answer` -- the room, the stage, audio, every phase past `pending`, every dial
|
|
5359
5316
|
outcome -- stays in `notExercised` for every adapter, and a rule about a live call is enforced only
|
|
5360
5317
|
in each adopter's own tests. `{ drive: true }` closes that: the exercise takes the first task the
|
|
5361
|
-
provider offers through one ordinary lifecycle -- accept it, wait for its
|
|
5318
|
+
provider offers through one ordinary lifecycle -- accept it, wait for its audio and open it on a
|
|
5362
5319
|
softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
|
|
5363
5320
|
complete it with an outcome where the agent completes -- and holds every step to the rules a
|
|
5364
5321
|
agent application holds a provider to. Each command is validated against the task as published
|
|
@@ -5373,17 +5330,17 @@ nothing (`getUserDetails.user.*`, `getUserDetails.unasked`, `connection.getUserD
|
|
|
5373
5330
|
the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
|
|
5374
5331
|
before it reads anything (`drive.reload.login`), and is then held to everything the first was on
|
|
5375
5332
|
connect: the methods its declarations call for, the agent application's report, a capacity stated to it, its
|
|
5376
|
-
snapshot read as the connect snapshot was, and its
|
|
5377
|
-
`
|
|
5333
|
+
snapshot read as the connect snapshot was, and its audio opened afresh on a task carried with
|
|
5334
|
+
`audio: "started"`, since the first client's session died with it. The result also says which rules the
|
|
5378
5335
|
run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
|
|
5379
5336
|
as each case was considered, so a test that needs a rule to have run asserts it there rather than
|
|
5380
5337
|
inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
|
|
5381
5338
|
gap and not a pass. With the audio open on a softphone,
|
|
5382
5339
|
the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
5383
5340
|
expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
|
|
5384
|
-
ends the call muted, as agents do, so the leg is open when the
|
|
5341
|
+
ends the call muted, as agents do, so the leg is open when the audio ends, the provider closes it
|
|
5385
5342
|
in the completing publication or the open entry is refused (`task.history.muted.open`),
|
|
5386
|
-
and the drive's closing report after the
|
|
5343
|
+
and the drive's closing report after the audio ended is expected `recorded` and to change nothing:
|
|
5387
5344
|
a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
|
|
5388
5345
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
5389
5346
|
(`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
|
|
@@ -5396,7 +5353,7 @@ the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.r
|
|
|
5396
5353
|
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
5397
5354
|
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
5398
5355
|
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
|
|
5399
|
-
`.
|
|
5356
|
+
`.audio`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
5400
5357
|
adapter is held to what the first was: the same provider
|
|
5401
5358
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
5402
5359
|
of the entries the first had published -- a record once read is not unread across a reload either.
|
|
@@ -5423,15 +5380,15 @@ completed from wherever it stands once the drive has nothing left to do on it --
|
|
|
5423
5380
|
after an `end-call`, or from `in-progress` where there is no call to end, which is every chat and
|
|
5424
5381
|
email and a voice task offering no `endCall` -- so a conversation reaches its end as a call does.
|
|
5425
5382
|
The drive stops where the task offers no way on and says nothing about what it could not reach. A
|
|
5426
|
-
softphone adapter written for a browser needs its
|
|
5427
|
-
harness: the drive opens
|
|
5383
|
+
softphone adapter written for a browser needs its audio APIs supplied by whatever runs the
|
|
5384
|
+
harness: the drive opens audio outside a browser, and a provider that quietly stopped carrying audio
|
|
5428
5385
|
where `AudioContext` was missing would be lying about the one thing the channel is for. It is off by default because it issues
|
|
5429
5386
|
commands against whatever platform the adapter is connected to: turn it on against a test backend.
|
|
5430
5387
|
|
|
5431
5388
|
```ts
|
|
5432
5389
|
const driven = await exerciseAdapter(adapter, context, { collectOnly: true, drive: true });
|
|
5433
5390
|
expect(driven.violations).toEqual([]);
|
|
5434
|
-
assertReached(driven, ["task.onCall", "task.
|
|
5391
|
+
assertReached(driven, ["task.onCall", "task.audio", "event.task-ended"]);
|
|
5435
5392
|
```
|
|
5436
5393
|
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
5437
5394
|
never met, so a test that meant to check a team member list cannot pass on a fixture that never produced one.
|
|
@@ -5459,7 +5416,7 @@ cannot be established from TypeScript structure alone.
|
|
|
5459
5416
|
|
|
5460
5417
|
| Helper | Contract checked |
|
|
5461
5418
|
| --- | --- |
|
|
5462
|
-
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads
|
|
5419
|
+
| `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
|
|
5463
5420
|
| `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
|
|
5464
5421
|
| `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
|
|
5465
5422
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
@@ -5468,11 +5425,11 @@ cannot be established from TypeScript structure alone.
|
|
|
5468
5425
|
| `stillHost(report?, guarantees?, mute?)` | An agent application that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
|
|
5469
5426
|
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for an agent application that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
|
|
5470
5427
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a forced break arriving in effect with `forced`. The harness applies the same rules after the connect snapshot. |
|
|
5471
|
-
| `
|
|
5428
|
+
| `assertAudioFollowsTheTask(envelopes, snapshot?)` | The audio follows the task and never decides it: every task is introduced once, `task-audio-started` and `task-audio-ended` alternate on work that has begun, audio ends only where it arrived, and what follows the audio ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no audio satisfies it by never testing it — pair it with the assertion that the audio end is present. |
|
|
5472
5429
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
5473
5430
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `on-break` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
5474
5431
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
5475
|
-
| `assertWrapTimeout(task,
|
|
5432
|
+
| `assertWrapTimeout(task, audioEndedAt, deadline, toleranceMs?)` | The wrap deadline equals audio end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
|
|
5476
5433
|
| `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|
|
5477
5434
|
| `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
|
|
5478
5435
|
|
|
@@ -5505,11 +5462,11 @@ action sets. The outer capability may be locked. Absence grants no permission, a
|
|
|
5505
5462
|
exist without any permission. A provider may offer only stop for a recording started automatically,
|
|
5506
5463
|
or withdraw a control on a later task update. There is no global recording mode and no automatic
|
|
5507
5464
|
start from a capability declaration. Agent application support is declared on `ConnectContext.host.recording`,
|
|
5508
|
-
not the provider-owned manifest. The task
|
|
5509
|
-
unknown
|
|
5510
|
-
recording or a default upload location. Initial agent application support requires voice softphone
|
|
5465
|
+
not the provider-owned manifest. The task names one of the storage places the agent application provisioned, by `storageId`;
|
|
5466
|
+
an unknown storage place or an unsupported action is refused visibly, never redirected to provider
|
|
5467
|
+
recording or a default upload location. Initial agent application support requires voice softphone audio and
|
|
5511
5468
|
capture of both local and remote audio; an agent application unable to capture either must refuse start/resume.
|
|
5512
|
-
This package declares that contract; it supplies no recorder,
|
|
5469
|
+
This package declares that contract; it supplies no recorder, audio mixing, storage or upload.
|
|
5513
5470
|
|
|
5514
5471
|
| Action | Required current recording state | Confirmed outcome |
|
|
5515
5472
|
| --- | --- | --- |
|
|
@@ -5540,7 +5497,7 @@ There is no automatic restart, transfer to another recorder, or stop on task hol
|
|
|
5540
5497
|
Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
|
|
5541
5498
|
agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
|
|
5542
5499
|
|
|
5543
|
-
Only start/resume require an in-progress or paused task with started
|
|
5500
|
+
Only start/resume require an in-progress or paused task with started audio. Pause/stop/cancel may
|
|
5544
5501
|
also finish an independently observed recorder while the task is completing. A pending task may
|
|
5545
5502
|
show active recording, but agent controls wait until interaction begins. The two recording paths may
|
|
5546
5503
|
both be active. A command to either path has no implied effect on the other.
|
|
@@ -5583,9 +5540,9 @@ This change does not publish a package or enable recording in any existing agent
|
|
|
5583
5540
|
semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
|
|
5584
5541
|
retention/completion or source truth from a status flag; those remain executor obligations.
|
|
5585
5542
|
|
|
5586
|
-
|
|
5543
|
+
The storage place named on the task is bound when start actually creates a recording. Existing recordings
|
|
5587
5544
|
keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
|
|
5588
|
-
The executor rejects a mismatched
|
|
5545
|
+
The executor rejects a mismatched storage place rather than moving or discarding another binding.
|
|
5589
5546
|
Action permission does not authorize unattended invocation: agent application controls require the agent's explicit
|
|
5590
5547
|
act, and provider authorization remains enforced at its authenticated command boundary.
|
|
5591
5548
|
|