@absolutejs/voice 0.0.22-beta.111 → 0.0.22-beta.113

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.
@@ -1,4 +1,5 @@
1
1
  import { Elysia } from 'elysia';
2
+ import type { VoiceRedisTaskLeaseCoordinator } from './queue';
2
3
  export type VoiceCampaignStatus = 'canceled' | 'completed' | 'draft' | 'paused' | 'running';
3
4
  export type VoiceCampaignRecipientStatus = 'canceled' | 'completed' | 'failed' | 'pending' | 'queued';
4
5
  export type VoiceCampaignAttemptStatus = 'canceled' | 'failed' | 'queued' | 'running' | 'succeeded';
@@ -153,17 +154,116 @@ export type VoiceCampaignRuntime = {
153
154
  export type VoiceCampaignRoutesOptions = VoiceCampaignRuntimeOptions & {
154
155
  headers?: HeadersInit;
155
156
  htmlPath?: false | string;
157
+ observability?: VoiceCampaignObservabilityOptions;
156
158
  name?: string;
157
159
  path?: string;
158
160
  title?: string;
159
161
  };
162
+ export type VoiceCampaignWorkerOptions = {
163
+ campaignIds?: string[];
164
+ leaseMs?: number;
165
+ leases: VoiceRedisTaskLeaseCoordinator;
166
+ maxCampaigns?: number;
167
+ runtime?: VoiceCampaignRuntime;
168
+ statuses?: VoiceCampaignStatus[];
169
+ store?: VoiceCampaignStore;
170
+ workerId: string;
171
+ };
172
+ export type VoiceCampaignWorkerResult = {
173
+ attempted: number;
174
+ campaigns: number;
175
+ errors: Array<{
176
+ campaignId: string;
177
+ error: string;
178
+ }>;
179
+ skipped: number;
180
+ started: VoiceCampaignAttempt[];
181
+ };
182
+ export type VoiceCampaignWorker = {
183
+ drain: () => Promise<VoiceCampaignWorkerResult>;
184
+ };
185
+ export type VoiceCampaignWorkerLoopOptions = {
186
+ onError?: (error: unknown) => void | Promise<void>;
187
+ pollIntervalMs?: number;
188
+ worker: VoiceCampaignWorker;
189
+ };
190
+ export type VoiceCampaignWorkerLoop = {
191
+ isRunning: () => boolean;
192
+ start: () => void;
193
+ stop: () => void;
194
+ tick: () => Promise<VoiceCampaignWorkerResult>;
195
+ };
196
+ export type VoiceCampaignObservabilityOptions = {
197
+ leases?: VoiceRedisTaskLeaseCoordinator;
198
+ now?: number;
199
+ rateWindowMs?: number;
200
+ stuckAfterMs?: number;
201
+ };
202
+ export type VoiceCampaignObservabilityReport = {
203
+ attemptRate: {
204
+ failed: number;
205
+ started: number;
206
+ succeeded: number;
207
+ windowMs: number;
208
+ };
209
+ campaigns: Array<{
210
+ activeAttempts: number;
211
+ campaignId: string;
212
+ lease?: {
213
+ expiresAt: number;
214
+ workerId: string;
215
+ };
216
+ name: string;
217
+ queueDepth: number;
218
+ status: VoiceCampaignStatus;
219
+ stuckAttempts: number;
220
+ stuckRecipients: number;
221
+ updatedAt: number;
222
+ }>;
223
+ failureReasons: Array<{
224
+ count: number;
225
+ reason: string;
226
+ }>;
227
+ generatedAt: number;
228
+ leases: {
229
+ active: number;
230
+ known: boolean;
231
+ };
232
+ queue: {
233
+ activeAttempts: number;
234
+ queuedRecipients: number;
235
+ runningCampaigns: number;
236
+ };
237
+ stuck: {
238
+ attempts: Array<{
239
+ attemptId: string;
240
+ campaignId: string;
241
+ recipientId: string;
242
+ status: VoiceCampaignAttemptStatus;
243
+ updatedAt: number;
244
+ }>;
245
+ recipients: Array<{
246
+ campaignId: string;
247
+ recipientId: string;
248
+ status: VoiceCampaignRecipientStatus;
249
+ updatedAt: number;
250
+ }>;
251
+ };
252
+ summary: VoiceCampaignSummary;
253
+ };
160
254
  export declare const createVoiceMemoryCampaignStore: () => VoiceCampaignStore;
161
255
  export declare const summarizeVoiceCampaigns: (records: VoiceCampaignRecord[]) => VoiceCampaignSummary;
256
+ export declare const buildVoiceCampaignObservabilityReport: (records: VoiceCampaignRecord[], options?: VoiceCampaignObservabilityOptions) => Promise<VoiceCampaignObservabilityReport>;
162
257
  export declare const createVoiceCampaign: (options: VoiceCampaignRuntimeOptions) => VoiceCampaignRuntime;
258
+ export declare const createVoiceCampaignWorker: (options: VoiceCampaignWorkerOptions) => VoiceCampaignWorker;
259
+ export declare const createVoiceCampaignWorkerLoop: (options: VoiceCampaignWorkerLoopOptions) => VoiceCampaignWorkerLoop;
163
260
  export declare const runVoiceCampaignProof: (options?: VoiceCampaignProofOptions) => Promise<VoiceCampaignProofReport>;
164
261
  export declare const renderVoiceCampaignsHTML: (records: VoiceCampaignRecord[], options?: {
165
262
  title?: string;
166
263
  }) => string;
264
+ export declare const renderVoiceCampaignObservabilityHTML: (report: VoiceCampaignObservabilityReport, options?: {
265
+ title?: string;
266
+ }) => string;
167
267
  export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOptions) => Elysia<"", {
168
268
  decorator: {};
169
269
  store: {};
@@ -194,6 +294,20 @@ export declare const createVoiceCampaignRoutes: (options: VoiceCampaignRoutesOpt
194
294
  };
195
295
  };
