Aller au contenu

Module XVI — agents vocaux et temps réel

Le module XVI gouverne les agents conversationnels et temps réel. C’est un plan d’observation et de gouvernance : il ne réimplémente pas un SDK vocal (Realtime API, WebRTC, ASR ou TTS) et il n’ouvre jamais lui-même un flux média. Il décide qui peut ouvrir une session vocale, avec quel modèle et fournisseur, sous quelle politique, et suit les métadonnées de cette session — jamais son contenu.

Ouvrir une interface vocale est traité comme une action privilégiée, pas comme une opération libre. La politique est deny-par-défaut : une session sans politique l’autorisant est refusée. Une ouverture est en deux phases et gated par human-in-the-loop via la porte d’approbation ; elle est liée à un plan_hash afin qu’une approbation ne puisse pas être silencieusement promue vers un modèle plus puissant (anti-TOCTOU), auditée au principal réel (jamais system), et attestée en append-only. Le module lui-même n’appelle jamais un fournisseur — l’actuation sort par un seam de dispatch distinct.

L’autre moitié est l’observation : le module ne suit que les métadonnées de session — état dérivé (live/idle/ended, calculé au moment de la lecture à partir de la récence d’activité, sans colonne de cycle de vie stockée), nombre de tours, durée, latence (moyenne et maximum honnêtes issus d’échantillons réels), et langue BCP-47. À partir de là, il lève des findings de gouvernance : une violation de politique lorsque la télémétrie nomme un agent/modèle/fournisseur qu’aucune politique n’autorise, un finding de latence dégradée lorsque la latence dépasse un SLA de politique, et un finding d’ouverture non gouvernée lorsqu’une ouverture est tentée sans porte câblée — l’écart est exposé et l’ouverture est tout de même refusée.

Le module déclare trois entités dans le modèle de données partagé :

Entité Mutabilité Objet
session mutable (upsert) métadonnées de session ; zéro contenu
policy mutable déclaration de gouvernance — qui peut ouvrir avec quel modèle/fournisseur (deny-par-défaut)
decision append-only registre immuable des décisions d’ouverture/fermeture

Une politique correspond sur l’agent, le modèle autorisé et le fournisseur autorisé (chacun spécifique ou wildcard), avec des bornes optionnelles de minutes de session et de SLA de latence. Aucune politique correspondante signifie DENY. Le registre de décisions enregistre chaque open_request, open et close avec son verdict de politique, son statut de porte et son statut de résultat. L’accès en lecture est le rôle viewer et au-dessus ; déclarer une politique et ouvrir une session sont des actions administratives, portées par le tenant et auditées. Ces routes de module sont publiées dans la référence des routes de module bêta distincte, et non dans le contrat stable du cœur — leurs formes au niveau des champs vivent dans les interfaces typées du produit. Les montants en dollars ne sont pas ici ; FinOps (module XI) assume le coût.

Le module possède un seam d’ingestion deny-closed — son propre événement voice.telemetry.observed — par lequel une sonde in-process alimenterait les métadonnées de session. Le fil est à données minimales par construction : le parser de télémétrie porte une allow-list et rejette l’événement entier s’il voit une clé interdite, de sorte qu’aucun audio, texte de transcription, texte ASR/TTS, contenu de prompt/réponse ou PII de locuteur ne peut jamais être persisté. Le seul signal de transcription conservé est un hash unidirectionnel d’un localisateur de transcription externe — la preuve qu’une transcription existe, jamais la transcription. Les findings de gouvernance sont émis comme finding.reported avec un détail hashé, après commit.

Une ouverture gouvernée dispatche en direct : une fois le dispatcher voice provisionné par l’opérateur, une ouverture approuvée crée un credential éphémère côté serveur et le renvoie avec les coordonnées de connexion. La configuration de session opérateur fournit voix et détection de tours ; un modèle configuré remplace celui demandé. Sans modèle configuré, le dispatcher utilise le modèle demandé autorisé par la politique du tenant. La clé maître du fournisseur ne quitte jamais le serveur. Sans provisionnement, le dispatch refuse en cas d’échec : l’ouverture approuvée est honnêtement enregistrée « déclarée, non ouverte ».

