@xema/omni-protocol 0.1.28 → 0.1.30

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
@@ -15,6 +15,7 @@ system.
15
15
  | `src/index.ts` | The TypeScript declarations. |
16
16
  | `src/validation.ts` | Runtime validators Omni applies to adapter output. |
17
17
  | `src/testing.ts` | Conformance helpers an adapter runs against its own test state. |
18
+ | `tests/` | One test file per source module, plus the guards over the guide and the repository's own text. |
18
19
 
19
20
  ## Entry points
20
21
 
package/dist/index.d.ts CHANGED
@@ -607,6 +607,13 @@ export type Task<C extends Channel = Channel> = {
607
607
  /** The person or entity on the other end of this task. Who the task is with; `contacts` on the snapshot is the directory. */
608
608
  party?: Contact;
609
609
  phase: TaskPhase;
610
+ /**
611
+ * How this offer is accepted, stated on the pending task rather than the offer so a reconnect
612
+ * snapshot says it too: an offer the host never received is not accepted on the person's behalf
613
+ * for want of a word. Required while `pending` when Omni said it may auto-accept
614
+ * (`autoAcceptTasks: true`), forbidden when it said not, and absent past `pending`.
615
+ */
616
+ acceptance?: AcceptanceMode;
610
617
  /** The identifier an agent reads back to a customer, where the provider has one. */
611
618
  reference?: string;
612
619
  attributes?: TaskAttribute[];
@@ -1077,7 +1084,6 @@ export type ProviderEvent<C extends Channel = Channel> = {
1077
1084
  } | {
1078
1085
  type: "task-offered";
1079
1086
  task: Task<C>;
1080
- acceptanceMode?: AcceptanceMode;
1081
1087
  allocationExpiresAt?: IsoTimestamp;
1082
1088
  preparationEndsAt?: IsoTimestamp;
1083
1089
  } | {
@@ -1102,6 +1108,11 @@ export type ProviderEvent<C extends Channel = Channel> = {
1102
1108
  } | {
1103
1109
  type: "queue-summary";
1104
1110
  summary: QueueSummary;
1111
+ } | {
1112
+ type: "diagnostic";
1113
+ expected: string;
1114
+ observed: string;
1115
+ taskId?: TaskId;
1105
1116
  } | {
1106
1117
  type: "team-updated";
1107
1118
  team: TeamRoster;
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.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.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"];
11
11
  export type ContractSubject = (typeof STATE_SUBJECTS)[number] | `event.${ProviderEvent["type"]}`;
12
12
  export interface AdapterContractResult {
13
13
  events: ProviderEventEnvelope[];
package/dist/testing.js CHANGED
@@ -16,6 +16,7 @@ const STATE_SUBJECTS = [
16
16
  "task.lead",
17
17
  "task.assisting",
18
18
  "task.media",
19
+ "task.acceptance",
19
20
  "task.dispositions",
20
21
  "task.destinations",
21
22
  "task.custom",
@@ -32,7 +33,7 @@ const STATE_SUBJECTS = [
32
33
  // `ProviderEvent` without a row here, or a row it lacks, is a compile error.
33
34
  const EVENT_TYPES = {
34
35
  snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
35
- "task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true,
36
+ "task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true, diagnostic: true,
36
37
  "team-updated": true, "contacts-updated": true, "calendar-updated": true,
37
38
  };
38
39
  const CONTRACT_SUBJECTS = [
@@ -61,6 +62,8 @@ function observeTask(value, seen) {
61
62
  seen.add("task.assisting");
62
63
  if (value.media !== undefined)
63
64
  seen.add("task.media");
65
+ if (value.acceptance !== undefined)
66
+ seen.add("task.acceptance");
64
67
  const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
65
68
  if (isRecord(capabilities.dispositions))
66
69
  seen.add("task.dispositions");
@@ -278,6 +281,12 @@ export async function exerciseAdapter(adapter, context, options = {}) {
278
281
  unsubscribe = connection.subscribe(envelope => {
279
282
  observeEvent(envelope, seen);
280
283
  violations.push(...validateEventEnvelope(envelope, adapter.manifest, "event", reader()));
284
+ // A diagnostic is informational to a host and a failure to a conformance run: the platform
285
+ // under test broke a rule the adapter relies on, and a green result must not paper over it.
286
+ if (isRecord(envelope?.event) && envelope.event.type === "diagnostic") {
287
+ violations.push({ rule: "diagnostic.raised", path: "event.diagnostic",
288
+ message: `the provider reported a diagnostic: expected ${String(envelope.event.expected)}; observed ${String(envelope.event.observed)}` });
289
+ }
281
290
  if (eventNamesUsers(envelope))
282
291
  requireMethod(live, "describeUsers", "an event publishes a UserId");
283
292
  // Cross-event rules apply once the stream has a beginning: the connect snapshot.
@@ -15,6 +15,8 @@ export interface TaskValidationContext {
15
15
  channel: string;
16
16
  /** The level ids in force, from the manifest. The defaults when absent. */
17
17
  levels?: readonly string[];
18
+ /** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether a pending task states its `acceptance`. */
19
+ autoAcceptTasks?: boolean;
18
20
  }
19
21
  export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
20
22
  /**
@@ -36,7 +38,7 @@ export interface ReaderContext {
36
38
  levels?: readonly string[];
37
39
  /** The login's `loginId`. A snapshot or event naming another belongs to a login that is gone. */
38
40
  loginId?: string;
39
- /** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether `task-offered` carries an `acceptanceMode`. */
41
+ /** `ConnectContext.autoAcceptTasks` as sent, absent meaning `true`: whether a pending task states its `acceptance`. */
40
42
  autoAcceptTasks?: boolean;
41
43
  }
42
44
  export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
@@ -729,6 +729,20 @@ function validateTaskInto(task, context, path, into) {
729
729
  into.filled(task.title, "task.title", `${path}.title`, "a task needs a title");
730
730
  into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
731
731
  into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`);
732
+ // Acceptance is the offer's word, carried on the pending task so a snapshot can say it: it
733
+ // travels exactly when Omni said tasks may be auto-accepted, and only while the task is pending.
734
+ if (task.acceptance !== undefined) {
735
+ into.oneOf(task.acceptance, ACCEPTANCE_MODES, "task.acceptance", `${path}.acceptance`);
736
+ if (task.phase !== "pending") {
737
+ into.add("task.acceptance.unexpected", `${path}.acceptance`, "acceptance is an offer's word; a task past pending has been accepted");
738
+ }
739
+ else if (context.autoAcceptTasks === false) {
740
+ into.add("task.acceptance.unexpected", `${path}.acceptance`, "autoAcceptTasks is off, so every task requires agent acceptance and a pending task carries no acceptance");
741
+ }
742
+ }
743
+ else if (task.phase === "pending" && context.autoAcceptTasks === true) {
744
+ into.add("task.acceptance.required", `${path}.acceptance`, "autoAcceptTasks is on, so a pending task states how it is accepted");
745
+ }
732
746
  into.oneOf(task.completionMode, COMPLETION_MODES, "task.completionMode", `${path}.completionMode`);
733
747
  // The allowance is coupled to the mode: a provider that will complete the task itself is going
734
748
  // to act on the allowance, so it must state one; a provider waiting for `complete` may omit it
@@ -1009,7 +1023,7 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot", context
1009
1023
  const seen = new Set();
1010
1024
  let assisting;
1011
1025
  snapshot.tasks.forEach((task, index) => {
1012
- validateTaskInto(task, { channel, levels }, `${path}.tasks[${index}]`, into);
1026
+ validateTaskInto(task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${path}.tasks[${index}]`, into);
1013
1027
  // A lead assists one call at a time.
1014
1028
  if (isPlainObject(task) && task.assisting !== undefined) {
1015
1029
  if (assisting !== undefined)
@@ -1196,28 +1210,18 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1196
1210
  validateBreakState(event.break, `${at}.break`, into);
1197
1211
  break;
1198
1212
  case "task-offered":
1199
- validateTaskInto(event.task, { channel, levels }, `${at}.task`, into);
1213
+ validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
1200
1214
  // An offer introduces work that is not yet under way; work in progress arrives only on a snapshot.
1201
1215
  if (isPlainObject(event.task) && typeof event.task.phase === "string") {
1202
1216
  into.require(OFFERABLE_PHASES.includes(event.task.phase), "event.taskOffered.phase", `${at}.task.phase`, `task-offered introduces a task as ${OFFERABLE_PHASES.join(", ")}, never as ${event.task.phase}`);
1203
1217
  }
1204
- if (event.acceptanceMode !== undefined) {
1205
- into.oneOf(event.acceptanceMode, ACCEPTANCE_MODES, "event.taskOffered.acceptanceMode", `${at}.acceptanceMode`);
1206
- }
1207
- // The mode travels exactly when Omni said tasks may be auto-accepted.
1208
- if (context.autoAcceptTasks === true) {
1209
- into.require(event.acceptanceMode !== undefined, "event.taskOffered.acceptanceMode.required", `${at}.acceptanceMode`, "autoAcceptTasks is on, so task-offered carries an acceptanceMode");
1210
- }
1211
- else if (context.autoAcceptTasks === false) {
1212
- into.require(event.acceptanceMode === undefined, "event.taskOffered.acceptanceMode.unexpected", `${at}.acceptanceMode`, "autoAcceptTasks is off, so every task requires agent acceptance and task-offered carries no acceptanceMode");
1213
- }
1214
1218
  for (const field of ["allocationExpiresAt", "preparationEndsAt"]) {
1215
1219
  if (event[field] !== undefined)
1216
1220
  into.timestamp(event[field], `event.taskOffered.${field}`, `${at}.${field}`);
1217
1221
  }
1218
1222
  break;
1219
1223
  case "task-updated":
1220
- validateTaskInto(event.task, { channel, levels }, `${at}.task`, into);
1224
+ validateTaskInto(event.task, { channel, levels, autoAcceptTasks: context.autoAcceptTasks }, `${at}.task`, into);
1221
1225
  break;
1222
1226
  case "task-media-started":
1223
1227
  into.require(isTaskId(event.taskId), "event.taskMediaStarted.taskId", `${at}.taskId`, "a task id is required");
@@ -1238,6 +1242,12 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
1238
1242
  into.require(typeof event.html === "string", "event.announcement.html", `${at}.html`, "html must be a string when present");
1239
1243
  }
1240
1244
  break;
1245
+ case "diagnostic":
1246
+ into.filled(event.expected, "event.diagnostic.expected", `${at}.expected`, "a diagnostic states the rule that was broken, as a sentence");
1247
+ into.filled(event.observed, "event.diagnostic.observed", `${at}.observed`, "a diagnostic states what was observed instead");
1248
+ if (event.taskId !== undefined)
1249
+ into.require(isTaskId(event.taskId), "event.diagnostic.taskId", `${at}.taskId`, "taskId must be a task id when present");
1250
+ break;
1241
1251
  case "queue-summary":
1242
1252
  validateQueueSummary(event.summary, `${at}.summary`, into);
1243
1253
  break;
package/guide.md CHANGED
@@ -604,6 +604,7 @@ type Task<C extends Channel = Channel> = {
604
604
  browsers: TaskBrowser[];
605
605
  party?: Contact;
606
606
  phase: TaskPhase;
607
+ acceptance?: AcceptanceMode;
607
608
  reference?: string;
608
609
  attributes?: TaskAttribute[];
609
610
  handlingHistory?: TaskHandlingStep[];
@@ -941,7 +942,6 @@ type ProviderEvent =
941
942
  | {
942
943
  type: "task-offered";
943
944
  task: Task;
944
- acceptanceMode?: AcceptanceMode;
945
945
  allocationExpiresAt?: IsoTimestamp;
946
946
  preparationEndsAt?: IsoTimestamp;
947
947
  }
@@ -951,6 +951,7 @@ type ProviderEvent =
951
951
  | { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
952
952
  | { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
953
953
  | { type: "queue-summary"; summary: QueueSummary }
954
+ | { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
954
955
  | { type: "team-updated"; team: TeamRoster }
955
956
  | { type: "contacts-updated"; contacts: Contact[] }
956
957
  | { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
@@ -1830,7 +1831,7 @@ Creates one live provider connection for the signed-in agent.
1830
1831
  | --- | --- |
1831
1832
  | `protocolVersion` | Version negotiated before authentication. Fixed for this login. |
1832
1833
  | `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
1833
- | `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. When `true`, `task-offered` carries an `acceptanceMode`; when `false`, every task requires agent acceptance. |
1834
+ | `autoAcceptTasks` | Agent provisioning policy relayed to the provider at login. Treated as `true` when omitted. 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()`. |
1834
1835
  | `host` | The host'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 host reports, the adapter decides**. |
1835
1836
  | `signal` | Optional cancellation signal. Stop startup promptly when aborted and do not begin new work. |
1836
1837
  | `log` | Optional structured logging callback. Never include credentials, tokens, or sensitive contact data. |
@@ -1974,13 +1975,15 @@ deliver again once it has gone quiet. Hold the capacity and deliver when work ar
1974
1975
  **4. A provider offers a task.** The provider emits `task-offered` within the stated capacity.
1975
1976
 
1976
1977
  **5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
1977
- agent acceptance. When it is `true`, the event's `acceptanceMode` states the provider's
1978
+ agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
1978
1979
  intent.
1979
1980
 
1980
1981
  ### Acceptance modes
1981
1982
 
1982
1983
  During login, Omni sends the agent's `autoAcceptTasks` value to the provider. When it is `true`, the
1983
- provider includes an acceptance directive with each allocation:
1984
+ provider states `acceptance` on each pending task — on the task rather than the offer, so a
1985
+ reconnect snapshot says it too and an offer the host never received is not accepted on the
1986
+ person's behalf for want of a word:
1984
1987
 
1985
1988
  | Directive | Contract |
1986
1989
  | --- | --- |
@@ -1988,7 +1991,7 @@ provider includes an acceptance directive with each allocation:
1988
1991
  | `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. A host that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
1989
1992
  | `automatic` | Omni accepts immediately without agent interaction. |
1990
1993
 
1991
- When Omni sent `autoAcceptTasks: false`, the provider omits `acceptanceMode` and every task
1994
+ When Omni sent `autoAcceptTasks: false`, the provider omits `acceptance` and every task
1992
1995
  requires agent acceptance. The two are never confused on the wire: `consent` is always the
1993
1996
  provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
1994
1997
  no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
@@ -1998,19 +2001,19 @@ press is Omni's doing, not the provider's.
1998
2001
  gone ready is telling the deployment they are working. Requiring a press before every contact is
1999
2002
  the exception a provisioning file asks for, not the state it falls into when a flag is missing.
2000
2003
 
2001
- Nothing is given away by that default. `acceptanceMode` is the provider's own control and outranks
2004
+ Nothing is given away by that default. `acceptance` is the provider's own control and outranks
2002
2005
  it: `consent` puts the decision back in the agent's hands for any task where it
2003
2006
  belongs, whatever the host was configured with.
2004
2007
 
2005
2008
  An automatically accepted task still arrives through `task-offered`.
2006
2009
 
2007
- Agent-initiated work arrives through `task-offered` with
2008
- `acceptanceMode: "automatic"`.
2010
+ Agent-initiated work arrives through `task-offered` with the task's
2011
+ `acceptance: "automatic"`.
2009
2012
 
2010
2013
  ### Pending
2011
2014
 
2012
2015
  A task in the `pending` phase has been **offered to the agent and not yet accepted**. Omni applies
2013
- `autoAcceptTasks` and the allocation's `acceptanceMode` to decide whether acceptance is
2016
+ `autoAcceptTasks` and the task's `acceptance` to decide whether acceptance is
2014
2017
  automatic or requires the agent. A provider that requires automatic acceptance still emits
2015
2018
  `task-offered`; it does not introduce new work as `in-progress`.
2016
2019
 
@@ -2019,8 +2022,7 @@ declare const task: Task;
2019
2022
 
2020
2023
  const allocation = {
2021
2024
  type: "task-offered",
2022
- task,
2023
- acceptanceMode: "consent",
2025
+ task: { ...task, phase: "pending", acceptance: "consent" },
2024
2026
  allocationExpiresAt: "2026-08-25T10:41:07.000Z",
2025
2027
  preparationEndsAt: "2026-08-25T10:40:37.000Z",
2026
2028
  } satisfies Extract<ProviderEvent, { type: "task-offered" }>;
@@ -2083,6 +2085,7 @@ time. Runtime conformance checks also require the task channel to match its prov
2083
2085
  | `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
2084
2086
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
2085
2087
  | `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. |
2088
+ | `acceptance` | How this offer is accepted — `no-preference`, `consent`, or `automatic` — stated on the pending task so a reconnect snapshot says it too. Required while `pending` when `autoAcceptTasks` was `true`, forbidden when it was `false`, and absent past `pending`. See **Acceptance modes**. |
2086
2089
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
2087
2090
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
2088
2091
  | `wrapAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. Required under `provider-automatic`, where the provider acts on it. Optional under `agent-command`: omitted says the provider imposes no deadline, and Omni counts nothing down. |
@@ -2134,12 +2137,12 @@ The canonical task transitions are:
2134
2137
  | Any phase | Provider emits `task-ended` | Removed |
2135
2138
 
2136
2139
  Allocation, acceptance, and progress are distinct. Acceptance follows `autoAcceptTasks` and the
2137
- allocation's `acceptanceMode`, moving the task from `pending` to `confirmed`. The provider reports
2140
+ task's `acceptance`, moving the task from `pending` to `confirmed`. The provider reports
2138
2141
  subsequent transitions to `preparing` or `in-progress`; Omni does not infer them from the acceptance
2139
2142
  command.
2140
2143
 
2141
2144
  **A task is never its audio.** A voice task is the allocation: the call is offered when it is
2142
- routed to the agent and accepted as `acceptanceMode` dictates, and its presence and phase follow
2145
+ routed to the agent and accepted as its `acceptance` dictates, and its presence and phase follow
2143
2146
  the provider's reports about the work — never the audio. Wherever audio moves — an offer, a hold, a
2144
2147
  consult, a conference leg joining or leaving, a transfer, a callback — the media follows
2145
2148
  separately, arriving on `task-media-started`, attaching through `openMedia` and ending with
@@ -3464,7 +3467,8 @@ Omni apply state the snapshot already superseded.
3464
3467
  `transport-status` is the only signal Omni has that a transport died. An adapter must emit
3465
3468
  `transport-status` with `connecting` or `error` as soon as it loses its transport, rather than
3466
3469
  leaving a stale `active` in place while it retries internally; Omni cannot distinguish a quiet
3467
- healthy provider from a dead one.
3470
+ healthy provider from a dead one. And what Omni is told, the agent is shown — see **The status is
3471
+ seen, not merely known** under `transport-status`.
3468
3472
 
3469
3473
  #### Requesting a resync
3470
3474
 
@@ -3521,6 +3525,12 @@ authentication state, not here.
3521
3525
  adapter that leaves a stale `active` in place is telling Omni to keep waiting for work that cannot
3522
3526
  come — see **Liveness**.
3523
3527
 
3528
+ **The status is seen, not merely known.** A host renders the transport's status where the agent
3529
+ works — `connecting` from the moment it is reported, `error` with what revives it — and never a
3530
+ healthy workspace over a transport that cannot serve it: an agent talking on a desk whose transport
3531
+ died was an incident, and it happened because the desk knew and did not say. The same obligation
3532
+ covers a `diagnostic`: shown where the agent works, and counted.
3533
+
3524
3534
  ### `break-state`
3525
3535
 
3526
3536
  Replaces this provider's complete `break` object. Its `approval` uses the canonical
@@ -3548,8 +3558,8 @@ multi-provider break**.
3548
3558
  ### `task-offered`
3549
3559
 
3550
3560
  Offers a task to Omni without a separate offer acknowledgement. An offer does not accept
3551
- the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the event's
3552
- `acceptanceMode`. `task-offered` must not introduce a task as `in-progress`; only a reconnect or
3561
+ the task: when its phase is `pending`, Omni applies `autoAcceptTasks` and the task's
3562
+ `acceptance`. `task-offered` must not introduce a task as `in-progress`; only a reconnect or
3553
3563
  resync snapshot may report work already in progress. The provider should include the task in later
3554
3564
  snapshots until it ends.
3555
3565
 
@@ -3604,6 +3614,26 @@ Publishes an agent-facing message. `text` is always required and is the accessib
3604
3614
  Optional HTML is sanitized by Omni. `announcedAt` and optional `expiresAt` are RFC-3339 times with
3605
3615
  explicit timezones.
3606
3616
 
3617
+ ### `diagnostic`
3618
+
3619
+ The provider's report that something its platform answered broke a rule the adapter relies on — a
3620
+ state read that contradicted itself, a `task-ended` naming a task nobody held, a party arriving
3621
+ without a name. `expected` is the rule as a sentence, `observed` is what came instead, and
3622
+ `taskId` names the task where there is one. **Informational, never behavioural**: the provider has
3623
+ already done the safe thing before it speaks, and nothing in the task or break machinery reacts to
3624
+ a diagnostic. It exists so that the loudness reaches a person — a host renders each one where the
3625
+ agent works and keeps a count an operator can read, because a healthy workspace over a shouting
3626
+ console is the silence problem one layer up. One event per occurrence; the host counts, the
3627
+ provider does not batch. `exerciseAdapter` treats a diagnostic delivered during a run as a
3628
+ violation (`diagnostic.raised`): a conformance run against a platform that is breaking its rules
3629
+ fails loudly rather than passing with a note.
3630
+
3631
+ | Field | Contract |
3632
+ | --- | --- |
3633
+ | `expected` | Required. The rule that was broken, as a sentence a person can read. |
3634
+ | `observed` | Required. What the platform answered instead. |
3635
+ | `taskId` | Optional. The task concerned, where there is one. |
3636
+
3607
3637
  ### `queue-summary`
3608
3638
 
3609
3639
  Publishes the provider's current dashboard contribution. Omni combines only the latest summary from
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "scripts": {
31
31
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
32
32
  "build": "pnpm run clean && tsc -p tsconfig.json",
33
- "test": "tsc -p tsconfig.type-tests.json && vitest run"
33
+ "test": "tsc -p tsconfig.test.json && vitest run"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^22.20.1",