@xema/omni-protocol 0.1.77 → 0.1.78
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 +49 -30
- package/dist/index.js +8 -3
- package/dist/testing.d.ts +6 -2
- package/dist/testing.js +220 -107
- package/dist/validation.js +41 -15
- package/guide.md +115 -72
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
This document is the normative contract between Omni Agent and a provider adapter. It describes
|
|
4
4
|
observable behavior in addition to TypeScript shapes. When an example and a type declaration
|
|
5
|
-
appear to disagree, the exported TypeScript declaration is authoritative
|
|
5
|
+
appear to disagree, the exported TypeScript declaration is authoritative over the example; when a
|
|
6
|
+
declaration's comment and this document's prose disagree, the prose is authoritative and the
|
|
7
|
+
comment is a defect.
|
|
6
8
|
|
|
7
9
|
All protocol definitions and code examples are written as valid TypeScript. Samples should name
|
|
8
10
|
their contract type or use `satisfies` so the relationship between the example and the contract is
|
|
@@ -163,7 +165,7 @@ type BrowserAccess = {
|
|
|
163
165
|
|
|
164
166
|
type PersonalBrowserCapability = {
|
|
165
167
|
access: BrowserAccess;
|
|
166
|
-
accessAppliesTo
|
|
168
|
+
accessAppliesTo: "initial-url" | "all-navigation";
|
|
167
169
|
};
|
|
168
170
|
|
|
169
171
|
type DialDestinations = "contacts-only" | "any-number";
|
|
@@ -429,9 +431,7 @@ type AgentCapacity = {
|
|
|
429
431
|
count: number; // absolute ceiling, zero or more; zero is host-stopped
|
|
430
432
|
};
|
|
431
433
|
|
|
432
|
-
type CapacityResult =
|
|
433
|
-
| { status: "applied" }
|
|
434
|
-
| { status: "failed"; failure: ProtocolFailure };
|
|
434
|
+
type CapacityResult = { status: "applied" };
|
|
435
435
|
```
|
|
436
436
|
|
|
437
437
|
### Idle contributions
|
|
@@ -489,7 +489,7 @@ type CustomCapability = {
|
|
|
489
489
|
control: "button" | "toggle" | "menu-item";
|
|
490
490
|
label: string;
|
|
491
491
|
placement: "primary" | "secondary" | "overflow";
|
|
492
|
-
render
|
|
492
|
+
render: "inline" | "page";
|
|
493
493
|
};
|
|
494
494
|
prompt?: { fields: CredentialField[] };
|
|
495
495
|
};
|
|
@@ -630,12 +630,10 @@ type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
|
630
630
|
| { role: "consulted" | "conferenced"; destinationId: string; stage: OnCallStage; dialId?: DialId; label?: string }
|
|
631
631
|
);
|
|
632
632
|
|
|
633
|
-
type TaskLeadAssist = {
|
|
634
|
-
stage: "requested"
|
|
635
|
-
leadId
|
|
636
|
-
|
|
637
|
-
since: IsoTimestamp;
|
|
638
|
-
};
|
|
633
|
+
type TaskLeadAssist = { note?: string; since: IsoTimestamp } & (
|
|
634
|
+
| { stage: "requested"; leadId?: never }
|
|
635
|
+
| { stage: "joined"; leadId: UserId }
|
|
636
|
+
);
|
|
639
637
|
|
|
640
638
|
type TaskAssisting = {
|
|
641
639
|
memberId: UserId;
|
|
@@ -702,6 +700,7 @@ type AcceptanceMode =
|
|
|
702
700
|
type TaskOutcome =
|
|
703
701
|
| { type: "completed"; by: "agent" | "provider" }
|
|
704
702
|
| { type: "transferred"; destinationId?: string }
|
|
703
|
+
| { type: "taken-over"; leadId: UserId }
|
|
705
704
|
| { type: "cancelled"; by: "agent" | "provider" | "party"; reason?: string }
|
|
706
705
|
| { type: "expired"; phase: "pending" | "confirmed" | "preview" }
|
|
707
706
|
| { type: "left" }
|
|
@@ -924,13 +923,13 @@ type BreakEndResult =
|
|
|
924
923
|
### Team
|
|
925
924
|
|
|
926
925
|
```ts
|
|
927
|
-
type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "signed-out";
|
|
926
|
+
type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "reserved" | "signed-out";
|
|
928
927
|
|
|
929
928
|
type TeamMember = {
|
|
930
929
|
id: UserId;
|
|
931
930
|
availability: TeamMemberAvailability;
|
|
932
931
|
since?: IsoTimestamp;
|
|
933
|
-
break?: BreakApproval
|
|
932
|
+
break?: Extract<BreakApproval, "awaiting-decision" | "granted" | "starting-after-task">;
|
|
934
933
|
};
|
|
935
934
|
|
|
936
935
|
type LeadRequest = {
|
|
@@ -976,7 +975,7 @@ type TeamLeadAssistCommand =
|
|
|
976
975
|
type TeamBreakCommand =
|
|
977
976
|
| { type: "decide"; memberId: UserId; decision: "granted" | "denied"; reason?: string }
|
|
978
977
|
| { type: "policy"; policy: "ask" | "auto-approve" | "suspended" }
|
|
979
|
-
| { type: "place"; memberId: UserId; reason?: string }
|
|
978
|
+
| { type: "place"; memberId: UserId; reasonId?: string; reason?: string }
|
|
980
979
|
| { type: "release"; memberId: UserId };
|
|
981
980
|
|
|
982
981
|
type TeamBreakCommandRequest = {
|
|
@@ -1378,8 +1377,9 @@ transport is up, a result says the provider accepted the command, and the event
|
|
|
1378
1377
|
|
|
1379
1378
|
**Classify on the rejection, never on a connection status published separately from it.** The
|
|
1380
1379
|
status is a report about the wire and races the rejection; the rejection is the event. A rejection
|
|
1381
|
-
is the provider's answer
|
|
1382
|
-
|
|
1380
|
+
is never the provider's answer: an answer travels only as a resolved `{ status: "failed", failure }`,
|
|
1381
|
+
and every rejection is transport loss, whatever the last `transport-status` said and whatever the
|
|
1382
|
+
rejection carries.
|
|
1383
1383
|
|
|
1384
1384
|
A command therefore carries no key. The provider names its own records — a task, a lead request, a
|
|
1385
1385
|
member — and Omni refers to them by those names; **Omni never asks a provider to remember a name
|
|
@@ -1515,7 +1515,7 @@ compile time.
|
|
|
1515
1515
|
| `phones` | Voice only, and required there: the phones this platform can put an agent on, `softphone` (the call's audio lands in the host) and/or `deskPhone` (a handset the platform rings; the host shows the call and opens nothing). The host picks one per login. See **How the agent hears the call**. |
|
|
1516
1516
|
| `dialOutcomes` | Voice only. How a dial can end on this platform, as it distinguishes them: `answered` and at least one way of not reaching the destination. Required of a provider that dials at all — an idle dialpad, or tasks that transfer, conference or call back — and a `dial-outcome` carries only a declared member. See **Every dial has an outcome**. |
|
|
1517
1517
|
| `runningStepReports` | The provider takes running reports of a host-performed step — `recordStep` with `seconds` so far and no `ended`. Omitted, the host sends exactly two reports per leg, when it began and when it ended, and a running one is refused. See **The host records what it performs**. |
|
|
1518
|
-
| `disposalSettleMs` | Required. How long after an applied disposal -- `complete`,
|
|
1518
|
+
| `disposalSettleMs` | Required. How long after an applied disposal -- `complete`, or a lead's `take-over` -- the provider's `task-ended` is owed, a positive whole number of milliseconds (`manifest.disposalSettleMs`). A warm transfer's `complete` is not a disposal: the agent's wrap runs after it. Stated per provider, since platforms settle at different speeds. See **`task-ended`**. |
|
|
1519
1519
|
|
|
1520
1520
|
### Authentication methods
|
|
1521
1521
|
|
|
@@ -1615,13 +1615,13 @@ idleCapabilities: {
|
|
|
1615
1615
|
| `access.mode` | `allow-all` permits unmatched URLs; `block-all` denies unmatched URLs. |
|
|
1616
1616
|
| `access.allowList` | URL-pattern exceptions permitted when the mode is `block-all`. |
|
|
1617
1617
|
| `access.blockList` | Explicit denials. A match takes precedence over the same policy's allow list and mode. |
|
|
1618
|
-
| `accessAppliesTo` | `all-navigation
|
|
1618
|
+
| `accessAppliesTo` | Required. `all-navigation`: every redirect and navigation is checked. `initial-url`: the starting URL alone. Stated, never defaulted (`manifest.personalBrowser.accessAppliesTo`). |
|
|
1619
1619
|
|
|
1620
1620
|
Patterns use the standard `URLPattern` syntax. Omni owns browser navigation, and the browser is
|
|
1621
1621
|
hidden when no active provider contributes one.
|
|
1622
1622
|
|
|
1623
|
-
`accessAppliesTo
|
|
1624
|
-
|
|
1623
|
+
`accessAppliesTo: "all-navigation"` has every redirect and subsequent navigation validated
|
|
1624
|
+
against the current combined policy, not only the starting URL. A provider may set
|
|
1625
1625
|
`initial-url` to check the first hop alone, but that has to be asked for. A `block-all` policy
|
|
1626
1626
|
enforced only on the initial URL stops nothing — one redirect leaves it — so the permissive
|
|
1627
1627
|
reading is not something to inherit from a default.
|
|
@@ -1807,8 +1807,11 @@ login fix, rendered apart from a provider Omni cannot reach. Commands meanwhile
|
|
|
1807
1807
|
kept, under the same `loginId` — the old `flowId` died with the expiry, so the adapter issues a
|
|
1808
1808
|
new challenge — and when the state returns to `authenticated` the connection and everything on it
|
|
1809
1809
|
carry on: Omni does not call `connect()` again, since a second connection would be a second
|
|
1810
|
-
session for one agent.
|
|
1811
|
-
|
|
1810
|
+
session for one agent. The exception is a transport that reported `error` with `recovery:
|
|
1811
|
+
"reauthenticate"`: that connection is finished, and once the login is restored Omni disposes it and
|
|
1812
|
+
calls `connect()` afresh, as for `reconnect` -- see **`transport-status`**. What "signing in again
|
|
1813
|
+
replaces the login" describes is a new `AuthenticationSession` under a new `loginId`, after
|
|
1814
|
+
`signed-out`.
|
|
1812
1815
|
|
|
1813
1816
|
### What the login may do
|
|
1814
1817
|
|
|
@@ -1960,7 +1963,10 @@ Creates one live provider connection for the signed-in agent.
|
|
|
1960
1963
|
- The returned connection owns reconnect until Omni calls `disconnect()` or aborts `context.signal`.
|
|
1961
1964
|
- May be called again on the same login after that: once per `Connection`, not once per login.
|
|
1962
1965
|
Omni disposes a connection whose `error` named `recovery: "reconnect"` with `disconnect()` and
|
|
1963
|
-
calls `connect()` afresh — see **`transport-status`**.
|
|
1966
|
+
calls `connect()` afresh — see **`transport-status`**. `recovery: "reauthenticate"` ends the same
|
|
1967
|
+
way, after the authentication flow has run on the same session and `loginId`: once the state
|
|
1968
|
+
returns to `authenticated`, Omni disposes the errored connection and calls `connect()` afresh.
|
|
1969
|
+
`expired` alone, with the transport not in `error`, keeps the connection.
|
|
1964
1970
|
|
|
1965
1971
|
### `ConnectContext`
|
|
1966
1972
|
|
|
@@ -2048,7 +2054,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
2048
2054
|
| `refused(report)` | Always. The host tells the adapter what it would not take -- a snapshot it did not replace its state with, an event it dropped -- with every rule broken, so a refusal is visible on both sides. See **What the host does with what it refuses**. |
|
|
2049
2055
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
2050
2056
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
2051
|
-
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone
|
|
2057
|
+
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. Each `User` carries its `timeZone`; a person whose zone the provider cannot name is omitted from the answer, as any unresolvable id is. |
|
|
2052
2058
|
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
2053
2059
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
2054
2060
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
@@ -2137,13 +2143,15 @@ Stops the connection and releases adapter-owned resources.
|
|
|
2137
2143
|
|
|
2138
2144
|
The task-allocation lifecycle has five ordered stages:
|
|
2139
2145
|
|
|
2140
|
-
**1. The agent signs in.**
|
|
2141
|
-
|
|
2142
|
-
|
|
2146
|
+
**1. The agent signs in.** Nothing on the wire says ready or not-ready: the agent is not ready
|
|
2147
|
+
until the host has stated a capacity, and ready once it has. A provider allocates nothing before a
|
|
2148
|
+
capacity is stated, and needs no other word for it.
|
|
2143
2149
|
|
|
2144
|
-
**2. The agent becomes ready.**
|
|
2145
|
-
|
|
2146
|
-
|
|
2150
|
+
**2. The agent becomes ready.** The host states the capacity when its provisioning says: at once,
|
|
2151
|
+
or when the agent presses Ready. A successful connection, a healthy provider, or the absence of a
|
|
2152
|
+
break does not imply readiness. After that, the only way an agent stops taking work is a break,
|
|
2153
|
+
under `capabilities.breaks`; a login that declares no breaks cannot go not-ready once its capacity
|
|
2154
|
+
is stated, and `count: 0` is the host's division of capacity, not the agent's choice.
|
|
2147
2155
|
|
|
2148
2156
|
**3. Omni states their concurrent capacity.** Only now does Omni ask providers for work, saying how
|
|
2149
2157
|
much the agent can take at once. It is a standing declaration rather than a poll: Omni restates the
|
|
@@ -2151,7 +2159,10 @@ capacity every time it changes, in either direction, and does not ask again whil
|
|
|
2151
2159
|
Silence keeps the last one in force, so a provider that waits to be asked a second time will never
|
|
2152
2160
|
deliver again once it has gone quiet. Hold the capacity and deliver when work arrives.
|
|
2153
2161
|
|
|
2154
|
-
**4. A provider offers a task.** The provider emits `task-offered` within the stated capacity
|
|
2162
|
+
**4. A provider offers a task.** The provider emits `task-offered` within the stated capacity, and
|
|
2163
|
+
the task is `pending`: an offer introduces work nobody has accepted, so it is the only phase an
|
|
2164
|
+
offer introduces (`event.taskOffered.phase`). `confirmed`, `preview` and the rest are reached from
|
|
2165
|
+
it, or carried on a snapshot.
|
|
2155
2166
|
|
|
2156
2167
|
**5. Omni decides how the task is accepted.** When `autoAcceptTasks` is `false`, every task requires
|
|
2157
2168
|
agent acceptance. When it is `true`, the pending task's `acceptance` states the provider's
|
|
@@ -2273,7 +2284,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2273
2284
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2274
2285
|
| `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. |
|
|
2275
2286
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preview`, `in-progress`, `paused`, or `completing`. `preview` is voice only. |
|
|
2276
|
-
| `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`**. Media names a task whose work has begun: on a `pending`, `confirmed` or `preview` task it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it
|
|
2287
|
+
| `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`**. Media names a task whose work has begun, or whose party the host is dialling: on a `pending`, `confirmed` or `preview` task with nobody ringing it is refused (`task.media.beforeWork`), on a snapshot as on the event, since a host opens the microphone on it; with the party ringing by a host dial, ring-back is audio and media starts on `dialling`. |
|
|
2277
2288
|
| `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**. |
|
|
2278
2289
|
| `previewEndsAt` | Voice only, in `preview`: when the system stops waiting for the agent to press Call. Absent, the agent has as long as they need. Always with `atDeadline`. See **Preview: the agent presses Call**. |
|
|
2279
2290
|
| `atDeadline` | Voice only, in `preview`, with `previewEndsAt`: what the system does at the deadline -- `calls` places the call itself, `expires` takes the record back and the task ends `expired`. |
|
|
@@ -2377,8 +2388,11 @@ const pressed: TaskCommand<"voice"> = { type: "call", dialId: "dial-7f2" };
|
|
|
2377
2388
|
|
|
2378
2389
|
**Call is a dial.** It carries the host's `dialId`, is answered `dialling`, and ends in exactly one
|
|
2379
2390
|
`dial-outcome`; the phase is its gate and there is no capability, since a record put in front of an
|
|
2380
|
-
agent is there to be called.
|
|
2381
|
-
|
|
2391
|
+
agent is there to be called. Its media starts on `dialling`, as every host-placed dial's does --
|
|
2392
|
+
ring-back is audio the agent hears -- so `task-media-started` arrives on the `preview` task with
|
|
2393
|
+
its party ringing. On `answered` the task is `in-progress`; on any other outcome the media ends,
|
|
2394
|
+
`task-media-ended` starts the wrap clock as on every voice task, and the task goes to
|
|
2395
|
+
`completing`, so the agent records the
|
|
2382
2396
|
no-answer as the disposition it is -- in a campaign that is the commonest outcome there is, and it
|
|
2383
2397
|
is work, not a cancellation. A `preview` task therefore needs a manifest that says how a dial ends
|
|
2384
2398
|
(`task.preview.dialOutcomes.required`).
|
|
@@ -2494,11 +2508,13 @@ whose outcome says so and a call whose media ended: the task returns to `complet
|
|
|
2494
2508
|
same event and the clock starts again from there. At no point is an agent dialling against a
|
|
2495
2509
|
deadline.
|
|
2496
2510
|
|
|
2497
|
-
**The room shows the party being dialled
|
|
2498
|
-
`party` entry carries `stage: "ringing"` and
|
|
2499
|
-
conferenced entry does from its dial
|
|
2500
|
-
|
|
2501
|
-
|
|
2511
|
+
**The room shows the party being dialled.** From the moment any host dial places the party -- a
|
|
2512
|
+
dialpad call, a preview Call, a connect-back -- the `party` entry carries `stage: "ringing"` and
|
|
2513
|
+
the host's `dialId`, exactly as a consulted or conferenced entry does from its dial; `joined` on
|
|
2514
|
+
the publication that follows the answered `dial-outcome` and never before it
|
|
2515
|
+
(`stream.taskUpdated.stage`); and no stage from the next publication on, so `joined` is transient
|
|
2516
|
+
and a later copy still carrying it is stale (`stream.taskUpdated.stage.lingering`). A host shows a
|
|
2517
|
+
call being placed, not a party it asserts is on a call that is still ringing. A callback the platform places itself on the same task -- the first
|
|
2502
2518
|
call over, the platform dialling the party again without a command -- shows the same thing with no
|
|
2503
2519
|
host `dialId`, as any platform-placed dial does. A party with no stage is on the call, and a dial
|
|
2504
2520
|
with no stage is half a claim (`task.onCall.party.dial`). The stage is also what lets the stream
|
|
@@ -2647,7 +2663,9 @@ Four rules a provider has to keep:
|
|
|
2647
2663
|
**An absent `by` means different things on different steps, and both are legitimate.** On
|
|
2648
2664
|
`queued` nobody takes part, so there is nothing to name. On every other step somebody did — see
|
|
2649
2665
|
`HANDLING_STEPS_WITH_A_PERSON` and `handlingStepExpectsAPerson()` — so an absent `by` there says
|
|
2650
|
-
*this was handled and the provider cannot say by whom*.
|
|
2666
|
+
*this was handled and the provider cannot say by whom*. The one step that is never unattributed is
|
|
2667
|
+
`muted`: the host has exactly one agent and the provider knows who, so `by` is required there
|
|
2668
|
+
(`task.handlingHistory.muted.by`).
|
|
2651
2669
|
|
|
2652
2670
|
That case is ordinary rather than theoretical: a leg answered on a shared phone, a manager's
|
|
2653
2671
|
handset, or a device the provider cannot resolve to a person. **Report the step without `by`
|
|
@@ -2889,16 +2907,17 @@ last statement stands and the failure is a `diagnostic`, and a task that was pub
|
|
|
2889
2907
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
2890
2908
|
entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
|
|
2891
2909
|
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
2892
|
-
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`);
|
|
2910
|
+
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); a voice task ends after
|
|
2911
|
+
its audio ends, never around it, whatever the outcome (`stream.taskEnded.mediaOpen`); and a task does not go
|
|
2893
2912
|
backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
|
|
2894
2913
|
stream sees publications, not
|
|
2895
2914
|
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
2896
2915
|
with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
|
|
2897
2916
|
the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
|
|
2898
2917
|
`completing` except by the party being dialled again, a connect-back or a platform's callback,
|
|
2899
|
-
which the update itself shows: the party
|
|
2900
|
-
republished as `in-progress` from a stale copy carries
|
|
2901
|
-
agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
|
|
2918
|
+
which the update itself shows: the party ringing, or joined by a dial whose answered outcome the
|
|
2919
|
+
stream saw in this life. A completing task republished as `in-progress` from a stale copy carries
|
|
2920
|
+
no such stage, and that is the ending the agent never saw. Audio arrives only on a task at work: `task-media-started` on a `completing` task
|
|
2902
2921
|
is refused as it is on a pending one (`stream.taskMediaStarted.beforeWork`), since a connect-back
|
|
2903
2922
|
returns the task to `in-progress` before any media. And a task completes after its audio ends,
|
|
2904
2923
|
never around it: an update moving a task to `completing` while the stream holds its media as
|
|
@@ -3075,9 +3094,11 @@ strand the customer and the destination both, which is why all three are command
|
|
|
3075
3094
|
that offers `warmTransfer` implements all three.
|
|
3076
3095
|
|
|
3077
3096
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
3078
|
-
dropping the agent's leg. What follows is what follows any
|
|
3079
|
-
the provider reports `task-media-ended`,
|
|
3080
|
-
|
|
3097
|
+
dropping the agent's leg. What follows is what follows any call the agent leaves: the agent's
|
|
3098
|
+
media ends and the provider reports `task-media-ended`, the task moves to `completing`, any wrap
|
|
3099
|
+
allowance runs, and the task ends `completed` as any call does. A warm complete is not a disposal:
|
|
3100
|
+
the agent has a wrap to do, so nothing is owed within `disposalSettleMs`. `transferred` names a
|
|
3101
|
+
cold transfer alone, where the agent had no wrap. `applied` on `cancel` says the destination is
|
|
3081
3102
|
dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
|
|
3082
3103
|
provider's report of both, as it does for every command.
|
|
3083
3104
|
|
|
@@ -3118,7 +3139,7 @@ Custom capability IDs must be non-empty and unique within the task. `ui.control`
|
|
|
3118
3139
|
or `menu-item`; `ui.placement` is `primary`, `secondary`, or `overflow`. `ui.render` says where the
|
|
3119
3140
|
control's work appears: `inline`, in the workspace beside the task, or `page`, as a page of its own
|
|
3120
3141
|
— a tab in the same work area as the task's browsers, beside them, and alone on a task that has
|
|
3121
|
-
none;
|
|
3142
|
+
none; stated on every control, never defaulted (`task.custom.ui.render`). `prompt.fields` are what the agent supplies before the action runs — a
|
|
3122
3143
|
destination number, a reference — as `CredentialField`s Omni renders as a form; the values travel
|
|
3123
3144
|
on the custom command under the fields' names, as strings, and Omni sends the command only once
|
|
3124
3145
|
every `required` field has a value. The provider validates what arrives as it validates any
|
|
@@ -3327,7 +3348,7 @@ nothing while none are being accepted — so they are not published separately.
|
|
|
3327
3348
|
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
3328
3349
|
| `retryAfterMs` | How long until the agent may retry, when the provider can say. |
|
|
3329
3350
|
| `reasons` | Not-ready codes this provider offers. Omitted when it defines none; an empty list is refused, being a second spelling of the same fact. |
|
|
3330
|
-
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break
|
|
3351
|
+
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `in-effect` or `starting-after-task` where the provider publishes `reasons`, an imposed break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
|
|
3331
3352
|
| `imposed` | Set when the break was placed on the agent rather than requested. |
|
|
3332
3353
|
|
|
3333
3354
|
A request can be waiting for two unrelated things, and they are separate values because
|
|
@@ -3338,7 +3359,7 @@ rendering one as the other tells an agent to wait for somebody who is never comi
|
|
|
3338
3359
|
| `not-requested` | No request outstanding. |
|
|
3339
3360
|
| `awaiting-decision` | A person has to decide. The agent is waiting on somebody. |
|
|
3340
3361
|
| `granted` | A person decided yes. Omni may now tell this provider to stop the agent; until it does, work continues normally, and this says nothing about why Omni has not. |
|
|
3341
|
-
| `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. |
|
|
3362
|
+
| `starting-after-task` | Omni has told the provider to stop; the break begins when the current task ends. No new work arrives meanwhile, and nobody needs to act. It waits on a task, so beside no task it is refused (`break.starting-after-task.tasks`): a committed break with nothing outstanding is `in-effect`. |
|
|
3342
3363
|
| `in-effect` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `in-effect` beside a task is refused as `break.in-effect.tasks`. The one exception is a lead's monitoring task during a `coaching`, `administrative` or `training` break -- see **Monitoring a call**. |
|
|
3343
3364
|
|
|
3344
3365
|
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
@@ -3367,6 +3388,11 @@ them, not the machinery that carried it out.
|
|
|
3367
3388
|
Omni resolves the name to show with `describeUsers()`, so a provider sends the identifier and never
|
|
3368
3389
|
a display name.
|
|
3369
3390
|
|
|
3391
|
+
**An imposed break travels with `in-effect` or `starting-after-task`, and nothing else.** It is a
|
|
3392
|
+
break in progress or about to be; beside `granted` or `awaiting-decision` the host would read a
|
|
3393
|
+
request the agent never made and commit it (`break.imposed.approval`). Where the provider publishes
|
|
3394
|
+
`reasons`, the lead's `place` named one, and the member's state carries it as `activeReasonId`.
|
|
3395
|
+
|
|
3370
3396
|
For example:
|
|
3371
3397
|
|
|
3372
3398
|
```ts
|
|
@@ -3435,18 +3461,23 @@ Your own tasks are the only ones you count. What the agent holds at other provid
|
|
|
3435
3461
|
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
3436
3462
|
providers, and the host divides their capacity among them rather than telling each the whole. A
|
|
3437
3463
|
provider that has none of it for now is told **`count: 0`, host-stopped**: allocate nothing, show
|
|
3438
|
-
the member as `
|
|
3439
|
-
|
|
3440
|
-
|
|
3464
|
+
the member as `reserved` on the roster -- signed in here, capacity held by the host for elsewhere,
|
|
3465
|
+
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
3466
|
+
count as any other when the host has capacity for this provider again. Zero is the one restatement
|
|
3441
3467
|
that follows work rather than provisioning.
|
|
3442
3468
|
|
|
3469
|
+
**A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
|
|
3470
|
+
statement has no failure to report, and a `failed` arm gave four host-provider pairings four
|
|
3471
|
+
readings of which ceiling stood after it. A provider that cannot carry the count allocates within
|
|
3472
|
+
what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
|
|
3473
|
+
refusal.
|
|
3474
|
+
|
|
3443
3475
|
Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
|
|
3444
3476
|
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
3445
3477
|
it was ever told, or returns early on a small one -- cannot be told to take less work, and a host
|
|
3446
3478
|
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
3447
|
-
both ways after the drive, two then one then nought, and
|
|
3448
|
-
(`
|
|
3449
|
-
caught against it (`stream.taskOffered.overCapacity`).
|
|
3479
|
+
both ways after the drive, two then one then nought, and an offer after a lower count is caught
|
|
3480
|
+
against it (`stream.taskOffered.overCapacity`).
|
|
3450
3481
|
|
|
3451
3482
|
**Capacity gates what the provider allocates, not what the agent starts.** A call placed from the
|
|
3452
3483
|
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
@@ -3718,6 +3749,7 @@ Each availability value means one thing:
|
|
|
3718
3749
|
| `ready` | Signed in, able to take work, none assigned. |
|
|
3719
3750
|
| `on-task` | Handling at least one task. It says nothing about how many, and nothing about whether more will fit. |
|
|
3720
3751
|
| `on-break` | Stopped and not taking work, whether they asked or somebody stopped them. The reason lives on their own `BreakState`, not here. |
|
|
3752
|
+
| `reserved` | Signed in here, and the host holds this agent's capacity for another provider (`count: 0`, host-stopped): not receiving this provider's work, and not on a break. See **Capacity**. |
|
|
3721
3753
|
| `signed-out` | Known to this team but not signed in to this provider. |
|
|
3722
3754
|
|
|
3723
3755
|
**Always publish the complete roster, never a change to it.** Team presence typically reaches an
|
|
@@ -3755,7 +3787,7 @@ One method, `executeTeamBreak`, taking a discriminated command exactly as `execu
|
|
|
3755
3787
|
| --- | --- |
|
|
3756
3788
|
| `{ type: "decide", memberId: UserId, decision, reason? }` | Settles one pending request. `decision` is `granted` or `denied`. A grant moves the member to `granted`; a denial ends the request and moves it directly to `not-requested`. |
|
|
3757
3789
|
| `{ type: "policy", policy }` | `ask`, `auto-approve`, or `suspended`. |
|
|
3758
|
-
| `{ type: "place", memberId: UserId, reason? }` | Puts a member on a break they did not ask for. |
|
|
3790
|
+
| `{ type: "place", memberId: UserId, reasonId?, reason? }` | Puts a member on a break they did not ask for. `reasonId` names a published `BreakReason.id` and is required whenever the provider publishes `reasons`; the member's imposed break carries it as `activeReasonId`, so its kind is known. |
|
|
3759
3791
|
| `{ type: "release", memberId: UserId }` | Ends an imposed break on that member, whoever placed it. |
|
|
3760
3792
|
|
|
3761
3793
|
`memberId` is this provider's own identifier for the member, as published on its roster. It is
|
|
@@ -3787,10 +3819,10 @@ Required when the login declares `capabilities.team.leadAssistControl`, and gate
|
|
|
3787
3819
|
```ts
|
|
3788
3820
|
// 1. The agent asks, with a small note. Their task carries `leadAssist` from here on.
|
|
3789
3821
|
execute({ taskId: "call-42", command: { type: "lead-assist", action: "request", note: "Refund dispute, needs approval" } })
|
|
3790
|
-
// task.leadAssist = {
|
|
3822
|
+
// task.leadAssist = { stage: "requested", note: "Refund dispute, needs approval", since }
|
|
3791
3823
|
|
|
3792
3824
|
// 2. Every lead entitled to it sees the request on their roster.
|
|
3793
|
-
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", note, since }]
|
|
3825
|
+
// team-updated: requests: [{ id: "req-7", memberId: "A-1", taskId: "call-42", allocationId: "alloc-42", note, since }]
|
|
3794
3826
|
|
|
3795
3827
|
// 3. A lead joins, or declines.
|
|
3796
3828
|
executeTeamLeadAssist({ command: { type: "join", requestId: "req-7" } })
|
|
@@ -3824,8 +3856,8 @@ choice that is no command at all:
|
|
|
3824
3856
|
|
|
3825
3857
|
| The lead | The agent's task | The lead's task |
|
|
3826
3858
|
| --- | --- | --- |
|
|
3827
|
-
| `{ type: "lead-assist", action: "take-over" }` | `task-ended` with `{ type: "
|
|
3828
|
-
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
3859
|
+
| `{ type: "lead-assist", action: "take-over" }` | `task-media-ended`, then `task-ended` with `{ type: "taken-over", leadId }`: **no `completing` window**, the agent is idle at once. The audio ends first, as before every voice ending (`stream.taskEnded.mediaOpen`), and the lead is named by user id, since a lead is not a directory item | Continues alone, and ends as any call does |
|
|
3860
|
+
| `{ type: "lead-assist", action: "leave" }` | Continues; `leadAssist` is cleared | `task-media-ended`, then `task-ended` with `{ type: "left" }` -- the call goes on without them |
|
|
3829
3861
|
| Stays until the customer hangs up | `task-media-ended`, `completing`, its own disposition | The same, independently: **both have the disposal window** |
|
|
3830
3862
|
|
|
3831
3863
|
`left` is the one outcome that ends a task without ending the call: this agent left a call that
|
|
@@ -4011,7 +4043,8 @@ other side: it validates the shape of whatever host a test hands the adapter, re
|
|
|
4011
4043
|
a softphone login and none elsewhere -- not on a desk phone, not off voice
|
|
4012
4044
|
(`context.host.audio.required` / `.unexpected`), holds `host.mute` to the same line (`host.mute.required`
|
|
4013
4045
|
/ `.unexpected`, `host.mute` for a third word) -- and refuses a voice adapter that never asked the
|
|
4014
|
-
host anything (`connection.host.consulted`)
|
|
4046
|
+
host anything (`connection.host.consulted`), and one that subscribed to nothing will never hear of a
|
|
4047
|
+
change (`connection.host.subscribed`): the obligation has two halves, and each is held.
|
|
4015
4048
|
|
|
4016
4049
|
### Capacity around setup
|
|
4017
4050
|
|
|
@@ -4442,8 +4475,12 @@ died; the host knows how to run a login. `recovery` joins the two:
|
|
|
4442
4475
|
re-establishes state exactly as a reconnect snapshot does. `connect()` is once per
|
|
4443
4476
|
`Connection`, not once per login.
|
|
4444
4477
|
- **`reauthenticate`** — the session under the login died: a token rejected, a remote logout. Omni
|
|
4445
|
-
runs the authentication flow first
|
|
4446
|
-
|
|
4478
|
+
runs the authentication flow first, on the same session and `loginId`, never through
|
|
4479
|
+
`signed-out`; the authentication session decides whether stored material refreshes it silently or
|
|
4480
|
+
the agent must act, exactly as at sign-in. Once the state returns to `authenticated`, Omni
|
|
4481
|
+
disposes the errored connection with `disconnect()` and calls `connect()` afresh, as for
|
|
4482
|
+
`reconnect`, and the fresh snapshot re-establishes state. The adapter tears nothing down itself
|
|
4483
|
+
and expects nothing to carry on: the connection that reported `error` is finished.
|
|
4447
4484
|
|
|
4448
4485
|
**Patience is the host's.** An adapter in `connecting` retries for as long as it takes and never
|
|
4449
4486
|
has to decide when to stop. Omni owns giving up: after however long it chooses to wait, it may
|
|
@@ -4543,8 +4580,8 @@ A `left` outcome ends the task for this agent alone: the call continues without
|
|
|
4543
4580
|
when a lead who joined it leaves -- see **Lead assist**.
|
|
4544
4581
|
|
|
4545
4582
|
A successful `complete` or `transfer` command does not clear the task. Omni waits for `task-ended`,
|
|
4546
|
-
and not for ever: `applied` to a disposal -- `complete`,
|
|
4547
|
-
|
|
4583
|
+
and not for ever: `applied` to a disposal -- `complete`, or a lead's `take-over` -- says the
|
|
4584
|
+
provider has disposed of the task, and its `task-ended` follows within the
|
|
4548
4585
|
manifest's `disposalSettleMs`. A provider never answers `applied` for a disposal it has not yet
|
|
4549
4586
|
performed. Past the bound the host calls `snapshot()`: a snapshot still carrying the task is a task
|
|
4550
4587
|
held open by a provider that said it was done, and the desk shows it as unsettled -- "Completing...
|
|
@@ -4665,7 +4702,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
4665
4702
|
| `validateHostReport(report)` | The host'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 host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
4666
4703
|
| `validateHostMute(mute, softphone)` | What the host's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the host holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
4667
4704
|
| `validateLoginStore(store)` | The login's store the host hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
4668
|
-
| `validateCapacity(capacity)` | What the host states as capacity: a whole number of zero or more (`capacity.count`), zero being host-stopped. The harness states one on connect and two, one and zero after the drive,
|
|
4705
|
+
| `validateCapacity(capacity)` | What the host states as capacity: a whole number of zero or more (`capacity.count`), zero being host-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`). |
|
|
4669
4706
|
| `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. |
|
|
4670
4707
|
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, 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.handling` for every control on the call or the conversation), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
|
|
4671
4708
|
| `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. |
|
|
@@ -4766,7 +4803,10 @@ capacity, or beyond the count with nothing dialled on it, is named
|
|
|
4766
4803
|
through `describeUsers` and the answer held to the shape, nobody unasked, nobody described as
|
|
4767
4804
|
nothing (`describeUsers.user.*`, `describeUsers.unasked`, `connection.describeUsers.empty`); and
|
|
4768
4805
|
the second adapter a `rebuild` gives comes up signed in as the same login from the secrets alone
|
|
4769
|
-
before it reads anything (`drive.reload.login`)
|
|
4806
|
+
before it reads anything (`drive.reload.login`), and is then held to everything the first was on
|
|
4807
|
+
connect: the methods its declarations call for, the host's report, a capacity stated to it, its
|
|
4808
|
+
snapshot read as the connect snapshot was, and its media opened afresh on a task carried with
|
|
4809
|
+
`media: "started"`, since the first client's session died with it. The result also says which rules the
|
|
4770
4810
|
run evaluated, pass or fail, in `rulesEvaluated`: the validators' as each was applied, the stream's
|
|
4771
4811
|
as each case was considered, so a test that needs a rule to have run asserts it there rather than
|
|
4772
4812
|
inferring it from an empty `violations`, and a rule absent from it was never looked at, which is a
|
|
@@ -4775,7 +4815,8 @@ the drive mutes it for one second and reports the leg through `recordStep`, begu
|
|
|
4775
4815
|
expecting each report `recorded` (`drive.recordStep.failed`, `.rejected`); then it mutes again and
|
|
4776
4816
|
ends the call muted, as agents do, so the leg is open when the media ends, the provider closes it
|
|
4777
4817
|
in the completing publication or the open entry is refused (`task.handlingHistory.muted.open`),
|
|
4778
|
-
and the drive's closing report after the media ended is expected `recorded` and to change nothing
|
|
4818
|
+
and the drive's closing report after the media ended is expected `recorded` and to change nothing:
|
|
4819
|
+
a record restated afterwards with that leg's duration altered is named (`drive.recordStep.overwritten`).
|
|
4779
4820
|
Where the provider restates the task's record afterwards, each leg is in it or the hole is named
|
|
4780
4821
|
(`drive.recordStep.history`). Given `rebuild`, a way to build the adapter again as a host reload
|
|
4781
4822
|
does, the drive reloads the host as a reload happens: the first client is unsubscribed,
|
|
@@ -4799,7 +4840,9 @@ open task back to, and nothing about the reload is exempt from any rule. The sec
|
|
|
4799
4840
|
has already put what would rebuild it into the secrets store, from the moment it was handed one,
|
|
4800
4841
|
not only when a flow completes -- the session is the adapter's, the store is the host's, and a
|
|
4801
4842
|
host reload is exactly when no flow will run. And once the task has ended, the store the drive
|
|
4802
|
-
handed the adapter holds no key
|
|
4843
|
+
handed the adapter holds no key naming the task's id, delimited, never as a run of characters inside
|
|
4844
|
+
another id (`drive.store.retained`); an adapter whose keys never named the task leaves the rule
|
|
4845
|
+
unevaluated, and the result says so rather than passing it: a task's keys go
|
|
4803
4846
|
with the task, or the next offer of the same id inherits them. Outside the
|
|
4804
4847
|
handling phases with `hold` still declared -- in
|
|
4805
4848
|
`confirmed`, where the provider publishes it, and in `completing` once the call has ended -- the
|
|
@@ -4854,7 +4897,7 @@ cannot be established from TypeScript structure alone.
|
|
|
4854
4897
|
| `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
|
|
4855
4898
|
| `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
|
|
4856
4899
|
| `assertReconnectWithMissedAssignments(before, reconnect, ids)` | A reconnect snapshot restores assignments received while offline. |
|
|
4857
|
-
| `stillHost(report?)` | A host that reports one thing and never changes, for a test context: `{ online: true }` by default, a report with audio for a voice adapter. |
|
|
4900
|
+
| `stillHost(report?, guarantees?, mute?)` | A host 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. |
|
|
4858
4901
|
| `TaskStream`, `BreakStream` | The cross-event models the harness applies after the connect snapshot, exported for a host that wants the same rules at its boundary: `seed(snapshot)`, then `apply(envelope)` returns the violations. |
|
|
4859
4902
|
| `assertBreakFollowsItsRequests(envelopes, snapshot?)` | A break follows its requests: a commit's states only after a grant, never backwards, and a placed break arriving in effect with `imposed`. The harness applies the same rules after the connect snapshot. |
|
|
4860
4903
|
| `assertMediaFollowsTheTask(envelopes, snapshot?)` | The media follows the task and never decides it: every task is introduced once, `task-media-started` and `task-media-ended` alternate on work that has begun, media ends only where it arrived, and what follows the media ending is `completing` or `task-ended`. The harness applies the same rules to every event after the connect snapshot (`stream.*`). A sequence with no media satisfies it by never testing it — pair it with the assertion that the media end is present. |
|