@voicelayer/sdk 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -495,9 +495,11 @@ var init_agents = __esm({
495
495
  });
496
496
  ProcessSchemaDTO = z.object({
497
497
  id: z.string().min(1).max(64),
498
- // System-prompt fragments compiled from say/confirm nodes. The SDK bridge
499
- // composes these into the agent's prompt.
500
- prompts: z.array(z.object({ id: z.string().min(1).max(128), text: z.string().max(2e3) })).max(64).optional(),
498
+ // System-prompt fragments compiled from say/confirm/llm nodes. The SDK bridge
499
+ // composes these into the agent's prompt. An AI step's instructions are a
500
+ // system prompt in their own right, so the cap is generous (2,000 refused real
501
+ // flows — G-27); publish checks it (validateFlowGraph → compiledSchemaIssues).
502
+ prompts: z.array(z.object({ id: z.string().min(1).max(128), text: z.string().max(16e3) })).max(64).optional(),
501
503
  fields: z.array(ProcessFieldDTO).max(64),
502
504
  completionGate: z.object({
503
505
  requiredFields: z.array(z.string()).max(64),
@@ -1872,28 +1874,17 @@ var init_flow_ports = __esm({
1872
1874
  INCOMPLETE_PORT_ID = "incomplete";
1873
1875
  }
1874
1876
  });
1877
+
1878
+ // ../contracts/src/flow-edges.ts
1875
1879
  function isLabelOnlyEdge(edge, sourceType) {
1876
1880
  if (!CONVERSATIONAL_NODE_TYPES.has(sourceType)) return false;
1877
1881
  if (edge.sourceHandle) return false;
1878
1882
  if (typeof edge.condition === "string" && edge.condition.trim()) return false;
1879
1883
  return typeof edge.label === "string" && edge.label.trim().length > 0;
1880
1884
  }
1881
- var FlowIssueSeverity, FlowIssue, CONVERSATIONAL_NODE_TYPES;
1882
- var init_flow_validate = __esm({
1883
- "../contracts/src/flow-validate.ts"() {
1884
- init_flow_ports();
1885
- FlowIssueSeverity = z.enum(["error", "warning"]);
1886
- FlowIssue = z.object({
1887
- code: z.string().max(64),
1888
- severity: FlowIssueSeverity,
1889
- message: z.string().max(500),
1890
- nodeId: z.string().max(128).optional(),
1891
- edgeId: z.string().max(128).optional()
1892
- });
1893
- z.object({
1894
- valid: z.boolean(),
1895
- issues: z.array(FlowIssue).max(256)
1896
- });
1885
+ var CONVERSATIONAL_NODE_TYPES;
1886
+ var init_flow_edges = __esm({
1887
+ "../contracts/src/flow-edges.ts"() {
1897
1888
  CONVERSATIONAL_NODE_TYPES = /* @__PURE__ */ new Set([
1898
1889
  "llm",
1899
1890
  "ask",
@@ -1902,838 +1893,909 @@ var init_flow_validate = __esm({
1902
1893
  }
1903
1894
  });
1904
1895
 
1905
- // ../contracts/src/flow-builtins.ts
1906
- var init_flow_builtins = __esm({
1907
- "../contracts/src/flow-builtins.ts"() {
1908
- }
1909
- });
1910
- var ComplianceRule, ComplianceDecision, ComplianceConfig;
1911
- var init_compliance = __esm({
1912
- "../contracts/src/compliance.ts"() {
1913
- ComplianceRule = z.enum(["dnc", "consent", "quiet_hours", "frequency_cap"]);
1914
- ComplianceDecision = z.enum(["allowed", "blocked"]);
1915
- ComplianceConfig = z.object({
1916
- // Local calling window for the CALLED party — TCPA default 8am–9pm.
1917
- quietHoursStartHour: z.number().int().min(0).max(23).default(8),
1918
- quietHoursEndHour: z.number().int().min(1).max(24).default(21),
1919
- enforceDnc: z.boolean().default(true),
1920
- enforceQuietHours: z.boolean().default(true),
1921
- // Max dial attempts to one number per rolling 24h. 0 = unlimited.
1922
- maxAttemptsPerDay: z.number().int().min(0).default(0),
1923
- // Require a recorded consent flag before dialing.
1924
- requireConsent: z.boolean().default(false),
1925
- // Fallback IANA tz when the number's region can't be resolved.
1926
- fallbackTimeZone: z.string().min(1).default("America/New_York")
1927
- });
1928
- z.object({
1929
- phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1930
- // ISO instant the check is evaluated at (defaults to now server-side).
1931
- now: z.string().datetime().optional(),
1932
- // Override the resolved timezone (else derived from the number's area code).
1933
- timeZone: z.string().optional(),
1934
- // Recent attempt count in the last 24h (for the frequency cap), if known.
1935
- recentAttempts: z.number().int().min(0).optional(),
1936
- // Whether consent is on file (for requireConsent).
1937
- hasConsent: z.boolean().optional()
1938
- });
1939
- z.object({
1940
- decision: ComplianceDecision,
1941
- // The first rule that blocked, if any.
1942
- rule: ComplianceRule.nullable(),
1943
- reason: z.string().nullable()
1944
- });
1945
- z.object({
1946
- id: z.string().uuid(),
1947
- projectId: z.string().min(1),
1948
- phoneE164: z.string(),
1949
- reason: z.string().nullable(),
1950
- source: z.string().nullable(),
1951
- createdAt: z.string().datetime()
1952
- });
1953
- z.object({
1954
- id: z.string().uuid(),
1955
- projectId: z.string().min(1),
1956
- phoneE164: z.string(),
1957
- decision: ComplianceDecision,
1958
- rule: ComplianceRule.nullable(),
1959
- reason: z.string().nullable(),
1960
- checkedAt: z.string().datetime()
1961
- });
1962
- z.object({
1963
- phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
1964
- reason: z.string().max(500).optional(),
1965
- source: z.string().max(120).optional()
1966
- });
1967
- ComplianceConfig.partial();
1968
- }
1969
- });
1970
-
1971
- // ../contracts/src/sms-keywords.ts
1972
- var init_sms_keywords = __esm({
1973
- "../contracts/src/sms-keywords.ts"() {
1896
+ // ../contracts/src/flow-enrichment.ts
1897
+ function planEnrichment(graph) {
1898
+ const ids = graph.nodes.map((n) => n.id);
1899
+ const indeg = new Map(ids.map((id) => [id, 0]));
1900
+ const adj = new Map(ids.map((id) => [id, []]));
1901
+ for (const edge of graph.edges) {
1902
+ if (!adj.has(edge.source) || !indeg.has(edge.target)) continue;
1903
+ adj.get(edge.source).push(edge.target);
1904
+ indeg.set(edge.target, (indeg.get(edge.target) ?? 0) + 1);
1974
1905
  }
1975
- });
1976
- var init_call_recording = __esm({
1977
- "../contracts/src/call-recording.ts"() {
1978
- z.object({
1979
- // When false, the call-event sink skips egress entirely — no recording is made.
1980
- recordingEnabled: z.boolean().default(true),
1981
- // Optional consent preamble the agent speaks first on every call. null/empty =
1982
- // no announcement. Capped so it stays a short notice, not a monologue.
1983
- consentAnnouncement: z.string().max(1e3).nullable().default(null)
1984
- });
1985
- z.object({
1986
- recordingEnabled: z.boolean().optional(),
1987
- consentAnnouncement: z.string().max(1e3).nullable().optional()
1988
- });
1906
+ const queue = ids.filter((id) => (indeg.get(id) ?? 0) === 0);
1907
+ const order = [];
1908
+ while (queue.length > 0) {
1909
+ const id = queue.shift();
1910
+ order.push(id);
1911
+ for (const next of adj.get(id) ?? []) {
1912
+ const left = (indeg.get(next) ?? 0) - 1;
1913
+ indeg.set(next, left);
1914
+ if (left === 0) queue.push(next);
1915
+ }
1989
1916
  }
1990
- });
1991
- var OutboundAmdMode;
1992
- var init_outbound = __esm({
1993
- "../contracts/src/outbound.ts"() {
1994
- OutboundAmdMode = z.enum(["off", "detect", "detectMessageEnd"]);
1995
- z.object({
1996
- // Callee in E.164 (e.g. +14155551234). call-router re-validates strictly.
1997
- to: z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164, e.g. +14155551234"),
1998
- // Caller-id phone number (must belong to the calling project).
1999
- phoneNumberId: z.string().min(1),
2000
- // Optional agent override; falls back to the number's bound agent.
2001
- agentId: z.string().min(1).optional(),
2002
- // Template/context variables forwarded to the agent as dispatch metadata.
2003
- variables: z.record(z.string(), z.string()).optional(),
2004
- // Per-call AMD override; takes precedence over the agent's stored outbound.amd.
2005
- amd: OutboundAmdMode.optional()
2006
- });
1917
+ return { order, acyclic: order.length === ids.length };
1918
+ }
1919
+ var init_flow_enrichment = __esm({
1920
+ "../contracts/src/flow-enrichment.ts"() {
2007
1921
  }
2008
1922
  });
2009
- var ConnectionAuthType, ConnectionStatus;
2010
- var init_connection = __esm({
2011
- "../contracts/src/connection.ts"() {
2012
- init_providers();
2013
- ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
2014
- ConnectionStatus = z.enum(["active", "revoked", "error"]);
2015
- z.object({
2016
- v: z.number().int(),
2017
- // Key id (short fingerprint) of the key that encrypted this envelope — lets the
2018
- // cipher pick the right key from its ring so CREDENTIAL_ENCRYPTION_KEY can rotate
2019
- // in-band (Tier-1 item 4). Optional/absent on legacy v1 envelopes, which decrypt
2020
- // with the designated legacy key.
2021
- kid: z.string().optional(),
2022
- iv: z.string(),
2023
- ct: z.string(),
2024
- tag: z.string()
2025
- });
2026
- z.object({
2027
- id: z.string().uuid(),
2028
- projectId: z.string().min(1),
2029
- name: z.string().min(1).max(128),
2030
- // Open string — customers name providers freely (e.g. 'google_calendar',
2031
- // 'salesforce', 'n8n', 'http').
2032
- provider: z.string().min(1).max(120),
2033
- authType: ConnectionAuthType,
2034
- status: ConnectionStatus,
2035
- // Non-secret metadata (scopes, account label, base URL, …).
2036
- metadata: z.record(z.unknown()),
2037
- expiresAt: z.string().datetime().nullable(),
2038
- createdAt: z.string().datetime(),
2039
- updatedAt: z.string().datetime()
2040
- });
2041
- z.object({
2042
- name: z.string().trim().min(1).max(128),
2043
- provider: z.string().trim().min(1).max(120),
2044
- authType: ConnectionAuthType,
2045
- // The secret material to encrypt at rest. Never stored or returned in plaintext.
2046
- secret: z.string().min(1).max(8192),
2047
- metadata: z.record(z.unknown()).optional(),
2048
- expiresAt: z.string().datetime().optional()
2049
- });
2050
- z.object({
2051
- name: z.string().trim().min(1).max(128).optional(),
2052
- // New secret material — re-encrypted at rest; the old envelope is replaced.
2053
- secret: z.string().min(1).max(8192).optional(),
2054
- // Merged over the existing metadata (top-level keys win).
2055
- metadata: z.record(z.unknown()).optional()
2056
- }).refine((v) => v.name !== void 0 || v.secret !== void 0 || v.metadata !== void 0, {
2057
- message: "nothing to update"
2058
- });
2059
- byokProviders().map((p) => ({
2060
- id: p.id,
2061
- label: p.label,
2062
- keyPlaceholder: p.keyPlaceholder ?? "",
2063
- supportsBaseUrl: p.supportsBaseUrl
2064
- }));
1923
+
1924
+ // ../contracts/src/flow-compile.ts
1925
+ function compileConversationFlow(graph, id = "flow") {
1926
+ const fields = [];
1927
+ const prompts = [];
1928
+ const triggers = [];
1929
+ let gateRequired = null;
1930
+ for (const node of graph.nodes) {
1931
+ const data = node.data;
1932
+ switch (node.type) {
1933
+ case "ask": {
1934
+ const name = typeof data.slot === "string" ? data.slot : node.id;
1935
+ const required = data.required !== false;
1936
+ fields.push({
1937
+ name,
1938
+ required,
1939
+ ...typeof data.ask === "string" ? { prompt: data.ask } : {}
1940
+ });
1941
+ break;
1942
+ }
1943
+ case "say":
1944
+ case "confirm": {
1945
+ if (typeof data.text === "string") prompts.push({ id: node.id, text: data.text });
1946
+ break;
1947
+ }
1948
+ case "trigger": {
1949
+ const on = typeof data.on === "string" ? data.on : "";
1950
+ const then = typeof data.then === "string" ? data.then : "handoff";
1951
+ if (on) triggers.push({ on, then });
1952
+ break;
1953
+ }
1954
+ case "gate": {
1955
+ if (Array.isArray(data.requiredFields)) {
1956
+ gateRequired = data.requiredFields.filter((x) => typeof x === "string");
1957
+ }
1958
+ break;
1959
+ }
1960
+ }
2065
1961
  }
2066
- });
2067
- var E164, RoomPrefix, RecordingPrefix, Cidr, TelephonySettings, ProfileSource, EffectiveCallProfile;
2068
- var init_telephony = __esm({
2069
- "../contracts/src/telephony.ts"() {
2070
- E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
2071
- RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
2072
- RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
2073
- Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
2074
- TelephonySettings = z.object({
2075
- outboundTrunkId: z.string().min(1).max(128).nullable().default(null),
2076
- defaultFromE164: E164.nullable().default(null),
2077
- roomPrefix: RoomPrefix.nullable().default(null),
2078
- sipAllowedCidrs: z.array(Cidr).max(32).nullable().default(null),
2079
- recordingPrefix: RecordingPrefix.nullable().default(null),
2080
- strictByok: z.boolean().default(false)
2081
- });
2082
- z.object({
2083
- outboundTrunkId: z.string().min(1).max(128).nullable().optional(),
2084
- defaultFromE164: E164.nullable().optional(),
2085
- roomPrefix: RoomPrefix.nullable().optional(),
2086
- sipAllowedCidrs: z.array(Cidr).max(32).nullable().optional(),
2087
- recordingPrefix: RecordingPrefix.nullable().optional(),
2088
- strictByok: z.boolean().optional()
2089
- }).refine((v) => Object.keys(v).length > 0, { message: "nothing to update" });
2090
- ProfileSource = z.enum(["org", "platform", "none"]);
2091
- EffectiveCallProfile = z.object({
2092
- projectId: z.string(),
2093
- outboundTrunkId: z.string().nullable(),
2094
- defaultFromE164: z.string().nullable(),
2095
- roomPrefix: z.string(),
2096
- sipAllowedCidrs: z.array(z.string()),
2097
- recordingPrefix: z.string(),
2098
- strictByok: z.boolean(),
2099
- sources: z.object({
2100
- outboundTrunkId: ProfileSource,
2101
- defaultFromE164: ProfileSource,
2102
- roomPrefix: ProfileSource,
2103
- sipAllowedCidrs: ProfileSource,
2104
- recordingPrefix: ProfileSource
2105
- })
1962
+ const requiredFields = gateRequired ?? fields.filter((f) => f.required).map((f) => f.name);
1963
+ return { id, fields, completionGate: { requiredFields }, triggers, prompts };
1964
+ }
1965
+ function asFieldType(value) {
1966
+ return typeof value === "string" && FIELD_TYPES.includes(value) ? value : "string";
1967
+ }
1968
+ function asHistoryPolicy(value) {
1969
+ return typeof value === "string" && HISTORY_POLICIES.has(value) ? value : "full";
1970
+ }
1971
+ function asAdherence(value) {
1972
+ return typeof value === "string" && ADHERENCE_MODES.has(value) ? value : "off";
1973
+ }
1974
+ function branchesFor(data) {
1975
+ return Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
1976
+ id: b.id,
1977
+ label: b.label,
1978
+ ...typeof b.when === "string" && b.when.trim() ? { when: b.when } : {},
1979
+ ...typeof b.digit === "string" && b.digit.trim() ? { digit: b.digit.trim() } : {},
1980
+ ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
1981
+ })) : [];
1982
+ }
1983
+ function nodeConfigFor(node) {
1984
+ const data = node.data;
1985
+ switch (node.type) {
1986
+ case "start":
1987
+ return {};
1988
+ case "say":
1989
+ case "confirm":
1990
+ return { text: typeof data.text === "string" ? data.text : "" };
1991
+ case "ask":
1992
+ return {
1993
+ slot: typeof data.slot === "string" ? data.slot : node.id,
1994
+ required: data.required !== false,
1995
+ type: asFieldType(data.type),
1996
+ historyPolicy: asHistoryPolicy(data.historyPolicy),
1997
+ ...typeof data.ask === "string" ? { ask: data.ask } : {},
1998
+ ...typeof data.pattern === "string" ? { pattern: data.pattern } : {},
1999
+ ...typeof data.min === "number" ? { min: data.min } : {},
2000
+ ...typeof data.max === "number" ? { max: data.max } : {},
2001
+ ...Array.isArray(data.enum) ? { enum: data.enum.filter((x) => typeof x === "string") } : {}
2002
+ };
2003
+ case "llm":
2004
+ return {
2005
+ instructions: typeof data.instructions === "string" ? data.instructions : typeof data.text === "string" ? data.text : "",
2006
+ // Chat-hub valve: a resident conversational node (entry/return point)
2007
+ // that routes to task subgraphs on its conditional edges and absorbs
2008
+ // off-script turns in place instead of dead-ending. See the interpreter.
2009
+ hub: data.hub === true,
2010
+ historyPolicy: asHistoryPolicy(data.historyPolicy),
2011
+ // Drift-gate (supervisor-lite): verify the reply still localizes to this
2012
+ // node. enforce → reject-and-regenerate, then speak `fallback` verbatim.
2013
+ adherence: asAdherence(data.adherence),
2014
+ ...typeof data.fallback === "string" ? { fallback: data.fallback } : {}
2015
+ };
2016
+ case "tool": {
2017
+ const url = typeof data.url === "string" ? data.url : "";
2018
+ const rawName = typeof data.name === "string" ? data.name : "";
2019
+ const name = /^[a-z][a-z0-9_]*$/.test(rawName) ? rawName : `tool_${node.id.replace(/[^a-z0-9]/gi, "").slice(0, 16).toLowerCase() || "fn"}`;
2020
+ const input = {};
2021
+ if (data.input && typeof data.input === "object") {
2022
+ for (const [key, val] of Object.entries(data.input)) {
2023
+ if (val === "string" || val === "number" || val === "boolean") input[key] = val;
2024
+ }
2025
+ }
2026
+ const headers = {};
2027
+ if (data.headers && typeof data.headers === "object") {
2028
+ for (const [key, val] of Object.entries(data.headers)) {
2029
+ if (typeof val === "string") headers[key] = val;
2030
+ }
2031
+ }
2032
+ return {
2033
+ name,
2034
+ description: typeof data.description === "string" && data.description ? data.description : name,
2035
+ input,
2036
+ url,
2037
+ method: HTTP_METHODS.has(data.method) ? data.method : "POST",
2038
+ ...Object.keys(headers).length ? { headers } : {},
2039
+ auth: data.auth === "connection" ? "connection" : "none",
2040
+ ...typeof data.connectionRef === "string" ? { connectionRef: data.connectionRef } : {},
2041
+ // Registry reference: run the project HTTP tool with this NAME via the
2042
+ // server-side proxy instead of the inline url/method/auth above.
2043
+ ...typeof data.toolRef === "string" && data.toolRef ? { toolRef: data.toolRef } : {},
2044
+ // Promote result fields into NAMED slots on success:
2045
+ // { slotName: "data.path" }. Later nodes then read {{ slotName }}
2046
+ // directly instead of {{ tool.<nodeId>.data.path }} via a set node.
2047
+ ...(() => {
2048
+ if (!data.outputAs || typeof data.outputAs !== "object") return {};
2049
+ const outputAs = {};
2050
+ for (const [slot, path] of Object.entries(data.outputAs)) {
2051
+ if (typeof path === "string" && path.trim() && /^[a-zA-Z_][\w.]*$/.test(slot)) {
2052
+ outputAs[slot] = path.trim();
2053
+ }
2054
+ }
2055
+ return Object.keys(outputAs).length ? { outputAs } : {};
2056
+ })()
2057
+ };
2058
+ }
2059
+ case "gate":
2060
+ return {
2061
+ requiredFields: Array.isArray(data.requiredFields) ? data.requiredFields.filter((x) => typeof x === "string") : []
2062
+ };
2063
+ case "handoff":
2064
+ return {
2065
+ ...typeof data.to === "string" ? { to: data.to } : {},
2066
+ ...typeof data.fallback === "string" ? { fallback: data.fallback } : {},
2067
+ ...typeof data.on === "string" ? { on: data.on } : {},
2068
+ // Per-node warm (conference brief) / cold (blind REFER) override.
2069
+ ...data.mode === "warm" || data.mode === "cold" ? { mode: data.mode } : {},
2070
+ // What the joining human HEARS (warm mode speaks it verbatim once they
2071
+ // answer). {{ }}-interpolated at runtime against the slot scope.
2072
+ ...typeof data.briefing === "string" && data.briefing.trim() ? { briefing: data.briefing } : {}
2073
+ };
2074
+ case "end":
2075
+ return {
2076
+ ...typeof data.on === "string" ? { on: data.on } : {},
2077
+ ...typeof data.voicemail === "boolean" ? { voicemail: data.voicemail } : {},
2078
+ // Optional closing line spoken before hang-up (interpolated at runtime).
2079
+ ...typeof data.endMessage === "string" ? { endMessage: data.endMessage } : {}
2080
+ };
2081
+ case "trigger":
2082
+ return {
2083
+ on: typeof data.on === "string" ? data.on : "",
2084
+ then: typeof data.then === "string" ? data.then : "handoff",
2085
+ ...typeof data.onlyWhile === "string" ? { onlyWhile: data.onlyWhile } : {}
2086
+ };
2087
+ case "decision":
2088
+ return {
2089
+ ...typeof data.condition === "string" ? { condition: data.condition } : {},
2090
+ branches: branchesFor(data)
2091
+ };
2092
+ case "playbook":
2093
+ return {
2094
+ ...typeof data.goal === "string" ? { goal: data.goal } : {},
2095
+ ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2096
+ // Optional reference to a reusable library playbook. playbookId is the
2097
+ // rename-safe key (pinned at deploy); playbookRef (name) remains for
2098
+ // display + legacy live resolution.
2099
+ ...typeof data.playbookRef === "string" ? { playbookRef: data.playbookRef } : {},
2100
+ ...typeof data.playbookId === "string" && data.playbookId ? { playbookId: data.playbookId } : {},
2101
+ requiredFields: Array.isArray(data.requiredFields) ? data.requiredFields.filter((x) => typeof x === "string") : [],
2102
+ // Agent loop: scoped registry-tool names + explicit opt-in + model knobs.
2103
+ // With tools (or agentLoop:true) the runtime swaps the plain conversational
2104
+ // turn for the LLM-with-tools runner and the structured exit tool.
2105
+ ...Array.isArray(data.tools) ? { tools: data.tools.filter((x) => typeof x === "string" && x.length > 0) } : {},
2106
+ ...data.agentLoop === true ? { agentLoop: true } : {},
2107
+ ...typeof data.model === "string" && data.model ? { model: data.model } : {},
2108
+ ...typeof data.temperature === "number" ? { temperature: data.temperature } : {},
2109
+ // Spoken close when the turn budget exhausts with required fields missing
2110
+ // and no `incomplete` port is wired (the gate never silently advances).
2111
+ ...typeof data.incompleteMessage === "string" && data.incompleteMessage ? { incompleteMessage: data.incompleteMessage } : {},
2112
+ // Turn policy: agent-generated speech is interruptible by default;
2113
+ // false locks it (verbatim lines are always locked regardless).
2114
+ ...data.interruptible === false ? { interruptible: false } : {},
2115
+ branches: branchesFor(data)
2116
+ };
2117
+ case "menu":
2118
+ return {
2119
+ ...typeof data.prompt === "string" ? { prompt: data.prompt } : {},
2120
+ branches: branchesFor(data)
2121
+ };
2122
+ case "classify":
2123
+ return {
2124
+ ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2125
+ branches: branchesFor(data)
2126
+ };
2127
+ case "router":
2128
+ return {
2129
+ hub: true,
2130
+ ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2131
+ branches: Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2132
+ id: b.id,
2133
+ label: b.label,
2134
+ ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
2135
+ })) : []
2136
+ };
2137
+ case "rag":
2138
+ return {
2139
+ ...typeof data.query === "string" ? { query: data.query } : {},
2140
+ ...typeof data.topK === "number" ? { topK: data.topK } : {}
2141
+ };
2142
+ case "code":
2143
+ return {
2144
+ ...typeof data.expression === "string" ? { expression: data.expression } : {},
2145
+ ...typeof data.description === "string" ? { description: data.description } : {}
2146
+ };
2147
+ case "set":
2148
+ return {
2149
+ assignments: Array.isArray(data.assignments) ? data.assignments.filter((a) => a && typeof a.name === "string" && a.name.trim() !== "").map((a) => ({
2150
+ name: a.name,
2151
+ value: typeof a.value === "string" ? a.value : ""
2152
+ })) : []
2153
+ };
2154
+ case "subflow":
2155
+ return {
2156
+ ...typeof data.flowRef === "string" ? { flowRef: data.flowRef } : {}
2157
+ };
2158
+ default:
2159
+ return typeof data.label === "string" ? { label: data.label } : {};
2160
+ }
2161
+ }
2162
+ function compileFlowToProgram(graph, id = "flow") {
2163
+ const stepNodes = graph.nodes.filter(
2164
+ (n) => !CAPABILITY_NODE_TYPES.has(n.type) && !ANNOTATION_NODE_TYPES.has(n.type)
2165
+ );
2166
+ const byId = new Map(stepNodes.map((n) => [n.id, n]));
2167
+ const indeg = new Map(stepNodes.map((n) => [n.id, 0]));
2168
+ const outBySource = /* @__PURE__ */ new Map();
2169
+ const droppedEdges = [];
2170
+ const labelOnlyEdges = [];
2171
+ for (const edge of graph.edges) {
2172
+ const source = byId.get(edge.source);
2173
+ if (!source || !byId.has(edge.target)) {
2174
+ droppedEdges.push(edge.id);
2175
+ continue;
2176
+ }
2177
+ if (isLabelOnlyEdge(edge, source.type)) labelOnlyEdges.push(edge.id);
2178
+ const list = outBySource.get(edge.source) ?? [];
2179
+ list.push({
2180
+ to: edge.target,
2181
+ ...edge.condition ? { condition: edge.condition } : {},
2182
+ ...edge.sourceHandle ? { sourceHandle: edge.sourceHandle } : {},
2183
+ ...edge.label ? { label: edge.label } : {}
2184
+ });
2185
+ outBySource.set(edge.source, list);
2186
+ indeg.set(edge.target, (indeg.get(edge.target) ?? 0) + 1);
2187
+ }
2188
+ const nodes = {};
2189
+ const unsupportedNodes = [];
2190
+ for (const node of stepNodes) {
2191
+ nodes[node.id] = {
2192
+ id: node.id,
2193
+ type: node.type,
2194
+ config: nodeConfigFor(node),
2195
+ // Defensive: the FlowProgram schema caps out[] at 64 — slice rather than
2196
+ // letting an over-wired node fail the read-side parse (which silently
2197
+ // degrades the whole call to the generic base prompt).
2198
+ out: (outBySource.get(node.id) ?? []).slice(0, 64)
2199
+ };
2200
+ if (UNSUPPORTED_NODE_TYPES.has(node.type)) unsupportedNodes.push(node.id);
2201
+ }
2202
+ const startNodes = stepNodes.filter((n) => n.type === "start").map((n) => n.id);
2203
+ let entry2 = startNodes.length ? startNodes : stepNodes.filter((n) => (indeg.get(n.id) ?? 0) === 0).map((n) => n.id);
2204
+ if (entry2.length === 0 && stepNodes.length > 0) entry2 = [stepNodes[0].id];
2205
+ const { acyclic } = planEnrichment(graph);
2206
+ const { completionGate } = compileConversationFlow(graph, id);
2207
+ return {
2208
+ v: 1,
2209
+ id: id.slice(0, 64),
2210
+ entry: entry2.slice(0, 64),
2211
+ nodes,
2212
+ completionGate: { requiredFields: completionGate.requiredFields.slice(0, 64) },
2213
+ // Diagnostics arrays are schema-capped at 256 — slice so a pathological
2214
+ // graph can't fail the read-side parse.
2215
+ diagnostics: {
2216
+ acyclic,
2217
+ droppedEdges: droppedEdges.slice(0, 256),
2218
+ unsupportedNodes: unsupportedNodes.slice(0, 256),
2219
+ labelOnlyEdges: labelOnlyEdges.slice(0, 256)
2220
+ }
2221
+ };
2222
+ }
2223
+ var FIELD_TYPES, HTTP_METHODS, HISTORY_POLICIES, ADHERENCE_MODES, UNSUPPORTED_NODE_TYPES, CAPABILITY_NODE_TYPES, ANNOTATION_NODE_TYPES;
2224
+ var init_flow_compile = __esm({
2225
+ "../contracts/src/flow-compile.ts"() {
2226
+ init_flow_enrichment();
2227
+ init_flow_edges();
2228
+ FIELD_TYPES = [
2229
+ "string",
2230
+ "string[]",
2231
+ "number",
2232
+ "boolean",
2233
+ "datetime",
2234
+ "email",
2235
+ "phone"
2236
+ ];
2237
+ HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
2238
+ HISTORY_POLICIES = /* @__PURE__ */ new Set(["full", "last-turn", "slots-only"]);
2239
+ ADHERENCE_MODES = /* @__PURE__ */ new Set(["off", "observe", "enforce"]);
2240
+ UNSUPPORTED_NODE_TYPES = /* @__PURE__ */ new Set([]);
2241
+ CAPABILITY_NODE_TYPES = /* @__PURE__ */ new Set(["memory", "trigger", "speech"]);
2242
+ ANNOTATION_NODE_TYPES = /* @__PURE__ */ new Set(["note"]);
2243
+ }
2244
+ });
2245
+
2246
+ // ../contracts/src/flow-compile-check.ts
2247
+ var init_flow_compile_check = __esm({
2248
+ "../contracts/src/flow-compile-check.ts"() {
2249
+ init_agents();
2250
+ init_flow_compile();
2251
+ }
2252
+ });
2253
+ var FlowIssueSeverity, FlowIssue;
2254
+ var init_flow_validate = __esm({
2255
+ "../contracts/src/flow-validate.ts"() {
2256
+ init_flow_ports();
2257
+ init_flow_edges();
2258
+ init_flow_compile_check();
2259
+ init_flow_edges();
2260
+ FlowIssueSeverity = z.enum(["error", "warning"]);
2261
+ FlowIssue = z.object({
2262
+ code: z.string().max(64),
2263
+ severity: FlowIssueSeverity,
2264
+ message: z.string().max(500),
2265
+ nodeId: z.string().max(128).optional(),
2266
+ edgeId: z.string().max(128).optional()
2267
+ });
2268
+ z.object({
2269
+ valid: z.boolean(),
2270
+ issues: z.array(FlowIssue).max(256)
2271
+ });
2272
+ }
2273
+ });
2274
+
2275
+ // ../contracts/src/flow-builtins.ts
2276
+ var init_flow_builtins = __esm({
2277
+ "../contracts/src/flow-builtins.ts"() {
2278
+ }
2279
+ });
2280
+ var ComplianceRule, ComplianceDecision, ComplianceConfig;
2281
+ var init_compliance = __esm({
2282
+ "../contracts/src/compliance.ts"() {
2283
+ ComplianceRule = z.enum(["dnc", "consent", "quiet_hours", "frequency_cap"]);
2284
+ ComplianceDecision = z.enum(["allowed", "blocked"]);
2285
+ ComplianceConfig = z.object({
2286
+ // Local calling window for the CALLED party — TCPA default 8am–9pm.
2287
+ quietHoursStartHour: z.number().int().min(0).max(23).default(8),
2288
+ quietHoursEndHour: z.number().int().min(1).max(24).default(21),
2289
+ enforceDnc: z.boolean().default(true),
2290
+ enforceQuietHours: z.boolean().default(true),
2291
+ // Max dial attempts to one number per rolling 24h. 0 = unlimited.
2292
+ maxAttemptsPerDay: z.number().int().min(0).default(0),
2293
+ // Require a recorded consent flag before dialing.
2294
+ requireConsent: z.boolean().default(false),
2295
+ // Fallback IANA tz when the number's region can't be resolved.
2296
+ fallbackTimeZone: z.string().min(1).default("America/New_York")
2297
+ });
2298
+ z.object({
2299
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
2300
+ // ISO instant the check is evaluated at (defaults to now server-side).
2301
+ now: z.string().datetime().optional(),
2302
+ // Override the resolved timezone (else derived from the number's area code).
2303
+ timeZone: z.string().optional(),
2304
+ // Recent attempt count in the last 24h (for the frequency cap), if known.
2305
+ recentAttempts: z.number().int().min(0).optional(),
2306
+ // Whether consent is on file (for requireConsent).
2307
+ hasConsent: z.boolean().optional()
2308
+ });
2309
+ z.object({
2310
+ decision: ComplianceDecision,
2311
+ // The first rule that blocked, if any.
2312
+ rule: ComplianceRule.nullable(),
2313
+ reason: z.string().nullable()
2314
+ });
2315
+ z.object({
2316
+ id: z.string().uuid(),
2317
+ projectId: z.string().min(1),
2318
+ phoneE164: z.string(),
2319
+ reason: z.string().nullable(),
2320
+ source: z.string().nullable(),
2321
+ createdAt: z.string().datetime()
2106
2322
  });
2107
2323
  z.object({
2108
- settings: TelephonySettings,
2109
- profile: EffectiveCallProfile
2324
+ id: z.string().uuid(),
2325
+ projectId: z.string().min(1),
2326
+ phoneE164: z.string(),
2327
+ decision: ComplianceDecision,
2328
+ rule: ComplianceRule.nullable(),
2329
+ reason: z.string().nullable(),
2330
+ checkedAt: z.string().datetime()
2331
+ });
2332
+ z.object({
2333
+ phoneE164: z.string().regex(/^\+[1-9]\d{1,14}$/, "must be E.164"),
2334
+ reason: z.string().max(500).optional(),
2335
+ source: z.string().max(120).optional()
2110
2336
  });
2337
+ ComplianceConfig.partial();
2111
2338
  }
2112
2339
  });
2113
- var SHORTENER_HOSTS, PLACEHOLDER_HOSTS, PublicUrl, A2pBusinessType, A2pBusinessInfo, A2pOptInType, A2pUseCase, A2pMessagingProfile, A2pState;
2114
- var init_a2p = __esm({
2115
- "../contracts/src/a2p.ts"() {
2116
- SHORTENER_HOSTS = [
2117
- "bit.ly",
2118
- "tinyurl.com",
2119
- "t.co",
2120
- "goo.gl",
2121
- "ow.ly",
2122
- "is.gd",
2123
- "buff.ly",
2124
- "rebrand.ly",
2125
- "cutt.ly",
2126
- "shorturl.at",
2127
- "tiny.cc"
2128
- ];
2129
- PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
2130
- PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
2131
- try {
2132
- const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2133
- return !SHORTENER_HOSTS.includes(host);
2134
- } catch {
2135
- return false;
2136
- }
2137
- }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
2138
- try {
2139
- const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2140
- return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
2141
- } catch {
2142
- return false;
2143
- }
2144
- }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
2145
- A2pBusinessType = z.enum([
2146
- "Sole Proprietorship",
2147
- "Partnership",
2148
- "Corporation",
2149
- "Co-operative",
2150
- "Limited Liability Corporation",
2151
- "Non-profit Corporation"
2152
- ]);
2153
- A2pBusinessInfo = z.object({
2154
- legalName: z.string().trim().min(2).max(200),
2155
- /** EIN (US) or equivalent registration number. */
2156
- registrationNumber: z.string().trim().min(4).max(50),
2157
- businessType: A2pBusinessType,
2158
- /** Publicly reachable production site — not staging, not a 404. */
2159
- website: PublicUrl,
2160
- industry: z.string().trim().min(2).max(60),
2161
- address: z.object({
2162
- street: z.string().trim().min(2).max(200),
2163
- city: z.string().trim().min(1).max(100),
2164
- region: z.string().trim().min(1).max(100),
2165
- postalCode: z.string().trim().min(2).max(20),
2166
- isoCountry: z.string().trim().length(2)
2167
- }),
2168
- authorizedRep: z.object({
2169
- firstName: z.string().trim().min(1).max(100),
2170
- lastName: z.string().trim().min(1).max(100),
2171
- email: z.string().trim().email(),
2172
- phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
2173
- jobTitle: z.string().trim().min(2).max(100)
2174
- })
2175
- });
2176
- A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
2177
- A2pUseCase = z.enum([
2178
- "MIXED",
2179
- "CUSTOMER_CARE",
2180
- "MARKETING",
2181
- "ACCOUNT_NOTIFICATION",
2182
- "2FA",
2183
- "DELIVERY_NOTIFICATION",
2184
- "HIGHER_EDUCATION",
2185
- "POLLING_VOTING",
2186
- "PUBLIC_SERVICE_ANNOUNCEMENT",
2187
- "LOW_VOLUME"
2188
- ]);
2189
- A2pMessagingProfile = z.object({
2190
- useCase: A2pUseCase,
2191
- /** Specific, not generic. "We send texts" gets rejected; describe the actual
2192
- * messages and when they are sent. */
2193
- description: z.string().trim().min(40).max(4096),
2194
- /**
2195
- * The single most-rejected field. Must describe HOW people opt in, state the
2196
- * message frequency, include the "message and data rates may apply"
2197
- * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
2198
- * to 40–2049 characters.
2199
- */
2200
- messageFlow: z.string().trim().min(40).max(2049),
2201
- optInType: A2pOptInType,
2202
- /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
2203
- * these; anything behind a login fails. */
2204
- optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
2205
- /** Real messages the customer will send. Must reflect the declared use case
2206
- * and carry opt-out language. */
2207
- messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
2208
- privacyPolicyUrl: PublicUrl,
2209
- termsAndConditionsUrl: PublicUrl,
2210
- hasEmbeddedLinks: z.boolean().default(false),
2211
- hasEmbeddedPhone: z.boolean().default(false)
2340
+
2341
+ // ../contracts/src/sms-keywords.ts
2342
+ var init_sms_keywords = __esm({
2343
+ "../contracts/src/sms-keywords.ts"() {
2344
+ }
2345
+ });
2346
+ var init_call_recording = __esm({
2347
+ "../contracts/src/call-recording.ts"() {
2348
+ z.object({
2349
+ // When false, the call-event sink skips egress entirely — no recording is made.
2350
+ recordingEnabled: z.boolean().default(true),
2351
+ // Optional consent preamble the agent speaks first on every call. null/empty =
2352
+ // no announcement. Capped so it stays a short notice, not a monologue.
2353
+ consentAnnouncement: z.string().max(1e3).nullable().default(null)
2212
2354
  });
2213
2355
  z.object({
2214
- business: A2pBusinessInfo,
2215
- messaging: A2pMessagingProfile,
2216
- /** Register against Twilio's mock endpoints — exercises the full pipeline
2217
- * with no fees and no real carrier submission. Used in CI and staging. */
2218
- mock: z.boolean().default(false)
2219
- }).superRefine((v, ctx) => {
2220
- const flow = v.messaging.messageFlow.toLowerCase();
2221
- if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
2222
- ctx.addIssue({
2223
- code: z.ZodIssueCode.custom,
2224
- path: ["messaging", "messageFlow"],
2225
- message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
2226
- });
2227
- }
2228
- if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
2229
- ctx.addIssue({
2230
- code: z.ZodIssueCode.custom,
2231
- path: ["messaging", "messageFlow"],
2232
- message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
2233
- });
2234
- }
2235
- const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
2236
- if (!hasOptOut) {
2237
- ctx.addIssue({
2238
- code: z.ZodIssueCode.custom,
2239
- path: ["messaging", "messageSamples"],
2240
- message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
2241
- });
2242
- }
2356
+ recordingEnabled: z.boolean().optional(),
2357
+ consentAnnouncement: z.string().max(1e3).nullable().optional()
2243
2358
  });
2244
- A2pState = z.enum([
2245
- "none",
2246
- "profile_pending",
2247
- "profile_approved",
2248
- "profile_failed",
2249
- "brand_pending",
2250
- "brand_approved",
2251
- "brand_failed",
2252
- "campaign_pending",
2253
- "messaging_ready",
2254
- "campaign_failed"
2255
- ]);
2359
+ }
2360
+ });
2361
+ var OutboundAmdMode;
2362
+ var init_outbound = __esm({
2363
+ "../contracts/src/outbound.ts"() {
2364
+ OutboundAmdMode = z.enum(["off", "detect", "detectMessageEnd"]);
2256
2365
  z.object({
2257
- state: A2pState,
2258
- customerProfileSid: z.string().nullable(),
2259
- trustProductSid: z.string().nullable(),
2260
- brandSid: z.string().nullable(),
2261
- messagingServiceSid: z.string().nullable(),
2262
- campaignSid: z.string().nullable(),
2263
- mock: z.boolean(),
2264
- /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
2265
- * fix the specific field rather than guess. */
2266
- failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
2267
- /** Plain-language next step for the dashboard. */
2268
- nextAction: z.string().nullable(),
2269
- updatedAt: z.string().nullable()
2366
+ // Callee in E.164 (e.g. +14155551234). call-router re-validates strictly.
2367
+ to: z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164, e.g. +14155551234"),
2368
+ // Caller-id phone number (must belong to the calling project).
2369
+ phoneNumberId: z.string().min(1),
2370
+ // Optional agent override; falls back to the number's bound agent.
2371
+ agentId: z.string().min(1).optional(),
2372
+ // Template/context variables forwarded to the agent as dispatch metadata.
2373
+ variables: z.record(z.string(), z.string()).optional(),
2374
+ // Per-call AMD override; takes precedence over the agent's stored outbound.amd.
2375
+ amd: OutboundAmdMode.optional()
2270
2376
  });
2271
2377
  }
2272
2378
  });
2273
- var ConnectorMode, ConnectorStatus, ConnectorNormalize;
2274
- var init_connector = __esm({
2275
- "../contracts/src/connector.ts"() {
2276
- ConnectorMode = z.enum(["tunnel", "direct"]);
2277
- ConnectorStatus = z.enum(["online", "degraded", "offline"]);
2278
- ConnectorNormalize = z.enum(["auto", "on", "off"]);
2379
+ var ConnectionAuthType, ConnectionStatus;
2380
+ var init_connection = __esm({
2381
+ "../contracts/src/connection.ts"() {
2382
+ init_providers();
2383
+ ConnectionAuthType = z.enum(["oauth2", "api_key", "bearer", "basic", "none"]);
2384
+ ConnectionStatus = z.enum(["active", "revoked", "error"]);
2385
+ z.object({
2386
+ v: z.number().int(),
2387
+ // Key id (short fingerprint) of the key that encrypted this envelope — lets the
2388
+ // cipher pick the right key from its ring so CREDENTIAL_ENCRYPTION_KEY can rotate
2389
+ // in-band (Tier-1 item 4). Optional/absent on legacy v1 envelopes, which decrypt
2390
+ // with the designated legacy key.
2391
+ kid: z.string().optional(),
2392
+ iv: z.string(),
2393
+ ct: z.string(),
2394
+ tag: z.string()
2395
+ });
2279
2396
  z.object({
2280
2397
  id: z.string().uuid(),
2281
2398
  projectId: z.string().min(1),
2282
2399
  name: z.string().min(1).max(128),
2283
- mode: ConnectorMode,
2284
- // Tunnel mode: a short non-secret preview of the daemon token. Null for direct.
2285
- tokenPreview: z.string().nullable(),
2286
- // Direct mode: the connections-vault row holding the URL + key. Null for tunnel.
2287
- connectionId: z.string().uuid().nullable(),
2288
- // Upstream model id / alias. Null → the brain's own default.
2289
- defaultModel: z.string().nullable(),
2290
- normalize: ConnectorNormalize,
2291
- enabled: z.boolean(),
2292
- status: ConnectorStatus,
2293
- lastSeenAt: z.string().datetime().nullable(),
2400
+ // Open string — customers name providers freely (e.g. 'google_calendar',
2401
+ // 'salesforce', 'n8n', 'http').
2402
+ provider: z.string().min(1).max(120),
2403
+ authType: ConnectionAuthType,
2404
+ status: ConnectionStatus,
2405
+ // Non-secret metadata (scopes, account label, base URL, …).
2294
2406
  metadata: z.record(z.unknown()),
2407
+ expiresAt: z.string().datetime().nullable(),
2295
2408
  createdAt: z.string().datetime(),
2296
2409
  updatedAt: z.string().datetime()
2297
2410
  });
2298
2411
  z.object({
2299
2412
  name: z.string().trim().min(1).max(128),
2300
- mode: ConnectorMode,
2301
- // Required when mode === 'direct': the vault connection holding URL + key.
2302
- connectionId: z.string().uuid().optional(),
2303
- defaultModel: z.string().trim().min(1).max(200).optional(),
2304
- normalize: ConnectorNormalize.optional(),
2305
- metadata: z.record(z.unknown()).optional()
2413
+ provider: z.string().trim().min(1).max(120),
2414
+ authType: ConnectionAuthType,
2415
+ // The secret material to encrypt at rest. Never stored or returned in plaintext.
2416
+ secret: z.string().min(1).max(8192),
2417
+ metadata: z.record(z.unknown()).optional(),
2418
+ expiresAt: z.string().datetime().optional()
2306
2419
  });
2307
2420
  z.object({
2308
2421
  name: z.string().trim().min(1).max(128).optional(),
2309
- connectionId: z.string().uuid().nullable().optional(),
2310
- defaultModel: z.string().trim().min(1).max(200).nullable().optional(),
2311
- normalize: ConnectorNormalize.optional(),
2312
- enabled: z.boolean().optional()
2313
- });
2314
- }
2315
- });
2316
- var CONNECTOR_REQUEST_CHANNEL, CONNECTOR_REPLY_CHANNEL, BrainWireMessage, HelloFrame, PongFrame, RegisteredFrame, PingFrame, ShutdownFrame, BrainRequestFrame, BrainCancelFrame, BrainDeltaFrame, BrainDoneFrame, BrainErrorFrame, ConnectorUpFrame;
2317
- var init_connector_stream = __esm({
2318
- "../contracts/src/connector-stream.ts"() {
2319
- CONNECTOR_REQUEST_CHANNEL = (connectorId) => `brain-req:${connectorId}`;
2320
- CONNECTOR_REPLY_CHANNEL = (streamId) => `brain-reply:${streamId}`;
2321
- BrainWireMessage = z.object({
2322
- role: z.enum(["system", "user", "assistant"]),
2323
- content: z.string()
2324
- });
2325
- HelloFrame = z.object({
2326
- kind: z.literal("hello"),
2327
- token: z.string().min(1),
2328
- replicaId: z.string().min(1),
2329
- version: z.string().min(1),
2330
- caps: z.object({ streaming: z.boolean(), tools: z.boolean(), litellm: z.boolean() })
2331
- });
2332
- PongFrame = z.object({ kind: z.literal("pong"), ts: z.number().int() });
2333
- RegisteredFrame = z.object({
2334
- kind: z.literal("registered"),
2335
- connectorId: z.string().uuid(),
2336
- projectId: z.string().min(1)
2337
- });
2338
- PingFrame = z.object({ kind: z.literal("ping"), ts: z.number().int() });
2339
- ShutdownFrame = z.object({ kind: z.literal("shutdown"), reason: z.string().optional() });
2340
- BrainRequestFrame = z.object({
2341
- kind: z.literal("brain.request"),
2342
- streamId: z.string().min(1),
2343
- model: z.string().optional(),
2344
- temperature: z.number().optional(),
2345
- messages: z.array(BrainWireMessage)
2422
+ // New secret material — re-encrypted at rest; the old envelope is replaced.
2423
+ secret: z.string().min(1).max(8192).optional(),
2424
+ // Merged over the existing metadata (top-level keys win).
2425
+ metadata: z.record(z.unknown()).optional()
2426
+ }).refine((v) => v.name !== void 0 || v.secret !== void 0 || v.metadata !== void 0, {
2427
+ message: "nothing to update"
2346
2428
  });
2347
- BrainCancelFrame = z.object({
2348
- kind: z.literal("brain.cancel"),
2349
- streamId: z.string().min(1)
2429
+ byokProviders().map((p) => ({
2430
+ id: p.id,
2431
+ label: p.label,
2432
+ keyPlaceholder: p.keyPlaceholder ?? "",
2433
+ supportsBaseUrl: p.supportsBaseUrl
2434
+ }));
2435
+ z.object({
2436
+ id: z.string().min(1),
2437
+ label: z.string().min(1),
2438
+ /** The vault `provider` the resulting connection is stored under. */
2439
+ vaultProvider: z.string().min(1),
2440
+ category: z.enum(["model", "tool"])
2350
2441
  });
2351
- BrainDeltaFrame = z.object({
2352
- kind: z.literal("brain.delta"),
2353
- streamId: z.string().min(1),
2354
- content: z.string()
2442
+ z.object({
2443
+ authorizeUrl: z.string().url()
2355
2444
  });
2356
- BrainDoneFrame = z.object({
2357
- kind: z.literal("brain.done"),
2358
- streamId: z.string().min(1),
2359
- finishReason: z.string()
2445
+ z.object({
2446
+ state: z.string().min(1).max(512),
2447
+ code: z.string().min(1).max(4096)
2360
2448
  });
2361
- BrainErrorFrame = z.object({
2362
- kind: z.literal("brain.error"),
2363
- streamId: z.string().min(1),
2364
- code: z.enum([
2365
- "upstream_timeout",
2366
- "upstream_error",
2367
- "bad_response",
2368
- "normalize_failed",
2369
- "unreachable"
2370
- ]),
2371
- message: z.string()
2449
+ z.object({
2450
+ providerId: z.string().min(1),
2451
+ /** The provider's display name ("ChatGPT"). */
2452
+ providerLabel: z.string().min(1),
2453
+ /** What the connection is for — which group the dashboard files it under. */
2454
+ category: z.enum(["model", "tool"]),
2455
+ /** The account's stable id at the provider (OIDC `sub`); reconnecting the same account updates the same row. */
2456
+ subject: z.string().min(1).nullable(),
2457
+ email: z.string().nullable(),
2458
+ displayName: z.string().nullable(),
2459
+ scopes: z.array(z.string()),
2460
+ /**
2461
+ * The workspace user who signed in. A person's model plan (category 'model') serves only them — never teammates or
2462
+ * API keys. Absent on rows connected before it was recorded.
2463
+ */
2464
+ connectedBy: z.string().nullable().optional(),
2465
+ /** Set when a refresh was refused (e.g. `invalid_grant`): the owner must connect again. */
2466
+ reauthRequired: z.boolean().optional()
2372
2467
  });
2373
- ConnectorUpFrame = z.discriminatedUnion("kind", [
2374
- HelloFrame,
2375
- PongFrame,
2376
- BrainDeltaFrame,
2377
- BrainDoneFrame,
2378
- BrainErrorFrame
2379
- ]);
2380
- z.discriminatedUnion("kind", [
2381
- RegisteredFrame,
2382
- PingFrame,
2383
- ShutdownFrame,
2384
- BrainRequestFrame,
2385
- BrainCancelFrame
2386
- ]);
2387
- }
2388
- });
2389
-
2390
- // ../contracts/src/flow-enrichment.ts
2391
- function planEnrichment(graph) {
2392
- const ids = graph.nodes.map((n) => n.id);
2393
- const indeg = new Map(ids.map((id) => [id, 0]));
2394
- const adj = new Map(ids.map((id) => [id, []]));
2395
- for (const edge of graph.edges) {
2396
- if (!adj.has(edge.source) || !indeg.has(edge.target)) continue;
2397
- adj.get(edge.source).push(edge.target);
2398
- indeg.set(edge.target, (indeg.get(edge.target) ?? 0) + 1);
2399
- }
2400
- const queue = ids.filter((id) => (indeg.get(id) ?? 0) === 0);
2401
- const order = [];
2402
- while (queue.length > 0) {
2403
- const id = queue.shift();
2404
- order.push(id);
2405
- for (const next of adj.get(id) ?? []) {
2406
- const left = (indeg.get(next) ?? 0) - 1;
2407
- indeg.set(next, left);
2408
- if (left === 0) queue.push(next);
2409
- }
2410
- }
2411
- return { order, acyclic: order.length === ids.length };
2412
- }
2413
- var init_flow_enrichment = __esm({
2414
- "../contracts/src/flow-enrichment.ts"() {
2415
2468
  }
2416
2469
  });
2417
-
2418
- // ../contracts/src/flow-compile.ts
2419
- function compileConversationFlow(graph, id = "flow") {
2420
- const fields = [];
2421
- const prompts = [];
2422
- const triggers = [];
2423
- let gateRequired = null;
2424
- for (const node of graph.nodes) {
2425
- const data = node.data;
2426
- switch (node.type) {
2427
- case "ask": {
2428
- const name = typeof data.slot === "string" ? data.slot : node.id;
2429
- const required = data.required !== false;
2430
- fields.push({
2431
- name,
2432
- required,
2433
- ...typeof data.ask === "string" ? { prompt: data.ask } : {}
2434
- });
2435
- break;
2436
- }
2437
- case "say":
2438
- case "confirm": {
2439
- if (typeof data.text === "string") prompts.push({ id: node.id, text: data.text });
2440
- break;
2441
- }
2442
- case "trigger": {
2443
- const on = typeof data.on === "string" ? data.on : "";
2444
- const then = typeof data.then === "string" ? data.then : "handoff";
2445
- if (on) triggers.push({ on, then });
2446
- break;
2447
- }
2448
- case "gate": {
2449
- if (Array.isArray(data.requiredFields)) {
2450
- gateRequired = data.requiredFields.filter((x) => typeof x === "string");
2451
- }
2452
- break;
2453
- }
2454
- }
2455
- }
2456
- const requiredFields = gateRequired ?? fields.filter((f) => f.required).map((f) => f.name);
2457
- return { id, fields, completionGate: { requiredFields }, triggers, prompts };
2458
- }
2459
- function asFieldType(value) {
2460
- return typeof value === "string" && FIELD_TYPES.includes(value) ? value : "string";
2461
- }
2462
- function asHistoryPolicy(value) {
2463
- return typeof value === "string" && HISTORY_POLICIES.has(value) ? value : "full";
2464
- }
2465
- function asAdherence(value) {
2466
- return typeof value === "string" && ADHERENCE_MODES.has(value) ? value : "off";
2467
- }
2468
- function branchesFor(data) {
2469
- return Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2470
- id: b.id,
2471
- label: b.label,
2472
- ...typeof b.when === "string" && b.when.trim() ? { when: b.when } : {},
2473
- ...typeof b.digit === "string" && b.digit.trim() ? { digit: b.digit.trim() } : {},
2474
- ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
2475
- })) : [];
2476
- }
2477
- function nodeConfigFor(node) {
2478
- const data = node.data;
2479
- switch (node.type) {
2480
- case "start":
2481
- return {};
2482
- case "say":
2483
- case "confirm":
2484
- return { text: typeof data.text === "string" ? data.text : "" };
2485
- case "ask":
2486
- return {
2487
- slot: typeof data.slot === "string" ? data.slot : node.id,
2488
- required: data.required !== false,
2489
- type: asFieldType(data.type),
2490
- historyPolicy: asHistoryPolicy(data.historyPolicy),
2491
- ...typeof data.ask === "string" ? { ask: data.ask } : {},
2492
- ...typeof data.pattern === "string" ? { pattern: data.pattern } : {},
2493
- ...typeof data.min === "number" ? { min: data.min } : {},
2494
- ...typeof data.max === "number" ? { max: data.max } : {},
2495
- ...Array.isArray(data.enum) ? { enum: data.enum.filter((x) => typeof x === "string") } : {}
2496
- };
2497
- case "llm":
2498
- return {
2499
- instructions: typeof data.instructions === "string" ? data.instructions : typeof data.text === "string" ? data.text : "",
2500
- // Chat-hub valve: a resident conversational node (entry/return point)
2501
- // that routes to task subgraphs on its conditional edges and absorbs
2502
- // off-script turns in place instead of dead-ending. See the interpreter.
2503
- hub: data.hub === true,
2504
- historyPolicy: asHistoryPolicy(data.historyPolicy),
2505
- // Drift-gate (supervisor-lite): verify the reply still localizes to this
2506
- // node. enforce → reject-and-regenerate, then speak `fallback` verbatim.
2507
- adherence: asAdherence(data.adherence),
2508
- ...typeof data.fallback === "string" ? { fallback: data.fallback } : {}
2509
- };
2510
- case "tool": {
2511
- const url = typeof data.url === "string" ? data.url : "";
2512
- const rawName = typeof data.name === "string" ? data.name : "";
2513
- const name = /^[a-z][a-z0-9_]*$/.test(rawName) ? rawName : `tool_${node.id.replace(/[^a-z0-9]/gi, "").slice(0, 16).toLowerCase() || "fn"}`;
2514
- const input = {};
2515
- if (data.input && typeof data.input === "object") {
2516
- for (const [key, val] of Object.entries(data.input)) {
2517
- if (val === "string" || val === "number" || val === "boolean") input[key] = val;
2518
- }
2470
+ var E164, RoomPrefix, RecordingPrefix, Cidr, TelephonySettings, ProfileSource, EffectiveCallProfile;
2471
+ var init_telephony = __esm({
2472
+ "../contracts/src/telephony.ts"() {
2473
+ E164 = z.string().regex(/^\+[1-9]\d{6,14}$/, "must be E.164 (+15551234567)");
2474
+ RoomPrefix = z.string().regex(/^[a-z0-9][a-z0-9-]{0,30}[a-z0-9-]?$/, "lowercase letters, digits, dashes; max 32");
2475
+ RecordingPrefix = z.string().min(1).max(128).refine((v) => !v.startsWith("/") && !v.includes(".."), "relative key prefix only");
2476
+ Cidr = z.string().regex(/^\d{1,3}(\.\d{1,3}){3}\/\d{1,2}$/, "must be IPv4 CIDR (a.b.c.d/nn)");
2477
+ TelephonySettings = z.object({
2478
+ outboundTrunkId: z.string().min(1).max(128).nullable().default(null),
2479
+ defaultFromE164: E164.nullable().default(null),
2480
+ roomPrefix: RoomPrefix.nullable().default(null),
2481
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().default(null),
2482
+ recordingPrefix: RecordingPrefix.nullable().default(null),
2483
+ strictByok: z.boolean().default(false)
2484
+ });
2485
+ z.object({
2486
+ outboundTrunkId: z.string().min(1).max(128).nullable().optional(),
2487
+ defaultFromE164: E164.nullable().optional(),
2488
+ roomPrefix: RoomPrefix.nullable().optional(),
2489
+ sipAllowedCidrs: z.array(Cidr).max(32).nullable().optional(),
2490
+ recordingPrefix: RecordingPrefix.nullable().optional(),
2491
+ strictByok: z.boolean().optional()
2492
+ }).refine((v) => Object.keys(v).length > 0, { message: "nothing to update" });
2493
+ ProfileSource = z.enum(["org", "platform", "none"]);
2494
+ EffectiveCallProfile = z.object({
2495
+ projectId: z.string(),
2496
+ outboundTrunkId: z.string().nullable(),
2497
+ defaultFromE164: z.string().nullable(),
2498
+ roomPrefix: z.string(),
2499
+ sipAllowedCidrs: z.array(z.string()),
2500
+ recordingPrefix: z.string(),
2501
+ strictByok: z.boolean(),
2502
+ sources: z.object({
2503
+ outboundTrunkId: ProfileSource,
2504
+ defaultFromE164: ProfileSource,
2505
+ roomPrefix: ProfileSource,
2506
+ sipAllowedCidrs: ProfileSource,
2507
+ recordingPrefix: ProfileSource
2508
+ })
2509
+ });
2510
+ z.object({
2511
+ settings: TelephonySettings,
2512
+ profile: EffectiveCallProfile
2513
+ });
2514
+ }
2515
+ });
2516
+ var SHORTENER_HOSTS, PLACEHOLDER_HOSTS, PublicUrl, A2pBusinessType, A2pBusinessInfo, A2pOptInType, A2pUseCase, A2pMessagingProfile, A2pState;
2517
+ var init_a2p = __esm({
2518
+ "../contracts/src/a2p.ts"() {
2519
+ SHORTENER_HOSTS = [
2520
+ "bit.ly",
2521
+ "tinyurl.com",
2522
+ "t.co",
2523
+ "goo.gl",
2524
+ "ow.ly",
2525
+ "is.gd",
2526
+ "buff.ly",
2527
+ "rebrand.ly",
2528
+ "cutt.ly",
2529
+ "shorturl.at",
2530
+ "tiny.cc"
2531
+ ];
2532
+ PLACEHOLDER_HOSTS = ["acme.com", "example.com", "example.org", "test.com", "localhost"];
2533
+ PublicUrl = z.string().url().refine((v) => v.startsWith("https://"), "must be https").refine((v) => {
2534
+ try {
2535
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2536
+ return !SHORTENER_HOSTS.includes(host);
2537
+ } catch {
2538
+ return false;
2519
2539
  }
2520
- const headers = {};
2521
- if (data.headers && typeof data.headers === "object") {
2522
- for (const [key, val] of Object.entries(data.headers)) {
2523
- if (typeof val === "string") headers[key] = val;
2524
- }
2540
+ }, "public URL shorteners are rejected by carriers \u2014 use your own domain").refine((v) => {
2541
+ try {
2542
+ const host = new URL(v).hostname.toLowerCase().replace(/^www\./, "");
2543
+ return !PLACEHOLDER_HOSTS.some((p) => host === p || host.endsWith(`.${p}`));
2544
+ } catch {
2545
+ return false;
2525
2546
  }
2526
- return {
2527
- name,
2528
- description: typeof data.description === "string" && data.description ? data.description : name,
2529
- input,
2530
- url,
2531
- method: HTTP_METHODS.has(data.method) ? data.method : "POST",
2532
- ...Object.keys(headers).length ? { headers } : {},
2533
- auth: data.auth === "connection" ? "connection" : "none",
2534
- ...typeof data.connectionRef === "string" ? { connectionRef: data.connectionRef } : {},
2535
- // Registry reference: run the project HTTP tool with this NAME via the
2536
- // server-side proxy instead of the inline url/method/auth above.
2537
- ...typeof data.toolRef === "string" && data.toolRef ? { toolRef: data.toolRef } : {},
2538
- // Promote result fields into NAMED slots on success:
2539
- // { slotName: "data.path" }. Later nodes then read {{ slotName }}
2540
- // directly instead of {{ tool.<nodeId>.data.path }} via a set node.
2541
- ...(() => {
2542
- if (!data.outputAs || typeof data.outputAs !== "object") return {};
2543
- const outputAs = {};
2544
- for (const [slot, path] of Object.entries(data.outputAs)) {
2545
- if (typeof path === "string" && path.trim() && /^[a-zA-Z_][\w.]*$/.test(slot)) {
2546
- outputAs[slot] = path.trim();
2547
- }
2548
- }
2549
- return Object.keys(outputAs).length ? { outputAs } : {};
2550
- })()
2551
- };
2552
- }
2553
- case "gate":
2554
- return {
2555
- requiredFields: Array.isArray(data.requiredFields) ? data.requiredFields.filter((x) => typeof x === "string") : []
2556
- };
2557
- case "handoff":
2558
- return {
2559
- ...typeof data.to === "string" ? { to: data.to } : {},
2560
- ...typeof data.fallback === "string" ? { fallback: data.fallback } : {},
2561
- ...typeof data.on === "string" ? { on: data.on } : {},
2562
- // Per-node warm (conference brief) / cold (blind REFER) override.
2563
- ...data.mode === "warm" || data.mode === "cold" ? { mode: data.mode } : {},
2564
- // What the joining human HEARS (warm mode speaks it verbatim once they
2565
- // answer). {{ }}-interpolated at runtime against the slot scope.
2566
- ...typeof data.briefing === "string" && data.briefing.trim() ? { briefing: data.briefing } : {}
2567
- };
2568
- case "end":
2569
- return {
2570
- ...typeof data.on === "string" ? { on: data.on } : {},
2571
- ...typeof data.voicemail === "boolean" ? { voicemail: data.voicemail } : {},
2572
- // Optional closing line spoken before hang-up (interpolated at runtime).
2573
- ...typeof data.endMessage === "string" ? { endMessage: data.endMessage } : {}
2574
- };
2575
- case "trigger":
2576
- return {
2577
- on: typeof data.on === "string" ? data.on : "",
2578
- then: typeof data.then === "string" ? data.then : "handoff",
2579
- ...typeof data.onlyWhile === "string" ? { onlyWhile: data.onlyWhile } : {}
2580
- };
2581
- case "decision":
2582
- return {
2583
- ...typeof data.condition === "string" ? { condition: data.condition } : {},
2584
- branches: branchesFor(data)
2585
- };
2586
- case "playbook":
2587
- return {
2588
- ...typeof data.goal === "string" ? { goal: data.goal } : {},
2589
- ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2590
- // Optional reference to a reusable library playbook. playbookId is the
2591
- // rename-safe key (pinned at deploy); playbookRef (name) remains for
2592
- // display + legacy live resolution.
2593
- ...typeof data.playbookRef === "string" ? { playbookRef: data.playbookRef } : {},
2594
- ...typeof data.playbookId === "string" && data.playbookId ? { playbookId: data.playbookId } : {},
2595
- requiredFields: Array.isArray(data.requiredFields) ? data.requiredFields.filter((x) => typeof x === "string") : [],
2596
- // Agent loop: scoped registry-tool names + explicit opt-in + model knobs.
2597
- // With tools (or agentLoop:true) the runtime swaps the plain conversational
2598
- // turn for the LLM-with-tools runner and the structured exit tool.
2599
- ...Array.isArray(data.tools) ? { tools: data.tools.filter((x) => typeof x === "string" && x.length > 0) } : {},
2600
- ...data.agentLoop === true ? { agentLoop: true } : {},
2601
- ...typeof data.model === "string" && data.model ? { model: data.model } : {},
2602
- ...typeof data.temperature === "number" ? { temperature: data.temperature } : {},
2603
- // Spoken close when the turn budget exhausts with required fields missing
2604
- // and no `incomplete` port is wired (the gate never silently advances).
2605
- ...typeof data.incompleteMessage === "string" && data.incompleteMessage ? { incompleteMessage: data.incompleteMessage } : {},
2606
- // Turn policy: agent-generated speech is interruptible by default;
2607
- // false locks it (verbatim lines are always locked regardless).
2608
- ...data.interruptible === false ? { interruptible: false } : {},
2609
- branches: branchesFor(data)
2610
- };
2611
- case "menu":
2612
- return {
2613
- ...typeof data.prompt === "string" ? { prompt: data.prompt } : {},
2614
- branches: branchesFor(data)
2615
- };
2616
- case "classify":
2617
- return {
2618
- ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2619
- branches: branchesFor(data)
2620
- };
2621
- case "router":
2622
- return {
2623
- hub: true,
2624
- ...typeof data.instructions === "string" ? { instructions: data.instructions } : {},
2625
- branches: Array.isArray(data.branches) ? data.branches.filter((b) => b && typeof b.id === "string" && typeof b.label === "string").map((b) => ({
2626
- id: b.id,
2627
- label: b.label,
2628
- ...typeof b.description === "string" && b.description.trim() ? { description: b.description } : {}
2629
- })) : []
2630
- };
2631
- case "rag":
2632
- return {
2633
- ...typeof data.query === "string" ? { query: data.query } : {},
2634
- ...typeof data.topK === "number" ? { topK: data.topK } : {}
2635
- };
2636
- case "code":
2637
- return {
2638
- ...typeof data.expression === "string" ? { expression: data.expression } : {},
2639
- ...typeof data.description === "string" ? { description: data.description } : {}
2640
- };
2641
- case "set":
2642
- return {
2643
- assignments: Array.isArray(data.assignments) ? data.assignments.filter((a) => a && typeof a.name === "string" && a.name.trim() !== "").map((a) => ({
2644
- name: a.name,
2645
- value: typeof a.value === "string" ? a.value : ""
2646
- })) : []
2647
- };
2648
- case "subflow":
2649
- return {
2650
- ...typeof data.flowRef === "string" ? { flowRef: data.flowRef } : {}
2651
- };
2652
- default:
2653
- return typeof data.label === "string" ? { label: data.label } : {};
2547
+ }, "placeholder domain \u2014 reviewers will follow this link and reject the campaign");
2548
+ A2pBusinessType = z.enum([
2549
+ "Sole Proprietorship",
2550
+ "Partnership",
2551
+ "Corporation",
2552
+ "Co-operative",
2553
+ "Limited Liability Corporation",
2554
+ "Non-profit Corporation"
2555
+ ]);
2556
+ A2pBusinessInfo = z.object({
2557
+ legalName: z.string().trim().min(2).max(200),
2558
+ /** EIN (US) or equivalent registration number. */
2559
+ registrationNumber: z.string().trim().min(4).max(50),
2560
+ businessType: A2pBusinessType,
2561
+ /** Publicly reachable production site — not staging, not a 404. */
2562
+ website: PublicUrl,
2563
+ industry: z.string().trim().min(2).max(60),
2564
+ address: z.object({
2565
+ street: z.string().trim().min(2).max(200),
2566
+ city: z.string().trim().min(1).max(100),
2567
+ region: z.string().trim().min(1).max(100),
2568
+ postalCode: z.string().trim().min(2).max(20),
2569
+ isoCountry: z.string().trim().length(2)
2570
+ }),
2571
+ authorizedRep: z.object({
2572
+ firstName: z.string().trim().min(1).max(100),
2573
+ lastName: z.string().trim().min(1).max(100),
2574
+ email: z.string().trim().email(),
2575
+ phone: z.string().trim().regex(/^\+[1-9]\d{6,14}$/, "must be E.164"),
2576
+ jobTitle: z.string().trim().min(2).max(100)
2577
+ })
2578
+ });
2579
+ A2pOptInType = z.enum(["WEB_FORM", "PAPER_FORM", "VERBAL", "VIA_TEXT", "MOBILE_QR_CODE"]);
2580
+ A2pUseCase = z.enum([
2581
+ "MIXED",
2582
+ "CUSTOMER_CARE",
2583
+ "MARKETING",
2584
+ "ACCOUNT_NOTIFICATION",
2585
+ "2FA",
2586
+ "DELIVERY_NOTIFICATION",
2587
+ "HIGHER_EDUCATION",
2588
+ "POLLING_VOTING",
2589
+ "PUBLIC_SERVICE_ANNOUNCEMENT",
2590
+ "LOW_VOLUME"
2591
+ ]);
2592
+ A2pMessagingProfile = z.object({
2593
+ useCase: A2pUseCase,
2594
+ /** Specific, not generic. "We send texts" gets rejected; describe the actual
2595
+ * messages and when they are sent. */
2596
+ description: z.string().trim().min(40).max(4096),
2597
+ /**
2598
+ * The single most-rejected field. Must describe HOW people opt in, state the
2599
+ * message frequency, include the "message and data rates may apply"
2600
+ * disclosure, and link to publicly reachable evidence. Twilio's API bounds it
2601
+ * to 40–2049 characters.
2602
+ */
2603
+ messageFlow: z.string().trim().min(40).max(2049),
2604
+ optInType: A2pOptInType,
2605
+ /** Publicly accessible screenshots/pages showing the opt-in. Reviewers open
2606
+ * these; anything behind a login fails. */
2607
+ optInEvidenceUrls: z.array(PublicUrl).min(1).max(5),
2608
+ /** Real messages the customer will send. Must reflect the declared use case
2609
+ * and carry opt-out language. */
2610
+ messageSamples: z.array(z.string().trim().min(10).max(1024)).min(2).max(5),
2611
+ privacyPolicyUrl: PublicUrl,
2612
+ termsAndConditionsUrl: PublicUrl,
2613
+ hasEmbeddedLinks: z.boolean().default(false),
2614
+ hasEmbeddedPhone: z.boolean().default(false)
2615
+ });
2616
+ z.object({
2617
+ business: A2pBusinessInfo,
2618
+ messaging: A2pMessagingProfile,
2619
+ /** Register against Twilio's mock endpoints — exercises the full pipeline
2620
+ * with no fees and no real carrier submission. Used in CI and staging. */
2621
+ mock: z.boolean().default(false)
2622
+ }).superRefine((v, ctx) => {
2623
+ const flow = v.messaging.messageFlow.toLowerCase();
2624
+ if (!/(msg|message)\s*(&|and)\s*data rates/.test(flow)) {
2625
+ ctx.addIssue({
2626
+ code: z.ZodIssueCode.custom,
2627
+ path: ["messaging", "messageFlow"],
2628
+ message: 'must include a "Message and data rates may apply" disclosure \u2014 carriers reject without it'
2629
+ });
2630
+ }
2631
+ if (!/\d/.test(flow) || !/(msg|message|text)/.test(flow)) {
2632
+ ctx.addIssue({
2633
+ code: z.ZodIssueCode.custom,
2634
+ path: ["messaging", "messageFlow"],
2635
+ message: 'must state message frequency, e.g. "Up to 4 msgs/month"'
2636
+ });
2637
+ }
2638
+ const hasOptOut = v.messaging.messageSamples.some((s) => /stop/i.test(s));
2639
+ if (!hasOptOut) {
2640
+ ctx.addIssue({
2641
+ code: z.ZodIssueCode.custom,
2642
+ path: ["messaging", "messageSamples"],
2643
+ message: 'at least one sample must include opt-out language (e.g. "Reply STOP to opt out")'
2644
+ });
2645
+ }
2646
+ });
2647
+ A2pState = z.enum([
2648
+ "none",
2649
+ "profile_pending",
2650
+ "profile_approved",
2651
+ "profile_failed",
2652
+ "brand_pending",
2653
+ "brand_approved",
2654
+ "brand_failed",
2655
+ "campaign_pending",
2656
+ "messaging_ready",
2657
+ "campaign_failed"
2658
+ ]);
2659
+ z.object({
2660
+ state: A2pState,
2661
+ customerProfileSid: z.string().nullable(),
2662
+ trustProductSid: z.string().nullable(),
2663
+ brandSid: z.string().nullable(),
2664
+ messagingServiceSid: z.string().nullable(),
2665
+ campaignSid: z.string().nullable(),
2666
+ mock: z.boolean(),
2667
+ /** Carrier/Twilio rejection details, surfaced verbatim so the customer can
2668
+ * fix the specific field rather than guess. */
2669
+ failures: z.array(z.object({ code: z.number().nullable(), field: z.string().nullable(), message: z.string() })).default([]),
2670
+ /** Plain-language next step for the dashboard. */
2671
+ nextAction: z.string().nullable(),
2672
+ updatedAt: z.string().nullable()
2673
+ });
2654
2674
  }
2655
- }
2656
- function compileFlowToProgram(graph, id = "flow") {
2657
- const stepNodes = graph.nodes.filter(
2658
- (n) => !CAPABILITY_NODE_TYPES.has(n.type) && !ANNOTATION_NODE_TYPES.has(n.type)
2659
- );
2660
- const byId = new Map(stepNodes.map((n) => [n.id, n]));
2661
- const indeg = new Map(stepNodes.map((n) => [n.id, 0]));
2662
- const outBySource = /* @__PURE__ */ new Map();
2663
- const droppedEdges = [];
2664
- const labelOnlyEdges = [];
2665
- for (const edge of graph.edges) {
2666
- const source = byId.get(edge.source);
2667
- if (!source || !byId.has(edge.target)) {
2668
- droppedEdges.push(edge.id);
2669
- continue;
2670
- }
2671
- if (isLabelOnlyEdge(edge, source.type)) labelOnlyEdges.push(edge.id);
2672
- const list = outBySource.get(edge.source) ?? [];
2673
- list.push({
2674
- to: edge.target,
2675
- ...edge.condition ? { condition: edge.condition } : {},
2676
- ...edge.sourceHandle ? { sourceHandle: edge.sourceHandle } : {},
2677
- ...edge.label ? { label: edge.label } : {}
2675
+ });
2676
+ var ConnectorMode, ConnectorStatus, ConnectorNormalize;
2677
+ var init_connector = __esm({
2678
+ "../contracts/src/connector.ts"() {
2679
+ ConnectorMode = z.enum(["tunnel", "direct"]);
2680
+ ConnectorStatus = z.enum(["online", "degraded", "offline"]);
2681
+ ConnectorNormalize = z.enum(["auto", "on", "off"]);
2682
+ z.object({
2683
+ id: z.string().uuid(),
2684
+ projectId: z.string().min(1),
2685
+ name: z.string().min(1).max(128),
2686
+ mode: ConnectorMode,
2687
+ // Tunnel mode: a short non-secret preview of the daemon token. Null for direct.
2688
+ tokenPreview: z.string().nullable(),
2689
+ // Direct mode: the connections-vault row holding the URL + key. Null for tunnel.
2690
+ connectionId: z.string().uuid().nullable(),
2691
+ // Upstream model id / alias. Null → the brain's own default.
2692
+ defaultModel: z.string().nullable(),
2693
+ normalize: ConnectorNormalize,
2694
+ enabled: z.boolean(),
2695
+ status: ConnectorStatus,
2696
+ lastSeenAt: z.string().datetime().nullable(),
2697
+ metadata: z.record(z.unknown()),
2698
+ createdAt: z.string().datetime(),
2699
+ updatedAt: z.string().datetime()
2700
+ });
2701
+ z.object({
2702
+ name: z.string().trim().min(1).max(128),
2703
+ mode: ConnectorMode,
2704
+ // Required when mode === 'direct': the vault connection holding URL + key.
2705
+ connectionId: z.string().uuid().optional(),
2706
+ defaultModel: z.string().trim().min(1).max(200).optional(),
2707
+ normalize: ConnectorNormalize.optional(),
2708
+ metadata: z.record(z.unknown()).optional()
2709
+ });
2710
+ z.object({
2711
+ name: z.string().trim().min(1).max(128).optional(),
2712
+ connectionId: z.string().uuid().nullable().optional(),
2713
+ defaultModel: z.string().trim().min(1).max(200).nullable().optional(),
2714
+ normalize: ConnectorNormalize.optional(),
2715
+ enabled: z.boolean().optional()
2678
2716
  });
2679
- outBySource.set(edge.source, list);
2680
- indeg.set(edge.target, (indeg.get(edge.target) ?? 0) + 1);
2681
2717
  }
2682
- const nodes = {};
2683
- const unsupportedNodes = [];
2684
- for (const node of stepNodes) {
2685
- nodes[node.id] = {
2686
- id: node.id,
2687
- type: node.type,
2688
- config: nodeConfigFor(node),
2689
- // Defensive: the FlowProgram schema caps out[] at 64 — slice rather than
2690
- // letting an over-wired node fail the read-side parse (which silently
2691
- // degrades the whole call to the generic base prompt).
2692
- out: (outBySource.get(node.id) ?? []).slice(0, 64)
2693
- };
2694
- if (UNSUPPORTED_NODE_TYPES.has(node.type)) unsupportedNodes.push(node.id);
2718
+ });
2719
+ var CONNECTOR_REQUEST_CHANNEL, CONNECTOR_REPLY_CHANNEL, BrainWireMessage, HelloFrame, PongFrame, RegisteredFrame, PingFrame, ShutdownFrame, BrainRequestFrame, BrainCancelFrame, BrainDeltaFrame, BrainDoneFrame, BrainErrorFrame, ConnectorUpFrame;
2720
+ var init_connector_stream = __esm({
2721
+ "../contracts/src/connector-stream.ts"() {
2722
+ CONNECTOR_REQUEST_CHANNEL = (connectorId) => `brain-req:${connectorId}`;
2723
+ CONNECTOR_REPLY_CHANNEL = (streamId) => `brain-reply:${streamId}`;
2724
+ BrainWireMessage = z.object({
2725
+ role: z.enum(["system", "user", "assistant"]),
2726
+ content: z.string()
2727
+ });
2728
+ HelloFrame = z.object({
2729
+ kind: z.literal("hello"),
2730
+ token: z.string().min(1),
2731
+ replicaId: z.string().min(1),
2732
+ version: z.string().min(1),
2733
+ caps: z.object({ streaming: z.boolean(), tools: z.boolean(), litellm: z.boolean() })
2734
+ });
2735
+ PongFrame = z.object({ kind: z.literal("pong"), ts: z.number().int() });
2736
+ RegisteredFrame = z.object({
2737
+ kind: z.literal("registered"),
2738
+ connectorId: z.string().uuid(),
2739
+ projectId: z.string().min(1)
2740
+ });
2741
+ PingFrame = z.object({ kind: z.literal("ping"), ts: z.number().int() });
2742
+ ShutdownFrame = z.object({ kind: z.literal("shutdown"), reason: z.string().optional() });
2743
+ BrainRequestFrame = z.object({
2744
+ kind: z.literal("brain.request"),
2745
+ streamId: z.string().min(1),
2746
+ model: z.string().optional(),
2747
+ temperature: z.number().optional(),
2748
+ messages: z.array(BrainWireMessage)
2749
+ });
2750
+ BrainCancelFrame = z.object({
2751
+ kind: z.literal("brain.cancel"),
2752
+ streamId: z.string().min(1)
2753
+ });
2754
+ BrainDeltaFrame = z.object({
2755
+ kind: z.literal("brain.delta"),
2756
+ streamId: z.string().min(1),
2757
+ content: z.string()
2758
+ });
2759
+ BrainDoneFrame = z.object({
2760
+ kind: z.literal("brain.done"),
2761
+ streamId: z.string().min(1),
2762
+ finishReason: z.string()
2763
+ });
2764
+ BrainErrorFrame = z.object({
2765
+ kind: z.literal("brain.error"),
2766
+ streamId: z.string().min(1),
2767
+ code: z.enum([
2768
+ "upstream_timeout",
2769
+ "upstream_error",
2770
+ "bad_response",
2771
+ "normalize_failed",
2772
+ "unreachable"
2773
+ ]),
2774
+ message: z.string()
2775
+ });
2776
+ ConnectorUpFrame = z.discriminatedUnion("kind", [
2777
+ HelloFrame,
2778
+ PongFrame,
2779
+ BrainDeltaFrame,
2780
+ BrainDoneFrame,
2781
+ BrainErrorFrame
2782
+ ]);
2783
+ z.discriminatedUnion("kind", [
2784
+ RegisteredFrame,
2785
+ PingFrame,
2786
+ ShutdownFrame,
2787
+ BrainRequestFrame,
2788
+ BrainCancelFrame
2789
+ ]);
2695
2790
  }
2696
- const startNodes = stepNodes.filter((n) => n.type === "start").map((n) => n.id);
2697
- let entry2 = startNodes.length ? startNodes : stepNodes.filter((n) => (indeg.get(n.id) ?? 0) === 0).map((n) => n.id);
2698
- if (entry2.length === 0 && stepNodes.length > 0) entry2 = [stepNodes[0].id];
2699
- const { acyclic } = planEnrichment(graph);
2700
- const { completionGate } = compileConversationFlow(graph, id);
2701
- return {
2702
- v: 1,
2703
- id: id.slice(0, 64),
2704
- entry: entry2.slice(0, 64),
2705
- nodes,
2706
- completionGate: { requiredFields: completionGate.requiredFields.slice(0, 64) },
2707
- // Diagnostics arrays are schema-capped at 256 — slice so a pathological
2708
- // graph can't fail the read-side parse.
2709
- diagnostics: {
2710
- acyclic,
2711
- droppedEdges: droppedEdges.slice(0, 256),
2712
- unsupportedNodes: unsupportedNodes.slice(0, 256),
2713
- labelOnlyEdges: labelOnlyEdges.slice(0, 256)
2714
- }
2715
- };
2716
- }
2717
- var FIELD_TYPES, HTTP_METHODS, HISTORY_POLICIES, ADHERENCE_MODES, UNSUPPORTED_NODE_TYPES, CAPABILITY_NODE_TYPES, ANNOTATION_NODE_TYPES;
2718
- var init_flow_compile = __esm({
2719
- "../contracts/src/flow-compile.ts"() {
2720
- init_flow_enrichment();
2721
- init_flow_validate();
2722
- FIELD_TYPES = [
2723
- "string",
2724
- "string[]",
2725
- "number",
2726
- "boolean",
2727
- "datetime",
2728
- "email",
2729
- "phone"
2730
- ];
2731
- HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
2732
- HISTORY_POLICIES = /* @__PURE__ */ new Set(["full", "last-turn", "slots-only"]);
2733
- ADHERENCE_MODES = /* @__PURE__ */ new Set(["off", "observe", "enforce"]);
2734
- UNSUPPORTED_NODE_TYPES = /* @__PURE__ */ new Set([]);
2735
- CAPABILITY_NODE_TYPES = /* @__PURE__ */ new Set(["memory", "trigger", "speech"]);
2736
- ANNOTATION_NODE_TYPES = /* @__PURE__ */ new Set(["note"]);
2791
+ });
2792
+ var FLOW_BOOT_FAILURES;
2793
+ var init_call_end_reason = __esm({
2794
+ "../contracts/src/call-end-reason.ts"() {
2795
+ FLOW_BOOT_FAILURES = ["schema_fetch_failed", "no_process_schema", "missing_agent_id", "no_api_key"];
2796
+ z.enum(
2797
+ FLOW_BOOT_FAILURES.map((cause) => `flow_boot:${cause}`)
2798
+ );
2737
2799
  }
2738
2800
  });
2739
2801
  var EnvironmentSpec;
@@ -3972,6 +4034,8 @@ var init_src = __esm({
3972
4034
  init_connector();
3973
4035
  init_connector_stream();
3974
4036
  init_flow_compile();
4037
+ init_flow_compile_check();
4038
+ init_call_end_reason();
3975
4039
  init_flow_enrichment();
3976
4040
  init_environments();
3977
4041
  init_variables();
@@ -9823,7 +9887,8 @@ var CallsClient = class {
9823
9887
  body: {
9824
9888
  ...input.attributes !== void 0 ? { attributes: input.attributes } : {},
9825
9889
  ...input.participants !== void 0 ? { participants: input.participants } : {},
9826
- ...input.legs !== void 0 ? { legs: input.legs } : {}
9890
+ ...input.legs !== void 0 ? { legs: input.legs } : {},
9891
+ ...input.endReason !== void 0 ? { endReason: input.endReason } : {}
9827
9892
  }
9828
9893
  },
9829
9894
  CallStateSyncResponse
@@ -14433,6 +14498,7 @@ function guardIsActive(opts) {
14433
14498
  }
14434
14499
 
14435
14500
  // src/runtime/call-sync.ts
14501
+ var END_REASON_WAIT_MS = 2e3;
14436
14502
  function makeNoopHandle(callId) {
14437
14503
  return {
14438
14504
  callId,
@@ -14441,6 +14507,8 @@ function makeNoopHandle(callId) {
14441
14507
  syncState() {
14442
14508
  },
14443
14509
  publishDtmf() {
14510
+ },
14511
+ async recordEndReason() {
14444
14512
  }
14445
14513
  };
14446
14514
  }
@@ -14481,6 +14549,24 @@ async function createCallSyncHandle(init) {
14481
14549
  });
14482
14550
  });
14483
14551
  },
14552
+ async recordEndReason(reason) {
14553
+ let timer;
14554
+ try {
14555
+ await Promise.race([
14556
+ init.client.calls.syncState(callId, { endReason: reason }),
14557
+ new Promise((resolve) => {
14558
+ timer = setTimeout(resolve, END_REASON_WAIT_MS);
14559
+ })
14560
+ ]);
14561
+ } catch (err) {
14562
+ console.warn("[sdk.call-sync] end reason not recorded", {
14563
+ reason,
14564
+ err: err instanceof Error ? err.message : err
14565
+ });
14566
+ } finally {
14567
+ clearTimeout(timer);
14568
+ }
14569
+ },
14484
14570
  publishDtmf(input) {
14485
14571
  void init.client.calls.appendDtmfEvent(callId, input).catch((err) => {
14486
14572
  console.warn("[sdk.call-sync] dtmf publish failed", {
@@ -15942,6 +16028,7 @@ var Agent = class {
15942
16028
  break;
15943
16029
  } catch (err) {
15944
16030
  fetchError = err;
16031
+ if (err instanceof VoiceLayerValidationError) break;
15945
16032
  await new Promise((resolve) => setTimeout(resolve, 500));
15946
16033
  }
15947
16034
  }
@@ -16756,6 +16843,7 @@ ${callIntent}`
16756
16843
  }
16757
16844
  }
16758
16845
  } else if (flowBootFailure) {
16846
+ const reported = callSync.recordEndReason(`flow_boot:${flowBootFailure}`);
16759
16847
  const apology = this.config.flowRuntime?.bootFailureMessage ?? "Sorry \u2014 we're having a technical issue starting this call. Please try again in a few minutes. Goodbye.";
16760
16848
  try {
16761
16849
  await adapter.generateReply({
@@ -16766,6 +16854,7 @@ ${apology}`
16766
16854
  } catch (err) {
16767
16855
  console.warn("[agent] flow boot apology failed to play", err);
16768
16856
  }
16857
+ await reported;
16769
16858
  await endCallController.hangUp({ trigger: "flow", reason: `flow_boot:${flowBootFailure}` });
16770
16859
  } else if (this.config.onCall) {
16771
16860
  await this.config.onCall(ctx);