@xema/omni-protocol 0.1.32 → 0.1.34

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
@@ -26,6 +26,12 @@ export type IsoTimestamp = string;
26
26
  export type UserId = string;
27
27
  /** A non-empty, opaque task identifier unique within one provider. Scope it with `taskKey()`. */
28
28
  export type TaskId = string;
29
+ /**
30
+ * The host's identity for one dial, minted before the command leaves and unique across the
31
+ * installation. A dial is the one command whose outcome arrives later and apart, so the provider
32
+ * names this on the outcome and the host has already placed it. Never shown to the agent.
33
+ */
34
+ export type DialId = string;
29
35
  /** A non-negative whole number of seconds. */
30
36
  export type DurationSeconds = number;
31
37
  /** New channels require a later protocol version. */
@@ -68,6 +74,14 @@ export type DialDestinations = "contacts-only" | "any-number";
68
74
  export interface DialCapability {
69
75
  destinations: DialDestinations;
70
76
  }
77
+ /**
78
+ * How a dial ended, as the switch distinguishes it. `answered` is the one success; the rest are
79
+ * the ways a destination was not reached -- `cancelled` because the dialler called it off before
80
+ * anyone answered, not because the destination failed to. A manifest declares which of these its
81
+ * platform can tell apart (`dialOutcomes`), and a `dial-outcome` carries only a declared one.
82
+ */
83
+ export type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected" | "cancelled";
84
+ export declare const DIAL_OUTCOMES: readonly ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled"];
71
85
  /** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
72
86
  export declare const IDLE_CAPABILITIES: readonly ["dial", "personalBrowser", "calendar", "contacts"];
73
87
  export type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
@@ -113,6 +127,12 @@ export interface Manifest<C extends Channel = Channel> {
113
127
  taskTypePresentation?: Record<string, TaskTypePresentation>;
114
128
  /** The organisation's whole ladder, stated outright, `person` included. Omitted for the typical four, `DEFAULT_LEVELS`. */
115
129
  orgLevels?: LevelDeclaration[];
130
+ /**
131
+ * The outcomes this platform distinguishes when a dial ends, `answered` and at least one way of
132
+ * not reaching the destination. Required of a provider that dials at all -- an idle dialpad, or
133
+ * a task that may transfer, conference, or call back -- and forbidden off voice, where nothing dials.
134
+ */
135
+ dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
116
136
  /**
117
137
  * The provider takes running reports of a host-performed step -- `recordStep` with `seconds`
118
138
  * so far and no `ended`. Absent, the host sends exactly two reports per leg, when it began and
@@ -520,6 +540,12 @@ export interface TaskHandlingHistory {
520
540
  export interface TaskHandlingStep {
521
541
  step: HandlingStep;
522
542
  at: IsoTimestamp;
543
+ /**
544
+ * On a step that dialled -- `transferred`, `conferenced`, `unanswered` -- the host's identity for
545
+ * that dial when a host placed it, and the address it went to. Neither belongs on any other step.
546
+ */
547
+ dialId?: DialId;
548
+ destination?: string;
523
549
  /**
524
550
  * Reported, never derived. An entry may be written while its leg is still running, so there is
525
551
  * no end to subtract from -- and omitted is the honest report of that, where nought would
@@ -547,16 +573,29 @@ export type TaskCompletion = {
547
573
  completionMode: "provider-automatic";
548
574
  wrapAllowance: DurationSeconds;
549
575
  };
576
+ export type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
577
+ export declare const ON_CALL_ROLES: readonly ["party", "agent", "consulted", "conferenced"];
550
578
  /**
551
- * A consultation in progress on a task: who is being consulted, and since when where the provider
552
- * records it. Present between `transfer` `consult` and whichever of `complete` or `cancel`
553
- * follows; its presence is what makes those two issuable.
579
+ * Somebody on the call right now, as the provider states it: who, since when, and whether they
580
+ * are held aside. `party` is the customer; `agent` a person, by user id; `consulted` and
581
+ * `conferenced` someone a dial brought in, carrying the dial when a host placed it and the address
582
+ * either way. `label` names a destination -- a person, a queue -- not a phrase. At most one
583
+ * `party` and one `consulted`: the consult commands name neither because there is exactly one.
554
584
  */