Activez le module existant avec olivares modules on voice. Le moteur enregistre la sélection et redémarre une fois si l’ensemble des modules actifs change ; olivares modules ls indique s’il tourne. Voice exige FinOps et governance selon la spécification des modules. Le désactiver conserve politiques, métadonnées de session et ledger des décisions.

Le dispatcher est provisionné sur l’hôte du moteur via OLIVARES_VOICE_DISPATCH_CONFIG, chemin absolu d’un fichier JSON détenu par l’opérateur. Seul le compte du moteur doit pouvoir le lire. Les clés maîtres des fournisseurs appartiennent à ce fichier, jamais aux arguments CLI, lignes de politique ou bundles de connexion client. Pour un adaptateur OpenAI :

{
"providers": [
{"ref": "openai", "kind": "openai", "api_key": "<server-held provider key>"}
],
"policies": [
{
"agent_ref": "contact-agent",
"provider_ref": "openai",
"model": "<your permitted realtime model>",
"voice": "marin",
"max_duration_seconds": 60
}
]
}

Définissez la variable d’environnement pour le service du moteur et redémarrez-le. Un fichier fourni illisible ou invalide empêche le démarrage. Sans configuration de dispatcher, le comportement reste « déclaré, non ouvert ». Le fichier opérateur choisit adaptateurs et paramètres de session ; la politique voice du tenant autorise séparément l’agent, le modèle et le fournisseur demandés. Utilisez les mêmes références de modèle et de fournisseur dans les deux.

Après olivares login, déclarez cette politique et demandez une approbation :

Fenêtre de terminal
olivares voice policies set --agent-ref contact-agent \
--allowed-model-ref '<your permitted realtime model>' --allowed-provider-ref openai \
--max-session-minutes 1 --max-latency-ms 300
olivares voice sessions open --session-ref contact-1 --agent-ref contact-agent \
--model-ref '<your permitted realtime model>' --provider-ref openai -o json

La première demande renvoie op_status: requested, une approval_ref et le code de sortie CLI 7. Elle n’ouvre aucune connexion média et ne crée aucun credential fournisseur. Les approbateurs indépendants requis approuvent cette référence via la page d’approbation governance ou olivares governance approvals approve <approval-ref>. Pour les nouvelles demandes via le pont local d’approbation par défaut, le demandeur ne peut pas approuver sa propre demande, même avec un autre credential du même compte. Répétez la même ouverture avec --approval-ref <approval-ref>. Un refus de politique ou une approbation en attente renvoie 403 et le code CLI 3 ; un échec d’adaptateur renvoie 502. Les contrôles de budget et d’arrêt de l’estate restent applicables.

Une demande configurée réussie renvoie op_status: dispatched. Son dispatch_ref est une chaîne JSON contenant le credential temporaire, les coordonnées connect, le transport, le modèle et l’expiration. Traitez cette réponse comme un credential : ne la copiez pas dans des rapports ou logs. Pour OpenAI, le client échange une offre SDP à l’URL connect renvoyée avec le credential temporaire, puis possède la connexion média WebRTC. La création du credential ne prouve pas une connexion média établie. Le ledger conserve une empreinte SHA-256 du bundle contenant un credential, pas le credential de connexion. Les anciens bundles stockés sont également hachés à la lecture ; les lignes append-only existantes ne sont pas réécrites. Les handles fournisseur simples conservent leur valeur.

Inspectez les métadonnées et décisions conservées :

Fenêtre de terminal
olivares voice sessions get contact-1 -o json
olivares voice sessions decisions contact-1 -o json
olivares voice policies ls -o json

Utilisez le même répertoire de données entre redémarrages. La politique et les décisions append-only restent disponibles après redémarrage et après désactivation puis réactivation de voice ; les credentials fournisseur restent à provisionner séparément. Les commandes JSON utilisent les mêmes routes /v1/m/voice, délimitées par tenant, que l’API.