@absolutejs/voice 0.0.22-beta.513 → 0.0.22-beta.515

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
@@ -50589,6 +50589,930 @@ var detectVoiceQualityDrift = (input) => {
50589
50589
  scope: { from: baselineFrom, to: cutoff }
50590
50590
  };
50591
50591
  };
50592
+ // src/pathway.ts
50593
+ var slotRefsInActions = (actions) => {
50594
+ if (!actions)
50595
+ return [];
50596
+ const refs = [];
50597
+ for (const action of actions) {
50598
+ if (action.kind === "collect-slot")
50599
+ refs.push(action.slotId);
50600
+ if (action.kind === "set-slot")
50601
+ refs.push(action.slotId);
50602
+ if (action.kind === "call-tool" && action.argsFromSlots) {
50603
+ refs.push(...action.argsFromSlots);
50604
+ }
50605
+ }
50606
+ return refs;
50607
+ };
50608
+ var slotRefsInCondition = (condition) => {
50609
+ if (condition.kind === "always" || condition.kind === "fallback")
50610
+ return [];
50611
+ return [condition.slotId];
50612
+ };
50613
+ var validateVoicePathway = (pathway) => {
50614
+ const issues = [];
50615
+ const stateIds = new Set;
50616
+ for (const state of pathway.states) {
50617
+ if (stateIds.has(state.id)) {
50618
+ issues.push({
50619
+ code: "duplicate-state",
50620
+ message: `Duplicate state id: ${state.id}`,
50621
+ severity: "error",
50622
+ stateId: state.id
50623
+ });
50624
+ }
50625
+ stateIds.add(state.id);
50626
+ }
50627
+ const slotIds = new Set;
50628
+ for (const slot of pathway.slots) {
50629
+ if (slotIds.has(slot.id)) {
50630
+ issues.push({
50631
+ code: "duplicate-slot",
50632
+ message: `Duplicate slot id: ${slot.id}`,
50633
+ severity: "error",
50634
+ slotId: slot.id
50635
+ });
50636
+ }
50637
+ slotIds.add(slot.id);
50638
+ }
50639
+ if (!stateIds.has(pathway.entryStateId)) {
50640
+ issues.push({
50641
+ code: "unknown-entry",
50642
+ message: `Entry state ${pathway.entryStateId} is not defined`,
50643
+ severity: "error"
50644
+ });
50645
+ }
50646
+ for (const state of pathway.states) {
50647
+ const seenTransitionKeys = new Set;
50648
+ state.transitions.forEach((transition, index) => {
50649
+ if (!stateIds.has(transition.to)) {
50650
+ issues.push({
50651
+ code: "missing-transition-target",
50652
+ message: `State ${state.id} transitions to unknown state ${transition.to}`,
50653
+ severity: "error",
50654
+ stateId: state.id
50655
+ });
50656
+ }
50657
+ const key = `${transition.to}::${transition.condition.kind}::${"slotId" in transition.condition ? transition.condition.slotId : ""}`;
50658
+ if (seenTransitionKeys.has(key)) {
50659
+ issues.push({
50660
+ code: "duplicate-transition",
50661
+ message: `State ${state.id} has duplicate transition to ${transition.to}`,
50662
+ severity: "warning",
50663
+ stateId: state.id
50664
+ });
50665
+ }
50666
+ seenTransitionKeys.add(key);
50667
+ if (transition.condition.kind === "fallback" && index !== state.transitions.length - 1) {
50668
+ issues.push({
50669
+ code: "fallback-not-last",
50670
+ message: `Fallback transition in ${state.id} must be the last transition`,
50671
+ severity: "error",
50672
+ stateId: state.id
50673
+ });
50674
+ }
50675
+ for (const ref of slotRefsInCondition(transition.condition)) {
50676
+ if (!slotIds.has(ref)) {
50677
+ issues.push({
50678
+ code: "missing-slot-ref",
50679
+ message: `Transition condition references unknown slot ${ref}`,
50680
+ severity: "error",
50681
+ slotId: ref,
50682
+ stateId: state.id
50683
+ });
50684
+ }
50685
+ }
50686
+ });
50687
+ for (const ref of slotRefsInActions(state.actions)) {
50688
+ if (!slotIds.has(ref)) {
50689
+ issues.push({
50690
+ code: "missing-slot-ref",
50691
+ message: `Action in ${state.id} references unknown slot ${ref}`,
50692
+ severity: "error",
50693
+ slotId: ref,
50694
+ stateId: state.id
50695
+ });
50696
+ }
50697
+ }
50698
+ }
50699
+ const reachable = new Set;
50700
+ const queue = stateIds.has(pathway.entryStateId) ? [pathway.entryStateId] : [];
50701
+ const stateById = new Map(pathway.states.map((s) => [s.id, s]));
50702
+ while (queue.length > 0) {
50703
+ const id = queue.shift();
50704
+ if (reachable.has(id))
50705
+ continue;
50706
+ reachable.add(id);
50707
+ const state = stateById.get(id);
50708
+ if (!state)
50709
+ continue;
50710
+ for (const transition of state.transitions) {
50711
+ if (stateIds.has(transition.to) && !reachable.has(transition.to)) {
50712
+ queue.push(transition.to);
50713
+ }
50714
+ }
50715
+ }
50716
+ for (const state of pathway.states) {
50717
+ if (!reachable.has(state.id)) {
50718
+ issues.push({
50719
+ code: "unreachable-state",
50720
+ message: `State ${state.id} is not reachable from entry ${pathway.entryStateId}`,
50721
+ severity: "warning",
50722
+ stateId: state.id
50723
+ });
50724
+ }
50725
+ }
50726
+ const hasReachableTerminal = pathway.states.some((state) => reachable.has(state.id) && (state.kind === "terminal" || state.transitions.length === 0));
50727
+ if (!hasReachableTerminal) {
50728
+ issues.push({
50729
+ code: "no-terminal-reachable",
50730
+ message: "No terminal state is reachable; pathway has no exit condition",
50731
+ severity: "error"
50732
+ });
50733
+ }
50734
+ const fatal = issues.some((i) => i.severity === "error");
50735
+ return {
50736
+ issues,
50737
+ reachableStates: Array.from(reachable),
50738
+ valid: !fatal
50739
+ };
50740
+ };
50741
+ var findVoicePathwayState = (pathway, id) => pathway.states.find((s) => s.id === id) ?? null;
50742
+ var findVoicePathwaySlot = (pathway, id) => pathway.slots.find((s) => s.id === id) ?? null;
50743
+ // src/pathwayRuntime.ts
50744
+ var evaluateCondition = (condition, slots) => {
50745
+ switch (condition.kind) {
50746
+ case "always":
50747
+ return true;
50748
+ case "fallback":
50749
+ return true;
50750
+ case "slot-filled":
50751
+ return slots[condition.slotId] !== undefined && slots[condition.slotId] !== null && slots[condition.slotId] !== "";
50752
+ case "slot-equals":
50753
+ return slots[condition.slotId] === condition.value;
50754
+ case "slot-matches": {
50755
+ const raw = slots[condition.slotId];
50756
+ if (typeof raw !== "string")
50757
+ return false;
50758
+ try {
50759
+ return new RegExp(condition.pattern, "u").test(raw);
50760
+ } catch {
50761
+ return false;
50762
+ }
50763
+ }
50764
+ }
50765
+ };
50766
+ var pickTransition = (state, slots) => {
50767
+ for (const transition of state.transitions) {
50768
+ if (transition.condition.kind === "fallback")
50769
+ continue;
50770
+ if (evaluateCondition(transition.condition, slots))
50771
+ return transition;
50772
+ }
50773
+ const fallback = state.transitions.find((t) => t.condition.kind === "fallback");
50774
+ return fallback ?? null;
50775
+ };
50776
+ var buildPendingActions = (state, slots, pathway, emit2) => {
50777
+ const pending = [];
50778
+ let awaitingSlotId = null;
50779
+ for (const action of state.actions ?? []) {
50780
+ if (action.kind === "say") {
50781
+ emit2({ text: action.text, type: "say" });
50782
+ continue;
50783
+ }
50784
+ if (action.kind === "collect-slot") {
50785
+ if (slots[action.slotId] === undefined || slots[action.slotId] === null) {
50786
+ const slot = findVoicePathwaySlot(pathway, action.slotId);
50787
+ emit2({
50788
+ prompt: slot?.prompt ?? `Please provide ${action.slotId}.`,
50789
+ slotId: action.slotId,
50790
+ type: "ask-slot"
50791
+ });
50792
+ awaitingSlotId = action.slotId;
50793
+ break;
50794
+ }
50795
+ continue;
50796
+ }
50797
+ if (action.kind === "call-tool") {
50798
+ const args = {};
50799
+ for (const id of action.argsFromSlots ?? []) {
50800
+ args[id] = slots[id] ?? null;
50801
+ }
50802
+ emit2({ call: { args, toolId: action.toolId }, type: "tool-call" });
50803
+ continue;
50804
+ }
50805
+ if (action.kind === "set-slot") {
50806
+ pending.push(action);
50807
+ continue;
50808
+ }
50809
+ if (action.kind === "transfer") {
50810
+ emit2({ destination: action.destination, type: "transfer" });
50811
+ return { actions: pending, awaitingSlotId: null, ended: true, reason: `transfer:${action.destination}` };
50812
+ }
50813
+ if (action.kind === "end-call") {
50814
+ emit2({ ...action.reason !== undefined ? { reason: action.reason } : {}, type: "end-call" });
50815
+ return { actions: pending, awaitingSlotId: null, ended: true, ...action.reason !== undefined ? { reason: action.reason } : {} };
50816
+ }
50817
+ }
50818
+ return { actions: pending, awaitingSlotId, ended: false };
50819
+ };
50820
+ var createVoicePathwayRuntime = (options) => {
50821
+ if (!options.skipValidation) {
50822
+ const report = validateVoicePathway(options.pathway);
50823
+ if (!report.valid) {
50824
+ throw new Error(`Invalid pathway: ${report.issues.filter((i) => i.severity === "error").map((i) => i.message).join("; ")}`);
50825
+ }
50826
+ }
50827
+ const now = options.now ?? (() => Date.now());
50828
+ const listeners = new Set;
50829
+ const emit2 = (event) => {
50830
+ for (const l of listeners)
50831
+ l(event);
50832
+ };
50833
+ let state = {
50834
+ awaitingSlotId: null,
50835
+ currentStateId: options.pathway.entryStateId,
50836
+ history: [],
50837
+ pendingActions: [],
50838
+ slots: { ...options.initialSlots ?? {} },
50839
+ status: "ready"
50840
+ };
50841
+ const enter = (stateId) => {
50842
+ const target = findVoicePathwayState(options.pathway, stateId);
50843
+ if (!target) {
50844
+ state = { ...state, lastError: `Unknown state ${stateId}`, status: "errored" };
50845
+ emit2({ message: state.lastError, type: "errored" });
50846
+ return;
50847
+ }
50848
+ state = {
50849
+ ...state,
50850
+ currentStateId: stateId,
50851
+ history: [...state.history, { at: now(), stateId }],
50852
+ pendingActions: []
50853
+ };
50854
+ emit2({ stateId, type: "state-entered" });
50855
+ const result = buildPendingActions(target, state.slots, options.pathway, emit2);
50856
+ state = {
50857
+ ...state,
50858
+ awaitingSlotId: result.awaitingSlotId,
50859
+ pendingActions: result.actions,
50860
+ status: result.ended ? "ended" : result.awaitingSlotId ? "awaiting-slot" : "branching",
50861
+ ...result.reason !== undefined ? { endedReason: result.reason } : {}
50862
+ };
50863
+ if (state.status === "branching")
50864
+ tryTransition();
50865
+ };
50866
+ const tryTransition = () => {
50867
+ const current = findVoicePathwayState(options.pathway, state.currentStateId);
50868
+ if (!current)
50869
+ return;
50870
+ if (current.transitions.length === 0) {
50871
+ state = { ...state, status: "ended" };
50872
+ return;
50873
+ }
50874
+ const transition = pickTransition(current, state.slots);
50875
+ if (!transition) {
50876
+ state = { ...state, status: "awaiting-slot" };
50877
+ return;
50878
+ }
50879
+ enter(transition.to);
50880
+ };
50881
+ const start = () => {
50882
+ state.history = [];
50883
+ enter(options.pathway.entryStateId);
50884
+ };
50885
+ const fillSlot = (slotId, value) => {
50886
+ state = { ...state, slots: { ...state.slots, [slotId]: value } };
50887
+ if (state.awaitingSlotId === slotId) {
50888
+ state = { ...state, awaitingSlotId: null, status: "branching" };
50889
+ const current = findVoicePathwayState(options.pathway, state.currentStateId);
50890
+ if (current) {
50891
+ const result = buildPendingActions(current, state.slots, options.pathway, emit2);
50892
+ state = {
50893
+ ...state,
50894
+ awaitingSlotId: result.awaitingSlotId,
50895
+ pendingActions: result.actions,
50896
+ status: result.ended ? "ended" : result.awaitingSlotId ? "awaiting-slot" : "branching",
50897
+ ...result.reason !== undefined ? { endedReason: result.reason } : {}
50898
+ };
50899
+ if (state.status === "branching")
50900
+ tryTransition();
50901
+ }
50902
+ }
50903
+ };
50904
+ return {
50905
+ fillSlot,
50906
+ getState: () => state,
50907
+ start,
50908
+ subscribe(listener) {
50909
+ listeners.add(listener);
50910
+ return () => {
50911
+ listeners.delete(listener);
50912
+ };
50913
+ },
50914
+ tryTransition
50915
+ };
50916
+ };
50917
+ // src/pathwaySlotCollector.ts
50918
+ var numberWords = {
50919
+ eight: 8,
50920
+ five: 5,
50921
+ four: 4,
50922
+ nine: 9,
50923
+ one: 1,
50924
+ seven: 7,
50925
+ six: 6,
50926
+ ten: 10,
50927
+ three: 3,
50928
+ two: 2,
50929
+ zero: 0
50930
+ };
50931
+ var parseString = (raw, slot) => {
50932
+ const trimmed = raw.trim();
50933
+ if (!trimmed)
50934
+ return { ok: false, reason: "empty" };
50935
+ const min = slot.validation?.minLength ?? 1;
50936
+ const max = slot.validation?.maxLength ?? Number.MAX_SAFE_INTEGER;
50937
+ if (trimmed.length < min || trimmed.length > max) {
50938
+ return {
50939
+ hint: `Expected ${min}\u2013${max} characters`,
50940
+ ok: false,
50941
+ reason: "out-of-range"
50942
+ };
50943
+ }
50944
+ if (slot.validation?.pattern) {
50945
+ try {
50946
+ if (!new RegExp(slot.validation.pattern, "u").test(trimmed)) {
50947
+ return { ok: false, reason: "no-match" };
50948
+ }
50949
+ } catch {
50950
+ return { ok: false, reason: "no-match" };
50951
+ }
50952
+ }
50953
+ return { normalized: trimmed, ok: true, value: trimmed };
50954
+ };
50955
+ var parseNumber = (raw, slot) => {
50956
+ const trimmed = raw.trim().toLowerCase();
50957
+ if (!trimmed)
50958
+ return { ok: false, reason: "empty" };
50959
+ let value;
50960
+ if (numberWords[trimmed] !== undefined) {
50961
+ value = numberWords[trimmed];
50962
+ } else {
50963
+ const cleaned = trimmed.replace(/[$,]/gu, "");
50964
+ value = Number(cleaned);
50965
+ if (Number.isNaN(value))
50966
+ return { ok: false, reason: "type-mismatch" };
50967
+ }
50968
+ const min = slot.validation?.min ?? Number.NEGATIVE_INFINITY;
50969
+ const max = slot.validation?.max ?? Number.POSITIVE_INFINITY;
50970
+ if (value < min || value > max) {
50971
+ return { hint: `Expected ${min}\u2013${max}`, ok: false, reason: "out-of-range" };
50972
+ }
50973
+ return { normalized: String(value), ok: true, value };
50974
+ };
50975
+ var parseBoolean2 = (raw) => {
50976
+ const trimmed = raw.trim().toLowerCase();
50977
+ if (!trimmed)
50978
+ return { ok: false, reason: "empty" };
50979
+ if (["yes", "yeah", "yep", "correct", "true", "sure", "ok"].includes(trimmed)) {
50980
+ return { normalized: "true", ok: true, value: true };
50981
+ }
50982
+ if (["no", "nope", "nah", "false", "incorrect"].includes(trimmed)) {
50983
+ return { normalized: "false", ok: true, value: false };
50984
+ }
50985
+ return { ok: false, reason: "type-mismatch" };
50986
+ };
50987
+ var parseDate = (raw) => {
50988
+ const trimmed = raw.trim();
50989
+ if (!trimmed)
50990
+ return { ok: false, reason: "empty" };
50991
+ const date = new Date(trimmed);
50992
+ if (Number.isNaN(date.getTime()))
50993
+ return { ok: false, reason: "type-mismatch" };
50994
+ const iso = date.toISOString().slice(0, 10);
50995
+ return { normalized: iso, ok: true, value: iso };
50996
+ };
50997
+ var parseTime2 = (raw) => {
50998
+ const trimmed = raw.trim();
50999
+ const match = /^([0-9]{1,2}):?([0-9]{2})?\s*(am|pm)?$/iu.exec(trimmed);
51000
+ if (!match)
51001
+ return { ok: false, reason: "type-mismatch" };
51002
+ let hour = Number(match[1]);
51003
+ const minute = Number(match[2] ?? "0");
51004
+ const meridiem = match[3]?.toLowerCase();
51005
+ if (meridiem === "pm" && hour < 12)
51006
+ hour += 12;
51007
+ if (meridiem === "am" && hour === 12)
51008
+ hour = 0;
51009
+ if (hour > 23 || minute > 59)
51010
+ return { ok: false, reason: "out-of-range" };
51011
+ const formatted = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
51012
+ return { normalized: formatted, ok: true, value: formatted };
51013
+ };
51014
+ var parsePhone = (raw) => {
51015
+ const trimmed = raw.trim();
51016
+ if (!trimmed)
51017
+ return { ok: false, reason: "empty" };
51018
+ const digits = trimmed.replace(/\D/gu, "");
51019
+ if (digits.length < 10 || digits.length > 15) {
51020
+ return { ok: false, reason: "type-mismatch" };
51021
+ }
51022
+ const normalized = digits.length === 10 ? `+1${digits}` : `+${digits}`;
51023
+ return { normalized, ok: true, value: normalized };
51024
+ };
51025
+ var parseEmail = (raw) => {
51026
+ const trimmed = raw.trim();
51027
+ if (!trimmed)
51028
+ return { ok: false, reason: "empty" };
51029
+ const collapsed = trimmed.replace(/\s+at\s+/gu, "@").replace(/\s+dot\s+/gu, ".");
51030
+ const ok = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/iu.test(collapsed);
51031
+ if (!ok)
51032
+ return { ok: false, reason: "type-mismatch" };
51033
+ return { normalized: collapsed.toLowerCase(), ok: true, value: collapsed.toLowerCase() };
51034
+ };
51035
+ var parseCurrency = (raw, slot) => {
51036
+ const trimmed = raw.trim().toLowerCase();
51037
+ if (!trimmed)
51038
+ return { ok: false, reason: "empty" };
51039
+ const cleaned = trimmed.replace(/[$,]/gu, "").replace(/\s*(dollars?|usd)$/u, "");
51040
+ const value = Number(cleaned);
51041
+ if (Number.isNaN(value))
51042
+ return { ok: false, reason: "type-mismatch" };
51043
+ const min = slot.validation?.min ?? 0;
51044
+ const max = slot.validation?.max ?? Number.POSITIVE_INFINITY;
51045
+ if (value < min || value > max) {
51046
+ return { hint: `Expected ${min}\u2013${max}`, ok: false, reason: "out-of-range" };
51047
+ }
51048
+ return { normalized: value.toFixed(2), ok: true, value };
51049
+ };
51050
+ var parseChoice = (raw, slot) => {
51051
+ const trimmed = raw.trim().toLowerCase();
51052
+ if (!trimmed)
51053
+ return { ok: false, reason: "empty" };
51054
+ const match = (slot.choices ?? []).find((choice) => choice.toLowerCase() === trimmed);
51055
+ if (!match)
51056
+ return { ok: false, reason: "no-match" };
51057
+ return { normalized: match, ok: true, value: match };
51058
+ };
51059
+ var DEFAULT_VOICE_PATHWAY_SLOT_PARSERS = {
51060
+ boolean: parseBoolean2,
51061
+ choice: parseChoice,
51062
+ currency: parseCurrency,
51063
+ date: parseDate,
51064
+ email: parseEmail,
51065
+ number: parseNumber,
51066
+ phone: parsePhone,
51067
+ string: parseString,
51068
+ time: parseTime2
51069
+ };
51070
+ var createVoicePathwaySlotCollector = (options = {}) => {
51071
+ const parsers = {
51072
+ ...DEFAULT_VOICE_PATHWAY_SLOT_PARSERS,
51073
+ ...options.parsers ?? {}
51074
+ };
51075
+ const maxAttempts = options.maxAttemptsPerSlot ?? 3;
51076
+ const attemptCounts = new Map;
51077
+ const interpret = (slot, raw) => {
51078
+ const parser = parsers[slot.type];
51079
+ if (!parser) {
51080
+ return {
51081
+ attempt: (attemptCounts.get(slot.id) ?? 0) + 1,
51082
+ raw,
51083
+ result: {
51084
+ hint: `No parser for ${slot.type}`,
51085
+ ok: false,
51086
+ reason: "no-match"
51087
+ },
51088
+ slotId: slot.id
51089
+ };
51090
+ }
51091
+ const next = (attemptCounts.get(slot.id) ?? 0) + 1;
51092
+ attemptCounts.set(slot.id, next);
51093
+ return {
51094
+ attempt: next,
51095
+ raw,
51096
+ result: parser(raw, slot),
51097
+ slotId: slot.id
51098
+ };
51099
+ };
51100
+ const attemptsExceeded = (slotId) => (attemptCounts.get(slotId) ?? 0) >= maxAttempts;
51101
+ return {
51102
+ attemptsExceeded,
51103
+ interpret,
51104
+ reset: (slotId) => {
51105
+ if (slotId)
51106
+ attemptCounts.delete(slotId);
51107
+ else
51108
+ attemptCounts.clear();
51109
+ }
51110
+ };
51111
+ };
51112
+ // src/pathwayCompiler.ts
51113
+ var describeAction = (action) => {
51114
+ switch (action.kind) {
51115
+ case "say":
51116
+ return `Say to the caller: "${action.text}"`;
51117
+ case "collect-slot":
51118
+ return `Collect slot \`${action.slotId}\` from the caller.`;
51119
+ case "call-tool":
51120
+ return `Call tool \`${action.toolId}\`${action.argsFromSlots ? ` with slots: ${action.argsFromSlots.join(", ")}` : ""}.`;
51121
+ case "set-slot":
51122
+ return `Set slot \`${action.slotId}\` to expression \`${action.valueExpression}\`.`;
51123
+ case "transfer":
51124
+ return `Transfer the call to \`${action.destination}\`.`;
51125
+ case "end-call":
51126
+ return `End the call${action.reason ? ` (reason: ${action.reason})` : ""}.`;
51127
+ }
51128
+ };
51129
+ var describeCondition = (condition) => {
51130
+ switch (condition.kind) {
51131
+ case "always":
51132
+ return "always";
51133
+ case "fallback":
51134
+ return "if no other condition matches";
51135
+ case "slot-filled":
51136
+ return `when slot \`${condition.slotId}\` is filled`;
51137
+ case "slot-equals":
51138
+ return `when slot \`${condition.slotId}\` equals ${JSON.stringify(condition.value)}`;
51139
+ case "slot-matches":
51140
+ return `when slot \`${condition.slotId}\` matches /${condition.pattern}/`;
51141
+ }
51142
+ };
51143
+ var describeTransition = (transition) => `\u2192 ${transition.to} (${describeCondition(transition.condition)})`;
51144
+ var describeState = (state) => {
51145
+ const header = `## State: ${state.id} \u2014 ${state.label}${state.kind ? ` (${state.kind})` : ""}`;
51146
+ const description = state.description ? `
51147
+ ${state.description}` : "";
51148
+ const note = state.systemNote ? `
51149
+ Note: ${state.systemNote}` : "";
51150
+ const actions = (state.actions ?? []).map((a) => `- ${describeAction(a)}`).join(`
51151
+ `);
51152
+ const transitions = state.transitions.map((t) => `- ${describeTransition(t)}`).join(`
51153
+ `);
51154
+ return [
51155
+ header,
51156
+ description,
51157
+ note,
51158
+ actions ? `
51159
+ Actions:
51160
+ ${actions}` : "",
51161
+ transitions ? `
51162
+ Transitions:
51163
+ ${transitions}` : ""
51164
+ ].filter((s) => s.length > 0).join(`
51165
+ `);
51166
+ };
51167
+ var buildTools = (pathway, prefix) => {
51168
+ const advanceTool = {
51169
+ description: `Advance the ${pathway.label} pathway by transitioning to the next state. Always call when a state's conditions are met.`,
51170
+ name: `${prefix}_advance`,
51171
+ parameters: {
51172
+ properties: {
51173
+ rationale: {
51174
+ description: "Why this transition is being taken now.",
51175
+ type: "string"
51176
+ },
51177
+ toStateId: {
51178
+ description: "ID of the target state.",
51179
+ enum: pathway.states.map((s) => s.id),
51180
+ type: "string"
51181
+ }
51182
+ },
51183
+ required: ["toStateId"],
51184
+ type: "object"
51185
+ }
51186
+ };
51187
+ const fillSlotTool = {
51188
+ description: `Record a slot value collected from the caller within the ${pathway.label} pathway.`,
51189
+ name: `${prefix}_fill_slot`,
51190
+ parameters: {
51191
+ properties: {
51192
+ slotId: {
51193
+ description: "ID of the slot being filled.",
51194
+ enum: pathway.slots.map((s) => s.id),
51195
+ type: "string"
51196
+ },
51197
+ value: {
51198
+ description: "The value the caller provided.",
51199
+ type: "string"
51200
+ }
51201
+ },
51202
+ required: ["slotId", "value"],
51203
+ type: "object"
51204
+ }
51205
+ };
51206
+ const endCallTool = {
51207
+ description: `End the ${pathway.label} call.`,
51208
+ name: `${prefix}_end_call`,
51209
+ parameters: {
51210
+ properties: {
51211
+ reason: { description: "Reason for ending.", type: "string" }
51212
+ },
51213
+ required: [],
51214
+ type: "object"
51215
+ }
51216
+ };
51217
+ const userTools = (pathway.tools ?? []).map((tool) => ({
51218
+ description: tool.description,
51219
+ name: `${prefix}_tool_${tool.id}`,
51220
+ parameters: {
51221
+ properties: {
51222
+ arguments: { description: "JSON-encoded arguments.", type: "string" }
51223
+ },
51224
+ required: [],
51225
+ type: "object"
51226
+ }
51227
+ }));
51228
+ return [advanceTool, fillSlotTool, endCallTool, ...userTools];
51229
+ };
51230
+ var compileVoicePathwayToAssistant = (options) => {
51231
+ const report = validateVoicePathway(options.pathway);
51232
+ if (!report.valid) {
51233
+ throw new Error(`Cannot compile invalid pathway: ${report.issues.filter((i) => i.severity === "error").map((i) => i.message).join("; ")}`);
51234
+ }
51235
+ const prefix = options.toolNamePrefix ?? `pathway_${options.pathway.id}`;
51236
+ const tools = buildTools(options.pathway, prefix);
51237
+ const fallback = options.fallbackBehavior === "end-call" ? "If no transition condition is met, end the call politely." : options.fallbackBehavior === "transfer" ? "If no transition condition is met, transfer to a human agent." : "If no transition condition is met, ask a clarifying question and stay in the current state.";
51238
+ const slotBlock = options.pathway.slots.map((slot) => `- \`${slot.id}\` (${slot.type}${slot.required ? ", required" : ""}): ${slot.description ?? slot.prompt}`).join(`
51239
+ `);
51240
+ const stateBlock = options.pathway.states.map(describeState).join(`
51241
+
51242
+ `);
51243
+ const systemPrompt = [
51244
+ `You are operating the "${options.pathway.label}" pathway as a voice agent.`,
51245
+ `Follow the state machine exactly. Use the provided tools to advance states, fill slots, and end the call.`,
51246
+ `Start in state \`${options.pathway.entryStateId}\`. Track which state you are in. Do not invent states or transitions.`,
51247
+ fallback,
51248
+ "",
51249
+ `Slots:
51250
+ ${slotBlock || "(none)"}`,
51251
+ "",
51252
+ `States:
51253
+ ${stateBlock}`
51254
+ ].join(`
51255
+ `);
51256
+ return {
51257
+ metadata: {
51258
+ entryStateId: options.pathway.entryStateId,
51259
+ pathwayId: options.pathway.id,
51260
+ pathwayLabel: options.pathway.label
51261
+ },
51262
+ systemPrompt,
51263
+ tools,
51264
+ ...options.introduction !== undefined ? { initialPrompt: options.introduction } : {}
51265
+ };
51266
+ };
51267
+ // src/pathwayVisualizer.ts
51268
+ var escapeMermaidLabel = (text) => text.replace(/[<>"]/gu, "").replace(/\|/gu, "/");
51269
+ var conditionLabel = (condition) => {
51270
+ switch (condition.kind) {
51271
+ case "always":
51272
+ return "always";
51273
+ case "fallback":
51274
+ return "else";
51275
+ case "slot-filled":
51276
+ return `${condition.slotId} \u2713`;
51277
+ case "slot-equals":
51278
+ return `${condition.slotId}=${condition.value}`;
51279
+ case "slot-matches":
51280
+ return `${condition.slotId}~/${condition.pattern}/`;
51281
+ }
51282
+ };
51283
+ var mermaidShape = (state) => {
51284
+ switch (state.kind) {
51285
+ case "entry":
51286
+ return { close: ")", open: "((" };
51287
+ case "terminal":
51288
+ return { close: "))", open: "((" };
51289
+ case "branch":
51290
+ return { close: "}", open: "{" };
51291
+ case "action":
51292
+ return { close: "]", open: "[" };
51293
+ case "collect":
51294
+ return { close: "]", open: "[" };
51295
+ default:
51296
+ return { close: "]", open: "[" };
51297
+ }
51298
+ };
51299
+ var renderVoicePathwayMermaid = (pathway) => {
51300
+ const lines = ["flowchart TD"];
51301
+ for (const state of pathway.states) {
51302
+ const shape = mermaidShape(state);
51303
+ const label = escapeMermaidLabel(`${state.id}: ${state.label}`);
51304
+ lines.push(` ${state.id}${shape.open}"${label}"${shape.close}`);
51305
+ }
51306
+ for (const state of pathway.states) {
51307
+ for (const transition of state.transitions) {
51308
+ const label = escapeMermaidLabel(conditionLabel(transition.condition));
51309
+ lines.push(` ${state.id} -- "${label}" --> ${transition.to}`);
51310
+ }
51311
+ }
51312
+ return lines.join(`
51313
+ `);
51314
+ };
51315
+ var renderVoicePathwayText = (pathway) => {
51316
+ const stateById = new Map(pathway.states.map((s) => [s.id, s]));
51317
+ const visited = new Set;
51318
+ const lines = [
51319
+ `Pathway: ${pathway.label} (${pathway.id})`,
51320
+ `Entry: ${pathway.entryStateId}`,
51321
+ ""
51322
+ ];
51323
+ if (pathway.slots.length > 0) {
51324
+ lines.push("Slots:");
51325
+ for (const slot of pathway.slots) {
51326
+ lines.push(` - ${slot.id} (${slot.type}${slot.required ? ", required" : ""}): ${slot.prompt}`);
51327
+ }
51328
+ lines.push("");
51329
+ }
51330
+ const walk = (stateId, depth) => {
51331
+ if (visited.has(stateId)) {
51332
+ lines.push(`${" ".repeat(depth)}\u2192 ${stateId} (already shown)`);
51333
+ return;
51334
+ }
51335
+ visited.add(stateId);
51336
+ const state = stateById.get(stateId);
51337
+ if (!state) {
51338
+ lines.push(`${" ".repeat(depth)}\u2192 ${stateId} (missing)`);
51339
+ return;
51340
+ }
51341
+ const indent = " ".repeat(depth);
51342
+ const kind = state.kind ? ` [${state.kind}]` : "";
51343
+ lines.push(`${indent}- ${state.id}: ${state.label}${kind}`);
51344
+ if (state.actions && state.actions.length > 0) {
51345
+ for (const action of state.actions) {
51346
+ lines.push(`${indent} \xB7 ${action.kind}`);
51347
+ }
51348
+ }
51349
+ for (const transition of state.transitions) {
51350
+ lines.push(`${indent} \u2192 ${transition.to} (${conditionLabel(transition.condition)})`);
51351
+ walk(transition.to, depth + 1);
51352
+ }
51353
+ };
51354
+ lines.push("States:");
51355
+ walk(pathway.entryStateId, 1);
51356
+ return lines.join(`
51357
+ `);
51358
+ };
51359
+ var visualizeVoicePathway = (pathway) => ({
51360
+ mermaid: renderVoicePathwayMermaid(pathway),
51361
+ text: renderVoicePathwayText(pathway)
51362
+ });
51363
+ // src/crmContract.ts
51364
+ var createVoiceCRMRegistry = (options) => {
51365
+ const byVendor = new Map;
51366
+ for (const contract of options.contracts) {
51367
+ byVendor.set(contract.vendor, contract);
51368
+ }
51369
+ const defaultVendor = options.defaultVendor ?? options.contracts[0]?.vendor ?? null;
51370
+ return {
51371
+ default() {
51372
+ return defaultVendor ? byVendor.get(defaultVendor) ?? null : null;
51373
+ },
51374
+ get(vendor) {
51375
+ return byVendor.get(vendor) ?? null;
51376
+ },
51377
+ list() {
51378
+ return Array.from(byVendor.values());
51379
+ }
51380
+ };
51381
+ };
51382
+ // src/callerCRMLinker.ts
51383
+ var cacheKeyFor = (identity, vendor) => {
51384
+ const id = identity.externalId ?? identity.phone ?? identity.email ?? "anonymous";
51385
+ return `${vendor}::${id}`;
51386
+ };
51387
+ var createInMemoryVoiceCallerCRMLinkCache = () => {
51388
+ const store = new Map;
51389
+ return {
51390
+ get: (key) => store.get(key) ?? null,
51391
+ put: (record) => {
51392
+ store.set(record.callerKey, { ...record });
51393
+ },
51394
+ remove: (key) => store.delete(key)
51395
+ };
51396
+ };
51397
+ var createVoiceCallerCRMLinker = (options) => {
51398
+ const now = options.now ?? (() => Date.now());
51399
+ const cache = options.cache ?? createInMemoryVoiceCallerCRMLinkCache();
51400
+ const staleAfter = options.staleAfterMs ?? 24 * 60 * 60 * 1000;
51401
+ const isFresh = (record) => now() - record.resolvedAt < staleAfter;
51402
+ const resolve3 = async (identity) => {
51403
+ const key = cacheKeyFor(identity, options.contract.vendor);
51404
+ const cached = await Promise.resolve(cache.get(key));
51405
+ if (cached && isFresh(cached)) {
51406
+ return cached;
51407
+ }
51408
+ let contact = null;
51409
+ let source = null;
51410
+ if (identity.phone) {
51411
+ contact = await options.contract.lookupByPhone(identity.phone);
51412
+ if (contact)
51413
+ source = "phone-lookup";
51414
+ }
51415
+ if (!contact && identity.email) {
51416
+ contact = await options.contract.lookupByEmail(identity.email);
51417
+ if (contact)
51418
+ source = "email-lookup";
51419
+ }
51420
+ if (!contact || !source)
51421
+ return null;
51422
+ const record = {
51423
+ callerKey: key,
51424
+ contact,
51425
+ contactId: contact.id,
51426
+ resolvedAt: now(),
51427
+ source,
51428
+ vendor: options.contract.vendor
51429
+ };
51430
+ await Promise.resolve(cache.put(record));
51431
+ return record;
51432
+ };
51433
+ const associate = async (identity, contact) => {
51434
+ const key = cacheKeyFor(identity, options.contract.vendor);
51435
+ const record = {
51436
+ callerKey: key,
51437
+ contact,
51438
+ contactId: contact.id,
51439
+ resolvedAt: now(),
51440
+ source: "manual",
51441
+ vendor: options.contract.vendor
51442
+ };
51443
+ await Promise.resolve(cache.put(record));
51444
+ return record;
51445
+ };
51446
+ const invalidate = async (identity) => {
51447
+ const key = cacheKeyFor(identity, options.contract.vendor);
51448
+ return Promise.resolve(cache.remove(key));
51449
+ };
51450
+ return {
51451
+ associate,
51452
+ contract: options.contract,
51453
+ invalidate,
51454
+ resolve: resolve3
51455
+ };
51456
+ };
51457
+ // src/crmCallLogger.ts
51458
+ var createVoiceCRMCallLogger = (options) => {
51459
+ const now = options.now ?? (() => Date.now());
51460
+ const errorPolicy = options.errorPolicy ?? "swallow";
51461
+ const logCallEnd = async (input) => {
51462
+ const endedAt = input.endedAt ?? now();
51463
+ const duration = input.durationSeconds ?? Math.max(0, Math.round((endedAt - input.startedAt) / 1000));
51464
+ const payload = {
51465
+ durationSeconds: duration,
51466
+ endedAt,
51467
+ sessionId: input.sessionId,
51468
+ startedAt: input.startedAt,
51469
+ ...input.contactId !== undefined ? { contactId: input.contactId } : {},
51470
+ ...input.summary !== undefined ? { summary: input.summary } : {},
51471
+ ...input.disposition !== undefined ? { disposition: input.disposition } : {},
51472
+ ...input.recordingUrl !== undefined ? { recordingUrl: input.recordingUrl } : {},
51473
+ ...input.transcriptUrl !== undefined ? { transcriptUrl: input.transcriptUrl } : {},
51474
+ ...input.metadata !== undefined ? { metadata: input.metadata } : {}
51475
+ };
51476
+ try {
51477
+ const result = await options.contract.logCall(payload);
51478
+ return {
51479
+ activityId: result.activityId,
51480
+ loggedAt: now(),
51481
+ vendor: options.contract.vendor
51482
+ };
51483
+ } catch (rawError) {
51484
+ const error = rawError instanceof Error ? rawError : new Error(String(rawError));
51485
+ await options.onError?.(error, input);
51486
+ if (errorPolicy === "queue") {
51487
+ await options.enqueueOnFailure?.(input);
51488
+ return null;
51489
+ }
51490
+ if (errorPolicy === "throw")
51491
+ throw error;
51492
+ return null;
51493
+ }
51494
+ };
51495
+ const noteOnContact = async (input) => {
51496
+ try {
51497
+ return await options.contract.addNote(input);
51498
+ } catch (rawError) {
51499
+ const error = rawError instanceof Error ? rawError : new Error(String(rawError));
51500
+ await options.onError?.(error, {
51501
+ sessionId: "(note)",
51502
+ startedAt: now(),
51503
+ ...input
51504
+ });
51505
+ if (errorPolicy === "throw")
51506
+ throw error;
51507
+ return null;
51508
+ }
51509
+ };
51510
+ return {
51511
+ contract: options.contract,
51512
+ logCallEnd,
51513
+ noteOnContact
51514
+ };
51515
+ };
50592
51516
  export {
50593
51517
  writeVoiceProofPack,
50594
51518
  writeVoiceMediaPipelineArtifacts,
@@ -50603,12 +51527,14 @@ export {
50603
51527
  voiceComplianceRedactionDefaults,
50604
51528
  voiceAgentUIStateOrder,
50605
51529
  voice,
51530
+ visualizeVoicePathway,
50606
51531
  verifyVoiceWebhookSignature,
50607
51532
  verifyVoiceTwilioWebhookSignature,
50608
51533
  verifyVoiceTelnyxWebhookSignature,
50609
51534
  verifyVoicePlivoWebhookSignature,
50610
51535
  verifyVoiceOpsWebhookSignature,
50611
51536
  validateVoiceWorkflowRouteResult,
51537
+ validateVoicePathway,
50612
51538
  validateVoiceObservabilityExportRecord,
50613
51539
  validateVoiceDTMFLuhn,
50614
51540
  ttsAdapterSessionCanCancel,
@@ -50759,6 +51685,8 @@ export {
50759
51685
  renderVoiceProductionReadinessHTML,
50760
51686
  renderVoicePostCallAnalysisMarkdown,
50761
51687
  renderVoicePhoneAgentProductionSmokeHTML,
51688
+ renderVoicePathwayText,
51689
+ renderVoicePathwayMermaid,
50762
51690
  renderVoiceOutcomeContractHTML,
50763
51691
  renderVoiceOpsStatusHTML,
50764
51692
  renderVoiceOpsRecoveryMarkdown,
@@ -50872,6 +51800,8 @@ export {
50872
51800
  fromVapiAssistantConfig,
50873
51801
  formatVoiceProofTrendAge,
50874
51802
  formatVoiceCallPlayerTimestamp,
51803
+ findVoicePathwayState,
51804
+ findVoicePathwaySlot,
50875
51805
  filterVoiceTraceEvents,
50876
51806
  filterVoiceAuditEvents,
50877
51807
  fetchVoiceProofTarget,
@@ -51127,6 +52057,8 @@ export {
51127
52057
  createVoicePhoneAgentProductionSmokeJSONHandler,
51128
52058
  createVoicePhoneAgentProductionSmokeHTMLHandler,
51129
52059
  createVoicePhoneAgent,
52060
+ createVoicePathwaySlotCollector,
52061
+ createVoicePathwayRuntime,
51130
52062
  createVoiceOutcomeContractRoutes,
51131
52063
  createVoiceOutcomeContractJSONHandler,
51132
52064
  createVoiceOutcomeContractHTMLHandler,
@@ -51252,6 +52184,7 @@ export {
51252
52184
  createVoiceCampaign,
51253
52185
  createVoiceCallingWindow,
51254
52186
  createVoiceCallerMemoryNamespace,
52187
+ createVoiceCallerCRMLinker,
51255
52188
  createVoiceCallReviewRecorder,
51256
52189
  createVoiceCallReviewFromSession,
51257
52190
  createVoiceCallReviewFromLiveTelephonyReport,
@@ -51259,6 +52192,8 @@ export {
51259
52192
  createVoiceCallDispositionTagger,
51260
52193
  createVoiceCallDebuggerRoutes,
51261
52194
  createVoiceCallCompletedEvent,
52195
+ createVoiceCRMRegistry,
52196
+ createVoiceCRMCallLogger,
51262
52197
  createVoiceCRMActivitySink,
51263
52198
  createVoiceBrowserMediaRoutes,
51264
52199
  createVoiceBrowserCallProfileRoutes,
@@ -51319,6 +52254,7 @@ export {
51319
52254
  createLiveCallViewerFromOptions,
51320
52255
  createLiveCallViewer,
51321
52256
  createJSONVoiceAssistantModel,
52257
+ createInMemoryVoiceCallerCRMLinkCache,
51322
52258
  createInMemoryVoiceCallQuota,
51323
52259
  createInMemoryDNCList,
51324
52260
  createId,
@@ -51332,6 +52268,7 @@ export {
51332
52268
  computeVoiceScorecardCalibration,
51333
52269
  computePcmDurationMs,
51334
52270
  completeVoiceOpsTask,
52271
+ compileVoicePathwayToAssistant,
51335
52272
  compareVoiceEvalBaseline,
51336
52273
  compareVoiceCostScenarios,
51337
52274
  collectVoiceDTMFInput,
@@ -51502,6 +52439,7 @@ export {
51502
52439
  DEFAULT_VOICE_PROMPT_INJECTION_RULES,
51503
52440
  DEFAULT_VOICE_PRICE_BOOK,
51504
52441
  DEFAULT_VOICE_POST_CALL_SURVEY_QUESTIONS,
52442
+ DEFAULT_VOICE_PATHWAY_SLOT_PARSERS,
51505
52443
  DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS,
51506
52444
  DEFAULT_VOICE_CALL_DISPOSITIONS,
51507
52445
  DEFAULT_VOICE_ANNOTATION_KIND_SEVERITY,