@xema/omni-protocol 0.1.102 → 0.1.104
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 +81 -40
- package/dist/index.js +4 -2
- package/dist/testing.d.ts +8 -1
- package/dist/testing.js +81 -8
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +134 -41
- package/guide.md +199 -94
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -153,6 +153,12 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
153
153
|
authenticationMethods: AuthenticationMethod[];
|
|
154
154
|
idleCapabilities?: IdleCapabilities<C>;
|
|
155
155
|
/** Optional provider-clock sampling through Connection.checkTime; absent means unsupported. */
|
|
156
|
+
/**
|
|
157
|
+
* Implements `checkTime`, and states `providerTime` on every snapshot. Required of a provider
|
|
158
|
+
* that publishes an instant the desk renders as a running duration -- a member's `since`, a
|
|
159
|
+
* lead's `listening.since`, an `onCall.since`, a shift's `signedInAt` -- so every screen counts
|
|
160
|
+
* from the provider's clock and two screens differ by their delay alone.
|
|
161
|
+
*/
|
|
156
162
|
timeCheck?: true;
|
|
157
163
|
/** Provider uses its own timestamps for final records; host timestamps are advisory. Omission makes no trust promise. */
|
|
158
164
|
timestampAuthority?: "provider";
|
|
@@ -179,12 +185,12 @@ export interface Manifest<C extends Channel = Channel> {
|
|
|
179
185
|
*/
|
|
180
186
|
runningStepReports?: true;
|
|
181
187
|
/**
|
|
182
|
-
* How long after
|
|
183
|
-
* `task-ended`
|
|
184
|
-
*
|
|
185
|
-
*
|
|
188
|
+
* How long after `applied` the wire shows what the provider did, in milliseconds: the
|
|
189
|
+
* `task-ended` after a `complete` or a lead's `take-over-call`, the `calendar-updated` after a
|
|
190
|
+
* `schedule`. Past it the host resyncs and shows the thing as unsettled. Stated per provider,
|
|
191
|
+
* since platforms settle at different speeds.
|
|
186
192
|
*/
|
|
187
|
-
|
|
193
|
+
settleMs: number;
|
|
188
194
|
}
|
|
189
195
|
export interface SecretStore {
|
|
190
196
|
get(key: string): Promise<string | undefined>;
|
|
@@ -666,8 +672,10 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
|
|
|
666
672
|
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
667
673
|
decline?: Lockable<true>;
|
|
668
674
|
hold?: Lockable<true>;
|
|
675
|
+
schedule?: Lockable<true>;
|
|
669
676
|
} : SharedTaskCapabilities & {
|
|
670
677
|
decline?: Lockable<true>;
|
|
678
|
+
schedule?: Lockable<true>;
|
|
671
679
|
};
|
|
672
680
|
/**
|
|
673
681
|
* How a reusing browser's session is keyed.
|
|
@@ -948,6 +956,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
948
956
|
* Omni said not, and absent past `pending`.
|
|
949
957
|
*/
|
|
950
958
|
acceptance?: AcceptanceMode;
|
|
959
|
+
/**
|
|
960
|
+
* In `pending` only: how long the offer has left, in whole seconds from this publication,
|
|
961
|
+
* restated on every publication that carries it so a reconnect snapshot says what is actually
|
|
962
|
+
* left. Absent, the offer stands until the provider says otherwise. Runs out: the provider ends
|
|
963
|
+
* the offer `expired` naming `pending`.
|
|
964
|
+
*/
|
|
965
|
+
expiresInSeconds?: DurationSeconds;
|
|
951
966
|
/**
|
|
952
967
|
* In `preview` only, and together: how long the preparation has left, in whole seconds from
|
|
953
968
|
* this publication -- restated on every publication that carries it, so a resync starts the
|
|
@@ -999,18 +1014,18 @@ export type TaskOutcome = {
|
|
|
999
1014
|
by: "agent" | "provider" | "party";
|
|
1000
1015
|
reason?: string;
|
|
1001
1016
|
}
|
|
1002
|
-
/** Only the phases in which somebody is still being waited on can expire
|
|
1017
|
+
/** Only the phases in which somebody is still being waited on can expire: an offer whose `expiresInSeconds` runs out names `pending`, accepted work the platform gave up on names `confirmed`. A preview never lapses: withdrawn, it is `cancelled` by the provider. */
|
|
1003
1018
|
| {
|
|
1004
1019
|
type: "expired";
|
|
1005
|
-
phase: "pending" | "confirmed"
|
|
1020
|
+
phase: "pending" | "confirmed";
|
|
1006
1021
|
} | {
|
|
1007
1022
|
type: "failed";
|
|
1008
1023
|
failure: ProtocolFailure;
|
|
1009
1024
|
};
|
|
1010
1025
|
export declare const TASK_COMMAND_NAMES: {
|
|
1011
1026
|
readonly voice: readonly ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call", "connect-back", "lead-assist", "conference", "recording", "schedule", "complete"];
|
|
1012
|
-
readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
|
|
1013
|
-
readonly email: readonly ["accept", "decline", "complete"];
|
|
1027
|
+
readonly chat: readonly ["accept", "decline", "pause", "resume", "schedule", "complete"];
|
|
1028
|
+
readonly email: readonly ["accept", "decline", "schedule", "complete"];
|
|
1014
1029
|
};
|
|
1015
1030
|
export type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> = (typeof TASK_COMMAND_NAMES)[C][number];
|
|
1016
1031
|
export interface OutcomePayload {
|
|
@@ -1043,17 +1058,7 @@ export type VoiceTaskCommand = {
|
|
|
1043
1058
|
| {
|
|
1044
1059
|
type: "connect-back";
|
|
1045
1060
|
dialId: DialId;
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Put a time on the calendar for this party: a follow-up promised on the call. Gated by
|
|
1049
|
-
* `schedule`, issuable in `in-progress`, `paused` or `completing`; the activity arrives on
|
|
1050
|
-
* `calendar-updated`, which is the provider's word that it stands.
|
|
1051
|
-
*/
|
|
1052
|
-
| {
|
|
1053
|
-
type: "schedule";
|
|
1054
|
-
at: IsoTimestamp;
|
|
1055
|
-
note?: string;
|
|
1056
|
-
}
|
|
1061
|
+
} | ScheduleCommand
|
|
1057
1062
|
/** Ask a lead to join, with a note. Gated by `leadAssist`. */
|
|
1058
1063
|
| {
|
|
1059
1064
|
type: "lead-assist";
|
|
@@ -1093,6 +1098,18 @@ export type VoiceTaskCommand = {
|
|
|
1093
1098
|
}) | ({
|
|
1094
1099
|
type: "complete";
|
|
1095
1100
|
} & OutcomePayload);
|
|
1101
|
+
/**
|
|
1102
|
+
* Put a time on the calendar for this party: a follow-up promised on the call or in the
|
|
1103
|
+
* conversation, or written up in wrap. Gated by `schedule` on any channel, issuable in
|
|
1104
|
+
* `in-progress`, `paused` or `completing`, under a manifest that declares `calendar` for it to
|
|
1105
|
+
* land on; `applied` says it is on the calendar, and the `calendar-updated` that shows it follows
|
|
1106
|
+
* within the manifest's `settleMs`.
|
|
1107
|
+
*/
|
|
1108
|
+
export type ScheduleCommand = {
|
|
1109
|
+
type: "schedule";
|
|
1110
|
+
at: IsoTimestamp;
|
|
1111
|
+
note?: string;
|
|
1112
|
+
};
|
|
1096
1113
|
export type ChatTaskCommand = {
|
|
1097
1114
|
type: "accept";
|
|
1098
1115
|
} | {
|
|
@@ -1101,14 +1118,14 @@ export type ChatTaskCommand = {
|
|
|
1101
1118
|
type: "pause";
|
|
1102
1119
|
} | {
|
|
1103
1120
|
type: "resume";
|
|
1104
|
-
} | ({
|
|
1121
|
+
} | ScheduleCommand | ({
|
|
1105
1122
|
type: "complete";
|
|
1106
1123
|
} & OutcomePayload);
|
|
1107
1124
|
export type EmailTaskCommand = {
|
|
1108
1125
|
type: "accept";
|
|
1109
1126
|
} | {
|
|
1110
1127
|
type: "decline";
|
|
1111
|
-
} | ({
|
|
1128
|
+
} | ScheduleCommand | ({
|
|
1112
1129
|
type: "complete";
|
|
1113
1130
|
} & OutcomePayload);
|
|
1114
1131
|
export interface CustomTaskCommand {
|
|
@@ -1187,14 +1204,21 @@ export interface BreakRequest {
|
|
|
1187
1204
|
reasonId?: string;
|
|
1188
1205
|
}
|
|
1189
1206
|
/**
|
|
1190
|
-
* A break forced on the agent; the agent must explicitly resume when ready.
|
|
1191
|
-
*
|
|
1192
|
-
*
|
|
1207
|
+
* A break forced on the agent; the agent must explicitly resume when ready. A lead's `force-break`
|
|
1208
|
+
* is a command to the provider, which forces the break: `by` names the lead who asked it to, or
|
|
1209
|
+
* `provider` where the platform itself did, on its own rule or a schedule. The agent sees who, a
|
|
1210
|
+
* person is named only when there is one, and a break the platform imposed is the platform's to
|
|
1211
|
+
* lift: a lead's `end-forced-break` on it fails with `omni.break-forced-by-provider`.
|
|
1193
1212
|
*/
|
|
1194
1213
|
export type ForcedBreak = {
|
|
1195
1214
|
by: UserId | "provider";
|
|
1196
|
-
/**
|
|
1197
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* How long the break is expected to run yet, in whole seconds from this publication, restated
|
|
1217
|
+
* on every publication that carries it so every screen counts down the same number from
|
|
1218
|
+
* receipt. Advisory only: nothing ends the break but the agent. Absent once it has run out, or
|
|
1219
|
+
* where none was expected.
|
|
1220
|
+
*/
|
|
1221
|
+
expectedEndsInSeconds?: DurationSeconds;
|
|
1198
1222
|
};
|
|
1199
1223
|
export interface BreakState {
|
|
1200
1224
|
status: BreakStatus;
|
|
@@ -1251,10 +1275,10 @@ export interface TeamMember {
|
|
|
1251
1275
|
break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
|
|
1252
1276
|
/** The member's open tasks, trimmed, kept current by the provider; `[]` when they hold none, omitted only where the provider cannot see them. */
|
|
1253
1277
|
tasks?: MemberTask[];
|
|
1254
|
-
/**
|
|
1255
|
-
listening?: MemberListening;
|
|
1256
|
-
/** The member's own history for the day: sign-in, sign-out, breaks and productivity. Omitted where the provider cannot say. */
|
|
1257
|
-
shift?:
|
|
1278
|
+
/** Every lead on this member's call, listening, coaching or joined, published to every lead alike; absent while none is. */
|
|
1279
|
+
listening?: MemberListening[];
|
|
1280
|
+
/** The member's own history for the day: sign-in, sign-out, breaks and productivity, the same numbers the member's own snapshot carries. Omitted where the provider cannot say. */
|
|
1281
|
+
shift?: Shift;
|
|
1258
1282
|
/** The member's standing ask for a lead on one of their calls; absent when they are not asking. */
|
|
1259
1283
|
request?: MemberRequest;
|
|
1260
1284
|
}
|
|
@@ -1297,11 +1321,14 @@ export type MemberTask<C extends Channel = Channel> = {
|
|
|
1297
1321
|
audio?: never;
|
|
1298
1322
|
});
|
|
1299
1323
|
/**
|
|
1300
|
-
*
|
|
1301
|
-
* when.
|
|
1302
|
-
*
|
|
1324
|
+
* A lead on this member's call: who, which of the member's calls, in the mode they are heard,
|
|
1325
|
+
* since when. Every lead sees every entry; the application opens `openAudio` on the entry whose
|
|
1326
|
+
* `leadId` is the signed-in lead, and closes it when that entry goes. A lead who is heard --
|
|
1327
|
+
* `join-call` -- is also on the task's `onCall` as an `agent`; `listen` and `coach` leave no trace
|
|
1328
|
+
* on the member's task.
|
|
1303
1329
|
*/
|
|
1304
1330
|
export interface MemberListening {
|
|
1331
|
+
leadId: UserId;
|
|
1305
1332
|
assignmentId: AssignmentId;
|
|
1306
1333
|
mode: ListeningMode;
|
|
1307
1334
|
since: IsoTimestamp;
|
|
@@ -1313,15 +1340,18 @@ export interface ShiftEvent {
|
|
|
1313
1340
|
kind: ShiftEventKind;
|
|
1314
1341
|
}
|
|
1315
1342
|
/**
|
|
1316
|
-
*
|
|
1343
|
+
* An agent's own history for the day, about the person rather than any one call: when they
|
|
1317
1344
|
* signed in and out, today's totals as the provider counts them, each present only when the
|
|
1318
|
-
* provider knows it, and the day's sign-in, sign-out and break events, oldest first.
|
|
1345
|
+
* provider knows it, and the day's sign-in, sign-out and break events, oldest first. Published
|
|
1346
|
+
* to the agent on their snapshot and on `shift-updated`, and to their lead on the team member
|
|
1347
|
+
* list: the same numbers from the same count.
|
|
1319
1348
|
*/
|
|
1320
|
-
export interface
|
|
1349
|
+
export interface Shift {
|
|
1321
1350
|
signedInAt: IsoTimestamp;
|
|
1322
1351
|
signedOutAt?: IsoTimestamp;
|
|
1323
1352
|
talkSeconds?: DurationSeconds;
|
|
1324
1353
|
holdSeconds?: DurationSeconds;
|
|
1354
|
+
/** Every break the member was on today, including time held elsewhere for a break another provider forced: the provider counts it under a break kind of its own. */
|
|
1325
1355
|
breakSeconds?: DurationSeconds;
|
|
1326
1356
|
tasksHandled?: number;
|
|
1327
1357
|
events?: ShiftEvent[];
|
|
@@ -1536,6 +1566,12 @@ export type PreferenceResult = {
|
|
|
1536
1566
|
export interface Snapshot<C extends Channel = Channel> {
|
|
1537
1567
|
transport: TransportStatus;
|
|
1538
1568
|
loginId: string;
|
|
1569
|
+
/**
|
|
1570
|
+
* The provider's own instant of this read, required of a provider that declares `timeCheck`:
|
|
1571
|
+
* every duration the desk renders off the active call is counted from the provider's clock, and
|
|
1572
|
+
* this sample saves a `checkTime` for it. Every envelope's `occurredAt` is a sample too.
|
|
1573
|
+
*/
|
|
1574
|
+
providerTime?: IsoTimestamp;
|
|
1539
1575
|
break: BreakState;
|
|
1540
1576
|
/** Every task currently owned by this agent for this provider. */
|
|
1541
1577
|
tasks: Task<C>[];
|
|
@@ -1543,6 +1579,8 @@ export interface Snapshot<C extends Channel = Channel> {
|
|
|
1543
1579
|
taskCount: number;
|
|
1544
1580
|
contacts?: Contact[];
|
|
1545
1581
|
calendar?: ScheduledActivity[];
|
|
1582
|
+
/** The agent's own day so far, as the provider counts it; replaced whole by `shift-updated`. Omitted where the provider cannot say. */
|
|
1583
|
+
shift?: Shift;
|
|
1546
1584
|
team?: TeamMembers;
|
|
1547
1585
|
}
|
|
1548
1586
|
/**
|
|
@@ -1590,8 +1628,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1590
1628
|
} | {
|
|
1591
1629
|
type: "task-offered";
|
|
1592
1630
|
task: Task<C>;
|
|
1593
|
-
/** How long the agent has to answer, in whole seconds from this event; the offer expires when it runs out. Absent, it stands until the provider says otherwise. */
|
|
1594
|
-
expiresInSeconds?: DurationSeconds;
|
|
1595
1631
|
} | {
|
|
1596
1632
|
type: "task-updated";
|
|
1597
1633
|
task: Task<C>;
|
|
@@ -1656,6 +1692,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1656
1692
|
} | {
|
|
1657
1693
|
type: "calendar-updated";
|
|
1658
1694
|
calendar: ScheduledActivity[];
|
|
1695
|
+
}
|
|
1696
|
+
/** The agent's own day, whole, whenever a total moves: a task ended, a break ended. */
|
|
1697
|
+
| {
|
|
1698
|
+
type: "shift-updated";
|
|
1699
|
+
shift: Shift;
|
|
1659
1700
|
};
|
|
1660
1701
|
/**
|
|
1661
1702
|
* The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
|
|
@@ -1669,7 +1710,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1669
1710
|
occurredAt: IsoTimestamp;
|
|
1670
1711
|
event: ProviderEvent<C>;
|
|
1671
1712
|
}
|
|
1672
|
-
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled"];
|
|
1713
|
+
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled", "omni.break-forced-by-provider"];
|
|
1673
1714
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1674
1715
|
/**
|
|
1675
1716
|
* What the host refused, told to the adapter that published it: a snapshot the host would not
|
package/dist/index.js
CHANGED
|
@@ -78,8 +78,8 @@ export const CAPABILITY_SOURCES = ["queue", "nobody", "not-yet-read"];
|
|
|
78
78
|
export const TASK_COMMAND_NAMES = {
|
|
79
79
|
voice: ["answer", "decline", "dial", "hold", "resume", "end-call", "terminate-call",
|
|
80
80
|
"connect-back", "lead-assist", "conference", "recording", "schedule", "complete"],
|
|
81
|
-
chat: ["accept", "decline", "pause", "resume", "complete"],
|
|
82
|
-
email: ["accept", "decline", "complete"],
|
|
81
|
+
chat: ["accept", "decline", "pause", "resume", "schedule", "complete"],
|
|
82
|
+
email: ["accept", "decline", "schedule", "complete"],
|
|
83
83
|
};
|
|
84
84
|
/** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
|
|
85
85
|
export function commandDialId(command) {
|
|
@@ -105,6 +105,8 @@ export const OMNI_FAILURE_CODES = [
|
|
|
105
105
|
"omni.break-already-committed",
|
|
106
106
|
/** A recording command the provider could not settle either way: no start, stop, pause or resume it can vouch for. */
|
|
107
107
|
"omni.recording-unsettled",
|
|
108
|
+
/** A lead asked to lift a break the platform itself imposed; the platform lifts its own. */
|
|
109
|
+
"omni.break-forced-by-provider",
|
|
108
110
|
];
|
|
109
111
|
/**
|
|
110
112
|
* Preserves the adapter's inferred concrete type while checking it implements `Adapter`.
|
package/dist/testing.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation }
|
|
|
7
7
|
* rules -- each optional part of a task, each optional part of the break state and team member list, each
|
|
8
8
|
* declared contribution, and each event type.
|
|
9
9
|
*/
|
|
10
|
-
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "contacts", "calendar", "team.policies"];
|
|
10
|
+
declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.audio", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.request", "team.tasks", "team.listening", "team.shift", "shift", "contacts", "calendar", "team.policies"];
|
|
11
11
|
export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
|
|
12
12
|
export interface AdapterContractResult {
|
|
13
13
|
events: ProviderEventEnvelope[];
|
|
@@ -249,6 +249,13 @@ export interface BreakCandidate {
|
|
|
249
249
|
/** Whether the agent can currently receive work from it: connected, with a capacity stated. */
|
|
250
250
|
holdsCapacity: boolean;
|
|
251
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* A break one provider forces stops the agent everywhere: the host states a capacity of zero on
|
|
254
|
+
* every other usable provider holding capacity -- taken, never refused, and nothing the agent can
|
|
255
|
+
* cancel -- and on the forcing provider states nothing, since the break itself holds the agent
|
|
256
|
+
* there. A provider whose login is not usable is left alone, as a break attempt leaves it.
|
|
257
|
+
*/
|
|
258
|
+
export declare function assertForcedBreakStopsTheRest(forcedOn: string, candidates: readonly BreakCandidate[], stopped: readonly string[]): void;
|
|
252
259
|
/**
|
|
253
260
|
* A break attempt asks every connected provider from which the agent can currently receive work.
|
|
254
261
|
* A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
|
package/dist/testing.js
CHANGED
|
@@ -28,6 +28,7 @@ const STATE_SUBJECTS = [
|
|
|
28
28
|
"team.tasks",
|
|
29
29
|
"team.listening",
|
|
30
30
|
"team.shift",
|
|
31
|
+
"shift",
|
|
31
32
|
"contacts",
|
|
32
33
|
"calendar",
|
|
33
34
|
"team.policies",
|
|
@@ -38,7 +39,7 @@ const EVENT_TYPES = {
|
|
|
38
39
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
39
40
|
"task-audio-started": true, "task-audio-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
|
|
40
41
|
"team-updated": true, "team-member-updated": true, "team-member-removed": true, "team-policies-updated": true,
|
|
41
|
-
"contacts-updated": true, "calendar-updated": true,
|
|
42
|
+
"contacts-updated": true, "calendar-updated": true, "shift-updated": true,
|
|
42
43
|
};
|
|
43
44
|
const CONTRACT_SUBJECTS = [
|
|
44
45
|
...STATE_SUBJECTS,
|
|
@@ -94,7 +95,7 @@ function observeMember(member, seen) {
|
|
|
94
95
|
return;
|
|
95
96
|
if (some(member.tasks))
|
|
96
97
|
seen.add("team.tasks");
|
|
97
|
-
if (member.listening
|
|
98
|
+
if (some(member.listening))
|
|
98
99
|
seen.add("team.listening");
|
|
99
100
|
if (member.shift !== undefined)
|
|
100
101
|
seen.add("team.shift");
|
|
@@ -126,6 +127,8 @@ function observeSnapshot(value, seen) {
|
|
|
126
127
|
seen.add("contacts");
|
|
127
128
|
if (some(value.calendar))
|
|
128
129
|
seen.add("calendar");
|
|
130
|
+
if (value.shift !== undefined)
|
|
131
|
+
seen.add("shift");
|
|
129
132
|
}
|
|
130
133
|
function observeEvent(envelope, seen) {
|
|
131
134
|
const event = isRecord(envelope) ? envelope.event : undefined;
|
|
@@ -162,6 +165,9 @@ function observeEvent(envelope, seen) {
|
|
|
162
165
|
if (some(event.calendar))
|
|
163
166
|
seen.add("calendar");
|
|
164
167
|
break;
|
|
168
|
+
case "shift-updated":
|
|
169
|
+
seen.add("shift");
|
|
170
|
+
break;
|
|
165
171
|
default: break;
|
|
166
172
|
}
|
|
167
173
|
}
|
|
@@ -719,9 +725,14 @@ function userIdsIn(snapshot) {
|
|
|
719
725
|
add(snapshot?.break?.forced?.by);
|
|
720
726
|
const team = snapshot?.team;
|
|
721
727
|
if (isRecord(team)) {
|
|
722
|
-
for (const member of Array.isArray(team.members) ? team.members : [])
|
|
723
|
-
if (isRecord(member))
|
|
724
|
-
|
|
728
|
+
for (const member of Array.isArray(team.members) ? team.members : []) {
|
|
729
|
+
if (!isRecord(member))
|
|
730
|
+
continue;
|
|
731
|
+
add(member.id);
|
|
732
|
+
for (const entry of Array.isArray(member.listening) ? member.listening : [])
|
|
733
|
+
if (isRecord(entry))
|
|
734
|
+
add(entry.leadId);
|
|
735
|
+
}
|
|
725
736
|
}
|
|
726
737
|
for (const task of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
|
|
727
738
|
const t = task;
|
|
@@ -1063,7 +1074,7 @@ const REACHABLE_PHASES = {
|
|
|
1063
1074
|
export const SUPERSEDED_BY_A_SNAPSHOT = new Set([
|
|
1064
1075
|
"snapshot", "transport-status", "break-state", "task-offered", "task-updated", "task-ended",
|
|
1065
1076
|
"task-audio-started", "task-audio-ended", "team-updated", "team-member-updated", "team-member-removed", "team-policies-updated",
|
|
1066
|
-
"contacts-updated", "calendar-updated",
|
|
1077
|
+
"contacts-updated", "calendar-updated", "shift-updated",
|
|
1067
1078
|
]);
|
|
1068
1079
|
export class TaskStream {
|
|
1069
1080
|
tasks = new Map();
|
|
@@ -1626,6 +1637,7 @@ async function driveOneCall(drive) {
|
|
|
1626
1637
|
const own = validateTaskCommand(command, latestTask(), `drive.command.${String(command.type)}`, {
|
|
1627
1638
|
levels: effectiveLevels(drive.manifest.orgLevels).map(level => level.id),
|
|
1628
1639
|
dialOutcomesDeclared: drive.manifest.dialOutcomes !== undefined,
|
|
1640
|
+
calendarDeclared: drive.manifest.idleCapabilities?.calendar === true,
|
|
1629
1641
|
autoAcceptTasks: drive.context.autoAcceptTasks,
|
|
1630
1642
|
});
|
|
1631
1643
|
found.push(...own);
|
|
@@ -1798,6 +1810,38 @@ async function driveOneCall(drive) {
|
|
|
1798
1810
|
}
|
|
1799
1811
|
}
|
|
1800
1812
|
}
|
|
1813
|
+
// 4a. Schedule a follow-up, where the task offers it: applied says it is on the calendar, and the
|
|
1814
|
+
// calendar-updated that shows it is owed within the manifest's bound. The time is the provider's
|
|
1815
|
+
// own: a day after the instant it published the offer, so no clock of the runner's is read.
|
|
1816
|
+
if (latestTask().phase === "in-progress" && offers("schedule")) {
|
|
1817
|
+
const publishedAt = drive.events.slice(0, cursor).reverse().find(envelope => {
|
|
1818
|
+
const event = envelope.event;
|
|
1819
|
+
return (event.type === "task-offered" || event.type === "task-updated") && isTask(event.task) && event.task.assignmentId === taskId;
|
|
1820
|
+
})?.occurredAt;
|
|
1821
|
+
const at = new Date(Date.parse(String(publishedAt ?? "2026-01-01T00:00:00Z")) + 24 * 60 * 60 * 1000).toISOString();
|
|
1822
|
+
if (await send({ type: "schedule", at, note: "Follow-up promised on the call" }) !== undefined) {
|
|
1823
|
+
ruleEvaluated("drive.schedule.unsettled");
|
|
1824
|
+
const settle = Number(drive.manifest.settleMs);
|
|
1825
|
+
const carries = (calendar) => Array.isArray(calendar) && calendar.some(activity => isRecord(activity) && activity.startsAt === at);
|
|
1826
|
+
let unsettled = false;
|
|
1827
|
+
const shown = await waitFor("a calendar-updated carrying the follow-up", envelope => {
|
|
1828
|
+
const event = envelope.event;
|
|
1829
|
+
return event.type === "calendar-updated" && carries(event.calendar) ? event : undefined;
|
|
1830
|
+
}, cursor, { ms: settle, onExpiry: () => { unsettled = true; } });
|
|
1831
|
+
if (shown === undefined && unsettled) {
|
|
1832
|
+
let resync;
|
|
1833
|
+
try {
|
|
1834
|
+
resync = await drive.connection.snapshot();
|
|
1835
|
+
}
|
|
1836
|
+
catch (error) {
|
|
1837
|
+
refuse("drive.command.rejected", "drive.schedule", `snapshot() after an unsettled schedule rejected: ${String(error)}`);
|
|
1838
|
+
}
|
|
1839
|
+
if (!(isRecord(resync) && carries(resync.calendar))) {
|
|
1840
|
+
refuse("drive.schedule.unsettled", "drive.schedule", `schedule was applied and ${settle}ms later no calendar-updated carries a follow-up at ${at}, and a snapshot does not either: applied says the follow-up is on the calendar, and it is not`);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1801
1845
|
// 5. End the call, where the agent may -- with the microphone muted, as agents do. The leg left
|
|
1802
1846
|
// open at audio end is the provider's to close, since the provider knows the instant the audio
|
|
1803
1847
|
// ended; the host's own closing report can only follow what it hears, is answered recorded, and
|
|
@@ -1853,7 +1897,7 @@ async function driveOneCall(drive) {
|
|
|
1853
1897
|
// provider. A conversation has no audio to end and no completing phase to wait for: a chat or an
|
|
1854
1898
|
// email, and a voice task offering no end-call, is completed from where it stands.
|
|
1855
1899
|
const completable = latestTask().phase === "completing" || latestTask().phase === "in-progress" || latestTask().phase === "paused";
|
|
1856
|
-
if (latestTask().wrapAllowance
|
|
1900
|
+
if (!(latestTask().completionMode === "provider-automatic" && latestTask().wrapAllowance === 0) && completable) {
|
|
1857
1901
|
const command = { type: "complete" };
|
|
1858
1902
|
const outcomes = isRecord(latestTask().capabilities) ? latestTask().capabilities.outcomes : undefined;
|
|
1859
1903
|
if (isRecord(outcomes) && outcomes.required === true && Array.isArray(outcomes.codes) && isRecord(outcomes.codes[0])) {
|
|
@@ -1864,7 +1908,7 @@ async function driveOneCall(drive) {
|
|
|
1864
1908
|
// the manifest stated. Past it the host resyncs: a snapshot still carrying the task is a task
|
|
1865
1909
|
// held open by a provider that said it was done, and the desk shows it as unsettled.
|
|
1866
1910
|
ruleEvaluated("drive.completion.unsettled");
|
|
1867
|
-
const settle = Number(drive.manifest.
|
|
1911
|
+
const settle = Number(drive.manifest.settleMs);
|
|
1868
1912
|
let unsettled = false;
|
|
1869
1913
|
const end = await ended({ ms: settle, onExpiry: () => { unsettled = true; } });
|
|
1870
1914
|
if (end !== undefined)
|
|
@@ -2083,6 +2127,35 @@ export function stillHost(report = { online: true }, guarantees = {}, mute) {
|
|
|
2083
2127
|
return { guarantees, ...(mute === undefined ? {} : { mute }), report: () => report, subscribe: () => () => undefined };
|
|
2084
2128
|
}
|
|
2085
2129
|
const usableLogin = (status) => status === "authenticated" || status === "refreshing";
|
|
2130
|
+
/**
|
|
2131
|
+
* A break one provider forces stops the agent everywhere: the host states a capacity of zero on
|
|
2132
|
+
* every other usable provider holding capacity -- taken, never refused, and nothing the agent can
|
|
2133
|
+
* cancel -- and on the forcing provider states nothing, since the break itself holds the agent
|
|
2134
|
+
* there. A provider whose login is not usable is left alone, as a break attempt leaves it.
|
|
2135
|
+
*/
|
|
2136
|
+
export function assertForcedBreakStopsTheRest(forcedOn, candidates, stopped) {
|
|
2137
|
+
if (!candidates.some(candidate => candidate.id === forcedOn))
|
|
2138
|
+
throw new Error(`${forcedOn} is not a provider the host knows`);
|
|
2139
|
+
const chosen = new Set(stopped);
|
|
2140
|
+
for (const id of stopped) {
|
|
2141
|
+
if (!candidates.some(candidate => candidate.id === id))
|
|
2142
|
+
throw new Error(`${id} is not a provider the host knows`);
|
|
2143
|
+
}
|
|
2144
|
+
if (chosen.has(forcedOn))
|
|
2145
|
+
throw new Error(`${forcedOn} forced the break and holds the agent itself: its capacity is not restated`);
|
|
2146
|
+
for (const candidate of candidates) {
|
|
2147
|
+
if (candidate.id === forcedOn)
|
|
2148
|
+
continue;
|
|
2149
|
+
const expected = usableLogin(candidate.authentication) && candidate.holdsCapacity;
|
|
2150
|
+
if (expected && !chosen.has(candidate.id)) {
|
|
2151
|
+
throw new Error(`${candidate.id} can give the agent work and must be stopped: capacity 0 stated`);
|
|
2152
|
+
}
|
|
2153
|
+
if (!expected && chosen.has(candidate.id)) {
|
|
2154
|
+
const why = usableLogin(candidate.authentication) ? "holds no capacity" : `is ${candidate.authentication}`;
|
|
2155
|
+
throw new Error(`${candidate.id} ${why} and is not stopped: there is nothing to stop`);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2086
2159
|
/**
|
|
2087
2160
|
* A break attempt asks every connected provider from which the agent can currently receive work.
|
|
2088
2161
|
* A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
|
package/dist/validation.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface TaskValidationContext {
|
|
|
39
39
|
autoAcceptTasks?: boolean;
|
|
40
40
|
/** Whether the manifest declares `dialOutcomes`. A task that may dial needs it to; absent, the question is not asked. */
|
|
41
41
|
dialOutcomesDeclared?: boolean;
|
|
42
|
+
/** Whether the manifest declares the `calendar` idle capability. A task that may schedule needs it to; absent, the question is not asked. */
|
|
43
|
+
calendarDeclared?: boolean;
|
|
42
44
|
/**
|
|
43
45
|
* The values the queue locked on this login's tasks -- a party's number or email -- as whoever
|
|
44
46
|
* runs the validator knows them. Where a task's party stands locked, no other field of it may
|