{
  "asyncapi": "3.1.0",
  "info": {
    "title": "Dialt Realtime API",
    "version": "1.0.0",
    "description": "Realtime voice or text conversations over one WebSocket. Send a JSON start frame first, then either binary microphone audio or input_text controls. Authenticate with api_key in the start frame or an Authorization: Bearer header on the WebSocket handshake."
  },
  "servers": {
    "production": {
      "host": "dialt.com",
      "protocol": "wss",
      "description": "Production realtime endpoint. A bearer handshake header may carry the API key instead of the start frame.",
      "security": [
        {
          "$ref": "#/components/securitySchemes/bearerAuth"
        }
      ]
    }
  },
  "channels": {
    "conversation": {
      "address": "/ws",
      "messages": {
        "start": {
          "$ref": "#/components/messages/start"
        },
        "reset": {
          "$ref": "#/components/messages/reset"
        },
        "inputText": {
          "$ref": "#/components/messages/inputText"
        },
        "setVoice": {
          "$ref": "#/components/messages/setVoice"
        },
        "toolResult": {
          "$ref": "#/components/messages/toolResult"
        },
        "toolDeferred": {
          "$ref": "#/components/messages/toolDeferred"
        },
        "toolProgress": {
          "$ref": "#/components/messages/toolProgress"
        },
        "toolPartialResult": {
          "$ref": "#/components/messages/toolPartialResult"
        },
        "toolInteractionUpdate": {
          "$ref": "#/components/messages/toolInteractionUpdate"
        },
        "toolCancelClient": {
          "$ref": "#/components/messages/toolCancelClient"
        },
        "playbackStopped": {
          "$ref": "#/components/messages/playbackStopped"
        },
        "playbackReport": {
          "$ref": "#/components/messages/playbackReport"
        },
        "webrtcOffer": {
          "$ref": "#/components/messages/webrtcOffer"
        },
        "microphoneAudio": {
          "$ref": "#/components/messages/microphoneAudio"
        },
        "ready": {
          "$ref": "#/components/messages/ready"
        },
        "silentMic": {
          "$ref": "#/components/messages/silentMic"
        },
        "turn": {
          "$ref": "#/components/messages/turn"
        },
        "asr": {
          "$ref": "#/components/messages/asr"
        },
        "injectContextAck": {
          "$ref": "#/components/messages/injectContextAck"
        },
        "utterance": {
          "$ref": "#/components/messages/utterance"
        },
        "done": {
          "$ref": "#/components/messages/done"
        },
        "sessionEndRequested": {
          "$ref": "#/components/messages/sessionEndRequested"
        },
        "interrupted": {
          "$ref": "#/components/messages/interrupted"
        },
        "canceled": {
          "$ref": "#/components/messages/canceled"
        },
        "toolCall": {
          "$ref": "#/components/messages/toolCall"
        },
        "toolDeferredAck": {
          "$ref": "#/components/messages/toolDeferredAck"
        },
        "toolDeferredResume": {
          "$ref": "#/components/messages/toolDeferredResume"
        },
        "toolCancelServer": {
          "$ref": "#/components/messages/toolCancelServer"
        },
        "toolJobDeliveryFailed": {
          "$ref": "#/components/messages/toolJobDeliveryFailed"
        },
        "toolJobNarration": {
          "$ref": "#/components/messages/toolJobNarration"
        },
        "working": {
          "$ref": "#/components/messages/working"
        },
        "toolInteractionUpdateAck": {
          "$ref": "#/components/messages/toolInteractionUpdateAck"
        },
        "voiceChanged": {
          "$ref": "#/components/messages/voiceChanged"
        },
        "error": {
          "$ref": "#/components/messages/error"
        },
        "assistantAudio": {
          "$ref": "#/components/messages/assistantAudio"
        },
        "webrtcIce": {
          "$ref": "#/components/messages/webrtcIce"
        },
        "webrtcAnswer": {
          "$ref": "#/components/messages/webrtcAnswer"
        },
        "webrtcBye": {
          "$ref": "#/components/messages/webrtcBye"
        },
        "ambience": {
          "$ref": "#/components/messages/ambience"
        },
        "interrupt": {
          "$ref": "#/components/messages/interrupt"
        },
        "setTools": {
          "$ref": "#/components/messages/setTools"
        },
        "setToolChoice": {
          "$ref": "#/components/messages/setToolChoice"
        },
        "triggerReply": {
          "$ref": "#/components/messages/triggerReply"
        },
        "injectContext": {
          "$ref": "#/components/messages/injectContext"
        },
        "textDelta": {
          "$ref": "#/components/messages/textDelta"
        },
        "permissionPending": {
          "$ref": "#/components/messages/permissionPending"
        },
        "permissionApproved": {
          "$ref": "#/components/messages/permissionApproved"
        },
        "permissionRefused": {
          "$ref": "#/components/messages/permissionRefused"
        },
        "sessionClosingSoon": {
          "$ref": "#/components/messages/sessionClosingSoon"
        },
        "lowCreditWarning": {
          "$ref": "#/components/messages/lowCreditWarning"
        }
      },
      "bindings": {
        "ws": {
          "method": "GET"
        }
      }
    }
  },
  "operations": {
    "sendClientMessages": {
      "action": "send",
      "channel": {
        "$ref": "#/channels/conversation"
      },
      "summary": "Send the start frame, microphone audio, and client controls",
      "messages": [
        {
          "$ref": "#/channels/conversation/messages/start"
        },
        {
          "$ref": "#/channels/conversation/messages/reset"
        },
        {
          "$ref": "#/channels/conversation/messages/inputText"
        },
        {
          "$ref": "#/channels/conversation/messages/setVoice"
        },
        {
          "$ref": "#/channels/conversation/messages/toolDeferred"
        },
        {
          "$ref": "#/channels/conversation/messages/toolResult"
        },
        {
          "$ref": "#/channels/conversation/messages/toolProgress"
        },
        {
          "$ref": "#/channels/conversation/messages/toolPartialResult"
        },
        {
          "$ref": "#/channels/conversation/messages/toolInteractionUpdate"
        },
        {
          "$ref": "#/channels/conversation/messages/toolCancelClient"
        },
        {
          "$ref": "#/channels/conversation/messages/ambience"
        },
        {
          "$ref": "#/channels/conversation/messages/playbackStopped"
        },
        {
          "$ref": "#/channels/conversation/messages/playbackReport"
        },
        {
          "$ref": "#/channels/conversation/messages/microphoneAudio"
        },
        {
          "$ref": "#/channels/conversation/messages/webrtcOffer"
        },
        {
          "$ref": "#/channels/conversation/messages/interrupt"
        },
        {
          "$ref": "#/channels/conversation/messages/setTools"
        },
        {
          "$ref": "#/channels/conversation/messages/setToolChoice"
        },
        {
          "$ref": "#/channels/conversation/messages/triggerReply"
        },
        {
          "$ref": "#/channels/conversation/messages/injectContext"
        }
      ]
    },
    "receiveServerMessages": {
      "action": "receive",
      "channel": {
        "$ref": "#/channels/conversation"
      },
      "summary": "Receive lifecycle events, assistant audio, transcripts, and tool calls",
      "messages": [
        {
          "$ref": "#/channels/conversation/messages/ready"
        },
        {
          "$ref": "#/channels/conversation/messages/silentMic"
        },
        {
          "$ref": "#/channels/conversation/messages/turn"
        },
        {
          "$ref": "#/channels/conversation/messages/asr"
        },
        {
          "$ref": "#/channels/conversation/messages/utterance"
        },
        {
          "$ref": "#/channels/conversation/messages/injectContextAck"
        },
        {
          "$ref": "#/channels/conversation/messages/done"
        },
        {
          "$ref": "#/channels/conversation/messages/sessionEndRequested"
        },
        {
          "$ref": "#/channels/conversation/messages/interrupted"
        },
        {
          "$ref": "#/channels/conversation/messages/canceled"
        },
        {
          "$ref": "#/channels/conversation/messages/toolDeferredAck"
        },
        {
          "$ref": "#/channels/conversation/messages/toolDeferredResume"
        },
        {
          "$ref": "#/channels/conversation/messages/toolCall"
        },
        {
          "$ref": "#/channels/conversation/messages/toolCancelServer"
        },
        {
          "$ref": "#/channels/conversation/messages/toolJobDeliveryFailed"
        },
        {
          "$ref": "#/channels/conversation/messages/toolJobNarration"
        },
        {
          "$ref": "#/channels/conversation/messages/working"
        },
        {
          "$ref": "#/channels/conversation/messages/toolInteractionUpdateAck"
        },
        {
          "$ref": "#/channels/conversation/messages/voiceChanged"
        },
        {
          "$ref": "#/channels/conversation/messages/error"
        },
        {
          "$ref": "#/channels/conversation/messages/assistantAudio"
        },
        {
          "$ref": "#/channels/conversation/messages/webrtcIce"
        },
        {
          "$ref": "#/channels/conversation/messages/webrtcAnswer"
        },
        {
          "$ref": "#/channels/conversation/messages/webrtcBye"
        },
        {
          "$ref": "#/channels/conversation/messages/textDelta"
        },
        {
          "$ref": "#/channels/conversation/messages/permissionPending"
        },
        {
          "$ref": "#/channels/conversation/messages/permissionApproved"
        },
        {
          "$ref": "#/channels/conversation/messages/permissionRefused"
        },
        {
          "$ref": "#/channels/conversation/messages/sessionClosingSoon"
        },
        {
          "$ref": "#/channels/conversation/messages/lowCreditWarning"
        }
      ]
    }
  },
  "components": {
    "messages": {
      "start": {
        "name": "start",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "session_id",
            "mode"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "start"
            },
            "session_id": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$",
              "minLength": 1,
              "maxLength": 64
            },
            "api_key": {
              "type": "string",
              "description": "Persistent or scoped key. Optional when supplied in the WebSocket Authorization header."
            },
            "audio": {
              "$ref": "#/components/schemas/AudioConfig"
            },
            "client": {
              "$ref": "#/components/schemas/ClientInfo"
            },
            "mode": {
              "$ref": "#/components/schemas/Mode"
            },
            "transport": {
              "$ref": "#/components/schemas/TransportConfig"
            },
            "resume_token": {
              "type": "string",
              "description": "A token from a previous session's ready frame. When it matches a still-live, unexpired stash from that session's disconnect, this session resumes its conversation context (and voice/tools/instructions, where this start frame doesn't already override them) instead of starting empty. An expired, unknown, consumed, or cross-account token is rejected with a non-retryable resume_failed error; callers may then deliberately start fresh without the token. Single-use: each resume consumes the stash and this session's own ready frame carries a new token."
            }
          },
          "additionalProperties": false,
          "allOf": [
            {
              "if": {
                "properties": {
                  "mode": {
                    "properties": {
                      "modality": {
                        "const": "text"
                      }
                    },
                    "required": [
                      "modality"
                    ]
                  }
                }
              },
              "then": {
                "not": {
                  "required": [
                    "audio"
                  ]
                }
              },
              "else": {
                "required": [
                  "audio"
                ]
              }
            }
          ]
        },
        "description": "Opens a conversation. api_key may be omitted when the WebSocket handshake supplied Authorization: Bearer <key>. Voice mode requires audio; text mode omits it and requires the WebSocket transport."
      },
      "inputText": {
        "name": "inputText",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "text"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "input_text"
            },
            "text": {
              "type": "string",
              "minLength": 1,
              "maxLength": 20000,
              "description": "One committed user turn. Accepted only when mode.modality is text."
            }
          },
          "additionalProperties": false
        },
        "description": "Commit a user turn in text mode. Dialt emits the ordinary asr, turn, text_delta, utterance, and done events without audio frames."
      },
      "reset": {
        "name": "reset",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "reset"
            }
          },
          "additionalProperties": false
        }
      },
      "setVoice": {
        "name": "setVoice",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "voice"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "set_voice"
            },
            "voice": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      },
      "toolResult": {
        "name": "toolResult",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "content"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_result"
            },
            "id": {
              "type": "string"
            },
            "content": {},
            "outcome": {
              "type": "string",
              "enum": [
                "succeeded",
                "failed",
                "cancelled",
                "timed_out",
                "unknown"
              ],
              "default": "unknown",
              "description": "Terminal real-world outcome; missing or invalid values normalize to unknown."
            },
            "verified": {
              "type": "boolean",
              "default": false,
              "description": "May be true only with outcome=succeeded; this pair authorizes success narration."
            }
          },
          "additionalProperties": false
        }
      },
      "toolDeferred": {
        "name": "toolDeferred",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "handle"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_deferred"
            },
            "id": {
              "type": "string"
            },
            "handle": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            },
            "status_label": {
              "type": "string",
              "minLength": 1,
              "maxLength": 80
            }
          },
          "additionalProperties": false
        },
        "description": "Detach an eligible call from its voice turn while preserving managed job identity."
      },
      "toolProgress": {
        "name": "toolProgress",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "note"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_progress"
            },
            "id": {
              "type": "string"
            },
            "note": {
              "type": "string",
              "maxLength": 500
            }
          },
          "additionalProperties": false
        }
      },
      "toolPartialResult": {
        "name": "toolPartialResult",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "content"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_partial_result"
            },
            "id": {
              "type": "string"
            },
            "content": {
              "description": "Structured partial result content. Capped at 2 KiB by default; at most 8 partials are accepted per call. Does not resolve the call."
            },
            "reply": {
              "type": "boolean",
              "default": false,
              "description": "Ask Dialt to narrate this milestone immediately. The partial always lands in context, but the proactive reply is skipped if another reply is already in flight. Ignored when `interaction` is given."
            },
            "interaction": {
              "type": "object",
              "description": "Marks this partial as needing a user decision rather than a routine milestone. Unlike reply:true alone, never silently dropped when busy -- falls back to the same completion-job queue tool completions use, preempting them, with a far more persistent delivery retry. See tool_job_narration for its lifecycle ack.",
              "required": [
                "prompt"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "maxLength": 64,
                  "description": "Stable, host-chosen identity for this interaction (1-64 characters), addressable later via tool_interaction_update. Single-use per session: re-raising a superseded ask means minting a fresh id. Omitted, the broker derives \"<call_id>#i<n>\" and echoes it in tool_job_narration's interaction_ids."
                },
                "prompt": {
                  "type": "string",
                  "description": "The question to ask the user, capped at 500 characters."
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "A discrete set of choices (at most 8, each capped at 64 characters). Omit for a free-text answer."
                },
                "resolver": {
                  "type": "object",
                  "required": [
                    "tool"
                  ],
                  "properties": {
                    "tool": {
                      "type": "string",
                      "description": "A CLIENT-declared tool without requires_permission that settles this decision (a permission-gated resolver would bypass the approval verifier; declare the gate OR bind an interaction, not both). Unknown or ineligible names drop the binding (loudly) while the ask itself stands; set_tools re-validates open bindings."
                    },
                    "args": {
                      "type": "object",
                      "description": "Base arguments always passed to the resolver."
                    },
                    "option_args": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "object"
                      },
                      "description": "Per-option argument overlays. Keys must be exactly (a subset of) the offered options -- a mismatch invalidates the whole interaction. The broker constructs the final call as args + option_args[chosen option]; the model picks the option, never the arguments."
                    },
                    "answer_arg": {
                      "type": "string",
                      "description": "For free-text asks (no options): the argument name that receives the user's spoken answer. REQUIRED for a free-text binding; resolve without an answer is rejected (missing_answer)."
                    }
                  },
                  "additionalProperties": false,
                  "description": "Binds this interaction to a resolver: once the ask has been VOICED (fully or partially), every subsequent user turn's first planning round is constrained to the broker-managed interaction_transition tool (resolve / clarify / supersede / cancel) until the decision is settled -- outranking mode.tool_choice. resolve executes the bound tool with host-declared arguments through the ordinary tool_call path (execute-then-close: a failed/timed-out resolver keeps the ask open and retryable); the binding as compact JSON is capped at 2 KiB."
                }
              },
              "additionalProperties": false
            }
          },
          "additionalProperties": false
        },
        "description": "Deliver a structured segment of an in-flight tool's eventual result without resolving the call."
      },
      "toolInteractionUpdate": {
        "name": "toolInteractionUpdate",
        "summary": "Host-side close of an open interaction (docs/client-tool-protocol.md §3a): the decision was resolved out-of-band, cancelled, or superseded by newer intent -- without completing the parent call. Stops the ask's queued or actively-speaking narration and tells the model the decision is closed. Always answered by toolInteractionUpdateAck; late/duplicate/unknown updates are rejected deterministically.",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "interaction_id",
            "state"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_interaction_update"
            },
            "id": {
              "type": "string",
              "description": "The parent call id (or deferred handle) the interaction was raised on."
            },
            "interaction_id": {
              "type": "string",
              "maxLength": 64,
              "description": "The interaction's stable identity: the host-chosen interaction.id from the raising tool_partial_result, or the broker-derived id echoed in tool_job_narration's interaction_ids."
            },
            "state": {
              "type": "string",
              "enum": [
                "resolved",
                "cancelled",
                "superseded"
              ],
              "description": "resolved: the decision was made out-of-band (e.g. in the host's own UI) and the tool is continuing. cancelled: the decision is no longer needed. superseded: replaced by newer intent (typically the user's latest instruction); raise a fresh interaction (new id) if a decision is still needed."
            },
            "note": {
              "type": "string",
              "maxLength": 200,
              "description": "Optional short human-readable reason, surfaced to the model as context (e.g. \"approved in the IDE\")."
            }
          },
          "additionalProperties": false
        }
      },
      "toolCancelClient": {
        "name": "toolCancelClient",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_cancel"
            },
            "id": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      },
      "playbackStopped": {
        "name": "playbackStopped",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "event"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "client_event"
            },
            "event": {
              "type": "string",
              "const": "playback_stopped"
            },
            "remaining_ms": {
              "type": "number",
              "minimum": 0
            },
            "discarded_ms": {
              "type": "number",
              "minimum": 0
            },
            "barge_seq": {
              "type": "integer",
              "minimum": 0,
              "description": "Optional interruption sequence; the server uses the current sequence when omitted."
            }
          },
          "additionalProperties": false,
          "anyOf": [
            {
              "required": [
                "remaining_ms"
              ]
            },
            {
              "required": [
                "discarded_ms"
              ]
            }
          ]
        }
      },
      "ready": {
        "name": "ready",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "voice",
            "name",
            "voices",
            "resume_token"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "ready"
            },
            "modality": {
              "type": "string",
              "enum": [
                "voice",
                "text"
              ],
              "default": "voice"
            },
            "voice": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "voices": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Voice"
              }
            },
            "audio": {
              "type": "object",
              "required": [
                "output_encoding",
                "output_sr"
              ],
              "properties": {
                "output_encoding": {
                  "type": "string",
                  "enum": [
                    "pcm16",
                    "pcm_f32le"
                  ]
                },
                "output_sr": {
                  "type": "integer"
                }
              },
              "additionalProperties": false,
              "description": "The negotiated downlink format for binary assistantAudio frames, echoing start.audio's output_encoding/output_sr (or their defaults, pcm16 at 16000, when omitted). Assert on this instead of assuming an encoding."
            },
            "resume_token": {
              "type": "string",
              "description": "Pass this back as start.resume_token on a reconnect to resume this session's conversation context if the connection drops. Short-lived (a couple of minutes) and single-process — does not survive a server restart."
            }
          },
          "additionalProperties": true,
          "allOf": [
            {
              "if": {
                "properties": {
                  "modality": {
                    "const": "text"
                  }
                },
                "required": [
                  "modality"
                ]
              },
              "then": {
                "not": {
                  "required": [
                    "audio"
                  ]
                }
              },
              "else": {
                "required": [
                  "audio"
                ]
              }
            }
          ]
        }
      },
      "silentMic": {
        "name": "silentMic",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "description": "Nonfatal warning that sustained digital silence or an unusually low startup signal is reaching the server. Keep the session live while the user checks the selected input, mute, and microphone permissions.",
          "required": [
            "type",
            "reason",
            "duration_ms",
            "peak"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "silent_mic"
            },
            "reason": {
              "type": "string",
              "enum": [
                "digital_silence",
                "near_silent"
              ]
            },
            "duration_ms": {
              "type": "number",
              "minimum": 0
            },
            "peak": {
              "type": "integer",
              "minimum": 0,
              "maximum": 32768,
              "description": "Highest absolute PCM16 sample observed in the detection window."
            }
          },
          "additionalProperties": true
        }
      },
      "turn": {
        "name": "turn",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "turn"
            },
            "turn_id": {
              "type": "string",
              "description": "Stable id for this assistant reply, shared by every frame that belongs to it (turn, text_delta, asr, utterance, interrupted, canceled, done)."
            }
          },
          "additionalProperties": true
        }
      },
      "asr": {
        "name": "asr",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "text",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "asr"
            },
            "text": {
              "type": "string"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the turn this transcript belongs to."
            },
            "message_id": {
              "type": "string",
              "maxLength": 128,
              "description": "Client-generated correlation id for an injected text turn. Omitted for voice input."
            },
            "input_source": {
              "type": "string",
              "enum": [
                "voice",
                "text"
              ],
              "description": "Whether the final transcript came from microphone speech or injected text."
            }
          },
          "additionalProperties": true
        }
      },
      "injectContextAck": {
        "name": "injectContextAck",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "message_id",
            "accepted"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "inject_context_ack"
            },
            "message_id": {
              "type": "string",
              "maxLength": 128,
              "description": "The client-generated id from inject_context."
            },
            "accepted": {
              "type": "boolean",
              "description": "Authoritative broker decision. False means the injection was not delivered."
            },
            "role": {
              "type": "string",
              "enum": [
                "user",
                "context"
              ]
            },
            "input_source": {
              "type": "string",
              "enum": [
                "text",
                "context"
              ]
            },
            "turn_id": {
              "type": "string",
              "description": "Stable server turn id for an accepted typed user turn."
            },
            "reply_started": {
              "type": "boolean",
              "description": "For context with reply=true, whether the requested reply also claimed the floor."
            },
            "retryable": {
              "type": "boolean",
              "description": "For a rejection, whether retrying later is safe."
            },
            "detail": {
              "type": "string",
              "description": "Human-readable rejection reason."
            }
          },
          "additionalProperties": false
        },
        "description": "Authoritative accepted/rejected acknowledgement for a correlated inject_context request."
      },
      "utterance": {
        "name": "utterance",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "text",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "utterance"
            },
            "text": {
              "type": "string"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the turn this assistant utterance belongs to."
            },
            "barge_seq": {
              "type": "integer"
            },
            "corrected": {
              "type": "boolean",
              "description": "True when this frame revises the earlier interrupted utterance with the same turn_id."
            }
          },
          "additionalProperties": true
        }
      },
      "done": {
        "name": "done",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "done"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the turn that just completed."
            }
          },
          "additionalProperties": true
        }
      },
      "sessionEndRequested": {
        "name": "sessionEndRequested",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "description": "The conversational model requested a clean end after its just-completed farewell. Headless relays should stop automatic turn injection. A real user may continue during the close grace period, which cancels the requested end.",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "session_end_requested"
            },
            "farewell": {
              "type": "string",
              "description": "The farewell the agent passed to end_call, already spoken as the turn's only utterance. Absent when the end was not requested through the tool."
            }
          },
          "additionalProperties": true
        }
      },
      "interrupted": {
        "name": "interrupted",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "interrupted"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the barged turn."
            },
            "clear": {
              "type": "boolean"
            },
            "barge_seq": {
              "type": "integer"
            }
          },
          "additionalProperties": true
        }
      },
      "canceled": {
        "name": "canceled",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "turn_id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "canceled"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the eager speculative turn that was retracted."
            }
          },
          "additionalProperties": true
        }
      },
      "textDelta": {
        "name": "textDelta",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "turn_id",
            "delta"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "text_delta"
            },
            "turn_id": {
              "type": "string",
              "description": "Id of the turn this text belongs to."
            },
            "delta": {
              "type": "string",
              "description": "An incremental slice of assistant text, as the model produces it. Concatenate deltas for a given turn_id to reconstruct the streamed text; the final text still arrives as utterance."
            }
          },
          "additionalProperties": false
        },
        "description": "Streamed assistant text, sent as the model produces it (ahead of audio and the final utterance frame)."
      },
      "permissionPending": {
        "name": "permissionPending",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "tool",
            "arguments"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "permission_pending"
            },
            "id": {
              "type": "string",
              "description": "Approval id — pass this back as confirm_tool_permission's approval_id."
            },
            "tool": {
              "type": "string",
              "description": "Name of the tool call awaiting explicit user approval."
            },
            "arguments": {
              "type": "object",
              "description": "Arguments the model wants to call the tool with, for display to the user."
            }
          },
          "additionalProperties": false
        },
        "description": "A tool call declared requires_permission is held pending explicit user approval."
      },
      "permissionApproved": {
        "name": "permissionApproved",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "tool"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "permission_approved"
            },
            "id": {
              "type": "string",
              "description": "The approval id that was resolved."
            },
            "tool": {
              "type": "string"
            }
          },
          "additionalProperties": false
        },
        "description": "A pending permission was approved and the held tool call ran."
      },
      "permissionRefused": {
        "name": "permissionRefused",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "tool"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "permission_refused"
            },
            "id": {
              "type": "string",
              "description": "The approval id that was resolved."
            },
            "tool": {
              "type": "string"
            }
          },
          "additionalProperties": false
        },
        "description": "A pending permission was declined; the held tool call did not run."
      },
      "toolCall": {
        "name": "toolCall",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "name",
            "args"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_call"
            },
            "id": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "args": {
              "type": "object"
            }
          },
          "additionalProperties": true
        }
      },
      "toolDeferredAck": {
        "name": "toolDeferredAck",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "accepted",
            "id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_deferred_ack"
            },
            "accepted": {
              "type": "boolean"
            },
            "id": {
              "type": "string"
            },
            "handle": {
              "type": "string"
            },
            "job_id": {
              "type": "string"
            },
            "status_label": {
              "type": "string"
            },
            "deferred_timeout": {
              "type": "number"
            },
            "reason": {
              "type": "string"
            }
          },
          "additionalProperties": true
        }
      },
      "toolDeferredResume": {
        "name": "toolDeferredResume",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "handle",
            "name",
            "job_id",
            "status_label"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_deferred_resume"
            },
            "id": {
              "type": "string"
            },
            "handle": {
              "type": "string"
            },
            "name": {
              "type": "string"
            },
            "job_id": {
              "type": "string"
            },
            "status_label": {
              "type": "string"
            }
          },
          "additionalProperties": true
        }
      },
      "toolCancelServer": {
        "name": "toolCancelServer",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_cancel"
            },
            "id": {
              "type": "string"
            }
          },
          "additionalProperties": true
        }
      },
      "toolJobDeliveryFailed": {
        "name": "toolJobDeliveryFailed",
        "summary": "A queued background-job completion voiceover could not be delivered. The turn still closes with done; job_ids lists jobs whose spoken delivery is terminally failed (their results remain in conversation context and can be asked about), requeued lists jobs re-queued for one more proactive spoken attempt (a newer user turn may supersede it, as with any queued completion).",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "job_ids",
            "requeued"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_job_delivery_failed"
            },
            "job_ids": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Jobs whose spoken delivery is terminally failed."
            },
            "requeued": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Jobs re-queued for one more proactive spoken attempt. A newer user turn may supersede the retry, as with any queued completion."
            },
            "detail": {
              "type": "string",
              "description": "Human-readable failure detail. May keep changing freely — do not match against it."
            }
          },
          "additionalProperties": true
        }
      },
      "toolJobNarration": {
        "name": "toolJobNarration",
        "summary": "Narration-lifecycle ack for a queued tool_partial_result `interaction` (docs/client-tool-protocol.md §3a). Scoped to interaction-kind jobs only -- a plain completion has no such wire-visible lifecycle.",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "state",
            "job_ids",
            "interaction_ids",
            "kind"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_job_narration"
            },
            "state": {
              "type": "string",
              "enum": [
                "queued",
                "started",
                "superseded",
                "cancelled",
                "failed",
                "resolved"
              ],
              "description": "queued: enqueued, floor busy -- always bounded: a deadline watchdog re-attempts delivery every INTERACTION_QUEUE_DEADLINE_S (10s default, up to INTERACTION_QUEUE_DEADLINE_ROUNDS rounds per delivery cycle), and an ask whose own blocked turn holds the floor yields that turn and is voiced immediately. started: now being spoken. superseded: a real user turn or barge preempted it before anything was heard (a fresh interaction can be raised again if still needed), a newer interaction on the same call replaced it, or the host closed it as superseded via tool_interaction_update. cancelled: cancel_pending_job, the host's tool_cancel/timeout of the call behind it, or a host tool_interaction_update state=cancelled dismissed it first. failed: a terminal give-up -- retried past the delivery bound, or the queue deadline expired with the floor unclaimable; telemetry-only (nothing was spoken -- fall back to your own UI for the decision). resolved: the decision no longer awaits the voice channel -- the interaction's own call sent its terminal result, or the host closed it as resolved via tool_interaction_update. A requeued ask (claimed but unheard) emits a fresh queued, so every delivery cycle ends in an observable state change."
            },
            "job_ids": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "The interaction job(s) this state applies to. Multiple pending interactions are coalesced into one turn, so more than one id may share a `started` event."
            },
            "interaction_ids": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "Stable interaction identities this state applies to (host-chosen `interaction.id`, or the broker-derived \"<call_id>#i<n>\"), aligned with the affected subset of job_ids. How a host learns a broker-derived id: it appears on the first queued/started ack for the ask."
            },
            "kind": {
              "type": "string",
              "description": "Always \"interaction\" today -- present so a future kind could share this ack without a new message type."
            }
          },
          "additionalProperties": false
        }
      },
      "working": {
        "name": "working",
        "summary": "Dialt is blocking on a tool result with nothing audible (the silence after the spoken bridge, until the final reply's first audio), or that wait has ended.",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "active"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "working"
            },
            "active": {
              "type": "boolean",
              "description": "true: a tool-calling turn has started waiting on its result (client tools, web_search, think_deeply) and nothing is playing - a long wait is now possible. false: that turn ended, for any reason (final reply spoken, barge, cancel, nothing to say). The final reply's own `turn` frame and first audio normally arrive before the false; a client covering the gap (the browser SDK's `ambience: 'thinking'` fades its bed in after a short threshold and out again on the reply's first audio) should end its cover on whichever comes first. Pure state signal: no audio is sent for it."
            }
          }
        }
      },
      "toolInteractionUpdateAck": {
        "name": "toolInteractionUpdateAck",
        "summary": "Deterministic answer to every toolInteractionUpdate. applied=false carries a stable reason: invalid_interaction_id, invalid_state, unknown_interaction (never seen, or from before a reconnect -- interaction state does not survive a resume), or already_closed (first close wins: cancel dismissed it, its call resolved, a newer ask replaced it, or a duplicate of this very frame -- a user turn superseding the ask's narration deliberately does NOT close the decision).",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "id",
            "interaction_id",
            "state",
            "applied",
            "reason"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "tool_interaction_update_ack"
            },
            "id": {
              "type": [
                "string",
                "null"
              ],
              "description": "The canonical server call id the update resolved to, or null if the call reference was unknown."
            },
            "interaction_id": {
              "type": [
                "string",
                "null"
              ]
            },
            "state": {
              "type": [
                "string",
                "null"
              ],
              "description": "Echo of the requested state."
            },
            "applied": {
              "type": "boolean"
            },
            "reason": {
              "type": [
                "string",
                "null"
              ],
              "enum": [
                "invalid_interaction_id",
                "invalid_state",
                "unknown_interaction",
                "already_closed",
                null
              ],
              "description": "null when applied; otherwise the stable rejection reason."
            }
          },
          "additionalProperties": false
        }
      },
      "voiceChanged": {
        "name": "voiceChanged",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "voice",
            "name"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "voice"
            },
            "voice": {
              "type": "string"
            },
            "name": {
              "type": "string"
            }
          },
          "additionalProperties": true
        }
      },
      "error": {
        "name": "error",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "detail",
            "code",
            "retryable"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "error"
            },
            "detail": {
              "type": "string",
              "description": "Human-readable detail. May keep changing freely — do not match against it; use code instead."
            },
            "code": {
              "type": "string",
              "description": "Stable machine-readable error code (e.g. unauthorized, server_busy, invalid_tools). Safe to match on for retry/handling logic."
            },
            "retryable": {
              "type": "boolean",
              "description": "Whether retrying the same request is expected to succeed. Defaults per-code (e.g. server_busy/server_restarting are retryable; unauthorized/invalid_start_frame are not) and is always present."
            }
          },
          "additionalProperties": true
        }
      },
      "webrtcIce": {
        "name": "webrtcIce",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "ice_servers"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "webrtc_ice"
            },
            "ice_servers": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/IceServer"
              },
              "description": "STUN plus (when configured) short-lived TURN credentials. Do not build the RTCPeerConnection until this arrives — gathering ICE earlier would miss TURN."
            }
          },
          "additionalProperties": false
        },
        "description": "Experimental. Sent over the signaling WebSocket right after a start frame carrying transport.kind webrtc. Build the RTCPeerConnection with these ice_servers, then send webrtcOffer."
      },
      "webrtcOffer": {
        "name": "webrtcOffer",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "sdp"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "webrtc_offer"
            },
            "sdp": {
              "type": "string",
              "description": "The client peer connection's local SDP offer, created after webrtcIce's servers are configured."
            }
          },
          "additionalProperties": false
        },
        "description": "Experimental. Client's SDP offer, sent over the signaling WebSocket once the RTCPeerConnection is built with webrtcIce's servers."
      },
      "webrtcAnswer": {
        "name": "webrtcAnswer",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "sdp"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "webrtc_answer"
            },
            "sdp": {
              "type": "string",
              "description": "The server peer connection's local SDP answer."
            },
            "ice_servers": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/IceServer"
              }
            }
          },
          "additionalProperties": false
        },
        "description": "Experimental. Server's SDP answer to webrtcOffer. The call then lives on the peer connection; the signaling socket may drop afterwards without affecting it."
      },
      "webrtcBye": {
        "name": "bye",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "code"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "bye"
            },
            "code": {
              "type": "integer",
              "description": "Close code from the same vocabulary as a WebSocket close code."
            },
            "reason": {
              "type": "string"
            }
          },
          "additionalProperties": false
        },
        "description": "Sent on the webrtc \"control\" data channel just before the server closes the peer connection — the webrtc transport's counterpart of a WebSocket close frame/code."
      },
      "microphoneAudio": {
        "name": "microphoneAudio",
        "contentType": "application/octet-stream",
        "payload": {
          "type": "string",
          "description": "PCM16 little-endian mono microphone audio at audio.sr, sent in binary WebSocket frames.",
          "format": "binary"
        }
      },
      "assistantAudio": {
        "name": "assistantAudio",
        "contentType": "application/octet-stream",
        "payload": {
          "type": "string",
          "description": "Float32 little-endian mono assistant audio at 16 kHz, received in binary WebSocket frames.",
          "format": "binary"
        }
      },
      "ambience": {
        "name": "ambience",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "active"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "ambience"
            },
            "active": {
              "type": "boolean"
            }
          },
          "additionalProperties": false
        }
      },
      "interrupt": {
        "name": "interrupt",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "type": "string",
              "enum": [
                "interrupt",
                "cancel"
              ],
              "description": "Either spelling stops the in-flight reply the same way."
            }
          },
          "additionalProperties": false
        },
        "description": "Host-initiated stop (moderation, a \"stop talking\" button, programmatic interruption). Unlike reset, this only drops the in-flight reply — conversation context and endpointer state are left intact. No payload beyond type."
      },
      "setTools": {
        "name": "setTools",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "tools"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "set_tools"
            },
            "tools": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/ToolDefinition"
              },
              "description": "Replacement tool manifest, same shape as start's mode.tools."
            }
          },
          "additionalProperties": false
        },
        "description": "Mid-session tool swap: agents that change capability by call phase no longer need a reconnect, which would destroy the conversation."
      },
      "setToolChoice": {
        "name": "setToolChoice",
        "summary": "Replace the effective tool_choice mid-session; applies from the next reply. one_shot: true reverts to the previous choice after the next user turn consumes the new one (\"the next turn must use one of these\"). An invalid value is rejected with an `error` frame (code invalid_tool_choice) and changes nothing. A mid-session setTools resets tool_choice to \"auto\" — re-apply it after swapping tools.",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "tool_choice"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "set_tool_choice"
            },
            "tool_choice": {
              "$ref": "#/components/schemas/DialtMode/properties/tool_choice"
            },
            "one_shot": {
              "type": "boolean",
              "default": false
            }
          },
          "additionalProperties": false
        }
      },
      "triggerReply": {
        "name": "triggerReply",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "trigger_reply"
            },
            "prompt": {
              "type": "string",
              "maxLength": 2000,
              "description": "Optional nudge text for the reply. Omit to make Dialt speak first with no real user turn."
            }
          },
          "additionalProperties": false
        },
        "description": "Client-triggered reply. Silently ignored if a reply is already in flight — do not stack these."
      },
      "injectContext": {
        "name": "injectContext",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "text",
            "role"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "inject_context"
            },
            "message_id": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "description": "Client-generated correlation id echoed by inject_context_ack and, for role=user, the canonical final asr event. New SDKs always send this field."
            },
            "text": {
              "type": "string",
              "maxLength": 2000,
              "description": "The text to inject. Required, 1–2000 characters."
            },
            "role": {
              "type": "string",
              "enum": [
                "user",
                "context"
              ],
              "description": "\"user\": treated exactly like a real spoken user turn arriving as text instead of voice — visible in the transcript, answered the same way a turn.end is. \"context\": silent background information (a CRM lookup, a RAG snippet) appended to the model's context; never shown to the client as something the user said."
            },
            "reply": {
              "type": "boolean",
              "default": false,
              "description": "Whether this injection should immediately trigger a reply. With message_id, rejection is an inject_context_ack with accepted=false; legacy uncorrelated calls receive an inject_rejected error frame."
            }
          },
          "additionalProperties": false
        },
        "description": "Text/context injection with authoritative delivery acknowledgement. Supply message_id to correlate inject_context_ack and typed-turn asr events."
      },
      "sessionClosingSoon": {
        "name": "sessionClosingSoon",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "seconds_remaining"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "session_closing_soon"
            },
            "seconds_remaining": {
              "type": "number",
              "description": "Approximate seconds until the server closes this connection: the session hit its maximum duration, a scoped API credential is expiring, or (text sessions, reason=\"idle\") the connection has been idle for configured text idle timeout with nothing in flight. Sent once, shortly before the close; any client frame cancels an idle close."
            },
            "reason": {
              "type": "string",
              "enum": [
                "idle"
              ],
              "description": "Present only for a text session's idle close. Absent for the session time limit and credential expiry."
            }
          },
          "additionalProperties": false
        },
        "description": "Warns the client shortly before the server closes the connection due to the session time limit (or scoped credential expiry) — gives the client a chance to wrap up, or to reconnect with start.resume_token once closed."
      },
      "lowCreditWarning": {
        "name": "lowCreditWarning",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "seconds_remaining"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "low_credit_warning"
            },
            "seconds_remaining": {
              "type": "number",
              "description": "Approximate remaining talk time at the current balance. Sent once, the first time it drops under the warning threshold — read-only, never adjusts the balance itself."
            }
          },
          "additionalProperties": false
        },
        "description": "API-key (paid) sessions only. Warns the client once when the account's remaining balance is running low, so the call doesn't just cut off mid-conversation when credit runs out."
      },
      "playbackReport": {
        "name": "playbackReport",
        "contentType": "application/json",
        "payload": {
          "type": "object",
          "required": [
            "type",
            "event",
            "underruns"
          ],
          "properties": {
            "type": {
              "type": "string",
              "const": "client_event"
            },
            "event": {
              "type": "string",
              "const": "playback_report"
            },
            "underruns": {
              "type": "integer",
              "minimum": 0,
              "description": "Times this reply's audio queue drained at the speaker before the next chunk arrived."
            },
            "starved_ms": {
              "type": "number",
              "minimum": 0,
              "description": "Total audible starvation across those underruns."
            },
            "max_gap_ms": {
              "type": "number",
              "minimum": 0
            },
            "turn_id": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      }
    },
    "schemas": {
      "Voice": {
        "type": "object",
        "required": [
          "key",
          "name",
          "kind",
          "descriptor"
        ],
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "regional",
              "designed"
            ]
          },
          "descriptor": {
            "type": "string",
            "description": "Display context such as Classic's regional accent; may be empty."
          }
        }
      },
      "AudioConfig": {
        "type": "object",
        "required": [
          "sr"
        ],
        "properties": {
          "sr": {
            "type": "integer",
            "minimum": 8000,
            "maximum": 192000
          },
          "uplink_format": {
            "type": "string"
          },
          "raw_assist": {
            "type": "boolean"
          },
          "output_encoding": {
            "type": "string",
            "enum": [
              "pcm16",
              "pcm_f32le"
            ],
            "default": "pcm16",
            "description": "Downlink (assistant audio) sample encoding — a separate concept from the uplink sr above. pcm16 is the wire default; pcm_f32le is opt-in for clients (e.g. the LiveKit plugin) that want float samples."
          },
          "output_sr": {
            "type": "integer",
            "minimum": 8000,
            "maximum": 192000,
            "default": 16000,
            "description": "Downlink sample rate for assistantAudio frames. Defaults to the server's client sample rate when omitted."
          }
        },
        "additionalProperties": false
      },
      "ClientInfo": {
        "type": "object",
        "properties": {
          "user": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "capabilities": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "audio_frontend": {
            "type": "string",
            "description": "Diagnostic label for the active microphone echo-cancellation frontend, or unknown before SDK-owned capture starts."
          }
        },
        "additionalProperties": false
      },
      "Mode": {
        "$ref": "#/components/schemas/DialtMode"
      },
      "TransportConfig": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "webrtc",
            "description": "Experimental. Selects the WebRTC media transport (UDP, SRTP/Opus) instead of the default WebSocket binary frames. No sdp here - send it once webrtcIce arrives with TURN credentials. Requires mode.kind dialt."
          }
        },
        "additionalProperties": false,
        "description": "Omit start.transport entirely to use the default WebSocket media transport."
      },
      "IceServer": {
        "type": "object",
        "required": [
          "urls"
        ],
        "properties": {
          "urls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "username": {
            "type": "string"
          },
          "credential": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "DialtMode": {
        "type": "object",
        "required": [
          "kind"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "const": "dialt"
          },
          "modality": {
            "type": "string",
            "enum": [
              "voice",
              "text"
            ],
            "default": "voice",
            "description": "The I/O surface for this session. text uses input_text and emits no audio; voice uses microphone and assistant audio. The model, instructions, tools, history, and conversation events are otherwise the same."
          },
          "voice": {
            "type": "string"
          },
          "instructions": {
            "type": "string"
          },
          "tools": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ToolDefinition"
            }
          },
          "web_search": {
            "type": "boolean",
            "description": "Enables the explicit managed web_search tool. False does not disable search inside other platform-managed reasoning tools when those capabilities are enabled."
          },
          "end_call": {
            "type": "boolean",
            "default": false,
            "description": "Declares the managed end_call(farewell) tool so the agent can end the session. On that turn the server speaks only the farewell, sends session_end_requested with it, waits a short grace in which user speech cancels the end, then closes with code 1000. Off: the agent cannot end the session; the host ends it with wrap_up or by closing. The tool name end_call is reserved while enabled, like web_search (invalid_tools)."
          },
          "flow": {
            "type": "boolean"
          },
          "background_audio": {
            "type": "boolean",
            "default": false,
            "description": "Mix a soft background underscore into the assistant audio for the whole call. WebRTC transport only: the bed rides the playout track, the only downlink that runs continuously between turns. Sending this with the WebSocket transport is rejected with invalid_start_frame."
          },
          "greeting": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "boolean",
                "const": false
              }
            ]
          },
          "temperature": {
            "type": "number"
          },
          "silence_nudge_s": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Per-session override of the server's silence check-in delay (env default 10s). Must be less than silence_end_s; omit to keep the server default. An invalid value (non-positive, or not less than silence_end_s) falls back to the server default rather than rejecting the session."
          },
          "silence_end_s": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Per-session override of the server's silence sign-off + session-end delay (env default 20s), counted after the check-in. Must be greater than silence_nudge_s; omit to keep the server default. An invalid value falls back to the server default rather than rejecting the session."
          },
          "tool_choice": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "auto",
                  "none",
                  "required"
                ]
              },
              {
                "type": "object",
                "required": [
                  "allowed"
                ],
                "properties": {
                  "allowed": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "additionalProperties": false
              },
              {
                "type": "object",
                "required": [
                  "tool"
                ],
                "properties": {
                  "tool": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            ],
            "description": "Generation restriction in the familiar OpenAI/Gemini vocabulary. Requires a non-empty `tools` list; `allowed`/`tool` names must be CLIENT-declared tools (broker-managed protocol tools are not valid targets; unknown names fail the session with invalid_tool_choice). The durable (non-one-shot) choice survives a resume via the session stash. `required`/`allowed`/`tool` constrain the FIRST planning round of each user turn (Gemini function-calling mode ANY [+ allowed_function_names]); later rounds of the same turn are unconstrained so the spoken reply can still be composed. `none` withholds the client-declared tools while broker-managed protocol tools (job cancellation, permission resolution, managed search) stay available. Speculative drafts are planned under the active choice and dropped when it changes."
          }
        },
        "additionalProperties": false
      },
      "ToolDefinition": {
        "type": "object",
        "allOf": [
          {
            "if": {
              "properties": {
                "deferred": {
                  "const": false
                }
              }
            },
            "then": {
              "not": {
                "anyOf": [
                  {
                    "required": [
                      "deferred_timeout"
                    ]
                  },
                  {
                    "required": [
                      "notify_on_complete"
                    ]
                  }
                ]
              }
            }
          }
        ],
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Client tool name. Platform-managed names such as think_deeply and web_search are reserved when those capabilities are enabled."
          },
          "description": {
            "type": "string",
            "default": ""
          },
          "parameters": {
            "type": "object"
          },
          "read_only": {
            "type": "boolean",
            "default": false,
            "description": "Allows early speculative execution. Use only for harmless, cheap lookups."
          },
          "requires_permission": {
            "type": "boolean",
            "default": false,
            "description": "Hold the exact call until Dialt verifies clear user approval."
          },
          "expected_duration": {
            "type": "string",
            "enum": [
              "instant",
              "seconds",
              "long"
            ],
            "description": "What the caller should hear on a turn that uses this tool. instant: a fast lookup, the caller hears the answer directly. seconds: anything that takes more than about a second, the assistant acknowledges first and answers when the result arrives. long: agent or batch jobs. Omitted: learned from observed results (a new tool is acknowledged first; holding for the answer is earned by fast results)."
          },
          "wait_for_tool": {
            "type": "boolean",
            "deprecated": true,
            "description": "Deprecated alias: true is expected_duration: \"instant\", an explicit false is \"seconds\" (an explicit expected_duration wins; omitted learns). Accepted during alpha, removed at beta."
          },
          "status_label": {
            "type": "string",
            "minLength": 1,
            "description": "Short user-safe label Dialt may use when reporting pending work. Defaults to the tool name."
          },
          "timeout": {
            "type": "number",
            "minimum": 1,
            "maximum": 600,
            "default": 30
          },
          "deferred": {
            "type": "boolean",
            "default": false,
            "description": "Allow the host to detach a call from its current voice turn."
          },
          "deferred_timeout": {
            "type": "number",
            "minimum": 1,
            "maximum": 86400,
            "default": 7200,
            "description": "Lifetime after an accepted tool_deferred frame. Valid only when deferred is true and must be at least timeout; the broker enforces the cross-field comparison."
          },
          "notify_on_complete": {
            "type": "boolean",
            "default": true,
            "description": "Proactively wake the brain when deferred work completes; the result always enters context. Valid only when deferred is true."
          }
        },
        "additionalProperties": false
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Dialt API key",
        "description": "Persistent or session-scoped Dialt key in the WebSocket Authorization header. Browser clients that cannot set handshake headers may instead send api_key in the start frame."
      }
    }
  }
}
