@xema/omni-protocol 0.1.112 → 0.1.114

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.
@@ -12,8 +12,8 @@ export declare function assertNoViolations(violations: readonly ProtocolViolatio
12
12
  * rule never evaluated is a visible gap, not a pass. The harness registers one for each exercise.
13
13
  */
14
14
  export declare function observeRules(listener: (rule: string) => void): () => void;
15
- /** Says a rule was evaluated, for code that decides outside a collector: the stream, the drive. */
16
- export declare function ruleEvaluated(...rules: readonly string[]): void;
15
+ /** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
16
+ export declare function ruleTested(...rules: readonly string[]): void;
17
17
  /** Every field is optional, so this checks what is present rather than what is missing. */
18
18
  export declare function validateContact(contact: unknown, path?: string): ProtocolViolation[];
19
19
  export declare function validateScheduledActivity(activity: unknown, path?: string): ProtocolViolation[];
@@ -167,8 +167,8 @@ export function observeRules(listener) {
167
167
  ruleListeners.add(listener);
168
168
  return () => { ruleListeners.delete(listener); };
169
169
  }
170
- /** Says a rule was evaluated, for code that decides outside a collector: the stream, the drive. */
171
- export function ruleEvaluated(...rules) {
170
+ /** Says a rule was evaluated, for code that decides outside a collector: the stream, the test. */
171
+ export function ruleTested(...rules) {
172
172
  for (const rule of rules)
173
173
  for (const listener of ruleListeners)
174
174
  listener(rule);
@@ -176,11 +176,11 @@ export function ruleEvaluated(...rules) {
176
176
  class Collector {
177
177
  violations = [];
178
178
  add(rule, path, message) {
179
- ruleEvaluated(rule);
179
+ ruleTested(rule);
180
180
  this.violations.push({ rule, path, message });
181
181
  }
182
182
  require(condition, rule, path, message) {
183
- ruleEvaluated(rule);
183
+ ruleTested(rule);
184
184
  if (!condition)
185
185
  this.violations.push({ rule, path, message });
186
186
  return Boolean(condition);
@@ -719,7 +719,7 @@ const levelIds = (levels) => levels ?? DEFAULT_LEVEL_IDS;
719
719
  * formatting hides it; anything else by its text, case aside.
720
720
  */
721
721
  function validateNothingLeaksInto(task, locked, path, into) {
722
- ruleEvaluated("task.locked.leak");
722
+ ruleTested("task.locked.leak");
723
723
  const carries = (text, value) => {
724
724
  if (typeof text !== "string")
725
725
  return false;
@@ -1160,7 +1160,7 @@ function validateTaskInto(task, context, path, into) {
1160
1160
  // that created it, and says so whatever the provisioning: the desk shows no Accept for a call
1161
1161
  // the agent placed. The provisioning governs work the queue routes, and nothing else.
1162
1162
  const originated = task.phase === "pending" && originatedByTheAgent(task);
1163
- ruleEvaluated("task.acceptance.originated");
1163
+ ruleTested("task.acceptance.originated");
1164
1164
  if (task.acceptance !== undefined) {
1165
1165
  into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
1166
1166
  if (task.phase !== "pending") {
@@ -1355,7 +1355,7 @@ export function validateBreakStatus(state, tasks, path = "snapshot") {
1355
1355
  // The converse: a committed break with nothing outstanding has begun. starting-after-task beside
1356
1356
  // no task is a break waiting on work that does not exist, and an empty list reading as "still
1357
1357
  // finishing" is the plausible nought.
1358
- ruleEvaluated("break.starting-after-task.tasks");
1358
+ ruleTested("break.starting-after-task.tasks");
1359
1359
  if (isPlainObject(state) && state.status === "starting-after-task"
1360
1360
  && Array.isArray(tasks) && tasks.length === 0) {
1361
1361
  into.add("break.starting-after-task.tasks", `${path}.break.status`, "a break starting after the task waits on a task, and the snapshot carries none: with nothing outstanding the break is on-break");
@@ -1619,7 +1619,7 @@ function validateBreakState(value, path, into) {
1619
1619
  // A break in effect, or about to be, on a provider that publishes reasons is on one of them: an
1620
1620
  // forced one included, since the lead's force-break command named it. A break of no kind is a break whose
1621
1621
  // rules -- who may listen through it, whether it counts -- nobody can apply.
1622
- ruleEvaluated("break.activeReasonId.required");
1622
+ ruleTested("break.activeReasonId.required");
1623
1623
  if (seen.size > 0 && (value.status === "on-break" || value.status === "starting-after-task")) {
1624
1624
  into.require(typeof value.activeReasonId === "string" && value.activeReasonId.length > 0, "break.activeReasonId.required", `${path}.activeReasonId`, `a break ${describeValue(value.status)} on a provider that publishes reasons names the one it is on`);
1625
1625
  }
@@ -2758,6 +2758,8 @@ export function validateTaskCommand(command, task, path = "command", taskContext
2758
2758
  case "end-call":
2759
2759
  offered("endCall");
2760
2760
  interaction();
2761
+ // Ending my part with the caller on hold leaves them in the hold, with nobody coming back to them.
2762
+ into.require(task.phase !== "paused", "command.endCall.held", path, "the caller is on hold: resume before ending your part");
2761
2763
  break;
2762
2764
  case "terminate-call":
2763
2765
  offered("terminateCall");
@@ -3140,7 +3142,7 @@ const filled = (v) => typeof v === "string" && v.trim().length > 0;
3140
3142
  function collector() {
3141
3143
  const violations = [];
3142
3144
  const check = (ok, rule, path, message) => {
3143
- ruleEvaluated(`recording.${rule}`);
3145
+ ruleTested(`recording.${rule}`);
3144
3146
  if (!ok)
3145
3147
  violations.push({ rule: `recording.${rule}`, path, message });
3146
3148
  return Boolean(ok);
package/guide/agent.md CHANGED
@@ -234,7 +234,7 @@ would otherwise find the next customer under a reused handle and act on them. A
234
234
  of the task the adapter keeps, not a field held in memory beside it: it lives in the login's
235
235
  `store` with the task, or is derived from a platform handle that itself never reuses, so a rebuilt
236
236
  adapter comes back with the same assignment on the same task, or every late event it was minted to
237
- catch mismatches and the second adapter's snapshot no longer carries the task (`drive.reload.snapshot`).
237
+ catch mismatches and the second adapter's snapshot no longer carries the task (`test.reload.snapshot`).
238
238
 
239
239
  Assignment, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
240
240
  task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
@@ -369,7 +369,7 @@ Migration from the earlier spellings:
369
369
  | command.complete.mode | `command.complete.wrapAllowance`: `complete` under either mode, refused only on `provider-automatic` with no wrap |
370
370
  | expiresInSeconds on task-offered | `Task.expiresInSeconds`, in `pending` (`task.pending.expiresInSeconds`, `.unexpected`; `event.taskOffered.expiresInSeconds.unexpected`) |
371
371
  | the expired outcome naming preview | gone: nothing expires a preview; withdrawn, it is `cancelled` by the provider |
372
- | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`drive.schedule.unsettled`) |
372
+ | schedule on voice alone | `schedule` on every channel, under a manifest that declares `calendar` (`task.capability.calendar.required`), bounded by `settleMs` (`test.schedule.unsettled`) |
373
373
  | a forced break requesting breaks on the other providers | `setCapacity({ count: 0 })` on every other usable provider (`assertForcedBreakStopsTheRest`) |
374
374
  | end-forced-break on a break the platform imposed | refused: `team.command.endForcedBreak.provider`, `omni.break-forced-by-provider` |
375
375
  | ForcedBreak.expectedDurationMs | `expectedEndsInSeconds`: seconds left, restated (`break.forced.expectedEndsInSeconds`, `.expectedDurationMs.renamed`); the command keeps `expectedDurationMs` |
@@ -383,6 +383,8 @@ Migration from the earlier spellings:
383
383
  | two clients on one login | the later wins; the first is ended with `recovery: "displaced"` |
384
384
  | the phone unspoken | `PhoneState` -- the device, its mute, its channels -- on `Snapshot.phone`, `phone-updated` and `TeamMember.phone`, from a manifest declaring `phoneStatus` (`snapshot.phone.required`, `.unexpected`, `phone.*`, `phone.channel.*`, `event.phone.capability`) |
385
385
  | the agent's readiness for the next call read into setCapacity | the agent's own queue: `capabilities.nextCall`, `requestNextCall`/`cancelNextCall`/`releaseLinedUp`, `Snapshot.nextCall` and `linedUp`, the `next-call` and `lined-up` events (`snapshot.nextCall.idle`, `nextCall.*`, `linedUp.*`) |
386
+ | a harness that exercised an adapter, drove a call and evaluated rules | the harness reads as a test: `testAdapter`, `withCall`, `timeoutMs`, `notTested`, `rulesTested`, rules `test.*` (`test.timeout`, `test.offer.expired`, `test.preview.deadline`, ...) |
387
+ | an `end-call` while the caller was on hold | refused: a `paused` task is resumed first (`command.endCall.held`), the desk shows End call disabled on hold, the provider answers `failed`, and the test names an adapter that ends from hold (`test.command.held`); `terminate-call` is not gated, it ends the caller's channel too |
386
388
 
387
389
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
388
390
  History and report rule names use `history` and `historyReport`; assignment rules use
@@ -592,7 +594,7 @@ duration with a later report. A later agent mute is a new leg, never a reopening
592
594
  leg, on its own clock, written when the provider takes the leg into its record, never an echo of
593
595
  the `at` the host reported: the host keeps the answer as the leg's identity from then on and names
594
596
  it on the closing report, and a provider that echoes the host's instant has assigned nothing
595
- (`drive.recordStep.identity`). Correlate the provider-published `muted` entry using that
597
+ (`test.recordStep.identity`). Correlate the provider-published `muted` entry using that
596
598
  provider-selected `at` acknowledged for the current report key; a final `seconds` closes that leg
597
599
  under the history contract.
598
600
  This is identity matching, not reconciliation between clocks. An old closed entry, an unrelated
@@ -818,10 +820,10 @@ words are close in English and far apart on the desk: `nobody` is silence, `not-
818
820
  standing notice.
819
821
 
820
822
  One consequence for whoever builds the agent application: because a conformance run fails on `not-yet-read`, a
821
- conformant adapter never shows an agent application `not-yet-read` under test, and a clean `exerciseAdapter`
823
+ conformant adapter never shows an agent application `not-yet-read` under test, and a clean `testAdapter`
822
824
  result says nothing about how the agent application renders it. That rendering is tested against a fixture --
823
825
  a task published under `not-yet-read` shows the notice, the same task under `queue` shows none --
824
- and never against an adapter, even in principle. `exerciseAdapter` treats a
826
+ and never against an adapter, even in principle. `testAdapter` treats a
825
827
  task published under `not-yet-read` as a violation (`capabilitySource.notYetRead`), as it treats
826
828
  a diagnostic: a conformance run against a platform that cannot say what it permits fails loudly
827
829
  rather than passing with a note.
@@ -919,9 +921,9 @@ on the calendar.
919
921
  follows within the manifest's `settleMs`, as a `task-ended` follows an applied `complete`. Past
920
922
  the bound the agent application calls `snapshot()`: a calendar carrying an activity at that instant
921
923
  clears the wait; one without it shows the follow-up as unsettled -- "Scheduled... the provider has
922
- not confirmed" -- naming the command. The drive schedules one follow-up where the task offers it,
924
+ not confirmed" -- naming the command. The test schedules one follow-up where the task offers it,
923
925
  at a time it takes from the provider's own publication, and holds the provider to the same bound
924
- (`drive.schedule.unsettled`).
926
+ (`test.schedule.unsettled`).
925
927
 
926
928
  ### Custom capabilities
927
929
 
@@ -1004,7 +1006,7 @@ declared:
1004
1006
  | Command | What makes it available |
1005
1007
  | --- | --- |
1006
1008
  | `answer`, `accept` | Nothing. A task that was offered can be accepted, or offering it meant nothing. |
1007
- | `end-call` | The `endCall` capability: ends my part, the caller continues. |
1009
+ | `end-call` | The `endCall` capability: ends my part, the caller continues. Never from hold: a `paused` task is resumed first (`command.endCall.held`), or the caller is left in the hold. |
1008
1010
  | `terminate-call` | The `terminateCall` capability: ends the whole call, every channel on it. |
1009
1011
  | `conference` with `action: "remove"` | The `conference` capability, and somebody else on the call: a remove that would leave the agent alone is `end-call`, and a provider answers it `failed`. |
1010
1012
  | `decline` | The `decline` capability on any channel, **and** Omni local policy permitting it. One word for refusing an offer, whatever the channel. |
@@ -1019,7 +1021,8 @@ declared:
1019
1021
 
1020
1022
  **A control on the contact belongs to the interaction phases**, `in-progress` and `paused`:
1021
1023
  `hold`, `resume` and `pause`, `end-call` and `terminate-call`, every `conference` action, and
1022
- every `lead-assist` action; `schedule` alone reaches into `completing`. These controls act within this agent's current interaction.
1024
+ every `lead-assist` action; `schedule` alone reaches into `completing`, and `end-call` alone stops
1025
+ short of `paused` (`command.endCall.held`), since ending my part from hold leaves the caller in it. These controls act within this agent's current interaction.
1023
1026
  In `completing`, that interaction has ended and its wrap work remains. The caller may still be in
1024
1027
  an IVR, queue or another agent's interaction, and other channels may remain connected. Completion
1025
1028
  of this interaction does not establish that the caller or bridge ended. The capability stays
package/guide/breaks.md CHANGED
@@ -243,7 +243,7 @@ Capacity supersedes rather than accumulates: the latest value is the ceiling, an
243
243
  ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
244
244
  it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
245
245
  taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
246
- both ways after the drive, two then one then nought, and an offer after a lower count is caught
246
+ both ways after the test, two then one then nought, and an offer after a lower count is caught
247
247
  against it (`stream.taskOffered.overCapacity`).
248
248
 
249
249
  **Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
package/guide/phone.md CHANGED
@@ -62,7 +62,7 @@ report is a task fact or a capacity fact; it is what the station can do, and the
62
62
  is the one that knows whether an agent without a microphone, or without a speaker, can work.
63
63
 
64
64
  The agent application's own obligations here — asking at connect, never publishing `not-asked` when it does,
65
- publishing a state and not a flicker — are Omni's tests' to hold. `exerciseAdapter` holds the
65
+ publishing a state and not a flicker — are Omni's tests' to hold. `testAdapter` holds the
66
66
  other side: it validates the shape of whatever agent application a test hands the adapter, requires `audio` on
67
67
  a softphone login and none elsewhere -- not on a desk phone, not off voice
68
68
  (`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
package/guide/voice.md CHANGED
@@ -131,7 +131,7 @@ stream that drops, a track that ends, a transport that disconnects, a microphone
131
131
  endpoint re-registering change nothing about the task until the provider says so. Structurally:
132
132
  `task-audio-started` and `task-audio-ended` alternate on a task whose work has begun, audio ends
133
133
  only where it arrived, what follows the audio ending is `completing` or `task-ended`, and every
134
- task is introduced once — `exerciseAdapter` holds the stream to that from the connect snapshot on,
134
+ task is introduced once — `testAdapter` holds the stream to that from the connect snapshot on,
135
135
  and `assertAudioFollowsTheTask` holds any sequence.
136
136
 
137
137
  ## Connecting back during completion
@@ -258,6 +258,13 @@ provider. Two of them end the agent's part; they differ in what happens to the c
258
258
  conferenced colleague, a lead who joined -- end, and the caller continues on the path the
259
259
  provider decides: another IVR, a queue, a survey, or the end of the call. Gated by `endCall`.
260
260
 
261
+ **Never from hold.** An `end-call` while the task is `paused` would leave the caller in the hold
262
+ with nobody coming back to them, and the provider's path for a caller who continues never begins.
263
+ So the caller is taken off hold first: the desk holds the command back while the task is `paused`
264
+ (`command.endCall.held`) and shows End call disabled on hold, and a provider that receives it
265
+ anyway answers `failed`. Resume, then end. `terminate-call` needs no such gate: it ends the
266
+ caller's channel with the rest, held or not, and nobody is left waiting.
267
+
261
268
  **`terminate-call` ends the whole call.** Every channel on it ends at the provider: the agent's,
262
269
  the caller's, any colleague the agent added, and anyone else on the call. Nobody is left on the
263
270
  line. Gated by `terminateCall`, a separate permission, since ending a customer's call is a
package/guide.md CHANGED
@@ -1646,9 +1646,9 @@ reload is the first client dying and a second coming up for the same login, and
1646
1646
  publishes the same truth about the same task as the first did, or the first was publishing
1647
1647
  something it made up: an assignment minted for the connection, audio remembered as a boolean, a
1648
1648
  phone the second never read. Three pieces of exactly that state were found in one adapter in an
1649
- afternoon, and none of it was visible until the adapter was built twice. The drive's `rebuild` is the test of this
1649
+ afternoon, and none of it was visible until the adapter was built twice. The test's `rebuild` is the test of this
1650
1650
  principle: given a way to build the adapter again, the run hands the login over and holds the
1651
- second to what the first published (`drive.reload.snapshot`, `.history`, `.openAudio`, and the
1651
+ second to what the first published (`test.reload.snapshot`, `.history`, `.openAudio`, and the
1652
1652
  stream's own rules across the resync). An adapter that cannot be built twice against its platform
1653
1653
  and publish the same task is not a conformant adapter yet, whatever a single run says.
1654
1654
 
@@ -2180,7 +2180,7 @@ Creates one live provider connection for the signed-in agent.
2180
2180
  | `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
2181
2181
  | `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
2182
2182
  | `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a desk-phone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's** in `guide/phone.md`. |
2183
- | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`drive.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`drive.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2183
+ | `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`test.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`test.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
2184
2184
  | `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides** in `guide/phone.md`. |
2185
2185
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
2186
2186
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -2426,12 +2426,12 @@ update against the last publication, and on a snapshot from a provider with a cl
2426
2426
  overruns; that is the true value, not a copy.
2427
2427
 
2428
2428
  **A deadline stated is a deadline kept.** The provider that says how long an offer has ends it
2429
- `expired` when it runs out, and the drive holds it to that: where the first offer carries
2430
- `expiresInSeconds` within the drive's timeout, the drive leaves that offer unanswered and expects
2431
- the `expired` ending within the seconds plus `settleMs`, then drives the next offer
2432
- (`drive.offer.expired`). A preview's deadline is held to its `atDeadline` the same way: under
2429
+ `expired` when it runs out, and the test holds it to that: where the first offer carries
2430
+ `expiresInSeconds` within the test's timeout, the test leaves that offer unanswered and expects
2431
+ the `expired` ending within the seconds plus `settleMs`, then tests the next offer
2432
+ (`test.offer.expired`). A preview's deadline is held to its `atDeadline` the same way: under
2433
2433
  `provider-dials` the provider dials when it runs out, under `host-dials` and `waits` the preview
2434
- stands until the desk dials (`drive.preview.deadline`).
2434
+ stands until the desk dials (`test.preview.deadline`).
2435
2435
 
2436
2436
  #### Optional periodic provider time checks and agent application estimates
2437
2437
 
@@ -2611,7 +2611,7 @@ break, which says so with `forced`: in effect at once, or `starting-after-task`
2611
2611
  finishes the call they are on (`stream.breakState.commitBeforeGrant`); and a break never moves backwards —
2612
2612
  from `on-break` or `starting-after-task` to a grant or a request, or from `granted` to
2613
2613
  `awaiting-approval` — a new request passes through `not-requested` (`stream.breakState.backwards`).
2614
- `exerciseAdapter` holds the stream to that from the connect snapshot on;
2614
+ `testAdapter` holds the stream to that from the connect snapshot on;
2615
2615
  `assertBreakFollowsItsRequests` holds any sequence.
2616
2616
 
2617
2617
  Adapters and hosts can call `validateBreakTransition(before, after)` from the package’s validation entry point
@@ -2675,7 +2675,7 @@ snapshots until it ends.
2675
2675
  **Every offer is owed an ending.** An assignment the provider introduced is an assignment it ends,
2676
2676
  with `task-ended` and an outcome, whatever became of the call: answered and completed, declined,
2677
2677
  withdrawn, abandoned in the ring, lapsed, taken over. An offer that is simply never mentioned
2678
- again leaves the desk holding a task nobody will close, and the conformance drive names it
2678
+ again leaves the desk holding a task nobody will close, and the test names it
2679
2679
  (`stream.taskOffered.unended`).
2680
2680
 
2681
2681
  ### `task-updated`
@@ -2730,8 +2730,8 @@ manifest's `settleMs`. A provider never answers `applied` for a completion it ha
2730
2730
  performed. Past the bound the agent application calls `snapshot()`: a snapshot still carrying the task is a task
2731
2731
  held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
2732
2732
  the provider has not confirmed" -- naming the command; a snapshot no longer carrying it clears the
2733
- task, since the ending was owed and lost. The drive holds a provider to the same bound
2734
- (`drive.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
2733
+ task, since the ending was owed and lost. The test holds a provider to the same bound
2734
+ (`test.completion.unsettled`). The `task-audio-ended` event and the `completing` phase are likewise
2735
2735
  non-terminal. A replacement
2736
2736
  snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
2737
2737
  same envelope ID is harmless, and a `task-ended` naming an assignment that has already ended is
@@ -2762,7 +2762,7 @@ already done the safe thing before it speaks, and nothing in the task or break m
2762
2762
  a diagnostic. It exists so that the loudness reaches a person — an agent application renders each one where the
2763
2763
  agent works and keeps a count an operator can read, because a healthy workspace over a shouting
2764
2764
  console is the silence problem one layer up. One event per occurrence; the agent application counts, the
2765
- provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
2765
+ provider does not batch. `testAdapter` treats a diagnostic delivered during a run as a
2766
2766
  violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
2767
2767
  fails loudly rather than passing with a note. A task published under `capabilitySource:
2768
2768
  "not-yet-read"` is the same kind of fact, stated on the task instead, and fails a run the same way
@@ -2875,7 +2875,7 @@ wider goes through this key.
2875
2875
 
2876
2876
  Whether two logins declare the same capabilities, field by field — key order aside, and with
2877
2877
  `team: {}` distinct from `team` absent. It is the comparison an adapter makes before republishing
2878
- `authenticated`, and the one `exerciseAdapter` holds `refreshing` to.
2878
+ `authenticated`, and the one `testAdapter` holds `refreshing` to.
2879
2879
 
2880
2880
  ## Runtime validation
2881
2881
 
@@ -2900,9 +2900,9 @@ same exported checks are used by Omni and adapter tests so their interpretations
2900
2900
  | `validateHostReport(report)` | The agent application's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever agent application a test hands the adapter; `stillHost(report)` builds one that never changes. |
2901
2901
  | `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
2902
2902
  | `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
2903
- | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the drive, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
2903
+ | `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the test, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
2904
2904
  | `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
2905
- | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`). An agent application validates before sending and an adapter before acting. |
2905
+ | `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a conference's item, a schedule's time, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`, `command.phase.interaction` for every control on the call or the conversation), and the state that has to stand: a lead requested, somebody else still on the call (`command.conference.remove.alone`), the caller off hold before the agent's part ends (`command.endCall.held`). An agent application validates before sending and an adapter before acting. |
2906
2906
  | `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
2907
2907
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
2908
2908
 
@@ -2917,7 +2917,7 @@ carries that agent reports `team.member.self`, on a `team-member-updated` and a
2917
2917
  reports `team.unentitled`, and a team event to such a login reports `event.team.capability`; given
2918
2918
  `leadFeatures: true`, a snapshot without a team reports `team.required`, and given `leadFeatures: false`,
2919
2919
  anything of the team reaching the lead reports `team.unexpected` or `event.team.features`. Without them those rules are not checked, because they cannot be.
2920
- `exerciseAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
2920
+ `testAdapter` passes all three, holding the switch off until it has sent it. Given the manifest, a
2921
2921
  snapshot or event that carries an instant the desk renders as a running duration under a manifest
2922
2922
  without `timeCheck` reports `manifest.timeCheck.required`.
2923
2923
 
@@ -2950,9 +2950,9 @@ A refusal has an aftermath on the desk and a report to the provider, and both ar
2950
2950
  side. The harness tells an adapter under test exactly as an agent application does
2951
2951
  (`connection.refused.required`, `connection.refused.rejected`).
2952
2952
 
2953
- ## Conformance helpers
2953
+ ## Testing an adapter
2954
2954
 
2955
- ### `exerciseAdapter(adapter, context, options?)`
2955
+ ### `testAdapter(adapter, context, options?)`
2956
2956
 
2957
2957
  Adapter conformance exercise from `@xema/omni-protocol/testing`.
2958
2958
 
@@ -2966,7 +2966,7 @@ By default it throws `ProtocolConformanceError` listing every violation. Pass
2966
2966
  `{ collectOnly: true }` to receive them on the result instead:
2967
2967
 
2968
2968
  ```ts
2969
- const result = await exerciseAdapter(adapter, context, { collectOnly: true });
2969
+ const result = await testAdapter(adapter, context, { collectOnly: true });
2970
2970
  expect(result.violations).toEqual([]);
2971
2971
  expect(result.disconnectWasClean).toBe(true);
2972
2972
  ```
@@ -2978,7 +2978,7 @@ latest the session published during the run, which differs only when the adapter
2978
2978
  `authentication.refreshing.identity`, a changed capability set `authentication.refreshing.capabilities`.
2979
2979
  A capability granted by a later login requires its methods just as one declared at sign-in does.
2980
2980
 
2981
- `result.notExercised` lists what the run never reached — one subject per family of rules: each
2981
+ `result.notTested` lists what the run never reached — one subject per family of rules: each
2982
2982
  optional part of a task (`task.browsers`, `task.history`, `task.leadAssist`, …), the break's
2983
2983
  `reasons` and `forced`, the team member list's `members` and a member's `request`, each declared contribution, and
2984
2984
  each event type (`event.task-ended`, …) — and so what a clean `violations` says nothing about.
@@ -2987,52 +2987,52 @@ no tasks exercises no task rule, and a pass over it reads as coverage it is not.
2987
2987
 
2988
2988
  **A static run never answers a call.** It states a capacity and disconnects, so everything
2989
2989
  downstream of `answer` -- the room, the stage, audio, every phase past `pending`, every dial
2990
- outcome -- stays in `notExercised` for every adapter, and a rule about a live call is enforced only
2991
- in each adopter's own tests. `{ drive: true }` closes that: the exercise takes the first task the
2990
+ outcome -- stays in `notTested` for every adapter, and a rule about a live call is enforced only
2991
+ in each adopter's own tests. `{ withCall: true }` closes that: the exercise takes the first task the
2992
2992
  provider offers through one ordinary lifecycle -- accept it, wait for its audio and open it on a
2993
2993
  softphone, hold and resume where the task offers `hold`, end the call where it offers `endCall`,
2994
2994
  complete it with an outcome where the agent completes -- and holds every step to the rules a
2995
2995
  agent application holds a provider to. Each command is validated against the task as published
2996
- (`drive.command.*`), each answer for its method, a refusal of a control the task offered is a
2997
- violation (`drive.command.failed`), and an event the provider owes and never sends is one too
2998
- (`drive.timeout`, after `driveTimeoutMs`, 5000 by default). A first offer that says how long it has is left to
2996
+ (`test.command.*`), each answer for its method, a refusal of a control the task offered is a
2997
+ violation (`test.command.failed`), and an event the provider owes and never sends is one too
2998
+ (`test.timeout`, after `timeoutMs`, 5000 by default). A first offer that says how long it has is left to
2999
2999
  lapse and expected to end `expired`, and a preview's deadline is held to its `atDeadline`
3000
- (`drive.offer.expired`, `drive.preview.deadline`); see **Every screen counts from the provider's
3001
- clock**. Around the drive the exercise holds
3000
+ (`test.offer.expired`, `test.preview.deadline`); see **Every screen counts from the provider's
3001
+ clock**. Around the test the exercise holds
3002
3002
  the run to what an agent application holds a provider to between commands: an offer before the agent application stated
3003
3003
  capacity, or beyond the count with nothing dialled on it, is named
3004
3004
  (`stream.taskOffered.beforeCapacity`, `.overCapacity`); every user the snapshot names is looked up
3005
3005
  through `getUserDetails` and the answer held to the shape, nobody unasked, nobody described as
3006
3006
  nothing (`getUserDetails.user.*`, `getUserDetails.unasked`, `connection.getUserDetails.empty`); and
3007
3007
  the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
3008
- before it reads anything (`drive.reload.login`), and is then held to everything the first was on
3008
+ before it reads anything (`test.reload.login`), and is then held to everything the first was on
3009
3009
  connect: the methods its declarations call for, the agent application's report, a capacity stated to it, its
3010
3010
  snapshot read as the connect snapshot was, and its audio opened afresh on a task carried with
3011
3011
  `audio: "started"`, since the first client's session died with it. The result also says which rules the
3012
- run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
3012
+ run evaluated, pass or fail, in `rulesTested`: the validators' as each was applied, the stream's
3013
3013
  as each case was considered, so a test that needs a rule to have run asserts it there rather than
3014
3014
  inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
3015
3015
  gap and not a pass. With the audio open on a softphone,
3016
- the drive mutes it for one second and reports the leg through `recordStep`, begun and then ended,
3017
- expecting each report `recorded` with the provider-selected history `at` (`drive.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
3016
+ the test mutes it for one second and reports the leg through `recordStep`, begun and then ended,
3017
+ expecting each report `recorded` with the provider-selected history `at` (`test.recordStep.failed`, `.rejected`, `result.recordStep.at`); then it mutes again and
3018
3018
  ends the call muted, as agents do, so the leg is open when the audio ends, the provider closes it
3019
3019
  in the completing publication or the open entry is refused (`task.history.muted.open`),
3020
- and the drive's closing report after the audio ended is expected `recorded` and to change nothing:
3021
- a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
3020
+ and the test's closing report after the audio ended is expected `recorded` and to change nothing:
3021
+ a record restated afterwards with that leg's duration altered is named (`test.recordStep.overwritten`).
3022
3022
  Where the provider restates the task's record afterwards, each leg is in it or the hole is named
3023
- (`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
3024
- does, the drive reloads the agent application as a reload happens: the first client is unsubscribed,
3025
- disconnected and its session closed (`drive.reload.handover`), and only then is a second adapter
3023
+ (`test.recordStep.history`). Given `rebuild`, a way to build the adapter again as an agent application reload
3024
+ does, the test reloads the agent application as a reload happens: the first client is unsubscribed,
3025
+ disconnected and its session closed (`test.reload.handover`), and only then is a second adapter
3026
3026
  built and connected for the same login with the same context and the same `store`; its snapshot
3027
3027
  must carry the task with that leg at its acknowledged provider timestamp and the reported actor, and the run goes on with the second as its
3028
3028
  connection to the end. A platform that holds the record hands it back, and an adapter that composed
3029
- the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
3029
+ the record in memory has nothing and is named (`test.reload.snapshot`, `test.reload.history`,
3030
3030
  `.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
3031
3031
  before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
3032
3032
  still at work is named by the stream's own rules (`stream.snapshot.phase`, `.history`,
3033
3033
  `.audio`) and a reload is a place those rules keep working, not one where they stop. The second
3034
3034
  adapter is held to what the first was: the same provider
3035
- (`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
3035
+ (`test.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
3036
3036
  of the entries the first had published -- a record once read is not unread across a reload either.
3037
3037
  A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
3038
3038
  survives it; only a second object separates kept-in-the-store from never-lost. And because the
@@ -3041,31 +3041,33 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
3041
3041
  `secrets`, so the reload is a restore before it is anything else: an adapter that holds a session
3042
3042
  has already put what would rebuild it into the secrets store, from the moment it was handed one,
3043
3043
  not only when a flow completes -- the session is the adapter's, the store is the agent application's, and a
3044
- agent application reload is exactly when no flow will run. And once the task has ended, the store the drive
3044
+ agent application reload is exactly when no flow will run. And once the task has ended, the store the test
3045
3045
  handed the adapter holds no key naming the task's assignment id, delimited, never as a run of characters inside
3046
- another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
3046
+ another id (`test.store.retained`); an adapter whose keys never named the task leaves the rule
3047
3047
  unevaluated, and the result says so rather than passing it: a task's keys go
3048
3048
  with the task, or whatever comes next inherits them. Outside the
3049
3049
  interaction phases with `hold` still declared -- in
3050
3050
  `confirmed`, where the provider publishes it, and in `completing` once this agent's interaction has ended -- the
3051
- drive sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
3051
+ test sends `hold` past the validator that would hold it back, and expects `failed`: the adapter
3052
3052
  is the second gate on a control on the contact, and one that applies it outside this task's
3053
- current interaction is named (`drive.command.interaction`). The drive cannot put a task into a phase the provider
3053
+ current interaction is named (`test.command.interaction`). With the task on hold, the test sends `end-call` past the
3054
+ validator the same way and expects `failed`, naming an adapter that ends the agent's part with the caller in the
3055
+ hold (`test.command.held`). The test cannot put a task into a phase the provider
3054
3056
  never publishes, so a provider that goes straight from `pending` to `in-progress` is checked in
3055
3057
  `completing` alone. A task the agent completes is
3056
- completed from wherever it stands once the drive has nothing left to do on it -- from `completing`
3058
+ completed from wherever it stands once the test has nothing left to do on it -- from `completing`
3057
3059
  after an `end-call`, or from `in-progress` where there is no call to end, which is every chat and
3058
3060
  email and a voice task offering no `endCall` -- so a conversation reaches its end as a call does.
3059
- The drive stops where the task offers no way on and says nothing about what it could not reach. A
3061
+ The test stops where the task offers no way on and says nothing about what it could not reach. A
3060
3062
  softphone adapter written for a browser needs its audio APIs supplied by whatever runs the
3061
- harness: the drive opens audio outside a browser, and a provider that quietly stopped carrying audio
3063
+ harness: the test opens audio outside a browser, and a provider that quietly stopped carrying audio
3062
3064
  where `AudioContext` was missing would be lying about the one thing the channel is for. It is off by default because it issues
3063
3065
  commands against whatever platform the adapter is connected to: turn it on against a test backend.
3064
3066
 
3065
3067
  ```ts
3066
- const driven = await exerciseAdapter(adapter, context, { collectOnly: true, drive: true });
3067
- expect(driven.violations).toEqual([]);
3068
- assertReached(driven, ["task.onCall", "task.audio", "event.task-ended"]);
3068
+ const tested = await testAdapter(adapter, context, { collectOnly: true, withCall: true });
3069
+ expect(tested.violations).toEqual([]);
3070
+ assertReached(tested, ["task.onCall", "task.audio", "event.task-ended"]);
3069
3071
  ```
3070
3072
  `assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
3071
3073
  never met, so a test that meant to check a team member list cannot pass on a fixture that never produced one.
@@ -3096,7 +3098,7 @@ cannot be established from TypeScript structure alone.
3096
3098
  | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no team member list for a login that no longer leads. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
3097
3099
  | `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
3098
3100
  | `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
3099
- | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
3101
+ | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `testAdapter` result. |
3100
3102
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
3101
3103
  | `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
3102
3104
  | `stillHost(report?, guarantees?, mute?)` | An agent application that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a softphone voice adapter, and never for a desk phone. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.112",
3
+ "version": "0.1.114",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",