@xema/omni-protocol 0.1.14 → 0.1.15

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/README.md CHANGED
@@ -30,7 +30,8 @@ An adapter is loaded from a separate package and may be compiled against a diffe
30
30
  version, so its output is untrusted input. Every validator takes `unknown` and returns every
31
31
  violation it found rather than throwing on the first, so a caller reports all of them at once.
32
32
  Validating a snapshot before it replaces provider state is what stops a malformed task reaching
33
- the agent's workspace.
33
+ the agent's workspace; validating a result with `validateResult(result, method)` before acting on
34
+ it is what stops a status the host does not know being shown as an outcome.
34
35
 
35
36
  ```ts
36
37
  const violations = validateSnapshot(snapshot, manifest);
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateSnapshot, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateResult, validateSnapshot, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -245,7 +245,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
245
245
  // Capacity is stated, not requested: nothing may be allocated until it is, so a connection
246
246
  // that will not accept one is a connection nothing can be given to.
247
247
  const capacity = await connection.setCapacity({ count: 1 });
248
- if (capacity.status === "failed") {
248
+ const malformed = validateResult(capacity, "setCapacity", "connection.setCapacity");
249
+ violations.push(...malformed);
250
+ // A refusal is read only from a result that has the shape of one.
251
+ if (malformed.length === 0 && capacity.status === "failed") {
249
252
  violations.push({
250
253
  rule: "connection.setCapacity.failed",
251
254
  path: "connection.setCapacity",
@@ -34,4 +34,13 @@ export interface ReaderContext {
34
34
  export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
35
35
  export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
36
36
  export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
37
+ /** The connection methods whose results `validateResult` knows. */
38
+ export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia";
39
+ /**
40
+ * Validates what a connection method answered. A result is untrusted for the same reason a
41
+ * snapshot is: it comes from an adapter that may be compiled against another version, and Omni
42
+ * shows the agent what it says. A status the method does not answer, a failure status without a
43
+ * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
44
+ */
45
+ export declare function validateResult(result: unknown, method: ResultMethod, path?: string): ProtocolViolation[];
37
46
  export declare function validateAuthenticationState(state: unknown, path?: string): ProtocolViolation[];
@@ -9,7 +9,7 @@
9
9
  // Each list is pinned to its type both ways -- a member the type lacks, or a member the list
10
10
  // lacks, fails to compile -- so what the validators accept cannot drift from what the
11
11
  // declarations say.
12
- import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
12
+ import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
13
13
  export class ProtocolConformanceError extends Error {
14
14
  violations;
15
15
  constructor(violations, summary = "Adapter violates the Omni protocol") {
@@ -895,9 +895,7 @@ function validateTaskOutcome(value, path, into) {
895
895
  into.add("event.taskEnded.outcome.failed", `${path}.failure`, "a failed outcome must carry a failure");
896
896
  }
897
897
  else {
898
- into.filled(value.failure.code, "failure.code", `${path}.failure.code`, "a failure needs a code");
899
- into.filled(value.failure.message, "failure.message", `${path}.failure.message`, "a failure needs a message");
900
- into.require(typeof value.failure.retryable === "boolean", "failure.retryable", `${path}.failure.retryable`, "a failure must say whether it is retryable");
898
+ validateFailureInto(value.failure, `${path}.failure`, into);
901
899
  }
902
900
  break;
903
901
  default:
@@ -1019,6 +1017,75 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1019
1017
  return into.violations;
1020
1018
  }
1021
1019
  // ---------------------------------------------------------------------------
1020
+ // Results. A result crosses the same boundary a snapshot does, from an adapter that may be
1021
+ // compiled against another version, and Omni shows the agent what it says.
1022
+ // ---------------------------------------------------------------------------
1023
+ /** A `ProtocolFailure`, wherever one appears: on a result, or on a task's failed outcome. */
1024
+ function validateFailureInto(value, path, into) {
1025
+ if (!isPlainObject(value)) {
1026
+ into.add("failure.shape", path, "a failure must be an object");
1027
+ return;
1028
+ }
1029
+ if (into.filled(value.code, "failure.code", `${path}.code`, "a failure needs a code")) {
1030
+ // A provider names its own codes freely; the `omni.` namespace is the contract's, and a code
1031
+ // in it that the contract lacks is one Omni would show without knowing what it means.
1032
+ if (value.code.startsWith("omni.")) {
1033
+ into.require(OMNI_FAILURE_CODES.includes(value.code), "failure.code.unknown", `${path}.code`, `not a contract failure code: ${value.code}`);
1034
+ }
1035
+ }
1036
+ into.filled(value.message, "failure.message", `${path}.message`, "a failure needs a message");
1037
+ into.require(typeof value.retryable === "boolean", "failure.retryable", `${path}.retryable`, "a failure must say whether it is retryable");
1038
+ if (value.retryAfterMs !== undefined) {
1039
+ into.require(typeof value.retryAfterMs === "number" && Number.isFinite(value.retryAfterMs) && value.retryAfterMs >= 0, "failure.retryAfterMs", `${path}.retryAfterMs`, "retryAfterMs must be a non-negative number when present");
1040
+ }
1041
+ }
1042
+ // Pinned to the result unions: each method's one success status, and the status that carries a
1043
+ // failure. A method added to `Connection` without a row here is a compile error at the call site.
1044
+ const RESULT_STATUSES = {
1045
+ execute: { success: "applied", failure: "failed" },
1046
+ dial: { success: "dialled", failure: "failed" },
1047
+ setCapacity: { success: "accepted", failure: "failed" },
1048
+ requestBreak: { success: "requested", failure: "failed" },
1049
+ commitBreak: { success: "committed", failure: "failed" },
1050
+ cancelBreak: { success: "cancelled", failure: "failed" },
1051
+ endBreak: { success: "ended", failure: "failed" },
1052
+ executeTeamBreak: { success: "applied", failure: "failed" },
1053
+ executeTeamConsult: { success: "applied", failure: "failed" },
1054
+ openMedia: { success: "opened", failure: "unavailable" },
1055
+ };
1056
+ /**
1057
+ * Validates what a connection method answered. A result is untrusted for the same reason a
1058
+ * snapshot is: it comes from an adapter that may be compiled against another version, and Omni
1059
+ * shows the agent what it says. A status the method does not answer, a failure status without a
1060
+ * failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
1061
+ */
1062
+ export function validateResult(result, method, path = "result") {
1063
+ const into = new Collector();
1064
+ const statuses = RESULT_STATUSES[method];
1065
+ if (!isPlainObject(result)) {
1066
+ into.add("result.shape", path, `${method} must answer an object`);
1067
+ return into.violations;
1068
+ }
1069
+ if (result.status === statuses.success) {
1070
+ into.require(result.failure === undefined, "result.failure.unexpected", `${path}.failure`, `${statuses.success} carries no failure`);
1071
+ if (method === "openMedia") {
1072
+ into.require(isPlainObject(result.session), "result.session", `${path}.session`, "opened carries the media session");
1073
+ }
1074
+ }
1075
+ else if (result.status === statuses.failure) {
1076
+ if (result.failure === undefined) {
1077
+ into.add("result.failure.required", `${path}.failure`, `${statuses.failure} carries the failure that says why`);
1078
+ }
1079
+ else {
1080
+ validateFailureInto(result.failure, `${path}.failure`, into);
1081
+ }
1082
+ }
1083
+ else {
1084
+ into.add("result.status", `${path}.status`, `${method} answers ${statuses.success} or ${statuses.failure}, not ${String(result.status)}`);
1085
+ }
1086
+ return into.violations;
1087
+ }
1088
+ // ---------------------------------------------------------------------------
1022
1089
  // Authentication.
1023
1090
  // ---------------------------------------------------------------------------
1024
1091
  function validateUser(value, rule, path, into) {
package/guide.md CHANGED
@@ -2225,7 +2225,8 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
2225
2225
  declares `dial`.
2226
2226
 
2227
2227
  - `destination` is the original number selected or entered by the agent.
2228
- - `source` is `contact` or `manual` and must comply with `destinationPolicy`.
2228
+ - The provider holds `destination` to its declared `destinationPolicy`: under `contacts-only`, a
2229
+ number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
2229
2230
  - `dialled` confirms that outbound call creation completed.
2230
2231
  - `failed` contains a `ProtocolFailure` and confirms no call was placed.
2231
2232
 
@@ -2934,6 +2935,11 @@ Applies a `TaskCommandRequest` to one provider-local task.
2934
2935
  deciding a member's break that another lead has already decided is `applied` when the decisions
2935
2936
  agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
2936
2937
  in effect is `committed` for the same reason.
2938
+ - **A result is untrusted for the same reason a snapshot is.** It comes from an adapter that may be
2939
+ compiled against another version, and Omni shows the agent what it says. Omni validates it at
2940
+ the boundary with `validateResult(result, "execute")` — a status the method does not answer, a
2941
+ `failed` without its failure, a success carrying one, or an `omni.` code this contract lacks is
2942
+ refused, and the command is treated as unsettled.
2937
2943
  - **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
2938
2944
  promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
2939
2945
  see **An unsettled result is unknown**. `failed` must never be returned for something the
@@ -3176,6 +3182,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
3176
3182
  | `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
3177
3183
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
3178
3184
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
3185
+ | `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. |
3179
3186
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. |
3180
3187
 
3181
3188
  Each returns `ProtocolViolation[]` rather than throwing, so a caller can report every problem at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",