import { z } from "zod"; import type { CallMode } from "./config.js"; // ----------------------------------------------------------------------------- // Provider Identifiers // ----------------------------------------------------------------------------- export const ProviderNameSchema = z.enum(["telnyx", "twilio", "plivo", "mock"]); export type ProviderName = z.infer; // ----------------------------------------------------------------------------- // Core Call Identifiers // ----------------------------------------------------------------------------- /** Internal call identifier (UUID) */ export type CallId = string; /** Provider-specific call identifier */ export type ProviderCallId = string; // ----------------------------------------------------------------------------- // Call Lifecycle States // ----------------------------------------------------------------------------- export const CallStateSchema = z.enum([ // Non-terminal states "initiated", "ringing", "answered", "active", "speaking", "listening", // Terminal states "completed", "hangup-user", "hangup-bot", "timeout", "error", "failed", "no-answer", "busy", "voicemail", ]); export type CallState = z.infer; export const TerminalStates = new Set([ "completed", "hangup-user", "hangup-bot", "timeout", "error", "failed", "no-answer", "busy", "voicemail", ]); export const EndReasonSchema = z.enum([ "completed", "hangup-user", "hangup-bot", "timeout", "error", "failed", "no-answer", "busy", "voicemail", ]); export type EndReason = z.infer; // ----------------------------------------------------------------------------- // Normalized Call Events // ----------------------------------------------------------------------------- const BaseEventSchema = z.object({ id: z.string(), // Stable provider-derived key for idempotency/replay dedupe. dedupeKey: z.string().optional(), callId: z.string(), providerCallId: z.string().optional(), timestamp: z.number(), // Optional per-turn nonce for speech events (Twilio replay hardening). turnToken: z.string().optional(), // Optional fields for inbound call detection direction: z.enum(["inbound", "outbound"]).optional(), from: z.string().optional(), to: z.string().optional(), }); export const NormalizedEventSchema = z.discriminatedUnion("type", [ BaseEventSchema.extend({ type: z.literal("call.initiated"), }), BaseEventSchema.extend({ type: z.literal("call.ringing"), }), BaseEventSchema.extend({ type: z.literal("call.answered"), }), BaseEventSchema.extend({ type: z.literal("call.active"), }), BaseEventSchema.extend({ type: z.literal("call.speaking"), text: z.string(), }), BaseEventSchema.extend({ type: z.literal("call.speech"), transcript: z.string(), isFinal: z.boolean(), confidence: z.number().min(0).max(1).optional(), }), BaseEventSchema.extend({ type: z.literal("call.silence"), durationMs: z.number(), }), BaseEventSchema.extend({ type: z.literal("call.dtmf"), digits: z.string(), }), BaseEventSchema.extend({ type: z.literal("call.ended"), reason: EndReasonSchema, }), BaseEventSchema.extend({ type: z.literal("call.error"), error: z.string(), retryable: z.boolean().optional(), }), ]); export type NormalizedEvent = z.infer; // ----------------------------------------------------------------------------- // Call Direction // ----------------------------------------------------------------------------- export const CallDirectionSchema = z.enum(["outbound", "inbound"]); export type CallDirection = z.infer; // ----------------------------------------------------------------------------- // Call Record // ----------------------------------------------------------------------------- export const TranscriptEntrySchema = z.object({ timestamp: z.number(), speaker: z.enum(["bot", "user"]), text: z.string(), isFinal: z.boolean().default(true), }); export type TranscriptEntry = z.infer; export const CallRecordSchema = z.object({ callId: z.string(), providerCallId: z.string().optional(), provider: ProviderNameSchema, direction: CallDirectionSchema, state: CallStateSchema, from: z.string(), to: z.string(), sessionKey: z.string().optional(), startedAt: z.number(), answeredAt: z.number().optional(), endedAt: z.number().optional(), endReason: EndReasonSchema.optional(), transcript: z.array(TranscriptEntrySchema).default([]), processedEventIds: z.array(z.string()).default([]), metadata: z.record(z.string(), z.unknown()).optional(), }); export type CallRecord = z.infer; // ----------------------------------------------------------------------------- // Webhook Types // ----------------------------------------------------------------------------- export type WebhookVerificationResult = { ok: boolean; reason?: string; /** Signature is valid, but request was seen before within replay window. */ isReplay?: boolean; /** Stable key derived from authenticated request material. */ verifiedRequestKey?: string; }; export type WebhookParseOptions = { /** Stable request key from verifyWebhook. */ verifiedRequestKey?: string; }; export type WebhookContext = { headers: Record; rawBody: string; url: string; method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; query?: Record; remoteAddress?: string; }; export type ProviderWebhookParseResult = { events: NormalizedEvent[]; providerResponseBody?: string; providerResponseHeaders?: Record; statusCode?: number; }; // ----------------------------------------------------------------------------- // Provider Method Types // ----------------------------------------------------------------------------- export type InitiateCallInput = { callId: CallId; from: string; to: string; webhookUrl: string; clientState?: Record; /** Inline TwiML to execute (skips webhook, used for notify mode) */ inlineTwiml?: string; }; export type InitiateCallResult = { providerCallId: ProviderCallId; status: "initiated" | "queued"; }; export type HangupCallInput = { callId: CallId; providerCallId: ProviderCallId; reason: EndReason; }; export type PlayTtsInput = { callId: CallId; providerCallId: ProviderCallId; text: string; voice?: string; locale?: string; }; export type StartListeningInput = { callId: CallId; providerCallId: ProviderCallId; language?: string; /** Optional per-turn nonce for provider callbacks (replay hardening). */ turnToken?: string; }; export type StopListeningInput = { callId: CallId; providerCallId: ProviderCallId; }; // ----------------------------------------------------------------------------- // Call Status Verification (used on restart to verify persisted calls) // ----------------------------------------------------------------------------- export type GetCallStatusInput = { providerCallId: ProviderCallId; }; export type GetCallStatusResult = { /** Provider-specific status string (e.g. "completed", "in-progress") */ status: string; /** True when the provider confirms the call has ended */ isTerminal: boolean; /** True when the status could not be determined (transient error) */ isUnknown?: boolean; }; // ----------------------------------------------------------------------------- // Outbound Call Options // ----------------------------------------------------------------------------- export type OutboundCallOptions = { /** Message to speak when call connects */ message?: string; /** Call mode (overrides config default) */ mode?: CallMode; }; // ----------------------------------------------------------------------------- // Tool Result Types // ----------------------------------------------------------------------------- export type InitiateCallToolResult = { success: boolean; callId?: string; status?: "initiated" | "queued" | "no-answer" | "busy" | "failed"; error?: string; }; export type ContinueCallToolResult = { success: boolean; transcript?: string; error?: string; }; export type SpeakToUserToolResult = { success: boolean; error?: string; }; export type EndCallToolResult = { success: boolean; error?: string; };