@absolutejs/voice 0.0.22-beta.27 → 0.0.22-beta.29

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.
@@ -596,6 +596,7 @@ class VoiceStreamService {
596
596
  const stream = createVoiceStream(path, options);
597
597
  const assistantAudioSignal = signal([]);
598
598
  const assistantTextsSignal = signal([]);
599
+ const callSignal = signal(null);
599
600
  const errorSignal = signal(null);
600
601
  const isConnectedSignal = signal(false);
601
602
  const partialSignal = signal("");
@@ -605,6 +606,7 @@ class VoiceStreamService {
605
606
  const sync = () => {
606
607
  assistantAudioSignal.set([...stream.assistantAudio]);
607
608
  assistantTextsSignal.set([...stream.assistantTexts]);
609
+ callSignal.set(stream.call);
608
610
  errorSignal.set(stream.error);
609
611
  isConnectedSignal.set(stream.isConnected);
610
612
  partialSignal.set(stream.partial);
@@ -617,6 +619,8 @@ class VoiceStreamService {
617
619
  return {
618
620
  assistantAudio: computed(() => assistantAudioSignal()),
619
621
  assistantTexts: computed(() => assistantTextsSignal()),
622
+ call: computed(() => callSignal()),
623
+ callControl: (message) => stream.callControl(message),
620
624
  close: () => {
621
625
  unsubscribe();
622
626
  stream.close();
@@ -8,6 +8,8 @@ export declare class VoiceStreamService {
8
8
  turnId?: string;
9
9
  }[]>;
10
10
  assistantTexts: import("@angular/core").Signal<string[]>;
11
+ call: import("@angular/core").Signal<import("..").VoiceCallLifecycleState | null>;
12
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
11
13
  close: () => void;
12
14
  endTurn: () => void;
13
15
  error: import("@angular/core").Signal<string | null>;
@@ -0,0 +1,40 @@
1
+ import type { VoiceHandoffAction, VoiceHandoffAdapter, VoiceHandoffConfig, VoiceHandoffInput, VoiceHandoffResult, VoiceSessionRecord } from './types';
2
+ type MaybePromise<T> = T | Promise<T>;
3
+ export type VoiceHandoffDelivery = VoiceHandoffResult & {
4
+ adapterId: string;
5
+ adapterKind?: string;
6
+ };
7
+ export type VoiceHandoffFanoutResult = {
8
+ action: VoiceHandoffAction;
9
+ deliveries: Record<string, VoiceHandoffDelivery>;
10
+ status: VoiceHandoffResult['status'];
11
+ };
12
+ export type VoiceWebhookHandoffAdapterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
13
+ actions?: VoiceHandoffAction[];
14
+ body?: (input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<Record<string, unknown>>;
15
+ fetch?: typeof fetch;
16
+ headers?: Record<string, string>;
17
+ id: string;
18
+ kind?: string;
19
+ method?: 'POST' | 'PUT' | 'PATCH';
20
+ signingSecret?: string;
21
+ timeoutMs?: number;
22
+ url: string;
23
+ };
24
+ export type VoiceTwilioRedirectHandoffAdapterOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
25
+ accountSid: string;
26
+ actions?: VoiceHandoffAction[];
27
+ authToken: string;
28
+ buildTwiML?: (input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<string>;
29
+ callSid?: string | ((input: VoiceHandoffInput<TContext, TSession, TResult>) => MaybePromise<string | undefined>);
30
+ fetch?: typeof fetch;
31
+ id?: string;
32
+ timeoutMs?: number;
33
+ };
34
+ export declare const deliverVoiceHandoff: <TContext, TSession extends VoiceSessionRecord, TResult>(input: {
35
+ config?: VoiceHandoffConfig<TContext, TSession, TResult>;
36
+ handoff: VoiceHandoffInput<TContext, TSession, TResult>;
37
+ }) => Promise<VoiceHandoffFanoutResult | undefined>;
38
+ export declare const createVoiceWebhookHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceWebhookHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
39
+ export declare const createVoiceTwilioRedirectHandoffAdapter: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: VoiceTwilioRedirectHandoffAdapterOptions<TContext, TSession, TResult>) => VoiceHandoffAdapter<TContext, TSession, TResult>;
40
+ export {};
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ 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
18
  export { createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
18
19
  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';
19
20
  export { createVoiceSession } from './session';
@@ -40,6 +41,7 @@ export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPrese
40
41
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
41
42
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
42
43
  export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
44
+ export type { VoiceHandoffDelivery, VoiceHandoffFanoutResult, VoiceTwilioRedirectHandoffAdapterOptions, VoiceWebhookHandoffAdapterOptions } from './handoff';
43
45
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
44
46
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
45
47
  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';
package/dist/index.js CHANGED
@@ -2992,6 +2992,214 @@ var toVoiceSessionSummary = (session) => ({
2992
2992
  // src/session.ts
2993
2993
  import { Buffer } from "buffer";
2994
2994
 
2995
+ // src/handoff.ts
2996
+ var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2997
+ var signHandoffBody = async (input) => {
2998
+ const encoder = new TextEncoder;
2999
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
3000
+ hash: "SHA-256",
3001
+ name: "HMAC"
3002
+ }, false, ["sign"]);
3003
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
3004
+ return `sha256=${toHex3(new Uint8Array(signature))}`;
3005
+ };
3006
+ var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
3007
+ var createSkippedDelivery = (adapter) => ({
3008
+ adapterId: adapter.id,
3009
+ adapterKind: adapter.kind,
3010
+ status: "skipped"
3011
+ });
3012
+ var aggregateHandoffStatus = (deliveries) => {
3013
+ const statuses = Object.values(deliveries).map((delivery) => delivery.status);
3014
+ if (statuses.some((status) => status === "failed")) {
3015
+ return "failed";
3016
+ }
3017
+ if (statuses.some((status) => status === "delivered")) {
3018
+ return "delivered";
3019
+ }
3020
+ return "skipped";
3021
+ };
3022
+ var defaultWebhookBody = (input) => ({
3023
+ action: input.action,
3024
+ metadata: input.metadata,
3025
+ reason: input.reason,
3026
+ result: input.result,
3027
+ session: {
3028
+ id: input.session.id,
3029
+ scenarioId: input.session.scenarioId,
3030
+ status: input.session.status
3031
+ },
3032
+ source: "absolutejs-voice",
3033
+ target: input.target
3034
+ });
3035
+ var deliverVoiceHandoff = async (input) => {
3036
+ if (!input.config || input.config.adapters.length === 0) {
3037
+ return;
3038
+ }
3039
+ const deliveries = {};
3040
+ for (const adapter of input.config.adapters) {
3041
+ if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
3042
+ deliveries[adapter.id] = createSkippedDelivery(adapter);
3043
+ continue;
3044
+ }
3045
+ try {
3046
+ const result = await adapter.handoff(input.handoff);
3047
+ deliveries[adapter.id] = {
3048
+ ...result,
3049
+ adapterId: adapter.id,
3050
+ adapterKind: adapter.kind
3051
+ };
3052
+ } catch (error) {
3053
+ deliveries[adapter.id] = {
3054
+ adapterId: adapter.id,
3055
+ adapterKind: adapter.kind,
3056
+ error: toErrorMessage2(error),
3057
+ status: "failed"
3058
+ };
3059
+ if (input.config.failMode === "throw") {
3060
+ throw error;
3061
+ }
3062
+ }
3063
+ }
3064
+ return {
3065
+ action: input.handoff.action,
3066
+ deliveries,
3067
+ status: aggregateHandoffStatus(deliveries)
3068
+ };
3069
+ };
3070
+ var createVoiceWebhookHandoffAdapter = (options) => ({
3071
+ actions: options.actions,
3072
+ handoff: async (input) => {
3073
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3074
+ if (typeof fetchImpl !== "function") {
3075
+ return {
3076
+ deliveredTo: options.url,
3077
+ error: "Handoff delivery failed: fetch is not available in this runtime.",
3078
+ status: "failed"
3079
+ };
3080
+ }
3081
+ const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
3082
+ const headers = {
3083
+ "content-type": "application/json",
3084
+ ...options.headers
3085
+ };
3086
+ if (options.signingSecret) {
3087
+ const timestamp = String(Date.now());
3088
+ headers["x-absolutejs-timestamp"] = timestamp;
3089
+ headers["x-absolutejs-signature"] = await signHandoffBody({
3090
+ body,
3091
+ secret: options.signingSecret,
3092
+ timestamp
3093
+ });
3094
+ }
3095
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3096
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3097
+ try {
3098
+ const response = await fetchImpl(options.url, {
3099
+ body,
3100
+ headers,
3101
+ method: options.method ?? "POST",
3102
+ signal: controller?.signal
3103
+ });
3104
+ if (!response.ok) {
3105
+ return {
3106
+ deliveredTo: options.url,
3107
+ error: `Handoff delivery failed with response ${response.status}.`,
3108
+ status: "failed"
3109
+ };
3110
+ }
3111
+ return {
3112
+ deliveredAt: Date.now(),
3113
+ deliveredTo: options.url,
3114
+ status: "delivered"
3115
+ };
3116
+ } finally {
3117
+ if (timeout) {
3118
+ clearTimeout(timeout);
3119
+ }
3120
+ }
3121
+ },
3122
+ id: options.id,
3123
+ kind: options.kind ?? "webhook"
3124
+ });
3125
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3126
+ var defaultTwilioTransferTwiML = (input) => {
3127
+ if (!input.target) {
3128
+ return "<Response><Hangup /></Response>";
3129
+ }
3130
+ return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
3131
+ };
3132
+ var resolveTwilioCallSid = async (resolver, input) => {
3133
+ if (typeof resolver === "function") {
3134
+ return resolver(input);
3135
+ }
3136
+ if (typeof resolver === "string" && resolver.length > 0) {
3137
+ return resolver;
3138
+ }
3139
+ const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
3140
+ const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
3141
+ const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
3142
+ return metadataSid ?? sessionSid;
3143
+ };
3144
+ var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
3145
+ actions: options.actions ?? ["transfer"],
3146
+ handoff: async (input) => {
3147
+ const fetchImpl = options.fetch ?? globalThis.fetch;
3148
+ const callSid = await resolveTwilioCallSid(options.callSid, input);
3149
+ if (!callSid) {
3150
+ return {
3151
+ error: "Twilio handoff requires a callSid.",
3152
+ status: "failed"
3153
+ };
3154
+ }
3155
+ if (typeof fetchImpl !== "function") {
3156
+ return {
3157
+ error: "Twilio handoff failed: fetch is not available in this runtime.",
3158
+ status: "failed"
3159
+ };
3160
+ }
3161
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
3162
+ const body = new URLSearchParams({
3163
+ Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
3164
+ });
3165
+ const auth = btoa(`${options.accountSid}:${options.authToken}`);
3166
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
3167
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
3168
+ try {
3169
+ const response = await fetchImpl(url, {
3170
+ body,
3171
+ headers: {
3172
+ authorization: `Basic ${auth}`,
3173
+ "content-type": "application/x-www-form-urlencoded"
3174
+ },
3175
+ method: "POST",
3176
+ signal: controller?.signal
3177
+ });
3178
+ if (!response.ok) {
3179
+ return {
3180
+ deliveredTo: url,
3181
+ error: `Twilio handoff failed with response ${response.status}.`,
3182
+ status: "failed"
3183
+ };
3184
+ }
3185
+ return {
3186
+ deliveredAt: Date.now(),
3187
+ deliveredTo: url,
3188
+ metadata: {
3189
+ callSid
3190
+ },
3191
+ status: "delivered"
3192
+ };
3193
+ } finally {
3194
+ if (timeout) {
3195
+ clearTimeout(timeout);
3196
+ }
3197
+ }
3198
+ },
3199
+ id: options.id ?? "twilio-redirect",
3200
+ kind: "twilio-redirect"
3201
+ });
3202
+
2995
3203
  // src/turnDetection.ts
2996
3204
  var DEFAULT_SILENCE_MS = 700;
2997
3205
  var DEFAULT_SPEECH_THRESHOLD = 0.015;
@@ -3400,6 +3608,34 @@ var createVoiceSession = (options) => {
3400
3608
  type: "call_lifecycle"
3401
3609
  });
3402
3610
  };
3611
+ const runHandoff = async (input) => {
3612
+ const result = await deliverVoiceHandoff({
3613
+ config: options.handoff,
3614
+ handoff: {
3615
+ action: input.action,
3616
+ api,
3617
+ context: options.context,
3618
+ metadata: input.metadata,
3619
+ reason: input.reason,
3620
+ result: input.result,
3621
+ session: input.session,
3622
+ target: input.target
3623
+ }
3624
+ });
3625
+ if (!result) {
3626
+ return;
3627
+ }
3628
+ await appendTrace({
3629
+ metadata: input.metadata,
3630
+ payload: {
3631
+ ...result,
3632
+ reason: input.reason,
3633
+ target: input.target
3634
+ },
3635
+ session: input.session,
3636
+ type: "call.handoff"
3637
+ });
3638
+ };
3403
3639
  const readSession = async () => options.store.getOrCreate(options.id);
3404
3640
  const writeSession = async (mutate) => {
3405
3641
  const session = await options.store.getOrCreate(options.id);
@@ -3679,6 +3915,14 @@ var createVoiceSession = (options) => {
3679
3915
  type: "call.lifecycle"
3680
3916
  });
3681
3917
  await sendCallLifecycle(session);
3918
+ await runHandoff({
3919
+ action: "transfer",
3920
+ metadata: input.metadata,
3921
+ reason: input.reason,
3922
+ result: input.result,
3923
+ session,
3924
+ target: input.target
3925
+ });
3682
3926
  await completeInternal(input.result, {
3683
3927
  disposition: "transferred",
3684
3928
  invokeOnComplete: false,
@@ -3705,6 +3949,13 @@ var createVoiceSession = (options) => {
3705
3949
  type: "call.lifecycle"
3706
3950
  });
3707
3951
  await sendCallLifecycle(session);
3952
+ await runHandoff({
3953
+ action: "escalate",
3954
+ metadata: input.metadata,
3955
+ reason: input.reason,
3956
+ result: input.result,
3957
+ session
3958
+ });
3708
3959
  await completeInternal(input.result, {
3709
3960
  disposition: "escalated",
3710
3961
  invokeOnComplete: false,
@@ -3728,6 +3979,12 @@ var createVoiceSession = (options) => {
3728
3979
  type: "call.lifecycle"
3729
3980
  });
3730
3981
  await sendCallLifecycle(session);
3982
+ await runHandoff({
3983
+ action: "no-answer",
3984
+ metadata: input?.metadata,
3985
+ result: input?.result,
3986
+ session
3987
+ });
3731
3988
  await completeInternal(input?.result, {
3732
3989
  disposition: "no-answer",
3733
3990
  invokeOnComplete: false,
@@ -3750,6 +4007,12 @@ var createVoiceSession = (options) => {
3750
4007
  type: "call.lifecycle"
3751
4008
  });
3752
4009
  await sendCallLifecycle(session);
4010
+ await runHandoff({
4011
+ action: "voicemail",
4012
+ metadata: input?.metadata,
4013
+ result: input?.result,
4014
+ session
4015
+ });
3753
4016
  await completeInternal(input?.result, {
3754
4017
  disposition: "voicemail",
3755
4018
  invokeOnComplete: false,
@@ -4922,6 +5185,7 @@ var voice = (config) => {
4922
5185
  audioConditioning: sessionOptions.audioConditioning,
4923
5186
  context,
4924
5187
  id: sessionId,
5188
+ handoff: config.handoff,
4925
5189
  languageStrategy: config.languageStrategy,
4926
5190
  lexicon,
4927
5191
  logger: sessionOptions.logger,
@@ -5117,7 +5381,7 @@ var voice = (config) => {
5117
5381
  };
5118
5382
  // src/agent.ts
5119
5383
  var normalizeText3 = (value) => typeof value === "string" ? value.trim() : "";
5120
- var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
5384
+ var toErrorMessage3 = (error) => error instanceof Error ? error.message : String(error);
5121
5385
  var createHistoryMessages = (session, turn) => {
5122
5386
  const messages = [];
5123
5387
  for (const previousTurn of session.turns) {
@@ -5298,7 +5562,7 @@ var createVoiceAgent = (options) => {
5298
5562
  toolCallId: toolCall.id
5299
5563
  });
5300
5564
  } catch (error) {
5301
- const errorMessage = toErrorMessage2(error);
5565
+ const errorMessage = toErrorMessage3(error);
5302
5566
  toolResults.push({
5303
5567
  error: errorMessage,
5304
5568
  status: "error",
@@ -6563,7 +6827,7 @@ var sleep3 = async (delayMs) => {
6563
6827
  }
6564
6828
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
6565
6829
  };
6566
- var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6830
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
6567
6831
  var signVoiceTraceSinkBody = async (input) => {
6568
6832
  const encoder = new TextEncoder;
6569
6833
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -6572,7 +6836,7 @@ var signVoiceTraceSinkBody = async (input) => {
6572
6836
  }, false, ["sign"]);
6573
6837
  const payload = encoder.encode(`${input.timestamp}.${input.body}`);
6574
6838
  const signature = await crypto.subtle.sign("HMAC", key, payload);
6575
- return `sha256=${toHex3(new Uint8Array(signature))}`;
6839
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
6576
6840
  };
6577
6841
  var createVoiceTraceSinkDeliveryError = (input) => {
6578
6842
  if (input.response) {
@@ -8937,7 +9201,7 @@ var createVoiceMemoryStore = () => {
8937
9201
  };
8938
9202
  // src/opsWebhook.ts
8939
9203
  import { Elysia as Elysia5 } from "elysia";
8940
- var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
9204
+ var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
8941
9205
  var signVoiceOpsWebhookBody = async (input) => {
8942
9206
  const encoder = new TextEncoder;
8943
9207
  const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
@@ -8945,7 +9209,7 @@ var signVoiceOpsWebhookBody = async (input) => {
8945
9209
  name: "HMAC"
8946
9210
  }, false, ["sign"]);
8947
9211
  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
8948
- return `sha256=${toHex4(new Uint8Array(signature))}`;
9212
+ return `sha256=${toHex5(new Uint8Array(signature))}`;
8949
9213
  };
8950
9214
  var timingSafeEqual = (left, right) => {
8951
9215
  const encoder = new TextEncoder;
@@ -10630,7 +10894,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
10630
10894
  import { Buffer as Buffer2 } from "buffer";
10631
10895
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
10632
10896
  var VOICE_PCM_SAMPLE_RATE = 16000;
10633
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
10897
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
10634
10898
  var normalizeOnTurn2 = (handler) => {
10635
10899
  if (handler.length > 1) {
10636
10900
  const directHandler = handler;
@@ -10826,8 +11090,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
10826
11090
  }
10827
11091
  });
10828
11092
  var createTwilioVoiceResponse = (options) => {
10829
- const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml(name)}" value="${escapeXml(String(value))}" />`).join("");
10830
- return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml(options.streamUrl)}"${options.track ? ` track="${escapeXml(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
11093
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
11094
+ return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
10831
11095
  };
10832
11096
  var createTwilioMediaStreamBridge = (socket, options) => {
10833
11097
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
@@ -11119,13 +11383,16 @@ export {
11119
11383
  deliverVoiceTraceEventsToSinks,
11120
11384
  deliverVoiceIntegrationEventToSinks,
11121
11385
  deliverVoiceIntegrationEvent,
11386
+ deliverVoiceHandoff,
11122
11387
  decodeTwilioMulawBase64,
11123
11388
  deadLetterVoiceOpsTask,
11124
11389
  createVoiceZendeskTicketUpdateSink,
11125
11390
  createVoiceZendeskTicketSyncSinks,
11126
11391
  createVoiceZendeskTicketSink,
11392
+ createVoiceWebhookHandoffAdapter,
11127
11393
  createVoiceWebhookDeliveryWorkerLoop,
11128
11394
  createVoiceWebhookDeliveryWorker,
11395
+ createVoiceTwilioRedirectHandoffAdapter,
11129
11396
  createVoiceTraceSinkStore,
11130
11397
  createVoiceTraceSinkDeliveryWorkerLoop,
11131
11398
  createVoiceTraceSinkDeliveryWorker,
@@ -589,6 +589,7 @@ var createVoiceStream = (path, options = {}) => {
589
589
  var EMPTY_SNAPSHOT = {
590
590
  assistantAudio: [],
591
591
  assistantTexts: [],
592
+ call: null,
592
593
  error: null,
593
594
  isConnected: false,
594
595
  partial: "",
@@ -606,6 +607,7 @@ var useVoiceStream = (path, options = {}) => {
606
607
  const snapshot = useSyncExternalStore(stream.subscribe, stream.getSnapshot, stream.getServerSnapshot) ?? EMPTY_SNAPSHOT;
607
608
  return {
608
609
  ...snapshot,
610
+ callControl: (message) => stream.callControl(message),
609
611
  close: () => stream.close(),
610
612
  endTurn: () => stream.endTurn(),
611
613
  sendAudio: (audio) => stream.sendAudio(audio)
@@ -1248,6 +1250,7 @@ var createVoiceController = (path, options = {}) => {
1248
1250
  var EMPTY_SNAPSHOT2 = {
1249
1251
  assistantAudio: [],
1250
1252
  assistantTexts: [],
1253
+ call: null,
1251
1254
  error: null,
1252
1255
  isConnected: false,
1253
1256
  isRecording: false,
@@ -1268,6 +1271,7 @@ var useVoiceController = (path, options = {}) => {
1268
1271
  return {
1269
1272
  ...snapshot,
1270
1273
  bindHTMX: controller.bindHTMX,
1274
+ callControl: (message) => controller.callControl(message),
1271
1275
  close: () => controller.close(),
1272
1276
  endTurn: () => controller.endTurn(),
1273
1277
  sendAudio: (audio) => controller.sendAudio(audio),
@@ -1,6 +1,7 @@
1
1
  import type { VoiceControllerOptions } from '../types';
2
2
  export declare const useVoiceController: <TResult = unknown>(path: string, options?: VoiceControllerOptions) => {
3
3
  bindHTMX: (options: import("..").VoiceHTMXBindingOptions) => () => void;
4
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
4
5
  close: () => void;
5
6
  endTurn: () => void;
6
7
  sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
@@ -1,5 +1,6 @@
1
1
  import type { VoiceConnectionOptions } from '../types';
2
2
  export declare const useVoiceStream: <TResult = unknown>(path: string, options?: VoiceConnectionOptions) => {
3
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
3
4
  close: () => void;
4
5
  endTurn: () => void;
5
6
  sendAudio: (audio: Uint8Array | ArrayBuffer) => void;
@@ -4459,6 +4459,214 @@ var createVoiceMemoryStore = () => {
4459
4459
  // src/session.ts
4460
4460
  import { Buffer } from "buffer";
4461
4461
 
4462
+ // src/handoff.ts
4463
+ var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
4464
+ var signHandoffBody = async (input) => {
4465
+ const encoder = new TextEncoder;
4466
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
4467
+ hash: "SHA-256",
4468
+ name: "HMAC"
4469
+ }, false, ["sign"]);
4470
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
4471
+ return `sha256=${toHex(new Uint8Array(signature))}`;
4472
+ };
4473
+ var toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
4474
+ var createSkippedDelivery = (adapter) => ({
4475
+ adapterId: adapter.id,
4476
+ adapterKind: adapter.kind,
4477
+ status: "skipped"
4478
+ });
4479
+ var aggregateHandoffStatus = (deliveries) => {
4480
+ const statuses = Object.values(deliveries).map((delivery) => delivery.status);
4481
+ if (statuses.some((status) => status === "failed")) {
4482
+ return "failed";
4483
+ }
4484
+ if (statuses.some((status) => status === "delivered")) {
4485
+ return "delivered";
4486
+ }
4487
+ return "skipped";
4488
+ };
4489
+ var defaultWebhookBody = (input) => ({
4490
+ action: input.action,
4491
+ metadata: input.metadata,
4492
+ reason: input.reason,
4493
+ result: input.result,
4494
+ session: {
4495
+ id: input.session.id,
4496
+ scenarioId: input.session.scenarioId,
4497
+ status: input.session.status
4498
+ },
4499
+ source: "absolutejs-voice",
4500
+ target: input.target
4501
+ });
4502
+ var deliverVoiceHandoff = async (input) => {
4503
+ if (!input.config || input.config.adapters.length === 0) {
4504
+ return;
4505
+ }
4506
+ const deliveries = {};
4507
+ for (const adapter of input.config.adapters) {
4508
+ if (adapter.actions && !adapter.actions.includes(input.handoff.action)) {
4509
+ deliveries[adapter.id] = createSkippedDelivery(adapter);
4510
+ continue;
4511
+ }
4512
+ try {
4513
+ const result = await adapter.handoff(input.handoff);
4514
+ deliveries[adapter.id] = {
4515
+ ...result,
4516
+ adapterId: adapter.id,
4517
+ adapterKind: adapter.kind
4518
+ };
4519
+ } catch (error) {
4520
+ deliveries[adapter.id] = {
4521
+ adapterId: adapter.id,
4522
+ adapterKind: adapter.kind,
4523
+ error: toErrorMessage(error),
4524
+ status: "failed"
4525
+ };
4526
+ if (input.config.failMode === "throw") {
4527
+ throw error;
4528
+ }
4529
+ }
4530
+ }
4531
+ return {
4532
+ action: input.handoff.action,
4533
+ deliveries,
4534
+ status: aggregateHandoffStatus(deliveries)
4535
+ };
4536
+ };
4537
+ var createVoiceWebhookHandoffAdapter = (options) => ({
4538
+ actions: options.actions,
4539
+ handoff: async (input) => {
4540
+ const fetchImpl = options.fetch ?? globalThis.fetch;
4541
+ if (typeof fetchImpl !== "function") {
4542
+ return {
4543
+ deliveredTo: options.url,
4544
+ error: "Handoff delivery failed: fetch is not available in this runtime.",
4545
+ status: "failed"
4546
+ };
4547
+ }
4548
+ const body = JSON.stringify(await options.body?.(input) ?? defaultWebhookBody(input));
4549
+ const headers = {
4550
+ "content-type": "application/json",
4551
+ ...options.headers
4552
+ };
4553
+ if (options.signingSecret) {
4554
+ const timestamp = String(Date.now());
4555
+ headers["x-absolutejs-timestamp"] = timestamp;
4556
+ headers["x-absolutejs-signature"] = await signHandoffBody({
4557
+ body,
4558
+ secret: options.signingSecret,
4559
+ timestamp
4560
+ });
4561
+ }
4562
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
4563
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
4564
+ try {
4565
+ const response = await fetchImpl(options.url, {
4566
+ body,
4567
+ headers,
4568
+ method: options.method ?? "POST",
4569
+ signal: controller?.signal
4570
+ });
4571
+ if (!response.ok) {
4572
+ return {
4573
+ deliveredTo: options.url,
4574
+ error: `Handoff delivery failed with response ${response.status}.`,
4575
+ status: "failed"
4576
+ };
4577
+ }
4578
+ return {
4579
+ deliveredAt: Date.now(),
4580
+ deliveredTo: options.url,
4581
+ status: "delivered"
4582
+ };
4583
+ } finally {
4584
+ if (timeout) {
4585
+ clearTimeout(timeout);
4586
+ }
4587
+ }
4588
+ },
4589
+ id: options.id,
4590
+ kind: options.kind ?? "webhook"
4591
+ });
4592
+ var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
4593
+ var defaultTwilioTransferTwiML = (input) => {
4594
+ if (!input.target) {
4595
+ return "<Response><Hangup /></Response>";
4596
+ }
4597
+ return `<Response><Dial>${escapeXml(input.target)}</Dial></Response>`;
4598
+ };
4599
+ var resolveTwilioCallSid = async (resolver, input) => {
4600
+ if (typeof resolver === "function") {
4601
+ return resolver(input);
4602
+ }
4603
+ if (typeof resolver === "string" && resolver.length > 0) {
4604
+ return resolver;
4605
+ }
4606
+ const metadataSid = typeof input.metadata?.callSid === "string" ? input.metadata.callSid : undefined;
4607
+ const sessionMetadata = input.session.metadata && typeof input.session.metadata === "object" ? input.session.metadata : undefined;
4608
+ const sessionSid = typeof sessionMetadata?.callSid === "string" ? sessionMetadata.callSid : undefined;
4609
+ return metadataSid ?? sessionSid;
4610
+ };
4611
+ var createVoiceTwilioRedirectHandoffAdapter = (options) => ({
4612
+ actions: options.actions ?? ["transfer"],
4613
+ handoff: async (input) => {
4614
+ const fetchImpl = options.fetch ?? globalThis.fetch;
4615
+ const callSid = await resolveTwilioCallSid(options.callSid, input);
4616
+ if (!callSid) {
4617
+ return {
4618
+ error: "Twilio handoff requires a callSid.",
4619
+ status: "failed"
4620
+ };
4621
+ }
4622
+ if (typeof fetchImpl !== "function") {
4623
+ return {
4624
+ error: "Twilio handoff failed: fetch is not available in this runtime.",
4625
+ status: "failed"
4626
+ };
4627
+ }
4628
+ const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(options.accountSid)}/Calls/${encodeURIComponent(callSid)}.json`;
4629
+ const body = new URLSearchParams({
4630
+ Twiml: await (options.buildTwiML?.(input) ?? defaultTwilioTransferTwiML(input))
4631
+ });
4632
+ const auth = btoa(`${options.accountSid}:${options.authToken}`);
4633
+ const controller = options.timeoutMs && options.timeoutMs > 0 ? new AbortController : undefined;
4634
+ const timeout = controller && options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : undefined;
4635
+ try {
4636
+ const response = await fetchImpl(url, {
4637
+ body,
4638
+ headers: {
4639
+ authorization: `Basic ${auth}`,
4640
+ "content-type": "application/x-www-form-urlencoded"
4641
+ },
4642
+ method: "POST",
4643
+ signal: controller?.signal
4644
+ });
4645
+ if (!response.ok) {
4646
+ return {
4647
+ deliveredTo: url,
4648
+ error: `Twilio handoff failed with response ${response.status}.`,
4649
+ status: "failed"
4650
+ };
4651
+ }
4652
+ return {
4653
+ deliveredAt: Date.now(),
4654
+ deliveredTo: url,
4655
+ metadata: {
4656
+ callSid
4657
+ },
4658
+ status: "delivered"
4659
+ };
4660
+ } finally {
4661
+ if (timeout) {
4662
+ clearTimeout(timeout);
4663
+ }
4664
+ }
4665
+ },
4666
+ id: options.id ?? "twilio-redirect",
4667
+ kind: "twilio-redirect"
4668
+ });
4669
+
4462
4670
  // src/logger.ts
4463
4671
  var noop2 = () => {};
4464
4672
  var createNoopLogger = () => ({
@@ -4763,6 +4971,34 @@ var createVoiceSession = (options) => {
4763
4971
  type: "call_lifecycle"
4764
4972
  });
4765
4973
  };
4974
+ const runHandoff = async (input) => {
4975
+ const result = await deliverVoiceHandoff({
4976
+ config: options.handoff,
4977
+ handoff: {
4978
+ action: input.action,
4979
+ api,
4980
+ context: options.context,
4981
+ metadata: input.metadata,
4982
+ reason: input.reason,
4983
+ result: input.result,
4984
+ session: input.session,
4985
+ target: input.target
4986
+ }
4987
+ });
4988
+ if (!result) {
4989
+ return;
4990
+ }
4991
+ await appendTrace({
4992
+ metadata: input.metadata,
4993
+ payload: {
4994
+ ...result,
4995
+ reason: input.reason,
4996
+ target: input.target
4997
+ },
4998
+ session: input.session,
4999
+ type: "call.handoff"
5000
+ });
5001
+ };
4766
5002
  const readSession = async () => options.store.getOrCreate(options.id);
4767
5003
  const writeSession = async (mutate) => {
4768
5004
  const session = await options.store.getOrCreate(options.id);
@@ -5042,6 +5278,14 @@ var createVoiceSession = (options) => {
5042
5278
  type: "call.lifecycle"
5043
5279
  });
5044
5280
  await sendCallLifecycle(session);
5281
+ await runHandoff({
5282
+ action: "transfer",
5283
+ metadata: input.metadata,
5284
+ reason: input.reason,
5285
+ result: input.result,
5286
+ session,
5287
+ target: input.target
5288
+ });
5045
5289
  await completeInternal(input.result, {
5046
5290
  disposition: "transferred",
5047
5291
  invokeOnComplete: false,
@@ -5068,6 +5312,13 @@ var createVoiceSession = (options) => {
5068
5312
  type: "call.lifecycle"
5069
5313
  });
5070
5314
  await sendCallLifecycle(session);
5315
+ await runHandoff({
5316
+ action: "escalate",
5317
+ metadata: input.metadata,
5318
+ reason: input.reason,
5319
+ result: input.result,
5320
+ session
5321
+ });
5071
5322
  await completeInternal(input.result, {
5072
5323
  disposition: "escalated",
5073
5324
  invokeOnComplete: false,
@@ -5091,6 +5342,12 @@ var createVoiceSession = (options) => {
5091
5342
  type: "call.lifecycle"
5092
5343
  });
5093
5344
  await sendCallLifecycle(session);
5345
+ await runHandoff({
5346
+ action: "no-answer",
5347
+ metadata: input?.metadata,
5348
+ result: input?.result,
5349
+ session
5350
+ });
5094
5351
  await completeInternal(input?.result, {
5095
5352
  disposition: "no-answer",
5096
5353
  invokeOnComplete: false,
@@ -5113,6 +5370,12 @@ var createVoiceSession = (options) => {
5113
5370
  type: "call.lifecycle"
5114
5371
  });
5115
5372
  await sendCallLifecycle(session);
5373
+ await runHandoff({
5374
+ action: "voicemail",
5375
+ metadata: input?.metadata,
5376
+ result: input?.result,
5377
+ session
5378
+ });
5116
5379
  await completeInternal(input?.result, {
5117
5380
  disposition: "voicemail",
5118
5381
  invokeOnComplete: false,
@@ -6490,7 +6753,7 @@ var createVoiceCallReviewFromLiveTelephonyReport = (report, options = {}) => {
6490
6753
  }
6491
6754
  };
6492
6755
  };
6493
- var toErrorMessage = (error) => {
6756
+ var toErrorMessage2 = (error) => {
6494
6757
  if (typeof error === "string" && error.trim().length > 0) {
6495
6758
  return error;
6496
6759
  }
@@ -6577,7 +6840,7 @@ var createVoiceCallReviewRecorder = (options = {}) => {
6577
6840
  };
6578
6841
  },
6579
6842
  recordError: (error) => {
6580
- const message = toErrorMessage(error);
6843
+ const message = toErrorMessage2(error);
6581
6844
  errors.push(message);
6582
6845
  push("turn", "error", {
6583
6846
  reason: message
@@ -7286,7 +7549,7 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
7286
7549
  import { Buffer as Buffer2 } from "buffer";
7287
7550
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
7288
7551
  var VOICE_PCM_SAMPLE_RATE = 16000;
7289
- var escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
7552
+ var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
7290
7553
  var normalizeOnTurn = (handler) => {
7291
7554
  if (handler.length > 1) {
7292
7555
  const directHandler = handler;
@@ -7482,8 +7745,8 @@ var createTwilioSocketAdapter = (socket, getState) => ({
7482
7745
  }
7483
7746
  });
7484
7747
  var createTwilioVoiceResponse = (options) => {
7485
- const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml(name)}" value="${escapeXml(String(value))}" />`).join("");
7486
- return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml(options.streamUrl)}"${options.track ? ` track="${escapeXml(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
7748
+ const parameters = Object.entries(options.parameters ?? {}).filter((entry) => entry[1] !== undefined).map(([name, value]) => `<Parameter name="${escapeXml2(name)}" value="${escapeXml2(String(value))}" />`).join("");
7749
+ return `<?xml version="1.0" encoding="UTF-8"?><Response><Connect><Stream url="${escapeXml2(options.streamUrl)}"${options.track ? ` track="${escapeXml2(options.track)}"` : ""}${options.streamName ? ` name="${escapeXml2(options.streamName)}"` : ""}>${parameters}</Stream></Connect></Response>`;
7487
7750
  };
7488
7751
  var createTwilioMediaStreamBridge = (socket, options) => {
7489
7752
  const runtimePreset = resolveVoiceRuntimePreset(options.preset);
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.lifecycle' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn.transcript';
1
+ export type VoiceTraceEventType = 'assistant.guardrail' | 'assistant.memory' | 'assistant.run' | 'agent.handoff' | 'agent.model' | 'agent.result' | 'agent.tool' | 'call.handoff' | 'call.lifecycle' | 'session.error' | 'turn.assistant' | 'turn.committed' | 'turn.cost' | 'turn.transcript';
2
2
  export type VoiceTraceEvent<TPayload extends Record<string, unknown> = Record<string, unknown>> = {
3
3
  at: number;
4
4
  id?: string;
package/dist/types.d.ts CHANGED
@@ -269,6 +269,35 @@ export type VoiceCallLifecycleState = {
269
269
  lastEventAt: number;
270
270
  startedAt: number;
271
271
  };
272
+ export type VoiceHandoffAction = 'escalate' | 'no-answer' | 'transfer' | 'voicemail';
273
+ export type VoiceHandoffStatus = 'delivered' | 'failed' | 'skipped';
274
+ export type VoiceHandoffResult = {
275
+ deliveredAt?: number;
276
+ deliveredTo?: string;
277
+ error?: string;
278
+ metadata?: Record<string, unknown>;
279
+ status: VoiceHandoffStatus;
280
+ };
281
+ export type VoiceHandoffInput<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
282
+ action: VoiceHandoffAction;
283
+ api: VoiceSessionHandle<TContext, TSession, TResult>;
284
+ context: TContext;
285
+ metadata?: Record<string, unknown>;
286
+ reason?: string;
287
+ result?: TResult;
288
+ session: TSession;
289
+ target?: string;
290
+ };
291
+ export type VoiceHandoffAdapter<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
292
+ actions?: VoiceHandoffAction[];
293
+ handoff: (input: VoiceHandoffInput<TContext, TSession, TResult>) => Promise<VoiceHandoffResult> | VoiceHandoffResult;
294
+ id: string;
295
+ kind?: string;
296
+ };
297
+ export type VoiceHandoffConfig<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
298
+ adapters: VoiceHandoffAdapter<TContext, TSession, TResult>[];
299
+ failMode?: 'record' | 'throw';
300
+ };
272
301
  export type VoiceSessionStore<TSession extends VoiceSessionRecord = VoiceSessionRecord> = SessionStore<TSession, VoiceSessionSummary>;
273
302
  export type VoiceLogger = {
274
303
  debug?: (message: string, meta?: Record<string, unknown>) => void;
@@ -567,6 +596,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
567
596
  audioConditioning?: VoiceAudioConditioningConfig;
568
597
  logger?: VoiceLogger;
569
598
  htmx?: boolean | VoiceHTMXConfig<TSession, NoInfer<TResult>>;
599
+ handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
570
600
  ops?: VoiceRuntimeOpsConfig<TContext, TSession, TResult>;
571
601
  trace?: VoiceTraceEventStore;
572
602
  } & VoiceRouteConfig<TContext, TSession, TResult>;
@@ -588,6 +618,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
588
618
  sttLifecycle: VoiceSTTLifecycle;
589
619
  turnDetection: VoiceResolvedTurnDetectionConfig;
590
620
  audioConditioning?: VoiceResolvedAudioConditioningConfig;
621
+ handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
591
622
  route: VoiceNormalizedRouteConfig<TContext, TSession, TResult>;
592
623
  logger?: VoiceLogger;
593
624
  };
package/dist/vue/index.js CHANGED
@@ -590,6 +590,7 @@ var useVoiceStream = (path, options = {}) => {
590
590
  const stream = createVoiceStream(path, options);
591
591
  const assistantAudio = shallowRef([]);
592
592
  const assistantTexts = shallowRef([]);
593
+ const call = shallowRef(null);
593
594
  const error = ref(null);
594
595
  const isConnected = ref(false);
595
596
  const partial = ref("");
@@ -599,6 +600,7 @@ var useVoiceStream = (path, options = {}) => {
599
600
  const sync = () => {
600
601
  assistantAudio.value = [...stream.assistantAudio];
601
602
  assistantTexts.value = [...stream.assistantTexts];
603
+ call.value = stream.call;
602
604
  error.value = stream.error;
603
605
  isConnected.value = stream.isConnected;
604
606
  partial.value = stream.partial;
@@ -616,6 +618,8 @@ var useVoiceStream = (path, options = {}) => {
616
618
  return {
617
619
  assistantAudio,
618
620
  assistantTexts,
621
+ call,
622
+ callControl: (message) => stream.callControl(message),
619
623
  close: () => destroy(),
620
624
  endTurn: () => stream.endTurn(),
621
625
  error,
@@ -12,6 +12,8 @@ export declare const useVoiceStream: <TResult = unknown>(path: string, options?:
12
12
  turnId?: string;
13
13
  }[]>;
14
14
  assistantTexts: import("vue").ShallowRef<string[], string[]>;
15
+ call: import("vue").ShallowRef<import("..").VoiceCallLifecycleState | null, import("..").VoiceCallLifecycleState | null>;
16
+ callControl: (message: Parameters<(message: Omit<import("..").VoiceClientCallControlMessage, "type">) => void>[0]) => void;
15
17
  close: () => void;
16
18
  endTurn: () => void;
17
19
  error: import("vue").Ref<string | null, string | null>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.27",
3
+ "version": "0.0.22-beta.29",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",