555
- export interface TaskConsultation {
585
+ export type OnCall = {
586
+ since: IsoTimestamp;
587
+ held?: true;
588
+ } & ({
589
+ role: "party";
590
+ } | {
591
+ role: "agent";
592
+ userId: UserId;
593
+ } | {
594
+ role: "consulted" | "conferenced";
556
595
  destination: string;
596
+ dialId?: DialId;
557
597
  label?: string;
558
- since?: IsoTimestamp;
559
- }
598
+ });
560
599
  /**
561
600
  * The agent's request for a lead, from asking until the lead leaves or the request ends.
562
601
  * `requested` while nobody has joined; `joined`, with `leadId`, once somebody has.
@@ -643,12 +682,12 @@ export type Task<C extends Channel = Channel> = {
643
682
  attributes?: TaskAttribute[];
644
683
  handlingHistory?: TaskHandlingHistory;
645
684
  } & TaskCompletion & (C extends "voice" ? {
646
- consultation?: TaskConsultation;
685
+ onCall?: OnCall[];
647
686
  lead?: TaskLead;
648
687
  assisting?: TaskAssisting;
649
688
  media?: TaskMediaState;
650
689
  } : {
651
- consultation?: never;
690
+ onCall?: never;
652
691
  lead?: never;
653
692
  assisting?: never;
654
693
  media?: never;
@@ -708,13 +747,15 @@ export type VoiceTaskCommand = {
708
747
  } | {
709
748
  type: "disconnect";
710
749
  }
711
- /** Issuable only in `completing`, under the `callback` capability. Carries no destination. */
750
+ /** Issuable only in `completing`, under the `callback` capability. Dials the party's own number, so it names none. */
712
751
  | {
713
752
  type: "callback";
753
+ dialId: DialId;
714
754
  }
715
755
  /** Blind: hand the customer to `destination` with nobody consulted. Gated by `blindTransfer`. */
716
756
  | {
717
757
  type: "transfer";
758
+ dialId: DialId;
718
759
  destination: string;
719
760
  action?: never;
720
761
  }
