@xema/omni-protocol 0.1.126 → 0.1.127
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 +9 -0
- package/dist/testing.js +3 -1
- package/dist/validation.d.ts +2 -0
- package/dist/validation.js +19 -0
- package/guide.md +16 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -281,6 +281,13 @@ export interface AuthenticationContext {
|
|
|
281
281
|
* later as `ConnectContext.timeZone`.
|
|
282
282
|
*/
|
|
283
283
|
timeZone: TimeZone;
|
|
284
|
+
/**
|
|
285
|
+
* The languages the agent reads, as BCP 47 tags, most preferred first, from the host's own
|
|
286
|
+
* setting. Never empty: the host states at least one. The provider words what the agent reads
|
|
287
|
+
* -- failure and transport messages -- by it, looked up as `localizedText()` does. The same
|
|
288
|
+
* value arrives later as `ConnectContext.languages`.
|
|
289
|
+
*/
|
|
290
|
+
languages: readonly LanguageTag[];
|
|
284
291
|
/**
|
|
285
292
|
* How this login hears its calls, chosen by the host from the manifest's `phones`. Required for
|
|
286
293
|
* a voice provider and absent for any other. The same value arrives later as `ConnectContext.phone`.
|
|
@@ -680,6 +687,8 @@ export type ConnectContext = {
|
|
|
680
687
|
* identity; a roaming agent corrects it by signing in.
|
|
681
688
|
*/
|
|
682
689
|
timeZone: TimeZone;
|
|
690
|
+
/** The same value passed as `AuthenticationContext.languages`: what the agent reads, most preferred first. */
|
|
691
|
+
languages: readonly LanguageTag[];
|
|
683
692
|
/** Where the adapter keeps this login's operational state across a reload of the host. */
|
|
684
693
|
store: LoginStore;
|
|
685
694
|
/** The same value passed as `AuthenticationContext.phone`: how this login hears its calls. Narrowed by the arms below. */
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
|
|
2
|
-
import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleTested, validateHostMute, validateHostRecording, validateLoginStore, validateHistoryReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
2
|
+
import { assertNoViolations, validateAuthenticationState, validateBreakTransition, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateUserDetails, observeRules, ruleTested, validateHostMute, validateHostRecording, validateLoginStore, validateHistoryReport, validateManifest, validateResult, validateSnapshot, validateTask, validateLanguages, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
@@ -345,6 +345,8 @@ export async function testAdapter(adapter, context, options = {}) {
|
|
|
345
345
|
violations.push(...validateHostGuarantees(context.host.guarantees, "context.host.guarantees", adapter.manifest.channel === "voice" ? context.phone : undefined));
|
|
346
346
|
// The zone is the host's to state, so a context without one is a host that cannot exist.
|
|
347
347
|
violations.push(...validateTimeZone(context.timeZone, "context.timeZone"));
|
|
348
|
+
// So are the languages: what the agent reads is stated, never assumed.
|
|
349
|
+
violations.push(...validateLanguages(context.languages, "context.languages"));
|
|
348
350
|
// How the agent hears the call decides what the host owns: on a softphone the host has the
|
|
349
351
|
// audio and reports it; on a hardphone, or off voice, there is none for it to report.
|
|
350
352
|
violations.push(...validatePhone(context.phone, adapter.manifest, "context.phone"));
|
package/dist/validation.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare function validateProviderTimeCheckRequest(value: unknown, path?:
|
|
|
30
30
|
/** Checks response shape/correlation, not source-clock accuracy, round-trip delay or freshness. */
|
|
31
31
|
export declare function validateProviderTimeCheckResult(value: unknown, request: unknown, loginId: string, path?: string): ProtocolViolation[];
|
|
32
32
|
export declare function validateManifest(manifest: unknown, path?: string): ProtocolViolation[];
|
|
33
|
+
/** The agent's languages, as a host states them on both contexts: BCP 47 tags, most preferred first, at least one. */
|
|
34
|
+
export declare function validateLanguages(value: unknown, path?: string): ProtocolViolation[];
|
|
33
35
|
/** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
|
|
34
36
|
export declare function validateLabels(value: unknown, path?: string): ProtocolViolation[];
|
|
35
37
|
export interface TaskValidationContext {
|
package/dist/validation.js
CHANGED
|
@@ -563,6 +563,25 @@ export function validateManifest(manifest, path = "manifest") {
|
|
|
563
563
|
}
|
|
564
564
|
/** Language tags as BCP 47 writes them: a language, then subtags of up to eight letters or digits. */
|
|
565
565
|
const LANGUAGE_TAG = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{1,8})*$/;
|
|
566
|
+
/** The agent's languages, as a host states them on both contexts: BCP 47 tags, most preferred first, at least one. */
|
|
567
|
+
export function validateLanguages(value, path = "context.languages") {
|
|
568
|
+
const into = new Collector();
|
|
569
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
570
|
+
into.add("context.languages", path, "the host states the languages the agent reads: at least one BCP 47 tag, most preferred first");
|
|
571
|
+
return into.violations;
|
|
572
|
+
}
|
|
573
|
+
const seen = new Set();
|
|
574
|
+
value.forEach((tag, index) => {
|
|
575
|
+
if (typeof tag !== "string" || !LANGUAGE_TAG.test(tag)) {
|
|
576
|
+
into.add("context.languages.tag", `${path}[${index}]`, `${describeValue(tag)} is not a BCP 47 language tag`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const key = tag.toLowerCase();
|
|
580
|
+
into.require(!seen.has(key), "context.languages.unique", `${path}[${index}]`, `${tag} is listed twice`);
|
|
581
|
+
seen.add(key);
|
|
582
|
+
});
|
|
583
|
+
return into.violations;
|
|
584
|
+
}
|
|
566
585
|
/** The customer's own words, as `Snapshot.labels` and `labels-updated` carry them. */
|
|
567
586
|
export function validateLabels(value, path = "labels") {
|
|
568
587
|
const into = new Collector();
|
package/guide.md
CHANGED
|
@@ -289,6 +289,7 @@ type AuthenticationContext = {
|
|
|
289
289
|
protocolVersion: number;
|
|
290
290
|
loginId: string;
|
|
291
291
|
timeZone: TimeZone;
|
|
292
|
+
languages: readonly LanguageTag[];
|
|
292
293
|
phone?: Phone;
|
|
293
294
|
redirectUri?: string;
|
|
294
295
|
secrets: SecretStore;
|
|
@@ -423,6 +424,7 @@ type ConnectContext = {
|
|
|
423
424
|
loginId: string;
|
|
424
425
|
autoAcceptTasks: boolean;
|
|
425
426
|
timeZone: TimeZone;
|
|
427
|
+
languages: readonly LanguageTag[];
|
|
426
428
|
store: LoginStore;
|
|
427
429
|
phone?: Phone;
|
|
428
430
|
host: Host;
|
|
@@ -2051,6 +2053,7 @@ Creates the provider-scoped authentication session.
|
|
|
2051
2053
|
| `protocolVersion` | The negotiated version, fixed for this login. |
|
|
2052
2054
|
| `loginId` | Omni-generated identity for this login. The same value Omni later passes as `ConnectContext.loginId`, and how an adapter ties a connection back to the session that authenticated it. |
|
|
2053
2055
|
| `timeZone` | The zone the agent's day is reckoned in, as an IANA name from the agent application's clock, stated before any identity exists so the first `authenticated` state already carries it. The same value Omni later passes as `ConnectContext.timeZone`. See **The agent's day**. |
|
|
2056
|
+
| `languages` | The languages the agent reads, as BCP 47 tags, most preferred first, from the agent application's own setting: `["hi-IN", "en"]`. Never empty (`context.languages`), each a well-formed tag (`context.languages.tag`), none twice (`context.languages.unique`). The same value Omni later passes as `ConnectContext.languages`. See **The agent's languages**. |
|
|
2054
2057
|
| `phone` | How this login hears its calls, chosen by the agent application from the manifest's `phones`: required for a voice provider, absent for any other. The same value Omni later passes as `ConnectContext.phone`. See **How the agent hears the call** in `guide/phone.md`. |
|
|
2055
2058
|
| `secrets` | Omni-provided `SecretStore`, scoped to this provider's manifest id. |
|
|
2056
2059
|
| `signal` | Optional cancellation signal. |
|
|
@@ -2323,6 +2326,7 @@ Creates one live provider connection for the signed-in agent.
|
|
|
2323
2326
|
| `loginId` | Omni-generated identity for this login. It is the same value passed as `AuthenticationContext.loginId`, so an adapter can correlate this connection with the session that authenticated it. Stable across transport reconnects and changed only by a new login. |
|
|
2324
2327
|
| `autoAcceptTasks` | Agent local policy policy relayed to the provider at login, stated by the agent application on every connection and never assumed from its absence. When `true`, a pending task states its `acceptance`; when `false`, every task requires agent acceptance. Fixed for this connection, like everything else here: the provider states or omits `acceptance` by the value it was sent, and Omni validates by that same value, not by a policy that has since moved — a change reaches the provider through a fresh `connect()`. |
|
|
2325
2328
|
| `timeZone` | The same value passed as `AuthenticationContext.timeZone`. The provider stores it on the agent and carries it on the identity. See **The agent's day**. |
|
|
2329
|
+
| `languages` | The same value passed as `AuthenticationContext.languages`. See **The agent's languages**. |
|
|
2326
2330
|
| `phone` | The same value passed as `AuthenticationContext.phone`: how this login hears its calls. The type ties `host.mute` to it: a `softphone` login's agent application states what its Mute does, and a hardphone or conversation login's agent application cannot, so the omission is a compile error rather than a live seat's discovery. See **The station is the agent application's** in `guide/phone.md`. |
|
|
2327
2331
|
| `store` | The login's operational store, kept by the agent application for the life of the login, across a reload of the agent application, and cleared at sign-out: where an adapter that composes a record keeps what its platform cannot hold for it, such as the interaction legs an agent application reported. Three functions, by key. Never for anything sensitive, which is `AuthenticationContext.secrets`, a store an agent application may clear aggressively. **A task's keys carry its assignment id and go with the task**: a key written about a task names the assignment id in the key, and is deleted before the task's end is published, so that nothing of a closed task survives its ending and nothing is left for whatever comes next -- a record with legs the agent application never reported against it. A login-scoped key carries no assignment id and outlives any task. The harness requires the store of every connection (`store.shape`, `store.get`, `.set`, `.delete`), watches the one it hands over, and names a task's key still held after `task-ended` (`test.store.retained`) or written about the task after its end -- a persist hung off a timer that saw the task as it was (`test.store.late`). The ordinary late write is the agent application's, not the adapter's: an agent application reports a leg without waiting for the answer, so an unmute can follow `complete` by a tick, and the adapter answers a report about a task that has ended `failed` with `omni.assignment-not-found` and writes nothing -- the agent application ends its own open legs at the task's end, so such a report is the agent application's error to see, and an agent application reads every `recordStep` answer and awaits the one for the leg it closes at a task's end, the only moment a refusal is expected, since a report nobody waits for is an error nobody can see; an adapter that names no task in its keys gets no cleanup check, which is a gap rather than a pass, never an exemption: the obligation is that nothing of a closed task survives its ending, and an adapter that keeps every open task in one login-scoped value owes exactly that inside the value, where the harness cannot look. One key per task, named for it, is the shape the harness can hold, and the shape to reach for. The store lists nothing, so an adapter that needs to find its tasks keeps a login-scoped index of ids beside them; at the task's end it deletes the body first and reindexes after, since a crash between the two then leaves an index naming a task with no body, which a reader skips, where the other order leaves a body for a task that has ended, which is the hazard itself. A reader of the index tolerates an id with no body as an ending that was underway, not as corruption. |
|
|
2328
2332
|
| `host` | The agent application's report of the agent's station — devices, permissions, network — to consult before declaring the agent ready to the platform, and on every change. See **The agent application reports, the adapter decides** in `guide/phone.md`. |
|
|
@@ -2360,6 +2364,17 @@ and a lead reading a colleague's day would still get the wrong one. What proves
|
|
|
2360
2364
|
the run never sent: a colleague's `User` from `getUserDetails()` carrying theirs. A provider's own
|
|
2361
2365
|
tests are where that is shown, with a second agent whose zone arrived through another session.
|
|
2362
2366
|
|
|
2367
|
+
### The agent's languages
|
|
2368
|
+
|
|
2369
|
+
Everything the provider writes for the agent to read -- a failure's `message`, a transport
|
|
2370
|
+
`message`, a refusal's reason -- is written for one person, and the agent application says which
|
|
2371
|
+
languages that person reads: `languages`, BCP 47 tags in order of preference, on both contexts and
|
|
2372
|
+
the same value in each. It is stated, never assumed, as the zone is: never empty, because an empty
|
|
2373
|
+
list would be a default nobody chose, and the provider's own default is what the agent application
|
|
2374
|
+
falls back to only when no tag it was given matches. The provider picks a language the way
|
|
2375
|
+
`localizedText()` does -- each tag whole, then without its last subtag, in the agent's order -- and
|
|
2376
|
+
words its messages in the first it has. The harness holds the host to the shape (`validateLanguages`).
|
|
2377
|
+
|
|
2363
2378
|
### Who the agent is
|
|
2364
2379
|
|
|
2365
2380
|
`ConnectContext` names no agent. The adapter already knows who is connected: it authenticated them,
|
|
@@ -3082,6 +3097,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3082
3097
|
| `validateLabels(value, path?)` | The customer's words on their own, as `labels-updated` and `Snapshot.labels` carry them: renames of the protocol's phases and steps only, and every word one text per well-formed language tag (`labels.*`). |
|
|
3083
3098
|
| `validatePhoneState(value, path?, context?)` | The phone as the platform sees it, on its own as `phone-updated` and `Snapshot.phone` carry it: its status, its one active channel, its mute where a hardphone's is observed (`phone.*`). |
|
|
3084
3099
|
| `validateHostMute(mute, softphone)` | What the agent application's Mute does, stated on a softphone login and nowhere else: `stream` or `station` (`host.mute`), required where the agent application holds a microphone (`host.mute.required`) and refused where it does not (`host.mute.unexpected`). The harness holds `ConnectContext.host.mute` to it. |
|
|
3100
|
+
| `validateLanguages(languages)` | The agent's languages a host states on both contexts: a non-empty list (`context.languages`) of well-formed BCP 47 tags (`context.languages.tag`), none twice ignoring case (`context.languages.unique`). The harness holds `ConnectContext.languages` to it. |
|
|
3085
3101
|
| `validateLoginStore(store)` | The login's store the agent application hands every connection: an object with `get`, `set` and `delete` (`store.shape`, `store.get`, `.set`, `.delete`). The harness holds `ConnectContext.store` to it. |
|
|
3086
3102
|
| `validateCapacity(capacity)` | What the agent application states as capacity: a whole number of zero or more (`capacity.count`), zero being agent application-stopped. The harness states one on connect and two, one and zero after the test, each answered `applied`, and holds any offer to the count in force (`stream.taskOffered.overCapacity`). |
|
|
3087
3103
|
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|