@xema/omni-protocol 0.1.98 → 0.1.99

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 CHANGED
@@ -232,25 +232,6 @@ export interface AuthenticationFailure {
232
232
  /** Names a declared credentials field when the failure belongs to one. */
233
233
  field?: string;
234
234
  }
235
- /** What a lead may do with their team. Declared by presence. */
236
- export interface TeamCapabilities {
237
- /**
238
- * This lead may act on their team's breaks through `executeTeamBreak` -- force-break, end-forced-break, decide-break-request,
239
- * set-break-policy -- as far as the provider supports; a command it lacks answers
240
- * `omni.capability-not-enabled`. Requires `executeTeamBreak`.
241
- */
242
- breakControl?: true;
243
- /** This lead may join a member's call on request. Requires `executeTeamLeadAssist`. */
244
- leadAssistControl?: true;
245
- /**
246
- * This lead may listen to a member's call unasked, in these modes and no others. Coach and
247
- * join-call begin with silent listening, so the list always includes `listen`. Nothing of it reaches the
248
- * member's task. Requires `executeTeamListen`.
249
- */
250
- listeningControl?: ListeningMode[];
251
- /** This lead sets the team's policy per capability -- on, off, or the agent's -- within what the queue allows. Requires `executeTeamPolicy`. */
252
- policyControl?: true;
253
- }
254
235
  /**
255
236
  * How a lead listening to a member's call is heard: `listen` hears both sides in silence,
256
237
  * `coach` is heard by the agent alone, `join-call` by everyone on the call.
@@ -267,8 +248,14 @@ export interface UserCapabilities {
267
248
  breaks?: true;
268
249
  /** The choices the team left to this person, with where each stands. Omitted when there are none. Requires `setPreference`. */
269
250
  preferences?: AgentPreference[];
270
- /** This login leads a team: a `TeamMembers` object is published to it on every snapshot, including `members: []`. */
271
- team?: TeamCapabilities;
251
+ /**
252
+ * This login leads a team. The flag alone turns on the team feature in the agent application:
253
+ * the lead sees their members with their tasks and stats, and acts on them through `executeTeam`
254
+ * with the authority the flag carries. No per-action permission rides beside it. While the lead
255
+ * has the feature on, a `TeamMembers` object is published to them on every snapshot, `members: []`
256
+ * included; to nobody else, ever.
257
+ */
258
+ lead?: true;
272
259
  }
273
260
  /**
274
261
  * Only `authenticated` and `refreshing` know who the agent is and what they may do, and only
@@ -668,6 +655,7 @@ export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ?
668
655
  hold?: Lockable<true>;
669
656
  /** The provider ends the caller connection and agent-added channels owned by this interaction, including inherited channels after transfer/takeover. Interaction completion remains separate. */
670
657
  endCall?: Lockable<true>;
658
+ terminateCall?: Lockable<true>;
671
659
  /** Connect back to the party while `completing`, whoever placed the call; the task returns to `in-progress`. */
672
660
  connectBack?: Lockable<true>;
673
661
  coldTransfer?: Lockable<DestinationDirectory>;
@@ -875,27 +863,13 @@ export type TaskLeadAssist = {
875
863
  leadId: UserId;
876
864
  });
877
865
  /**
878
- * On the lead's own task for a call they joined: which member asked, with their note. Its
879
- * presence is what makes `lead-assist` `take-over-call` and `leave` issuable.
880
- */
881
- export interface TaskAssisting {
882
- memberId: UserId;
883
- /** The member's assignment the lead joined: the lead's task has an assignment of its own. */
884
- assignmentId: AssignmentId;
885
- note?: string;
886
- since: IsoTimestamp;
887
- }
888
- /**
889
- * On the lead's own task while they listen to a member's call: whose call, which call, and how the
890
- * lead is heard, restated on every change of mode. Nothing of it reaches the member's task. There
891
- * is no take-over here; a lead who wants the call uses lead assist. It is the lead's only task:
892
- * a lead listens while holding no work of their own, idle or on a break of a kind in
893
- * `LISTENING_BREAK_KINDS`.
866
+ * On a task that reached this agent by a lead's take-over: which member the call was taken from,
867
+ * and when. The provider moves the call from one agent to another, and the lead is the new agent,
868
+ * so the task is an ordinary assignment with the call's history -- offered whatever break the lead
869
+ * is on, counted against no capacity, provided their voice channel is free.
894
870
  */
