@xema/omni-protocol 0.1.31 → 0.1.33
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 +146 -17
- package/dist/index.js +18 -0
- package/dist/testing.d.ts +12 -8
- package/dist/testing.js +72 -16
- package/dist/validation.d.ts +10 -2
- package/dist/validation.js +181 -22
- package/guide.md +279 -79
- package/package.json +1 -1
package/guide.md
CHANGED
|
@@ -35,6 +35,8 @@ are used precisely throughout and mean nothing looser here.
|
|
|
35
35
|
| **Event** | One completed transaction reported after a snapshot established the baseline. |
|
|
36
36
|
| **Break** | A reported, supervised state in which the agent is not working — one with a reason, a decision behind it and a return. It covers what a platform may call *not-ready*, including equipment trouble. An agent who is merely at capacity is not on a break. |
|
|
37
37
|
| **Workspace** | What Omni shows the agent. The **task workspace** holds the selected task, its controls and its browsers; the **idle workspace** holds what a provider contributes when no task is selected — dialpad, contacts, calendar, roster. |
|
|
38
|
+
| **Dial** | One outbound call the host asks a provider to place — from the idle dialpad, a transfer, a conference add, or a callback. Identified by the host's `dialId`, accepted as `dialling`, and ended by exactly one `dial-outcome`. See **Every dial has an outcome**. |
|
|
39
|
+
| **On the call** | Who a voice task's audio joins right now, as the provider states it on `Task.onCall`: the party, the agents, and anyone consulted or conferenced in. |
|
|
38
40
|
|
|
39
41
|
Six words describe *what state a thing is in*, and they are not interchangeable: each belongs to
|
|
40
42
|
one thing, so a bare "status" in conversation is always the authentication session's, and a
|
|
@@ -94,6 +96,7 @@ the semantic name in every contract field rather than repeating the primitive ty
|
|
|
94
96
|
type IsoTimestamp = string;
|
|
95
97
|
type UserId = string;
|
|
96
98
|
type TaskId = string;
|
|
99
|
+
type DialId = string;
|
|
97
100
|
type DurationSeconds = number;
|
|
98
101
|
```
|
|
99
102
|
|
|
@@ -162,6 +165,8 @@ type DialDestinations = "contacts-only" | "any-number";
|
|
|
162
165
|
|
|
163
166
|
type DialCapability = { destinations: DialDestinations };
|
|
164
167
|
|
|
168
|
+
type DialOutcome = "answered" | "busy" | "no-answer" | "unreachable" | "rejected";
|
|
169
|
+
|
|
165
170
|
type IdleCapabilities<C extends Channel = Channel> = {
|
|
166
171
|
personalBrowser?: PersonalBrowserCapability;
|
|
167
172
|
calendar?: true;
|
|
@@ -178,6 +183,8 @@ type Manifest<C extends Channel = Channel> = {
|
|
|
178
183
|
phaseLabels?: TaskPhaseLabels;
|
|
179
184
|
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
180
185
|
orgLevels?: LevelDeclaration[];
|
|
186
|
+
dialOutcomes?: C extends "voice" ? DialOutcome[] : never;
|
|
187
|
+
runningStepReports?: true;
|
|
181
188
|
};
|
|
182
189
|
```
|
|
183
190
|
|
|
@@ -360,6 +367,15 @@ type SetPreferenceRequest =
|
|
|
360
367
|
| { id: PreferenceId; enabled: boolean }
|
|
361
368
|
| { id: PreferenceId; inherit: true };
|
|
362
369
|
|
|
370
|
+
type HandlingReport = { taskId: TaskId; step: HandlingStep; at: IsoTimestamp } & (
|
|
371
|
+
| { ended: true; seconds: DurationSeconds }
|
|
372
|
+
| { ended?: never; seconds?: DurationSeconds }
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
type HandlingReportResult =
|
|
376
|
+
| { status: "recorded" }
|
|
377
|
+
| { status: "failed"; failure: ProtocolFailure };
|
|
378
|
+
|
|
363
379
|
type PreferenceResult =
|
|
364
380
|
| { status: "applied" }
|
|
365
381
|
| { status: "failed"; failure: ProtocolFailure };
|
|
@@ -404,11 +420,12 @@ type ScheduledActivity = {
|
|
|
404
420
|
};
|
|
405
421
|
|
|
406
422
|
type DialRequest = {
|
|
423
|
+
dialId: DialId;
|
|
407
424
|
destination: string;
|
|
408
425
|
};
|
|
409
426
|
|
|
410
427
|
type DialResult =
|
|
411
|
-
| { status: "
|
|
428
|
+
| { status: "dialling"; dialId: DialId }
|
|
412
429
|
| { status: "failed"; failure: ProtocolFailure };
|
|
413
430
|
```
|
|
414
431
|
|
|
@@ -549,9 +566,19 @@ type HandlingStep =
|
|
|
549
566
|
| "conferenced"
|
|
550
567
|
| "unanswered";
|
|
551
568
|
|
|
569
|
+
type TaskHandlingHistory = {
|
|
570
|
+
steps: TaskHandlingStep[];
|
|
571
|
+
handleSeconds?: DurationSeconds;
|
|
572
|
+
holdSeconds?: DurationSeconds;
|
|
573
|
+
queueSeconds?: DurationSeconds;
|
|
574
|
+
transfers?: number;
|
|
575
|
+
};
|
|
576
|
+
|
|
552
577
|
type TaskHandlingStep = {
|
|
553
578
|
step: HandlingStep;
|
|
554
579
|
at: IsoTimestamp;
|
|
580
|
+
dialId?: DialId;
|
|
581
|
+
destination?: string;
|
|
555
582
|
seconds?: DurationSeconds;
|
|
556
583
|
by?: UserId;
|
|
557
584
|
};
|
|
@@ -560,11 +587,13 @@ type TaskCompletion =
|
|
|
560
587
|
| { completionMode: "agent-command"; wrapAllowance?: DurationSeconds }
|
|
561
588
|
| { completionMode: "provider-automatic"; wrapAllowance: DurationSeconds };
|
|
562
589
|
|
|
563
|
-
type
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
590
|
+
type OnCallRole = "party" | "agent" | "consulted" | "conferenced";
|
|
591
|
+
|
|
592
|
+
type OnCall = { since: IsoTimestamp; held?: true } & (
|
|
593
|
+
| { role: "party" }
|
|
594
|
+
| { role: "agent"; userId: UserId }
|
|
595
|
+
| { role: "consulted" | "conferenced"; destination: string; dialId?: DialId; label?: string }
|
|
596
|
+
);
|
|
568
597
|
|
|
569
598
|
type TaskLead = {
|
|
570
599
|
stage: "requested" | "joined";
|
|
@@ -607,11 +636,11 @@ type Task<C extends Channel = Channel> = {
|
|
|
607
636
|
acceptance?: AcceptanceMode;
|
|
608
637
|
reference?: string;
|
|
609
638
|
attributes?: TaskAttribute[];
|
|
610
|
-
handlingHistory?:
|
|
639
|
+
handlingHistory?: TaskHandlingHistory;
|
|
611
640
|
} & TaskCompletion & (
|
|
612
641
|
C extends "voice"
|
|
613
|
-
? {
|
|
614
|
-
: {
|
|
642
|
+
? { onCall?: OnCall[]; lead?: TaskLead; assisting?: TaskAssisting; media?: TaskMediaState }
|
|
643
|
+
: { onCall?: never; lead?: never; assisting?: never; media?: never }
|
|
615
644
|
);
|
|
616
645
|
|
|
617
646
|
type AcceptanceMode =
|
|
@@ -664,16 +693,17 @@ type VoiceTaskCommand =
|
|
|
664
693
|
| { type: "hold" }
|
|
665
694
|
| { type: "resume" }
|
|
666
695
|
| { type: "disconnect" }
|
|
667
|
-
| { type: "callback" }
|
|
668
|
-
| { type: "transfer"; destination: string; action?: never }
|
|
669
|
-
| { type: "transfer"; action: "consult"; destination: string }
|
|
696
|
+
| { type: "callback"; dialId: DialId }
|
|
697
|
+
| { type: "transfer"; dialId: DialId; destination: string; action?: never }
|
|
698
|
+
| { type: "transfer"; action: "consult"; dialId: DialId; destination: string }
|
|
670
699
|
| { type: "transfer"; action: "complete" }
|
|
671
700
|
| { type: "transfer"; action: "cancel" }
|
|
672
701
|
| { type: "lead"; action: "request"; note?: string }
|
|
673
702
|
| { type: "lead"; action: "cancel" }
|
|
674
703
|
| { type: "lead"; action: "take-over" }
|
|
675
704
|
| { type: "lead"; action: "leave" }
|
|
676
|
-
| { type: "conference";
|
|
705
|
+
| { type: "conference"; action: "add"; dialId: DialId; destination: string }
|
|
706
|
+
| { type: "conference"; action: "remove"; destination: string }
|
|
677
707
|
| { type: "recording"; action: "start" | "pause" | "resume" | "stop" }
|
|
678
708
|
| ({ type: "complete" } & DispositionPayload);
|
|
679
709
|
|
|
@@ -702,6 +732,7 @@ type TaskCommandRequest<C extends Channel = Channel> = {
|
|
|
702
732
|
|
|
703
733
|
type TaskCommandResult =
|
|
704
734
|
| { status: "applied" }
|
|
735
|
+
| { status: "dialling"; dialId: DialId }
|
|
705
736
|
| { status: "failed"; failure: ProtocolFailure };
|
|
706
737
|
```
|
|
707
738
|
|
|
@@ -949,6 +980,7 @@ type ProviderEvent =
|
|
|
949
980
|
| { type: "task-media-started"; taskId: TaskId }
|
|
950
981
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
951
982
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
983
|
+
| { type: "dial-outcome"; dialId: DialId; outcome: DialOutcome; taskId?: TaskId; destination?: string; reason?: string }
|
|
952
984
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
953
985
|
| { type: "queue-summary"; summary: QueueSummary }
|
|
954
986
|
| { type: "diagnostic"; expected: string; observed: string; taskId?: TaskId }
|
|
@@ -990,6 +1022,7 @@ type Connection<C extends Channel = Channel> = {
|
|
|
990
1022
|
executeTeamConsult?(request: TeamConsultCommandRequest): Promise<TeamCommandResult>;
|
|
991
1023
|
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
992
1024
|
setPreference?(request: SetPreferenceRequest): Promise<PreferenceResult>;
|
|
1025
|
+
recordStep?(report: HandlingReport): Promise<HandlingReportResult>;
|
|
993
1026
|
executeTeamPolicy?(request: TeamPolicyCommandRequest): Promise<TeamCommandResult>;
|
|
994
1027
|
};
|
|
995
1028
|
|
|
@@ -1277,11 +1310,12 @@ already committed stops an agent who is already stopped. A command whose second
|
|
|
1277
1310
|
cost — a dial places a second call — is not made safe by anyone, which is why it is never repeated
|
|
1278
1311
|
without a person deciding to.
|
|
1279
1312
|
|
|
1280
|
-
An agent dials. The provider places the call and the answer is lost. Omni shows the dial
|
|
1281
|
-
unknown, not failed. Within a moment the provider offers the resulting call through
|
|
1282
|
-
and the
|
|
1283
|
-
|
|
1284
|
-
|
|
1313
|
+
An agent dials. The provider places the call and the `dialling` answer is lost. Omni shows the dial
|
|
1314
|
+
as unknown, not failed. Within a moment the provider offers the resulting call through
|
|
1315
|
+
`task-offered` and reports the dial's `dial-outcome` under the `dialId` Omni sent, and the agent is
|
|
1316
|
+
on it; had nothing been placed, nothing arrives and the agent dials again. What Omni must not do is
|
|
1317
|
+
dial again on the agent's behalf — the one outcome worse than a lost answer is two phones ringing
|
|
1318
|
+
at the customer.
|
|
1285
1319
|
|
|
1286
1320
|
An agent presses Hold while the provider is reconnecting. Nothing is queued at either end: the
|
|
1287
1321
|
adapter answers `failed` with `omni.unavailable`, Omni shows the refusal, and the agent presses
|
|
@@ -1358,6 +1392,7 @@ export default defineAdapter({
|
|
|
1358
1392
|
idleCapabilities: {
|
|
1359
1393
|
dial: { destinations: "any-number" },
|
|
1360
1394
|
},
|
|
1395
|
+
dialOutcomes: ["answered", "no-answer"],
|
|
1361
1396
|
},
|
|
1362
1397
|
createAuthenticationSession: context => createAcmeAuthentication(context),
|
|
1363
1398
|
connect: context => createConnection(context),
|
|
@@ -1382,6 +1417,8 @@ compile time.
|
|
|
1382
1417
|
| `phaseLabels` | Optional static adapter-defined display names for canonical `TaskPhase` values. They cannot vary at runtime. |
|
|
1383
1418
|
| `taskTypePresentation` | Optional static adapter-defined presentation keyed by exact `taskType`. It names the item and its optional agent-facing reference. |
|
|
1384
1419
|
| `orgLevels` | The organisation's whole ladder as the provider calls it, each level with the label a desk shows for "who decided". Stated outright, `person` included: what it leaves out does not exist. Omitted for the typical four, `DEFAULT_LEVELS`. See **Who decides what an agent may do**. |
|
|
1420
|
+
| `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**. |
|
|
1421
|
+
| `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**. |
|
|
1385
1422
|
|
|
1386
1423
|
### Authentication methods
|
|
1387
1424
|
|
|
@@ -1451,9 +1488,9 @@ aggregated from every provider, but a `contacts-only` dialpad offers only the en
|
|
|
1451
1488
|
contributed. Any other reading lets a second provider put destinations into a directory the first
|
|
1452
1489
|
restricted precisely so it could control what was dialled.
|
|
1453
1490
|
|
|
1454
|
-
A declared dial capability requires `Connection.dial()
|
|
1455
|
-
|
|
1456
|
-
|
|
1491
|
+
A declared dial capability requires `Connection.dial()` and a `dialOutcomes` declaration, since a
|
|
1492
|
+
dialpad dials. Omni sends the destination as the agent selected or typed it, under a `dialId` of
|
|
1493
|
+
its own minting, and the provider holds it to the declared `destinations`.
|
|
1457
1494
|
|
|
1458
1495
|
#### Personal Browser (`personalBrowser`)
|
|
1459
1496
|
|
|
@@ -1878,7 +1915,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1878
1915
|
| `setCapacity(capacity)` | Always. Nothing may be allocated until a capacity is stated, so there is no connection that does not receive it. |
|
|
1879
1916
|
| `execute(request)` | Always. Every channel has commands no capability gates — see **Which commands need a capability**. |
|
|
1880
1917
|
| `describeUsers(ids)` | The adapter publishes any `UserId`: on `ImposedBreak.by`, a roster, or `handlingHistory[].by`. |
|
|
1881
|
-
| `dial(request)` | The manifest declares `idleCapabilities.dial`. |
|
|
1918
|
+
| `dial(request)` | The manifest declares `idleCapabilities.dial`, and with it `dialOutcomes`. |
|
|
1882
1919
|
| `requestBreak(request)` | The login declares `capabilities.breaks`. |
|
|
1883
1920
|
| `commitBreak()` | The login declares `capabilities.breaks`. Commit and cancel are not optional halves of it. |
|
|
1884
1921
|
| `cancelBreak()` | The login declares `capabilities.breaks`. |
|
|
@@ -1886,6 +1923,7 @@ surface in one place, and what obliges an adapter to implement each one.
|
|
|
1886
1923
|
| `executeTeamBreak(command)` | The login declares `capabilities.team.breakControl`. |
|
|
1887
1924
|
| `executeTeamConsult(command)` | The login declares `capabilities.team.consultControl`. |
|
|
1888
1925
|
| `setPreference(request)` | The login declares `capabilities.preferences`: the person's choice has to have somewhere to go. |
|
|
1926
|
+
| `recordStep(report)` | A task declares `mute`: the host performs that leg, and the provider's record has to have somewhere to take it. See **The host records what it performs**. |
|
|
1889
1927
|
| `executeTeamPolicy(command)` | The login declares `capabilities.team.policyControl`. |
|
|
1890
1928
|
| `openMedia(request)` | The manifest channel is `voice`. Every voice task's audio lands in Omni, so there is no voice adapter that does not implement it. |
|
|
1891
1929
|
|
|
@@ -2090,8 +2128,8 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2090
2128
|
| `completionMode` | `agent-command` waits for the channel's `complete` command; `provider-automatic` completes without one. |
|
|
2091
2129
|
| `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. |
|
|
2092
2130
|
| `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. |
|
|
2093
|
-
| `handlingHistory` |
|
|
2094
|
-
| `
|
|
2131
|
+
| `handlingHistory` | The call record: `steps` — the ordered handling history of this open task, one entry per occurrence, oldest first — and what they add up to before this agent, `handleSeconds`, `holdSeconds`, `queueSeconds`, `transfers`, each present when the provider knows it. Live task data restated with the task, not a permanent archive. See **How a task has been handled**. |
|
|
2132
|
+
| `onCall` | Voice only. Who is on the call right now, as the provider states it, replaced whole with the task: `party` is the customer, `agent` a person by user id, `consulted` and `conferenced` somebody a dial brought in -- with the `destination` dialled, the `dialId` where a host placed it, and `held: true` on anyone parked. A `consulted` entry is what makes `transfer` `complete` and `cancel` issuable. `label` names a destination -- a person, a queue -- not a phrase; the host supplies the verb. Present when the provider knows the room, absent when it does not. See **Every dial has an outcome**. |
|
|
2095
2133
|
| `lead` | Voice only. Present from the agent's request for a lead until the lead leaves or the request ends: `requested` while nobody has joined, `joined` with the lead's `leadId` once somebody has. See **Consulting a lead**. |
|
|
2096
2134
|
| `assisting` | Voice only, on the lead's own task for a call they joined: which member asked, with their note. Its presence is what makes `lead` `take-over` and `leave` issuable. See **Consulting a lead**. |
|
|
2097
2135
|
|
|
@@ -2254,15 +2292,23 @@ const immediateProviderCompletion = {
|
|
|
2254
2292
|
|
|
2255
2293
|
### How a task has been handled
|
|
2256
2294
|
|
|
2257
|
-
`Task.handlingHistory` is the
|
|
2295
|
+
`Task.handlingHistory` is the call record: the steps that brought the task to the agent, oldest
|
|
2296
|
+
first, and what they add up to before this agent:
|
|
2258
2297
|
|
|
2259
2298
|
```ts
|
|
2260
|
-
handlingHistory:
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2299
|
+
handlingHistory: {
|
|
2300
|
+
steps: [
|
|
2301
|
+
{ step: "queued", at: "2026-08-21T00:59:00Z", seconds: 41 },
|
|
2302
|
+
{ step: "answered", at: "2026-08-21T00:59:41Z", seconds: 312, by: "a-17" },
|
|
2303
|
+
{ step: "held", at: "2026-08-21T01:02:10Z", seconds: 35, by: "a-17" },
|
|
2304
|
+
{ step: "transferred", at: "2026-08-21T01:04:53Z", by: "a-17" },
|
|
2305
|
+
{ step: "answered", at: "2026-08-21T01:05:02Z", by: "a-23" },
|
|
2306
|
+
],
|
|
2307
|
+
handleSeconds: 312, // handled by others before this agent
|
|
2308
|
+
holdSeconds: 35, // holds others put the caller on
|
|
2309
|
+
queueSeconds: 41, // waiting before anyone answered
|
|
2310
|
+
transfers: 1, // hands the call has changed
|
|
2311
|
+
}
|
|
2266
2312
|
```
|
|
2267
2313
|
|
|
2268
2314
|
**This is not an archive.** It is live data about a task that is still open: it travels with the task
|
|
@@ -2286,9 +2332,16 @@ duration is the `held` entry's `seconds`, and a desk that restarts its counter o
|
|
|
2286
2332
|
counting the wrong thing.
|
|
2287
2333
|
|
|
2288
2334
|
`muted` is there because Omni performs the mute rather than the provider — see **Where a command
|
|
2289
|
-
executes** — so without a step the
|
|
2335
|
+
executes** — so without a step the provider, which alone keeps the task's record, would have no
|
|
2290
2336
|
account of a period the agent could not be heard.
|
|
2291
2337
|
|
|
2338
|
+
**A step that dialled says which dial and where.** `transferred`, `conferenced` and `unanswered`
|
|
2339
|
+
each end a dial, so the entry carries the `destination` it went to and, where a host placed it, the
|
|
2340
|
+
`dialId` the host minted; no other step dialled, and either field on one is refused
|
|
2341
|
+
(`task.handlingHistory.dialId.unexpected`). This is how a dial made before a transfer is placeable
|
|
2342
|
+
by whoever holds the task now: a `dial-outcome` naming a `dialId` the host never minted is looked
|
|
2343
|
+
up in the record, not discarded.
|
|
2344
|
+
|
|
2292
2345
|
Four rules a provider has to keep:
|
|
2293
2346
|
|
|
2294
2347
|
- **Report `seconds`; never expect Omni to derive it.** Omni does not subtract one timestamp from
|
|
@@ -2321,8 +2374,52 @@ A host must render the two differently. Showing an unattributed `answered` the s
|
|
|
2321
2374
|
`queued` tells the agent nobody was involved, which is not what was said. Omni renders it as
|
|
2322
2375
|
*"not recorded"* in the place the name would go.
|
|
2323
2376
|
|
|
2324
|
-
Omit `handlingHistory` entirely when the provider cannot observe the steps.
|
|
2325
|
-
claim — it says the task has had none.
|
|
2377
|
+
Omit `handlingHistory` entirely when the provider cannot observe the steps. Empty `steps` is a
|
|
2378
|
+
different claim — it says the task has had none.
|
|
2379
|
+
|
|
2380
|
+
**What the record adds up to is stated, not summed.** A call that has changed hands arrives
|
|
2381
|
+
carrying what others already used of it, and the agent reads that before they say hello: the
|
|
2382
|
+
totals are the provider's own — never a desk summing instants that may be rounded or skewed, for
|
|
2383
|
+
the same reason `seconds` is reported and not derived — and each is present when the provider
|
|
2384
|
+
knows it and absent when it does not, never a plausible nought. A fresh call from the queue has
|
|
2385
|
+
`steps` with no prior `answered` and no totals to state, and a desk shows nothing rather than
|
|
2386
|
+
zeros. The record is restated with the task on every `task-updated` and snapshot, so an entry
|
|
2387
|
+
that arrives late corrects the sums. These four are what every platform reports; more will be
|
|
2388
|
+
added here as a need is shown, not invented ahead of one.
|
|
2389
|
+
|
|
2390
|
+
#### The host records what it performs
|
|
2391
|
+
|
|
2392
|
+
`muted` is the one leg Omni performs rather than the provider, and a record kept by the provider
|
|
2393
|
+
would have a hole exactly there. So the host reports it, through `recordStep`, and the provider
|
|
2394
|
+
writes it into its record as it writes every other leg:
|
|
2395
|
+
|
|
2396
|
+
```ts
|
|
2397
|
+
declare const connection: Connection<"voice">;
|
|
2398
|
+
declare const taskId: TaskId;
|
|
2399
|
+
declare const at: IsoTimestamp;
|
|
2400
|
+
|
|
2401
|
+
void connection.recordStep?.({ taskId, step: "muted", at }); // the moment the agent mutes
|
|
2402
|
+
void connection.recordStep?.({ taskId, step: "muted", at, seconds: 15 }); // still running, if the host chooses to say
|
|
2403
|
+
void connection.recordStep?.({ taskId, step: "muted", at, seconds: 42, ended: true }); // the moment they unmute
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
The entry is keyed by `step` and `at`, so every report about one leg names the same instant. The
|
|
2407
|
+
host is the authority for the legs it performs, so it may say how long so far — `seconds` is
|
|
2408
|
+
elapsed while the leg runs and final once it has ended — and **the end is stated, never
|
|
2409
|
+
inferred**: `ended: true` marks the last report, and it carries the final duration. **What a
|
|
2410
|
+
provider never asked for never crosses.** The running report is sent only to a provider whose
|
|
2411
|
+
manifest declares `runningStepReports`; every other provider receives exactly two reports per
|
|
2412
|
+
leg, when it began and when it ended, and `validateHandlingReport(report, path, manifest)` refuses
|
|
2413
|
+
a running one it was never asked for (`handlingReport.running.unexpected`). What a provider that
|
|
2414
|
+
did ask for them forwards upstream, and how often, is its own business. The step appears in
|
|
2415
|
+
`handlingHistory` when the *provider* publishes it: Omni never writes the record itself.
|
|
2416
|
+
`recordStep` is required of a connection whose tasks declare `mute`, and answers `recorded`.
|
|
2417
|
+
|
|
2418
|
+
**A host-performed leg still open when the task's media ends, or the task ends, is ended by the
|
|
2419
|
+
host at that instant** — `ended: true`, `seconds` to the end, the same `at` — since a provider
|
|
2420
|
+
left to close it would be guessing at a host-performed duration. Both ends are read off one clock,
|
|
2421
|
+
the host's, and a leg shorter than a second is reported as the second it was, never as the nought
|
|
2422
|
+
the record refuses.
|
|
2326
2423
|
|
|
2327
2424
|
### Browser capability
|
|
2328
2425
|
|
|
@@ -2461,7 +2558,7 @@ const taskCapabilities = {
|
|
|
2461
2558
|
| `blindTransfer` | Secondary menu item: Blind transfer | Omni may transfer the caller directly to a destination. |
|
|
2462
2559
|
| `consultTransfer` | Secondary menu item: Consult transfer | Omni may park the customer and call a destination first, then hand the customer over or cancel back. See **Consult transfer**. |
|
|
2463
2560
|
| `consultLead` | Secondary menu item: Consult lead | Omni may ask a lead to join this call, with a note. The lead's decision reaches the agent on `Task.lead`. See **Consulting a lead**. |
|
|
2464
|
-
| `conference` | Secondary button: Conference | Omni may
|
|
2561
|
+
| `conference` | Secondary button: Conference | Omni may dial a destination into the active call, and drop a `conferenced` entry from it. |
|
|
2465
2562
|
| `recording` | Overflow menu item: Recording | Omni may expose start, pause, resume, and stop recording controls. |
|
|
2466
2563
|
| `dispositions` | Primary button: Complete | Omni may request task disposal with a provider disposition and notes. |
|
|
2467
2564
|
|
|
@@ -2510,7 +2607,7 @@ capabilities: {
|
|
|
2510
2607
|
| Field | Contract |
|
|
2511
2608
|
| --- | --- |
|
|
2512
2609
|
| `destinations` | Directory Omni renders, with unique `id` values. |
|
|
2513
|
-
| `address` | The value Omni sends as `
|
|
2610
|
+
| `address` | The value Omni sends as `destination` on a `transfer` or a `conference` command. |
|
|
2514
2611
|
| `kind` | Where the contact is going. See below. |
|
|
2515
2612
|
| `allowManualEntry` | Whether the agent may type a destination that is not in the directory. A directory with no destinations must allow manual entry, or the control has nothing to offer. |
|
|
2516
2613
|
|
|
@@ -2534,9 +2631,10 @@ from `blindTransfer` (a hand-over with nobody consulted) and from `conference` (
|
|
|
2534
2631
|
call): a queue may offer any of the three without the others, and each is declared on its own.
|
|
2535
2632
|
|
|
2536
2633
|
```ts
|
|
2537
|
-
// 1. Consult.
|
|
2538
|
-
// `
|
|
2539
|
-
|
|
2634
|
+
// 1. Consult. A dial: the provider parks the customer and calls the destination, answers
|
|
2635
|
+
// `dialling`, and the task reports `paused` with the destination `consulted` on `onCall`
|
|
2636
|
+
// while the call to it stands.
|
|
2637
|
+
{ type: "transfer", action: "consult", dialId: "dial-7f2", destination: "+14155550111" }
|
|
2540
2638
|
|
|
2541
2639
|
// 2a. Hand the customer to the consulted destination and leave.
|
|
2542
2640
|
{ type: "transfer", action: "complete" }
|
|
@@ -2546,21 +2644,25 @@ call): a queue may offer any of the three without the others, and each is declar
|
|
|
2546
2644
|
```
|
|
2547
2645
|
|
|
2548
2646
|
`consult` is gated by the `consultTransfer` capability and takes a destination exactly as a blind
|
|
2549
|
-
transfer does, from the same kind of directory.
|
|
2550
|
-
|
|
2551
|
-
destination
|
|
2552
|
-
|
|
2553
|
-
|
|
2647
|
+
transfer does, from the same kind of directory. It is a dial, so it carries the host's `dialId`
|
|
2648
|
+
and is answered `dialling`, and its `dial-outcome` says whether the destination was reached. While
|
|
2649
|
+
the destination is on the line the task carries a `consulted` entry on `onCall`, and that presence
|
|
2650
|
+
is what makes `complete` and `cancel` issuable -- they name no destination because there is exactly
|
|
2651
|
+
one they could mean, which is why a task carries at most one `consulted` entry
|
|
2652
|
+
(`task.onCall.consulted.single`). A consultation that could be started but not finished would
|
|
2653
|
+
strand the customer and the destination both, which is why all three are commands and a provider
|
|
2654
|
+
that offers `consultTransfer` implements all three.
|
|
2554
2655
|
|
|
2555
2656
|
`applied` on `complete` says the provider is bridging the customer to the destination and
|
|
2556
2657
|
dropping the agent's leg. What follows is what follows any transfer: the agent's media ends and
|
|
2557
2658
|
the provider reports `task-media-ended`, any wrap allowance runs, and the task ends with a
|
|
2558
2659
|
`transferred` outcome naming the destination. `applied` on `cancel` says the destination is
|
|
2559
|
-
dropped; the task returns to `in-progress` with `
|
|
2660
|
+
dropped; the task returns to `in-progress` with the `consulted` entry gone. Omni waits for the
|
|
2560
2661
|
provider's report of both, as it does for every command.
|
|
2561
2662
|
|
|
2562
|
-
A destination that does not answer is a
|
|
2563
|
-
|
|
2663
|
+
A destination that does not answer is a dial that ended: the provider reports its `dial-outcome`,
|
|
2664
|
+
returns the task to `in-progress` with no `consulted` entry, and the agent is back with the
|
|
2665
|
+
customer.
|
|
2564
2666
|
|
|
2565
2667
|
### Chat capabilities
|
|
2566
2668
|
|
|
@@ -2623,14 +2725,101 @@ Custom capabilities must not redefine the meaning of a standard channel capabili
|
|
|
2623
2725
|
Starts one outbound call from the idle dialpad. It is present only when the voice provider
|
|
2624
2726
|
declares `dial`.
|
|
2625
2727
|
|
|
2728
|
+
- `dialId` is the host's identity for this dial, minted before the request leaves.
|
|
2626
2729
|
- `destination` is the original number selected or entered by the agent.
|
|
2627
2730
|
- The provider holds `destination` to its declared `destinations`: under `contacts-only`, a
|
|
2628
2731
|
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
2629
|
-
- `
|
|
2732
|
+
- `dialling` says the request was accepted and the call is being placed, and restates the
|
|
2733
|
+
`dialId`. It says nothing about whether anyone will answer.
|
|
2630
2734
|
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
2631
2735
|
|
|
2632
|
-
The resulting call is offered through the normal `task-offered` event
|
|
2633
|
-
does not manufacture a task
|
|
2736
|
+
The resulting call is offered through the normal `task-offered` event, and how the dial ended
|
|
2737
|
+
arrives on `dial-outcome` under the same `dialId`. A `dialling` result does not manufacture a task
|
|
2738
|
+
inside Omni.
|
|
2739
|
+
|
|
2740
|
+
## Every dial has an outcome
|
|
2741
|
+
|
|
2742
|
+
Four commands place a call: `dial` from the idle dialpad, a blind or consult `transfer`, a
|
|
2743
|
+
`conference` `add`, and `callback`. Each is accepted or refused at once, and each then ends later
|
|
2744
|
+
and apart from its answer -- the destination picks up, is busy, or never does -- and a dial placed
|
|
2745
|
+
late in a call routinely outlives the call. Nothing in between is reported: the wire says
|
|
2746
|
+
`dialling`, then says how it ended, once.
|
|
2747
|
+
|
|
2748
|
+
**The host mints the identity.** Every command that dials carries a `dialId` the host made, unique
|
|
2749
|
+
across the installation, and it travels four ways so no leg is a lookup:
|
|
2750
|
+
|
|
2751
|
+
```ts
|
|
2752
|
+
declare const connection: Connection<"voice">;
|
|
2753
|
+
declare const taskId: TaskId;
|
|
2754
|
+
|
|
2755
|
+
// 1. Out, minted by the host.
|
|
2756
|
+
const result = await connection.execute({ taskId, command: { type: "conference", action: "add", dialId: "dial-7f2", destination: "+14155550111" } });
|
|
2757
|
+
|
|
2758
|
+
// 2. Back on the result, restated, so the host compares and confirms.
|
|
2759
|
+
expect(result).toEqual({ status: "dialling", dialId: "dial-7f2" });
|
|
2760
|
+
|
|
2761
|
+
// 3. On the outcome, however late, against the task it belonged to.
|
|
2762
|
+
const outcome: ProviderEvent<"voice"> = { type: "dial-outcome", dialId: "dial-7f2", taskId, destination: "+14155550111", outcome: "no-answer", reason: "No route to destination" };
|
|
2763
|
+
|
|
2764
|
+
// 4. In the record, so a dial made before a transfer is placeable by whoever holds the task now.
|
|
2765
|
+
const step: TaskHandlingStep = { step: "unanswered", at: "2026-08-21T09:15:30Z", by: "A-12", dialId: "dial-7f2", destination: "+14155550111" };
|
|
2766
|
+
```
|
|
2767
|
+
|
|
2768
|
+
The identity is a correlation handle between host and provider and **is never shown to the
|
|
2769
|
+
agent**. A host holding several tasks places an outcome by the `dialId` it minted; one it did not
|
|
2770
|
+
mint -- a dial made by the previous agent before a transfer -- it finds on the task's `onCall` or in
|
|
2771
|
+
its `handlingHistory`, which is why the steps that dial carry it. The `dialId` is not a retry key:
|
|
2772
|
+
Omni never repeats a dial, and a command still carries no key for that purpose.
|
|
2773
|
+
|
|
2774
|
+
**`dialling` is the answer to a dial, and `applied` is not.** A command that dials answers
|
|
2775
|
+
`{ status: "dialling", dialId }`, restating the host's identity (`result.dialId`,
|
|
2776
|
+
`result.dialId.mismatch`); `applied` from such a command, or `dialling` from one that dialled
|
|
2777
|
+
nothing, is refused (`result.status`). `validateResult(result, method, path, dialId)` takes the
|
|
2778
|
+
`dialId` the host sent for that reason.
|
|
2779
|
+
|
|
2780
|
+
**The outcome is stated, once, either way.** A `dial-outcome` names the `dialId`, the task where
|
|
2781
|
+
there was one, the destination where the provider knows it, and how the dial ended -- from a closed
|
|
2782
|
+
set: `answered`, `busy`, `no-answer`, `unreachable`, `rejected`. `answered` is stated rather than
|
|
2783
|
+
read off somebody appearing on the call, because a host that infers success cannot tell "reached"
|
|
2784
|
+
from "still ringing". `reason` is the switch's own words, optional, and shown to the agent
|
|
2785
|
+
attributed to the switch rather than to Omni. The task named may already have ended, and the
|
|
2786
|
+
harness places the outcome all the same (`TaskStream`, `stream.dialOutcome.unknown`,
|
|
2787
|
+
`stream.dialOutcome.duplicate`); an agent who moved on still learns nobody was reached.
|
|
2788
|
+
|
|
2789
|
+
**A provider says which outcomes it can tell apart.** No switch distinguishes all five, and a
|
|
2790
|
+
provider that cannot tell busy from unreachable must not pick one. The manifest declares
|
|
2791
|
+
`dialOutcomes`, and a `dial-outcome` carries only a declared member
|
|
2792
|
+
(`event.dialOutcome.undeclared`). The list includes `answered`, or the provider could never state a
|
|
2793
|
+
success (`manifest.dialOutcomes.answered`), and at least one other member, or it could never state
|
|
2794
|
+
a failure (`manifest.dialOutcomes.failure`). A provider that dials at all declares it: an idle
|
|
2795
|
+
dialpad requires it (`manifest.dialOutcomes.required`), and a task that declares `callback`,
|
|
2796
|
+
`blindTransfer`, `consultTransfer` or `conference` under a manifest without it is refused
|
|
2797
|
+
(`task.capability.dialOutcomes.required`). Off voice nothing dials, and the field is a compile
|
|
2798
|
+
error there.
|
|
2799
|
+
|
|
2800
|
+
**Who is on the call is stated on the task.** `Task.onCall` lists everyone the task's audio joins
|
|
2801
|
+
right now, replaced whole with the task like every other field, and present when the provider
|
|
2802
|
+
knows the room:
|
|
2803
|
+
|
|
2804
|
+
```ts
|
|
2805
|
+
const onCall: OnCall[] = [
|
|
2806
|
+
{ role: "party", since: "2026-08-21T09:12:00Z" },
|
|
2807
|
+
{ role: "agent", userId: "A-12", since: "2026-08-21T09:12:04Z" },
|
|
2808
|
+
{ role: "conferenced", destination: "+14155550111", dialId: "dial-7f2", held: true, since: "2026-08-21T09:15:30Z" },
|
|
2809
|
+
];
|
|
2810
|
+
```
|
|
2811
|
+
|
|
2812
|
+
`party` is the customer and `agent` a person by user id; neither was dialled from anywhere, so
|
|
2813
|
+
neither carries a destination. `consulted` and `conferenced` were, so both carry the `destination`
|
|
2814
|
+
and, where a host's dial brought them, its `dialId`; a person the platform added itself carries
|
|
2815
|
+
none. `held: true` is presence as claim. A snapshot establishes state, so a host that attaches
|
|
2816
|
+
after a transfer reads the room from here rather than inferring it from a sequence of outcomes it
|
|
2817
|
+
never saw. A `conference` `remove` names the `destination` of the `conferenced` entry to drop.
|
|
2818
|
+
|
|
2819
|
+
The desk shows a dial as placed on `dialling`, never as reached; shows the person in the room when
|
|
2820
|
+
they appear on `onCall`; and shows the outcome, with the switch's reason, against the call it
|
|
2821
|
+
belonged to -- with no retry, since a wrong number is not worth redialling and the decision is the
|
|
2822
|
+
agent's.
|
|
2634
2823
|
|
|
2635
2824
|
## Breaks
|
|
2636
2825
|
|
|
@@ -2718,6 +2907,7 @@ everywhere; success is not.
|
|
|
2718
2907
|
| Method | Succeeded |
|
|
2719
2908
|
| --- | --- |
|
|
2720
2909
|
| `setCapacity` | `applied` |
|
|
2910
|
+
| `recordStep` | `recorded` |
|
|
2721
2911
|
| `requestBreak` | `requested` |
|
|
2722
2912
|
| `commitBreak` | `committed` |
|
|
2723
2913
|
| `cancelBreak` | `cancelled` |
|
|
@@ -2880,36 +3070,36 @@ Omni offers the aggregate Break control only when every provider currently holdi
|
|
|
2880
3070
|
declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
|
|
2881
3071
|
knowingly permit partial availability.
|
|
2882
3072
|
|
|
2883
|
-
Omni coordinates one attempt as follows:
|
|
3073
|
+
Omni coordinates one break attempt as follows:
|
|
2884
3074
|
|
|
2885
|
-
1. Freeze the
|
|
2886
|
-
receive work. A provider joining during the attempt is given no capacity until it
|
|
2887
|
-
A provider whose authentication is `expired` is not one the agent can receive work
|
|
2888
|
-
not
|
|
2889
|
-
is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
2890
|
-
until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
2891
|
-
and work continues. `
|
|
3075
|
+
1. Freeze the providers the attempt asks: every connected provider from which the agent can
|
|
3076
|
+
currently receive work. A provider joining during the attempt is given no capacity until it
|
|
3077
|
+
finishes. A provider whose authentication is `expired` is not one the agent can receive work
|
|
3078
|
+
from and is not asked: nothing is asked of it, the break proceeds without it, and when the
|
|
3079
|
+
login is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
3080
|
+
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
3081
|
+
available and work continues. `assertBreakAttemptProviders` holds a host to this set.
|
|
2892
3082
|
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
2893
|
-
3. Send one `requestBreak` to every
|
|
3083
|
+
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-decision` or
|
|
2894
3084
|
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
2895
3085
|
causes Omni to take the cancel path.
|
|
2896
|
-
4. If every
|
|
2897
|
-
and send `commitBreak()` to
|
|
3086
|
+
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
3087
|
+
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
2898
3088
|
and reports `starting-after-task` or `in-effect`. The **commit bound** — ten seconds from the
|
|
2899
|
-
decision, tunable per deployment — decides who is kept: a
|
|
3089
|
+
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
2900
3090
|
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
2901
|
-
begins without it. `in-effect` decides `on-break`: Omni enters it once every kept
|
|
2902
|
-
reports `in-effect`. A kept
|
|
3091
|
+
begins without it. `in-effect` decides `on-break`: Omni enters it once every kept provider
|
|
3092
|
+
reports `in-effect`. A kept provider reporting `starting-after-task` has applied the commit
|
|
2903
3093
|
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
2904
3094
|
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
2905
|
-
5. If any
|
|
3095
|
+
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
2906
3096
|
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
2907
|
-
`cancelling-break`. Send `cancelBreak()` to every
|
|
3097
|
+
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
2908
3098
|
`awaiting-decision` or `granted`. Work continues during cancellation because no stop was
|
|
2909
|
-
committed. Return to `working` only after no
|
|
3099
|
+
committed. Return to `working` only after no provider retains either state.
|
|
2910
3100
|
|
|
2911
|
-
Commit and cancel are mutually exclusive decisions for one attempt. Once Omni chooses commit
|
|
2912
|
-
never rolls that attempt back: it is reconciled by snapshot until every
|
|
3101
|
+
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
3102
|
+
it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
|
|
2913
3103
|
A provider that reports `granted` must therefore preserve the request across reconnects and must
|
|
2914
3104
|
honour a later commit or cancel. This
|
|
2915
3105
|
durable promise prevents a provider from failing the commit after another provider has already
|
|
@@ -2918,7 +3108,7 @@ stopped the agent.
|
|
|
2918
3108
|
#### Why the commit phase is bounded and the decision is not
|
|
2919
3109
|
|
|
2920
3110
|
Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
|
|
2921
|
-
durable and cannot be rolled back, so a
|
|
3111
|
+
durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
|
|
2922
3112
|
or is uninstalled between granting and committing would hold Omni in `committing-break` with no
|
|
2923
3113
|
exit: the providers that did commit have already stopped the agent, and the agent is neither
|
|
2924
3114
|
working nor on a break.
|
|
@@ -2928,7 +3118,7 @@ on one platform while another keeps routing work to them — and **a provider Om
|
|
|
2928
3118
|
routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
|
|
2929
3119
|
for it costs the agent their break.
|
|
2930
3120
|
|
|
2931
|
-
Setting a
|
|
3121
|
+
Setting a provider aside is not a rollback and not a cancel. The commit stands and the
|
|
2932
3122
|
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
2933
3123
|
snapshot before anything else, and the snapshot decides:
|
|
2934
3124
|
|
|
@@ -2943,7 +3133,7 @@ snapshot before anything else, and the snapshot decides:
|
|
|
2943
3133
|
The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
|
|
2944
3134
|
no capacity until it is reconciled.
|
|
2945
3135
|
|
|
2946
|
-
**If the agent has already ended the break elsewhere, the attempt is over** and the returning
|
|
3136
|
+
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
2947
3137
|
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
2948
3138
|
would stop an agent who is working again; `starting-after-task` or `in-effect` gets `endBreak()`.
|
|
2949
3139
|
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
@@ -2952,7 +3142,7 @@ Omni may tell the agent which platforms the break has not yet reached, as it alr
|
|
|
2952
3142
|
break cannot be paired across every provider.
|
|
2953
3143
|
|
|
2954
3144
|
The decision phase needs no such bound, because nothing has stopped. Work continues throughout
|
|
2955
|
-
`requesting-break`, so a
|
|
3145
|
+
`requesting-break`, so a provider that never answers costs the agent a wait rather than their
|
|
2956
3146
|
availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
|
|
2957
3147
|
because no stop was ever committed.
|
|
2958
3148
|
|
|
@@ -3368,7 +3558,7 @@ declared:
|
|
|
3368
3558
|
| `complete` | `completionMode: "agent-command"`. The `dispositions` capability decides whether a code travels with the command, never whether the command exists — a task Omni cannot complete never ends. |
|
|
3369
3559
|
| `callback` | The `callback` capability **and** the `completing` phase. It exists to reach the party again after the call, so it has no meaning while the call is up. |
|
|
3370
3560
|
| `transfer` with `action: "consult"` | The `consultTransfer` capability. Blind `transfer` is gated by `blindTransfer`; the two are declared and offered separately. |
|
|
3371
|
-
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- `
|
|
3561
|
+
| `transfer` with `action: "complete"` or `"cancel"` | A consultation in progress -- a `consulted` entry on `Task.onCall`. Without one there is nothing to complete or cancel, and a provider that receives either answers `failed`. |
|
|
3372
3562
|
| `lead` with `action: "request"` or `"cancel"` | The `consultLead` capability. `cancel` needs a request standing -- `Task.lead` with status `requested`. |
|
|
3373
3563
|
| `lead` with `action: "take-over"` or `"leave"` | The lead's own task, on a call they joined -- `Task.assisting` present. An agent's task never has it, and a provider that receives either without it answers `failed`. |
|
|
3374
3564
|
| Everything else | Its own named capability. |
|
|
@@ -3559,11 +3749,11 @@ from `in-effect` or `starting-after-task` to a grant or a request, or from `gran
|
|
|
3559
3749
|
`exerciseAdapter` holds the stream to that from the connect snapshot on;
|
|
3560
3750
|
`assertBreakFollowsItsRequests` holds any sequence.
|
|
3561
3751
|
|
|
3562
|
-
For a multi-provider attempt, "every provider" is the
|
|
3563
|
-
entered `requesting-break`. Omni commits only after every
|
|
3752
|
+
For a multi-provider break attempt, "every provider" is the set of providers frozen when the
|
|
3753
|
+
attempt entered `requesting-break`. Omni commits only after every asked provider reports `granted` —
|
|
3564
3754
|
that one is unconditional, because nothing has stopped yet and waiting costs only time. It enters
|
|
3565
|
-
`on-break` once every kept
|
|
3566
|
-
setting aside a
|
|
3755
|
+
`on-break` once every kept provider reports `in-effect`; the commit bound decides who is kept,
|
|
3756
|
+
setting aside a provider that has not applied the commit rather than holding a break that has
|
|
3567
3757
|
already begun elsewhere. Otherwise it follows the two-phase rules under **Coordinating a
|
|
3568
3758
|
multi-provider break**.
|
|
3569
3759
|
|
|
@@ -3620,6 +3810,15 @@ The `task-media-ended` event and the `completing` phase are likewise non-termina
|
|
|
3620
3810
|
snapshot that no longer contains the task also clears it. Repeated `task-ended` delivery with the
|
|
3621
3811
|
same envelope ID is harmless.
|
|
3622
3812
|
|
|
3813
|
+
### `dial-outcome`
|
|
3814
|
+
|
|
3815
|
+
How a dial the host placed ended, once, either way, named by the `dialId` the host sent: `answered`,
|
|
3816
|
+
or one of the declared ways of not reaching the destination, with the switch's `reason` where it
|
|
3817
|
+
gave one. `taskId` names the task the dial was on, where there was one, and that task may already
|
|
3818
|
+
have ended. Omni shows it to the agent against the call it belonged to and does nothing else: no
|
|
3819
|
+
audio moves, since the provider bridges the leg on its side, and no dial is repeated. See **Every
|
|
3820
|
+
dial has an outcome**.
|
|
3821
|
+
|
|
3623
3822
|
### `announcement`
|
|
3624
3823
|
|
|
3625
3824
|
Publishes an agent-facing message. `text` is always required and is the accessible fallback.
|
|
@@ -3714,6 +3913,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3714
3913
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
3715
3914
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
3716
3915
|
| `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
|
|
3916
|
+
| `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
3717
3917
|
| `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. |
|
|
3718
3918
|
| `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. |
|
|
3719
3919
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|
|
@@ -3805,7 +4005,7 @@ cannot be established from TypeScript structure alone.
|
|
|
3805
4005
|
| `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. |
|
|
3806
4006
|
| `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. |
|
|
3807
4007
|
| `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. |
|
|
3808
|
-
| `
|
|
4008
|
+
| `assertBreakAttemptProviders(candidates, asked)` | A break attempt asks every usable provider holding capacity, `refreshing` included, and nothing of a provider whose login is `expired`. |
|
|
3809
4009
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
3810
4010
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
3811
4011
|
| `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. |
|