@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 +17 -4
- package/dist/testing.d.ts +7 -4
- package/dist/testing.js +14 -2
- package/dist/validation.js +9 -1
- package/guide.md +28 -9
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -375,7 +375,22 @@ export interface TaskHandlingStep {
|
|
|
375
375
|
*/
|
|
376
376
|
by?: UserId;
|
|
377
377
|
}
|
|
378
|
-
|
|
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
|
|
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
|
-
*
|
|
49
|
-
*
|
|
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<
|
|
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
|
-
*
|
|
249
|
-
*
|
|
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))
|
package/dist/validation.js
CHANGED
|
@@ -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
|
-
|
|
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`.
|
|
1694
|
-
|
|
1695
|
-
|
|
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
|
|