@absolutejs/voice 0.0.22-beta.30 → 0.0.22-beta.31

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/handoff.d.ts CHANGED
@@ -1,14 +1,24 @@
1
- import type { VoiceHandoffAction, VoiceHandoffAdapter, VoiceHandoffConfig, VoiceHandoffInput, VoiceHandoffResult, VoiceSessionRecord } from './types';
1
+ import type { VoiceHandoffAction, VoiceHandoffAdapter, VoiceHandoffConfig, VoiceHandoffDeliveryStore, VoiceHandoffInput, VoiceHandoffResult, VoiceSessionRecord, StoredVoiceHandoffDelivery } from './types';
2
2
  type MaybePromise<T> = T | Promise<T>;
3
3
  export type VoiceHandoffDelivery = VoiceHandoffResult & {
4
4
  adapterId: string;
5
5
  adapterKind?: string;
6
6
  };
7
+ export type VoiceHandoffDeliveryRecord<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = StoredVoiceHandoffDelivery<TContext, TSession, TResult>;
7
8
  export type VoiceHandoffFanoutResult = {
8
9
  action: VoiceHandoffAction;
9
10
  deliveries: Record<string, VoiceHandoffDelivery>;
10
11
  status: VoiceHandoffResult['status'];
11
12
  };
13
+ export type VoiceHandoffDeliveryRecordInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = Omit<VoiceHandoffInput<TContext, TSession, TResult>, 'api'> & {
14
+ id?: string;
15
+ };
16
+ export type VoiceQueuedHandoffDeliveryOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
17
+ adapters: VoiceHandoffAdapter<TContext, TSession, TResult>[];
18
+ api: VoiceHandoffInput<TContext, TSession, TResult>['api'];
19
+ delivery: VoiceHandoffDeliveryRecord<TContext, TSession, TResult>;
20
+ failMode?: VoiceHandoffConfig<TContext, TSession, TResult>['failMode'];
21
+ };
12
22
  export type VoiceWebhookHandoffAdapterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
13
23
  actions?: VoiceHandoffAction[];
14
24
  body?: (input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<Record<string, unknown>>;
@@ -35,6 +45,10 @@ export declare const deliverVoiceHandoff: <TContext, TSession extends VoiceSessi
35
45
  config?: VoiceHandoffConfig<TContext, TSession, TResult>;
36
46
  handoff: VoiceHandoffInput<TContext, TSession, TResult>;
37
47
  }) => Promise<VoiceHandoffFanoutResult | undefined>;
48
+ export declare const createVoiceHandoffDeliveryRecord: <TContext, TSession extends VoiceSessionRecord, TResult>(input: VoiceHandoffDeliveryRecordInput<TContext, TSession, TResult>) => VoiceHandoffDeliveryRecord<TContext, TSession, TResult>;
49
+ export declare const applyVoiceHandoffDeliveryResult: <TContext, TSession extends VoiceSessionRecord, TResult>(delivery: VoiceHandoffDeliveryRecord<TContext, TSession, TResult>, result: VoiceHandoffFanoutResult) => VoiceHandoffDeliveryRecord<TContext, TSession, TResult>;
50
+ export declare const deliverVoiceHandoffDelivery: <TContext, TSession extends VoiceSessionRecord, TResult>(options: VoiceQueuedHandoffDeliveryOptions<TContext, TSession, TResult>) => Promise<VoiceHandoffDeliveryRecord<TContext, TSession, TResult>>;
51
+ export declare const createVoiceMemoryHandoffDeliveryStore: <TDelivery extends VoiceHandoffDeliveryRecord = VoiceHandoffDeliveryRecord>() => VoiceHandoffDeliveryStore<TDelivery>;
38
52
  export declare const createVoiceWebhookHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceWebhookHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
39
53
  export declare const createVoiceTwilioRedirectHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceTwilioRedirectHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
40
54
  export {};
package/dist/index.d.ts CHANGED
@@ -14,9 +14,9 @@ export { createVoiceS3ReviewStore } from './s3Store';
14
14
  export { createVoiceMemoryStore } from './memoryStore';
15
15
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks } from './opsSinks';
16
16
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature } from './opsWebhook';
17
- export { createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff } from './handoff';
17
+ export { applyVoiceHandoffDeliveryResult, createVoiceHandoffDeliveryRecord, createVoiceMemoryHandoffDeliveryStore, createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff, deliverVoiceHandoffDelivery } from './handoff';
18
18
  export { createVoiceHandoffHealthHTMLHandler, createVoiceHandoffHealthJSONHandler, createVoiceHandoffHealthRoutes, renderVoiceHandoffHealthHTML, summarizeVoiceHandoffHealth } from './handoffHealth';
