@xema/omni-protocol 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -375,7 +375,22 @@ export interface TaskHandlingStep {
375
375
  */
376
376
  by?: UserId;
377
377
  }
378
- export interface Task<C extends Channel = Channel> {
378
+ /**
379
+ * How the task ends, and how long after handling the provider allows for it.
380
+ *
381
+ * The allowance is coupled to the mode. Under `provider-automatic` the provider acts on it, so it
382
+ * is required. Under `agent-command` the provider will not complete the task itself, so it may
383
+ * omit the allowance to say it imposes no deadline -- and Omni then counts nothing down. Omitted
384
+ * and `0` are different claims: no deadline to see, and a deadline of now.
385
+ */
386
+ export type TaskCompletion = {
387
+ completionMode: "agent-command";
388
+ completionAllowance?: DurationSeconds;
389
+ } | {
390
+ completionMode: "provider-automatic";
391
+ completionAllowance: DurationSeconds;
392
+ };
393
+ export type Task<C extends Channel = Channel> = {
379
394
  id: TaskId;
380
395
  title: string;
381
396
  channel: C;
@@ -387,11 +402,9 @@ export interface Task<C extends Channel = Channel> {
387
402
  phase: TaskPhase;
388
403
  /** The identifier an agent reads back to a customer, where the provider has one. */
389
404
  reference?: string;
390
- completionMode: CompletionMode;
391
- completionAllowance: DurationSeconds;
392
405
  attributes?: TaskAttribute[];
393
406
  handlingHistory?: TaskHandlingStep[];
394
- }
407
+ } & TaskCompletion;
395
408
  /** What the provider wants of Omni's acceptance policy for one offer. */
396
409
  export type AcceptanceMode = "no-preference" | "require-agent-acceptance" | "require-automatic-acceptance";
397
410
  export type TaskOutcome = {
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Connection, type Snapshot, type Task, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type DialRequest, type TaskCommandRequest } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Connection, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type DialRequest, type TaskCommandRequest } from "./index.js";
2
2
  import { type ProtocolViolation } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