895
- export interface TaskListening {
871
+ export interface TaskTakenOver {
896
872
  memberId: UserId;
897
- assignmentId: AssignmentId;
898
- mode: ListeningMode;
899
873
  since: IsoTimestamp;
900
874
  }
901
875
  /**
@@ -995,15 +969,13 @@ export type Task<C extends Channel = Channel> = {
995
969
  };
996
970
  onCall?: OnCall[];
997
971
  leadAssist?: TaskLeadAssist;
998
- assisting?: TaskAssisting;
999
- listening?: TaskListening;
972
+ takenOver?: TaskTakenOver;
1000
973
  media?: TaskMediaState;
1001
974
  } : {
1002
975
  recording?: never;
1003
976
  onCall?: never;
1004
977
  leadAssist?: never;
1005
- assisting?: never;
1006
- listening?: never;
978
+ takenOver?: never;
1007
979
  media?: never;
1008
980
  });
1009
981
  /**
@@ -1022,7 +994,7 @@ export type TaskOutcome = {
1022
994
  type: "transferred";
1023
995
  destinationId?: string;
1024
996
  }
1025
- /** A lead took the call over: the agent's task ends and the lead's continues. Named by the lead, since a lead is not a directory item. */
997
+ /** A lead took the call over and the agent has wrapped it: the outcome at the agent's own completion. Named by the lead, since a lead is not a directory item. */
1026
998
  | {
1027
999
  type: "taken-over";
1028
1000
  leadId: UserId;
@@ -1037,16 +1009,12 @@ export type TaskOutcome = {
1037
1009
  | {
1038
1010
  type: "expired";
1039
1011
  phase: "pending" | "confirmed" | "preview";
1040
- }
1041
- /** This agent left a call that continues without them: a lead who joined and dropped. */
1042
- | {
1043
- type: "left";
1044
1012
  } | {
1045
1013
  type: "failed";
1046
1014
  failure: ProtocolFailure;
1047
1015
  };
1048
1016
  export declare const TASK_COMMAND_NAMES: {
1049
- readonly voice: readonly ["answer", "decline", "call", "hold", "resume", "end-call", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
1017
+ readonly voice: readonly ["answer", "decline", "call", "hold", "resume", "end-call", "terminate-call", "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"];
1050
1018
  readonly chat: readonly ["accept", "decline", "pause", "resume", "complete"];
1051
1019
  readonly email: readonly ["accept", "decline", "complete"];
1052
1020
  };
@@ -1069,10 +1037,14 @@ export type VoiceTaskCommand = {
1069
1037
  } | {
1070
1038
  type: "resume";
1071
1039
  }
1072
- /** End the caller connection and all agent-added channels owned or inherited by this interaction. Wrap/completion remain separate. Gated by `endCall`. */
1040
+ /** End the agent's channel and every channel the agent added; the caller continues on the provider's path. Gated by `endCall`. */
1073
1041
  | {
1074
1042
  type: "end-call";
1075
1043
  }
1044
+ /** End the caller channel. Gated by `terminateCall`. */
1045
+ | {
1046
+ type: "terminate-call";
1047
+ }
1076
1048
  /** Issuable only in `completing`, under the `connectBack` capability. Dials the party's own number, so it names none. */
1077
1049
  | {
1078
1050
  type: "connect-back";
@@ -1113,16 +1085,6 @@ export type VoiceTaskCommand = {
1113
1085
  type: "lead-assist";
1114
1086
  action: "cancel";
1115
1087
  }
1116
- /** The lead keeps the customer; the agent's task ends `transferred`. Needs `Task.assisting`. */
1117
- | {
1118
- type: "lead-assist";
1119
- action: "take-over-call";
1120
- }
1121
- /** The lead drops; the agent continues. The lead's task ends `left`. Needs `Task.assisting`. */
1122
- | {
1123
- type: "lead-assist";
1124
- action: "leave";
1125
- }
1126
1088
  /** Dial the directory item `destinationId` into the call. Gated by `conference`. */
1127
1089
  | {
1128
1090
  type: "conference";
@@ -1230,12 +1192,6 @@ export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "trai
1230
1192
  * problem one level up. `other` matches nothing, including another provider's `other`.
1231
1193
  */
1232
1194
  export type BreakKind = (typeof BREAK_KINDS)[number];
1233
- /**
1234
- * The breaks during which a lead may listen to a member's call: work of another sort, not rest.
1235
- * A listening task is the one task a break in effect may hold, and only on one of these.
1236
- */
1237
- export declare const LISTENING_BREAK_KINDS: readonly ["coaching", "administrative", "training"];
1238
- export declare const breakKindAllowsListening: (kind: BreakKind | undefined) => boolean;
1239
1195
  export interface BreakReason {
1240
1196
  id: string;
1241
1197
  label: string;
@@ -1309,24 +1265,65 @@ export interface TeamMember {
1309
1265
  since?: IsoTimestamp;
1310
1266
  /** A request in flight or a grant not yet in effect. `not-requested` is absence, and `on-break` is `availability: "on-break"`. */
1311
1267
  break?: Extract<BreakStatus, "awaiting-approval" | "granted" | "starting-after-task">;
1268
+ /** The member's open tasks, trimmed, kept current by the provider; `[]` when they hold none, omitted only where the provider cannot see them. */
1269
+ tasks?: MemberTask[];
1270
+ /** Present while this lead is on the member's call, listening, coaching or joined; absent otherwise. */
1271
+ listening?: MemberListening;
1312
1272
  }
1313
- /** A member asking this lead to join their call. */
1273
+ /** A member asking this lead to join their call, named by the member and the assignment. */
1314
1274
  export interface LeadRequest {
1315
- id: string;
1316
1275
  memberId: UserId;
1317
1276
  assignmentId: AssignmentId;
1318
1277
  note?: string;
1319
1278
  since: IsoTimestamp;
1320
1279
  }
1321
1280
  /**
1322
- * The login is the permission: published to a login that declares `capabilities.team`, on every
1323
- * snapshot, and to nobody else. What the lead may do with it is on the login too, not here.
1281
+ * A member's task as the lead sees it: the same task the member's desk holds, trimmed by the
1282
+ * provider. The assignment, what the call is and where it stands, the room and the full history
1283
+ * are always here; the workspace -- browsers, controls, completion terms -- is the member's and
1284
+ * travels only if the provider chooses to send it.
1285
+ */
1286
+ export type MemberTask<C extends Channel = Channel> = {
1287
+ assignmentId: AssignmentId;
1288
+ title: string;
1289
+ channel: C;
1290
+ taskType: string;
1291
+ phase: TaskPhase;
1292
+ party?: Contact;
1293
+ reference?: string;
1294
+ attributes?: TaskAttribute[];
1295
+ history?: TaskHistory;
1296
+ capabilities?: TaskCapabilities<C>;
1297
+ capabilitySource?: CapabilitySource;
1298
+ browsers?: TaskBrowser[];
1299
+ completionMode?: CompletionMode;
1300
+ wrapAllowance?: DurationSeconds;
1301
+ } & (C extends "voice" ? {
1302
+ onCall?: OnCall[];
1303
+ leadAssist?: TaskLeadAssist;
1304
+ takenOver?: TaskTakenOver;
1305
+ media?: TaskMediaState;
1306
+ } : {
1307
+ onCall?: never;
1308
+ leadAssist?: never;
1309
+ takenOver?: never;
1310
+ media?: never;
1311
+ });
1312
+ /** The lead on this member's call, in the mode they are heard, since when. */
1313
+ export interface MemberListening {
1314
+ mode: ListeningMode;
1315
+ since: IsoTimestamp;
1316
+ }
1317
+ /**
1318
+ * The lead's view of their team: published to a login that declares `capabilities.lead` while the
1319
+ * lead has the team feature on, on every snapshot, and to nobody else. The provider keeps it
1320
+ * current: when a member holds, mutes or is transferred, the list is republished whole.
1324
1321
  */
1325
1322
  export interface TeamMembers {
1326
1323
  members: TeamMember[];
1327
- /** Omitted when the login lacks `team.leadAssistControl`; `[]` when nobody is asking. */
1328
- requests?: LeadRequest[];
1329
- /** The team's policy per capability, as it stands. Present exactly when the login declares `team.policyControl`. */
1324
+ /** Members asking a lead to join their call; `[]` when nobody is asking. */
1325
+ requests: LeadRequest[];
1326
+ /** The team's policy per capability, as it stands. Present where the provider offers policy control; absent where it does not. */
1330
1327
  policies?: TeamPolicies;
1331
1328
  }
1332
1329
  /** A capability a team policy can name: any task control, new call, or a skill by its provider id. */
@@ -1339,42 +1336,57 @@ export interface TeamPolicy extends Resolved {
1339
1336
  }
1340
1337
  export type TeamPolicies = Partial<Record<PolicyKey, TeamPolicy>>;
1341
1338
  export type TeamPolicyCommand = {
1342
- type: "set";
1339
+ type: "set-policy";
1343
1340
  capability: PolicyKey;
1344
1341
  setting: TeamPolicySetting;
1345
1342
  };
1346
- export interface TeamPolicyCommandRequest {
1347
- command: TeamPolicyCommand;
1348
- }
1349
- export type TeamLeadAssistCommand = {
1343
+ /**
1344
+ * The team feature on or off. The lead flag enables it; the lead switches it on the fly, and the
1345
+ * provider needs to know: while it is on, team events scoped to the lead's team arrive and the
1346
+ * lead may act; while it is off, the lead is an ordinary agent and nothing of the team reaches them.
1347
+ */
1348
+ export type TeamFeatureCommand = {
1349
+ type: "lead-features";
1350
+ enabled: boolean;
1351
+ };
1352
+ /**
1353
+ * The lead's acts on a member's call, each naming the member and, where the lead has it, the
1354
+ * member's assignment; the provider resolves the member's current assignment from the member alone.
1355
+ * `join` answers a member's request and `decline` refuses it; `listen` starts unasked, silent,
1356
+ * `coach` is heard by the member alone and `join-call` by everyone; `leave` takes the lead's channel
1357
+ * off the call however it got there; `take-over-call` moves the call to the lead as its new agent,
1358
+ * from a joined call or a listened one alike. One member's call at a time.
1359
+ */
1360
+ export type TeamCallCommand = {
1350
1361
  type: "join";
1351
- requestId: string;
1362
+ memberId: UserId;
1363
+ assignmentId?: AssignmentId;
1352
1364
  } | {
1353
1365
  type: "decline";
1354
- requestId: string;
1366
+ memberId: UserId;
1367
+ assignmentId?: AssignmentId;
1355
1368
  reason?: string;
1356
- };
1357
- export interface TeamLeadAssistCommandRequest {
1358
- command: TeamLeadAssistCommand;
1359
- }
1360
- /**
1361
- * A lead listening to a member's call. `listen` starts one, silent, on a member from the team member list;
1362
- * `coach` and `join-call` change how the standing one is heard; `leave` ends it. One member at a
1363
- * time, and only the modes the login's `listeningControl` lists.
1364
- */
1365
- export type TeamListenCommand = {
1369
+ } | {
1366
1370
  type: "listen";
1367
1371
  memberId: UserId;
1372
+ assignmentId?: AssignmentId;
1368
1373
  } | {
1369
1374
  type: "coach";
1375
+ memberId: UserId;
1376
+ assignmentId?: AssignmentId;
1370
1377
  } | {
1371
1378
  type: "join-call";
1379
+ memberId: UserId;
1380
+ assignmentId?: AssignmentId;
1372
1381
  } | {
1373
1382
  type: "leave";
1383
+ memberId: UserId;
1384
+ assignmentId?: AssignmentId;
1385
+ } | {
1386
+ type: "take-over-call";
1387
+ memberId: UserId;
1388
+ assignmentId?: AssignmentId;
1374
1389
  };
1375
- export interface TeamListenCommandRequest {
1376
- command: TeamListenCommand;
1377
- }
1378
1390
  export type TeamBreakCommand = {
1379
1391
  type: "decide-break-request";
1380
1392
  memberId: UserId;
@@ -1397,15 +1409,17 @@ export type TeamBreakCommand = {
1397
1409
  type: "end-forced-break";
1398
1410
  memberId: UserId;
1399
1411
  };
1412
+ /** Every lead act, as `execute` takes a `TaskCommand`: discriminated by `type`. */
1413
+ export type TeamCommand = TeamFeatureCommand | TeamBreakCommand | TeamCallCommand | TeamPolicyCommand;
1414
+ export interface TeamCommandRequest {
1415
+ command: TeamCommand;
1416
+ }
1400
1417
  export type TeamCommandResult = {
1401
1418
  status: "applied";
1402
1419
  } | {
1403
1420
  status: "failed";
1404
1421
  failure: ProtocolFailure;
1405
1422
  };
1406
- export interface TeamBreakCommandRequest {
1407
- command: TeamBreakCommand;
1408
- }
1409
1423
  /**
1410
1424
  * The task's real-time audio as the provider holds it: `started` while audio should be attached,
1411
1425
  * `ended` once primary interaction's audio ended, and the field omitted while none should be. The
@@ -1574,6 +1588,20 @@ export type ProviderEvent<C extends Channel = Channel> = {
1574
1588
  } | {
1575
1589
  type: "task-media-ended";
1576
1590
  assignmentId: AssignmentId;
1591
+ }
1592
+ /**
1593
+ * The lead's channel is in a member's call -- listening, coaching or joined -- and its audio should
1594
+ * attach: on a softphone the application opens media on the member's assignment as it does for a
1595
+ * task. Voice, to a login that declares `lead`, never to the member.
1596
+ */
1597
+ | {
1598
+ type: "team-media-started";
1599
+ memberId: UserId;
1600
+ assignmentId: AssignmentId;
1601
+ } | {
1602
+ type: "team-media-ended";
1603
+ memberId: UserId;
1604
+ assignmentId: AssignmentId;
1577
1605
  } | {
1578
1606
  type: "task-ended";
1579
1607
  assignmentId: AssignmentId;
@@ -1687,14 +1715,8 @@ export interface Connection<C extends Channel = Channel> {
1687
1715
  commitBreak?(): Promise<BreakCommitResult>;
1688
1716
  cancelBreak?(): Promise<BreakCancelResult>;
1689
1717
  endBreak?(): Promise<BreakEndResult>;
1690
- /** Required when the login declares `capabilities.team.breakControl`. */
1691
- executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
1692
- /** Required when the login declares `capabilities.team.leadAssistControl`. */
1693
- executeTeamLeadAssist?(request: TeamLeadAssistCommandRequest): Promise<TeamCommandResult>;
1694
- /** Required when the login declares `capabilities.team.listeningControl`. */
1695
- executeTeamListen?(request: TeamListenCommandRequest): Promise<TeamCommandResult>;
1696
- /** Required when the login declares `capabilities.team.policyControl`. */
1697
- executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
1718
+ /** Required when the login declares `capabilities.lead`: every lead act, on the team surface and nowhere else. */
1719
+ executeTeam?(request: TeamCommandRequest): Promise<TeamCommandResult>;
1698
1720
  /** Required of a voice adapter whose manifest lists `softphone`: on one, the call's audio lands in Omni. */
1699
1721
  openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
1700
1722
  /** Required when the login declares `capabilities.preferences`: the person's own choice, kept by the provider and republished as `authenticated`. */
@@ -1791,7 +1813,7 @@ export interface BrowserSessionKeyInput {
1791
1813
  * Whether two logins declare the same capabilities, field by field. The comparison an adapter
1792
1814
  * makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
1793
1815
  * guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
1794
- * not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
1816
+ * not the thing being compared. Key order does not matter, and a withdrawn `lead` is a change.
1795
1817
  */
1796
1818
  export declare function sameCapabilities(a: UserCapabilities, b: UserCapabilities): boolean;
1797
1819
  /**
package/dist/index.js CHANGED
@@ -76,7 +76,7 @@ export const CAPABILITY_SOURCES = ["queue", "ungoverned", "undetermined"];
76
76
  // Task commands.
77
77
  // ---------------------------------------------------------------------------
78
78
  export const TASK_COMMAND_NAMES = {
79
- voice: ["answer", "decline", "call", "hold", "resume", "end-call",
79
+ voice: ["answer", "decline", "call", "hold", "resume", "end-call", "terminate-call",
80
80
  "connect-back", "transfer", "lead-assist", "conference", "recording", "complete"],
81
81
  chat: ["accept", "decline", "pause", "resume", "complete"],
82
82
  email: ["accept", "decline", "complete"],
@@ -95,12 +95,6 @@ export const BREAK_KINDS = [
95
95
  "short-break", "meal", "rest", "training", "coaching",
96
96
  "meeting", "administrative", "technical", "personal", "other",
97
97
  ];
98
- /**
99
- * The breaks during which a lead may listen to a member's call: work of another sort, not rest.
100
- * A listening task is the one task a break in effect may hold, and only on one of these.
101
- */
102
- export const LISTENING_BREAK_KINDS = ["coaching", "administrative", "training"];
103
- export const breakKindAllowsListening = (kind) => kind !== undefined && LISTENING_BREAK_KINDS.includes(kind);
104
98
  export const OMNI_FAILURE_CODES = [
105
99
  "omni.not-authenticated",
106
100
  "omni.capability-not-enabled",
@@ -177,19 +171,15 @@ export function historyStepExpectsAPerson(step) {
177
171
  * Whether two logins declare the same capabilities, field by field. The comparison an adapter
178
172
  * makes before republishing `authenticated`: capabilities are current, not fixed, and the natural
179
173
  * guard -- comparing the identity -- never fires on a demotion, because the thing that changed is
180
- * not the thing being compared. Key order does not matter, and `team: {}` is not `team` absent.
174
+ * not the thing being compared. Key order does not matter, and a withdrawn `lead` is a change.
181
175
  */
182
176
  export function sameCapabilities(a, b) {
183
- const modes = (team) => team?.listeningControl === undefined ? undefined : [...team.listeningControl].sort().join(",");
184
177
  // Every field, so a capability added later cannot be missed here: `satisfies` pins the field
185
- // lists to the types, and a new key is a compile error until it is compared.
186
- const teamKeys = { breakControl: true, leadAssistControl: true, listeningControl: true, policyControl: true };
187
- void { breaks: true, preferences: true, team: true };
178
+ // list to the type, and a new key is a compile error until it is compared.
179
+ void { breaks: true, preferences: true, lead: true };
188
180
  const preferences = (list) => list === undefined ? undefined : list.map(p => [p.id, p.label, p.enabled, p.setBy, p.lockedBy ?? "", p.reason ?? ""].join("\u0000")).sort().join("\u0001");
189
181
  return a.breaks === b.breaks &&
190
- (a.team === undefined) === (b.team === undefined) &&
191
- Object.keys(teamKeys).every(key => key === "listeningControl" || a.team?.[key] === b.team?.[key]) &&
192
- modes(a.team) === modes(b.team) &&
182
+ a.lead === b.lead &&
193
183
  preferences(a.preferences) === preferences(b.preferences);
194
184
  }
195
185
  /**
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.assisting", "task.listening", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
10
+ declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.history", "task.onCall", "task.leadAssist", "task.takenOver", "task.media", "task.acceptance", "task.outcomes", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.forced", "team.members", "team.requests", "team.tasks", "team.listening", "contacts", "scheduledActivities", "team.policies"];
11
11
  export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
12
12
  export interface AdapterContractResult {
13
13
  events: ProviderEventEnvelope[];
package/dist/testing.js CHANGED
@@ -14,8 +14,7 @@ const STATE_SUBJECTS = [
14
14
  "task.history",
15
15
  "task.onCall",
16
16
  "task.leadAssist",
17
- "task.assisting",
18
- "task.listening",
17
+ "task.takenOver",
19
18
  "task.media",
20
19
  "task.acceptance",
21
20
  "task.outcomes",
@@ -26,6 +25,8 @@ const STATE_SUBJECTS = [
26
25
  "break.forced",
27
26
  "team.members",
28
27
  "team.requests",
28
+ "team.tasks",
29
+ "team.listening",
29
30
  "contacts",
30
31
  "scheduledActivities",
31
32
  "team.policies",
@@ -35,7 +36,7 @@ const STATE_SUBJECTS = [
35
36
  const EVENT_TYPES = {
36
37
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
37
38
  "task-media-started": true, "task-media-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
38
- "team-updated": true, "contacts-updated": true, "calendar-updated": true,
39
+ "team-updated": true, "team-media-started": true, "team-media-ended": true, "contacts-updated": true, "calendar-updated": true,
39
40
  };
40
41
  const CONTRACT_SUBJECTS = [
41
42
  ...STATE_SUBJECTS,
@@ -59,10 +60,8 @@ function observeTask(value, seen) {
59
60
  seen.add("task.onCall");
60
61
  if (value.leadAssist !== undefined)
61
62
  seen.add("task.leadAssist");
62
- if (value.assisting !== undefined)
63
- seen.add("task.assisting");
64
- if (value.listening !== undefined)
65
- seen.add("task.listening");
63
+ if (value.takenOver !== undefined)
64
+ seen.add("task.takenOver");
66
65
  if (value.media !== undefined)
67
66
  seen.add("task.media");
68
67
  if (value.acceptance !== undefined)
@@ -95,6 +94,15 @@ function observeTeam(value, seen) {
95
94
  seen.add("team.members");
96
95
  if (some(value.requests))
97
96
  seen.add("team.requests");
97
+ if (Array.isArray(value.members))
98
+ for (const member of value.members) {
99
+ if (!isRecord(member))
100
+ continue;
101
+ if (some(member.tasks))
102
+ seen.add("team.tasks");
103
+ if (member.listening !== undefined)
104
+ seen.add("team.listening");
105
+ }
98
106
  if (isRecord(value.policies) && Object.keys(value.policies).length > 0)
99
107
  seen.add("team.policies");
100
108
  }
@@ -229,14 +237,8 @@ export async function exerciseAdapter(adapter, context, options = {}) {
229
237
  requireMethod(on, method, "the login declares capabilities.breaks");
230
238
  }
231
239
  }
232
- if (capabilities.team?.breakControl === true)
233
- requireMethod(on, "executeTeamBreak", "the login declares capabilities.team.breakControl");
234
- if (capabilities.team?.leadAssistControl === true)
235
- requireMethod(on, "executeTeamLeadAssist", "the login declares capabilities.team.leadAssistControl");
236
- if (capabilities.team?.listeningControl !== undefined)
237
- requireMethod(on, "executeTeamListen", "the login declares capabilities.team.listeningControl");
238
- if (capabilities.team?.policyControl === true)
239
- requireMethod(on, "executeTeamPolicy", "the login declares capabilities.team.policyControl");
240
+ if (capabilities.lead === true)
241
+ requireMethod(on, "executeTeam", "the login declares capabilities.lead");
240
242
  if (some(capabilities.preferences))
241
243
  requireMethod(on, "setPreference", "the login declares capabilities.preferences");
242
244
  };
@@ -671,11 +673,28 @@ function userIdsIn(snapshot) {
671
673
  add(entry.userId);
672
674
  if (isRecord(t.leadAssist))
673
675
  add(t.leadAssist.leadId);
674
- if (isRecord(t.assisting))
675
- add(t.assisting.memberId);
676
- if (isRecord(t.listening)) {
677
- add(t.listening.memberId);
678
- add(t.listening.agentId);
676
+ if (isRecord(t.takenOver))
677
+ add(t.takenOver.memberId);
678
+ }
679
+ if (isRecord(team)) {
680
+ for (const member of Array.isArray(team.members) ? team.members : []) {
681
+ if (!isRecord(member) || !Array.isArray(member.tasks))
682
+ continue;
683
+ for (const task of member.tasks) {
684
+ const t = task;
685
+ if (isRecord(t.history) && Array.isArray(t.history.steps))
686
+ for (const step of t.history.steps)
687
+ if (isRecord(step))
688
+ add(step.by);
689
+ if (Array.isArray(t.onCall))
690
+ for (const entry of t.onCall)
691
+ if (isRecord(entry))
692
+ add(entry.userId);
693
+ if (isRecord(t.leadAssist))
694
+ add(t.leadAssist.leadId);
695
+ if (isRecord(t.takenOver))
696
+ add(t.takenOver.memberId);
697
+ }
679
698
  }
680
699
  }
681
700
  return [...ids];
@@ -683,8 +702,7 @@ function userIdsIn(snapshot) {
683
702
  const teamNamesUsers = (team) => isRecord(team) && (some(team.members) || some(team.requests));
684
703
  const taskNamesUsers = (task) => isRecord(task) && ((isRecord(task.history) && Array.isArray(task.history.steps) && task.history.steps.some(step => isRecord(step) && step.by !== undefined)) ||
685
704
  (isRecord(task.leadAssist) && task.leadAssist.leadId !== undefined) ||
686
- isRecord(task.assisting) ||
687
- isRecord(task.listening));
705
+ isRecord(task.takenOver));
688
706
  /** Whether an event publishes a `UserId`, on a team member list, a task, or the snapshot a reconnect carries. */
689
707
  function eventNamesUsers(envelope) {
690
708
  const event = isRecord(envelope) ? envelope.event : undefined;
@@ -815,11 +833,7 @@ export function assertCapabilityWithdrawal(states, snapshot, manifest) {
815
833
  const before = first.capabilities;
816
834
  const after = last.capabilities;
817
835
  const withdrawn = (before.breaks === true && after.breaks !== true) ||
818
- (before.team !== undefined && after.team === undefined) ||
819
- (before.team?.breakControl === true && after.team?.breakControl !== true) ||
820
- (before.team?.leadAssistControl === true && after.team?.leadAssistControl !== true) ||
821
- (before.team?.listeningControl !== undefined && after.team?.listeningControl === undefined) ||
822
- (before.team?.policyControl === true && after.team?.policyControl !== true) ||
836
+ (before.lead === true && after.lead !== true) ||
823
837
  (before.preferences ?? []).some(was => !(after.preferences ?? []).some(now => now.id === was.id));
824
838
  if (!withdrawn) {
825
839
  throw new Error("Capability withdrawal must end with at least one capability the first login declared withdrawn");
@@ -1,4 +1,4 @@
1
- import { type RecordingState, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
1
+ import { type RecordingState, type Channel, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
2
2
  export type { ProtocolViolation } from "./index.js";
3
3
  export declare class ProtocolConformanceError extends Error {
4
4
  readonly violations: readonly ProtocolViolation[];
@@ -45,16 +45,23 @@ export interface TaskValidationContext {
45
45
  * carry one of these. Unknown to a host, which never sees the value, and then unchecked.
46
46
  */
47
47
  locked?: readonly string[];
48
+ /**
49
+ * A member's task on the team member list, trimmed by the provider: the workspace -- controls,
50
+ * their source, browsers, completion terms -- is the member's and may be absent; what is present
51
+ * is held to the same rules as on the member's own desk.
52
+ */
53
+ member?: true;
48
54
  }
49
55
  export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
50
56
  /** Checks break status against the full currently retained task set, after a transaction. */
51
57
  export declare function validateBreakStatus(state: unknown, tasks: unknown, path?: string): ProtocolViolation[];
52
58
  /**
53
- * Checks lead break dispatch using current authentication, transport, team member list (`team`) and,
54
- * for force-break/end-forced-break, the target's full `memberBreak`. The provider must authorize the target
55
- * and recheck the decision atomically; a team member list is not authority to act after it has changed.
59
+ * Checks a lead command before dispatch, against the current authentication, transport and team
60
+ * member list (`team`) and, for force-break and end-forced-break, the target's full `memberBreak`.
61
+ * The authority is the login's `lead` flag; the provider rechecks the target and the state
62
+ * atomically, since a team member list is not authority to act after it has changed.
56
63
  */
57
- export declare function validateTeamBreakCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
64
+ export declare function validateTeamCommand(request: unknown, context: unknown, path?: string): ProtocolViolation[];
58
65
  /** The four agent break methods; this is a validation API, not a wire command. */
59
66
  export type BreakMethod = "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak";
60
67
  /**
@@ -90,6 +97,13 @@ export interface ReaderContext {
90
97
  levels?: readonly string[];
91
98
  /** The login's `loginId`. A snapshot or event naming another belongs to a login that is gone. */
92
99
  loginId?: string;
100
+ /**
101
+ * Whether the lead has the team feature on, as the application last told the provider. On by
102
+ * default for a lead; off, no team member list is owed or expected. Unknown to a caller without it.
103
+ */
104
+ leadFeatures?: boolean;
105
+ /** The provider's channel, for a member's task that states none. Filled from the manifest by `validateSnapshot` and `validateEventEnvelope`. */
106
+ channel?: Channel;
93
107
  /**
94
108
  * The values the queue locked on this login's tasks, as whoever runs the validator knows them:
95
109
  * a conformance run states them, a host never has them. Where a task's party stands locked, no
@@ -135,7 +149,7 @@ export declare function validateTaskCommandRequest(request: unknown, task: unkno
135
149
  */
136
150
  export declare function validateTaskCommand(command: unknown, task?: unknown, path?: string, taskContext?: Omit<TaskValidationContext, "channel">): ProtocolViolation[];
137
151
  /** The connection methods whose results `validateResult` knows. */
138
- export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamLeadAssist" | "executeTeamListen" | "openMedia" | "setPreference" | "recordStep" | "executeTeamPolicy";
152
+ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeam" | "openMedia" | "setPreference" | "recordStep";
139
153
  /**
140
154
  * Validates what a connection method answered. A result is untrusted for the same reason a
141
155
  * snapshot is: it comes from an adapter that may be compiled against another version, and Omni