@onereach/step-voice 7.0.26-voicemaildetection.0 → 7.0.27

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.
@@ -4,31 +4,27 @@ const tslib_1 = require("tslib");
4
4
  const voice_1 = tslib_1.__importDefault(require("./voice"));
5
5
  class VoiceAgentFunctionResult extends voice_1.default {
6
6
  async runStep() {
7
- const ctx = await this.thread.get('__voiceAgentContext') ?? {};
8
- const { callId, callCallback, name: functionName, functionId } = ctx;
9
7
  const { result, stopAgent } = this.data;
10
- if (!callId) {
8
+ const ctx = await this.thread.get(`__voiceAgentContext_${this.thread.id}`);
9
+ if (!ctx?.callId) {
11
10
  throw new Error('Voice Agent Function Result: missing call context (must be placed on a Voice Agent function exit)');
12
11
  }
13
- this.log.info("Voice Agent Function Result Params", ctx);
12
+ const { callId, callCallback, name: functionName, functionId } = ctx;
13
+ this.log.info("Voice Agent Function Result", { functionId, callId, threadId: this.thread.id });
14
14
  const params = {
15
- functionName: functionName,
16
- functionId: functionId,
17
- result: result,
18
- options: {
19
- stop: stopAgent
20
- }
15
+ functionName,
16
+ functionId,
17
+ result,
18
+ options: { stop: stopAgent }
21
19
  };
22
- const event = {
20
+ await this.thread.eventManager.emit({
23
21
  target: this.helpers.providersAccountId,
24
22
  name: 'out/voice/voicer',
25
23
  params: {
26
24
  id: callId,
27
25
  commands: [{ name: 'agentFunctionResult', params }]
28
26
  }
29
- };
30
- this.log.info("Voice Agent Function Result", event);
31
- await this.thread.eventManager.emit(event, {
27
+ }, {
32
28
  target: callCallback,
33
29
  invocationType: 'async',
34
30
  timeout: 5000
@@ -1,8 +1,34 @@
1
1
  import VoiceStep from './voice';
2
+ type PreToolSpeechMode = 'auto' | 'static' | 'off';
3
+ type ToolCallSound = 'typing' | 'none';
4
+ type ToolCallSoundBehavior = 'always' | 'with_pre_speech';
5
+ type SoftTimeoutMode = 'static' | 'auto';
6
+ interface ToolCallFeedbackConfig {
7
+ enabled: boolean;
8
+ pre_tool_speech: PreToolSpeechMode;
9
+ pre_tool_speech_phrases: string[];
10
+ tool_call_sound: ToolCallSound;
11
+ tool_call_sound_behavior: ToolCallSoundBehavior;
12
+ delay_seconds: number;
13
+ }
14
+ interface PerToolConfig {
15
+ enabled?: boolean | null;
16
+ pre_tool_speech?: PreToolSpeechMode | null;
17
+ pre_tool_speech_phrases?: string[] | null;
18
+ tool_call_sound?: ToolCallSound | null;
19
+ tool_call_sound_behavior?: ToolCallSoundBehavior | null;
20
+ delay_seconds?: number | null;
21
+ }
22
+ interface SoftTimeoutConfig {
23
+ timeout_seconds: number;
24
+ phrases: string[];
25
+ mode: SoftTimeoutMode;
26
+ }
2
27
  interface Handler {
3
28
  name: string;
4
29
  description: string;
5
30
  parameters: string;
31
+ feedback?: PerToolConfig;
6
32
  }
7
33
  interface AgentServiceConfig {
8
34
  provider: string;
@@ -23,7 +49,10 @@ interface INPUT {
23
49
  prompt?: string;
24
50
  }>;
25
51
  idle_goodbye_message: string;
26
- voicemail_detection_enabled: boolean;
52
+ dtmf_enabled: boolean;
53
+ dtmf_timeout: number | string;
54
+ dtmf_termination_digit: string;
55
+ dtmf_prefix: string;
27
56
  custom_config_enabled: boolean;
28
57
  agent_url_overwritten: boolean;
29
58
  agent_url: string;
@@ -32,14 +61,19 @@ interface INPUT {
32
61
  llm: Partial<AgentServiceConfig>;
33
62
  tts: Partial<AgentServiceConfig>;
34
63
  realtime: Partial<AgentServiceConfig>;
64
+ tool_feedback?: ToolCallFeedbackConfig;
65
+ soft_timeout?: SoftTimeoutConfig;
35
66
  }
36
67
  export default class VoiceAgent extends VoiceStep<Partial<INPUT>> {
37
68
  runStep(): Promise<void>;
38
69
  private normalizeIdlePrompts;
39
70
  private findExitByLabel;
71
+ private validateToolFeedback;
40
72
  private isStepFlagEnabled;
41
73
  private isValidAgentUrl;
42
74
  private buildAgentServiceConfig;
75
+ private validatePerToolFeedback;
76
+ private validateSoftTimeout;
43
77
  private parseAgentServiceOptions;
44
78
  exitToThread(): Promise<void>;
45
79
  }
@@ -17,25 +17,21 @@ class VoiceAgent extends voice_1.default {
17
17
  const functionId = event.params.functionId ?? '';
18
18
  const exitId = this.findExitByLabel(name) ?? this.getExitStepId('function_call');
19
19
  if (exitId) {
20
- this.exitStep(exitId, { [name]: params, name, functionId, __voicecb: cb, callId: call.id, callType: call.type, callCallback: call.callback }, true);
20
+ this.exitStep(exitId, {
21
+ [name]: params,
22
+ name,
23
+ functionId,
24
+ __voicecb: cb,
25
+ callId: call.id,
26
+ callType: call.type,
27
+ callCallback: call.callback
28
+ }, true);
21
29
  }
22
30
  else {
23
31
  this.log.warn('no exit for function_call', { name });
24
32
  }
25
33
  return;
26
34
  }
27
- case 'voicemail_detected': {
28
- const exitId = this.getExitStepId('voicemail_detected');
29
- if (exitId) {
30
- return this.exitStep(exitId, {
31
- callId: call.id,
32
- callType: call.type,
33
- callCallback: call.callback,
34
- }, false);
35
- }
36
- this.log.warn('no exit for voicemail_detected');
37
- return;
38
- }
39
35
  case 'hangup':
40
36
  await this.handleHangup(call);
41
37
  return await this.waitConvEnd();
@@ -46,7 +42,7 @@ class VoiceAgent extends voice_1.default {
46
42
  }
47
43
  });
48
44
  this.triggers.otherwise(async () => {
49
- const { handlers = [], system_instruction = '', developer_messages = [], greeting_message = '', prevent_greeting_interruption = false, idle_detection_enabled = false, idle_timeout_secs = 8, idle_max_retries = 3, idle_prompts = [], idle_goodbye_message = '', voicemail_detection_enabled = false, custom_config_enabled = false, agent_url_overwritten = false, agent_url = '', mode = '', stt = {}, llm = {}, tts = {}, realtime = {} } = this.data;
45
+ const { handlers = [], system_instruction = '', developer_messages = [], greeting_message = '', prevent_greeting_interruption = false, idle_detection_enabled = false, idle_timeout_secs = 8, idle_max_retries = 3, idle_prompts = [], idle_goodbye_message = '', dtmf_enabled = false, dtmf_timeout = 2, dtmf_termination_digit = '#', dtmf_prefix = 'DTMF: ', custom_config_enabled = false, agent_url_overwritten = false, tool_feedback, soft_timeout, agent_url = '', mode = '', stt = {}, llm = {}, tts = {}, realtime = {} } = this.data;
50
46
  if (lodash_1.default.isEmpty(system_instruction)) {
51
47
  this.throwError(new Error('Voice Agent: system instruction is required'));
52
48
  }
@@ -60,7 +56,19 @@ class VoiceAgent extends voice_1.default {
60
56
  parameters = {};
61
57
  }
62
58
  }
63
- return { type: 'function', name: h.name, description: h.description, parameters: parameters || {} };
59
+ const tool = {
60
+ type: 'function',
61
+ name: h.name,
62
+ description: h.description,
63
+ parameters: parameters || {}
64
+ };
65
+ if (h.feedback != null) {
66
+ const validatedFeedback = this.validatePerToolFeedback(h.feedback, h.name);
67
+ if (validatedFeedback) {
68
+ tool.feedback = validatedFeedback;
69
+ }
70
+ }
71
+ return tool;
64
72
  });
65
73
  const config = {
66
74
  system_instruction,
@@ -81,12 +89,16 @@ class VoiceAgent extends voice_1.default {
81
89
  goodbye_message: lodash_1.default.isEmpty(lodash_1.default.trim(idle_goodbye_message)) ? undefined : idle_goodbye_message
82
90
  };
83
91
  }
84
- if (this.isStepFlagEnabled(voicemail_detection_enabled)) {
85
- if (this.isStepFlagEnabled(custom_config_enabled)
86
- && mode === "realtime" /* AgentPipelineMode.REALTIME */) {
87
- this.throwError(new Error('Voice Agent: voicemail detection requires waterfall mode (not compatible with realtime)'));
88
- }
89
- config.voicemail_detection = { enabled: true };
92
+ if (this.isStepFlagEnabled(dtmf_enabled)) {
93
+ const timeout = Number(dtmf_timeout);
94
+ const terminationDigit = String(dtmf_termination_digit || '#');
95
+ const prefix = dtmf_prefix == null || dtmf_prefix === '' ? 'DTMF: ' : String(dtmf_prefix);
96
+ config.dtmf = {
97
+ enabled: true,
98
+ timeout: Number.isFinite(timeout) ? timeout : 2,
99
+ termination_digit: terminationDigit,
100
+ prefix
101
+ };
90
102
  }
91
103
  let agentCustomUrl = '';
92
104
  if (this.isStepFlagEnabled(agent_url_overwritten)) {
@@ -110,6 +122,18 @@ class VoiceAgent extends voice_1.default {
110
122
  this.throwError(new Error(`Voice Agent: unknown mode "${mode}"`));
111
123
  }
112
124
  }
125
+ if (this.isStepFlagEnabled(tool_feedback?.enabled) && tools.length > 0) {
126
+ const validatedFeedback = this.validateToolFeedback(tool_feedback);
127
+ if (validatedFeedback) {
128
+ config.tool_feedback = validatedFeedback;
129
+ }
130
+ }
131
+ if (soft_timeout != null) {
132
+ const validatedSoftTimeout = this.validateSoftTimeout(soft_timeout);
133
+ if (validatedSoftTimeout) {
134
+ config.soft_timeout = validatedSoftTimeout;
135
+ }
136
+ }
113
137
  const params = { config };
114
138
  if (agentCustomUrl) {
115
139
  params.url = agentCustomUrl;
@@ -127,6 +151,57 @@ class VoiceAgent extends voice_1.default {
127
151
  const exit = lodash_1.default.find(this.step.exits, (e) => e.label === label);
128
152
  return exit?.id;
129
153
  }
154
+ validateToolFeedback(config) {
155
+ if (config == null || typeof config !== 'object')
156
+ return null;
157
+ const cfg = config;
158
+ // If enabled is not boolean true, silently return undefined (disabled case)
159
+ if (typeof cfg.enabled !== 'boolean' || cfg.enabled !== true)
160
+ return null;
161
+ // Validate pre_tool_speech
162
+ const validPreToolSpeech = ['auto', 'static', 'off'];
163
+ if (!validPreToolSpeech.includes(cfg.pre_tool_speech)) {
164
+ this.log.warn('Voice Agent: invalid tool_feedback.pre_tool_speech — feedback config excluded');
165
+ return null;
166
+ }
167
+ // Validate tool_call_sound
168
+ const validToolCallSound = ['typing', 'none'];
169
+ if (!validToolCallSound.includes(cfg.tool_call_sound)) {
170
+ this.log.warn('Voice Agent: invalid tool_feedback.tool_call_sound — feedback config excluded');
171
+ return null;
172
+ }
173
+ // Validate tool_call_sound_behavior
174
+ const validToolCallSoundBehavior = ['always', 'with_pre_speech'];
175
+ if (!validToolCallSoundBehavior.includes(cfg.tool_call_sound_behavior)) {
176
+ this.log.warn('Voice Agent: invalid tool_feedback.tool_call_sound_behavior — feedback config excluded');
177
+ return null;
178
+ }
179
+ // Validate delay_seconds
180
+ const delay = Number(cfg.delay_seconds);
181
+ if (delay <= 0 || delay > 30) {
182
+ this.log.warn('Voice Agent: invalid tool_feedback.delay_seconds — feedback config excluded');
183
+ return null;
184
+ }
185
+ // Validate pre_tool_speech_phrases (only required when mode is 'static')
186
+ const phrases = cfg.pre_tool_speech_phrases;
187
+ if (cfg.pre_tool_speech === 'static') {
188
+ if (!Array.isArray(phrases) ||
189
+ phrases.length < 1 ||
190
+ phrases.length > 20 ||
191
+ !phrases.every((p) => typeof p === 'string' && p.trim().length > 0)) {
192
+ this.log.warn('Voice Agent: invalid tool_feedback.pre_tool_speech_phrases — feedback config excluded');
193
+ return null;
194
+ }
195
+ }
196
+ return {
197
+ enabled: true,
198
+ pre_tool_speech: cfg.pre_tool_speech,
199
+ pre_tool_speech_phrases: (Array.isArray(phrases) ? phrases : []),
200
+ tool_call_sound: cfg.tool_call_sound,
201
+ tool_call_sound_behavior: cfg.tool_call_sound_behavior,
202
+ delay_seconds: delay
203
+ };
204
+ }
130
205
  isStepFlagEnabled(value) {
131
206
  return value === true || value === 'true';
132
207
  }
@@ -146,6 +221,75 @@ class VoiceAgent extends voice_1.default {
146
221
  options: this.parseAgentServiceOptions(label, service.options)
147
222
  };
148
223
  }
224
+ validatePerToolFeedback(config, toolName) {
225
+ if (config == null || typeof config !== 'object' || Array.isArray(config)) {
226
+ return null;
227
+ }
228
+ const raw = config;
229
+ const result = {};
230
+ if ('delay_seconds' in raw && raw.delay_seconds != null) {
231
+ const delay = Number(raw.delay_seconds);
232
+ if (delay <= 0 || delay > 30) {
233
+ this.log.warn(`Voice Agent: invalid feedback.delay_seconds for tool "${toolName}" — per-tool feedback excluded`);
234
+ return null;
235
+ }
236
+ result.delay_seconds = delay;
237
+ }
238
+ if ('pre_tool_speech' in raw && raw.pre_tool_speech != null) {
239
+ const valid = ['auto', 'static', 'off'];
240
+ if (!valid.includes(raw.pre_tool_speech)) {
241
+ this.log.warn(`Voice Agent: invalid feedback.pre_tool_speech for tool "${toolName}" — per-tool feedback excluded`);
242
+ return null;
243
+ }
244
+ result.pre_tool_speech = raw.pre_tool_speech;
245
+ }
246
+ if ('tool_call_sound' in raw && raw.tool_call_sound != null) {
247
+ const valid = ['typing', 'none'];
248
+ if (!valid.includes(raw.tool_call_sound)) {
249
+ this.log.warn(`Voice Agent: invalid feedback.tool_call_sound for tool "${toolName}" — per-tool feedback excluded`);
250
+ return null;
251
+ }
252
+ result.tool_call_sound = raw.tool_call_sound;
253
+ }
254
+ if ('tool_call_sound_behavior' in raw && raw.tool_call_sound_behavior != null) {
255
+ const valid = ['always', 'with_pre_speech'];
256
+ if (!valid.includes(raw.tool_call_sound_behavior)) {
257
+ this.log.warn(`Voice Agent: invalid feedback.tool_call_sound_behavior for tool "${toolName}" — per-tool feedback excluded`);
258
+ return null;
259
+ }
260
+ result.tool_call_sound_behavior = raw.tool_call_sound_behavior;
261
+ }
262
+ return result;
263
+ }
264
+ validateSoftTimeout(config) {
265
+ if (config == null || typeof config !== 'object' || Array.isArray(config))
266
+ return null;
267
+ const cfg = config;
268
+ const timeoutSeconds = Number(cfg.timeout_seconds);
269
+ if (timeoutSeconds <= 0 || timeoutSeconds > 30) {
270
+ this.log.warn('Voice Agent: invalid soft_timeout.timeout_seconds — soft timeout config excluded');
271
+ return null;
272
+ }
273
+ const validModes = ['static', 'auto'];
274
+ if (!validModes.includes(cfg.mode)) {
275
+ this.log.warn('Voice Agent: invalid soft_timeout.mode — soft timeout config excluded');
276
+ return null;
277
+ }
278
+ // Validate phrases
279
+ const phrases = cfg.phrases;
280
+ if (!Array.isArray(phrases) ||
281
+ phrases.length < 1 ||
282
+ phrases.length > 20 ||
283
+ !phrases.every((p) => typeof p === 'string' && p.trim().length > 0)) {
284
+ this.log.warn('Voice Agent: invalid soft_timeout.phrases — soft timeout config excluded');
285
+ return null;
286
+ }
287
+ return {
288
+ timeout_seconds: timeoutSeconds,
289
+ phrases: phrases,
290
+ mode: cfg.mode
291
+ };
292
+ }
149
293
  parseAgentServiceOptions(label, options) {
150
294
  if (options == null || options === '')
151
295
  return undefined;
@@ -163,8 +307,9 @@ class VoiceAgent extends voice_1.default {
163
307
  return options;
164
308
  }
165
309
  async exitToThread() {
166
- await this.thread.set('__voiceAgentContext', this.state.result);
167
- this.thread.exitStep(this.state.exitStep, this.state.result);
310
+ const result = this.state.result;
311
+ await this.thread.set(`__voiceAgentContext_${this.thread.id}`, result);
312
+ this.thread.exitStep(this.state.exitStep, result);
168
313
  }
169
314
  }
170
315
  exports.default = VoiceAgent;
package/dst/voice.js CHANGED
@@ -18,8 +18,7 @@ class VoiceStep extends step_1.default {
18
18
  if (this.cache != null) {
19
19
  if (this.thread.id !== this.dataThreadId)
20
20
  this.thread.refThread(this.dataThreadId);
21
- if (!this.state.direct)
22
- await this.handleHeartbeat(this.cache);
21
+ await this.handleHeartbeat(this.cache);
23
22
  }
24
23
  }
25
24
  exitStep(exitId, data, byThread = false) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onereach/step-voice",
3
- "version": "7.0.26-voicemaildetection.0",
3
+ "version": "7.0.27",
4
4
  "author": "Roman Zolotarov <roman.zolotarov@onereach.com>",
5
5
  "contributors": [
6
6
  "Roman Zolotarov",