@xema/omni-protocol 0.1.106 → 0.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/guide/agent.md +1090 -0
- package/guide/breaks.md +540 -0
- package/guide/chat.md +12 -0
- package/guide/email.md +11 -0
- package/guide/lead.md +333 -0
- package/guide/phone.md +274 -0
- package/guide/voice.md +602 -0
- package/guide.md +41 -2865
- package/package.json +2 -1
package/guide/breaks.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# `@xema/omni-protocol`: Breaks
|
|
2
|
+
|
|
3
|
+
Asking for a break, a break forced on the agent, capacity, and one break across several providers. Part of the contract in `guide.md`, which holds the terms, the shapes and the rules every file here relies on; a reference in bold names the file it points into where that is not this one.
|
|
4
|
+
|
|
5
|
+
## Breaks
|
|
6
|
+
|
|
7
|
+
Everything about an agent's breaks on one provider arrives as one object, `Snapshot.break`,
|
|
8
|
+
replaced whole by a single `break-state` event. These facts are only meaningful together — an
|
|
9
|
+
approval says nothing without knowing whether the agent chose the break, and a list of reasons says
|
|
10
|
+
nothing while none are being accepted — so they are not published separately.
|
|
11
|
+
|
|
12
|
+
| Field | Contract |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `status` | Where the agent’s break lifecycle stands. See the states below. |
|
|
15
|
+
| `canRequestBreak` | Whether the agent may ask at all. Distinct from `status`. |
|
|
16
|
+
| `requestUnavailableReason` | Display-ready reason shown when `canRequestBreak` is false — a standing gate that applies to everyone. |
|
|
17
|
+
| `decisionReason` | The words whoever decided attached, from `decide.reason`. About one request and one decision, not a standing gate. |
|
|
18
|
+
| `retryRequestAfterMs` | Milliseconds until the agent may retry a break request, when the provider can say. |
|
|
19
|
+
| `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. |
|
|
20
|
+
| `activeReasonId` | The `BreakReason.id` the current break is on. Omitted when there is no break. Required on a break `on-break` or `starting-after-task` where the provider publishes `reasons`, a forced break included (`break.activeReasonId.required`): a break with a kind the provider cannot name is a break whose rules nobody can apply. |
|
|
21
|
+
| `forced` | Set when the break was forced on the agent rather than requested. |
|
|
22
|
+
|
|
23
|
+
A request can be waiting for two unrelated things, and they are separate values because
|
|
24
|
+
rendering one as the other tells an agent to wait for somebody who is never coming:
|
|
25
|
+
|
|
26
|
+
| `status` | Meaning |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `not-requested` | No request outstanding. |
|
|
29
|
+
| `awaiting-approval` | A person has to decide. The agent is waiting on somebody. |
|
|
30
|
+
| `granted` | The request was granted, by a person or by an `automatically-approved` policy. 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. |
|
|
31
|
+
| `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 `on-break`. |
|
|
32
|
+
| `on-break` | The agent is on the break now. It holds no task: a break begins when the work ends, so a snapshot reporting `on-break` beside a task is refused as `break.on-break.tasks`. The one exception is a call a lead took over, which the provider assigns whatever break the lead is on -- see **Lead assist** in `guide/lead.md`. |
|
|
33
|
+
|
|
34
|
+
A denial is a decision, not a standing approval state. The provider transitions the request directly
|
|
35
|
+
to `not-requested`; Omni returns the agent to idle and never asks again on their behalf. They saw the
|
|
36
|
+
answer and ask again when they want to. `decisionReason` may carry the words attached to that
|
|
37
|
+
decision, but `status` does not remain denied.
|
|
38
|
+
|
|
39
|
+
A provider reports `starting-after-task` only after Omni commits a `granted` request while
|
|
40
|
+
work is still active. Omni does not send the request again, because asking again would not move
|
|
41
|
+
it; it sends the commit again only from a reconnect snapshot that shows the grant still standing.
|
|
42
|
+
|
|
43
|
+
`canRequestBreak: false` is what lets Omni withdraw the control rather than let an agent ask and be
|
|
44
|
+
refused. A `BreakReason` marked `alwaysAvailable` survives it: a mandatory rest period is not
|
|
45
|
+
something a busy hour can cancel, and Omni keeps offering those while the rest are withdrawn.
|
|
46
|
+
|
|
47
|
+
### Forced breaks
|
|
48
|
+
|
|
49
|
+
Migration: the former awaiting-decision value is now `awaiting-approval` in break status
|
|
50
|
+
and the team member’s pending break state. The old value is rejected without an alias;
|
|
51
|
+
the request can still be granted or denied, and ordering rules are unchanged.
|
|
52
|
+
|
|
53
|
+
Migration: the former BreakApproval type is now `BreakStatus`, matching `BreakState.status`.
|
|
54
|
+
The former type is not exported as an alias; its values and behavior are unchanged.
|
|
55
|
+
|
|
56
|
+
Migration: `BreakState.status` replaces the former approval field. It covers the full break
|
|
57
|
+
lifecycle. The old field is rejected even alongside status; related diagnostics now use status.
|
|
58
|
+
The `BreakStatus` union and its values, `reasons`, `activeReasonId`, and the separate testing
|
|
59
|
+
helper BreakOnTaskStep.approval are unchanged. Hosts and providers must update together.
|
|
60
|
+
|
|
61
|
+
Migration: the break-state field formerly named retryAfterMs is now `BreakState.retryRequestAfterMs`,
|
|
62
|
+
with diagnostic `break.retryRequestAfterMs`. It remains an optional non-negative finite number
|
|
63
|
+
of milliseconds; the former break field is rejected even alongside the new field.
|
|
64
|
+
The separate authentication and general failure retryAfterMs fields are unchanged.
|
|
65
|
+
|
|
66
|
+
Migration: the former in-effect break approval value is now `on-break`. It means the
|
|
67
|
+
agent is on the break, not merely granted permission. The old value is rejected without an
|
|
68
|
+
alias. The task prerequisite, the take-over exception and ordering rules are unchanged;
|
|
69
|
+
the task-conflict diagnostic is `break.on-break.tasks`. Team member availability already uses
|
|
70
|
+
`on-break` and is unchanged.
|
|
71
|
+
|
|
72
|
+
Migration: the former refusedReason field is now `BreakState.requestUnavailableReason`.
|
|
73
|
+
It explains why requests are unavailable when `canRequestBreak` is false; `decisionReason`
|
|
74
|
+
continues to explain a particular approval or denial. The old field is rejected even alongside
|
|
75
|
+
the new one, and related diagnostics use requestUnavailableReason.
|
|
76
|
+
|
|
77
|
+
Migration: the former mayAsk field is now `BreakState.canRequestBreak`. The old field is
|
|
78
|
+
rejected even beside the new field. Validation diagnostics use canRequestBreak in place of
|
|
79
|
+
mayAsk. Request eligibility remains distinct from approval; existing alwaysAvailable reason
|
|
80
|
+
exceptions are unchanged. Hosts and providers must update together.
|
|
81
|
+
|
|
82
|
+
Migration: ImposedBreak is now `ForcedBreak`, and the former imposed field is now
|
|
83
|
+
`BreakState.forced`. Diagnostic and harness coverage names use `break.forced`. The former agent-end prohibition
|
|
84
|
+
has been removed: forced breaks also permit explicit agent resumption. Update hosts and providers together. The old field
|
|
85
|
+
is rejected, including when both spellings are sent; no compatibility alias is provided.
|
|
86
|
+
The break policy value formerly named auto-approve is now `automatically-approved`.
|
|
87
|
+
Requests are approved automatically; approval does not skip commitment or the prerequisites
|
|
88
|
+
for starting a break. The former value is rejected without an alias.
|
|
89
|
+
The break policy value formerly named suspended is now `requests-suspended`. The former
|
|
90
|
+
value is rejected without an alias. It suspends new requests, not breaks already underway.
|
|
91
|
+
The break policy value formerly named ask is now `approval-required`: requests require
|
|
92
|
+
approval. The former value is rejected without an alias; automatically-approved and requests-suspended retain
|
|
93
|
+
their existing behavior.
|
|
94
|
+
The team break command formerly named policy is now `set-break-policy`. The former
|
|
95
|
+
command is rejected without an alias; its policy field is unchanged. Hosts and providers must adopt the new name together.
|
|
96
|
+
The team break command formerly named decide is now `decide-break-request`. The former
|
|
97
|
+
spelling is rejected without an alias; the granted/denied decisions and pending-request
|
|
98
|
+
prerequisite are unchanged.
|
|
99
|
+
The team break command formerly named place is now `force-break`. Hosts and providers must
|
|
100
|
+
adopt the new command together; place and the interim force spelling are rejected without aliases.
|
|
101
|
+
The team break command formerly named release is now `end-forced-break`, and its diagnostic is
|
|
102
|
+
`team.command.endForcedBreak`. The former release command and the interim end spelling are rejected without aliases.
|
|
103
|
+
The agent’s `endBreak` now applies to forced breaks as well as requested breaks.
|
|
104
|
+
|
|
105
|
+
`ForcedBreak` names who forced the break and may say how long it is expected to run yet, as
|
|
106
|
+
`expectedEndsInSeconds`: whole seconds from the publication that carries it, restated on every
|
|
107
|
+
publication, so every screen -- the agent's, reloaded or not, and every lead's -- counts down the
|
|
108
|
+
same number from receipt (`break.forced.expectedEndsInSeconds`). The duration the lead asked for
|
|
109
|
+
travels on the `force-break` command alone; the state says what is left, and drops the field once
|
|
110
|
+
it has run out. The agent must explicitly resume when ready. The retired endsAutomatically and
|
|
111
|
+
endsAt fields are rejected, including when an expectation is also provided. Who forced it is
|
|
112
|
+
`by`, as **A forced break is the provider's act** below sets out.
|
|
113
|
+
|
|
114
|
+
Omni resolves the name to show with `getUserDetails()`, so a provider sends the identifier and never
|
|
115
|
+
a display name.
|
|
116
|
+
|
|
117
|
+
**A forced break travels with `on-break` or `starting-after-task`, and nothing else.** It is a
|
|
118
|
+
break in progress or about to be; beside `granted` or `awaiting-approval` the agent application would read a
|
|
119
|
+
request the agent never made and commit it (`break.forced.status`). Where the provider publishes
|
|
120
|
+
`reasons`, the lead's `force-break` named one, and the member's state carries it as `activeReasonId`.
|
|
121
|
+
|
|
122
|
+
For example:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
forced: {
|
|
126
|
+
by: "manager-1042",
|
|
127
|
+
expectedEndsInSeconds: 600
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The agent application keeps **Resume** available for an agent on a forced break. The agent explicitly chooses
|
|
132
|
+
Resume; the agent application sends `endBreak()` and follows the provider-confirmed state before resuming
|
|
133
|
+
work. The expectation is whole seconds still to run, restated on every publication; omission
|
|
134
|
+
means none was stated, or it has run out. Neither its reaching zero nor an overdue indication
|
|
135
|
+
authorizes the agent application or provider to end the break, restore availability, or route
|
|
136
|
+
work to the agent.
|
|
137
|
+
|
|
138
|
+
**A forced break is the provider's act.** A lead's `force-break` is a command to the provider,
|
|
139
|
+
which forces the break on the member; `by` names the lead who asked it to. A break the platform
|
|
140
|
+
imposed on its own -- a schedule, a compliance hold -- says `by: "provider"`, and is the platform's
|
|
141
|
+
to lift: a lead's `end-forced-break` on it is refused before it is sent
|
|
142
|
+
(`team.command.endForcedBreak.provider`), and a provider that receives one answers `failed` with
|
|
143
|
+
`omni.break-forced-by-provider`. The desk offers End forced break on a member only where
|
|
144
|
+
`forced.by` names a lead.
|
|
145
|
+
|
|
146
|
+
**A lead lifting the restriction does not resume the agent.** On an applied
|
|
147
|
+
`end-forced-break`, the provider clears `BreakState.forced` while preserving the current
|
|
148
|
+
`on-break` or `starting-after-task` status and `activeReasonId`. This command cannot publish
|
|
149
|
+
`not-requested`, restore readiness, or start routing work. Ordinary progress from
|
|
150
|
+
`starting-after-task` to `on-break` still happens when work finishes. The agent application follows the
|
|
151
|
+
published state and waits for the agent's explicit Resume, even after reconnect. Historical
|
|
152
|
+
attribution is not rewritten by clearing the current forced marker.
|
|
153
|
+
|
|
154
|
+
The agent may explicitly resume either before or after the lead lifts the restriction. A lead
|
|
155
|
+
command, timer, late snapshot or reconnect is never a substitute for that choice. Providers
|
|
156
|
+
must correlate resumption to the authenticated agent's action; a state-only ordering validator
|
|
157
|
+
cannot establish the cause of a transition. The usual multi-provider end/reconciliation flow
|
|
158
|
+
still applies, and work resumes only on confirmed provider state.
|
|
159
|
+
|
|
160
|
+
An agent application counts `expectedEndsInSeconds` down from receipt, on every publication that
|
|
161
|
+
carries it, and shows nothing of it where the provider sends none. It invents no start and no
|
|
162
|
+
return time, and nothing acts on the number reaching zero.
|
|
163
|
+
|
|
164
|
+
A break applies to the **agent**, not to one provider. When a provider forces one, Omni stops the
|
|
165
|
+
agent everywhere else at once, and not by asking: it states `setCapacity({ count: 0 })` on every
|
|
166
|
+
other usable provider holding capacity, which is taken and never refused, and which their team
|
|
167
|
+
lists show as `elsewhere`. Nobody on those providers can deny it, and the agent cannot cancel it,
|
|
168
|
+
because there is nothing to cancel: a break request on them would be the agent's own to withdraw
|
|
169
|
+
and a lead's to refuse, and a forced break is neither. When the forcing provider ends the forced
|
|
170
|
+
break and the agent resumes there, the agent application restates its capacity on the rest. A
|
|
171
|
+
provider whose login is not usable is left alone, as a break attempt leaves it;
|
|
172
|
+
`assertForcedBreakStopsTheRest` holds an agent application to this set. **The provider keeps its
|
|
173
|
+
shift totals true.** A member held elsewhere is one the provider sees -- `elsewhere` is a state it
|
|
174
|
+
publishes -- and the day's `shift` is its own account of the day, so the time the agent was
|
|
175
|
+
stopped for a break forced on another provider is the provider's to count, as a break of a kind
|
|
176
|
+
its platform has, or introduces, for exactly this. A `breakSeconds` that leaves it out is a total
|
|
177
|
+
the provider knew to be short, and **Never report a value you cannot observe** cuts the other way
|
|
178
|
+
here: the provider observed it.
|
|
179
|
+
|
|
180
|
+
`ForcedBreak.by` says who forced it: a lead, by user id, or `provider` where the platform itself
|
|
181
|
+
did -- on its own rule, a schedule, a compliance hold. The agent sees who, and `getUserDetails()`
|
|
182
|
+
is owed for a person, never for `provider` (`break.forced.by`).
|
|
183
|
+
|
|
184
|
+
## Capacity and break actions
|
|
185
|
+
|
|
186
|
+
**Each method answers in its own words.** A result is read by a person far more often than it is
|
|
187
|
+
branched on by code — in a log, a support ticket, a conformance failure — so it says what happened
|
|
188
|
+
rather than that something happened. `failed` is shared, because failing is the same act
|
|
189
|
+
everywhere; success is not.
|
|
190
|
+
|
|
191
|
+
| Method | Succeeded |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `setCapacity` | `applied` |
|
|
194
|
+
| `recordStep` | `recorded` |
|
|
195
|
+
| `requestBreak` | `requested` |
|
|
196
|
+
| `commitBreak` | `committed` |
|
|
197
|
+
| `cancelBreak` | `cancelled` |
|
|
198
|
+
| `endBreak` | `ended` |
|
|
199
|
+
|
|
200
|
+
`failed` carries a typed `ProtocolFailure` and means the provider did not take the action, whether
|
|
201
|
+
it would not or could not.
|
|
202
|
+
|
|
203
|
+
**Succeeding is not the outcome.** `requested` says the provider holds the request, not that a
|
|
204
|
+
break was granted; `ended` says the provider has the instruction, not that the agent is working
|
|
205
|
+
again.
|
|
206
|
+
Every break method reports its real result through `break-state`; `setCapacity` reports none at
|
|
207
|
+
all, because capacity is a statement rather than a request.
|
|
208
|
+
|
|
209
|
+
`execute` keeps `applied` rather than a verb per command, because the command
|
|
210
|
+
is in the request: `execute({ command: { type: "hold" } })` returning `applied` already says the
|
|
211
|
+
hold applied. A `held` result would repeat the discriminant that travelled with it.
|
|
212
|
+
|
|
213
|
+
### `setCapacity(capacity)`
|
|
214
|
+
|
|
215
|
+
States how many tasks this provider may have assigned to the agent **at once**.
|
|
216
|
+
|
|
217
|
+
`count` is an absolute ceiling, not an increment, a whole number of zero or more
|
|
218
|
+
(`capacity.count`). An agent's capacity is a property of the agent, not of the moment: it is
|
|
219
|
+
stated when the agent is set up and restated only when it genuinely changes, which is a
|
|
220
|
+
local policy change rather than a task starting or ending -- with one exception below.
|
|
221
|
+
|
|
222
|
+
**The provider counts its own outstanding tasks against it.** Assign while you hold fewer than
|
|
223
|
+
`count` tasks for this agent, and stop when you hold that many; when one of yours ends you have
|
|
224
|
+
room again and need no new signal to know it. Omni does not re-state capacity as tasks come and
|
|
225
|
+
go, and a provider that waits for it will stall.
|
|
226
|
+
|
|
227
|
+
Your own tasks are the only ones you count. What the agent holds at other providers is not your
|
|
228
|
+
concern — Omni set `count` knowing it, and this is how: the agent is one person on several
|
|
229
|
+
providers, and the agent application divides their capacity among them rather than telling each the whole. A
|
|
230
|
+
provider that has none of it for now is told **`count: 0`, agent application-stopped**: assign nothing, show
|
|
231
|
+
the member as `elsewhere` on the team member list -- signed in here, capacity held by the agent application for elsewhere,
|
|
232
|
+
a fact this provider holds, where `on-task` would assert work it cannot see -- and take the next
|
|
233
|
+
count as any other when the agent application has capacity for this provider again. Zero is the one restatement
|
|
234
|
+
that follows work rather than local policy.
|
|
235
|
+
|
|
236
|
+
**A capacity is taken, never refused.** `setCapacity` answers `applied` and nothing else: a
|
|
237
|
+
statement has no failure to report, and a `failed` arm gave four agent application-provider pairings four
|
|
238
|
+
readings of which ceiling stood after it. A provider that cannot carry the count assigns within
|
|
239
|
+
what it can and says so on a `diagnostic`, so the gap is visible and nothing is inferred from a
|
|
240
|
+
refusal.
|
|
241
|
+
|
|
242
|
+
Capacity supersedes rather than accumulates: the latest value is the ceiling, and a decrease is as
|
|
243
|
+
ordinary as an increase. A provider whose ceiling can only rise -- one that keeps the highest count
|
|
244
|
+
it was ever told, or returns early on a small one -- cannot be told to take less work, and an agent application
|
|
245
|
+
taking capacity away is answered `applied` while the work keeps coming. The harness moves the axis
|
|
246
|
+
both ways after the drive, two then one then nought, and an offer after a lower count is caught
|
|
247
|
+
against it (`stream.taskOffered.overCapacity`).
|
|
248
|
+
|
|
249
|
+
**Capacity gates what the provider assigns, not what the agent starts.** A call placed from the
|
|
250
|
+
idle dialpad arrives through `task-offered` like any other task, and a full agent does not forbid
|
|
251
|
+
it: the ceiling binds assignment, not the agent's own hand.
|
|
252
|
+
|
|
253
|
+
### Runtime break prerequisite checks
|
|
254
|
+
|
|
255
|
+
Use `validateBreakCommand(method, request, state, context)` from the validation entry point
|
|
256
|
+
before any agent break dispatch. The context supplies current authentication and transport;
|
|
257
|
+
all four methods require the live login's break capability and active transport. Pass the
|
|
258
|
+
request object only to the request method, and undefined to the other three.
|
|
259
|
+
|
|
260
|
+
| Method | Required current state |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `requestBreak` | `not-requested`; selected current reason code when codes exist; `canRequestBreak` or the selected reason's `alwaysAvailable` exception. Free text does not replace a code. |
|
|
263
|
+
| `commitBreak` | `granted`, or already committed for an idempotent repeat. A later change to `canRequestBreak` does not revoke the grant. |
|
|
264
|
+
| `cancelBreak` | `awaiting-approval` or `granted`. A concurrent commit winning still answers `omni.break-already-committed` and requires recovery. |
|
|
265
|
+
| `endBreak` | `on-break` or `starting-after-task` during reconciliation; the agent may explicitly end a requested or forced break. |
|
|
266
|
+
|
|
267
|
+
Use `validateTeamCommand(request, context)` before every lead act: the team feature switch, break
|
|
268
|
+
decisions, forcing and ending a forced break, policy, and every act on a member's call. It requires
|
|
269
|
+
the live `lead` flag and active transport, a current team member list for any command naming a
|
|
270
|
+
member, and the target's complete break state for forcing/ending a break. Approve/deny requires an
|
|
271
|
+
awaiting decision; forcing a break uses the target's reason codes; ending a forced break requires a
|
|
272
|
+
forced committed break; join and decline need the member's request on the list; coach, join-call
|
|
273
|
+
and leave need the lead already on that member's call; an assignment named must be one the list
|
|
274
|
+
shows the member holding. The context's target state must belong to the named member; that
|
|
275
|
+
association and backend authorization are provider responsibilities.
|
|
276
|
+
|
|
277
|
+
Use `validateBreakStatus(state, tasks)` on the complete retained task view after each transaction,
|
|
278
|
+
as well as `validateBreakTransition` for event ordering. Snapshot validation shares the status
|
|
279
|
+
checks. Pending and completing tasks still count as work. Only a call a lead took over
|
|
280
|
+
(`takenOver`) sits beside a break in effect. Validate full task shapes and the envelope separately.
|
|
281
|
+
|
|
282
|
+
These validators report violations without dispatching, rewriting state or proving freshness.
|
|
283
|
+
Recheck prerequisites atomically at the provider; client validation cannot prevent a race.
|
|
284
|
+
Continue to use `validateResult` for each method's response vocabulary. A request acknowledgment
|
|
285
|
+
is not a grant, and no method response overwrites a newer state emission. Rejected promises
|
|
286
|
+
remain unknown and recover by snapshot, not automatic retry. The agent application's frozen provider set,
|
|
287
|
+
durable mutually exclusive commit/cancel decision and reconciliation obligations under
|
|
288
|
+
**Coordinating a multi-provider break** remain
|
|
289
|
+
required; per-provider validation does not implement the multi-provider coordinator.
|
|
290
|
+
|
|
291
|
+
### `requestBreak(request)`
|
|
292
|
+
|
|
293
|
+
**Explanatory text is optional.** An agent may omit `BreakRequest.reason` for any break type.
|
|
294
|
+
A lead may likewise omit reason text from `force-break`. Do not require a text explanation
|
|
295
|
+
just because a break is agent-requested or forced. If supplied, text must be nonempty; omit
|
|
296
|
+
it rather than sending an empty string. A selected `reasonId` identifies a published break
|
|
297
|
+
choice and is separate from this optional text.
|
|
298
|
+
|
|
299
|
+
With published choices, `{ reasonId: "lunch" }` is a valid request without explanation;
|
|
300
|
+
without published choices, `{}` is valid. A lead can similarly send a `force-break` command
|
|
301
|
+
with its member ID and, where required, the selected reason ID, without reason text.
|
|
302
|
+
|
|
303
|
+
Requests permission to stop the agent later; it does not itself stop work. The provider continues
|
|
304
|
+
offering work and reports `awaiting-approval` or `granted` through `break-state` events. If the request is denied, the
|
|
305
|
+
provider reports `not-requested` directly, with `decisionReason` when one was supplied.
|
|
306
|
+
|
|
307
|
+
**An agent asks from anywhere — idle or on a task** — and Omni offers the request in the task
|
|
308
|
+
workspace as it does on the idle dashboard. Asked on a task, the break is decided and committed like
|
|
309
|
+
any other and begins when the work ends: that is `starting-after-task`, and
|
|
310
|
+
`assertBreakBeginsAfterTask` is the scenario that holds a provider to it.
|
|
311
|
+
|
|
312
|
+
#### Break reasons
|
|
313
|
+
|
|
314
|
+
A provider that defines not-ready reason codes publishes them on `Snapshot.break.reasons`,
|
|
315
|
+
and Omni returns the agent's choice as `BreakRequest.reasonId`:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
break: {
|
|
319
|
+
reasons: [
|
|
320
|
+
{ id: "lunch", label: "Lunch" },
|
|
321
|
+
{ id: "training", label: "Training", group: "Scheduled" },
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Reason ids are non-empty and unique within the provider. They live on the snapshot rather than the
|
|
327
|
+
manifest because a provider may change the codes it offers during a shift; a `snapshot` event
|
|
328
|
+
replaces the list. `BreakRequest.reason` remains available for free text and is never a substitute
|
|
329
|
+
for `reasonId` when the provider publishes codes. A provider that defines no codes omits the
|
|
330
|
+
field.
|
|
331
|
+
|
|
332
|
+
#### One break across several providers
|
|
333
|
+
|
|
334
|
+
An agent connected to several providers takes **one** break, not one per platform. Omni gathers
|
|
335
|
+
what every connected provider offers, shows the distinct breaks, and on a choice sends one request
|
|
336
|
+
per provider: the same `reason`, and each provider's own `reasonId`.
|
|
337
|
+
|
|
338
|
+
To do that, Omni has to know when two providers mean the same break. Say so with `kind`:
|
|
339
|
+
|
|
340
|
+
```ts
|
|
341
|
+
break: {
|
|
342
|
+
reasons: [
|
|
343
|
+
{ id: "lunch", label: "Lunch", kind: "meal" },
|
|
344
|
+
{ id: "rest", label: "Mandatory rest period", kind: "rest", alwaysAvailable: true },
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
A provider decides which breaks it offers and what it calls them. `BREAK_KINDS` is the list Omni
|
|
350
|
+
supports mapping them onto, and every member means something specific:
|
|
351
|
+
|
|
352
|
+
| Kind | What it means |
|
|
353
|
+
| --- | --- |
|
|
354
|
+
| `short-break` | A brief rest between contacts — the comfort break a shift plan allows for. |
|
|
355
|
+
| `meal` | A meal: lunch, dinner, whatever the shift calls it. |
|
|
356
|
+
| `rest` | A rest period the agent is entitled to and a busy hour cannot cancel. Usually the reason also marked `alwaysAvailable`, though the two are separate: this says what the break *is*, that says whether policy can withdraw it. |
|
|
357
|
+
| `training` | Learning something the agent is expected to know afterwards: a course, e-learning, a product walkthrough. However it is delivered, and whoever attends. |
|
|
358
|
+
| `coaching` | Reviewing this agent's own work with somebody accountable for it: a call listened back, quality feedback, a one-to-one about their interactions. Even where the outcome is that they learn something. |
|
|
359
|
+
| `meeting` | A scheduled gathering that is neither — a team huddle, a project call, a town hall. The agent attends and contributes; nobody is assessing their work and there is nothing they must know by the end. |
|
|
360
|
+
| `administrative` | Paperwork and follow-up not attached to a particular contact. |
|
|
361
|
+
| `technical` | Equipment or system trouble stopping the agent taking work: a dead headset, a phone that never registered, a tool that will not load. The one member that is not an activity — it says why the agent *cannot* work rather than what they are doing, which makes it the right home for a not-ready state raised about the agent's equipment. |
|
|
362
|
+
| `personal` | Personal time the deployment does not classify further. |
|
|
363
|
+
| `other` | None of the above. Matches nothing, including another provider's `other`. |
|
|
364
|
+
|
|
365
|
+
**The definitions are the point, not decoration.** Ten undefined strings would be the label
|
|
366
|
+
problem one level up: two providers could both declare `technical`, one meaning a dead headset
|
|
367
|
+
and the other scheduled maintenance, match on it, and nothing could tell the difference.
|
|
368
|
+
|
|
369
|
+
`training`, `coaching` and `meeting` can all fit one session, so take them in that order of
|
|
370
|
+
specificity: about this agent's own work is `coaching`, else something they must know afterwards is
|
|
371
|
+
`training`, else `meeting`.
|
|
372
|
+
|
|
373
|
+
**A break somebody forced on the agent is not automatically one of these.** Where the agent was
|
|
374
|
+
stopped rather than choosing to stop — see **Forced breaks** — set `BreakState.forced` and prefer
|
|
375
|
+
omitting `kind` to reaching for `other`. None of the ten describes "something was done to this
|
|
376
|
+
agent", and `other` claims a classification that was never made.
|
|
377
|
+
|
|
378
|
+
Omni matches in this order:
|
|
379
|
+
|
|
380
|
+
1. **`kind`**, where both providers declare one. This wins over the label, so `{ id: "MEAL",
|
|
381
|
+
label: "Meal", kind: "meal" }` lines up with `{ id: "lunch", label: "Lunch", kind: "meal" }`
|
|
382
|
+
and nobody has to word their codes the same way.
|
|
383
|
+
2. **The label**, folded for case and surrounding spacing, where a kind is missing. `"other"`
|
|
384
|
+
counts as missing: two providers saying `"other"` have only said their break is not on the
|
|
385
|
+
list, which is no evidence they mean the same thing.
|
|
386
|
+
|
|
387
|
+
Nothing else is matched. A break Omni cannot pair stays on its own, the agent is told how many
|
|
388
|
+
platforms their break will actually reach, and they can pair the odd one by hand — Omni remembers
|
|
389
|
+
that for next time.
|
|
390
|
+
|
|
391
|
+
Three rules for a provider:
|
|
392
|
+
|
|
393
|
+
- **Declare `kind` where you can, and leave it out where you cannot.** Leaving it out costs a
|
|
394
|
+
little precision. Getting it wrong sends an agent to lunch on one platform and to training on
|
|
395
|
+
another, and nothing can detect that.
|
|
396
|
+
- **You may publish several reasons of one kind** — two meal slots, say. Omni will not choose
|
|
397
|
+
between them for the agent; saying they are the same kind is not saying they are interchangeable.
|
|
398
|
+
- **Do not read `reason` as your own label.** It is the agent's single choice, in whichever
|
|
399
|
+
provider's wording they picked it from, sent unchanged to every provider in the break.
|
|
400
|
+
|
|
401
|
+
A provider that publishes no reason codes still receives the request, with `reason` set and
|
|
402
|
+
`reasonId` omitted.
|
|
403
|
+
|
|
404
|
+
#### Coordinating a multi-provider break
|
|
405
|
+
|
|
406
|
+
Sending the requests is a two-phase operation, not independent best effort. A provider first grants
|
|
407
|
+
permission for the agent to stop, then Omni either commits or cancels that permission.
|
|
408
|
+
|
|
409
|
+
`granted` is provider-visible state. It means only that this provider has granted the active
|
|
410
|
+
request and is ready to stop the agent when Omni asks. It does not reveal that other providers exist
|
|
411
|
+
or why Omni has not committed yet. While reporting `granted`, the provider remains available,
|
|
412
|
+
continues to honour Omni's current capacity, and continues offering work normally.
|
|
413
|
+
|
|
414
|
+
Omni separately tracks the aggregate agent application states `working`, `requesting-break`, `committing-break`,
|
|
415
|
+
`cancelling-break`, and `on-break`. While `requesting-break`, Omni shows which providers remain
|
|
416
|
+
outstanding and offers **Cancel break request**, but does not tell the agent that their break has
|
|
417
|
+
begun.
|
|
418
|
+
|
|
419
|
+
Omni offers the aggregate Break control only when every provider currently holding capacity
|
|
420
|
+
declares `capabilities.breaks` at login. If one cannot be stopped, offering a global break would
|
|
421
|
+
knowingly permit partial availability.
|
|
422
|
+
|
|
423
|
+
Omni coordinates one break attempt as follows:
|
|
424
|
+
|
|
425
|
+
1. Freeze the providers the attempt asks: every connected provider from which the agent can
|
|
426
|
+
currently receive work. A provider joining during the attempt is given no capacity until it
|
|
427
|
+
finishes. A provider whose authentication is `expired` is not one the agent can receive work
|
|
428
|
+
from and is not asked: nothing is asked of it, the break proceeds without it, and when the
|
|
429
|
+
login is restored it is reconciled from its snapshot as a set-aside provider is, with no
|
|
430
|
+
capacity until then. `refreshing` keeps a provider in — its identity and capabilities remain
|
|
431
|
+
available and work continues. `assertBreakAttemptProviders` holds an agent application to this set.
|
|
432
|
+
2. Enter `requesting-break`. Keep the agent's normal capacity in place throughout this phase.
|
|
433
|
+
3. Send one `requestBreak` to every asked provider. A provider reports `awaiting-approval` or
|
|
434
|
+
`granted`; neither state stops work. A denial transitions directly to `not-requested` and
|
|
435
|
+
causes Omni to take the cancel path.
|
|
436
|
+
4. If every asked provider reports `granted`, durably choose commit, enter `committing-break`,
|
|
437
|
+
and send `commitBreak()` to each of them. A provider then stops offering new work
|
|
438
|
+
and reports `starting-after-task` or `on-break`. The **commit bound** — ten seconds from the
|
|
439
|
+
decision, tunable per deployment — decides who is kept: a provider that has not applied the
|
|
440
|
+
commit by then, still `granted` or unreachable, is set aside as unreconciled and the break
|
|
441
|
+
begins without it. `on-break` decides `on-break`: Omni enters it once every kept provider
|
|
442
|
+
reports `on-break`. A kept provider reporting `starting-after-task` has applied the commit
|
|
443
|
+
and is finishing a task; the bound is on delivery, not on that task. Omni shows the break as
|
|
444
|
+
settled and beginning when the task ends, and offers no cancel, because the commit is durable.
|
|
445
|
+
5. If any asked provider fails or denies the request, cannot be reconciled within the bounded
|
|
446
|
+
decision timeout, or the agent cancels before commit, durably choose cancel and enter
|
|
447
|
+
`cancelling-break`. Send `cancelBreak()` to every provider still reporting
|
|
448
|
+
`awaiting-approval` or `granted`. Work continues during cancellation because no stop was
|
|
449
|
+
committed. Return to `working` only after no provider retains either state.
|
|
450
|
+
|
|
451
|
+
Commit and cancel are mutually exclusive decisions for one break attempt. Once Omni chooses commit
|
|
452
|
+
it never rolls that attempt back: it is reconciled by snapshot until every asked provider is stopped.
|
|
453
|
+
A provider that reports `granted` must therefore preserve the request across reconnects and must
|
|
454
|
+
honour a later commit or cancel. This
|
|
455
|
+
durable promise prevents a provider from failing the commit after another provider has already
|
|
456
|
+
stopped the agent.
|
|
457
|
+
|
|
458
|
+
#### Why the commit phase is bounded and the decision is not
|
|
459
|
+
|
|
460
|
+
Waiting for unanimity forever is the one way this algorithm can strand an agent. The commit is
|
|
461
|
+
durable and cannot be rolled back, so a provider that crashes, has its authentication revoked,
|
|
462
|
+
or is uninstalled between granting and committing would hold Omni in `committing-break` with no
|
|
463
|
+
exit: the providers that did commit have already stopped the agent, and the agent is neither
|
|
464
|
+
working nor on a break.
|
|
465
|
+
|
|
466
|
+
Unanimity is required for a reason that survives the bound. It exists so the agent is not stopped
|
|
467
|
+
on one platform while another keeps routing work to them — and **a provider Omni cannot reach is
|
|
468
|
+
routing nothing.** Setting it aside therefore costs none of the property it was protecting. Waiting
|
|
469
|
+
for it costs the agent their break.
|
|
470
|
+
|
|
471
|
+
Setting a provider aside is not a rollback and not a cancel. The commit stands and the
|
|
472
|
+
obligation stands: until that provider is stopped it has not stopped. When it returns it emits a
|
|
473
|
+
snapshot before anything else, and the snapshot decides:
|
|
474
|
+
|
|
475
|
+
- `on-break` or `starting-after-task` — the commit arrived after all. Nothing to send.
|
|
476
|
+
- still `granted` — the commit was lost. Omni sends `commitBreak()` now. If the original turns up
|
|
477
|
+
late behind it, the provider stops an agent who is already stopped; nothing happens, because a
|
|
478
|
+
commit is a state to be in, not an act to be done.
|
|
479
|
+
- `not-requested` — the grant did not survive. Omni makes a new request for that provider alone,
|
|
480
|
+
against an agent who is already on break elsewhere. **A new login is this case too**: the grant
|
|
481
|
+
belonged to the old session.
|
|
482
|
+
|
|
483
|
+
The provider must not offer work in the meantime, and the commit is what stops it; Omni gives it
|
|
484
|
+
no capacity until it is reconciled.
|
|
485
|
+
|
|
486
|
+
**If the agent has already ended the break elsewhere, the break attempt is over** and the returning
|
|
487
|
+
provider is reconciled to that instead: still `granted` gets `cancelBreak()`, because committing
|
|
488
|
+
would stop an agent who is working again; `starting-after-task` or `on-break` gets `endBreak()`.
|
|
489
|
+
Never rolling back is about a break that is still on, not one the agent has finished.
|
|
490
|
+
|
|
491
|
+
Omni may tell the agent which platforms the break has not yet reached, as it already does when a
|
|
492
|
+
break cannot be paired across every provider.
|
|
493
|
+
|
|
494
|
+
The decision phase needs no such bound, because nothing has stopped. Work continues throughout
|
|
495
|
+
`requesting-break`, so a provider that never answers costs the agent a wait rather than their
|
|
496
|
+
availability, and the existing decision timeout resolves it by cancelling — which is safe precisely
|
|
497
|
+
because no stop was ever committed.
|
|
498
|
+
|
|
499
|
+
If cancel races with a late approval, the provider remains on the cancel path. If commit has already
|
|
500
|
+
won, cancel returns `omni.break-already-committed`, and Omni resumes commit recovery rather than
|
|
501
|
+
returning the agent to `working`.
|
|
502
|
+
|
|
503
|
+
A forced break is not rolled back by this algorithm. If one appears during either the request or
|
|
504
|
+
cancellation, the attempt is abandoned: Omni cancels every grant still outstanding and states
|
|
505
|
+
`setCapacity({ count: 0 })` on every other usable provider, as **Forced breaks** sets out; no grant
|
|
506
|
+
is committed, because the agent has already stopped everywhere.
|
|
507
|
+
|
|
508
|
+
This is two-phase coordination across vendor systems: the approval phase keeps the agent working;
|
|
509
|
+
the durable commit decision and snapshot reconciliation provide convergence after partial delivery.
|
|
510
|
+
|
|
511
|
+
#### Reporting the break the agent is on
|
|
512
|
+
|
|
513
|
+
`BreakState.activeReasonId` is the `BreakReason.id` the current break is on. Omni remembers what it
|
|
514
|
+
asked for, but only until the session ends — after a reload or reconnect, or where the provider put
|
|
515
|
+
the agent on the break itself, the provider is the only one who knows.
|
|
516
|
+
|
|
517
|
+
Omit it when you cannot say, and when there is no break: reporting a reason alongside
|
|
518
|
+
`status: "not-requested"` describes a break that is not happening, and is rejected.
|
|
519
|
+
|
|
520
|
+
### `cancelBreak()`
|
|
521
|
+
|
|
522
|
+
Cancels the active pre-commit request while its status is `awaiting-approval` or `granted`.
|
|
523
|
+
Cancellation releases the request but does not restore work because work never stopped. If
|
|
524
|
+
commit already won, the provider returns `omni.break-already-committed`. The resulting state is
|
|
525
|
+
reported through `break-state`.
|
|
526
|
+
|
|
527
|
+
### `commitBreak()`
|
|
528
|
+
|
|
529
|
+
Commits the `granted` request. Once the provider has reported `granted`, it cannot fail for a
|
|
530
|
+
business reason. On commit the provider stops offering new work and reports
|
|
531
|
+
`starting-after-task` while existing work finishes, or `on-break` when the break is in effect.
|
|
532
|
+
Committing a break that is already in effect changes nothing and answers `committed`.
|
|
533
|
+
|
|
534
|
+
### `endBreak()`
|
|
535
|
+
|
|
536
|
+
Tells a provider that an agent already on a break wants to become available again. The provider
|
|
537
|
+
reports the resulting state through `break-state` and provider status events.
|
|
538
|
+
|
|
539
|
+
It ends the break, which is the thing that started. Nothing about the connection was ever paused,
|
|
540
|
+
so there is nothing on it to resume.
|
package/guide/chat.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# `@xema/omni-protocol`: Chat
|
|
2
|
+
|
|
3
|
+
What is true of a chat and of nothing else. Part of the contract in `guide.md`, which holds the terms, the shapes and the rules every file here relies on; a reference in bold names the file it points into where that is not this one.
|
|
4
|
+
|
|
5
|
+
## Chat capabilities
|
|
6
|
+
|
|
7
|
+
| Capability | Omni UI | Contract |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending chat offer. Omni shows it only when local policy also permits declining. |
|
|
10
|
+
| `hold` | Primary toggle: Hold | Omni may pause and resume the agent’s interaction in the chat. |
|
|
11
|
+
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the conversation or from its wrap. See **Scheduling a follow-up** in `guide/agent.md`. |
|
|
12
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|
package/guide/email.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# `@xema/omni-protocol`: Email
|
|
2
|
+
|
|
3
|
+
What is true of an email and of nothing else. Part of the contract in `guide.md`, which holds the terms, the shapes and the rules every file here relies on; a reference in bold names the file it points into where that is not this one.
|
|
4
|
+
|
|
5
|
+
## Email capabilities
|
|
6
|
+
|
|
7
|
+
| Capability | Omni UI | Contract |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `decline` | Pending-task button: Decline | The provider can decline a pending email offer. Omni shows it only when local policy also permits declining. |
|
|
10
|
+
| `schedule` | Secondary menu item: Schedule | Omni may put a follow-up for this party on the calendar, from the message or from its wrap. See **Scheduling a follow-up** in `guide/agent.md`. |
|
|
11
|
+
| `outcomes` | Primary button: Complete | Omni may request task completion with a provider outcome and notes. |
|