@@ -722,14 +763,15 @@ export type VoiceTaskCommand = {
722
763
  | {
723
764
  type: "transfer";
724
765
  action: "consult";
766
+ dialId: DialId;
725
767
  destination: string;
726
768
  }
727
- /** Hand the customer to the consulted destination and leave. Needs `Task.consultation`. */
769
+ /** Hand the customer to the consulted destination and leave. Needs a `consulted` entry on `Task.onCall`. */
728
770
  | {
729
771
  type: "transfer";
730
772
  action: "complete";
731
773
  }
732
- /** Drop the consulted destination and return to the customer. Needs `Task.consultation`. */
774
+ /** Drop the consulted destination and return to the customer. Needs a `consulted` entry on `Task.onCall`. */
733
775
  | {
734
776
  type: "transfer";
735
777
  action: "cancel";
@@ -754,10 +796,19 @@ export type VoiceTaskCommand = {
754
796
  | {
755
797
  type: "lead";
756
798
  action: "leave";
757
- } | {
799
+ }
800
+ /** Dial `destination` into the call. Gated by `conference`. */
801
+ | {
802
+ type: "conference";
803
+ action: "add";
804
+ dialId: DialId;
805
+ destination: string;
806
+ }
807
+ /** Drop the `conferenced` entry on `Task.onCall` with this destination. */
808
+ | {
758
809
  type: "conference";
759
- participant: string;
760
- action: "add" | "remove";
810
+ action: "remove";
811
+ destination: string;
761
812
  } | {
762
813
  type: "recording";
763
814
  action: "start" | "pause" | "resume" | "stop";
@@ -795,22 +846,33 @@ export interface TaskCommandRequest<C extends Channel = Channel> {
795
846
  /**
796
847
  * `applied` rather than a verb per command: the command travels in the request, so
797
848
  * `execute({ command: { type: "hold" } })` returning `applied` already says the hold applied.
849
+ * A command that dials answers `dialling` instead, restating the host's `dialId`: accepted, the
850
+ * call being placed, and the outcome still to come on `dial-outcome`.
798
851
  *
799
852
  * A promise that rejects with no result means *unknown*, not `failed`. Omni does not retry --
800
853
  * no adapter on the agent's PC can make a repeat safe -- and the next snapshot shows what the
801
- * provider did. A command therefore carries no key of Omni's making.
854
+ * provider did. A command therefore carries no retry key of Omni's making; a `dialId` is not
855
+ * one, it is how an outcome arriving later is placed.
802
856
  */
803
857
  export type TaskCommandResult = {
804
858
  status: "applied";
859
+ } | {
860
+ status: "dialling";
861
+ dialId: DialId;
805
862
  } | {
806
863
  status: "failed";
807
864
  failure: ProtocolFailure;
808
865
  };
866
+ /** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
867
+ export declare function commandDialId(command: TaskCommand): DialId | undefined;
809
868
  export interface DialRequest {
869
+ dialId: DialId;
810
870
  destination: string;
811
871
  }
872
+ /** `dialling` says accepted and being placed, nothing more; the outcome arrives on `dial-outcome`. */
812
873
  export type DialResult = {
813
- status: "dialled";
874
+ status: "dialling";
875
+ dialId: DialId;
814
876
  } | {
815
877
  status: "failed";
816
878
  failure: ProtocolFailure;
@@ -1148,6 +1210,21 @@ export type ProviderEvent<C extends Channel = Channel> = {
1148
1210
  type: "task-ended";
1149
1211
  taskId: TaskId;
1150
1212
  outcome: TaskOutcome;
1213
+ }
1214
+ /**
1215
+ * How a dial the host placed ended, once, either way -- `answered` is stated, never read off
1216
+ * somebody appearing on the call, and says what happened to the dial; who is on the call is
1217
+ * `Task.onCall`. `taskId` is the task the dial was placed on, resolved from the dial and never
1218
+ * from what the agent is looking at, and that task may already have ended: a dial placed late
1219
+ * routinely outlives its call. `reason` is the switch's own words, shown to the agent as such.
1220
+ */
1221
+ | {
1222
+ type: "dial-outcome";
1223
+ dialId: DialId;
1224
+ outcome: DialOutcome;
1225
+ taskId?: TaskId;
1226
+ destination?: string;
1227
+ reason?: string;
1151
1228
  } | {
1152
1229
  type: "announcement";
1153
1230
  text: string;
@@ -1291,6 +1368,9 @@ export declare const taskKey: (providerId: string, taskId: TaskId) => string;
1291
1368
  export declare const userKey: (providerId: string, userId: UserId) => string;
1292
1369
  /** Every handling step somebody takes part in. `queued` is the one nobody does. */
1293
1370
  export declare const HANDLING_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
1371
+ /** The steps a dial writes, and so the only ones that carry a `dialId` and a `destination`. */
1372
+ export declare const HANDLING_STEPS_THAT_DIAL: readonly ["transferred", "conferenced", "unanswered"];
1373
+ export declare const handlingStepDials: (step: HandlingStep) => boolean;
1294
1374
  /** Whether an absent `by` means "could not attribute" rather than "nobody was involved". */
1295
1375
  export declare function handlingStepExpectsAPerson(step: HandlingStep): boolean;
1296
1376
  export interface BrowserSessionKeyInput {
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export function negotiateProtocolVersion(adapterVersions, hostVersions = OMNI_SU
15
15
  const shared = adapterVersions.filter(version => host.has(version));
16
16
  return shared.length === 0 ? undefined : Math.max(...shared);
17
17
  }
18
+ export const DIAL_OUTCOMES = ["answered", "busy", "no-answer", "unreachable", "rejected", "cancelled"];
18
19
  /** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
19
20
  export const IDLE_CAPABILITIES = ["dial", "personalBrowser", "calendar", "contacts"];
20
21
  /** What Omni calls each idle capability. */
@@ -50,6 +51,7 @@ export function isAllowedBrowserUrl(url) {
50
51
  return false;
51
52
  }
52
53
  }
54
+ export const ON_CALL_ROLES = ["party", "agent", "consulted", "conferenced"];
53
55
  /**
54
56
  * The levels a typical organisation has: the ladder in force when a manifest declares no
55
57
  * `orgLevels`. A manifest that declares any states its whole ladder outright.
@@ -73,6 +75,16 @@ export const TASK_COMMAND_NAMES = {
73
75
  chat: ["accept", "reject", "pause", "resume", "complete"],
74
76
  email: ["accept", "reject", "complete"],
75
77
  };
78
+ /** The dial a command places, by the host's identity for it; `undefined` for a command that dials nothing. */
79
+ export function commandDialId(command) {
80
+ if (command.type === "callback")
81
+ return command.dialId;
82
+ if (command.type === "transfer" && (command.action === undefined || command.action === "consult"))
83
+ return command.dialId;
84
+ if (command.type === "conference" && command.action === "add")
85
+ return command.dialId;
86
+ return undefined;
87
+ }
76
88
  export const BREAK_KINDS = [
77
89
  "short-break", "meal", "rest", "training", "coaching",
78
90
  "meeting", "administrative", "technical", "personal", "other",
@@ -138,6 +150,12 @@ export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)
138
150
  export const HANDLING_STEPS_WITH_A_PERSON = [
139
151
  "offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
140
152
  ];
153
+ /** The steps a dial writes, and so the only ones that carry a `dialId` and a `destination`. */
154
+ export const HANDLING_STEPS_THAT_DIAL = ["transferred", "conferenced", "unanswered"];
155
+ export const handlingStepDials = (step) => HANDLING_STEPS_THAT_DIAL.includes(step);
156
+ // Pinned both ways: a step added to `HandlingStep` has to be placed here, and a step listed here
157
+ // has to exist there. `satisfies` on the list covers the second; this statement covers the first.
158
+ true;
141
159
  /** Whether an absent `by` means "could not attribute" rather than "nobody was involved". */
142
160
  export function handlingStepExpectsAPerson(step) {
143
161
  return HANDLING_STEPS_WITH_A_PERSON.includes(step);
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 roster, each
8
8
  * declared contribution, and each event type.
9
9
  */
10
- declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.consultation", "task.lead", "task.assisting", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "contacts", "scheduledActivities", "team.policies"];
10
+ declare const STATE_SUBJECTS: readonly ["tasks", "task.browsers", "task.attributes", "task.handlingHistory", "task.onCall", "task.lead", "task.assisting", "task.media", "task.acceptance", "task.dispositions", "task.destinations", "task.custom", "task.locked", "break.reasons", "break.imposed", "team.members", "team.requests", "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[];
@@ -98,6 +98,10 @@ export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakApp
98
98
  /** What a stream has said about the tasks it carries, and the rules across events. */
99
99
  export declare class TaskStream {
100
100
  private readonly tasks;
101
+ private readonly dials;
102
+ /** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
103
+ dialled(dialId: string): void;
104
+ private noteDials;
101
105
  private static stated;
102
106
  /** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
103
107
  seed(snapshot: unknown): void;
@@ -127,7 +131,7 @@ export declare function assertBreakFollowsItsRequests(envelopes: readonly Provid
127
131
  export declare function assertMediaFollowsTheTask(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
128
132
  /** A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`. */
129
133
  export declare function stillHost(report?: HostReport, guarantees?: HostGuarantees): Host;
130
- /** One provider as the host sees it when freezing a break attempt's participant set. */
134
+ /** One provider as the host sees it when freezing the providers a break attempt asks. */
131
135
  export interface BreakCandidate {
132
136
  id: string;
133
137
  authentication: AuthenticationState["status"];
@@ -135,13 +139,13 @@ export interface BreakCandidate {
135
139
  holdsCapacity: boolean;
136
140
  }
137
141
  /**
138
- * The participant set of a break attempt is every connected provider from which the agent can
139
- * currently receive work. A provider whose login is not usable -- `expired` above all -- is not
140
- * one, whatever else is true of it: nothing can be asked of it, and a host that waits on it
141
- * stalls the break for everyone. A usable provider holding capacity is one, and cannot be left
142
- * out. `refreshing` is usable: identity and capabilities remain available and work continues.
142
+ * A break attempt asks every connected provider from which the agent can currently receive work.
143
+ * A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
144
+ * true of it: nothing can be asked of it, and a host that waits on it stalls the break for
145
+ * everyone. A usable provider holding capacity is asked, and cannot be left out. `refreshing` is
146
+ * usable: identity and capabilities remain available and work continues.
143
147
  */
144
- export declare function assertBreakParticipants(candidates: readonly BreakCandidate[], participants: readonly string[]): void;
148
+ export declare function assertBreakAttemptProviders(candidates: readonly BreakCandidate[], asked: readonly string[]): void;
145
149
  /** One published moment of a break asked for on a task: the approval, and how many tasks were outstanding. */
146
150
  export interface BreakOnTaskStep {
147
151
  approval: BreakApproval;
package/dist/testing.js CHANGED
@@ -12,7 +12,7 @@ const STATE_SUBJECTS = [
12
12
  "task.browsers",
13
13
  "task.attributes",
14
14
  "task.handlingHistory",
15
- "task.consultation",
15
+ "task.onCall",
16
16
  "task.lead",
17
17
  "task.assisting",
18
18
  "task.media",
@@ -33,7 +33,7 @@ const STATE_SUBJECTS = [
33
33
  // `ProviderEvent` without a row here, or a row it lacks, is a compile error.
34
34
  const EVENT_TYPES = {
35
35
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
36
- "task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true, diagnostic: true,
36
+ "task-media-started": true, "task-media-ended": true, "task-ended": true, "dial-outcome": true, announcement: true, "queue-summary": true, diagnostic: true,
37
37
  "team-updated": true, "contacts-updated": true, "calendar-updated": true,
38
38
  };
39
39
  const CONTRACT_SUBJECTS = [
@@ -54,8 +54,8 @@ function observeTask(value, seen) {
54
54
  seen.add("task.attributes");
55
55
  if (isRecord(value.handlingHistory) && some(value.handlingHistory.steps))
56
56
  seen.add("task.handlingHistory");
57
- if (value.consultation !== undefined)
58
- seen.add("task.consultation");
57
+ if (some(value.onCall))
58
+ seen.add("task.onCall");
59
59
  if (value.lead !== undefined)
60
60
  seen.add("task.lead");
61
61
  if (value.assisting !== undefined)
@@ -617,6 +617,27 @@ const WORK_BEGUN = new Set(["in-progress", "paused", "completing"]);
617
617
  /** What a stream has said about the tasks it carries, and the rules across events. */
618
618
  export class TaskStream {
619
619
  tasks = new Map();
620
+ // Every dial the stream can place an outcome against: one the host said it placed, or one a
621
+ // task carried on `onCall` or in its record -- which is how a dial made before a transfer is known
622
+ // to whoever holds the task now. `ended` once its outcome arrived, since an outcome comes once.
623
+ dials = new Map();
624
+ /** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
625
+ dialled(dialId) {
626
+ if (!this.dials.has(dialId))
627
+ this.dials.set(dialId, "placed");
628
+ }
629
+ noteDials(task) {
630
+ if (!isRecord(task))
631
+ return;
632
+ const entries = [
633
+ ...(Array.isArray(task.onCall) ? task.onCall : []),
634
+ ...(isRecord(task.handlingHistory) && Array.isArray(task.handlingHistory.steps) ? task.handlingHistory.steps : []),
635
+ ];
636
+ for (const entry of entries) {
637
+ if (isRecord(entry) && typeof entry.dialId === "string")
638
+ this.dialled(entry.dialId);
639
+ }
640
+ }
620
641
  static stated(task) {
621
642
  const media = isRecord(task) && (task.media === "started" || task.media === "ended") ? task.media : "none";
622
643
  return { phase: String(isRecord(task) ? task.phase : undefined), media };
@@ -629,6 +650,7 @@ export class TaskStream {
629
650
  for (const task of snapshot.tasks) {
630
651
  if (isRecord(task) && typeof task.id === "string")
631
652
  this.tasks.set(task.id, TaskStream.stated(task));
653
+ this.noteDials(task);
632
654
  }
633
655
  }
634
656
  /** Applies one envelope and returns what it may not say given what came before. */
@@ -651,6 +673,7 @@ export class TaskStream {
651
673
  if (known !== undefined)
652
674
  refuse("stream.taskOffered.duplicate", `${at}.task.id`, `${id} is already on the stream; an offer introduces a task once`);
653
675
  this.tasks.set(id, TaskStream.stated(event.task));
676
+ this.noteDials(event.task);
654
677
  break;
655
678
  case "task-updated":
656
679
  if (id === undefined)
@@ -675,6 +698,7 @@ export class TaskStream {
675
698
  }
676
699
  this.tasks.set(id, next);
677
700
  }
701
+ this.noteDials(event.task);
678
702
  break;
679
703
  case "task-media-started":
680
704
  if (id === undefined)
@@ -713,6 +737,21 @@ export class TaskStream {
713
737
  refuse("stream.taskEnded.unknown", `${at}.taskId`, `${id} was never offered or carried on a snapshot`);
714
738
  this.tasks.delete(id);
715
739
  break;
740
+ case "dial-outcome": {
741
+ // An outcome ends a dial somebody placed, once. The task it names may already have ended;
742
+ // a dial placed late routinely outlives its call, which is why the dial has its own identity.
743
+ if (typeof event.dialId !== "string")
744
+ break;
745
+ const dial = this.dials.get(event.dialId);
746
+ if (dial === undefined) {
747
+ refuse("stream.dialOutcome.unknown", `${at}.dialId`, `${event.dialId} is not a dial the host placed or a task carried: an outcome ends a dial somebody made`);
748
+ }
749
+ else if (dial === "ended") {
750
+ refuse("stream.dialOutcome.duplicate", `${at}.dialId`, `${event.dialId} already has its outcome; a dial ends once`);
751
+ }
752
+ this.dials.set(event.dialId, "ended");
753
+ break;
754
+ }
716
755
  default:
717
756
  break;
718
757
  }
@@ -802,26 +841,26 @@ export function stillHost(report = { online: true }, guarantees = {}) {
802
841
  }
803
842
  const usableLogin = (status) => status === "authenticated" || status === "refreshing";
804
843
  /**
805
- * The participant set of a break attempt is every connected provider from which the agent can
806
- * currently receive work. A provider whose login is not usable -- `expired` above all -- is not
807
- * one, whatever else is true of it: nothing can be asked of it, and a host that waits on it
808
- * stalls the break for everyone. A usable provider holding capacity is one, and cannot be left
809
- * out. `refreshing` is usable: identity and capabilities remain available and work continues.
844
+ * A break attempt asks every connected provider from which the agent can currently receive work.
845
+ * A provider whose login is not usable -- `expired` above all -- is not asked, whatever else is
846
+ * true of it: nothing can be asked of it, and a host that waits on it stalls the break for
847
+ * everyone. A usable provider holding capacity is asked, and cannot be left out. `refreshing` is
848
+ * usable: identity and capabilities remain available and work continues.
810
849
  */
811
- export function assertBreakParticipants(candidates, participants) {
812
- const chosen = new Set(participants);
813
- for (const id of participants) {
850
+ export function assertBreakAttemptProviders(candidates, asked) {
851
+ const chosen = new Set(asked);
852
+ for (const id of asked) {
814
853
  if (!candidates.some(candidate => candidate.id === id))
815
854
  throw new Error(`${id} is not a provider the host knows`);
816
855
  }
817
856
  for (const candidate of candidates) {
818
857
  const expected = usableLogin(candidate.authentication) && candidate.holdsCapacity;
819
858
  if (expected && !chosen.has(candidate.id)) {
820
- throw new Error(`${candidate.id} can give the agent work and must be a participant`);
859
+ throw new Error(`${candidate.id} can give the agent work and must be asked`);
821
860
  }
822
861
  if (!expected && chosen.has(candidate.id)) {
823
862
  const why = usableLogin(candidate.authentication) ? "holds no capacity" : `is ${candidate.authentication}`;
824
- throw new Error(`${candidate.id} ${why} and is not a participant: nothing can be asked of it`);
863
+ throw new Error(`${candidate.id} ${why} and is not asked: nothing can be asked of it`);
825
864
  }
826
865
  }
827
866
  }
@@ -17,6 +17,8 @@ export interface TaskValidationContext {
17
17
  levels?: readonly string[];
18
18
  /** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether a pending task states its `acceptance`. */
19
19
  autoAcceptTasks?: boolean;
20
+ /** Whether the manifest declares `dialOutcomes`. A task that may dial needs it to; absent, the question is not asked. */
21
+ dialOutcomesDeclared?: boolean;
20
22
  }
21
23
  export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
22
24
  /**
@@ -64,7 +66,7 @@ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" |
64
66
  * shows the agent what it says. A status the method does not answer, a failure status without a
65
67
  * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
66
68
  */
67
- export declare function validateResult(result: unknown, method: ResultMethod, path?: string): ProtocolViolation[];
69
+ export declare function validateResult(result: unknown, method: ResultMethod, path?: string, dialId?: string): ProtocolViolation[];
68
70
  /** What a login is validated against beyond its own shape. */
69
71
  export interface LoginValidationContext {
70
72
  /** The level ids in force, from the manifest. The defaults when absent. */