4
4
  export interface AdapterContractResult {
@@ -45,10 +45,13 @@ export declare function assertDeniedAndRetriedBreak(approvals: readonly BreakApp
45
45
  export declare function assertDialIdempotency(connection: Pick<Connection, "dial">, request: DialRequest): Promise<void>;
46
46
  /**
47
47
  * Validates the deadline derived from media end and the task's fixed wrap allowance.
48
- * `toleranceMs` absorbs scheduler jitter in a real implementation; pass 0 to demand
49
- * an exact match.
48
+ *
49
+ * A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
50
+ * host counting down what the provider left open is the violation, and so is a host counting
51
+ * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
52
+ * implementation; pass 0 to demand an exact match.
50
53
  */
51
- export declare function assertWrapTimeout(task: Pick<Task, "completionAllowance">, mediaEndedAt: string, observedDeadline: string, toleranceMs?: number): void;
54
+ export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completionMode" | "completionAllowance">, mediaEndedAt: string, observedDeadline: string | undefined, toleranceMs?: number): void;
52
55
  /** One browser in one task of one provider. `providerId` is `Manifest.id`, never `displayName`. */
53
56
  export type BrowserIsolationScenario = BrowserSessionKeyInput;
54
57
  /** Validates whether two task-browser definitions should share one browser session. */
package/dist/testing.js CHANGED
@@ -245,10 +245,22 @@ export async function assertDialIdempotency(connection, request) {
245
245
  }
246
246
  /**
247
247
  * Validates the deadline derived from media end and the task's fixed wrap allowance.
248
- * `toleranceMs` absorbs scheduler jitter in a real implementation; pass 0 to demand
249
- * an exact match.
248
+ *
249
+ * A task with no allowance has no deadline, so `observedDeadline` must then be `undefined`: a
250
+ * host counting down what the provider left open is the violation, and so is a host counting
251
+ * nothing down when the provider set a clock. `toleranceMs` absorbs scheduler jitter in a real
252
+ * implementation; pass 0 to demand an exact match.
250
253
  */
251
254
  export function assertWrapTimeout(task, mediaEndedAt, observedDeadline, toleranceMs = 1_000) {
255
+ if (task.completionAllowance === undefined) {
256
+ if (observedDeadline !== undefined) {
257
+ throw new Error(`Wrap deadline mismatch: the task states no allowance, so there is no deadline, received ${observedDeadline}`);
258
+ }
259
+ return;
260
+ }
261
+ if (observedDeadline === undefined) {
262
+ throw new Error(`Wrap deadline mismatch: the task allows ${task.completionAllowance}s, but no deadline was observed`);
263
+ }
252
264
  const ended = Date.parse(mediaEndedAt);
253
265
  const deadline = Date.parse(observedDeadline);
254
266
  if (Number.isNaN(ended) || Number.isNaN(deadline))
@@ -517,7 +517,15 @@ function validateTaskInto(task, context, path, into) {
517
517
  into.filled(task.taskType, "task.taskType", `${path}.taskType`, "a task needs a task type");
518
518
  into.oneOf(task.phase, TASK_PHASES, "task.phase", `${path}.phase`);
519
519
  into.oneOf(task.completionMode, COMPLETION_MODES, "task.completionMode", `${path}.completionMode`);
520
- into.require(isDurationSeconds(task.completionAllowance), "task.completionAllowance", `${path}.completionAllowance`, "completionAllowance must be a whole number of seconds, zero or more");
520
+ // The allowance is coupled to the mode: a provider that will complete the task itself is going
521
+ // to act on the allowance, so it must state one; a provider waiting for `complete` may omit it
522
+ // to say it imposes no deadline. Present, it is a duration either way.
523
+ if (task.completionAllowance === undefined) {
524
+ into.require(task.completionMode !== "provider-automatic", "task.completionAllowance.required", `${path}.completionAllowance`, "provider-automatic completion needs an allowance to act on");
525
+ }
526
+ else {
527
+ into.require(isDurationSeconds(task.completionAllowance), "task.completionAllowance", `${path}.completionAllowance`, "completionAllowance must be a whole number of seconds, zero or more, or omitted under agent-command");
528
+ }
521
529
  // The channel is fixed per provider by its manifest, so a task claiming another one is a
522
530
  // task Omni would render with the wrong controls.
523
531
  into.require(task.channel === context.channel, "task.channel", `${path}.channel`, `a ${context.channel} provider may not publish a ${String(task.channel)} task`);
package/guide.md CHANGED
@@ -416,6 +416,10 @@ type TaskHandlingStep = {
416
416
  by?: UserId;
417
417
  };
418
418
 
419
+ type TaskCompletion =
420
+ | { completionMode: "agent-command"; completionAllowance?: DurationSeconds }
421
+ | { completionMode: "provider-automatic"; completionAllowance: DurationSeconds };
422
+
419
423
  type Task<C extends Channel = Channel> = {
420
424
  id: TaskId;
421
425
  title: string;
@@ -426,11 +430,9 @@ type Task<C extends Channel = Channel> = {
426
430
  contact?: Contact;
427
431
  phase: TaskPhase;
428
432
  reference?: string;
429
- completionMode: CompletionMode;
430
- completionAllowance: DurationSeconds;
431
433
  attributes?: TaskAttribute[];
432
434
  handlingHistory?: TaskHandlingStep[];
433
- };
435
+ } & TaskCompletion;
434
436
 
435
437
  type AcceptanceMode =
436
438
  | "no-preference"
@@ -1616,7 +1618,7 @@ time. Runtime conformance checks also require the task channel to match its prov
1616
1618
  | `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
1617
1619
  | `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
1618
1620
  | `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
1619
- | `completionAllowance` | Fixed time allowed to complete the task after primary handling ends. For real-time media, it begins after `task-media-ended`. |
1621
+ | `completionAllowance` | 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. |
1620
1622
  | `attributes` | Optional ordered, typed `TaskAttribute` entries with keys unique within the task. Each contact or timestamp is a separate array item; new attribute shapes require new union members. |
1621
1623
  | `handlingHistory` | Optional ordered handling history for this currently open task. It is live task data, not a permanent archive. |
1622
1624
 
@@ -1690,9 +1692,25 @@ In this example, the agent has two minutes after sending the email to add notes,
1690
1692
  disposition, and complete the task.
1691
1693
 
1692
1694
  `0` means completion may happen immediately. With `provider-automatic`, the provider may complete
1693
- without waiting for a command; with `agent-command`, it still waits for `complete`. There is no
1694
- value meaning "unlimited": a provider that does not want a deadline keeps the task `in-progress` or
1695
- `paused` and moves it to `completing` only when the clock should start.
1695
+ without waiting for a command; with `agent-command`, it still waits for `complete`.
1696
+
1697
+ There is no value meaning "unlimited", because a number that is not a duration would be read as
1698
+ one. A provider that imposes no deadline says so by **omitting** `completionAllowance`, which
1699
+ only `agent-command` permits: the provider will not complete the task itself, so there is nothing
1700
+ for a deadline to trigger, and Omni counts nothing down. **Omitted and empty are different
1701
+ claims** applies -- omitted says there is no deadline to see, where `0` says the deadline is now.
1702
+ Under `provider-automatic` the field is required, because the provider is going to act on it.
1703
+
1704
+ ```ts
1705
+ const untimedWrap = {
1706
+ completionMode: "agent-command",
1707
+ } satisfies Pick<Task<"voice">, "completionMode" | "completionAllowance">;
1708
+ ```
1709
+
1710
+ Here the customer has hung up, `task-media-ended` has been sent on time, the task is `completing`,
1711
+ and the agent takes as long as the work needs. Moving the task to `completing` late to avoid a
1712
+ deadline is not an alternative on a media channel: the clock starts at a real event, and delaying
1713
+ that event would falsify the phase and everything timed from it.
1696
1714
 
1697
1715
  ```ts
1698
1716
  const immediateProviderCompletion = {
@@ -2666,7 +2684,8 @@ They are published as `OMNI_FAILURE_CODES`.
2666
2684
  #### Provider instants are read against a provider clock
2667
2685
 
2668
2686
  Every deadline in this contract is a provider instant that Omni counts down: `allocationExpiresAt`,
2669
- `preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `completionAllowance`.
2687
+ `preparationEndsAt`, and the wrap deadline of `task-media-ended` plus `completionAllowance` where
2688
+ one is stated.
2670
2689
  Comparing those against the host clock is wrong by whatever the two machines disagree by, and the
2671
2690
  damaging direction is early — **Accept** withdrawn from an offer still ringing, a wrap timer
2672
2691
  expiring before the agent has finished.
@@ -2913,7 +2932,7 @@ cannot be established from TypeScript structure alone.
2913
2932
  | `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
2914
2933
  | `assertCommandIdempotency(connection, request)` | Retrying a task command does not repeat its side effect. |
2915
2934
  | `assertDialIdempotency(connection, request)` | Retrying a dial command does not place another call. |
2916
- | `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms. |
2935
+ | `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
2917
2936
  | `assertBrowserIsolationAndReuse(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
2918
2937
  | `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
2919
2938
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",