196
296
  };
297
+ } & {
298
+ [x: string]: {
299
+ observability: {
300
+ get: {
301
+ body: unknown;
302
+ params: {};
303
+ query: unknown;
304
+ headers: unknown;
305
+ response: {
306
+ 200: VoiceCampaignObservabilityReport;
307
+ };
308
+ };
309
+ };
310
+ };
197
311
  } & {
198
312
  [x: string]: {
199
313
  post: {
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { voice } from './plugin';
2
2
  export { createVoiceAppKit, createVoiceAppKitRoutes, summarizeVoiceAppKitStatus } from './appKit';
3
- export { createVoiceCampaign, createVoiceCampaignRoutes, createVoiceMemoryCampaignStore, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
3
+ export { buildVoiceCampaignObservabilityReport, createVoiceCampaign, createVoiceCampaignRoutes, createVoiceCampaignWorker, createVoiceCampaignWorkerLoop, createVoiceMemoryCampaignStore, renderVoiceCampaignObservabilityHTML, renderVoiceCampaignsHTML, runVoiceCampaignProof, summarizeVoiceCampaigns } from './campaign';
4
4
  export { createVoiceAssistant, createVoiceExperiment, summarizeVoiceAssistantRuns } from './assistant';
5
5
  export { createVoiceAssistantHealthHTMLHandler, createVoiceAssistantHealthJSONHandler, createVoiceAssistantHealthRoutes, renderVoiceAssistantHealthHTML, summarizeVoiceAssistantHealth } from './assistantHealth';
6
6
  export { createVoiceBargeInRoutes, renderVoiceBargeInHTML, summarizeVoiceBargeIn } from './bargeInRoutes';
package/dist/index.js CHANGED
@@ -10628,6 +10628,112 @@ var summarizeVoiceCampaigns = (records) => {
10628
10628
  }
10629
10629
  return summary;
10630
10630
  };
10631
+ var buildVoiceCampaignObservabilityReport = async (records, options = {}) => {
10632
+ const now = options.now ?? Date.now();
10633
+ const stuckAfterMs = Math.max(1, options.stuckAfterMs ?? 15 * 60000);
10634
+ const rateWindowMs = Math.max(1, options.rateWindowMs ?? 60 * 60000);
10635
+ const rateWindowStart = now - rateWindowMs;
10636
+ const failureReasons = new Map;
10637
+ const report = {
10638
+ attemptRate: {
10639
+ failed: 0,
10640
+ started: 0,
10641
+ succeeded: 0,
10642
+ windowMs: rateWindowMs
10643
+ },
10644
+ campaigns: [],
10645
+ failureReasons: [],
10646
+ generatedAt: now,
10647
+ leases: {
10648
+ active: 0,
10649
+ known: Boolean(options.leases)
10650
+ },
10651
+ queue: {
10652
+ activeAttempts: 0,
10653
+ queuedRecipients: 0,
10654
+ runningCampaigns: 0
10655
+ },
10656
+ stuck: {
10657
+ attempts: [],
10658
+ recipients: []
10659
+ },
10660
+ summary: summarizeVoiceCampaigns(records)
10661
+ };
10662
+ for (const record of records) {
10663
+ const campaignId = record.campaign.id;
10664
+ const queuedRecipients = record.recipients.filter((recipient) => recipient.status === "queued");
10665
+ const activeAttempts = record.attempts.filter((attempt) => attempt.status === "queued" || attempt.status === "running");
10666
+ const campaignReport = {
10667
+ activeAttempts: activeAttempts.length,
10668
+ campaignId,
10669
+ name: record.campaign.name,
10670
+ queueDepth: queuedRecipients.length,
10671
+ status: record.campaign.status,
10672
+ stuckAttempts: 0,
10673
+ stuckRecipients: 0,
10674
+ updatedAt: record.campaign.updatedAt
10675
+ };
10676
+ if (record.campaign.status === "running") {
10677
+ report.queue.runningCampaigns += 1;
10678
+ }
10679
+ report.queue.queuedRecipients += queuedRecipients.length;
10680
+ report.queue.activeAttempts += activeAttempts.length;
10681
+ for (const recipient of record.recipients) {
10682
+ if ((recipient.status === "pending" || recipient.status === "queued") && now - recipient.updatedAt >= stuckAfterMs) {
10683
+ campaignReport.stuckRecipients += 1;
10684
+ report.stuck.recipients.push({
10685
+ campaignId,
10686
+ recipientId: recipient.id,
10687
+ status: recipient.status,
10688
+ updatedAt: recipient.updatedAt
10689
+ });
10690
+ }
10691
+ if (recipient.error) {
10692
+ failureReasons.set(recipient.error, (failureReasons.get(recipient.error) ?? 0) + 1);
10693
+ }
10694
+ }
10695
+ for (const attempt of record.attempts) {
10696
+ if ((attempt.startedAt ?? attempt.createdAt) >= rateWindowStart) {
10697
+ report.attemptRate.started += 1;
10698
+ }
10699
+ if (attempt.status === "failed" && attempt.updatedAt >= rateWindowStart) {
10700
+ report.attemptRate.failed += 1;
10701
+ }
10702
+ if (attempt.status === "succeeded" && attempt.updatedAt >= rateWindowStart) {
10703
+ report.attemptRate.succeeded += 1;
10704
+ }
10705
+ if (attempt.error) {
10706
+ failureReasons.set(attempt.error, (failureReasons.get(attempt.error) ?? 0) + 1);
10707
+ }
10708
+ if ((attempt.status === "queued" || attempt.status === "running") && now - attempt.updatedAt >= stuckAfterMs) {
10709
+ campaignReport.stuckAttempts += 1;
10710
+ report.stuck.attempts.push({
10711
+ attemptId: attempt.id,
10712
+ campaignId,
10713
+ recipientId: attempt.recipientId,
10714
+ status: attempt.status,
10715
+ updatedAt: attempt.updatedAt
10716
+ });
10717
+ }
10718
+ }
10719
+ if (options.leases) {
10720
+ const lease = await options.leases.get(getCampaignLeaseTaskId(campaignId));
10721
+ if (lease) {
10722
+ report.leases.active += 1;
10723
+ campaignReport.lease = {
10724
+ expiresAt: lease.expiresAt,
10725
+ workerId: lease.workerId
10726
+ };
10727
+ }
10728
+ }
10729
+ report.campaigns.push(campaignReport);
10730
+ }
10731
+ report.failureReasons = [...failureReasons.entries()].map(([reason, count]) => ({ count, reason })).sort((left, right) => right.count === left.count ? left.reason.localeCompare(right.reason) : right.count - left.count);
10732
+ report.campaigns.sort((left, right) => right.updatedAt - left.updatedAt);
10733
+ report.stuck.attempts.sort((left, right) => left.updatedAt - right.updatedAt);
10734
+ report.stuck.recipients.sort((left, right) => left.updatedAt - right.updatedAt);
10735
+ return report;
10736
+ };
10631
10737
  var createVoiceCampaign = (options) => {
10632
10738
  const { store } = options;
10633
10739
  return {
@@ -10794,6 +10900,90 @@ var createVoiceCampaign = (options) => {
10794
10900
  }
10795
10901
  };
10796
10902
  };
10903
+ var getCampaignLeaseTaskId = (campaignId) => `voice-campaign:${campaignId}`;
10904
+ var createVoiceCampaignWorker = (options) => {
10905
+ const leaseMs = Math.max(1, options.leaseMs ?? 30000);
10906
+ const runtime = options.runtime ?? createVoiceCampaign({
10907
+ store: options.store ?? createVoiceMemoryCampaignStore()
10908
+ });
10909
+ const statuses = options.statuses ?? ["running"];
10910
+ const campaignIds = new Set(options.campaignIds);
10911
+ const maxCampaigns = Math.max(1, options.maxCampaigns ?? Number.MAX_SAFE_INTEGER);
10912
+ return {
10913
+ drain: async () => {
10914
+ const result = {
10915
+ attempted: 0,
10916
+ campaigns: 0,
10917
+ errors: [],
10918
+ skipped: 0,
10919
+ started: []
10920
+ };
10921
+ const campaigns = [...await runtime.list()].filter((record) => (campaignIds.size === 0 || campaignIds.has(record.campaign.id)) && statuses.includes(record.campaign.status)).sort((left, right) => left.campaign.createdAt - right.campaign.createdAt).slice(0, maxCampaigns);
10922
+ for (const record of campaigns) {
10923
+ const campaignId = record.campaign.id;
10924
+ const taskId = getCampaignLeaseTaskId(campaignId);
10925
+ const claimed = await options.leases.claim({
10926
+ leaseMs,
10927
+ taskId,
10928
+ workerId: options.workerId
10929
+ });
10930
+ if (!claimed) {
10931
+ result.skipped += 1;
10932
+ continue;
10933
+ }
10934
+ try {
10935
+ const tick = await runtime.tick(campaignId);
10936
+ result.campaigns += 1;
10937
+ result.attempted += tick.attempted;
10938
+ result.started.push(...tick.started);
10939
+ result.errors.push(...tick.errors.map((error) => ({
10940
+ campaignId,
10941
+ error: error.error
10942
+ })));
10943
+ } catch (error) {
10944
+ result.errors.push({
10945
+ campaignId,
10946
+ error: error instanceof Error ? error.message : String(error)
10947
+ });
10948
+ } finally {
10949
+ await options.leases.release({
10950
+ taskId,
10951
+ workerId: options.workerId
10952
+ });
10953
+ }
10954
+ }
10955
+ return result;
10956
+ }
10957
+ };
10958
+ };
10959
+ var createVoiceCampaignWorkerLoop = (options) => {
10960
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
10961
+ let timer;
10962
+ let running = false;
10963
+ const tick = async () => options.worker.drain();
10964
+ return {
10965
+ isRunning: () => running,
10966
+ start: () => {
10967
+ if (timer) {
10968
+ return;
10969
+ }
10970
+ running = true;
10971
+ timer = setInterval(() => {
10972
+ tick().catch((error) => {
10973
+ options.onError?.(error);
10974
+ });
10975
+ }, pollIntervalMs);
10976
+ },
10977
+ stop: () => {
10978
+ if (timer) {
10979
+ clearInterval(timer);
10980
+ timer = undefined;
10981
+ }
10982
+ running = false;
10983
+ },
10984
+ tick
10985
+ };
10986
+ };
10797
10987
  var defaultProofRecipients = () => [
10798
10988
  {
10799
10989
  id: "campaign-proof-recipient-1",
@@ -10869,6 +11059,12 @@ var renderVoiceCampaignsHTML = (records, options = {}) => {
10869
11059
  const summary = summarizeVoiceCampaigns(records);
10870
11060
  return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#111827;color:#f9fafb;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1080px;padding:32px}.hero{background:linear-gradient(135deg,rgba(251,146,60,.18),rgba(45,212,191,.12));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#fdba74;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.4rem,6vw,5rem);line-height:.9;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));margin:18px 0}.grid article,table{background:#172033;border:1px solid #334155;border-radius:18px}.grid article{padding:16px}.grid span{color:#aab5c0}.grid strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}</style></head><body><main><section class="hero"><p class="eyebrow">Self-hosted outbound</p><h1>${escapeHtml18(title)}</h1><p>Campaign orchestration, recipients, attempts, retries, and outcomes without a hosted dialer dashboard.</p><section class="grid"><article><span>Campaigns</span><strong>${String(summary.campaigns.total)}</strong></article><article><span>Recipients</span><strong>${String(summary.recipients.total)}</strong></article><article><span>Attempts</span><strong>${String(summary.attempts.total)}</strong></article><article><span>Running</span><strong>${String(summary.campaigns.running)}</strong></article></section></section><table><thead><tr><th>Name</th><th>Status</th><th>Recipients</th><th>Attempts</th><th>Updated</th></tr></thead><tbody>${rows || '<tr><td colspan="5">No campaigns yet.</td></tr>'}</tbody></table></main></body></html>`;
10871
11061
  };
11062
+ var renderVoiceCampaignObservabilityHTML = (report, options = {}) => {
11063
+ const title = options.title ?? "Voice Campaign Observability";
11064
+ const campaignRows = report.campaigns.map((campaign) => `<tr><td>${escapeHtml18(campaign.name)}</td><td>${escapeHtml18(campaign.status)}</td><td>${String(campaign.queueDepth)}</td><td>${String(campaign.activeAttempts)}</td><td>${String(campaign.stuckRecipients + campaign.stuckAttempts)}</td><td>${campaign.lease ? escapeHtml18(campaign.lease.workerId) : "none"}</td></tr>`).join("");
11065
+ const failureRows = report.failureReasons.map((failure) => `<tr><td>${escapeHtml18(failure.reason)}</td><td>${String(failure.count)}</td></tr>`).join("");
11066
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>${escapeHtml18(title)}</title><style>body{background:#0b1220;color:#e5edf7;font-family:ui-sans-serif,system-ui,sans-serif;margin:0}main{margin:auto;max-width:1120px;padding:32px}.hero{background:linear-gradient(135deg,rgba(20,184,166,.2),rgba(251,146,60,.14));border:1px solid #334155;border-radius:28px;margin-bottom:18px;padding:28px}.eyebrow{color:#5eead4;font-weight:900;letter-spacing:.12em;text-transform:uppercase}h1{font-size:clamp(2.2rem,5vw,4.6rem);line-height:.95;margin:.2rem 0 1rem}.grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));margin:18px 0}.card,table{background:#111c2f;border:1px solid #334155;border-radius:18px}.card{padding:16px}.card span{color:#9fb0c5}.card strong{display:block;font-size:2rem;margin:.25rem 0}table{border-collapse:collapse;margin-top:18px;overflow:hidden;width:100%}td,th{border-bottom:1px solid #334155;padding:12px;text-align:left}.warn{color:#fde68a}.bad{color:#fecaca}</style></head><body><main><section class="hero"><p class="eyebrow">Campaign ops</p><h1>${escapeHtml18(title)}</h1><p>Queue depth, active leases, attempt rates, failure reasons, and stuck work for self-hosted outbound voice.</p><section class="grid"><article class="card"><span>Queued recipients</span><strong>${String(report.queue.queuedRecipients)}</strong></article><article class="card"><span>Active attempts</span><strong>${String(report.queue.activeAttempts)}</strong></article><article class="card"><span>Running campaigns</span><strong>${String(report.queue.runningCampaigns)}</strong></article><article class="card"><span>Active leases</span><strong>${report.leases.known ? String(report.leases.active) : "n/a"}</strong></article><article class="card"><span>Attempts/window</span><strong>${String(report.attemptRate.started)}</strong></article><article class="card"><span>Stuck work</span><strong class="${report.stuck.attempts.length + report.stuck.recipients.length > 0 ? "bad" : ""}">${String(report.stuck.attempts.length + report.stuck.recipients.length)}</strong></article></section></section><h2>Campaigns</h2><table><thead><tr><th>Name</th><th>Status</th><th>Queued</th><th>Active</th><th>Stuck</th><th>Lease</th></tr></thead><tbody>${campaignRows || '<tr><td colspan="6">No campaigns yet.</td></tr>'}</tbody></table><h2>Failure Reasons</h2><table><thead><tr><th>Reason</th><th>Count</th></tr></thead><tbody>${failureRows || '<tr><td colspan="2">No failures recorded.</td></tr>'}</tbody></table></main></body></html>`;
11067
+ };
10872
11068
  var readJsonBody = async (request) => {
10873
11069
  const text = await request.text();
10874
11070
  return text.trim() ? JSON.parse(text) : {};
@@ -10880,7 +11076,7 @@ var createVoiceCampaignRoutes = (options) => {
10880
11076
  const app = new Elysia17({ name: options.name ?? "absolutejs-voice-campaigns" }).get(path, async () => ({
10881
11077
  campaigns: await runtime.list(),
10882
11078
  summary: await runtime.summarize()
10883
- })).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
11079
+ })).get(`${path}/observability`, async () => buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability)).post(path, async ({ request }) => runtime.create(await readJsonBody(request))).get(`${path}/:campaignId`, ({ params }) => runtime.get(params.campaignId)).delete(`${path}/:campaignId`, async ({ params }) => {
10884
11080
  await runtime.remove(params.campaignId);
10885
11081
  return { ok: true };
10886
11082
  }).post(`${path}/:campaignId/recipients`, async ({ params, request }) => {
@@ -10899,6 +11095,14 @@ var createVoiceCampaignRoutes = (options) => {
10899
11095
  ...options.headers
10900
11096
  }
10901
11097
  });
11098
+ }).get(`${htmlPath}/observability`, async () => {
11099
+ const report = await buildVoiceCampaignObservabilityReport(await runtime.list(), options.observability);
11100
+ return new Response(renderVoiceCampaignObservabilityHTML(report, options), {
11101
+ headers: {
11102
+ "content-type": "text/html; charset=utf-8",
11103
+ ...options.headers
11104
+ }
11105
+ });
10902
11106
  });
10903
11107
  }
10904
11108
  return app;
@@ -18949,6 +19153,7 @@ export {
18949
19153
  renderVoiceEvalHTML,
18950
19154
  renderVoiceEvalBaselineHTML,
18951
19155
  renderVoiceCampaignsHTML,
19156
+ renderVoiceCampaignObservabilityHTML,
18952
19157
  renderVoiceCallReviewMarkdown,
18953
19158
  renderVoiceCallReviewHTML,
18954
19159
  renderVoiceBargeInHTML,
@@ -19123,6 +19328,8 @@ export {
19123
19328
  createVoiceExperiment,
19124
19329
  createVoiceEvalRoutes,
19125
19330
  createVoiceDiagnosticsRoutes,
19331
+ createVoiceCampaignWorkerLoop,
19332
+ createVoiceCampaignWorker,
19126
19333
  createVoiceCampaignRoutes,
19127
19334
  createVoiceCampaign,
19128
19335
  createVoiceCallReviewRecorder,
@@ -19176,6 +19383,7 @@ export {
19176
19383
  buildVoiceOpsTaskFromReview,
19177
19384
  buildVoiceOpsConsoleReport,
19178
19385
  buildVoiceDiagnosticsMarkdown,
19386
+ buildVoiceCampaignObservabilityReport,
19179
19387
  assignVoiceOpsTask,
19180
19388
  applyVoiceTelephonyOutcome,
19181
19389
  applyVoiceOpsTaskPolicy,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.111",
3
+ "version": "0.0.22-beta.113",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",