@xema/omni-protocol 0.1.95 → 0.1.97
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 +50 -63
- package/dist/index.js +10 -10
- package/dist/testing.d.ts +6 -7
- package/dist/testing.js +105 -130
- package/dist/validation.d.ts +4 -4
- package/dist/validation.js +81 -93
- package/guide.md +552 -544
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -19,27 +19,28 @@ are used precisely throughout and mean nothing looser here.
|
|
|
19
19
|
| Word | What it means |
|
|
20
20
|
| --- | --- |
|
|
21
21
|
| **Omni** | The desktop application an agent works in. It composes several providers into one agent-facing experience and owns everything outside a provider's own system. |
|
|
22
|
-
| **
|
|
22
|
+
| **Agent application** | The software the agent uses, such as Omni, whether web or desktop. It presents provider tasks and controls and manages local audio where supported. Existing API names use `Host` and `host` for this role. |
|
|
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
26
|
| **Lead** | An agent whose login declares `capabilities.team`. The provider publishes a `TeamMembers` object to them and to nobody else: **the login is the permission**. |
|
|
27
|
-
| **
|
|
28
|
-
| **Call** | The caller’s complete phone call, which may continue through IVRs, queues and several agents. |
|
|
29
|
-
| **
|
|
30
|
-
| **
|
|
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
|
+
| **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
|
+
| **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. |
|
|
30
|
+
| **Interaction** | The agent's time on the call, from answer until their audio ends: the `in-progress` and `paused` phases. An assignment produces at most one interaction, and a call that returns produces a new assignment and so a new interaction. |
|
|
31
|
+
| **Task** | The desk's record of one assignment, from offer through wrap: the workspace, controls and completion work the agent has for it, described by the provider and held by the desk. A task is identified by the assignment it is the record of. `pending`, `confirmed` and `preview` are assigned and not yet interacting; `in-progress` and `paused` are the interaction; `completing` is the assignment outliving its interaction. A voice task is one assignment, never the caller's whole call. |
|
|
31
32
|
| **Channel** | The kind of work a provider carries: `voice`, `chat`, or `email`. Fixed per provider by its manifest. |
|
|
32
33
|
| **Task type** | The provider's own name for a category of work — a queue, a mailbox folder, a chat source. Free-form, and finer-grained than a channel. |
|
|
33
|
-
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the
|
|
34
|
+
| **Capability** | A provider's declaration that a control exists for a task or a session. It says *offer this*; the provider performs it, and the agent application only offers it — see **Where a command executes**. |
|
|
34
35
|
| **Login** | One authenticated sign-in to one provider, identified by `loginId`. A transport reconnect keeps it; signing in again replaces it, and nothing tied to the old `loginId` survives. |
|
|
35
36
|
| **Transport** | The adapter's connection to its platform: a WebSocket or SignalR connection, required to be persistent and ordered. Which one and how it reconnects are the adapter's business; losing it does not end a login. |
|
|
36
37
|
| **Connection** | The `Connection` object Omni holds for one login: the methods it can call and the events it receives. |
|
|
37
|
-
| **Concurrent capacity** | How many tasks this provider may have
|
|
38
|
+
| **Concurrent capacity** | How many tasks this provider may have assigned to the agent at once — an absolute ceiling, stated as `AgentCapacity.count` and standing until Omni restates it. The provider counts its own outstanding tasks against it. |
|
|
38
39
|
| **Snapshot** | The provider's complete state at one moment. It replaces what Omni holds; it is never a patch. |
|
|
39
40
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
40
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. |
|
|
41
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. |
|
|
42
|
-
| **Dial** | One outbound call the
|
|
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**. |
|
|
43
44
|
| **Listen** | A lead listening to a member's call unasked, from the team member list: `listen` in silence, `coach` heard by the agent alone, `join-call` heard by everyone. Nothing of it reaches the member's task, and there is no take-over in it. See **Listening to a call**. |
|
|
44
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. |
|
|
45
46
|
|
|
@@ -55,6 +56,11 @@ state use `status`; name the object when discussing them to avoid ambiguity.
|
|
|
55
56
|
| `status` | The agent’s break lifecycle | `not-requested`, `awaiting-approval`, `granted`, `starting-after-task`, `on-break` |
|
|
56
57
|
| `availability` | A team member | `ready`, `on-task`, `on-break`, `signed-out` |
|
|
57
58
|
|
|
59
|
+
Migration: describeUsers is now `getUserDetails`, and validateDescribedUsers is now
|
|
60
|
+
`validateUserDetails`. Related diagnostics use getUserDetails. Hosts and providers must
|
|
61
|
+
update together; the old method and validator have no compatibility aliases. The requested
|
|
62
|
+
user IDs, returned user details and directory requirements are unchanged.
|
|
63
|
+
|
|
58
64
|
## Versioning
|
|
59
65
|
|
|
60
66
|
### `OMNI_PROTOCOL_VERSION`
|
|
@@ -67,12 +73,12 @@ An adapter declares **every** version it can speak, not just the one it was comp
|
|
|
67
73
|
|
|
68
74
|
```ts
|
|
69
75
|
supportedProtocolVersions: [1] // v1 only
|
|
70
|
-
supportedProtocolVersions: [1, 2] // can serve either
|
|
76
|
+
supportedProtocolVersions: [1, 2] // can serve either agent application
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
A single pinned version would make migration impossible: recompiling against a newer package
|
|
74
80
|
would silently move an adapter to the new version with no window in which both sides
|
|
75
|
-
interoperate. Declaring a set lets an adapter support the old and new
|
|
81
|
+
interoperate. Declaring a set lets an adapter support the old and new agent application at once, so the two
|
|
76
82
|
can be deployed independently.
|
|
77
83
|
|
|
78
84
|
### `negotiateProtocolVersion(adapterVersions, hostVersions?)`
|
|
@@ -99,8 +105,7 @@ the semantic name in every contract field rather than repeating the primitive ty
|
|
|
99
105
|
```ts
|
|
100
106
|
type IsoTimestamp = string;
|
|
101
107
|
type UserId = string;
|
|
102
|
-
type
|
|
103
|
-
type AllocationId = string;
|
|
108
|
+
type AssignmentId = string;
|
|
104
109
|
type DialId = string;
|
|
105
110
|
type DurationSeconds = number;
|
|
106
111
|
```
|
|
@@ -109,7 +114,7 @@ type DurationSeconds = number;
|
|
|
109
114
|
| --- | --- | --- |
|
|
110
115
|
| `IsoTimestamp` | `string` | An RFC-3339 timestamp with `Z` or an explicit numeric offset. Timezone-less values are invalid. It must pass the shared runtime validator. A JavaScript `Date` never crosses the protocol boundary. |
|
|
111
116
|
| `UserId` | `string` | A non-empty, opaque, stable identifier for a person, **issued by the provider** and drawn from the same directory as `AuthenticationState.identity.id`. It names agents and managers alike; the role is established by where the value appears, not by its type. Compare it exactly and only within one provider; do not parse it or infer meaning from its format. |
|
|
112
|
-
| `
|
|
117
|
+
| `AssignmentId` | `string` | A non-empty, opaque identifier for one assignment, **issued by the provider**: unique within the provider and never reused while the provider is still speaking about it. Whether it is the platform's own handle passed through or one the adapter minted is the adapter's business. Omni scopes it with the provider ID -- see `taskKey()`. |
|
|
113
118
|
| `DurationSeconds` | `number` | A non-negative integer duration measured in seconds. |
|
|
114
119
|
|
|
115
120
|
### There is no Omni-wide user identity
|
|
@@ -119,7 +124,7 @@ identifier of its own for an agent or a manager, and none crosses this boundary
|
|
|
119
124
|
operating-system account, not a directory identity, not a licence.
|
|
120
125
|
|
|
121
126
|
So a `UserId` means nothing outside the provider that issued it. Provider A's
|
|
122
|
-
`
|
|
127
|
+
`history[].by` and provider B's team member list `memberId` are unrelated strings that will
|
|
123
128
|
eventually collide, and one person on several providers has several identities that nothing here
|
|
124
129
|
pairs. Scope every user identifier with its provider ID before storing or comparing it, exactly as
|
|
125
130
|
`taskKey()` already does for tasks — see `userKey()` under **Utilities**.
|
|
@@ -322,8 +327,7 @@ type RecordingCommand = {
|
|
|
322
327
|
| { action: "pause" | "resume" | "stop" | "cancel"; recordingId: string }
|
|
323
328
|
);
|
|
324
329
|
interface HostRecordingReport {
|
|
325
|
-
|
|
326
|
-
allocationId: AllocationId;
|
|
330
|
+
assignmentId: AssignmentId;
|
|
327
331
|
state: RecordingState;
|
|
328
332
|
}
|
|
329
333
|
interface HostRecording {
|
|
@@ -334,8 +338,7 @@ interface HostRecording {
|
|
|
334
338
|
}
|
|
335
339
|
type RecordingCommandResult = Exclude<TaskCommandResult, { status: "dialling" }>;
|
|
336
340
|
interface HostRecordingRequest {
|
|
337
|
-
|
|
338
|
-
allocationId: AllocationId;
|
|
341
|
+
assignmentId: AssignmentId;
|
|
339
342
|
command: RecordingCommand & { source: "host" };
|
|
340
343
|
}
|
|
341
344
|
|
|
@@ -460,15 +463,15 @@ type SetPreferenceRequest =
|
|
|
460
463
|
| { id: PreferenceId; enabled: boolean }
|
|
461
464
|
| { id: PreferenceId; inherit: true };
|
|
462
465
|
|
|
463
|
-
type
|
|
466
|
+
type HistoryReport = { assignmentId: AssignmentId; at: IsoTimestamp } & (
|
|
464
467
|
| { step: "muted"; mutedBy: MutedBy }
|
|
465
|
-
| { step: Exclude<
|
|
468
|
+
| { step: Exclude<HistoryStep, "muted">; mutedBy?: never }
|
|
466
469
|
) & (
|
|
467
470
|
| { ended: true; seconds: DurationSeconds }
|
|
468
471
|
| { ended?: never; seconds?: DurationSeconds }
|
|
469
472
|
);
|
|
470
473
|
|
|
471
|
-
type
|
|
474
|
+
type HistoryReportResult =
|
|
472
475
|
| { status: "recorded"; at: IsoTimestamp }
|
|
473
476
|
| { status: "failed"; failure: ProtocolFailure };
|
|
474
477
|
|
|
@@ -488,7 +491,7 @@ type Snapshot = {
|
|
|
488
491
|
};
|
|
489
492
|
|
|
490
493
|
type AgentCapacity = {
|
|
491
|
-
count: number; // absolute ceiling, zero or more; zero is
|
|
494
|
+
count: number; // absolute ceiling, zero or more; zero is agent application-stopped
|
|
492
495
|
};
|
|
493
496
|
|
|
494
497
|
type CapacityResult = { status: "applied" };
|
|
@@ -613,8 +616,7 @@ type PersonalBrowser = Browser;
|
|
|
613
616
|
|
|
614
617
|
type BrowserSessionKeyInput = {
|
|
615
618
|
providerId: string;
|
|
616
|
-
|
|
617
|
-
allocationId: AllocationId;
|
|
619
|
+
assignmentId: AssignmentId;
|
|
618
620
|
taskType: string;
|
|
619
621
|
browser: TaskBrowser;
|
|
620
622
|
};
|
|
@@ -647,7 +649,7 @@ type TaskAttribute = TaskAttributeBase & (
|
|
|
647
649
|
| { type: "timestamp"; at: IsoTimestamp }
|
|
648
650
|
);
|
|
649
651
|
|
|
650
|
-
type
|
|
652
|
+
type HistoryStep =
|
|
651
653
|
| "queued"
|
|
652
654
|
| "offered"
|
|
653
655
|
| "answered"
|
|
@@ -657,16 +659,16 @@ type InteractionStep =
|
|
|
657
659
|
| "conferenced"
|
|
658
660
|
| "unanswered";
|
|
659
661
|
|
|
660
|
-
type
|
|
661
|
-
steps:
|
|
662
|
+
type TaskHistory = {
|
|
663
|
+
steps: TaskHistoryStep[];
|
|
662
664
|
interactionSeconds?: DurationSeconds;
|
|
663
665
|
holdSeconds?: DurationSeconds;
|
|
664
666
|
queueSeconds?: DurationSeconds;
|
|
665
667
|
transfers?: number;
|
|
666
668
|
};
|
|
667
669
|
|
|
668
|
-
type
|
|
669
|
-
step:
|
|
670
|
+
type TaskHistoryStep = {
|
|
671
|
+
step: HistoryStep;
|
|
670
672
|
at: IsoTimestamp;
|
|
671
673
|
dialId?: DialId;
|
|
672
674
|
destinationId?: string;
|
|
@@ -697,14 +699,14 @@ type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
|
|
|
697
699
|
|
|
698
700
|
type TaskAssisting = {
|
|
699
701
|
memberId: UserId;
|
|
702
|
+
assignmentId: AssignmentId;
|
|
700
703
|
note?: string;
|
|
701
704
|
since: IsoTimestamp;
|
|
702
705
|
};
|
|
703
706
|
|
|
704
707
|
type TaskListening = {
|
|
705
708
|
memberId: UserId;
|
|
706
|
-
|
|
707
|
-
allocationId: AllocationId;
|
|
709
|
+
assignmentId: AssignmentId;
|
|
708
710
|
mode: ListeningMode;
|
|
709
711
|
since: IsoTimestamp;
|
|
710
712
|
};
|
|
@@ -728,8 +730,7 @@ type TaskMediaState = "started" | "ended";
|
|
|
728
730
|
type CapabilitySource = "queue" | "ungoverned" | "undetermined";
|
|
729
731
|
|
|
730
732
|
type Task<C extends Channel = Channel> = {
|
|
731
|
-
|
|
732
|
-
allocationId: AllocationId;
|
|
733
|
+
assignmentId: AssignmentId;
|
|
733
734
|
title: string;
|
|
734
735
|
channel: C;
|
|
735
736
|
taskType: string;
|
|
@@ -743,7 +744,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
743
744
|
atDeadline?: PreviewDeadline;
|
|
744
745
|
reference?: string;
|
|
745
746
|
attributes?: TaskAttribute[];
|
|
746
|
-
|
|
747
|
+
history?: TaskHistory;
|
|
747
748
|
} & TaskCompletion & (
|
|
748
749
|
C extends "voice"
|
|
749
750
|
? { recording?: { provider?: RecordingState }; onCall?: OnCall[]; leadAssist?: TaskLeadAssist; assisting?: TaskAssisting; listening?: TaskListening; media?: TaskMediaState }
|
|
@@ -835,8 +836,7 @@ type TaskCommand<C extends Channel = Channel> =
|
|
|
835
836
|
| CustomTaskCommand;
|
|
836
837
|
|
|
837
838
|
type TaskCommandRequest<C extends Channel = Channel> = {
|
|
838
|
-
|
|
839
|
-
allocationId: AllocationId;
|
|
839
|
+
assignmentId: AssignmentId;
|
|
840
840
|
command: TaskCommand<C>;
|
|
841
841
|
};
|
|
842
842
|
|
|
@@ -870,7 +870,7 @@ leaves out does not exist — a structure with no site level declares `org`, `te
|
|
|
870
870
|
at: a label with no policy behind it decides nothing. A manifest that declares none has exactly
|
|
871
871
|
the four. `lockedBy` is any level in force except `person`, who never locks their own value;
|
|
872
872
|
`setBy` is any level in force, or `provider`, the protocol's word for "no level has said
|
|
873
|
-
anything and the provider's own configuration supplied the value".
|
|
873
|
+
anything and the provider's own configuration supplied the value". An agent application renders "who decided" from the
|
|
874
874
|
declared labels and needs no others, and validates every republished `authenticated` state
|
|
875
875
|
against them, not only the sign-in. What the wire carries is the resolution:
|
|
876
876
|
|
|
@@ -897,7 +897,7 @@ The adapter is the one that knows the value, so it is the one held to it: given
|
|
|
897
897
|
queue locked, the validator refuses any other field carrying one, digits compared as digits so no
|
|
898
898
|
formatting hides them (`task.locked.leak`), and a conformance run whose tasks lock a party's number
|
|
899
899
|
or email states those values in `lockedValues`, since a run that cannot ask the question is not a
|
|
900
|
-
pass.
|
|
900
|
+
pass. An agent application never has the value and never asks. A name is not locked. What the queue provides rather than permits — browsers, outcomes, custom
|
|
901
901
|
controls — is content, and is never locked.
|
|
902
902
|
|
|
903
903
|
**A lead sets the team's policy from their team member list.** A login that declares
|
|
@@ -921,9 +921,9 @@ person's preference from their own screen, which arrives the same way. A prefere
|
|
|
921
921
|
the capability's own name because it is the same capability at another level: effective in
|
|
922
922
|
`Task.capabilities`, set for the team in `policies`, left to the person in `preferences`. The
|
|
923
923
|
command `hold` acts on one call; the preference `hold` says whether the person wants the control
|
|
924
|
-
at all, and
|
|
925
|
-
these ladders: the microphone is the
|
|
926
|
-
keeps a choice about it. See **The station is the
|
|
924
|
+
at all, and an agent application renders it in its settings, never as the button on a call. Mute is in none of
|
|
925
|
+
these ladders: the microphone is the agent application's, and nobody on the provider's side allows, locks or
|
|
926
|
+
keeps a choice about it. See **The station is the agent application's**.
|
|
927
927
|
|
|
928
928
|
## Breaks
|
|
929
929
|
|
|
@@ -996,8 +996,7 @@ type TeamMember = {
|
|
|
996
996
|
type LeadRequest = {
|
|
997
997
|
id: string;
|
|
998
998
|
memberId: UserId;
|
|
999
|
-
|
|
1000
|
-
allocationId: AllocationId;
|
|
999
|
+
assignmentId: AssignmentId;
|
|
1001
1000
|
note?: string;
|
|
1002
1001
|
since: IsoTimestamp;
|
|
1003
1002
|
};
|
|
@@ -1076,8 +1075,7 @@ type OpenMediaResult =
|
|
|
1076
1075
|
| { status: "unavailable"; failure: ProtocolFailure };
|
|
1077
1076
|
|
|
1078
1077
|
type OpenMediaRequest = {
|
|
1079
|
-
|
|
1080
|
-
allocationId: AllocationId;
|
|
1078
|
+
assignmentId: AssignmentId;
|
|
1081
1079
|
localAudio?: MediaStream;
|
|
1082
1080
|
};
|
|
1083
1081
|
```
|
|
@@ -1105,16 +1103,16 @@ type ProviderEvent =
|
|
|
1105
1103
|
| {
|
|
1106
1104
|
type: "task-offered";
|
|
1107
1105
|
task: Task;
|
|
1108
|
-
|
|
1106
|
+
assignmentExpiresAt?: IsoTimestamp;
|
|
1109
1107
|
}
|
|
1110
1108
|
| { type: "task-updated"; task: Task }
|
|
1111
|
-
| { type: "task-media-started";
|
|
1112
|
-
| { type: "task-media-ended";
|
|
1113
|
-
| { type: "task-ended";
|
|
1114
|
-
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome;
|
|
1109
|
+
| { type: "task-media-started"; assignmentId: AssignmentId }
|
|
1110
|
+
| { type: "task-media-ended"; assignmentId: AssignmentId }
|
|
1111
|
+
| { type: "task-ended"; assignmentId: AssignmentId; outcome: TaskOutcome }
|
|
1112
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; assignmentId?: AssignmentId; destinationId?: string; reason?: string }
|
|
1115
1113
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
1116
1114
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
1117
|
-
| { type: "diagnostic"; expected: string; observed: string;
|
|
1115
|
+
| { type: "diagnostic"; expected: string; observed: string; assignmentId?: AssignmentId }
|
|
1118
1116
|
| { type: "team-updated"; team: TeamMembers }
|
|
1119
1117
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
1120
1118
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
@@ -1163,7 +1161,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1163
1161
|
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
|
1164
1162
|
disconnect(): Promise<void>;
|
|
1165
1163
|
|
|
1166
|
-
|
|
1164
|
+
getUserDetails?(ids: UserId[]): Promise<User[]>;
|
|
1167
1165
|
dial?(request: DialRequest): Promise<DialResult>;
|
|
1168
1166
|
|
|
1169
1167
|
requestBreak?(request: BreakRequest): Promise<BreakRequestResult>;
|
|
@@ -1176,7 +1174,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
1176
1174
|
executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
|
|
1177
1175
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
1178
1176
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1179
|
-
recordStep?(report:
|
|
1177
|
+
recordStep?(report: HistoryReport): Promise<HistoryReportResult>;
|
|
1180
1178
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
1181
1179
|
};
|
|
1182
1180
|
|
|
@@ -1224,7 +1222,7 @@ const BREAK_KINDS = [
|
|
|
1224
1222
|
|
|
1225
1223
|
type BreakKind = (typeof BREAK_KINDS)[number];
|
|
1226
1224
|
|
|
1227
|
-
const
|
|
1225
|
+
const HISTORY_STEPS_WITH_A_PERSON = [
|
|
1228
1226
|
"offered",
|
|
1229
1227
|
"answered",
|
|
1230
1228
|
"held",
|
|
@@ -1247,8 +1245,8 @@ const OMNI_FAILURE_CODES = [
|
|
|
1247
1245
|
type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1248
1246
|
```
|
|
1249
1247
|
|
|
1250
|
-
`
|
|
1251
|
-
takes part in. `
|
|
1248
|
+
`HISTORY_STEPS_WITH_A_PERSON` is every `HistoryStep` except `queued`, which is the one nobody
|
|
1249
|
+
takes part in. `historyStepExpectsAPerson()` tests membership.
|
|
1252
1250
|
|
|
1253
1251
|
### Failure and validation
|
|
1254
1252
|
|
|
@@ -1401,7 +1399,7 @@ fact.
|
|
|
1401
1399
|
|
|
1402
1400
|
Omitting a field says *I cannot see this*. An empty value says *I looked, and there is nothing*.
|
|
1403
1401
|
|
|
1404
|
-
Send whichever is true.
|
|
1402
|
+
Send whichever is true. An agent application renders them differently and cannot recover the distinction once
|
|
1405
1403
|
it is lost.
|
|
1406
1404
|
|
|
1407
1405
|
The same distinction applies to nested fields. Omit a nested field only when its value is unknown;
|
|
@@ -1457,8 +1455,8 @@ is never the provider's answer: an answer travels only as a resolved `{ status:
|
|
|
1457
1455
|
and every rejection is transport loss, whatever the last `transport-status` said and whatever the
|
|
1458
1456
|
rejection carries.
|
|
1459
1457
|
|
|
1460
|
-
A command therefore carries no key. The provider names
|
|
1461
|
-
member
|
|
1458
|
+
A command therefore carries no key. The provider names the assignment, the lead request and the
|
|
1459
|
+
member, and Omni refers to them by those names; **Omni never asks a provider to remember a name
|
|
1462
1460
|
Omni made up.**
|
|
1463
1461
|
|
|
1464
1462
|
A command whose second execution changes nothing needs no protection: committing a break that is
|
|
@@ -1480,11 +1478,11 @@ fired into a state that has moved on.
|
|
|
1480
1478
|
|
|
1481
1479
|
### 7. Work is pulled, never pushed
|
|
1482
1480
|
|
|
1483
|
-
|
|
1481
|
+
Assign only within the concurrent capacity Omni has stated for this agent.
|
|
1484
1482
|
|
|
1485
|
-
An
|
|
1483
|
+
An assignment beyond that capacity, or with none currently stated, is invalid and Omni rejects it
|
|
1486
1484
|
as a protocol violation. Work the agent was already working on when the connection came back is
|
|
1487
|
-
reported in the snapshot; it is not an
|
|
1485
|
+
reported in the snapshot; it is not an assignment.
|
|
1488
1486
|
|
|
1489
1487
|
### 8. State is authoritative at the provider
|
|
1490
1488
|
|
|
@@ -1494,13 +1492,13 @@ replaces its local provider view; it does not overwrite the provider.
|
|
|
1494
1492
|
|
|
1495
1493
|
### 9. Define a vocabulary; do not merely list it
|
|
1496
1494
|
|
|
1497
|
-
Every member of a closed set — break kinds,
|
|
1495
|
+
Every member of a closed set — break kinds, history steps, destination kinds — must have a
|
|
1498
1496
|
normative definition; matching names alone do not establish shared meaning.
|
|
1499
1497
|
|
|
1500
1498
|
### 10. Order on the wire is display order
|
|
1501
1499
|
|
|
1502
1500
|
A list the provider publishes — a destination directory, outcome codes, a task's custom
|
|
1503
|
-
controls, its browsers — is in the order the provider wants it shown, and
|
|
1501
|
+
controls, its browsers — is in the order the provider wants it shown, and an agent application keeps that order.
|
|
1504
1502
|
A reader will find meaning in position unless something says there is none, so the provider
|
|
1505
1503
|
decides: where it has an intention, an administrator's sequence, it lists in that order; where it
|
|
1506
1504
|
has none, it orders by the label, so the order is visibly arbitrary and does not move under the
|
|
@@ -1533,20 +1531,20 @@ Request/response polling does not have those properties and is not a transport f
|
|
|
1533
1531
|
### 3. An adapter's memory is the platform's and the login's, never the connection's
|
|
1534
1532
|
|
|
1535
1533
|
Everything an adapter publishes about a task is derived from what the platform states, or kept in
|
|
1536
|
-
the login's store where the platform cannot hold it. Per offer, the `
|
|
1534
|
+
the login's store where the platform cannot hold it. Per offer, the `assignmentId` comes from what
|
|
1537
1535
|
the offer itself states; per call, the media follows `task-media-started` and `task-media-ended`,
|
|
1538
1536
|
never a flag set when the audio was opened; per session, the phone the agent holds is what the
|
|
1539
1537
|
platform restates on activation, not a mapping read once over HTTP. What the platform cannot hold
|
|
1540
|
-
--
|
|
1538
|
+
-- an agent application's muted leg -- goes in `ConnectContext.store`, keyed by the task and gone with it.
|
|
1541
1539
|
|
|
1542
|
-
A fact read once over a connection and kept in the adapter object dies with the client.
|
|
1540
|
+
A fact read once over a connection and kept in the adapter object dies with the client. An agent application
|
|
1543
1541
|
reload is the first client dying and a second coming up for the same login, and the second
|
|
1544
1542
|
publishes the same truth about the same task as the first did, or the first was publishing
|
|
1545
|
-
something it made up: an
|
|
1543
|
+
something it made up: an assignment minted for the connection, media remembered as a boolean, a
|
|
1546
1544
|
phone the second never read. Three pieces of exactly that state were found in one adapter in an
|
|
1547
1545
|
afternoon, and none of it was visible until the adapter was built twice. The drive's `rebuild` is the test of this
|
|
1548
1546
|
principle: given a way to build the adapter again, the run hands the login over and holds the
|
|
1549
|
-
second to what the first published (`drive.reload.
|
|
1547
|
+
second to what the first published (`drive.reload.snapshot`, `.history`, `.openMedia`, and the
|
|
1550
1548
|
stream's own rules across the resync). An adapter that cannot be built twice against its platform
|
|
1551
1549
|
and publish the same task is not a conformant adapter yet, whatever a single run says.
|
|
1552
1550
|
|
|
@@ -1608,11 +1606,11 @@ compile time.
|
|
|
1608
1606
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1609
1607
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1610
1608
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1611
|
-
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the
|
|
1609
|
+
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the agent application) and/or `deskPhone` (a handset the platform rings; the agent application shows the call and opens nothing). The agent application picks one per login. See **How the agent hears the call**. |
|
|
1612
1610
|
| `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
|
|
1613
|
-
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples;
|
|
1614
|
-
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final;
|
|
1615
|
-
| `runningStepReports` | The provider takes running reports of
|
|
1611
|
+
| `timeCheck` | Optional `true`: implements `checkTime` for fresh provider-clock samples; agent application polling is independently opt-in. |
|
|
1612
|
+
| `timestampAuthority` | Optional `"provider"`: provider timestamps are final; agent application instants are advisory. Omission makes no trust promise. |
|
|
1613
|
+
| `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**. |
|
|
1616
1614
|
| `completionSettleMs` | Required. How long after an applied completion -- `complete`, or a lead's `take-over-call` -- 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`**. |
|
|
1617
1615
|
|
|
1618
1616
|
### Authentication methods
|
|
@@ -1867,8 +1865,8 @@ Creates the provider-scoped authentication session.
|
|
|
1867
1865
|
| --- | --- |
|
|
1868
1866
|
| `protocolVersion` | The negotiated version, fixed for this login. |
|
|
1869
1867
|
| `loginId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.loginId`, and how an adapter ties a connection back to the session that authenticated it. |
|
|
1870
|
-
| `timeZone` | The zone the agent's day is reckoned in, as an IANA name from the
|
|
1871
|
-
| `phone` | How this login hears its calls, chosen by the
|
|
1868
|
+
| `timeZone` | The zone the agent's day is reckoned in, as an IANA name from the agent application's clock, stated before any identity exists so the first `authenticated` state already carries it. The same value Omni later passes as `ConnectContext.timeZone`. See **The agent's day**. |
|
|
1869
|
+
| `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call**. |
|
|
1872
1870
|
| `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
|
|
1873
1871
|
| `signal` | Optional cancellation signal. |
|
|
1874
1872
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
@@ -1930,7 +1928,7 @@ them from what arrives later.
|
|
|
1930
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`. |
|
|
1931
1929
|
| `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**. |
|
|
1932
1930
|
|
|
1933
|
-
A session action is available only when both the capability and Omni
|
|
1931
|
+
A session action is available only when both the capability and Omni local policy permit it.
|
|
1934
1932
|
|
|
1935
1933
|
**Capabilities are current, not fixed.** They describe the login as of its latest `authenticated`
|
|
1936
1934
|
state. A provider that reads roles live — a lead demoted mid-shift — republishes `authenticated`
|
|
@@ -1954,7 +1952,7 @@ short-lived, opaque `flowId`; rejection returns `AuthenticationFailure`.
|
|
|
1954
1952
|
|
|
1955
1953
|
#### Browser SSO
|
|
1956
1954
|
|
|
1957
|
-
For `browser-sso`, Omni
|
|
1955
|
+
For `browser-sso`, Omni issues a one-time callback URL and passes it to `start()`:
|
|
1958
1956
|
|
|
1959
1957
|
```ts
|
|
1960
1958
|
{
|
|
@@ -2023,7 +2021,7 @@ failure:
|
|
|
2023
2021
|
```
|
|
2024
2022
|
|
|
2025
2023
|
The `User` it carries is the **root of this provider's user namespace**. Every other person this
|
|
2026
|
-
provider names — a team member, the manager on a forced break, the agent on
|
|
2024
|
+
provider names — a team member, the manager on a forced break, the agent on a history step —
|
|
2027
2025
|
is identified from the same directory and carries the same `UserId` type.
|
|
2028
2026
|
|
|
2029
2027
|
| Field | Contract |
|
|
@@ -2072,11 +2070,11 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2072
2070
|
| --- | --- |
|
|
2073
2071
|
| `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
|
|
2074
2072
|
| `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
|
|
2075
|
-
| `autoAcceptTasks` | Agent
|
|
2073
|
+
| `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2076
2074
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2077
|
-
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's
|
|
2078
|
-
| `store` | The login's operational store, kept by the
|
|
2079
|
-
| `host` | The
|
|
2075
|
+
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's**. |
|
|
2076
|
+
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.task-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2077
|
+
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides**. |
|
|
2080
2078
|
| `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
|
|
2081
2079
|
| `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
|
|
2082
2080
|
|
|
@@ -2088,12 +2086,12 @@ target, an answer streak, a per-queue count for today -- anything bucketed by da
|
|
|
2088
2086
|
by somebody's day, and a platform that was never told whose uses its own. An agent in Chennai
|
|
2089
2087
|
then finds their day rolling at 05:30, and a night shift in Chicago lands in two buckets.
|
|
2090
2088
|
|
|
2091
|
-
So the
|
|
2089
|
+
So the agent application says whose day it is, and it says so first. `AuthenticationContext.timeZone` is the
|
|
2092
2090
|
agent's zone as an IANA name -- `Asia/Kolkata`, `America/Chicago` -- never an offset, since an
|
|
2093
2091
|
offset cannot survive a daylight-saving boundary and a day boundary is exactly where that bites
|
|
2094
2092
|
(`context.timeZone`). It is stated before any identity exists, and `ConnectContext.timeZone` is the
|
|
2095
2093
|
same value again. The provider **stores it on the agent** and carries it as `identity.timeZone` on
|
|
2096
|
-
every `authenticated` state and on every `User` it returns from `
|
|
2094
|
+
every `authenticated` state and on every `User` it returns from `getUserDetails()`, so a lead
|
|
2097
2095
|
reading a colleague's yesterday sees the colleague's yesterday and a summary is bucketed by the
|
|
2098
2096
|
right day after a session has ended. A roaming agent corrects it by signing in from where they are.
|
|
2099
2097
|
|
|
@@ -2102,12 +2100,12 @@ field to fill in later: an identity without one is refused (`authentication.iden
|
|
|
2102
2100
|
so is a zone that is not an IANA name, wherever it appears. Nothing is ever assumed in its place --
|
|
2103
2101
|
not the viewer's browser, which gives a different answer per reader for the same record, and not the
|
|
2104
2102
|
provider's clock. The harness holds a provider to the round trip: the identity carries the zone the
|
|
2105
|
-
|
|
2103
|
+
agent application stated (`authentication.identity.timeZone.republished`), judged by what the name denotes:
|
|
2106
2104
|
`Asia/Kolkata` and `Asia/Calcutta` are one zone, and a provider that keeps the canonical name has
|
|
2107
2105
|
kept the zone. **The round trip is not the store.**
|
|
2108
2106
|
An adapter that echoes the stated zone back onto the identity passes that check with nothing kept,
|
|
2109
2107
|
and a lead reading a colleague's day would still get the wrong one. What proves the store is a zone
|
|
2110
|
-
the run never sent: a colleague's `User` from `
|
|
2108
|
+
the run never sent: a colleague's `User` from `getUserDetails()` carrying theirs. A provider's own
|
|
2111
2109
|
tests are where that is shown, with a second agent whose zone arrived through another session.
|
|
2112
2110
|
|
|
2113
2111
|
### Who the agent is
|
|
@@ -2150,10 +2148,10 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2150
2148
|
| `checkTime(request)` | The manifest declares `timeCheck: true`. Read-only clock check; no task command or accuracy guarantee. |
|
|
2151
2149
|
| `subscribe(listener)` | Always. |
|
|
2152
2150
|
| `disconnect()` | Always. |
|
|
2153
|
-
| `refused(report)` | Always. The
|
|
2154
|
-
| `setCapacity(capacity)` | Always. Nothing may be
|
|
2151
|
+
| `refused(report)` | Always. The agent application tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the agent application does with what it refuses**. |
|
|
2152
|
+
| `setCapacity(capacity)` | Always. Nothing may be assigned until a capacity is stated, so there is no connection that does not receive it. |
|
|
2155
2153
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2156
|
-
| `
|
|
2154
|
+
| `getUserDetails(ids)` | The adapter publishes any `UserId`: on `ForcedBreak.by`, a team member list, or `history[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2157
2155
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2158
2156
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2159
2157
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2163,7 +2161,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2163
2161
|
| `executeTeamLeadAssist(command)` | The login declares `capabilities.team.leadAssistControl`. |
|
|
2164
2162
|
| `executeTeamListen(command)` | The login declares `capabilities.team.listeningControl`. |
|
|
2165
2163
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
2166
|
-
| `recordStep(report)` | The manifest lists `softphone` among its `phones`. On a softphone the
|
|
2164
|
+
| `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**. |
|
|
2167
2165
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
2168
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. |
|
|
2169
2167
|
|
|
@@ -2203,16 +2201,16 @@ Registers a listener for provider changes and returns an idempotent unsubscribe
|
|
|
2203
2201
|
while disconnected without requiring a durable event log.
|
|
2204
2202
|
- After unsubscribe, the listener must receive no further events.
|
|
2205
2203
|
|
|
2206
|
-
### `Connection.
|
|
2204
|
+
### `Connection.getUserDetails(ids)`
|
|
2207
2205
|
|
|
2208
2206
|
Turns `UserId` values into something an agent can read.
|
|
2209
2207
|
|
|
2210
2208
|
```ts
|
|
2211
|
-
|
|
2209
|
+
getUserDetails(ids: UserId[]): Promise<User[]>
|
|
2212
2210
|
```
|
|
2213
2211
|
|
|
2214
2212
|
Required of any adapter that publishes a `UserId` — on `ForcedBreak.by`, a team member list, or
|
|
2215
|
-
`
|
|
2213
|
+
`history[].by`. Publishing an identifier Omni cannot resolve puts a name on screen
|
|
2216
2214
|
that reads as a database key.
|
|
2217
2215
|
|
|
2218
2216
|
- **Omit an id you cannot resolve; do not invent a name for it.** A missing entry says *I do not
|
|
@@ -2226,7 +2224,7 @@ that reads as a database key.
|
|
|
2226
2224
|
particular value, or on Omni asking again at any particular moment.
|
|
2227
2225
|
|
|
2228
2226
|
This is the only place a name comes from. Task data carries identifiers alone —
|
|
2229
|
-
`
|
|
2227
|
+
`history[].by` is an id and nothing more — so a name is never copied into a task, never
|
|
2230
2228
|
duplicated across tasks, and never stale.
|
|
2231
2229
|
|
|
2232
2230
|
### `Connection.disconnect()`
|
|
@@ -2238,19 +2236,19 @@ Stops the connection and releases adapter-owned resources.
|
|
|
2238
2236
|
- Must remove event handlers and release media resources owned by the adapter.
|
|
2239
2237
|
- Does not imply that active tasks were completed or removed.
|
|
2240
2238
|
|
|
2241
|
-
## Task
|
|
2239
|
+
## Task assignment lifecycle
|
|
2242
2240
|
|
|
2243
|
-
The task-
|
|
2241
|
+
The task-assignment lifecycle has five ordered stages:
|
|
2244
2242
|
|
|
2245
2243
|
**1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
|
|
2246
|
-
until the
|
|
2244
|
+
until the agent application has stated a capacity, and ready once it has. A provider assigns nothing before a
|
|
2247
2245
|
capacity is stated, and needs no other word for it.
|
|
2248
2246
|
|
|
2249
|
-
**2. The agent becomes ready.** The
|
|
2247
|
+
**2. The agent becomes ready.** The agent application states the capacity when its local policy says: at once,
|
|
2250
2248
|
or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
|
|
2251
2249
|
break does not imply readiness. After that, the only way an agent stops taking work is a break,
|
|
2252
2250
|
under `capabilities.breaks`; a login that declares no breaks cannot go not-ready once its capacity
|
|
2253
|
-
is stated, and `count: 0` is the
|
|
2251
|
+
is stated, and `count: 0` is the agent application's division of capacity, not the agent's choice.
|
|
2254
2252
|
|
|
2255
2253
|
**3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
|
|
2256
2254
|
much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
|
|
@@ -2271,13 +2269,13 @@ intent.
|
|
|
2271
2269
|
|
|
2272
2270
|
During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
|
|
2273
2271
|
provider states `acceptance` on each pending task — on the task rather than the offer, so a
|
|
2274
|
-
reconnect snapshot says it too and an offer the
|
|
2272
|
+
reconnect snapshot says it too and an offer the agent application never received is not accepted on the
|
|
2275
2273
|
person's behalf for want of a word:
|
|
2276
2274
|
|
|
2277
2275
|
| Directive | Contract |
|
|
2278
2276
|
| --- | --- |
|
|
2279
2277
|
| `no-preference` | The provider leaves acceptance to Omni; with `autoAcceptTasks: true`, Omni accepts automatically. |
|
|
2280
|
-
| `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done.
|
|
2278
|
+
| `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. An agent application that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
|
|
2281
2279
|
| `automatic` | Omni accepts immediately without agent interaction. |
|
|
2282
2280
|
|
|
2283
2281
|
When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task the
|
|
@@ -2287,20 +2285,20 @@ no-auto-accept policy puts no word on the wire at all — the field is absent, a
|
|
|
2287
2285
|
press is Omni's doing, not the provider's.
|
|
2288
2286
|
|
|
2289
2287
|
**The value is stated, never assumed.** `autoAcceptTasks` is required of every connection: a
|
|
2290
|
-
|
|
2288
|
+
agent application says which policy the deployment provisioned, and a validator that is not told checks
|
|
2291
2289
|
neither rule rather than guess the permissive one (`task.acceptance.required` and
|
|
2292
2290
|
`.unexpected` fire only against a stated value). `acceptance` is the provider's own control and
|
|
2293
2291
|
outranks the policy: `consent` puts the decision back in the agent's hands for any task where it
|
|
2294
|
-
belongs, whatever the
|
|
2292
|
+
belongs, whatever the agent application was configured with.
|
|
2295
2293
|
|
|
2296
2294
|
An automatically accepted task still arrives through `task-offered`.
|
|
2297
2295
|
|
|
2298
2296
|
**Work the agent originated is accepted by the command that created it.** A task born of the
|
|
2299
|
-
agent's own act — a dialpad call or a connect-back, recognisable by the
|
|
2297
|
+
agent's own act — a dialpad call or a connect-back, recognisable by the agent application's `dialId` on
|
|
2300
2298
|
`onCall`; a lead's join, carrying `assisting`; a lead's listen, carrying `listening` — arrives
|
|
2301
2299
|
through `task-offered` with `acceptance: "automatic"` whatever `autoAcceptTasks` says, and the
|
|
2302
|
-
validator holds it there under either
|
|
2303
|
-
the desk shows no **Accept** for a call the agent placed. The
|
|
2300
|
+
validator holds it there under either local policy and under none (`task.acceptance.originated`):
|
|
2301
|
+
the desk shows no **Accept** for a call the agent placed. The local policy governs work the queue
|
|
2304
2302
|
routes to the agent, and nothing else.
|
|
2305
2303
|
|
|
2306
2304
|
### Pending
|
|
@@ -2313,15 +2311,15 @@ automatic or requires the agent. A provider that requires automatic acceptance s
|
|
|
2313
2311
|
```ts
|
|
2314
2312
|
declare const task: Task;
|
|
2315
2313
|
|
|
2316
|
-
const
|
|
2314
|
+
const assignment = {
|
|
2317
2315
|
type: "task-offered",
|
|
2318
2316
|
task: { ...task, phase: "pending", acceptance: "consent" },
|
|
2319
|
-
|
|
2317
|
+
assignmentExpiresAt: "2026-08-25T10:41:07.000Z",
|
|
2320
2318
|
} satisfies Extract<ProviderEvent, { type: "task-offered" }>;
|
|
2321
2319
|
```
|
|
2322
2320
|
|
|
2323
2321
|
The rule the phase exists to express: **nothing is acquired on the agent's behalf while a task
|
|
2324
|
-
is pending.**
|
|
2322
|
+
is pending.** An agent application that carries media must not open the microphone until the task is
|
|
2325
2323
|
accepted. Omni does not open the task's browsers either — a task that rings out costs nothing.
|
|
2326
2324
|
|
|
2327
2325
|
When consent is required, Omni offers the agent an **Accept** control. The call is the
|
|
@@ -2334,7 +2332,7 @@ on a desk phone — and is not in question per call. See **How the agent hears t
|
|
|
2334
2332
|
|
|
2335
2333
|
Automatic acceptance still begins with `task-offered`.
|
|
2336
2334
|
|
|
2337
|
-
`
|
|
2335
|
+
`assignmentExpiresAt` is the deadline after which the offer lapses. Where present, Omni counts
|
|
2338
2336
|
down and stops offering **Accept** once it passes; the provider ends the lapsed offer with
|
|
2339
2337
|
`task-ended` and an `expired` outcome naming `pending`, since nobody cancelled it. **Omit it unless the provider can observe it.**
|
|
2340
2338
|
A provider that reports only elapsed ring time after the fact cannot say when an offer is due
|
|
@@ -2348,7 +2346,7 @@ A provider may withdraw a pending task by emitting `task-ended` with a `cancelle
|
|
|
2348
2346
|
`agent` for a decline, `provider` for a withdrawal or a re-route, `party` for a caller who abandoned
|
|
2349
2347
|
the ring. One word for the three left a supervisor's record unable to tell them apart, so `by` is
|
|
2350
2348
|
required and closed (`event.taskEnded.outcome.cancelled.by`). An offer nobody acted on before
|
|
2351
|
-
`
|
|
2349
|
+
`assignmentExpiresAt` is not cancelled by anyone: it lapses, and ends `expired` naming `pending`.
|
|
2352
2350
|
|
|
2353
2351
|
### Tasks already in progress
|
|
2354
2352
|
|
|
@@ -2356,7 +2354,7 @@ The provider does not introduce new work as already `in-progress`.
|
|
|
2356
2354
|
|
|
2357
2355
|
A task appears already `in-progress` only in a snapshot taken after a reconnect or a resync,
|
|
2358
2356
|
reporting work that began earlier in this login and never stopped. A fresh login has none to
|
|
2359
|
-
report: nothing has been
|
|
2357
|
+
report: nothing has been assigned yet, and work the agent was working elsewhere is not carried
|
|
2360
2358
|
into a new session.
|
|
2361
2359
|
|
|
2362
2360
|
## Tasks
|
|
@@ -2373,28 +2371,27 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2373
2371
|
|
|
2374
2372
|
| Field | Contract |
|
|
2375
2373
|
| --- | --- |
|
|
2376
|
-
| `
|
|
2377
|
-
| `allocationId` | Required `AllocationId`: this life of the task, minted once per offer and never reused for the life of the login, whatever the id does. Every event, command and report that names a task names its allocation too, so a late dial outcome or a late interaction report for the first customer never lands on the next under the same id. The stream refuses an offer reusing one (`stream.taskOffered.allocation`), an update restating another life of the id (`stream.taskUpdated.allocation`), and a media or ending event naming a life that has ended or that nobody has seen (`stream.allocation.ended`, `.unknown`); a `dial-outcome` may name an ended life, since a dial placed late routinely outlives its call, and the host routes it there. See **A task's life on the wire**. |
|
|
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 a media 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**. |
|
|
2378
2375
|
| `title` | Agent-facing task title. |
|
|
2379
2376
|
| `channel` | Channel used for this task. It must equal the source provider's manifest channel. |
|
|
2380
2377
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
2381
2378
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2382
|
-
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `ungoverned` when nothing handed the work over -- an agent's own outbound -- and `undetermined` 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.
|
|
2379
|
+
| `capabilitySource` | Required. Who chose the capabilities: `queue` when somebody configured these terms, `ungoverned` when nothing handed the work over -- an agent's own outbound -- and `undetermined` 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 `undetermined` where the agent works. See **Task capabilities**. |
|
|
2383
2380
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2384
2381
|
| `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. |
|
|
2385
2382
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2386
|
-
| `media` | 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-media-started`**. Media names a task whose work has begun, or whose party the
|
|
2383
|
+
| `media` | 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-media-started`**. Media 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.media.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 media may precede answer. |
|
|
2387
2384
|
| `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**. |
|
|
2388
2385
|
| `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**. |
|
|
2389
|
-
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` makes the provider initiate dialing, `host-calls` makes the
|
|
2386
|
+
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` makes the provider initiate dialing, `host-calls` makes the agent application issue Call, `waits` keeps the task in preview awaiting the agent. |
|
|
2390
2387
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
2391
2388
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2392
2389
|
| `wrapAllowance` | Fixed time allowed to complete the task after primary interaction ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
|
|
2393
2390
|
| `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. |
|
|
2394
|
-
| `
|
|
2395
|
-
| `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
|
|
2391
|
+
| `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
|
+
| `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**. |
|
|
2396
2393
|
| `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**. |
|
|
2397
|
-
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked,
|
|
2394
|
+
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, the member's `assignmentId`, and their note. The lead's task has an assignment of its own, so the member's is named, or the copy no longer says which call it joined. Its presence is what makes `lead-assist` `take-over-call` and `leave` issuable. See **Lead assist**. |
|
|
2398
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**. |
|
|
2399
2396
|
|
|
2400
2397
|
`TaskAttribute` entries carry typed detail alongside the task:
|
|
@@ -2423,15 +2420,15 @@ The canonical task transitions are:
|
|
|
2423
2420
|
|
|
2424
2421
|
| From | Decision or event | To |
|
|
2425
2422
|
| --- | --- | --- |
|
|
2426
|
-
| No task | Provider
|
|
2423
|
+
| No task | Provider assigns a task | `pending` |
|
|
2427
2424
|
| `pending` | Task is accepted | `confirmed` |
|
|
2428
2425
|
| `confirmed` | The customer's record is put in front of the agent before any call goes out (voice) | `preview` |
|
|
2429
2426
|
| `confirmed` | Work begins | `in-progress` |
|
|
2430
2427
|
| `preview` | Agent presses Call (`call`) and the customer answers, or the deadline `calls` and they answer | `in-progress` |
|
|
2431
2428
|
| `preview` | The call goes out and nobody answers | `completing` |
|
|
2432
2429
|
| `preview` | The preparation target passes with `atDeadline: "waits"` | Remains `preview`, waiting for the agent to press Call |
|
|
2433
|
-
| `pending` | Provider withdraws the
|
|
2434
|
-
| `pending` | The offer lapses at `
|
|
2430
|
+
| `pending` | Provider withdraws the assignment, the agent declines, or the party abandons the ring | Removed by `task-ended` with `cancelled` outcome, `by` saying which |
|
|
2431
|
+
| `pending` | The offer lapses at `assignmentExpiresAt` | Removed by `task-ended` with `expired` outcome naming `pending` |
|
|
2435
2432
|
| No task | Snapshot reports work already underway | `in-progress` |
|
|
2436
2433
|
| `in-progress` | Provider or agent pauses the task | `paused` |
|
|
2437
2434
|
| `in-progress` | Agent starts a warm transfer (`transfer` `warm`); the customer is parked | `paused` |
|
|
@@ -2443,23 +2440,26 @@ The canonical task transitions are:
|
|
|
2443
2440
|
|
|
2444
2441
|
#### A task's life on the wire
|
|
2445
2442
|
|
|
2446
|
-
A task
|
|
2447
|
-
|
|
2448
|
-
one
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2443
|
+
A task has one name, the assignment it is the record of, and the provider issues it. Platforms
|
|
2444
|
+
reuse their own handles: a closed one is retired minutes later, a requeue takes seconds, and on
|
|
2445
|
+
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, a media event, an
|
|
2447
|
+
ending, a history report, a command -- would land on whichever assignment is open when it arrives.
|
|
2448
|
+
So the assignment id is unique within the provider and never reused: where the platform's handle
|
|
2449
|
+
never comes back, the adapter passes it through; where it does, the adapter mints the assignment id
|
|
2450
|
+
and keeps the mapping, and the desk never learns which. A late event finds the assignment it
|
|
2451
|
+
belongs to or is refused, and a task-scoped browser session (`PROVIDER_NAME__TASK_ID__TAB_NAME`)
|
|
2452
|
+
lives one assignment, never the next customer's cookies. A dial the agent application placed was
|
|
2453
|
+
already safe, since its outcome is placed by the agent application's own `dialId`; where the
|
|
2454
|
+
assignment earns its place is everything the agent application does not mint --
|
|
2456
2455
|
`task-media-started`, `task-media-ended`, `task-ended`, a `recordStep` naming a task -- any of which
|
|
2457
|
-
would otherwise find the
|
|
2458
|
-
the adapter keeps, not a field held in memory beside it:
|
|
2459
|
-
|
|
2460
|
-
|
|
2456
|
+
would otherwise find the next customer under a reused handle and act on them. A minted id is part
|
|
2457
|
+
of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
|
|
2458
|
+
`store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
|
|
2459
|
+
adapter comes back with the same assignment on the same task, or every late event it was minted to
|
|
2460
|
+
catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
|
|
2461
2461
|
|
|
2462
|
-
|
|
2462
|
+
Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
|
|
2463
2463
|
task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
|
|
2464
2464
|
subsequent transitions to `preview` or `in-progress`; Omni does not infer them from the acceptance
|
|
2465
2465
|
command.
|
|
@@ -2486,9 +2486,9 @@ const previewed = {
|
|
|
2486
2486
|
const pressed: TaskCommand<"voice"> = { type: "call", dialId: "dial-7f2" };
|
|
2487
2487
|
```
|
|
2488
2488
|
|
|
2489
|
-
**Call is a dial.** It carries the
|
|
2489
|
+
**Call is a dial.** It carries the agent application's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2490
2490
|
`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 media starts on `dialling`, as every
|
|
2491
|
+
agent is there to be called. Its media starts on `dialling`, as every agent application-placed dial's does --
|
|
2492
2492
|
ring-back is audio the agent hears -- so `task-media-started` arrives on the `preview` task with
|
|
2493
2493
|
its party ringing. On `answered` the task is `in-progress`; on any other outcome the media ends,
|
|
2494
2494
|
`task-media-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
@@ -2503,8 +2503,8 @@ carries the provider's authoritative preparation-end instant and exactly one tri
|
|
|
2503
2503
|
|
|
2504
2504
|
| Declaration | At preparation end |
|
|
2505
2505
|
| --- | --- |
|
|
2506
|
-
| `atDeadline: "calls"` | The provider initiates dialing. The
|
|
2507
|
-
| `atDeadline: "host-calls"` | The
|
|
2506
|
+
| `atDeadline: "calls"` | The provider initiates dialing. The agent application never sends a timer-triggered Call. |
|
|
2507
|
+
| `atDeadline: "host-calls"` | The agent application sends the ordinary `call` command with a fresh agent application `dialId`. The provider does not independently auto-dial this task. |
|
|
2508
2508
|
| `atDeadline: "waits"` | Neither side auto-dials. The task remains in preview until the agent presses Call. |
|
|
2509
2509
|
|
|
2510
2510
|
The agent may press Call early in either fixed-preparation dialing mode, or at any time in
|
|
@@ -2518,53 +2518,52 @@ answer at that exact instant. Scheduling/dispatch delay must remain visible, and
|
|
|
2518
2518
|
prevented initiation must be reported rather than leaving a silently expired countdown.
|
|
2519
2519
|
A source eligibility threshold that may never trigger an attempt is not this promise.
|
|
2520
2520
|
|
|
2521
|
-
The two deadline fields travel together and only in preview. The
|
|
2522
|
-
time; clock estimation is allowed only under the explicit
|
|
2521
|
+
The two deadline fields travel together and only in preview. The agent application uses trusted provider-domain
|
|
2522
|
+
time; clock estimation is allowed only under the explicit agent application-triggered preview exception below.
|
|
2523
2523
|
Without usable provider-domain time
|
|
2524
2524
|
it cannot safely auto-trigger and must expose the uncertainty and reconcile. No zero-duration,
|
|
2525
2525
|
receipt-time or local-default deadline is inferred. Neither elapsed time nor submission changes
|
|
2526
2526
|
the task phase: subsequent authoritative events state ringing, answer, media and completion.
|
|
2527
2527
|
|
|
2528
|
-
|
|
2528
|
+
An agent application must serialize manual clicks and its timer under the exact provider/login/task/assignment
|
|
2529
2529
|
scope, allowing at most one unresolved submission. Recheck the current phase, deadline owner,
|
|
2530
|
-
|
|
2531
|
-
cancels an obsolete timer; login loss, disconnect, task end and
|
|
2530
|
+
assignment and whether an attempt already exists before dispatch. A new snapshot or policy update
|
|
2531
|
+
cancels an obsolete timer; login loss, disconnect, task end and assignment replacement fence its
|
|
2532
2532
|
callback. Reconnect past a deadline first reconciles current task and attempt state; it must not
|
|
2533
2533
|
blindly submit again. An uncertain prior submission remains unresolved, without automatic retry.
|
|
2534
2534
|
The provider atomically arbitrates any residual manual/timer race and does not accept a second
|
|
2535
2535
|
command as ownership of an already-running independent attempt.
|
|
2536
2536
|
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
dial has no invented
|
|
2537
|
+
Agent application-triggered automatic dialing is an ordinary agent application dial: its result is `dialling` with the same
|
|
2538
|
+
agent application `dialId`, followed by exactly one correlated terminal `dial-outcome`. A provider-triggered
|
|
2539
|
+
dial has no invented agent application ID or agent application dial outcome. It reports source-evidenced party ringing,
|
|
2540
2540
|
actual media and customer answer separately. In preview with `atDeadline: "calls"`, a ringing
|
|
2541
|
-
party without
|
|
2541
|
+
party without an agent application ID may carry actual pre-answer media; the deadline alone permits no media.
|
|
2542
2542
|
Media is still introduced/ended by the corresponding events and must not imply customer answer.
|
|
2543
2543
|
Submission acceptance is never an answered outcome or permission to publish `in-progress` early.
|
|
2544
2544
|
Unsupported source correlation/outcome/media evidence remains an integration blocker.
|
|
2545
2545
|
|
|
2546
2546
|
**Drop both fields when the phase moves.** A provider that builds the `in-progress` task by
|
|
2547
|
-
spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the
|
|
2547
|
+
spreading the `preview` one carries `previewEndsAt` and `atDeadline` with it, and the agent application refuses
|
|
2548
2548
|
the update (`task.preview.deadline.unexpected`): the desk keeps the task as it last stood, and the
|
|
2549
2549
|
provider is told through `refused` exactly which rule, so the state that looked right on its side
|
|
2550
2550
|
is named on its side. The task past preview has no deadline to wait for, so it carries neither
|
|
2551
2551
|
field.
|
|
2552
2552
|
|
|
2553
|
-
**Provider execution and
|
|
2553
|
+
**Provider execution and agent application controls are distinct.** Answer, hold/resume, transfer (including
|
|
2554
2554
|
IVR routing), conference and interaction completion are executed by the provider
|
|
2555
2555
|
through the adapter. The task's applicable capability, phase, completion mode and command-specific
|
|
2556
|
-
prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is
|
|
2557
|
-
TaskCommand or task capability. The separately declared
|
|
2558
|
-
recording contract; this distinction does not remove it or move provider call operations to the
|
|
2559
|
-
|
|
2560
|
-
A caller journey may revisit the same agent while an earlier interaction still wraps.
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
and recheck at the provider; the validator cannot prove source freshness or authorization.
|
|
2556
|
+
prerequisites decide which controls are offered. Caller disconnect also executes at the provider. Microphone mute is agent application-owned, never a provider
|
|
2557
|
+
TaskCommand or task capability. The separately declared agent application recording facility keeps its existing
|
|
2558
|
+
recording contract; this distinction does not remove it or move provider call operations to the agent application.
|
|
2559
|
+
|
|
2560
|
+
A caller journey may revisit the same agent while an earlier interaction still wraps. Each is its
|
|
2561
|
+
own assignment, and two open at once carry two assignment ids. Restore them unchanged on
|
|
2562
|
+
reconnect. Commands, media, endings and history reports name the exact assignment; never retarget
|
|
2563
|
+
an old command to the newest interaction of a journey. `validateTaskCommandRequest` checks the
|
|
2564
|
+
request's assignment against the supplied published task and then its command prerequisites. The
|
|
2565
|
+
caller must select that task within the correct provider/login and recheck at the provider; the
|
|
2566
|
+
validator cannot prove source freshness or authorization.
|
|
2568
2567
|
`task-ended` retires only that interaction's resources. Another interaction, caller channel or IVR/queue
|
|
2569
2568
|
portion must not be cleared because this task ended. Snapshot counts include all still-owned wrap
|
|
2570
2569
|
and active interactions; task completion is not caller hangup.
|
|
@@ -2615,7 +2614,7 @@ whether the channel carries real-time media:
|
|
|
2615
2614
|
| Other non-media channels | The `task-updated` that moves the task to `completing` |
|
|
2616
2615
|
|
|
2617
2616
|
**Off voice, `completing` is the provider's word that interaction ended.** There is no media event to
|
|
2618
|
-
carry it, so the phase does;
|
|
2617
|
+
carry it, so the phase does; an agent application starts the wrap clock at that publication and nowhere else. It
|
|
2619
2618
|
is optional: a conversation with nothing to wrap moves from `in-progress` to `task-ended` and
|
|
2620
2619
|
`completing` is never published. Under `provider-automatic` with a non-zero `wrapAllowance` it is
|
|
2621
2620
|
required, because the clock has to start somewhere: a chat or email task the provider completes
|
|
@@ -2661,7 +2660,7 @@ A task that declares `connectBack` lets the agent connect back to the party whil
|
|
|
2661
2660
|
whoever placed the call in the first place. Most such calls came in, so this is not a redial: the
|
|
2662
2661
|
agent rings a party who rang them. Omni issues `{ type: "connect-back", dialId }`; it is issuable
|
|
2663
2662
|
only in `completing`, and only where the capability is declared. The provider knows who the party
|
|
2664
|
-
is; the command carries no destination, and it is a dial like any other, so it carries the
|
|
2663
|
+
is; the command carries no destination, and it is a dial like any other, so it carries the agent application's
|
|
2665
2664
|
`dialId`, is answered `dialling`, and ends in one `dial-outcome`.
|
|
2666
2665
|
|
|
2667
2666
|
On `dialling` the provider is placing the call and the task returns to `in-progress`: the agent is
|
|
@@ -2672,15 +2671,15 @@ whose outcome says so and a call whose media ended: the task returns to `complet
|
|
|
2672
2671
|
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2673
2672
|
deadline.
|
|
2674
2673
|
|
|
2675
|
-
**The room shows the party being dialled.** From the moment any
|
|
2674
|
+
**The room shows the party being dialled.** From the moment any agent application dial places the party -- a
|
|
2676
2675
|
dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
|
|
2677
|
-
the
|
|
2676
|
+
the agent application's `dialId`, exactly as a consulted or conferenced entry does from its dial; `joined` on
|
|
2678
2677
|
the publication that follows the answered `dial-outcome` and never before it
|
|
2679
2678
|
(`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
|
|
2680
|
-
and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`).
|
|
2679
|
+
and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). An agent application shows a
|
|
2681
2680
|
call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
|
|
2682
2681
|
call over, the platform dialling the party again without a command -- shows the same thing with no
|
|
2683
|
-
|
|
2682
|
+
agent application `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
|
|
2684
2683
|
with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
|
|
2685
2684
|
tell a task coming back from one going backwards (`stream.taskUpdated.phase`).
|
|
2686
2685
|
|
|
@@ -2714,37 +2713,46 @@ const immediateProviderCompletion = {
|
|
|
2714
2713
|
} satisfies Pick<Task, "completionMode" | "wrapAllowance">;
|
|
2715
2714
|
```
|
|
2716
2715
|
|
|
2717
|
-
###
|
|
2716
|
+
### History
|
|
2718
2717
|
|
|
2719
|
-
The
|
|
2720
|
-
`
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2718
|
+
The record is the call's, not the task's: most of what it holds happened before this agent, and
|
|
2719
|
+
`queued` and `offered` are not interactions at all, so the field is `history` and its types are
|
|
2720
|
+
`TaskHistory`, `TaskHistoryStep`, `HistoryStep`, `HistoryReport` and `HistoryReportResult`.
|
|
2721
|
+
*Interaction* keeps the one meaning **Terms** gives it, the agent's time on the call, which is why
|
|
2722
|
+
the total of earlier agents' time is `interactionSeconds` and the phase rules are
|
|
2723
|
+
`command.phase.interaction`. Hosts and providers adopt these names together; no legacy aliases
|
|
2724
|
+
are provided. The manifest's `completionSettleMs` bounds final task completion, not entry into
|
|
2725
|
+
the `completing` wrap-up phase. The `recordStep` method and `complete` command keep their names.
|
|
2724
2726
|
|
|
2725
|
-
Migration from the earlier
|
|
2727
|
+
Migration from the earlier spellings:
|
|
2726
2728
|
|
|
2727
2729
|
| Former API | Current API |
|
|
2728
2730
|
| --- | --- |
|
|
2729
|
-
| Task.handlingHistory | `Task.
|
|
2730
|
-
| TaskHandlingHistory,
|
|
2731
|
-
|
|
|
2732
|
-
|
|
|
2733
|
-
|
|
|
2731
|
+
| Task.handlingHistory, Task.interactionHistory | `Task.history` |
|
|
2732
|
+
| TaskHandlingHistory, TaskInteractionHistory | `TaskHistory` |
|
|
2733
|
+
| TaskHandlingStep, HandlingStep, TaskInteractionStep, InteractionStep | `TaskHistoryStep`, `HistoryStep` |
|
|
2734
|
+
| HandlingReport, HandlingReportResult, validateHandlingReport, and their Interaction spellings | `HistoryReport`, `HistoryReportResult`, `validateHistoryReport` |
|
|
2735
|
+
| HANDLING_STEPS_WITH_A_PERSON, HANDLING_STEPS_THAT_DIAL, and their INTERACTION spellings | `HISTORY_STEPS_WITH_A_PERSON`, `HISTORY_STEPS_THAT_DIAL` |
|
|
2736
|
+
| handlingStepExpectsAPerson, handlingStepDials, and their interaction spellings | `historyStepExpectsAPerson`, `historyStepDials` |
|
|
2734
2737
|
| handleSeconds | `interactionSeconds` |
|
|
2738
|
+
| Task.allocationId, AllocationId, allocationExpiresAt | `Task.assignmentId`, `AssignmentId`, `assignmentExpiresAt` |
|
|
2739
|
+
| Task.id, TaskId, and `taskId` on every event, command, report and lead request | gone: a task is named by `assignmentId` alone, and `taskKey(providerId, assignmentId)` scopes it |
|
|
2735
2740
|
| Manifest.disposalSettleMs | `Manifest.completionSettleMs` |
|
|
2736
2741
|
|
|
2737
2742
|
Update producers, consumers, saved task snapshots, and validation-rule assertions together.
|
|
2738
|
-
History
|
|
2739
|
-
|
|
2740
|
-
|
|
2743
|
+
History and report rule names use `history` and `historyReport`; assignment rules use
|
|
2744
|
+
`assignment` (`task.assignmentId`, `stream.taskOffered.duplicate`, `stream.assignment.ended`);
|
|
2745
|
+
phase and conformance rules use `interaction` and final-task rules use `completion`. The former
|
|
2746
|
+
fields are refused even when the new field is also supplied (`task.history.renamed`,
|
|
2747
|
+
`task.assignmentId.renamed` for a task's `id` or `allocationId`, `event.assignmentId.renamed`,
|
|
2748
|
+
`command.request.assignmentId.renamed` and `historyReport.assignmentId.renamed` for a `taskId`).
|
|
2741
2749
|
|
|
2742
2750
|
|
|
2743
|
-
`Task.
|
|
2751
|
+
`Task.history` is the call record: the steps that brought the task to the agent, oldest
|
|
2744
2752
|
first, and what they add up to before this agent:
|
|
2745
2753
|
|
|
2746
2754
|
```ts
|
|
2747
|
-
|
|
2755
|
+
history: {
|
|
2748
2756
|
steps: [
|
|
2749
2757
|
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2750
2758
|
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
@@ -2767,7 +2775,7 @@ something else, which will arrive under its own name and must not be folded in h
|
|
|
2767
2775
|
It rides in the snapshot and is replaced whole like everything else there.
|
|
2768
2776
|
|
|
2769
2777
|
Steps are `queued`, `offered`, `answered`, `held`, `muted`, `transferred`, `conferenced`,
|
|
2770
|
-
`unanswered`, and each is defined on `
|
|
2778
|
+
`unanswered`, and each is defined on `TaskHistoryStep`.
|
|
2771
2779
|
|
|
2772
2780
|
**The record is one entry per occurrence, oldest first.** Each hold is its own `held` entry — `at`
|
|
2773
2781
|
when it began, `seconds` once it ended and omitted while it runs — and a second hold is a second
|
|
@@ -2775,20 +2783,20 @@ entry after the first, never a revision of it. A leg that has ended states its d
|
|
|
2775
2783
|
task says whether a leg can still be running: a hold runs only while the task is `paused`, a mute
|
|
2776
2784
|
only while its media is up, so a `held` entry without `seconds` on a task that is not paused, or a
|
|
2777
2785
|
`muted` one on a call that is over, is a leg nobody closed and reads exactly like a leg running now
|
|
2778
|
-
(`task.
|
|
2779
|
-
provider whose platform parks the caller, the
|
|
2786
|
+
(`task.history.held.open`, `.muted.open`). Whoever performs the leg closes it — the
|
|
2787
|
+
provider whose platform parks the caller, the agent application whose microphone it is — by restating the entry
|
|
2780
2788
|
with its duration, and **an entry that cannot be closed is not written**: open for ever is a
|
|
2781
2789
|
plausible nought one level down from a total. **A leg still open when the media ends is closed by
|
|
2782
2790
|
the provider**, at that instant, in the same publication that says the media ended: the provider
|
|
2783
|
-
is the one that knows the instant, and the
|
|
2784
|
-
muted, so the
|
|
2785
|
-
from the leg's `at` to the media's end, and the
|
|
2791
|
+
is the one that knows the instant, and the agent application can only learn of it afterwards. Agents end calls
|
|
2792
|
+
muted, so the agent application's leg is routinely open at media end; the provider closes it with the duration
|
|
2793
|
+
from the leg's `at` to the media's end, and the agent application's own closing report for that leg, which
|
|
2786
2794
|
follows what it hears, is answered `recorded` and changes nothing, the entry standing as the
|
|
2787
2795
|
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
|
|
2788
2796
|
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
|
|
2789
2797
|
`muted` entries, and a call that joined two queues -- a menu's, then this one -- has two `queued`
|
|
2790
2798
|
entries, one per join. Order is enforced: an entry earlier than the one before it is refused
|
|
2791
|
-
(`task.
|
|
2799
|
+
(`task.history.order`). **Intervals between steps are the agent application's to subtract**, never a
|
|
2792
2800
|
total the provider adds: *time to offer*, from the call's arrival to the agent's screen lighting
|
|
2793
2801
|
up, is the `offered` entry's `at` minus the last `queued` entry's, and the ring is outside it.
|
|
2794
2802
|
`queueSeconds` keeps the industry's meaning -- the whole wait until somebody answered -- and is not
|
|
@@ -2798,32 +2806,32 @@ grain: rounded to the nearest second, and a leg that rounds to nought is stated
|
|
|
2798
2806
|
being under the contract's resolution, where dropping the entry would say the leg never happened.
|
|
2799
2807
|
|
|
2800
2808
|
**A record once read is not unread.** Every restatement of a task's record -- on `task-updated`,
|
|
2801
|
-
on a resync snapshot -- carries every entry the
|
|
2809
|
+
on a resync snapshot -- carries every entry the agent application has already read, by step and instant, and
|
|
2802
2810
|
may add to them; it never has fewer, and never drops the record while the task is open. A fact
|
|
2803
|
-
stated to the
|
|
2811
|
+
stated to the agent application and then withdrawn without an event is a record contradicting itself, exactly
|
|
2804
2812
|
as a task that lost the terms it had read would be, and the stream refuses it the same way
|
|
2805
|
-
(`stream.taskUpdated.
|
|
2806
|
-
platform cannot hold a leg the
|
|
2807
|
-
task, so a reload of the
|
|
2813
|
+
(`stream.taskUpdated.history`, `stream.snapshot.history`). A provider whose
|
|
2814
|
+
platform cannot hold a leg the agent application reported keeps it in the login's `store` for the life of the
|
|
2815
|
+
task, so a reload of the agent application restates the same record; the entry is keyed by `step` and `at`, so
|
|
2808
2816
|
a running hold restated with its final `seconds` is the same entry.
|
|
2809
2817
|
|
|
2810
|
-
**The provider owns the timestamps in its record.**
|
|
2811
|
-
may retain
|
|
2812
|
-
copy the
|
|
2818
|
+
**The provider owns the timestamps in its record.** Agent application timestamps are advisory. The provider
|
|
2819
|
+
may retain an agent application instant it accepts or assign its own observation/receipt instant; it need not
|
|
2820
|
+
copy the agent application clock. A provider declaring `timestampAuthority: "provider"` uses its own timestamps
|
|
2813
2821
|
for final records. Receipt time is an observation boundary, not a claim about when the physical
|
|
2814
|
-
|
|
2815
|
-
rewriting of an already-published entry when a later
|
|
2822
|
+
agent application action happened. Records are ordered by their published provider-selected instants, with no
|
|
2823
|
+
rewriting of an already-published entry when a later agent application report arrives.
|
|
2816
2824
|
|
|
2817
|
-
The incoming
|
|
2825
|
+
The incoming agent application `step` and `at`, scoped to the task and its current life, identify one reported
|
|
2818
2826
|
leg for correlation only. The provider retains a binding from that key to its chosen history
|
|
2819
2827
|
`at`. Every successful `recordStep` returns `{ status: "recorded", at }` with that same canonical
|
|
2820
|
-
history instant, even for subsequent duration/end reports. The
|
|
2821
|
-
report key, never the returned history timestamp. A changed
|
|
2828
|
+
history instant, even for subsequent duration/end reports. The agent application keeps sending the original
|
|
2829
|
+
report key, never the returned history timestamp. A changed agent application timestamp is not a correction
|
|
2822
2830
|
to the same report: it names a different leg. Bindings survive reconnect/reload for the retained
|
|
2823
|
-
task lifetime. The
|
|
2831
|
+
task lifetime. The agent application renders provider-published history unchanged. It never compares the
|
|
2824
2832
|
provider instant back to its own clock, substitutes its own timestamp, or adjusts later messages
|
|
2825
|
-
to match an earlier
|
|
2826
|
-
correlation/conformance only; it is not
|
|
2833
|
+
to match an earlier agent application estimate. The acknowledgment identifies the provider record for
|
|
2834
|
+
correlation/conformance only; it is not an application-side timestamp reconciliation instruction. Conflicting reuse is refused visibly, not paired by arrival time or nearest instant.
|
|
2827
2835
|
If two distinct entries would collide under the history's `(step, at)` key, do not invent a time
|
|
2828
2836
|
or combine them: report the representation conflict. No ambiguous event is silently accepted.
|
|
2829
2837
|
|
|
@@ -2833,17 +2841,17 @@ history — until the task's media ends, and a hold neither pauses nor resets it
|
|
|
2833
2841
|
duration is the `held` entry's `seconds`, and a desk that restarts its counter on resume is
|
|
2834
2842
|
counting the wrong thing.
|
|
2835
2843
|
|
|
2836
|
-
`muted` is there because the mute is the
|
|
2837
|
-
the
|
|
2838
|
-
account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the
|
|
2839
|
-
reported it: `host` for the agent's own Mute, `station` for a headset or system mute the
|
|
2840
|
-
observed. No other step has it (`task.
|
|
2844
|
+
`muted` is there because the mute is the agent application's and never the provider's — see **The station is
|
|
2845
|
+
the agent application's** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2846
|
+
account of a period the agent could not be heard. Each `muted` entry carries `mutedBy`, as the agent application
|
|
2847
|
+
reported it: `host` for the agent's own Mute, `station` for a headset or system mute the agent application
|
|
2848
|
+
observed. No other step has it (`task.history.mutedBy`, `.mutedBy.unexpected`).
|
|
2841
2849
|
|
|
2842
2850
|
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2843
|
-
each end a dial, so the entry carries the `destinationId` it went to and, where
|
|
2844
|
-
`dialId` the
|
|
2845
|
-
(`task.
|
|
2846
|
-
by whoever holds the task now: a `dial-outcome` naming a `dialId` the
|
|
2851
|
+
each end a dial, so the entry carries the `destinationId` it went to and, where an agent application placed it, the
|
|
2852
|
+
`dialId` the agent application minted; no other step dialled, and either field on one is refused
|
|
2853
|
+
(`task.history.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2854
|
+
by whoever holds the task now: a `dial-outcome` naming a `dialId` the agent application never minted is looked
|
|
2847
2855
|
up in the record, not discarded.
|
|
2848
2856
|
|
|
2849
2857
|
Four rules a provider has to keep:
|
|
@@ -2859,16 +2867,16 @@ Four rules a provider has to keep:
|
|
|
2859
2867
|
the same way `ForcedBreak.by` does. It comes from this provider's own directory, the same
|
|
2860
2868
|
namespace as `AuthenticationState.identity.id` and the team member list, so entries pair
|
|
2861
2869
|
within a provider and never across one.
|
|
2862
|
-
- **A task carries no names.** Omni resolves what to display with `
|
|
2870
|
+
- **A task carries no names.** Omni resolves what to display with `getUserDetails()`. Two people
|
|
2863
2871
|
called Arun on one site is ordinary, and anything pairing entries on a display name pairs them
|
|
2864
2872
|
wrongly; carrying the name here would also copy it into every task and leave it to go stale.
|
|
2865
2873
|
|
|
2866
2874
|
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
2867
2875
|
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
2868
|
-
`
|
|
2876
|
+
`HISTORY_STEPS_WITH_A_PERSON` and `historyStepExpectsAPerson()` — so an absent `by` there says
|
|
2869
2877
|
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
2870
|
-
`muted`: the
|
|
2871
|
-
(`task.
|
|
2878
|
+
`muted`: the agent application has exactly one agent and the provider knows who, so `by` is required there
|
|
2879
|
+
(`task.history.muted.by`).
|
|
2872
2880
|
|
|
2873
2881
|
That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
|
|
2874
2882
|
handset, or a device the provider cannot resolve to a person. **Report the step without `by`
|
|
@@ -2876,11 +2884,11 @@ rather than dropping it.** A list missing a real handler looks complete and is w
|
|
|
2876
2884
|
worse than one saying plainly it could not attribute a leg — and far better than publishing
|
|
2877
2885
|
nothing because a single leg could not be named.
|
|
2878
2886
|
|
|
2879
|
-
|
|
2887
|
+
An agent application must render the two differently. Showing an unattributed `answered` the same way as
|
|
2880
2888
|
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2881
2889
|
*"not recorded"* in the place the name would go.
|
|
2882
2890
|
|
|
2883
|
-
Omit `
|
|
2891
|
+
Omit `history` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2884
2892
|
different claim — it says the task has had none.
|
|
2885
2893
|
|
|
2886
2894
|
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
@@ -2893,45 +2901,44 @@ zeros. The record is restated with the task on every `task-updated` and snapshot
|
|
|
2893
2901
|
that arrives late corrects the sums. These four are what every platform reports; more will be
|
|
2894
2902
|
added here as a need is shown, not invented ahead of one.
|
|
2895
2903
|
|
|
2896
|
-
#### The
|
|
2904
|
+
#### The agent application records what it performs
|
|
2897
2905
|
|
|
2898
|
-
`muted` is the one leg the
|
|
2899
|
-
provider would have a hole exactly there. So the
|
|
2906
|
+
`muted` is the one leg the agent application performs rather than the provider, and a record kept by the
|
|
2907
|
+
provider would have a hole exactly there. So the agent application reports it, through `recordStep`, and the provider
|
|
2900
2908
|
writes it into its record as it writes every other leg:
|
|
2901
2909
|
|
|
2902
2910
|
```ts
|
|
2903
2911
|
declare const connection: Connection<"voice">;
|
|
2904
|
-
declare const
|
|
2905
|
-
declare const allocationId: AllocationId;
|
|
2912
|
+
declare const assignmentId: AssignmentId;
|
|
2906
2913
|
declare const at: IsoTimestamp;
|
|
2907
2914
|
|
|
2908
|
-
void connection.recordStep?.({
|
|
2909
|
-
void connection.recordStep?.({
|
|
2910
|
-
void connection.recordStep?.({
|
|
2915
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host" }); // the moment the agent mutes
|
|
2916
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 15 }); // still running, if the agent application chooses to say
|
|
2917
|
+
void connection.recordStep?.({ assignmentId, step: "muted", at, mutedBy: "host", seconds: 42, ended: true }); // the moment they unmute
|
|
2911
2918
|
```
|
|
2912
2919
|
|
|
2913
|
-
Every report about one leg repeats its original `step` and
|
|
2914
|
-
the published history uses the provider-selected `at` returned by `recordStep`. The
|
|
2920
|
+
Every report about one leg repeats its original `step` and agent application `at` as a correlation key;
|
|
2921
|
+
the published history uses the provider-selected `at` returned by `recordStep`. The agent application reports
|
|
2915
2922
|
the action it performed and its measured duration, not an authoritative provider timestamp. It may say how long so far — `seconds` is
|
|
2916
2923
|
elapsed while the leg runs and final once it has ended — and **the end is stated, never
|
|
2917
2924
|
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
2918
2925
|
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
2919
2926
|
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
2920
|
-
leg, when it began and when it ended, and `
|
|
2921
|
-
a running one it was never asked for (`
|
|
2927
|
+
leg, when it began and when it ended, and `validateHistoryReport(report, path, manifest)` refuses
|
|
2928
|
+
a running one it was never asked for (`historyReport.running.unexpected`). What a provider that
|
|
2922
2929
|
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
2923
|
-
`
|
|
2930
|
+
`history` when the *provider* publishes it: Omni never writes the record itself.
|
|
2924
2931
|
`recordStep` is required of every softphone login's connection, since every call on a softphone
|
|
2925
|
-
can be muted by the
|
|
2926
|
-
`mutedBy: "host"` or `"station"`, and no other report has the word (`
|
|
2932
|
+
can be muted by the agent application, and answers `recorded` with the canonical history `at`. A `muted` report says whose the silence was,
|
|
2933
|
+
`mutedBy: "host"` or `"station"`, and no other report has the word (`historyReport.mutedBy`,
|
|
2927
2934
|
`.mutedBy.unexpected`). On a desk phone the microphone is the phone's:
|
|
2928
|
-
the
|
|
2935
|
+
the agent application mutes nothing and records nothing.
|
|
2929
2936
|
|
|
2930
|
-
**The provider's confirmed end is decisive.** The
|
|
2937
|
+
**The provider's confirmed end is decisive.** The agent application keeps local state for interaction and
|
|
2931
2938
|
reporting, but follows the provider's authoritative state. When the provider publishes the end
|
|
2932
|
-
of the current mute leg, the
|
|
2939
|
+
of the current mute leg, the agent application ends that local mute and releases its agent application-controlled mute
|
|
2933
2940
|
on the matching task's media. It does not wait for its own timer, reopen the leg, replace the
|
|
2934
|
-
provider timestamp, or overwrite the provider's final duration with a later
|
|
2941
|
+
provider timestamp, or overwrite the provider's final duration with a later agent application report.
|
|
2935
2942
|
A later agent mute is a new leg, never a reopening of the ended one.
|
|
2936
2943
|
|
|
2937
2944
|
Correlate the provider-published `muted` entry using the provider-selected `at` acknowledged
|
|
@@ -2939,15 +2946,15 @@ for the current report key; a final `seconds` closes that leg under the history
|
|
|
2939
2946
|
This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
|
|
2940
2947
|
task, or a stale connection cannot end a newer local mute. If publication precedes the acknowledgment,
|
|
2941
2948
|
retain the current provider view and apply the matching closure once correlation is available;
|
|
2942
|
-
never guess a match from arrival order or nearest timestamp. Failure to release
|
|
2949
|
+
never guess a match from arrival order or nearest timestamp. Failure to release an agent application-controlled
|
|
2943
2950
|
mute is reported visibly; local device reports still describe the actual device state.
|
|
2944
2951
|
|
|
2945
|
-
At a provider-confirmed task/media end, the
|
|
2946
|
-
its observed ending where the report is still accepted.
|
|
2952
|
+
At a provider-confirmed task/media end, the agent application also stops the associated local mute and reports
|
|
2953
|
+
its observed ending where the report is still accepted. An agent application closing report repeats its original
|
|
2947
2954
|
key and may include its measured duration, but cannot reverse an already confirmed provider end.
|
|
2948
2955
|
The provider acknowledges a known closed leg without rewriting its final record; after the task
|
|
2949
|
-
has been completed, it may refuse the report as task-not-found. The
|
|
2950
|
-
than retrying or recreating the task. An observed
|
|
2956
|
+
has been completed, it may refuse the report as task-not-found. The agent application reads that response rather
|
|
2957
|
+
than retrying or recreating the task. An observed agent application end before a provider closure is still
|
|
2951
2958
|
reported normally; the provider decides the final timestamp and publishes the record.
|
|
2952
2959
|
|
|
2953
2960
|
### Browser capability
|
|
@@ -2981,7 +2988,7 @@ Each `TaskBrowser` defines one named browser in the task workspace.
|
|
|
2981
2988
|
| `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
|
|
2982
2989
|
| `sharedSession` | Required. `false` creates a task-specific browser session. |
|
|
2983
2990
|
| `isolationScheme` | **Required when `sharedSession` is `true`**, and rejected when it is `false`. There is no default: see below. |
|
|
2984
|
-
| `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word;
|
|
2991
|
+
| `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; an agent application that declares `guarantees.browserUrlVisibility` honours it tab by tab, and a provider checks that guarantee before it sends such a URL at all. |
|
|
2985
2992
|
|
|
2986
2993
|
##### Choosing an isolation scheme
|
|
2987
2994
|
|
|
@@ -3042,7 +3049,7 @@ The supported `BrowserIsolationScheme` values, declared under **Shapes**, key as
|
|
|
3042
3049
|
|
|
3043
3050
|
| Enum member | Example session key |
|
|
3044
3051
|
| --- | --- |
|
|
3045
|
-
| `PROVIDER_NAME__TASK_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's
|
|
3052
|
+
| `PROVIDER_NAME__TASK_ID__TAB_NAME` | `mailflow.EMAIL-829102%2Ea1.CRM` -- the task's assignment id, so one assignment, never the next customer's cookies; the scheme's name is a stable wire value and keeps its spelling |
|
|
3046
3053
|
| `TAB_NAME` | `CRM` |
|
|
3047
3054
|
| `PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME` | `mailflow.Support.CRM` |
|
|
3048
3055
|
| `PROVIDER_NAME__TAB_NAME` | `mailflow.CRM` |
|
|
@@ -3072,8 +3079,8 @@ that produces two consumers disagreeing about one fact. The task is the one sour
|
|
|
3072
3079
|
provider puts on the task what the platform permits for it -- and says, in `capabilitySource`, who
|
|
3073
3080
|
chose those terms. `queue` says somebody configured them. `ungoverned` says nothing handed the work
|
|
3074
3081
|
over: an agent's own outbound call has no queue behind it, and the provider states what it permits
|
|
3075
|
-
for such a call. Neither changes where
|
|
3076
|
-
source is one more published fact about them, and the one that lets
|
|
3082
|
+
for such a call. Neither changes where an agent application reads the capabilities from, which is the task; the
|
|
3083
|
+
source is one more published fact about them, and the one that lets an agent application tell a fact from a fault
|
|
3077
3084
|
(see below).
|
|
3078
3085
|
|
|
3079
3086
|
**Completing the task is not a capability, and no capability set withholds it.** `complete` is
|
|
@@ -3092,14 +3099,14 @@ task said so both times, at the moment it became true. A call moves queues the s
|
|
|
3092
3099
|
identified as a priority customer is transferred to the priority queue, and the task arrives under
|
|
3093
3100
|
that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
|
|
3094
3101
|
restated on the task at the hand-over, not inferred from the queue it left. A capability stated
|
|
3095
|
-
once at offer or answer and never corrected is a fact with a shelf life and no expiry, and
|
|
3102
|
+
once at offer or answer and never corrected is a fact with a shelf life and no expiry, and an agent application
|
|
3096
3103
|
draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
|
|
3097
|
-
the task, with its capabilities as they now stand, and
|
|
3104
|
+
the task, with its capabilities as they now stand, and an agent application treats the last statement as current
|
|
3098
3105
|
rather than re-deriving anything. This is the same shape as a room left full after the call ends
|
|
3099
3106
|
-- a field describing the present, carried past the moment it stopped being true -- carried past
|
|
3100
3107
|
this time not by a spread but by nobody sending the correction. A provider whose platform can
|
|
3101
3108
|
change a permission mid-task and does not republish is in breach, however conformant its offer was.
|
|
3102
|
-
A capability set can shrink as well as grow, and
|
|
3109
|
+
A capability set can shrink as well as grow, and an agent application that has only ever seen it grow meets a
|
|
3103
3110
|
control that was there a moment ago and is gone; a command that arrives after its capability was
|
|
3104
3111
|
withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
|
|
3105
3112
|
provider gets right first: a control gained late is a nicety, a control withdrawn and not
|
|
@@ -3108,12 +3115,12 @@ direction: the task as offered and as republished, and one command that was issu
|
|
|
3108
3115
|
first and is refused under the last for want of the capability withdrawn -- and nothing else.
|
|
3109
3116
|
|
|
3110
3117
|
**An empty capability set is a statement, not a shrug.** `capabilities: {}` under `queue` or
|
|
3111
|
-
`ungoverned` says the platform permits nothing capability-gated on this task, and
|
|
3118
|
+
`ungoverned` says the platform permits nothing capability-gated on this task, and an agent application draws
|
|
3112
3119
|
nothing beyond what the phase and `completionMode` require. A provider that has not yet learned
|
|
3113
3120
|
what the platform permits -- a queue's configuration that has not reached it -- knows nothing of
|
|
3114
3121
|
the kind, and must not publish the task as if it did, neither as `{}` nor as every control it has.
|
|
3115
3122
|
It publishes the task under `capabilitySource: "undetermined"`, with the capabilities it will
|
|
3116
|
-
honour until it knows, and
|
|
3123
|
+
honour until it knows, and an agent application shows that where the agent works, beside the controls it draws
|
|
3117
3124
|
from them: "no queue governs this call" is a fact, "the configuration has not arrived" is a fault,
|
|
3118
3125
|
and the set alone cannot tell them apart, so the provider says which. What a provider honours
|
|
3119
3126
|
under undetermined terms is its own call -- a floor that would rather an agent briefly hold a
|
|
@@ -3126,7 +3133,7 @@ Terms once read stay read: a re-read that fails mid-task is not a new fact about
|
|
|
3126
3133
|
last statement stands and the failure is a `diagnostic`, and a task that was published under
|
|
3127
3134
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
3128
3135
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
3129
|
-
entry the same two ways (`stream.taskUpdated.
|
|
3136
|
+
entry the same two ways (`stream.taskUpdated.history`, `stream.snapshot.history`);
|
|
3130
3137
|
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
3131
3138
|
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); a voice task ends after
|
|
3132
3139
|
its audio ends, never around it, whatever the outcome (`stream.taskEnded.mediaOpen`); and a task does not go
|
|
@@ -3158,9 +3165,9 @@ a message that shows and clears has said nothing about the buttons still on the
|
|
|
3158
3165
|
words are close in English and far apart on the desk: `ungoverned` is silence, `undetermined` is a
|
|
3159
3166
|
standing notice.
|
|
3160
3167
|
|
|
3161
|
-
One consequence for whoever builds the
|
|
3162
|
-
conformant adapter never shows
|
|
3163
|
-
result says nothing about how the
|
|
3168
|
+
One consequence for whoever builds the agent application: because a conformance run fails on `undetermined`, a
|
|
3169
|
+
conformant adapter never shows an agent application `undetermined` under test, and a clean `exerciseAdapter`
|
|
3170
|
+
result says nothing about how the agent application renders it. That rendering is tested against a fixture --
|
|
3164
3171
|
a task published under `undetermined` shows the notice, the same task under `queue` shows none --
|
|
3165
3172
|
and never against an adapter, even in principle. `exerciseAdapter` treats a
|
|
3166
3173
|
task published under `undetermined` as a violation (`capabilitySource.undetermined`), as it treats
|
|
@@ -3187,7 +3194,7 @@ See **Which commands need a capability**.
|
|
|
3187
3194
|
|
|
3188
3195
|
| Capability | Omni UI | Contract |
|
|
3189
3196
|
| --- | --- | --- |
|
|
3190
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when
|
|
3197
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending voice offer. Omni shows it only when local policy also permits declining. |
|
|
3191
3198
|
| `hold` | Primary toggle: Hold | Omni may issue voice-task `hold` and `resume` commands. |
|
|
3192
3199
|
| `endCall` | Primary button: End call | The provider ends the caller connection and all owned/inherited agent-added channels; wrap/completion remain separate. See **Ending a call, and removing one person from it**. |
|
|
3193
3200
|
| `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**. |
|
|
@@ -3195,7 +3202,7 @@ See **Which commands need a capability**.
|
|
|
3195
3202
|
| `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**. |
|
|
3196
3203
|
| `leadAssist` | Secondary menu item: Lead assist | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.leadAssist`. See **Lead assist**. |
|
|
3197
3204
|
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and remove one person from it -- a conferenced entry, one still ringing included, which calls the dial off, or the party, leaving the agent with the colleague. See **Ending a call, and removing one person from it**. |
|
|
3198
|
-
| `recording` | Overflow menu item: Recording | Per-task provider and
|
|
3205
|
+
| `recording` | Overflow menu item: Recording | Per-task provider and agent application policies expose only their permitted recording actions; each has independent state and routing. |
|
|
3199
3206
|
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3200
3207
|
|
|
3201
3208
|
### Publishing codes and destinations
|
|
@@ -3277,7 +3284,7 @@ reason (`task.destinations.shape`): once nothing is typed, the directory is the
|
|
|
3277
3284
|
|
|
3278
3285
|
**The provider chooses the supported destinations.** A published item may route to an IVR,
|
|
3279
3286
|
queue, named agent or another configured destination. No destination kind is inferred from its
|
|
3280
|
-
label. The
|
|
3287
|
+
label. The agent application offers only the directory attached to this task's specific transfer capability,
|
|
3281
3288
|
and sends the exact destination ID; it does not invent an agent picker or arbitrary dial target.
|
|
3282
3289
|
The provider rechecks eligibility and executes the routing. Returning to an IVR or queue does not
|
|
3283
3290
|
end the caller journey; a later assignment to the same agent is a new interaction.
|
|
@@ -3293,19 +3300,19 @@ provider.
|
|
|
3293
3300
|
{ type: "conference", action: "remove", destinationId: "tier2" } // the conferenced person leaves; ringing, this calls the dial off
|
|
3294
3301
|
```
|
|
3295
3302
|
|
|
3296
|
-
The
|
|
3303
|
+
The agent application sends the ordinary task-scoped `execute` request with `command: { type: "end-call" }`.
|
|
3297
3304
|
The provider knows the caller and agent-added channel bindings; no channel list or owner details
|
|
3298
|
-
are sent by the
|
|
3299
|
-
`validateTaskCommandRequest` checks the exact interaction/
|
|
3305
|
+
are sent by the agent application. The task's `endCall` capability is permission, not an executor selection.
|
|
3306
|
+
`validateTaskCommandRequest` checks the exact interaction/assignment and delegates the command's
|
|
3300
3307
|
policy/phase checks. The provider rechecks current ownership atomically; no client validator can
|
|
3301
|
-
prove that its view is fresh. The
|
|
3302
|
-
this agent action. There is no
|
|
3308
|
+
prove that its view is fresh. The agent application does not implement agent end-call; low-level removal of individual channels is not
|
|
3309
|
+
this agent action. There is no agent application end-call execution API or fallback route.
|
|
3303
3310
|
|
|
3304
3311
|
`end-call` is gated by `endCall` on the current interaction. It ends the caller connection and
|
|
3305
3312
|
all agent-added channels owned by that interaction, including channels originally added by previous
|
|
3306
3313
|
agents and inherited through transfer or takeover. Ownership is not inferred from who originally
|
|
3307
3314
|
dialed, the current UI selection, or a person's label. The provider maintains that authority
|
|
3308
|
-
and checks the exact interaction/
|
|
3315
|
+
and checks the exact interaction/assignment when acting. Agent-added channels are not left behind
|
|
3309
3316
|
merely because another agent added them.
|
|
3310
3317
|
|
|
3311
3318
|
On a successful transfer or takeover, ownership of those channels passes to the receiving
|
|
@@ -3351,7 +3358,7 @@ offer any of the three without the others, and each is declared on its own.
|
|
|
3351
3358
|
```
|
|
3352
3359
|
|
|
3353
3360
|
`warm` is gated by the `warmTransfer` capability and names a directory item exactly as a cold
|
|
3354
|
-
transfer does, from the same kind of directory. It is a dial, so it carries the
|
|
3361
|
+
transfer does, from the same kind of directory. It is a dial, so it carries the agent application's `dialId`
|
|
3355
3362
|
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
3356
3363
|
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
3357
3364
|
is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
|
|
@@ -3377,7 +3384,7 @@ customer.
|
|
|
3377
3384
|
|
|
3378
3385
|
| Capability | Omni UI | Contract |
|
|
3379
3386
|
| --- | --- | --- |
|
|
3380
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when
|
|
3387
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
|
|
3381
3388
|
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
3382
3389
|
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3383
3390
|
|
|
@@ -3385,7 +3392,7 @@ customer.
|
|
|
3385
3392
|
|
|
3386
3393
|
| Capability | Omni UI | Contract |
|
|
3387
3394
|
| --- | --- | --- |
|
|
3388
|
-
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when
|
|
3395
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
|
|
3389
3396
|
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
|
3390
3397
|
|
|
3391
3398
|
### Custom capabilities
|
|
@@ -3420,7 +3427,7 @@ control and invokes it with the shared custom task command:
|
|
|
3420
3427
|
}
|
|
3421
3428
|
```
|
|
3422
3429
|
|
|
3423
|
-
`name` is the `id` of the custom capability the agent used. There is no `
|
|
3430
|
+
`name` is the `id` of the custom capability the agent used. There is no `assignmentId` here: like every
|
|
3424
3431
|
other command it travels on the `TaskCommandRequest` around it. A `toggle` carries the state it
|
|
3425
3432
|
wants — `{ type: "custom", name: "mark-vip", on: true }` — never a flip, for the reason under
|
|
3426
3433
|
**Task commands**.
|
|
@@ -3434,7 +3441,7 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
|
|
|
3434
3441
|
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
3435
3442
|
declares `dial`.
|
|
3436
3443
|
|
|
3437
|
-
- `dialId` is the
|
|
3444
|
+
- `dialId` is the agent application's identity for this dial, minted before the request leaves.
|
|
3438
3445
|
- `destination` is the original number selected or entered by the agent.
|
|
3439
3446
|
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
3440
3447
|
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
@@ -3454,43 +3461,42 @@ and apart from its answer -- the destination picks up, is busy, or never does --
|
|
|
3454
3461
|
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
3455
3462
|
`dialling`, then says how it ended, once.
|
|
3456
3463
|
|
|
3457
|
-
**The
|
|
3464
|
+
**The agent application mints the identity.** Every command that dials carries a `dialId` the agent application made, unique
|
|
3458
3465
|
across the installation, and it travels four ways so no leg is a lookup:
|
|
3459
3466
|
|
|
3460
3467
|
```ts
|
|
3461
3468
|
declare const connection: Connection<"voice">;
|
|
3462
|
-
declare const
|
|
3463
|
-
declare const allocationId: AllocationId;
|
|
3469
|
+
declare const assignmentId: AssignmentId;
|
|
3464
3470
|
|
|
3465
|
-
// 1. Out, minted by the
|
|
3466
|
-
const result = await connection.execute({
|
|
3471
|
+
// 1. Out, minted by the agent application.
|
|
3472
|
+
const result = await connection.execute({ assignmentId, command: { type: "conference", action: "add", dialId: "dial-7f2", destinationId: "tier2" } });
|
|
3467
3473
|
|
|
3468
|
-
// 2. Back on the result, restated, so the
|
|
3474
|
+
// 2. Back on the result, restated, so the agent application compares and confirms.
|
|
3469
3475
|
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
3470
3476
|
|
|
3471
3477
|
// 3. On the outcome, however late, against the task it belonged to.
|
|
3472
|
-
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2",
|
|
3478
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", assignmentId, destinationId: "tier2", outcome: "no-answer", reason: "No route to destination" };
|
|
3473
3479
|
|
|
3474
3480
|
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
3475
|
-
const step:
|
|
3481
|
+
const step: TaskHistoryStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destinationId: "tier2" };
|
|
3476
3482
|
```
|
|
3477
3483
|
|
|
3478
|
-
The identity is a correlation handle between
|
|
3479
|
-
agent**.
|
|
3484
|
+
The identity is a correlation handle between agent application and provider and **is never shown to the
|
|
3485
|
+
agent**. An agent application holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
3480
3486
|
mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
|
|
3481
|
-
its `
|
|
3487
|
+
its `history`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
3482
3488
|
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
3483
3489
|
|
|
3484
3490
|
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
3485
|
-
`{ status: "dialling", dialId }`, restating the
|
|
3491
|
+
`{ status: "dialling", dialId }`, restating the agent application's identity (`result.dialId`,
|
|
3486
3492
|
`result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
|
|
3487
3493
|
nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
|
|
3488
|
-
`dialId` the
|
|
3494
|
+
`dialId` the agent application sent for that reason.
|
|
3489
3495
|
|
|
3490
3496
|
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
3491
3497
|
there was one, the directory item it went to where there was one, and how the dial ended -- from a closed
|
|
3492
3498
|
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`, `cancelled`, `unexplained`.
|
|
3493
|
-
`answered` is stated rather than read off somebody appearing on the call, because
|
|
3499
|
+
`answered` is stated rather than read off somebody appearing on the call, because an agent application that infers
|
|
3494
3500
|
success cannot tell "reached" from "still ringing". `cancelled` is the dialler calling the dial off
|
|
3495
3501
|
before anyone answered -- the agent hanging up while a consult still rings -- which is not a
|
|
3496
3502
|
`no-answer` the destination never gave. `unexplained` is the switch dropping the call with no cause among
|
|
@@ -3501,14 +3507,14 @@ create dialog" in words and still name no cause a code would carry, so `unexplai
|
|
|
3501
3507
|
`reason` is a dial the switch described but did not classify, and a desk renders the words without
|
|
3502
3508
|
inventing the class.
|
|
3503
3509
|
|
|
3504
|
-
**`
|
|
3510
|
+
**`assignmentId` is the task the dial was placed on**, resolved from the dial's own identity and never from
|
|
3505
3511
|
whatever task the agent is looking at when the outcome arrives. That task may already have ended,
|
|
3506
3512
|
and the harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
3507
3513
|
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached, against the
|
|
3508
3514
|
call it belonged to and never against the next one.
|
|
3509
3515
|
|
|
3510
|
-
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which
|
|
3511
|
-
places one; an entry on a task's `onCall`; and a step in its `
|
|
3516
|
+
The stream knows a dial from three places: `TaskStream.dialled(dialId)`, which an agent application calls when it
|
|
3517
|
+
places one; an entry on a task's `onCall`; and a step in its `history`. Since a dialled entry
|
|
3512
3518
|
is listed from placement, `dialled()` is load-bearing only for a dial that never has an entry -- a
|
|
3513
3519
|
cold transfer, a connect-back, an idle dialpad call -- or whose entry has already left the room; an
|
|
3514
3520
|
outcome for a dial nobody placed and no task mentions is what `stream.dialOutcome.unknown` refuses.
|
|
@@ -3548,8 +3554,8 @@ const onCall: OnCall[] = [
|
|
|
3548
3554
|
|
|
3549
3555
|
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
3550
3556
|
neither carries a destination. `consulted` and `conferenced` were, so both carry the
|
|
3551
|
-
`destinationId` of the directory item and, where
|
|
3552
|
-
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so
|
|
3557
|
+
`destinationId` of the directory item and, where an agent application's dial brought them, its `dialId`; a person
|
|
3558
|
+
the platform added itself carries none. `held: true` is presence as claim. A snapshot establishes state, so an agent application that attaches
|
|
3553
3559
|
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
3554
3560
|
never saw.
|
|
3555
3561
|
|
|
@@ -3564,7 +3570,7 @@ task may remain open for wrap. Completion is a separate task action, not the cal
|
|
|
3564
3570
|
`previewEndsAt` and `atDeadline` are three instances of one shape, and there will be more: each
|
|
3565
3571
|
describes a state that is true now, and each is carried past the moment it stops being true by the
|
|
3566
3572
|
most natural implementation there is, building the next task by spreading the last one. The
|
|
3567
|
-
provider's own state looks right, the claim on the wire is stale, and only the
|
|
3573
|
+
provider's own state looks right, the claim on the wire is stale, and only the agent application sees it. An
|
|
3568
3574
|
adapter that builds each transition from what is true after it, rather than from what was true
|
|
3569
3575
|
before, needs none of these rules named. Two agents were
|
|
3570
3576
|
once shown on calls for the better part of an hour while callers queued, because the last message
|
|
@@ -3707,11 +3713,11 @@ thing as a break the platform forced on its own. Where a platform applies one au
|
|
|
3707
3713
|
executing a preference somebody configured, and that person is the owner of the action — `by` names
|
|
3708
3714
|
them, not the machinery that carried it out.
|
|
3709
3715
|
|
|
3710
|
-
Omni resolves the name to show with `
|
|
3716
|
+
Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
|
|
3711
3717
|
a display name.
|
|
3712
3718
|
|
|
3713
3719
|
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
3714
|
-
break in progress or about to be; beside `granted` or `awaiting-approval` the
|
|
3720
|
+
break in progress or about to be; beside `granted` or `awaiting-approval` the agent application would read a
|
|
3715
3721
|
request the agent never made and commit it (`break.forced.status`). Where the provider publishes
|
|
3716
3722
|
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
3717
3723
|
|
|
@@ -3724,18 +3730,18 @@ forced: {
|
|
|
3724
3730
|
}
|
|
3725
3731
|
```
|
|
3726
3732
|
|
|
3727
|
-
The
|
|
3728
|
-
Resume; the
|
|
3733
|
+
The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
|
|
3734
|
+
Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
|
|
3729
3735
|
work. An expected duration is a positive finite number of milliseconds, measured from actual
|
|
3730
3736
|
entry into `on-break`, excluding any `starting-after-task` wait. Omission means no expected
|
|
3731
|
-
duration was supplied. Neither duration expiry nor an overdue indication authorizes the
|
|
3737
|
+
duration was supplied. Neither duration expiry nor an overdue indication authorizes the agent application
|
|
3732
3738
|
or provider to end the break, restore availability, or route work to the agent.
|
|
3733
3739
|
|
|
3734
3740
|
**A lead lifting the restriction does not resume the agent.** On an applied
|
|
3735
3741
|
`end-forced-break`, the provider clears `BreakState.forced` while preserving the current
|
|
3736
3742
|
`on-break` or `starting-after-task` status and `activeReasonId`. This command cannot publish
|
|
3737
3743
|
`not-requested`, restore readiness, or start routing work. Ordinary progress from
|
|
3738
|
-
`starting-after-task` to `on-break` still happens when work finishes. The
|
|
3744
|
+
`starting-after-task` to `on-break` still happens when work finishes. The agent application follows the
|
|
3739
3745
|
published state and waits for the agent's explicit Resume, even after reconnect. Historical
|
|
3740
3746
|
attribution is not rewritten by clearing the current forced marker.
|
|
3741
3747
|
|
|
@@ -3745,7 +3751,7 @@ must correlate resumption to the authenticated agent's action; a state-only orde
|
|
|
3745
3751
|
cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
|
|
3746
3752
|
still applies, and work resumes only on confirmed provider state.
|
|
3747
3753
|
|
|
3748
|
-
|
|
3754
|
+
An agent application may display the expected duration. A countdown requires an evidenced actual start;
|
|
3749
3755
|
a received snapshot, replay or reconnect is not a new start and cannot restart the duration.
|
|
3750
3756
|
Without that evidence, show the duration without inventing a start or return time.
|
|
3751
3757
|
|
|
@@ -3784,43 +3790,43 @@ hold applied. A `held` result would repeat the discriminant that travelled with
|
|
|
3784
3790
|
|
|
3785
3791
|
### `setCapacity(capacity)`
|
|
3786
3792
|
|
|
3787
|
-
States how many tasks this provider may have
|
|
3793
|
+
States how many tasks this provider may have assigned to the agent **at once**.
|
|
3788
3794
|
|
|
3789
3795
|
`count` is an absolute ceiling, not an increment, a whole number of zero or more
|
|
3790
3796
|
(`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
|
|
3791
3797
|
stated when the agent is set up and restated only when it genuinely changes, which is a
|
|
3792
|
-
|
|
3798
|
+
local policy change rather than a task starting or ending -- with one exception below.
|
|
3793
3799
|
|
|
3794
|
-
**The provider counts its own outstanding tasks against it.**
|
|
3800
|
+
**The provider counts its own outstanding tasks against it.** Assign while you hold fewer than
|
|
3795
3801
|
`count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
|
|
3796
3802
|
room again and need no new signal to know it. Omni does not re-state capacity as tasks come and
|
|
3797
3803
|
go, and a provider that waits for it will stall.
|
|
3798
3804
|
|
|
3799
3805
|
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
3800
3806
|
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3801
|
-
providers, and the
|
|
3802
|
-
provider that has none of it for now is told **`count: 0`,
|
|
3803
|
-
the member as `reserved` on the team member list -- signed in here, capacity held by the
|
|
3807
|
+
providers, and the agent application divides their capacity among them rather than telling each the whole. A
|
|
3808
|
+
provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
|
|
3809
|
+
the member as `reserved` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
|
|
3804
3810
|
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3805
|
-
count as any other when the
|
|
3806
|
-
that follows work rather than
|
|
3811
|
+
count as any other when the agent application has capacity for this provider again. Zero is the one restatement
|
|
3812
|
+
that follows work rather than local policy.
|
|
3807
3813
|
|
|
3808
3814
|
**A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
|
|
3809
|
-
statement has no failure to report, and a `failed` arm gave four
|
|
3810
|
-
readings of which ceiling stood after it. A provider that cannot carry the count
|
|
3815
|
+
statement has no failure to report, and a `failed` arm gave four agent application-provider pairings four
|
|
3816
|
+
readings of which ceiling stood after it. A provider that cannot carry the count assigns within
|
|
3811
3817
|
what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
|
|
3812
3818
|
refusal.
|
|
3813
3819
|
|
|
3814
3820
|
Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
|
|
3815
3821
|
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
3816
|
-
it was ever told, or returns early on a small one -- cannot be told to take less work, and
|
|
3822
|
+
it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
|
|
3817
3823
|
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
3818
3824
|
both ways after the drive, two then one then nought, and an offer after a lower count is caught
|
|
3819
3825
|
against it (`stream.taskOffered.overCapacity`).
|
|
3820
3826
|
|
|
3821
|
-
**Capacity gates what the provider
|
|
3827
|
+
**Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
|
|
3822
3828
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
3823
|
-
it: the ceiling binds
|
|
3829
|
+
it: the ceiling binds assignment, not the agent's own hand.
|
|
3824
3830
|
|
|
3825
3831
|
### Runtime break prerequisite checks
|
|
3826
3832
|
|
|
@@ -3852,7 +3858,7 @@ These validators report violations without dispatching, rewriting state or provi
|
|
|
3852
3858
|
Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
|
|
3853
3859
|
Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
|
|
3854
3860
|
is not a grant, and no method response overwrites a newer state emission. Rejected promises
|
|
3855
|
-
remain unknown and recover by snapshot, not automatic retry. The
|
|
3861
|
+
remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
|
|
3856
3862
|
durable mutually exclusive commit/cancel decision and reconciliation obligations above remain
|
|
3857
3863
|
required; per-provider validation does not implement the multi-provider coordinator.
|
|
3858
3864
|
|
|
@@ -3979,7 +3985,7 @@ request and is ready to stop the agent when Omni asks. It does not reveal that o
|
|
|
3979
3985
|
or why Omni has not committed yet. While reporting `granted`, the provider remains available,
|
|
3980
3986
|
continues to honour Omni's current capacity, and continues offering work normally.
|
|
3981
3987
|
|
|
3982
|
-
Omni separately tracks the aggregate
|
|
3988
|
+
Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
|
|
3983
3989
|
`cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
|
|
3984
3990
|
outstanding and offers **Cancel break request**, but does not tell the agent that their break has
|
|
3985
3991
|
begun.
|
|
@@ -3996,7 +4002,7 @@ Omni coordinates one break attempt as follows:
|
|
|
3996
4002
|
from and is not asked: nothing is asked of it, the break proceeds without it, and when the
|
|
3997
4003
|
login is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
3998
4004
|
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
3999
|
-
available and work continues. `assertBreakAttemptProviders` holds
|
|
4005
|
+
available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
|
|
4000
4006
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
4001
4007
|
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
|
|
4002
4008
|
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
@@ -4128,7 +4134,7 @@ an absent `team` still means the login is not entitled to that contribution.
|
|
|
4128
4134
|
|
|
4129
4135
|
| `TeamMember` field | Contract |
|
|
4130
4136
|
| --- | --- |
|
|
4131
|
-
| `id` | Required `UserId`. A task carries no names and neither does a team member list: Omni resolves what to display with `
|
|
4137
|
+
| `id` | Required `UserId`. A task carries no names and neither does a team member list: Omni resolves what to display with `getUserDetails()`. |
|
|
4132
4138
|
| `availability` | Required. What the member is doing now. |
|
|
4133
4139
|
| `since` | Optional. When the current `availability` began — not when they signed in, and not when the team member list was read. |
|
|
4134
4140
|
| `break` | Present only while the member has an outstanding break request. See **A member waiting for a break**. |
|
|
@@ -4140,7 +4146,7 @@ Each availability value means one thing:
|
|
|
4140
4146
|
| `ready` | Signed in, able to take work, none assigned. |
|
|
4141
4147
|
| `on-task` | Working on at least one task. It says nothing about how many, and nothing about whether more will fit. |
|
|
4142
4148
|
| `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
|
|
4143
|
-
| `reserved` | Signed in here, and the
|
|
4149
|
+
| `reserved` | 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**. |
|
|
4144
4150
|
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
4145
4151
|
|
|
4146
4152
|
**Always publish the complete team member list, never a change to it.** Team presence typically reaches an
|
|
@@ -4183,7 +4189,7 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
4183
4189
|
|
|
4184
4190
|
`memberId` is this provider's own identifier for the member, as published on its team member list. It is
|
|
4185
4191
|
never an identifier from another provider, and Omni does not translate between them; names come
|
|
4186
|
-
from `
|
|
4192
|
+
from `getUserDetails()`.
|
|
4187
4193
|
|
|
4188
4194
|
`requests-suspended` means requests are **rejected outright** rather than left pending — nobody is coming to
|
|
4189
4195
|
approve them. A provider that suspends break requests must also publish `canRequestBreak: false` to the team's
|
|
@@ -4209,23 +4215,24 @@ Required when the login declares `capabilities.team.leadAssistControl`, and gate
|
|
|
4209
4215
|
|
|
4210
4216
|
```ts
|
|
4211
4217
|
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
4212
|
-
execute({
|
|
4218
|
+
execute({ assignmentId: "alloc-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
4213
4219
|
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
4214
4220
|
|
|
4215
4221
|
// 2. Every lead entitled to it sees the request on their team member list.
|
|
4216
|
-
// team-updated: requests: [{ id: "req-7", memberId: "A-1",
|
|
4222
|
+
// team-updated: requests: [{ id: "req-7", memberId: "A-1", assignmentId: "alloc-42", note, since }]
|
|
4217
4223
|
|
|
4218
4224
|
// 3. A lead joins, or declines.
|
|
4219
4225
|
executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
|
|
4220
4226
|
executeTeamLeadAssist({ command: { type: "decline", requestId: "req-7", reason: "In a call" } })
|
|
4221
4227
|
```
|
|
4222
4228
|
|
|
4223
|
-
**On `join` the provider bridges three parties and the lead is on a task of their own**,
|
|
4224
|
-
|
|
4225
|
-
-- the way a call an agent placed themselves arrives -- and carrying `assisting
|
|
4229
|
+
**On `join` the provider bridges three parties and the lead is on a task of their own**, an
|
|
4230
|
+
assignment of its own, arriving on the lead's connection as `task-offered` with `automatic`
|
|
4231
|
+
-- the way a call an agent placed themselves arrives -- and carrying `assisting`, which names the
|
|
4232
|
+
member and the member's assignment. The agent's task
|
|
4226
4233
|
moves to `leadAssist: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
|
|
4227
4234
|
trigger it; but from then on it is an outstanding task the provider counts against the lead's
|
|
4228
|
-
stated ceiling like any other, nothing more is
|
|
4235
|
+
stated ceiling like any other, nothing more is assigned to the lead while it stands, and a
|
|
4229
4236
|
provider whose lead is already at the ceiling answers the join `failed`.
|
|
4230
4237
|
|
|
4231
4238
|
**A lead assists one call at a time.** A `join` from a lead already on a call -- their own or one
|
|
@@ -4272,7 +4279,7 @@ const leadAssistCapable = {
|
|
|
4272
4279
|
```
|
|
4273
4280
|
|
|
4274
4281
|
Lead and member alike are `UserId`s of this provider, so an adapter publishing them implements
|
|
4275
|
-
`
|
|
4282
|
+
`getUserDetails()`; names never travel on a task or a team member list.
|
|
4276
4283
|
|
|
4277
4284
|
### Listening to a call
|
|
4278
4285
|
|
|
@@ -4333,8 +4340,8 @@ declaration requires `executeTeamListen`, gated exactly as `executeTeamLeadAssis
|
|
|
4333
4340
|
executeTeamListen({ command: { type: "listen", memberId: "A-1" } })
|
|
4334
4341
|
|
|
4335
4342
|
// 2. The lead's own task arrives -- task-offered with `automatic`, as a joined call does -- and
|
|
4336
|
-
// carries `listening`; the
|
|
4337
|
-
// listening: { memberId: "A-1",
|
|
4343
|
+
// carries `listening`; the assignment is the lead's own, the member's is named inside it.
|
|
4344
|
+
// listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "listen", since }
|
|
4338
4345
|
|
|
4339
4346
|
// 3. The lead changes how they are heard; the provider restates the task with the new mode.
|
|
4340
4347
|
executeTeamListen({ command: { type: "coach" } })
|
|
@@ -4372,7 +4379,7 @@ const listeningLead = {
|
|
|
4372
4379
|
channel: "voice",
|
|
4373
4380
|
capabilities: {},
|
|
4374
4381
|
phase: "in-progress",
|
|
4375
|
-
listening: { memberId: "A-1",
|
|
4382
|
+
listening: { memberId: "A-1", assignmentId: "alloc-42", mode: "coach", since: "2026-08-21T09:04:00Z" },
|
|
4376
4383
|
} satisfies Pick<Task<"voice">, "channel" | "capabilities" | "phase" | "listening">;
|
|
4377
4384
|
```
|
|
4378
4385
|
|
|
@@ -4413,25 +4420,25 @@ decision or it is not.
|
|
|
4413
4420
|
## Real-time media
|
|
4414
4421
|
|
|
4415
4422
|
Every voice provider has media: `channel: "voice"` says audio exists. Where the agent hears it
|
|
4416
|
-
depends on the `phone` the
|
|
4423
|
+
depends on the `phone` the agent application selected at authentication and connect from the manifest's
|
|
4417
4424
|
`phones`. On a softphone, audio lands in Omni; on a desk phone, it lands on the handset and Omni
|
|
4418
4425
|
opens no audio. See **How the agent hears the call**. Media transitions are voice-only;
|
|
4419
4426
|
chat and email publish none (`event.media.channel`, `stream.taskMedia.channel`).
|
|
4420
4427
|
|
|
4421
|
-
The provider owns signalling and the platform's endpoint configuration. The
|
|
4428
|
+
The provider owns signalling and the platform's endpoint configuration. The agent application selects the
|
|
4422
4429
|
declared phone mode; it does not enumerate or reconfigure the platform's devices. The selected
|
|
4423
4430
|
mode stays with the login rather than being inferred from a snapshot or a device failure.
|
|
4424
4431
|
|
|
4425
4432
|
Nor does the audio ever stand in for the task: a task's presence and
|
|
4426
4433
|
phase follow the provider's reports about the work, and the media — attaching, moving through a
|
|
4427
4434
|
hold, a consult, a conference or a transfer, and ending — is transient beside it. See **A task is
|
|
4428
|
-
never its audio** under **Task
|
|
4435
|
+
never its audio** under **Task assignment lifecycle**.
|
|
4429
4436
|
|
|
4430
|
-
### The
|
|
4437
|
+
### The agent application reports, the adapter decides
|
|
4431
4438
|
|
|
4432
4439
|
The adapter runs inside Omni and sees nothing of the station for itself: the devices, the
|
|
4433
|
-
permissions, the audio element and the network are the
|
|
4434
|
-
the
|
|
4440
|
+
permissions, the audio element and the network are the agent application's, and only the agent application can tell. So
|
|
4441
|
+
the agent application reports, and the adapter asks. **An adapter consults `ConnectContext.host` before it
|
|
4435
4442
|
declares the agent ready to its platform, and again whenever the report changes**, and it decides
|
|
4436
4443
|
what any of it means and what to relay upstream — go not-ready, refuse calls, carry on because the
|
|
4437
4444
|
platform's audio lands elsewhere. Omni facilitates and does not take responsibility: it captures
|
|
@@ -4439,29 +4446,29 @@ the microphone once as the voice connection opens, so the permission prompt land
|
|
|
4439
4446
|
is signing in rather than over a contact, prompts, retries on the agent's request, tells the agent
|
|
4440
4447
|
what failed, and reports. It never decides for the adapter what a missing microphone means.
|
|
4441
4448
|
|
|
4442
|
-
**The
|
|
4443
|
-
Two of this contract's obligations fall on the
|
|
4449
|
+
**The agent application also guarantees, and a promise the provider cannot see is not one it can rely on.**
|
|
4450
|
+
Two of this contract's obligations fall on the agent application rather than the provider — honouring a task
|
|
4444
4451
|
browser's `urlVisibility`, and taking a `consent` offer only on the person's own press — and more
|
|
4445
4452
|
than one desk speaks this contract: a browser and a native application differ in what they can
|
|
4446
4453
|
reach, and a provider never branches on which it is talking to, only on what it has declared —
|
|
4447
|
-
here, and in `host.mute` (see **The station is the
|
|
4448
|
-
connected
|
|
4454
|
+
here, and in `host.mute` (see **The station is the agent application's**). So `ConnectContext.host.guarantees` says which promises the
|
|
4455
|
+
connected agent application makes, declared once per connection, presence being the guarantee exactly as it is
|
|
4449
4456
|
the permission everywhere else:
|
|
4450
4457
|
|
|
4451
4458
|
| Guarantee | Contract |
|
|
4452
4459
|
| --- | --- |
|
|
4453
|
-
| `browserUrlVisibility` | Every task browser's `urlVisibility` is honoured in this
|
|
4460
|
+
| `browserUrlVisibility` | Every task browser's `urlVisibility` is honoured in this agent application's chrome, tab by tab. A provider that would send a caller's number in a URL checks this first and tokenises where the promise is absent. |
|
|
4454
4461
|
| `personConsent` | A `consent` offer is accepted only by the person's own explicit act, never on their behalf. A provider whose work may only be taken by a human checks this first and does not offer it where the promise is absent. |
|
|
4455
4462
|
|
|
4456
|
-
A guarantee the
|
|
4463
|
+
A guarantee the agent application does not make is an absent key, never `false` — `validateHostGuarantees`
|
|
4457
4464
|
refuses a false one, as it refuses a name this contract does not list.
|
|
4458
4465
|
|
|
4459
4466
|
| Field | Contract |
|
|
4460
4467
|
| --- | --- |
|
|
4461
|
-
| `online` | Whether the
|
|
4468
|
+
| `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. |
|
|
4462
4469
|
| `audio` | Present on a voice connection, absent where there is no audio. |
|
|
4463
|
-
| `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openMedia` receives — and `flowing`, false while no audio moves through it, when `mutedBy` says who stopped it: `host`, the
|
|
4464
|
-
| `audio.output` | `available`, with `flowing` where the
|
|
4470
|
+
| `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openMedia` 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. |
|
|
4471
|
+
| `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. |
|
|
4465
4472
|
|
|
4466
4473
|
Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
|
|
4467
4474
|
a network gone — and it publishes a state, not a flicker: a change that resolves within moments is
|
|
@@ -4469,13 +4476,13 @@ not reported, so an adapter may act on what it reads without debouncing it again
|
|
|
4469
4476
|
report is a task fact or a capacity fact; it is what the station can do, and the adapter's platform
|
|
4470
4477
|
is the one that knows whether an agent without a microphone, or without a speaker, can work.
|
|
4471
4478
|
|
|
4472
|
-
The
|
|
4479
|
+
The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
|
|
4473
4480
|
publishing a state and not a flicker — are Omni's tests' to hold. `exerciseAdapter` holds the
|
|
4474
|
-
other side: it validates the shape of whatever
|
|
4481
|
+
other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
|
|
4475
4482
|
a softphone login and none elsewhere -- not on a desk phone, not off voice
|
|
4476
4483
|
(`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
|
|
4477
4484
|
/ `.unexpected`, `host.mute` for a third word) -- and refuses a voice adapter that never asked the
|
|
4478
|
-
|
|
4485
|
+
agent application anything (`connection.host.consulted`), and one that subscribed to nothing will never hear of a
|
|
4479
4486
|
change (`connection.host.subscribed`): the obligation has two halves, and each is held.
|
|
4480
4487
|
|
|
4481
4488
|
### Capacity around setup
|
|
@@ -4485,9 +4492,9 @@ as reachability opens a window where it believes the agent is available and the
|
|
|
4485
4492
|
set up — the adapter's own registration incomplete, its credentials not yet renewed.
|
|
4486
4493
|
|
|
4487
4494
|
Nothing closes that window, because nothing opens it: **Omni states no capacity until the
|
|
4488
|
-
connection is established**, and **Work is pulled, never pushed** makes an
|
|
4489
|
-
stated a violation. Capacity does not wait on the microphone: whether an agent without
|
|
4490
|
-
can take calls is the platform's question, answered by the adapter from the
|
|
4495
|
+
connection is established**, and **Work is pulled, never pushed** makes an assignment with none
|
|
4496
|
+
stated a violation. Capacity does not wait on the microphone: whether an agent without agent application audio
|
|
4497
|
+
can take calls is the platform's question, answered by the adapter from the agent application's report, not by
|
|
4491
4498
|
Omni withholding capacity for every platform alike.
|
|
4492
4499
|
|
|
4493
4500
|
Capacity follows **automatically** once the connection is established; the agent does not press
|
|
@@ -4495,12 +4502,12 @@ anything to become available.
|
|
|
4495
4502
|
|
|
4496
4503
|
| Situation | What Omni sends |
|
|
4497
4504
|
| --- | --- |
|
|
4498
|
-
| Connecting | Nothing. No capacity has been stated, so nothing may be
|
|
4499
|
-
| Connected and idle | `setCapacity({ count: n })`, with the
|
|
4505
|
+
| Connecting | Nothing. No capacity has been stated, so nothing may be assigned. |
|
|
4506
|
+
| Connected and idle | `setCapacity({ count: n })`, with the agent application's report already available to consult. |
|
|
4500
4507
|
| A task starts or ends | Nothing. The provider counts its own against the ceiling. |
|
|
4501
4508
|
| The agent's provisioned capacity changes | `setCapacity({ count: n })` |
|
|
4502
4509
|
| Agent asks for a break | `requestBreak`. Capacity is unchanged and work continues. |
|
|
4503
|
-
| Omni commits a break | `commitBreak`. The break stops
|
|
4510
|
+
| Omni commits a break | `commitBreak`. The break stops assignment, not the ceiling. |
|
|
4504
4511
|
| Agent returns from break | `endBreak` |
|
|
4505
4512
|
|
|
4506
4513
|
**A break is not a capacity of zero, and neither is the reverse.** Capacity says how much of this
|
|
@@ -4512,57 +4519,57 @@ a return.
|
|
|
4512
4519
|
|
|
4513
4520
|
### How the agent hears the call
|
|
4514
4521
|
|
|
4515
|
-
A voice platform can put an agent on a **softphone**, where the call's audio lands in the
|
|
4522
|
+
A voice platform can put an agent on a **softphone**, where the call's audio lands in the agent application
|
|
4516
4523
|
and Omni owns the microphone and the playback, or on a **desk phone**, a handset on the platform's
|
|
4517
|
-
switch that the platform rings, where the
|
|
4524
|
+
switch that the platform rings, where the agent application owns no audio and only shows the call. Which of
|
|
4518
4525
|
these a platform can do is a fact about the platform, and which one a login is on is a choice the
|
|
4519
|
-
|
|
4526
|
+
agent application makes for that agent -- so the manifest declares and the agent application chooses.
|
|
4520
4527
|
|
|
4521
4528
|
```ts
|
|
4522
4529
|
// Manifest: what the platform can do
|
|
4523
4530
|
phones: ["softphone", "deskPhone"]
|
|
4524
|
-
// The
|
|
4531
|
+
// The agent application, at authentication and again at connect: which one this login is on
|
|
4525
4532
|
{ phone: "softphone" }
|
|
4526
4533
|
```
|
|
4527
4534
|
|
|
4528
4535
|
A voice manifest lists its `phones` and any other channel lists none (`manifest.phones.required`,
|
|
4529
|
-
`manifest.phones.channel`). The
|
|
4536
|
+
`manifest.phones.channel`). The agent application's `phone` is required on a voice login, absent on any other,
|
|
4530
4537
|
and one the manifest listed (`context.phone.required`, `.unexpected`, `.unsupported`). Everything
|
|
4531
|
-
about audio then follows the phone rather than the channel: on a softphone the
|
|
4538
|
+
about audio then follows the phone rather than the channel: on a softphone the agent application reports its
|
|
4532
4539
|
audio, the adapter implements `openMedia`, and `task-media-started` is the word to open it; on a
|
|
4533
|
-
desk phone the
|
|
4540
|
+
desk phone the agent application reports no audio, opens nothing, and `task-media-started` still marks the
|
|
4534
4541
|
moment the call is live so the desk shows it, with the sound on the handset. A platform that lists
|
|
4535
4542
|
`deskPhone` alone never implements `openMedia`; one that lists `softphone` always does.
|
|
4536
4543
|
|
|
4537
|
-
**The mode comes from the
|
|
4544
|
+
**The mode comes from the agent application; the status comes from the provider.** The agent application knows which kind
|
|
4538
4545
|
of station the agent signed in at, because the person chose it, and nothing else can know that.
|
|
4539
4546
|
The provider knows whether that station can carry a call right now -- whether a handset is
|
|
4540
|
-
registered -- which
|
|
4547
|
+
registered -- which an agent application in a browser cannot see. So the agent application's `phone` selects the branch, and
|
|
4541
4548
|
the provider's own knowledge of the device decides readiness within it. A provider never overrides
|
|
4542
|
-
the
|
|
4549
|
+
the agent application's declaration from its device record: treating a desk-phone login as a softphone because
|
|
4543
4550
|
the handset is momentarily unregistered, and demanding a microphone of it, is the reading this
|
|
4544
4551
|
sentence exists to refuse. What the provider does when the handset is not registered is what it
|
|
4545
4552
|
does for any station that cannot take a call -- hold the agent not-ready and say why.
|
|
4546
4553
|
|
|
4547
|
-
**On a desk-phone login the
|
|
4548
|
-
no audio to attach;
|
|
4554
|
+
**On a desk-phone login the agent application never calls `openMedia`.** There is no stream to hand over and
|
|
4555
|
+
no audio to attach; an agent application that calls it anyway is in error, and an adapter that receives the call
|
|
4549
4556
|
answers `unavailable` with a non-retryable failure, since waiting changes nothing about a station
|
|
4550
4557
|
that is a telephone. The harness requires no `openMedia` of such an adapter and never calls it.
|
|
4551
4558
|
|
|
4552
4559
|
**Which handset a desk-phone login rings is the platform's configuration for that agent**, and
|
|
4553
|
-
this wire never asks the agent for it:
|
|
4560
|
+
this wire never asks the agent for it: an agent application declares `phone` and nothing more. What a platform
|
|
4554
4561
|
asks on surfaces of its own is its own decision.
|
|
4555
4562
|
|
|
4556
4563
|
**A declared phone the platform does not permit for this agent is a login that cannot be
|
|
4557
4564
|
established.** The manifest says what the platform can do; the agent's record, kept by an
|
|
4558
|
-
administrator, says what this agent is configured for, and the two can disagree with the
|
|
4565
|
+
administrator, says what this agent is configured for, and the two can disagree with the agent application's
|
|
4559
4566
|
declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
|
|
4560
4567
|
declaration on a switch that allows one registration per endpoint would evict the handset: the
|
|
4561
4568
|
agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
|
|
4562
|
-
refuses the login at authentication, with `omni.phone-not-permitted` and a message the
|
|
4569
|
+
refuses the login at authentication, with `omni.phone-not-permitted` and a message the agent application shows
|
|
4563
4570
|
-- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
|
|
4564
|
-
|
|
4565
|
-
never retryable: trying again does not reconfigure the agent, and
|
|
4571
|
+
agent application. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
|
|
4572
|
+
never retryable: trying again does not reconfigure the agent, and an agent application validating the answer
|
|
4566
4573
|
(`validateAuthenticationResult`) refuses a phone refusal marked otherwise
|
|
4567
4574
|
(`authentication.failure.phone.retryable`). And **a
|
|
4568
4575
|
provider never makes a declared phone true by changing the platform's configuration**: the record
|
|
@@ -4574,16 +4581,16 @@ is the administrator's, and a login is not a request to reconfigure an agent.
|
|
|
4574
4581
|
`softphone` implements it, because on a softphone the call's audio lands in Omni:
|
|
4575
4582
|
|
|
4576
4583
|
```ts
|
|
4577
|
-
openMedia({
|
|
4584
|
+
openMedia({ assignmentId, localAudio }): Promise<OpenMediaResult>
|
|
4578
4585
|
// { status: "opened", session } | { status: "unavailable", failure }
|
|
4579
4586
|
```
|
|
4580
4587
|
|
|
4581
4588
|
The adapter speaks whatever its platform speaks — SIP over WebSocket, a vendor SDK, plain
|
|
4582
4589
|
WebRTC — and **none of that appears in this contract**. Registration, signalling, credential
|
|
4583
4590
|
renewal and reconnect are the adapter's, exactly as its authentication and transport already
|
|
4584
|
-
are. Omni owns what belongs to the
|
|
4591
|
+
are. Omni owns what belongs to the agent application: the microphone, the output element, mute, and when a
|
|
4585
4592
|
session ends — owning the microphone meaning capturing it, prompting, retrying and reporting how
|
|
4586
|
-
it stands, never deciding for the adapter what a missing one means (see **The
|
|
4593
|
+
it stands, never deciding for the adapter what a missing one means (see **The agent application reports, the
|
|
4587
4594
|
adapter decides**).
|
|
4588
4595
|
|
|
4589
4596
|
| Member | Contract |
|
|
@@ -4592,9 +4599,9 @@ adapter decides**).
|
|
|
4592
4599
|
| `setMuted(muted)` | Mutes the agent's microphone on this session. |
|
|
4593
4600
|
| `close()` | Releases the session. Omni calls it when the task ends. |
|
|
4594
4601
|
|
|
4595
|
-
`localAudio` is the agent's microphone as Omni captured it, the same stream the
|
|
4602
|
+
`localAudio` is the agent's microphone as Omni captured it, the same stream the agent application's report
|
|
4596
4603
|
carries as `audio.input.localAudio`, and absent while that input is `unavailable`. A provider that
|
|
4597
|
-
bridges audio without
|
|
4604
|
+
bridges audio without an application-side input may ignore it; one that needs it and finds it absent
|
|
4598
4605
|
answers `unavailable` with a failure Omni shows the agent.
|
|
4599
4606
|
|
|
4600
4607
|
**When to ask is the provider's word, not Omni's guess.** On a softphone login, Omni opens media on `task-media-started`,
|
|
@@ -4614,7 +4621,7 @@ Command names follow the channel's operational vocabulary, and each channel's co
|
|
|
4614
4621
|
discriminated by `type` — the same discriminant `executeTeamBreak` and `custom` already use. The
|
|
4615
4622
|
unions are declared under **Shapes**.
|
|
4616
4623
|
|
|
4617
|
-
`
|
|
4624
|
+
`assignmentId` is not repeated on the command. It travels on the `TaskCommandRequest` around it.
|
|
4618
4625
|
|
|
4619
4626
|
**A toggle carries the state it wants, not a flip.** Inverting whatever is found cannot converge
|
|
4620
4627
|
with a stale view: a flip against a state the provider has already changed turns something on and
|
|
@@ -4632,87 +4639,87 @@ command asks the provider to **perform** something: `hold`, `transfer`, `confere
|
|
|
4632
4639
|
Provider-targeted `recording` commands and the rest act on the platform's own call leg, its bridge, or its record of the
|
|
4633
4640
|
task. Nothing has happened until the provider applies them, and `failed` means nothing happened.
|
|
4634
4641
|
|
|
4635
|
-
**The provider performs every action; the
|
|
4636
|
-
an affordance, never the enforcement: the
|
|
4642
|
+
**The provider performs every action; the agent application only offers it.** A control drawn on the agent application is
|
|
4643
|
+
an affordance, never the enforcement: the agent application asks, and the provider does or declines. A provider
|
|
4637
4644
|
answers `failed` for a command whose capability it did not publish, and that is the provider
|
|
4638
|
-
honouring its own declaration, not the
|
|
4645
|
+
honouring its own declaration, not the agent application deciding policy -- an agent application that refuses a command on
|
|
4639
4646
|
its own reading of a queue's flag has decided something that was never its to decide. What a
|
|
4640
4647
|
command means on the platform is the provider's to work out: `end-call` disconnects the caller channel, and
|
|
4641
|
-
which legs on which bridge that touches is a fact about the switch, never a choice the
|
|
4642
|
-
The one thing the
|
|
4648
|
+
which legs on which bridge that touches is a fact about the switch, never a choice the agent application makes.
|
|
4649
|
+
The one thing the agent application performs physically is the microphone, and that is not a command at all.
|
|
4643
4650
|
|
|
4644
|
-
### The station is the
|
|
4651
|
+
### The station is the agent application's
|
|
4645
4652
|
|
|
4646
4653
|
The microphone, the speaker, the headset and its buttons are the station's, and the station is the
|
|
4647
|
-
|
|
4654
|
+
agent application's. So mute is not a capability a provider declares, not a control a queue allows or a team
|
|
4648
4655
|
locks, not a preference the person keeps with the provider, and not a command: nothing about the
|
|
4649
|
-
station crosses to the provider except two things -- the station's condition, in the
|
|
4650
|
-
and the record of when the agent could not be heard, through `recordStep`.
|
|
4651
|
-
every voice task with media open, under its own
|
|
4656
|
+
station crosses to the provider except two things -- the station's condition, in the agent application report,
|
|
4657
|
+
and the record of when the agent could not be heard, through `recordStep`. An agent application offers Mute on
|
|
4658
|
+
every voice task with media open, under its own local policy, in `in-progress` and `paused`
|
|
4652
4659
|
alone, and performs it through `VoiceMediaSession.setMuted()`. A task carrying
|
|
4653
4660
|
`capabilities.mute` is refused (`task.capability.unknown`), and so is a `mute` command
|
|
4654
4661
|
(`command.type`), a `mute` preference (`preference.id`) and a `mute` policy (`team.policy.key`).
|
|
4655
4662
|
|
|
4656
|
-
**Two mutes, and the
|
|
4657
|
-
|
|
4658
|
-
changes, and every softphone
|
|
4663
|
+
**Two mutes, and the agent application states which its Mute performs.** A *stream* mute stops the audio the
|
|
4664
|
+
agent application sends: the microphone keeps capturing, the party hears silence, nothing else on the machine
|
|
4665
|
+
changes, and every softphone agent application can do it. A *station* mute silences the microphone itself at
|
|
4659
4666
|
the operating system: every application on the machine goes quiet, the system shows it, and a
|
|
4660
|
-
headset that follows the system follows it. Only a native
|
|
4661
|
-
this
|
|
4667
|
+
headset that follows the system follows it. Only a native agent application can do it. `Host.mute` says which
|
|
4668
|
+
this agent application does -- `stream` or `station` -- stated on a softphone login, where the agent application holds the
|
|
4662
4669
|
microphone, and absent on a desk phone and off voice (`host.mute.required` / `.unexpected`). A
|
|
4663
|
-
browser says `stream`. A native
|
|
4664
|
-
application the
|
|
4670
|
+
browser says `stream`. A native agent application says whichever it does. Nothing on the wire says what kind of
|
|
4671
|
+
application the agent application is; a provider reads only what the agent application declared.
|
|
4665
4672
|
|
|
4666
4673
|
**A silenced device says who silenced it.** `audio.input.flowing: false` and
|
|
4667
|
-
`audio.output.flowing: false` carry `mutedBy`: `host` when the
|
|
4668
|
-
only ever true of
|
|
4669
|
-
flowing -- and `station` when a slider on the headset or the operating system did, which the
|
|
4670
|
-
observed and did not do. Without the word, a station-muting
|
|
4674
|
+
`audio.output.flowing: false` carry `mutedBy`: `host` when the agent application's own Mute did it -- which is
|
|
4675
|
+
only ever true of an agent application whose mute is `station`, since a stream mute leaves the microphone
|
|
4676
|
+
flowing -- and `station` when a slider on the headset or the operating system did, which the agent application
|
|
4677
|
+
observed and did not do. Without the word, a station-muting agent application's own Mute would read as a fault
|
|
4671
4678
|
and an adapter would take the agent out of ready in the middle of a call. A browser reads a station
|
|
4672
|
-
mute through the capture track's muted state, read-only, and cannot lift it; a native
|
|
4679
|
+
mute through the capture track's muted state, read-only, and cannot lift it; a native agent application reads
|
|
4673
4680
|
the endpoint and can clear an operating-system mute. A hardware slider is nobody's to clear.
|
|
4674
4681
|
|
|
4675
4682
|
**The record carries the same word.** A `muted` interaction leg is a period the agent could not be
|
|
4676
|
-
heard, and the record exists so that period is not a hole. The
|
|
4683
|
+
heard, and the record exists so that period is not a hole. The agent application reports every such period
|
|
4677
4684
|
through `recordStep` -- its own Mute, and a station mute it observed during a call -- with
|
|
4678
4685
|
`mutedBy` saying whose the silence was, and the provider writes the word into the entry
|
|
4679
|
-
(`task.
|
|
4686
|
+
(`task.history.mutedBy`). The report names no agent because the agent application has exactly one, and
|
|
4680
4687
|
the provider knows who that is: it attributes the leg to the login's agent in `by`, a fact it
|
|
4681
|
-
holds and not an inference, for a station mute as much as for the
|
|
4682
|
-
`muted` entry is refused (`task.
|
|
4688
|
+
holds and not an inference, for a station mute as much as for the agent application's own. An unattributed
|
|
4689
|
+
`muted` entry is refused (`task.history.muted.by`); a shared handset's unattributed hold
|
|
4683
4690
|
is a different claim, and stands. A supervisor reading the record then sees "the agent muted for
|
|
4684
4691
|
forty seconds" and "the agent's headset was muted for forty seconds" as the different things they
|
|
4685
|
-
are. See **The
|
|
4692
|
+
are. See **The agent application records what it performs**.
|
|
4686
4693
|
|
|
4687
4694
|
**Mute has a lifecycle, and none of it is inferred.** A call that starts while the station is
|
|
4688
4695
|
already muted begins a `muted` leg the moment its media starts, `mutedBy: "station"`. Media that
|
|
4689
|
-
ends while any leg is open ends the leg at that instant, as every
|
|
4690
|
-
|
|
4696
|
+
ends while any leg is open ends the leg at that instant, as every agent application-performed leg ends. And the
|
|
4697
|
+
agent application's own Mute starts off on every call, which is every `task-media-started` -- a connect-back
|
|
4691
4698
|
opens a second call on the same task with no offer -- so an agent is never muted by the call before.
|
|
4692
4699
|
|
|
4693
|
-
**Every press on a headset is the agent's own press**, performed the
|
|
4694
|
-
follow the
|
|
4700
|
+
**Every press on a headset is the agent's own press**, performed the agent application's way, and the lights
|
|
4701
|
+
follow the agent application's state. The `personConsent` guarantee is honoured by a hook-switch press exactly
|
|
4695
4702
|
as by a click. What a call-control headset sends, and what becomes of it:
|
|
4696
4703
|
|
|
4697
|
-
| Input | Where it comes from | What the
|
|
4704
|
+
| Input | Where it comes from | What the agent application does | What crosses to the provider |
|
|
4698
4705
|
| --- | --- | --- | --- |
|
|
4699
|
-
| Mute button | The headset, on the HID Telephony usage page: a Phone Mute report on the press, a Mute LED the
|
|
4700
|
-
| Hook switch | The headset, HID Telephony | A press of the
|
|
4706
|
+
| Mute button | The headset, on the HID Telephony usage page: a Phone Mute report on the press, a Mute LED the agent application writes back | A press of the agent application's Mute, performed the agent application's way; the LED follows the agent application's state, so button, light and control are one state with one owner | The `muted` leg, `mutedBy: "host"` |
|
|
4707
|
+
| Hook switch | The headset, HID Telephony | A press of the agent application's Answer or End call, only where the task is in a phase that has one; the off-hook and ring lights follow the task | The existing `answer` and `end-call` commands |
|
|
4701
4708
|
| Flash, redial, speed dial | Older headsets and desk-phone style devices | Flash is Hold and Resume where the task offers `hold`; redial and speed dial map to nothing on a desk with no dial pad on a transfer, and are ignored | Nothing new |
|
|
4702
|
-
| Volume up and down | The headset's own amplifier, or the operating system through the consumer-control keys | Nothing: the device and the system handle it | Nothing, except that a speaker at nought is `audio.output.flowing: false` where the
|
|
4709
|
+
| Volume up and down | The headset's own amplifier, or the operating system through the consumer-control keys | Nothing: the device and the system handle it | Nothing, except that a speaker at nought is `audio.output.flowing: false` where the agent application can know it |
|
|
4703
4710
|
| Microphone gain and level | The operating system, the headset's own boost | A level meter, so the agent can see they are heard | Nothing: a level is a flicker, not a state |
|
|
4704
|
-
| Hardware or operating-system mute | A slider on the headset, the system's input mute | Observes it, publishes `flowing: false, mutedBy: "station"`, records the leg during a call, and tells the agent which it is where it knows -- headset or system -- and what to do;
|
|
4711
|
+
| Hardware or operating-system mute | A slider on the headset, the system's input mute | Observes it, publishes `flowing: false, mutedBy: "station"`, records the leg during a call, and tells the agent which it is where it knows -- headset or system -- and what to do; an agent application whose mute is `station` clears a system mute itself, a browser can only say so | The report and the leg |
|
|
4705
4712
|
| Device changed, unplugged, permission revoked | The operating system | Re-capture, prompt, report | Already `audio.input` and `audio.output` with `lost`, `denied`, `no-device` |
|
|
4706
4713
|
|
|
4707
|
-
**What the
|
|
4708
|
-
what to do, with a button that clears a system mute only on
|
|
4714
|
+
**What the agent application shows.** Beside Mute, a standing line while the station is muted, saying which and
|
|
4715
|
+
what to do, with a button that clears a system mute only on an agent application whose mute is `station`. The
|
|
4709
4716
|
Mute control does not pretend: while the station is muted it shows the agent cannot be heard and
|
|
4710
|
-
that pressing it will not change that, and a press still toggles the
|
|
4717
|
+
that pressing it will not change that, and a press still toggles the agent application's own mute, so the two
|
|
4711
4718
|
states stay separate and the agent is not unmuted by surprise when the slider moves back. Before a
|
|
4712
4719
|
call, the same line in the ready state, so an agent does not learn of it from a silent customer;
|
|
4713
4720
|
whether work is held back is the adapter's decision from the report, as it is for every station
|
|
4714
4721
|
fact. Where a provider's own platform holds a mute of its own -- a bridge that silences a leg --
|
|
4715
|
-
that is a fact about the switch, reported as the provider sees fit, and never the
|
|
4722
|
+
that is a fact about the switch, reported as the provider sees fit, and never the agent application's Mute.
|
|
4716
4723
|
|
|
4717
4724
|
### Which commands need a capability
|
|
4718
4725
|
|
|
@@ -4725,7 +4732,7 @@ declared:
|
|
|
4725
4732
|
| `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
|
|
4726
4733
|
| `end-call` | The `endCall` capability. |
|
|
4727
4734
|
| `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`. |
|
|
4728
|
-
| `decline` | The `decline` capability on any channel, **and** Omni
|
|
4735
|
+
| `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
|
|
4729
4736
|
| `call` | The `preview` phase. A record put in front of an agent is there to be called, so the phase is the gate and there is no capability. It is a dial, with a `dialId` and a `dial-outcome`. |
|
|
4730
4737
|
| `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`). |
|
|
4731
4738
|
| `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. |
|
|
@@ -4751,10 +4758,10 @@ The commands with a phase of their own -- `answer`, `accept` and `decline` in `p
|
|
|
4751
4758
|
|
|
4752
4759
|
`validateTaskCommand(command, task)` holds a command to this table at runtime, both ways: the
|
|
4753
4760
|
capability it needs, the phase it belongs to, and the state that has to stand. The task it wants
|
|
4754
|
-
is the one the provider published, not
|
|
4761
|
+
is the one the provider published, not an agent application's own mapping of it: an agent application that keeps only its
|
|
4755
4762
|
mapped shape has nothing honest to pass, and then checks shape alone, which is still worth doing
|
|
4756
4763
|
-- it names a command the wire never had -- but is not the table. Keeping the published task
|
|
4757
|
-
beside the mapped one is what the full check costs
|
|
4764
|
+
beside the mapped one is what the full check costs an agent application; an adapter has it for free. The
|
|
4758
4765
|
validator holds a command only to a task that stands: a task handed in that is not one the wire
|
|
4759
4766
|
published is named (`command.task`) rather than checked against.
|
|
4760
4767
|
|
|
@@ -4805,8 +4812,8 @@ react rather than only display the message:
|
|
|
4805
4812
|
| --- | --- |
|
|
4806
4813
|
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
4807
4814
|
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
4808
|
-
| `omni.task-not-found` | The
|
|
4809
|
-
| `omni.phone-not-permitted` | The
|
|
4815
|
+
| `omni.task-not-found` | The assignment named is not one the provider holds, typically after the task already ended. |
|
|
4816
|
+
| `omni.phone-not-permitted` | The agent application declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
|
|
4810
4817
|
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
4811
4818
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
4812
4819
|
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
@@ -4820,7 +4827,7 @@ They are published as `OMNI_FAILURE_CODES`.
|
|
|
4820
4827
|
|
|
4821
4828
|
| Field | Contract |
|
|
4822
4829
|
| --- | --- |
|
|
4823
|
-
| `id` | Required identifier for this event, unique within the login. Omni does not act on it; it exists so
|
|
4830
|
+
| `id` | Required identifier for this event, unique within the login. Omni does not act on it; it exists so an agent application log line and an adapter log line can be matched when something has to be traced. |
|
|
4824
4831
|
| `loginId` | Login session that produced the event. Omni rejects any other value, which only reaches it if an adapter kept an old connection emitting after a re-login. |
|
|
4825
4832
|
| `occurredAt` | Valid RFC-3339 timestamp with an explicit timezone, representing provider observation time. |
|
|
4826
4833
|
| `event` | Typed `ProviderEvent` payload. |
|
|
@@ -4835,7 +4842,7 @@ instant that the source cannot establish makes that evidence unavailable, not pe
|
|
|
4835
4842
|
invent a timestamp or silently omit a required field.
|
|
4836
4843
|
|
|
4837
4844
|
Clock estimation is an exception, not the standard interpretation of events. An ISO timestamp
|
|
4838
|
-
identifies an instant; it does not prove that the
|
|
4845
|
+
identifies an instant; it does not prove that the agent application and provider clocks are synchronized.
|
|
4839
4846
|
Use an explicitly trusted clock in the relevant domain. Do not infer a general clock offset
|
|
4840
4847
|
from event arrivals, shift source timestamps, or use an estimate to order events or create history.
|
|
4841
4848
|
|
|
@@ -4843,29 +4850,29 @@ The current exceptions and their reasons are:
|
|
|
4843
4850
|
|
|
4844
4851
|
| Case | Exception and reason |
|
|
4845
4852
|
| --- | --- |
|
|
4846
|
-
|
|
|
4847
|
-
|
|
|
4853
|
+
| Agent application display of provider deadlines (`assignmentExpiresAt`, `previewEndsAt`, wrap deadline) | A countdown may use an explicitly bounded provider-clock estimate when a trusted direct clock is unavailable, because the deadline belongs to another clock domain. This estimates the display only; it does not establish that the provider acted. Without usable time, show timing uncertainty. |
|
|
4854
|
+
| Agent application-triggered preview deadline | A bounded clock estimate with monotonic aging may schedule the agent application's Call command because the provider owns the deadline but the agent application owns the trigger. Do not trigger before the deadline is known to have passed; invalidate on clock discontinuity and reconcile when uncertain. |
|
|
4848
4855
|
| Recording evidence expiry | A bounded observer-domain clock estimate with monotonic aging may assess freshness because observation and expiry belong to the recorder's clock. If time cannot be trusted, recording state is unknown; never renew evidence from receipt or replay. |
|
|
4849
4856
|
| Unknown recording state | `observedAt` and `validUntil` are absent because there is no confirmed observation. The containing provider event still has its own `occurredAt`; that publication is not a recorder observation. |
|
|
4850
4857
|
| Direct snapshot reads and method requests/results | These are reads/operations, not event envelopes, and their current types have no general event timestamp. A snapshot event still carries `occurredAt`; embedded source instants remain unchanged. Do not treat a method result or read completion time as an occurrence boundary. |
|
|
4851
|
-
|
|
|
4852
|
-
| Provider receipt timestamps | The provider may use its own receipt instant for final records of receipt/processing because
|
|
4853
|
-
| Optional source instants and deadlines | Omit only where the declared type permits absence and the source has no evidence or the policy has no deadline (for example unlimited preview). Do not replace absence with
|
|
4858
|
+
| Agent application-provided provider-clock estimate | Best-effort ISO time may be supplied by the agent application for optional comparisons; it is not a source observation or accuracy guarantee. The reason is that the agent application does not own the provider clock. |
|
|
4859
|
+
| Provider receipt timestamps | The provider may use its own receipt instant for final records of receipt/processing because agent application timestamps are untrusted advisory input. Receipt must not be presented as an earlier action or capture boundary. |
|
|
4860
|
+
| Optional source instants and deadlines | Omit only where the declared type permits absence and the source has no evidence or the policy has no deadline (for example unlimited preview). Do not replace absence with agent application time. |
|
|
4854
4861
|
|
|
4855
4862
|
Every additional exception must be listed with its scope and reason before adoption. Estimates
|
|
4856
4863
|
must specify their uncertainty and validity; elapsed time should use monotonic aging, and a clock
|
|
4857
4864
|
jump invalidates the estimate. The optional check below samples provider time without changing event timestamps. Report
|
|
4858
4865
|
source-measured durations where required; timestamp subtraction is not a substitute.
|
|
4859
4866
|
|
|
4860
|
-
#### Optional periodic provider time checks and
|
|
4867
|
+
#### Optional periodic provider time checks and agent application estimates
|
|
4861
4868
|
|
|
4862
4869
|
A provider may declare `Manifest.timeCheck: true` and implement `Connection.checkTime(request)`.
|
|
4863
|
-
Omission means unsupported. The
|
|
4864
|
-
|
|
4870
|
+
Omission means unsupported. The agent application separately opts in with `ProviderTimeCheckPolicy`; these
|
|
4871
|
+
agent application-local settings are not task policy or a guarantee. All four durations are explicit positive
|
|
4865
4872
|
safe integer milliseconds, with `maxRoundTripMs <= timeoutMs <= intervalMs`. No default interval
|
|
4866
4873
|
is assumed. Validate settings with `validateProviderTimeCheckPolicy`.
|
|
4867
4874
|
|
|
4868
|
-
While the connection is active, the
|
|
4875
|
+
While the connection is active, the agent application checks immediately on opt-in and then at `intervalMs`
|
|
4869
4876
|
using a monotonic scheduler. Only one check may be outstanding. Missed ticks are skipped, never
|
|
4870
4877
|
replayed in a burst. Each request has a fresh ID. The provider samples its own event/deadline
|
|
4871
4878
|
clock after receiving the request and before returning `providerTime` as an ISO/RFC3339 instant,
|
|
@@ -4874,25 +4881,25 @@ may masquerade as provider time. A provider unable to sample that domain must no
|
|
|
4874
4881
|
Clock authority replacement or a discontinuity changes `clockId`.
|
|
4875
4882
|
|
|
4876
4883
|
Validate requests and responses with `validateProviderTimeCheckRequest` and
|
|
4877
|
-
`validateProviderTimeCheckResult`. The
|
|
4884
|
+
`validateProviderTimeCheckResult`. The agent application also checks the outstanding request, provider/connection
|
|
4878
4885
|
and login, monotonic elapsed time and cancellation after the await: a late successful response is
|
|
4879
4886
|
still rejected. Reject samples at or beyond `timeoutMs`, or above `maxRoundTripMs`. Cancel local
|
|
4880
4887
|
tracking and invalidate estimates on disconnect, logout, policy disable, clock change or clock
|
|
4881
4888
|
jump; late results cannot restore them. Resume with a fresh check after reconnection. Expire a
|
|
4882
|
-
sample at `maxSampleAgeMs` using monotonic elapsed time. Failure is visible through the
|
|
4889
|
+
sample at `maxSampleAgeMs` using monotonic elapsed time. Failure is visible through the agent application's
|
|
4883
4890
|
existing diagnostic path, with the estimate unavailable; the next scheduled check can recover.
|
|
4884
4891
|
Checking failure alone does not assert call/recording state or require a healthy connection to close.
|
|
4885
4892
|
This read-only check is not a task command. Protocol supplies declarations and validators; the
|
|
4886
|
-
|
|
4893
|
+
agent application implements the scheduler and the provider implements the clock read.
|
|
4887
4894
|
|
|
4888
|
-
The
|
|
4895
|
+
The agent application may also expose `Host.estimateProviderTime({ providerId, loginId })`. It returns an ISO
|
|
4889
4896
|
`at` in that provider's clock domain, the same explicit scope and `clockId`, or `undefined` when
|
|
4890
4897
|
unavailable. Optional `uncertaintyMs` is an estimate, not a guaranteed bound. The method is optional
|
|
4891
|
-
at the
|
|
4892
|
-
checks does not require the
|
|
4893
|
-
this check if the
|
|
4898
|
+
at the agent application level and is deliberately absent from `HostGuarantees`. A provider declaring time
|
|
4899
|
+
checks does not require the agent application to expose estimates, and an estimate need not originate from
|
|
4900
|
+
this check if the agent application has another explicitly configured source for that provider clock.
|
|
4894
4901
|
|
|
4895
|
-
To estimate the difference, the
|
|
4902
|
+
To estimate the difference, the agent application brackets the request with local send/receive instants and
|
|
4896
4903
|
measures elapsed time monotonically. For provider sample P and local bracketing instants H0/H1,
|
|
4897
4904
|
the offset under stable clocks lies between P-H1 and P-H0, before clock error and timestamp
|
|
4898
4905
|
precision are considered. A midpoint is only an estimate; one-way delays need not be symmetric.
|
|
@@ -4901,22 +4908,22 @@ sample with monotonic elapsed time, and return unavailable after its configured
|
|
|
4901
4908
|
`validateProviderTimeEstimate` checks shape and scope, not actual accuracy or freshness.
|
|
4902
4909
|
|
|
4903
4910
|
A provider may declare `Manifest.timestampAuthority: "provider"`: its own timestamps are
|
|
4904
|
-
used for final records, and
|
|
4905
|
-
of time-check support and the optional
|
|
4906
|
-
Omission makes no promise that
|
|
4911
|
+
used for final records, and agent application-supplied timestamps are advisory. This declaration is independent
|
|
4912
|
+
of time-check support and the optional agent application estimate; neither changes who owns the final record.
|
|
4913
|
+
Omission makes no promise that agent application timestamps will be trusted. No agent application-authoritative default
|
|
4907
4914
|
is inferred. Successful `recordStep` responses identify the provider-selected history instant;
|
|
4908
|
-
see **The
|
|
4915
|
+
see **The agent application records what it performs** for correlation and retained bindings.
|
|
4909
4916
|
|
|
4910
|
-
A provider need not trust or adopt any
|
|
4917
|
+
A provider need not trust or adopt any agent application-supplied timestamp. It may timestamp an incoming
|
|
4911
4918
|
message using its own clock at receipt and use that for its own processing/accounting. The reason
|
|
4912
|
-
for this exception is that
|
|
4913
|
-
Receipt time must be described as receipt/observation time, not relabeled as the original
|
|
4919
|
+
for this exception is that agent application clocks and agent application estimates are not authoritative at the provider.
|
|
4920
|
+
Receipt time must be described as receipt/observation time, not relabeled as the original agent application
|
|
4914
4921
|
action, recorder capture boundary, or proof an operation applied. An existing field with a
|
|
4915
4922
|
specific occurrence meaning still requires that evidence; this option does not silently redefine
|
|
4916
|
-
it.
|
|
4923
|
+
it. Agent application-only instants remain advisory input; the provider owns its authoritative publication.
|
|
4917
4924
|
|
|
4918
|
-
This
|
|
4919
|
-
provider-clock comparisons when the
|
|
4925
|
+
This agent application estimate is another explicit timestamp exception: it helps optional displays and
|
|
4926
|
+
provider-clock comparisons when the agent application lacks that clock directly. It is not a source occurrence,
|
|
4920
4927
|
may not replace event/history timestamps, and alone cannot authorize deadline actions or prove
|
|
4921
4928
|
recording freshness. Uses requiring a trustworthy bound still need independently established
|
|
4922
4929
|
clock accuracy/drift assumptions. It neither synchronizes the operating-system clock nor changes
|
|
@@ -4979,8 +4986,8 @@ safe for the agent to see.
|
|
|
4979
4986
|
| `active` | The transport is up and the provider is serving this session. It is the only value under which work arrives. |
|
|
4980
4987
|
| `error` | The adapter cannot serve the session and is not simply mid-reconnect. Say why in `message`, and say what revives it in `recovery` — required here, forbidden on any other status. It is not terminal: an adapter that recovers on its own still reports `connecting` and then `active`, and one that cannot is revived as `recovery` says. |
|
|
4981
4988
|
|
|
4982
|
-
**An error names its recovery, and the
|
|
4983
|
-
died; the
|
|
4989
|
+
**An error names its recovery, and the agent application acts on that word.** The adapter knows why its session
|
|
4990
|
+
died; the agent application knows how to run a login. `recovery` joins the two:
|
|
4984
4991
|
|
|
4985
4992
|
- **`reconnect`** — the login is good and this connection is not: a backend restart, a session the
|
|
4986
4993
|
platform no longer recognises. Omni calls `disconnect()` on the dead connection and then
|
|
@@ -4995,7 +5002,7 @@ died; the host knows how to run a login. `recovery` joins the two:
|
|
|
4995
5002
|
`reconnect`, and the fresh snapshot re-establishes state. The adapter tears nothing down itself
|
|
4996
5003
|
and expects nothing to carry on: the connection that reported `error` is finished.
|
|
4997
5004
|
|
|
4998
|
-
**Patience is the
|
|
5005
|
+
**Patience is the agent application's.** An adapter in `connecting` retries for as long as it takes and never
|
|
4999
5006
|
has to decide when to stop. Omni owns giving up: after however long it chooses to wait, it may
|
|
5000
5007
|
call `disconnect()` and either `connect()` afresh or surface the failure — so neither side waits
|
|
5001
5008
|
for the other to blink.
|
|
@@ -5007,11 +5014,11 @@ availability on `BreakState`. Nor does it carry authentication — a session tha
|
|
|
5007
5014
|
the transport underneath may be perfectly `active`. Provider login identity likewise belongs to
|
|
5008
5015
|
authentication state, not here.
|
|
5009
5016
|
|
|
5010
|
-
**Only `active` means work can arrive.** Omni stops expecting
|
|
5017
|
+
**Only `active` means work can arrive.** Omni stops expecting assignments in any other value, so an
|
|
5011
5018
|
adapter that leaves a stale `active` in place is telling Omni to keep waiting for work that cannot
|
|
5012
5019
|
come — see **Liveness**.
|
|
5013
5020
|
|
|
5014
|
-
**The status is seen, not merely known.**
|
|
5021
|
+
**The status is seen, not merely known.** An agent application renders the transport's status where the agent
|
|
5015
5022
|
works — `connecting` from the moment it is reported, `error` with what revives it — and never a
|
|
5016
5023
|
healthy workspace over a transport that cannot serve it: an agent talking on a desk whose transport
|
|
5017
5024
|
died was an incident, and it happened because the desk knew and did not say. The same obligation
|
|
@@ -5046,9 +5053,9 @@ snapshot. `seed(snapshot)` returns violations, which callers must check. This is
|
|
|
5046
5053
|
tracker, not a complete production reducer: snapshot freshness, login/connection fencing and
|
|
5047
5054
|
full task consistency remain separately required.
|
|
5048
5055
|
|
|
5049
|
-
**Both sides must enforce the order.** The
|
|
5056
|
+
**Both sides must enforce the order.** The agent application validates immediately before dispatch; the
|
|
5050
5057
|
provider rechecks current permission and state atomically before acting. The provider validates
|
|
5051
|
-
the resulting break/task state before publication and serializes those publications. The
|
|
5058
|
+
the resulting break/task state before publication and serializes those publications. The agent application
|
|
5052
5059
|
validates the envelope, transition and full task consistency before replacing local state. A
|
|
5053
5060
|
request result does not optimistically advance break state, and a delayed command cannot apply
|
|
5054
5061
|
to a later attempt merely because its status happens to look compatible. Keep at most one
|
|
@@ -5061,7 +5068,7 @@ Normal order is `not-requested` → `awaiting-approval` → `granted` →
|
|
|
5061
5068
|
returns a precommit request to `not-requested`; an explicit agent end returns a committed
|
|
5062
5069
|
break there. Same-state restatements are allowed. An evidenced forced break is the explicit
|
|
5063
5070
|
exception to requesting/granting, and must carry its forced actor/state. Neither skipped
|
|
5064
|
-
publication nor
|
|
5071
|
+
publication nor an agent application-local guess creates another exception. A later normal attempt starts
|
|
5065
5072
|
from `not-requested`, never by regressing an active break into a request.
|
|
5066
5073
|
|
|
5067
5074
|
A fresh authoritative snapshot establishes a new baseline; do not run this event-transition
|
|
@@ -5102,8 +5109,8 @@ The provider's word that the task's audio should now attach. Omni calls `openMed
|
|
|
5102
5109
|
a task carried with `media: "started"`, which is how a reconnect snapshot reattaches audio an
|
|
5103
5110
|
earlier event brought — and renders the call as live from that word, never from its own senses. It
|
|
5104
5111
|
precedes `openMedia` and is never a reply to it: a provider whose media state comes from the
|
|
5105
|
-
platform, a station going in use the moment a call is answered, sends it then, before any
|
|
5106
|
-
opened anything, and `openMedia` has its own answer for what the
|
|
5112
|
+
platform, a station going in use the moment a call is answered, sends it then, before any agent application has
|
|
5113
|
+
opened anything, and `openMedia` has its own answer for what the agent application did. It
|
|
5107
5114
|
names a task whose work has begun, and it alternates with `task-media-ended`: media that never
|
|
5108
5115
|
started cannot end, so a live call whose provider says nothing about its audio is a provider in
|
|
5109
5116
|
breach, not a state a desk fills in from its own devices.
|
|
@@ -5138,21 +5145,21 @@ A successful `complete` or `transfer` command does not clear the task. Omni wait
|
|
|
5138
5145
|
and not for ever: `applied` to a completion -- `complete`, or a lead's `take-over-call` -- says the
|
|
5139
5146
|
provider has completed the task, and its `task-ended` follows within the
|
|
5140
5147
|
manifest's `completionSettleMs`. A provider never answers `applied` for a completion it has not yet
|
|
5141
|
-
performed. Past the bound the
|
|
5148
|
+
performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
|
|
5142
5149
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
5143
5150
|
the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
|
|
5144
5151
|
task, since the ending was owed and lost. The drive holds a provider to the same bound
|
|
5145
5152
|
(`drive.completion.unsettled`). The `task-media-ended` event and the `completing` phase are likewise
|
|
5146
5153
|
non-terminal. A replacement
|
|
5147
5154
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
5148
|
-
same envelope ID is harmless, and a `task-ended` naming an
|
|
5149
|
-
recognised as the late event it is, never applied to
|
|
5155
|
+
same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
|
|
5156
|
+
recognised as the late event it is, never applied to whatever assignment is open now.
|
|
5150
5157
|
|
|
5151
5158
|
### `dial-outcome`
|
|
5152
5159
|
|
|
5153
|
-
How a dial the
|
|
5160
|
+
How a dial the agent application placed ended, once, either way, named by the `dialId` the agent application sent: `answered`,
|
|
5154
5161
|
or one of the declared ways of not reaching the destination, with the switch's `reason` where it
|
|
5155
|
-
gave one. `
|
|
5162
|
+
gave one. `assignmentId` names the task the dial was on, where there was one, and that task may already
|
|
5156
5163
|
have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
|
|
5157
5164
|
audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
|
|
5158
5165
|
dial has an outcome**.
|
|
@@ -5168,11 +5175,11 @@ explicit timezones.
|
|
|
5168
5175
|
The provider's report that something its platform answered broke a rule the adapter relies on — a
|
|
5169
5176
|
state read that contradicted itself, a `task-ended` naming a task nobody held, a party arriving
|
|
5170
5177
|
without a name. `expected` is the rule as a sentence, `observed` is what came instead, and
|
|
5171
|
-
`
|
|
5178
|
+
`assignmentId` names the task where there is one. **Informational, never behavioural**: the provider has
|
|
5172
5179
|
already done the safe thing before it speaks, and nothing in the task or break machinery reacts to
|
|
5173
|
-
a diagnostic. It exists so that the loudness reaches a person —
|
|
5180
|
+
a diagnostic. It exists so that the loudness reaches a person — an agent application renders each one where the
|
|
5174
5181
|
agent works and keeps a count an operator can read, because a healthy workspace over a shouting
|
|
5175
|
-
console is the silence problem one layer up. One event per occurrence; the
|
|
5182
|
+
console is the silence problem one layer up. One event per occurrence; the agent application counts, the
|
|
5176
5183
|
provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
|
|
5177
5184
|
violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
|
|
5178
5185
|
fails loudly rather than passing with a note. A task published under `capabilitySource:
|
|
@@ -5183,7 +5190,7 @@ fails loudly rather than passing with a note. A task published under `capability
|
|
|
5183
5190
|
| --- | --- |
|
|
5184
5191
|
| `expected` | Required. The rule that was broken, as a sentence a person can read. |
|
|
5185
5192
|
| `observed` | Required. What the platform answered instead. |
|
|
5186
|
-
| `
|
|
5193
|
+
| `assignmentId` | Optional. The task concerned, where there is one, named by its assignment. |
|
|
5187
5194
|
|
|
5188
5195
|
### `queue-summary`
|
|
5189
5196
|
|
|
@@ -5217,10 +5224,11 @@ declares the `calendar` idle capability.
|
|
|
5217
5224
|
|
|
5218
5225
|
## Utilities
|
|
5219
5226
|
|
|
5220
|
-
### `taskKey(providerId,
|
|
5227
|
+
### `taskKey(providerId, assignmentId)`
|
|
5221
5228
|
|
|
5222
|
-
Returns a collision-safe global task key by encoding and joining the provider
|
|
5223
|
-
Use this key in Omni state;
|
|
5229
|
+
Returns a collision-safe global task key by encoding and joining the provider id and the
|
|
5230
|
+
assignment id. Use this key in Omni state; an assignment id is unique within its provider and
|
|
5231
|
+
never across providers.
|
|
5224
5232
|
|
|
5225
5233
|
### `userKey(providerId, userId)`
|
|
5226
5234
|
|
|
@@ -5228,7 +5236,7 @@ The same treatment for a `UserId`, and needed for the same reason: user identifi
|
|
|
5228
5236
|
issued by each provider independently, so two providers will eventually issue the same string for
|
|
5229
5237
|
different people. Encode and join before storing or comparing.
|
|
5230
5238
|
|
|
5231
|
-
Use it for every `UserId` — `
|
|
5239
|
+
Use it for every `UserId` — `history[].by`, team members, `memberId` on a
|
|
5232
5240
|
lead command, `ForcedBreak.by`. A bare one is only ever compared against another from the **same** provider; anything
|
|
5233
5241
|
wider goes through this key.
|
|
5234
5242
|
|
|
@@ -5252,18 +5260,18 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
5252
5260
|
| `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
|
|
5253
5261
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
5254
5262
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
5255
|
-
| `validateHostGuarantees(guarantees)` | What
|
|
5263
|
+
| `validateHostGuarantees(guarantees)` | What an agent application promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever agent application a test hands the adapter. |
|
|
5256
5264
|
| `validateProviderTimeCheckPolicy(policy)` | Explicit optional polling settings, positive safe-integer durations and round-trip/timeout/interval ordering. |
|
|
5257
|
-
| `validateProviderTimeCheckRequest(request)` | Fresh-request shape; the
|
|
5265
|
+
| `validateProviderTimeCheckRequest(request)` | Fresh-request shape; the agent application enforces actual uniqueness and outstanding-request lifetime. |
|
|
5258
5266
|
| `validateProviderTimeCheckResult(result, request, loginId)` | ISO timestamp, clock identity, exact request/login correlation; timing and source accuracy remain runtime checks. |
|
|
5259
|
-
| `validateProviderTimeEstimate(estimate, scope)` | Optional
|
|
5260
|
-
| `
|
|
5261
|
-
| `validateHostReport(report)` | The
|
|
5262
|
-
| `validateHostMute(mute, softphone)` | What the
|
|
5263
|
-
| `validateLoginStore(store)` | The login's store the
|
|
5264
|
-
| `validateCapacity(capacity)` | What the
|
|
5267
|
+
| `validateProviderTimeEstimate(estimate, scope)` | Optional agent application estimate shape and provider/login scope; no accuracy guarantee. |
|
|
5268
|
+
| `validateHistoryReport(report, path?, manifest?)` | What the agent application reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
5269
|
+
| `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
5270
|
+
| `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
5271
|
+
| `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
5272
|
+
| `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the drive, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
|
|
5265
5273
|
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|
|
5266
|
-
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`).
|
|
5274
|
+
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
|
|
5267
5275
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
5268
5276
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|
|
5269
5277
|
|
|
@@ -5289,25 +5297,25 @@ and may be compiled against a different protocol version, so its output is untru
|
|
|
5289
5297
|
Validating a snapshot before it replaces provider state is what stops a malformed task from
|
|
5290
5298
|
reaching the workspace.
|
|
5291
5299
|
|
|
5292
|
-
### What the
|
|
5300
|
+
### What the agent application does with what it refuses
|
|
5293
5301
|
|
|
5294
5302
|
A refusal has an aftermath on the desk and a report to the provider, and both are stated.
|
|
5295
5303
|
|
|
5296
|
-
- **A refused snapshot replaces nothing.** The
|
|
5304
|
+
- **A refused snapshot replaces nothing.** The agent application keeps the last state it took from this
|
|
5297
5305
|
provider whole, and shows the provider as faulted -- the transport as it stands, and the words
|
|
5298
5306
|
"last update refused" with the rule -- so the agent knows the view is standing still rather than
|
|
5299
5307
|
believing it current. It does not adopt the good half of a snapshot: a task the agent is on would
|
|
5300
5308
|
vanish from the desk while the call is up.
|
|
5301
|
-
- **A refused event is dropped and counted.** The state the
|
|
5302
|
-
provider is told, and may republish a corrected state or raise a `diagnostic`; the
|
|
5309
|
+
- **A refused event is dropped and counted.** The state the agent application holds does not move for it. The
|
|
5310
|
+
provider is told, and may republish a corrected state or raise a `diagnostic`; the agent application retries
|
|
5303
5311
|
nothing. The desk shows nothing for it: the view kept moving for every other event, so "last
|
|
5304
5312
|
update refused" would be false of it, and a chip that said nothing would be right about the
|
|
5305
|
-
transport. Counted, and the provider told, is all
|
|
5313
|
+
transport. Counted, and the provider told, is all an agent application can truthfully do for one event.
|
|
5306
5314
|
- **The provider is told, every time.** `refused(report)` carries the artefact, the envelope id for
|
|
5307
5315
|
an event, and every violation with its rule and path. An adapter logs it at error and treats it as
|
|
5308
|
-
its own defect until shown otherwise: what the
|
|
5316
|
+
its own defect until shown otherwise: what the agent application refused never reached the agent, and a
|
|
5309
5317
|
provider that hears nothing runs a whole shift beside a frozen desk with nothing wrong on its
|
|
5310
|
-
side. The harness tells an adapter under test exactly as
|
|
5318
|
+
side. The harness tells an adapter under test exactly as an agent application does
|
|
5311
5319
|
(`connection.refused.required`, `connection.refused.rejected`).
|
|
5312
5320
|
|
|
5313
5321
|
## Conformance helpers
|
|
@@ -5339,7 +5347,7 @@ latest the session published during the run, which differs only when the adapter
|
|
|
5339
5347
|
A capability granted by a later login requires its methods just as one declared at sign-in does.
|
|
5340
5348
|
|
|
5341
5349
|
`result.notExercised` lists what the run never reached — one subject per family of rules: each
|
|
5342
|
-
optional part of a task (`task.browsers`, `task.
|
|
5350
|
+
optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
|
|
5343
5351
|
`reasons` and `forced`, the team member list's `members` and `requests`, each declared contribution, and
|
|
5344
5352
|
each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
|
|
5345
5353
|
Nothing there is a violation: an adapter with no team has nothing to exercise. But a fixture with
|
|
@@ -5352,18 +5360,18 @@ in each adopter's own tests. `{ drive: true }` closes that: the exercise takes t
|
|
|
5352
5360
|
provider offers through one ordinary lifecycle -- accept it, wait for its media and open it on a
|
|
5353
5361
|
softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
|
|
5354
5362
|
complete it with an outcome where the agent completes -- and holds every step to the rules a
|
|
5355
|
-
|
|
5363
|
+
agent application holds a provider to. Each command is validated against the task as published
|
|
5356
5364
|
(`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
|
|
5357
5365
|
violation (`drive.command.failed`), and an event the provider owes and never sends is one too
|
|
5358
5366
|
(`drive.timeout`, after `driveTimeoutMs`, 5000 by default). Around the drive the exercise holds
|
|
5359
|
-
the run to what
|
|
5367
|
+
the run to what an agent application holds a provider to between commands: an offer before the agent application stated
|
|
5360
5368
|
capacity, or beyond the count with nothing dialled on it, is named
|
|
5361
5369
|
(`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
|
|
5362
|
-
through `
|
|
5363
|
-
nothing (`
|
|
5370
|
+
through `getUserDetails` and the answer held to the shape, nobody unasked, nobody described as
|
|
5371
|
+
nothing (`getUserDetails.user.*`, `getUserDetails.unasked`, `connection.getUserDetails.empty`); and
|
|
5364
5372
|
the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
|
|
5365
5373
|
before it reads anything (`drive.reload.login`), and is then held to everything the first was on
|
|
5366
|
-
connect: the methods its declarations call for, the
|
|
5374
|
+
connect: the methods its declarations call for, the agent application's report, a capacity stated to it, its
|
|
5367
5375
|
snapshot read as the connect snapshot was, and its media opened afresh on a task carried with
|
|
5368
5376
|
`media: "started"`, since the first client's session died with it. The result also says which rules the
|
|
5369
5377
|
run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
|
|
@@ -5373,12 +5381,12 @@ gap and not a pass. With the audio open on a softphone,
|
|
|
5373
5381
|
the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
|
|
5374
5382
|
expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
|
|
5375
5383
|
ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
|
|
5376
|
-
in the completing publication or the open entry is refused (`task.
|
|
5384
|
+
in the completing publication or the open entry is refused (`task.history.muted.open`),
|
|
5377
5385
|
and the drive's closing report after the media ended is expected `recorded` and to change nothing:
|
|
5378
5386
|
a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
|
|
5379
5387
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
5380
|
-
(`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as
|
|
5381
|
-
does, the drive reloads the
|
|
5388
|
+
(`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
|
|
5389
|
+
does, the drive reloads the agent application as a reload happens: the first client is unsubscribed,
|
|
5382
5390
|
disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
|
|
5383
5391
|
built and connected for the same login with the same context and the same `store`; its snapshot
|
|
5384
5392
|
must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
|
|
@@ -5386,7 +5394,7 @@ connection to the end. A platform that holds the record hands it back, and an ad
|
|
|
5386
5394
|
the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
|
|
5387
5395
|
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
5388
5396
|
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
5389
|
-
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.
|
|
5397
|
+
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
|
|
5390
5398
|
`.media`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
5391
5399
|
adapter is held to what the first was: the same provider
|
|
5392
5400
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
@@ -5397,12 +5405,12 @@ first client is gone before the second connects, there is no client for the plat
|
|
|
5397
5405
|
open task back to, and nothing about the reload is exempt from any rule. The second adapter opens its session from the same
|
|
5398
5406
|
`secrets`, so the reload is a restore before it is anything else: an adapter that holds a session
|
|
5399
5407
|
has already put what would rebuild it into the secrets store, from the moment it was handed one,
|
|
5400
|
-
not only when a flow completes -- the session is the adapter's, the store is the
|
|
5401
|
-
|
|
5402
|
-
handed the adapter holds no key naming the task's id, delimited, never as a run of characters inside
|
|
5408
|
+
not only when a flow completes -- the session is the adapter's, the store is the agent application's, and a
|
|
5409
|
+
agent application reload is exactly when no flow will run. And once the task has ended, the store the drive
|
|
5410
|
+
handed the adapter holds no key naming the task's assignment id, delimited, never as a run of characters inside
|
|
5403
5411
|
another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
|
|
5404
5412
|
unevaluated, and the result says so rather than passing it: a task's keys go
|
|
5405
|
-
with the task, or
|
|
5413
|
+
with the task, or whatever comes next inherits them. Outside the
|
|
5406
5414
|
interaction phases with `hold` still declared -- in
|
|
5407
5415
|
`confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
|
|
5408
5416
|
drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
|
|
@@ -5456,8 +5464,8 @@ cannot be established from TypeScript structure alone.
|
|
|
5456
5464
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
5457
5465
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|
|
5458
5466
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
5459
|
-
| `stillHost(report?, guarantees?, mute?)` |
|
|
5460
|
-
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for
|
|
5467
|
+
| `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. |
|
|
5468
|
+
| `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. |
|
|
5461
5469
|
| `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. |
|
|
5462
5470
|
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
|
|
5463
5471
|
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
@@ -5475,7 +5483,7 @@ Adapters should run the relevant scenarios against deterministic test state befo
|
|
|
5475
5483
|
|
|
5476
5484
|
## A provider does not style the workspace
|
|
5477
5485
|
|
|
5478
|
-
How a deployment themes Omni is the
|
|
5486
|
+
How a deployment themes Omni is the agent application's concern and is specified with the agent application, not here. What
|
|
5479
5487
|
belongs in this contract is the boundary. A provider says what a control **is** through its
|
|
5480
5488
|
capabilities and what its work is **called** through `phaseLabels` and `taskTypePresentation`; how
|
|
5481
5489
|
any of it is drawn is Omni's. A task cannot select a design language, inject a component, or
|
|
@@ -5485,21 +5493,21 @@ override the agent's theme and font preferences.
|
|
|
5485
5493
|
|
|
5486
5494
|
Recording is voice-only. A task can arrive already recording, including while pending, and offer
|
|
5487
5495
|
no recording controls. The provider publishes only its own current state on
|
|
5488
|
-
the provider field of the task’s `recording` state. The
|
|
5489
|
-
A provider task update cannot replace
|
|
5496
|
+
the provider field of the task’s `recording` state. The agent application publishes its own full scoped view in `HostReport.recordings`.
|
|
5497
|
+
A provider task update cannot replace agent application state. Neither a capability nor an accepted command
|
|
5490
5498
|
establishes recording. Missing state, lost recorder observation, transport loss and expired
|
|
5491
5499
|
observation mean unknown; they never mean stopped. Paused means a recording remains open without
|
|
5492
5500
|
capturing. Task hold, task pause and recording pause are independent.
|
|
5493
5501
|
|
|
5494
|
-
The per-task policy is `Task.capabilities.recording`, with independently optional provider and
|
|
5502
|
+
The per-task policy is `Task.capabilities.recording`, with independently optional provider and agent application
|
|
5495
5503
|
action sets. The outer capability may be locked. Absence grants no permission, and a state may
|
|
5496
5504
|
exist without any permission. A provider may offer only stop for a recording started automatically,
|
|
5497
5505
|
or withdraw a control on a later task update. There is no global recording mode and no automatic
|
|
5498
|
-
start from a capability declaration.
|
|
5499
|
-
not the provider-owned manifest. The task chooses an explicitly provisioned
|
|
5506
|
+
start from a capability declaration. Agent application support is declared on `ConnectContext.host.recording`,
|
|
5507
|
+
not the provider-owned manifest. The task chooses an explicitly provisioned agent application destination;
|
|
5500
5508
|
unknown destinations or unsupported actions are refused visibly, never redirected to provider
|
|
5501
|
-
recording or a default upload location. Initial
|
|
5502
|
-
capture of both local and remote audio;
|
|
5509
|
+
recording or a default upload location. Initial agent application support requires voice softphone media and
|
|
5510
|
+
capture of both local and remote audio; an agent application unable to capture either must refuse start/resume.
|
|
5503
5511
|
This package declares that contract; it supplies no recorder, media mixing, storage or upload.
|
|
5504
5512
|
|
|
5505
5513
|
| Action | Required current recording state | Confirmed outcome |
|
|
@@ -5520,16 +5528,16 @@ Partial success (capture stopped but storage outcome unknown) cannot return fail
|
|
|
5520
5528
|
of no effect. It rejects with unknown outcome, reports the failure visibly and publishes whatever
|
|
5521
5529
|
current capture state is actually known. Retention is not a promise of sample-perfect audio.
|
|
5522
5530
|
|
|
5523
|
-
Provider commands go exclusively to `Connection.execute`;
|
|
5524
|
-
`HostRecording.execute`. Both include
|
|
5531
|
+
Provider commands go exclusively to `Connection.execute`; agent application commands go exclusively to
|
|
5532
|
+
`HostRecording.execute`. Both include the assignment, request and observation identities; all
|
|
5525
5533
|
non-start commands identify the particular recording. IDs are opaque and scoped by provider login,
|
|
5526
5534
|
task and recorder owner. They are never inferred from filenames or current agent identity. A
|
|
5527
|
-
recording ID survives pause/resume and
|
|
5528
|
-
commands always name the current
|
|
5535
|
+
recording ID survives pause/resume and reassignment only where the same recorder confirms continuity;
|
|
5536
|
+
commands always name the current assignment. A later start gets a different ID. Reconnect does not
|
|
5529
5537
|
create a new recording or fresh evidence. After lost continuity, use unknown until reconciled.
|
|
5530
5538
|
There is no automatic restart, transfer to another recorder, or stop on task hold/disconnect.
|
|
5531
|
-
Task removal does not prove recording stopped: outstanding
|
|
5532
|
-
|
|
5539
|
+
Task removal does not prove recording stopped: outstanding agent application recorders remain tracked by the
|
|
5540
|
+
agent application until its executor reconciles/finishes them, with visible unresolved cleanup failures.
|
|
5533
5541
|
|
|
5534
5542
|
Only start/resume require an in-progress or paused task with started media. Pause/stop/cancel may
|
|
5535
5543
|
also finish an independently observed recorder while the task is completing. A pending task may
|
|
@@ -5544,40 +5552,40 @@ Receipt, replay and task publication never extend freshness. Clock discontinuity
|
|
|
5544
5552
|
consumers using the explicit recording-expiry clock-estimation exception must invalidate that
|
|
5545
5553
|
estimate and use monotonic aging so clock rollback cannot
|
|
5546
5554
|
revive expired evidence. The executor compares observation identity and recording identity against
|
|
5547
|
-
its latest state atomically before I/O. An intervening observation or
|
|
5555
|
+
its latest state atomically before I/O. An intervening observation or assignment change refuses the
|
|
5548
5556
|
stale command; it never acts on a replacement recorder. Providers lacking trustworthy current-state
|
|
5549
5557
|
identity or observation time publish unknown and offer no issuable controls.
|
|
5550
5558
|
|
|
5551
5559
|
Use `validateTask` and `validateTaskCommand` for published shape and static permissions.
|
|
5552
5560
|
Immediately before dispatch, additionally use `validateRecordingRequest` against the full current
|
|
5553
|
-
task and explicit observer-domain time. For
|
|
5554
|
-
full
|
|
5561
|
+
task and explicit observer-domain time. For agent application commands also supply the current agent application declaration,
|
|
5562
|
+
full agent application report and softphone context. The same checks must run at the executing boundary;
|
|
5555
5563
|
client validation alone is not authorization. The executor serializes operations on each recorder
|
|
5556
5564
|
and durably correlates request identity with scope, action and outcome: an exact retry refers to the
|
|
5557
5565
|
same operation; reuse with different contents is refused. It never silently retries an ambiguous
|
|
5558
5566
|
operation. Implementations without reliable request correlation must refuse controls.
|
|
5559
5567
|
|
|
5560
|
-
Keep command progress in
|
|
5561
|
-
recording transition and any retain/discard effect actually completed; publish the confirming task or
|
|
5568
|
+
Keep command progress in agent application UI separately from authoritative state. Applied means the requested
|
|
5569
|
+
recording transition and any retain/discard effect actually completed; publish the confirming task or agent application report before
|
|
5562
5570
|
resolving applied. Failed means confirmed no effect. Rejected promise means outcome unknown:
|
|
5563
5571
|
show the failure, reconcile that path and do not automatically retry or pretend inactive. Authoritative
|
|
5564
|
-
updates remain full current task/
|
|
5572
|
+
updates remain full current task/agent application views, not replayed provider events. These current-state fields
|
|
5565
5573
|
create no interaction-history entries and infer no actors, durations or historical capture boundaries.
|
|
5566
5574
|
|
|
5567
5575
|
Migration is explicit: replace the old true recording capability with per-path action policies,
|
|
5568
5576
|
replace untargeted commands with scoped commands, and retain protocol version 1. During pre-release
|
|
5569
5577
|
development, hosts and adapters must align their recording contracts; negotiation does not detect
|
|
5570
5578
|
differences between package revisions that share that protocol number. No legacy-shape fallback is provided.
|
|
5571
|
-
This change does not publish a package or enable recording in any existing
|
|
5579
|
+
This change does not publish a package or enable recording in any existing agent application/provider by itself.
|
|
5572
5580
|
|
|
5573
5581
|
`validateRecordingOutcome` checks confirming observations against recording-specific applied/failed
|
|
5574
5582
|
semantics, including pause/resume identity and rejection of a dialling result. It cannot prove audio
|
|
5575
5583
|
retention/completion or source truth from a status flag; those remain executor obligations.
|
|
5576
5584
|
|
|
5577
|
-
|
|
5585
|
+
Agent application destination selection binds storage when start actually creates a recording. Existing recordings
|
|
5578
5586
|
keep that binding through pause/resume/stop/cancel; a later policy cannot redirect their stored audio.
|
|
5579
5587
|
The executor rejects a mismatched destination rather than moving or discarding another binding.
|
|
5580
|
-
Action permission does not authorize unattended invocation:
|
|
5588
|
+
Action permission does not authorize unattended invocation: agent application controls require the agent's explicit
|
|
5581
5589
|
act, and provider authorization remains enforced at its authenticated command boundary.
|
|
5582
5590
|
|
|
5583
5591
|
|
|
@@ -5589,31 +5597,31 @@ capability restrictions from disappearing at dispatch. The standalone `validateR
|
|
|
5589
5597
|
requires an affirmative permission; false, malformed permission declarations and unknown actions grant nothing.
|
|
5590
5598
|
|
|
5591
5599
|
|
|
5592
|
-
###
|
|
5600
|
+
### Agent application recording announcements to the caller
|
|
5593
5601
|
|
|
5594
|
-
`HostRecording.announcesToCaller`, when true, is a guarantee that the
|
|
5602
|
+
`HostRecording.announcesToCaller`, when true, is a guarantee that the agent application delivers audible recording
|
|
5595
5603
|
status messages to the remote party over the call's outgoing audio. It belongs inside the optional
|
|
5596
|
-
|
|
5604
|
+
agent application recording capability: without that capability there is no such guarantee. Omission makes no
|
|
5597
5605
|
promise; false is invalid. It does not belong in the provider manifest, the task's recording policy
|
|
5598
|
-
or the general `HostGuarantees` object. A provider can inspect this
|
|
5599
|
-
whether to permit
|
|
5606
|
+
or the general `HostGuarantees` object. A provider can inspect this agent application guarantee when deciding
|
|
5607
|
+
whether to permit agent application recording on a task.
|
|
5600
5608
|
|
|
5601
|
-
The guarantee applies only to
|
|
5602
|
-
own recordings at their end; provider state updates must not cause the
|
|
5609
|
+
The guarantee applies only to agent application-owned recordings. Providers implement announcements for their
|
|
5610
|
+
own recordings at their end; provider state updates must not cause the agent application to announce those
|
|
5603
5611
|
recordings. Both recorders can operate independently on the same call.
|
|
5604
5612
|
|
|
5605
|
-
On a confirmed
|
|
5613
|
+
On a confirmed agent application start, the remote party hears that recording started. On a confirmed stop or
|
|
5606
5614
|
cancel, they hear that recording stopped. If pause/resume is supported, say paused/resumed so a
|
|
5607
5615
|
pause is not presented as a finished recording. Announcements follow actual capture transitions,
|
|
5608
5616
|
not button clicks, accepted requests or task-policy changes. When attaching to an already-active
|
|
5609
|
-
|
|
5617
|
+
agent application recording, announce that recording is active, without inventing its original start time.
|
|
5610
5618
|
Repeated observations of the same state do not repeat announcements. Unknown or expired evidence
|
|
5611
5619
|
must never produce a stopped announcement.
|
|
5612
5620
|
|
|
5613
5621
|
A local sound, UI indicator, screen-reader message to the agent, or sound leaking through the
|
|
5614
|
-
microphone does not fulfil this guarantee. The
|
|
5622
|
+
microphone does not fulfil this guarantee. The agent application must deliver the message directly in the audio
|
|
5615
5623
|
sent to the remote party, even when the agent microphone is muted. It may also notify the agent.
|
|
5616
|
-
Only declare the guarantee when the
|
|
5624
|
+
Only declare the guarantee when the agent application can provide that outgoing audio path. An announcement
|
|
5617
5625
|
that cannot be delivered, including after the remote party has disconnected, is a visible failure;
|
|
5618
5626
|
never claim delivery or replay the notice into a different call.
|
|
5619
5627
|
|
|
@@ -5623,4 +5631,4 @@ report the announcement failure visibly and reject with unknown command outcome.
|
|
|
5623
5631
|
effect, automatically repeat the recording action or silently switch to an agent-only notice.
|
|
5624
5632
|
|
|
5625
5633
|
`validateHostRecording` checks the declaration, including its true-or-absent guarantee. Actual audio
|
|
5626
|
-
delivery remains the
|
|
5634
|
+
delivery remains the agent application implementation's responsibility and must be exercised in its own tests.
|