19
- export { createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
19
+ export { createVoiceHandoffDeliveryWorker, createVoiceHandoffDeliveryWorkerLoop, createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceHandoffDeliveries, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
20
20
  export { assignVoiceOpsTask, applyVoiceOpsTaskAssignmentRule, applyVoiceOpsTaskPolicy, buildVoiceOpsTaskFromReview, buildVoiceOpsTaskFromSLABreach, claimVoiceOpsTask, completeVoiceOpsTask, createVoiceExternalObjectMap, createVoiceExternalObjectMapId, createVoiceCallCompletedEvent, createVoiceTaskSLABreachedEvent, deadLetterVoiceOpsTask, deliverVoiceIntegrationEvent, failVoiceOpsTask, hasVoiceOpsTaskSLABreach, heartbeatVoiceOpsTask, isVoiceOpsTaskOverdue, markVoiceOpsTaskSLABreached, matchesVoiceOpsTaskAssignmentRule, resolveVoiceOpsTaskAgeBucket, createVoiceIntegrationEvent, createVoiceReviewSavedEvent, resolveVoiceOpsTaskAssignment, resolveVoiceOpsTaskPolicy, requeueVoiceOpsTask, createVoiceTaskCreatedEvent, createVoiceTaskUpdatedEvent, listVoiceOpsTasks, reopenVoiceOpsTask, startVoiceOpsTask, summarizeVoiceOpsTaskAnalytics, summarizeVoiceOpsTasks, withVoiceIntegrationEventId, withVoiceOpsTaskId } from './ops';
21
21
  export { createVoiceSession } from './session';
22
22
  export { createVoiceCallReviewFromSession, recordVoiceRuntimeOps } from './runtimeOps';
@@ -42,13 +42,13 @@ export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPrese
42
42
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
43
43
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
44
44
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
45
- export type { VoiceHandoffDelivery, VoiceHandoffFanoutResult, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
45
+ export type { VoiceHandoffDelivery, VoiceHandoffDeliveryRecord, VoiceHandoffDeliveryRecordInput, VoiceHandoffFanoutResult, VoiceQueuedHandoffDeliveryOptions, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
46
46
  export type { VoiceHandoffHealthDelivery, VoiceHandoffHealthEvent, VoiceHandoffHealthHTMLHandlerOptions, VoiceHandoffHealthRoutesOptions, VoiceHandoffHealthStatus, VoiceHandoffHealthSummary, VoiceHandoffHealthSummaryOptions } from './handoffHealth';
47
47
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
48
48
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
49
49
  export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
50
50
  export type { VoicePostgresClient, VoicePostgresRuntimeStorage, VoicePostgresStoreOptions } from './postgresStore';
51
- export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
51
+ export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceHandoffDeliveryQueueSummary, VoiceHandoffDeliveryWorkerLoop, VoiceHandoffDeliveryWorkerLoopOptions, VoiceHandoffDeliveryWorkerOptions, VoiceHandoffDeliveryWorkerResult, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
52
52
  export type { VoiceS3ReviewStoreClient, VoiceS3ReviewStoreFile, VoiceS3ReviewStoreOptions } from './s3Store';
53
53
  export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqliteStore';
54
54
  export type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceOpsTaskAgeBucket, VoiceOpsTaskAnalyticsOptions, VoiceOpsTaskAnalyticsSummary, VoiceOpsTaskAssignmentRule, VoiceOpsTaskAssignmentRuleCondition, VoiceOpsTaskAssignmentRules, VoiceOpsTaskAssigneeAnalytics, VoiceOpsDispositionTaskPolicies, VoiceOpsSLABreachPolicy, VoiceIntegrationDeliveryStatus, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceIntegrationSinkDelivery, VoiceIntegrationEventType, VoiceIntegrationWebhookConfig, VoiceOpsTask, VoiceOpsTaskHistoryEntry, VoiceOpsTaskKind, VoiceOpsTaskPolicy, VoiceOpsTaskPriority, VoiceOpsTaskStatus, VoiceOpsTaskStore, VoiceOpsTaskSummary, VoiceOpsTaskWorkerAnalytics } from './ops';
package/dist/index.js CHANGED
@@ -3019,6 +3019,14 @@ var aggregateHandoffStatus = (deliveries) => {
3019
3019
  }
3020
3020
  return "skipped";
3021
3021
  };
3022
+ var createHandoffDeliveryId = (input) => [
3023
+ "voice-handoff",
3024
+ input.sessionId,
3025
+ input.action,
3026
+ Date.now(),
3027
+ crypto.randomUUID()
3028
+ ].join(":");
3029
+ var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
3022
3030
  var defaultWebhookBody = (input) => ({
3023
3031
  action: input.action,
3024
3032
  metadata: input.metadata,
@@ -3067,6 +3075,73 @@ var deliverVoiceHandoff = async (input) => {
3067
3075
  status: aggregateHandoffStatus(deliveries)
3068
3076
  };
3069
3077
  };
3078
+ var createVoiceHandoffDeliveryRecord = (input) => {
3079
+ const now = Date.now();
3080
+ return {
3081
+ action: input.action,
3082
+ context: input.context,
3083
+ createdAt: now,
3084
+ deliveryAttempts: 0,
3085
+ deliveryStatus: "pending",
3086
+ id: input.id ?? createHandoffDeliveryId({
3087
+ action: input.action,
3088
+ sessionId: input.session.id
3089
+ }),
3090
+ metadata: input.metadata,
3091
+ reason: input.reason,
3092
+ result: input.result,
3093
+ session: input.session,
3094
+ sessionId: input.session.id,
3095
+ target: input.target,
3096
+ updatedAt: now
3097
+ };
3098
+ };
3099
+ var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
3100
+ ...delivery,
3101
+ deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
3102
+ deliveries: result.deliveries,
3103
+ deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
3104
+ deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
3105
+ deliveryStatus: result.status,
3106
+ updatedAt: Date.now()
3107
+ });
3108
+ var deliverVoiceHandoffDelivery = async (options) => {
3109
+ const result = await deliverVoiceHandoff({
3110
+ config: {
3111
+ adapters: options.adapters,
3112
+ failMode: options.failMode
3113
+ },
3114
+ handoff: {
3115
+ action: options.delivery.action,
3116
+ api: options.api,
3117
+ context: options.delivery.context,
3118
+ metadata: options.delivery.metadata,
3119
+ reason: options.delivery.reason,
3120
+ result: options.delivery.result,
3121
+ session: options.delivery.session,
3122
+ target: options.delivery.target
3123
+ }
3124
+ });
3125
+ return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
3126
+ ...options.delivery,
3127
+ deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
3128
+ deliveryStatus: "skipped",
3129
+ updatedAt: Date.now()
3130
+ };
3131
+ };
3132
+ var createVoiceMemoryHandoffDeliveryStore = () => {
3133
+ const deliveries = new Map;
3134
+ return {
3135
+ get: async (id) => deliveries.get(id),
3136
+ list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
3137
+ remove: async (id) => {
3138
+ deliveries.delete(id);
3139
+ },
3140
+ set: async (id, delivery) => {
3141
+ deliveries.set(id, delivery);
3142
+ }
3143
+ };
3144
+ };
3070
3145
  var createVoiceWebhookHandoffAdapter = (options) => ({
3071
3146
  actions: options.actions,
3072
3147
  handoff: async (input) => {
@@ -3609,6 +3684,21 @@ var createVoiceSession = (options) => {
3609
3684
  });
3610
3685
  };
3611
3686
  const runHandoff = async (input) => {
3687
+ const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
3688
+ action: input.action,
3689
+ context: options.context,
3690
+ metadata: input.metadata,
3691
+ reason: input.reason,
3692
+ result: input.result,
3693
+ session: input.session,
3694
+ target: input.target
3695
+ }) : undefined;
3696
+ if (queuedDelivery) {
3697
+ await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
3698
+ }
3699
+ if (options.handoff?.enqueueOnly) {
3700
+ return;
3701
+ }
3612
3702
  const result = await deliverVoiceHandoff({
3613
3703
  config: options.handoff,
3614
3704
  handoff: {
@@ -3625,6 +3715,10 @@ var createVoiceSession = (options) => {
3625
3715
  if (!result) {
3626
3716
  return;
3627
3717
  }
3718
+ if (queuedDelivery) {
3719
+ const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
3720
+ await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
3721
+ }
3628
3722
  await appendTrace({
3629
3723
  metadata: input.metadata,
3630
3724
  payload: {
@@ -9625,6 +9719,8 @@ var shouldDeadLetterSinkEvent = (event, sinks, maxFailures) => typeof maxFailure
9625
9719
  var shouldDeadLetterTask = (task, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (task.processingAttempts ?? 0) >= maxFailures;
9626
9720
  var shouldProcessTraceDeliveryStatus = (status, allowed) => allowed.includes(status);
9627
9721
  var shouldDeadLetterTraceDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
9722
+ var shouldProcessHandoffDeliveryStatus = (status, allowed) => allowed.includes(status);
9723
+ var shouldDeadLetterHandoffDelivery = (delivery, maxFailures) => typeof maxFailures === "number" && maxFailures > 0 && (delivery.deliveryAttempts ?? 0) >= maxFailures;
9628
9724
  var summarizeVoiceIntegrationEvents = (events, input = {}) => {
9629
9725
  const buildSummary = async () => {
9630
9726
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((event) => event.id) : []);
@@ -9706,6 +9802,48 @@ var summarizeVoiceTraceSinkDeliveries = (deliveries, input = {}) => {
9706
9802
  };
9707
9803
  return buildSummary();
9708
9804
  };
9805
+ var summarizeVoiceHandoffDeliveries = (deliveries, input = {}) => {
9806
+ const buildSummary = async () => {
9807
+ const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((delivery) => delivery.id) : []);
9808
+ const byAction = new Map;
9809
+ const summary = {
9810
+ byAction: [],
9811
+ deadLettered: 0,
9812
+ delivered: 0,
9813
+ failed: 0,
9814
+ pending: 0,
9815
+ retryEligible: 0,
9816
+ skipped: 0,
9817
+ total: deliveries.length
9818
+ };
9819
+ for (const delivery of deliveries) {
9820
+ byAction.set(delivery.action, (byAction.get(delivery.action) ?? 0) + 1);
9821
+ if (deadLetterIds.has(delivery.id)) {
9822
+ summary.deadLettered += 1;
9823
+ }
9824
+ switch (delivery.deliveryStatus) {
9825
+ case "delivered":
9826
+ summary.delivered += 1;
9827
+ break;
9828
+ case "failed":
9829
+ summary.failed += 1;
9830
+ if ((delivery.deliveryAttempts ?? 0) > 0) {
9831
+ summary.retryEligible += 1;
9832
+ }
9833
+ break;
9834
+ case "skipped":
9835
+ summary.skipped += 1;
9836
+ break;
9837
+ case "pending":
9838
+ summary.pending += 1;
9839
+ break;
9840
+ }
9841
+ }
9842
+ summary.byAction = [...byAction.entries()].sort((left, right) => right[1] - left[1]);
9843
+ return summary;
9844
+ };
9845
+ return buildSummary();
9846
+ };
9709
9847
  var summarizeVoiceOpsTaskQueue = (tasks, input = {}) => {
9710
9848
  const buildSummary = async () => {
9711
9849
  const deadLetterIds = new Set(input.deadLetters ? (await input.deadLetters.list()).map((task) => task.id) : []);
@@ -10135,6 +10273,108 @@ var createVoiceTraceSinkDeliveryWorkerLoop = (options) => {
10135
10273
  tick
10136
10274
  };
10137
10275
  };
10276
+ var createVoiceHandoffDeliveryWorker = (options) => {
10277
+ const allowedStatuses = options.statuses ?? ["pending", "failed"];
10278
+ const leaseMs = Math.max(1, options.leaseMs ?? 30000);
10279
+ return {
10280
+ drain: async () => {
10281
+ const result = {
10282
+ alreadyProcessed: 0,
10283
+ attempted: 0,
10284
+ deadLettered: 0,
10285
+ delivered: 0,
10286
+ failed: 0,
10287
+ skipped: 0
10288
+ };
10289
+ const deliveries = [...await options.deliveries.list()].sort((left, right) => left.createdAt - right.createdAt);
10290
+ for (const delivery of deliveries) {
10291
+ if (!shouldProcessHandoffDeliveryStatus(delivery.deliveryStatus, allowedStatuses)) {
10292
+ continue;
10293
+ }
10294
+ if (shouldDeadLetterHandoffDelivery(delivery, options.maxFailures)) {
10295
+ await options.deadLetters?.set(delivery.id, delivery);
10296
+ await options.onDeadLetter?.(delivery);
10297
+ result.deadLettered += 1;
10298
+ continue;
10299
+ }
10300
+ const claimed = await options.leases.claim({
10301
+ leaseMs,
10302
+ taskId: delivery.id,
10303
+ workerId: options.workerId
10304
+ });
10305
+ if (!claimed) {
10306
+ continue;
10307
+ }
10308
+ try {
10309
+ const idempotencyKey = `${delivery.id}:handoff`;
10310
+ if (options.idempotency && await options.idempotency.has(idempotencyKey)) {
10311
+ result.alreadyProcessed += 1;
10312
+ continue;
10313
+ }
10314
+ result.attempted += 1;
10315
+ const updatedDelivery = await deliverVoiceHandoffDelivery({
10316
+ adapters: options.adapters,
10317
+ api: options.api,
10318
+ delivery,
10319
+ failMode: options.failMode
10320
+ });
10321
+ await options.deliveries.set(updatedDelivery.id, updatedDelivery);
10322
+ if (updatedDelivery.deliveryStatus === "delivered" || updatedDelivery.deliveryStatus === "skipped") {
10323
+ await options.idempotency?.set(idempotencyKey, {
10324
+ ttlSeconds: options.idempotencyTtlSeconds
10325
+ });
10326
+ }
10327
+ if (updatedDelivery.deliveryStatus === "delivered") {
10328
+ result.delivered += 1;
10329
+ } else if (updatedDelivery.deliveryStatus === "skipped") {
10330
+ result.skipped += 1;
10331
+ } else if (updatedDelivery.deliveryStatus === "failed") {
10332
+ result.failed += 1;
10333
+ if (shouldDeadLetterHandoffDelivery(updatedDelivery, options.maxFailures)) {
10334
+ await options.deadLetters?.set(updatedDelivery.id, updatedDelivery);
10335
+ await options.onDeadLetter?.(updatedDelivery);
10336
+ result.deadLettered += 1;
10337
+ }
10338
+ }
10339
+ } finally {
10340
+ await options.leases.release({
10341
+ taskId: delivery.id,
10342
+ workerId: options.workerId
10343
+ });
10344
+ }
10345
+ }
10346
+ return result;
10347
+ }
10348
+ };
10349
+ };
10350
+ var createVoiceHandoffDeliveryWorkerLoop = (options) => {
10351
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 1000);
10352
+ let timer;
10353
+ let running = false;
10354
+ const tick = async () => options.worker.drain();
10355
+ return {
10356
+ isRunning: () => running,
10357
+ start: () => {
10358
+ if (timer) {
10359
+ return;
10360
+ }
10361
+ running = true;
10362
+ timer = setInterval(() => {
10363
+ tick().catch((error) => {
10364
+ options.onError?.(error);
10365
+ });
10366
+ }, pollIntervalMs);
10367
+ },
10368
+ stop: () => {
10369
+ if (timer) {
10370
+ clearInterval(timer);
10371
+ timer = undefined;
10372
+ }
10373
+ running = false;
10374
+ },
10375
+ tick
10376
+ };
10377
+ };
10138
10378
  var createVoiceOpsTaskWorker = (options) => {
10139
10379
  const leaseMs = Math.max(1, options.leaseMs ?? 30000);
10140
10380
  const getTask = async (taskId) => {
@@ -11532,6 +11772,7 @@ export {
11532
11772
  summarizeVoiceOpsTaskAnalytics,
11533
11773
  summarizeVoiceIntegrationEvents,
11534
11774
  summarizeVoiceHandoffHealth,
11775
+ summarizeVoiceHandoffDeliveries,
11535
11776
  summarizeVoiceAssistantRuns,
11536
11777
  summarizeVoiceAssistantHealth,
11537
11778
  startVoiceOpsTask,
@@ -11577,6 +11818,7 @@ export {
11577
11818
  deliverVoiceTraceEventsToSinks,
11578
11819
  deliverVoiceIntegrationEventToSinks,
11579
11820
  deliverVoiceIntegrationEvent,
11821
+ deliverVoiceHandoffDelivery,
11580
11822
  deliverVoiceHandoff,
11581
11823
  decodeTwilioMulawBase64,
11582
11824
  deadLetterVoiceOpsTask,
@@ -11641,6 +11883,7 @@ export {
11641
11883
  createVoiceMemoryTraceSinkDeliveryStore,
11642
11884
  createVoiceMemoryTraceEventStore,
11643
11885
  createVoiceMemoryStore,
11886
+ createVoiceMemoryHandoffDeliveryStore,
11644
11887
  createVoiceMemoryAssistantMemoryStore,
11645
11888
  createVoiceLinearIssueUpdateSink,
11646
11889
  createVoiceLinearIssueSyncSinks,
@@ -11656,6 +11899,9 @@ export {
11656
11899
  createVoiceHandoffHealthRoutes,
11657
11900
  createVoiceHandoffHealthJSONHandler,
11658
11901
  createVoiceHandoffHealthHTMLHandler,
11902
+ createVoiceHandoffDeliveryWorkerLoop,
11903
+ createVoiceHandoffDeliveryWorker,
11904
+ createVoiceHandoffDeliveryRecord,
11659
11905
  createVoiceFileTraceSinkDeliveryStore,
11660
11906
  createVoiceFileTraceEventStore,
11661
11907
  createVoiceFileTaskStore,
@@ -11706,6 +11952,7 @@ export {
11706
11952
  assignVoiceOpsTask,
11707
11953
  applyVoiceOpsTaskPolicy,
11708
11954
  applyVoiceOpsTaskAssignmentRule,
11955
+ applyVoiceHandoffDeliveryResult,
11709
11956
  applyRiskTieredPhraseHintCorrections,
11710
11957
  applyPhraseHintCorrections,
11711
11958
  TURN_PROFILE_DEFAULTS
package/dist/queue.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { RedisClient } from 'bun';
2
2
  import type { VoiceIntegrationSink } from './opsSinks';
3
3
  import type { VoiceTraceRedactionConfig, VoiceTraceSink, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryStore, VoiceTraceSinkDeliveryQueueStatus } from './trace';
4
+ import type { StoredVoiceHandoffDelivery, VoiceHandoffAdapter, VoiceHandoffDeliveryQueueStatus, VoiceHandoffDeliveryStore, VoiceSessionHandle, VoiceSessionRecord } from './types';
4
5
  import type { VoiceOpsTaskPriority, StoredVoiceOpsTask, StoredVoiceIntegrationEvent, VoiceIntegrationDeliveryStatus, VoiceIntegrationEventStore, VoiceIntegrationWebhookConfig, VoiceOpsTaskKind, VoiceOpsTaskStatus, VoiceOpsTaskStore } from './ops';
5
6
  export type VoiceOpsTaskLease = {
6
7
  expiresAt: number;
@@ -159,6 +160,50 @@ export type VoiceTraceSinkDeliveryQueueSummary = {
159
160
  skipped: number;
160
161
  total: number;
161
162
  };
163
+ export type VoiceHandoffDeliveryWorkerOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TDelivery extends StoredVoiceHandoffDelivery<TContext, TSession, TResult> = StoredVoiceHandoffDelivery<TContext, TSession, TResult>> = {
164
+ adapters: VoiceHandoffAdapter<TContext, TSession, TResult>[];
165
+ api: VoiceSessionHandle<TContext, TSession, TResult>;
166
+ deadLetters?: VoiceHandoffDeliveryStore<TDelivery>;
167
+ deliveries: VoiceHandoffDeliveryStore<TDelivery>;
168
+ failMode?: 'record' | 'throw';
169
+ idempotency?: VoiceIdempotencyStore;
170
+ idempotencyTtlSeconds?: number;
171
+ leaseMs?: number;
172
+ leases: VoiceRedisTaskLeaseCoordinator;
173
+ maxFailures?: number;
174
+ onDeadLetter?: (delivery: TDelivery) => Promise<void> | void;
175
+ statuses?: VoiceHandoffDeliveryQueueStatus[];
176
+ workerId: string;
177
+ };
178
+ export type VoiceHandoffDeliveryWorkerResult = {
179
+ alreadyProcessed: number;
180
+ attempted: number;
181
+ deadLettered: number;
182
+ delivered: number;
183
+ failed: number;
184
+ skipped: number;
185
+ };
186
+ export type VoiceHandoffDeliveryWorkerLoopOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TDelivery extends StoredVoiceHandoffDelivery<TContext, TSession, TResult> = StoredVoiceHandoffDelivery<TContext, TSession, TResult>> = {
187
+ onError?: (error: unknown) => Promise<void> | void;
188
+ pollIntervalMs?: number;
189
+ worker: ReturnType<typeof createVoiceHandoffDeliveryWorker<TContext, TSession, TResult, TDelivery>>;
190
+ };
191
+ export type VoiceHandoffDeliveryWorkerLoop = {
192
+ isRunning: () => boolean;
193
+ start: () => void;
194
+ stop: () => void;
195
+ tick: () => Promise<VoiceHandoffDeliveryWorkerResult>;
196
+ };
197
+ export type VoiceHandoffDeliveryQueueSummary = {
198
+ byAction: Array<[StoredVoiceHandoffDelivery['action'], number]>;
199
+ deadLettered: number;
200
+ delivered: number;
201
+ failed: number;
202
+ pending: number;
203
+ retryEligible: number;
204
+ skipped: number;
205
+ total: number;
206
+ };
162
207
  export type VoiceOpsTaskWorkerOptions<TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask> = {
163
208
  leaseMs?: number;
164
209
  leases: VoiceRedisTaskLeaseCoordinator;
@@ -252,6 +297,9 @@ export declare const summarizeVoiceIntegrationEvents: <TEvent extends StoredVoic
252
297
  export declare const summarizeVoiceTraceSinkDeliveries: <TDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(deliveries: TDelivery[], input?: {
253
298
  deadLetters?: VoiceTraceSinkDeliveryStore<TDelivery>;
254
299
  }) => Promise<VoiceTraceSinkDeliveryQueueSummary> | VoiceTraceSinkDeliveryQueueSummary;
300
+ export declare const summarizeVoiceHandoffDeliveries: <TDelivery extends StoredVoiceHandoffDelivery = StoredVoiceHandoffDelivery>(deliveries: TDelivery[], input?: {
301
+ deadLetters?: VoiceHandoffDeliveryStore<TDelivery>;
302
+ }) => Promise<VoiceHandoffDeliveryQueueSummary> | VoiceHandoffDeliveryQueueSummary;
255
303
  export declare const summarizeVoiceOpsTaskQueue: <TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask>(tasks: TTask[], input?: {
256
304
  deadLetters?: VoiceOpsTaskStore<TTask>;
257
305
  }) => Promise<VoiceOpsTaskQueueSummary> | VoiceOpsTaskQueueSummary;
@@ -269,6 +317,10 @@ export declare const createVoiceTraceSinkDeliveryWorker: <TDelivery extends Voic
269
317
  drain: () => Promise<VoiceTraceSinkDeliveryWorkerResult>;
270
318
  };
271
319
  export declare const createVoiceTraceSinkDeliveryWorkerLoop: <TDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(options: VoiceTraceSinkDeliveryWorkerLoopOptions<TDelivery>) => VoiceTraceSinkDeliveryWorkerLoop;
320
+ export declare const createVoiceHandoffDeliveryWorker: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TDelivery extends StoredVoiceHandoffDelivery<TContext, TSession, TResult> = StoredVoiceHandoffDelivery<TContext, TSession, TResult>>(options: VoiceHandoffDeliveryWorkerOptions<TContext, TSession, TResult, TDelivery>) => {
321
+ drain: () => Promise<VoiceHandoffDeliveryWorkerResult>;
322
+ };
323
+ export declare const createVoiceHandoffDeliveryWorkerLoop: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown, TDelivery extends StoredVoiceHandoffDelivery<TContext, TSession, TResult> = StoredVoiceHandoffDelivery<TContext, TSession, TResult>>(options: VoiceHandoffDeliveryWorkerLoopOptions<TContext, TSession, TResult, TDelivery>) => VoiceHandoffDeliveryWorkerLoop;
272
324
  export declare const createVoiceOpsTaskWorker: <TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask>(options: VoiceOpsTaskWorkerOptions<TTask>) => VoiceOpsTaskWorker<TTask>;
273
325
  export declare const createVoiceOpsTaskProcessorWorker: <TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask>(options: VoiceOpsTaskProcessorWorkerOptions<TTask>) => {
274
326
  drain: () => Promise<VoiceOpsTaskProcessorWorkerResult>;
@@ -4486,6 +4486,14 @@ var aggregateHandoffStatus = (deliveries) => {
4486
4486
  }
4487
4487
  return "skipped";
4488
4488
  };
4489
+ var createHandoffDeliveryId = (input) => [
4490
+ "voice-handoff",
4491
+ input.sessionId,
4492
+ input.action,
4493
+ Date.now(),
4494
+ crypto.randomUUID()
4495
+ ].join(":");
4496
+ var resolveHandoffDeliveryError = (deliveries) => Object.values(deliveries).map((delivery) => delivery.error).find(Boolean);
4489
4497
  var defaultWebhookBody = (input) => ({
4490
4498
  action: input.action,
4491
4499
  metadata: input.metadata,
@@ -4534,6 +4542,73 @@ var deliverVoiceHandoff = async (input) => {
4534
4542
  status: aggregateHandoffStatus(deliveries)
4535
4543
  };
4536
4544
  };
4545
+ var createVoiceHandoffDeliveryRecord = (input) => {
4546
+ const now = Date.now();
4547
+ return {
4548
+ action: input.action,
4549
+ context: input.context,
4550
+ createdAt: now,
4551
+ deliveryAttempts: 0,
4552
+ deliveryStatus: "pending",
4553
+ id: input.id ?? createHandoffDeliveryId({
4554
+ action: input.action,
4555
+ sessionId: input.session.id
4556
+ }),
4557
+ metadata: input.metadata,
4558
+ reason: input.reason,
4559
+ result: input.result,
4560
+ session: input.session,
4561
+ sessionId: input.session.id,
4562
+ target: input.target,
4563
+ updatedAt: now
4564
+ };
4565
+ };
4566
+ var applyVoiceHandoffDeliveryResult = (delivery, result) => ({
4567
+ ...delivery,
4568
+ deliveredAt: result.status === "delivered" || result.status === "skipped" ? Date.now() : delivery.deliveredAt,
4569
+ deliveries: result.deliveries,
4570
+ deliveryAttempts: (delivery.deliveryAttempts ?? 0) + 1,
4571
+ deliveryError: result.status === "failed" ? resolveHandoffDeliveryError(result.deliveries) : undefined,
4572
+ deliveryStatus: result.status,
4573
+ updatedAt: Date.now()
4574
+ });
4575
+ var deliverVoiceHandoffDelivery = async (options) => {
4576
+ const result = await deliverVoiceHandoff({
4577
+ config: {
4578
+ adapters: options.adapters,
4579
+ failMode: options.failMode
4580
+ },
4581
+ handoff: {
4582
+ action: options.delivery.action,
4583
+ api: options.api,
4584
+ context: options.delivery.context,
4585
+ metadata: options.delivery.metadata,
4586
+ reason: options.delivery.reason,
4587
+ result: options.delivery.result,
4588
+ session: options.delivery.session,
4589
+ target: options.delivery.target
4590
+ }
4591
+ });
4592
+ return result ? applyVoiceHandoffDeliveryResult(options.delivery, result) : {
4593
+ ...options.delivery,
4594
+ deliveryAttempts: (options.delivery.deliveryAttempts ?? 0) + 1,
4595
+ deliveryStatus: "skipped",
4596
+ updatedAt: Date.now()
4597
+ };
4598
+ };
4599
+ var createVoiceMemoryHandoffDeliveryStore = () => {
4600
+ const deliveries = new Map;
4601
+ return {
4602
+ get: async (id) => deliveries.get(id),
4603
+ list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
4604
+ remove: async (id) => {
4605
+ deliveries.delete(id);
4606
+ },
4607
+ set: async (id, delivery) => {
4608
+ deliveries.set(id, delivery);
4609
+ }
4610
+ };
4611
+ };
4537
4612
  var createVoiceWebhookHandoffAdapter = (options) => ({
4538
4613
  actions: options.actions,
4539
4614
  handoff: async (input) => {
@@ -4972,6 +5047,21 @@ var createVoiceSession = (options) => {
4972
5047
  });
4973
5048
  };
4974
5049
  const runHandoff = async (input) => {
5050
+ const queuedDelivery = options.handoff?.deliveryQueue ? createVoiceHandoffDeliveryRecord({
5051
+ action: input.action,
5052
+ context: options.context,
5053
+ metadata: input.metadata,
5054
+ reason: input.reason,
5055
+ result: input.result,
5056
+ session: input.session,
5057
+ target: input.target
5058
+ }) : undefined;
5059
+ if (queuedDelivery) {
5060
+ await options.handoff?.deliveryQueue?.set(queuedDelivery.id, queuedDelivery);
5061
+ }
5062
+ if (options.handoff?.enqueueOnly) {
5063
+ return;
5064
+ }
4975
5065
  const result = await deliverVoiceHandoff({
4976
5066
  config: options.handoff,
4977
5067
  handoff: {
@@ -4988,6 +5078,10 @@ var createVoiceSession = (options) => {
4988
5078
  if (!result) {
4989
5079
  return;
4990
5080
  }
5081
+ if (queuedDelivery) {
5082
+ const updatedDelivery = applyVoiceHandoffDeliveryResult(queuedDelivery, result);
5083
+ await options.handoff?.deliveryQueue?.set(updatedDelivery.id, updatedDelivery);
5084
+ }
4991
5085
  await appendTrace({
4992
5086
  metadata: input.metadata,
4993
5087
  payload: {
package/dist/types.d.ts CHANGED
@@ -278,6 +278,34 @@ export type VoiceHandoffResult = {
278
278
  metadata?: Record<string, unknown>;
279
279
  status: VoiceHandoffStatus;
280
280
  };
281
+ export type VoiceHandoffDeliveryQueueStatus = VoiceHandoffStatus | 'pending';
282
+ export type StoredVoiceHandoffDelivery<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
283
+ action: VoiceHandoffAction;
284
+ context: TContext;
285
+ createdAt: number;
286
+ deliveredAt?: number;
287
+ deliveries?: Record<string, VoiceHandoffResult & {
288
+ adapterId: string;
289
+ adapterKind?: string;
290
+ }>;
291
+ deliveryAttempts?: number;
292
+ deliveryError?: string;
293
+ deliveryStatus: VoiceHandoffDeliveryQueueStatus;
294
+ id: string;
295
+ metadata?: Record<string, unknown>;
296
+ reason?: string;
297
+ result?: TResult;
298
+ session: TSession;
299
+ sessionId: string;
300
+ target?: string;
301
+ updatedAt: number;
302
+ };
303
+ export type VoiceHandoffDeliveryStore<TDelivery extends StoredVoiceHandoffDelivery = StoredVoiceHandoffDelivery> = {
304
+ get: (id: string) => Promise<TDelivery | undefined> | TDelivery | undefined;
305
+ list: () => Promise<TDelivery[]> | TDelivery[];
306
+ remove: (id: string) => Promise<void> | void;
307
+ set: (id: string, delivery: TDelivery) => Promise<void> | void;
308
+ };
281
309
  export type VoiceHandoffInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
282
310
  action: VoiceHandoffAction;
283
311
  api: VoiceSessionHandle<TContext, TSession, TResult>;
@@ -296,6 +324,8 @@ export type VoiceHandoffAdapter<TContext = unknown, TSession extends VoiceSessio
296
324
  };
297
325
  export type VoiceHandoffConfig<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
298
326
  adapters: VoiceHandoffAdapter<TContext, TSession, TResult>[];
327
+ deliveryQueue?: VoiceHandoffDeliveryStore<StoredVoiceHandoffDelivery<TContext, TSession, TResult>>;
328
+ enqueueOnly?: boolean;
299
329
  failMode?: 'record' | 'throw';
300
330
  };
301
331
  export type VoiceSessionStore<TSession extends VoiceSessionRecord = VoiceSessionRecord> = SessionStore<TSession, VoiceSessionSummary>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.30",
3
+ "version": "0.0.22-beta.31",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",