@onereach/step-voice 7.0.26-agenttimeout.0 → 7.0.26-agenttimeout.10

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.
@@ -2,6 +2,7 @@ import VoiceStep from './voice';
2
2
  interface INPUT {
3
3
  result: string;
4
4
  stopAgent: boolean;
5
+ functionId?: string;
5
6
  }
6
7
  export default class VoiceAgentFunctionResult extends VoiceStep<INPUT> {
7
8
  runStep(): Promise<void>;
@@ -4,13 +4,70 @@ 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') ?? {};
7
+ const { result, stopAgent, functionId: inputFunctionId } = this.data;
8
+ this.log.info("VoiceAgentFunctionResult: start", {
9
+ threadId: this.thread.id,
10
+ inputFunctionId,
11
+ stateKeys: Object.keys(this.state || {}),
12
+ stateDirect: this.state?.direct,
13
+ stateResultFunctionId: this.state?.result?.functionId,
14
+ stateResultCallId: this.state?.result?.callId,
15
+ });
16
+ // Attempt to get functionId from multiple sources to handle parallel function calls.
17
+ // Session storage is race-prone when multiple function_call threads run concurrently.
18
+ const threadStateFunctionId = this.state?.result?.functionId;
19
+ const resolvedFunctionId = inputFunctionId || threadStateFunctionId;
20
+ this.log.info("VoiceAgentFunctionResult: resolvedFunctionId", {
21
+ resolvedFunctionId,
22
+ source: inputFunctionId ? 'data.functionId' : threadStateFunctionId ? 'state.result.functionId' : 'none',
23
+ });
24
+ let ctx = null;
25
+ let ctxSource = 'none';
26
+ // Priority 1: functionId-keyed context (race-safe, unique per function call)
27
+ if (resolvedFunctionId) {
28
+ ctx = await this.thread.get(`__voiceAgentContext_fn_${resolvedFunctionId}`);
29
+ if (ctx) {
30
+ ctxSource = `__voiceAgentContext_fn_${resolvedFunctionId}`;
31
+ this.log.info("VoiceAgentFunctionResult: ctx from fn-keyed store", { ctxSource, functionId: ctx.functionId });
32
+ }
33
+ else {
34
+ this.log.info("VoiceAgentFunctionResult: fn-keyed store miss", { key: `__voiceAgentContext_fn_${resolvedFunctionId}` });
35
+ }
36
+ }
37
+ // Priority 2: Thread-specific context
38
+ if (!ctx) {
39
+ ctx = await this.thread.get(`__voiceAgentContext_${this.thread.id}`);
40
+ if (ctx) {
41
+ ctxSource = `__voiceAgentContext_${this.thread.id}`;
42
+ this.log.info("VoiceAgentFunctionResult: ctx from thread-specific store", { ctxSource, functionId: ctx.functionId });
43
+ }
44
+ else {
45
+ this.log.info("VoiceAgentFunctionResult: thread-specific store miss", { key: `__voiceAgentContext_${this.thread.id}` });
46
+ }
47
+ }
48
+ // Priority 3: Use state.result directly if it has callId (from exitToThread → exitStep chain)
49
+ if (!ctx && this.state?.result?.callId) {
50
+ ctx = this.state.result;
51
+ ctxSource = 'state.result';
52
+ this.log.info("VoiceAgentFunctionResult: ctx from state.result", { functionId: ctx.functionId, callId: ctx.callId });
53
+ }
54
+ // Priority 4: Shared session context (backward compat, NOT race-safe for parallel calls)
55
+ if (!ctx) {
56
+ ctx = await this.thread.get('__voiceAgentContext') ?? {};
57
+ ctxSource = '__voiceAgentContext (shared session)';
58
+ this.log.info("VoiceAgentFunctionResult: ctx from shared session", { functionId: ctx?.functionId, callId: ctx?.callId });
59
+ }
8
60
  const { callId, callCallback, name: functionName, functionId } = ctx;
9
- const { result, stopAgent } = this.data;
61
+ this.log.info("VoiceAgentFunctionResult: resolved context", {
62
+ ctxSource,
63
+ callId,
64
+ functionId,
65
+ functionName,
66
+ hasCallback: !!callCallback,
67
+ });
10
68
  if (!callId) {
11
69
  throw new Error('Voice Agent Function Result: missing call context (must be placed on a Voice Agent function exit)');
12
70
  }
13
- this.log.info("Voice Agent Function Result Params", ctx);
14
71
  const params = {
15
72
  functionName: functionName,
16
73
  functionId: functionId,
@@ -27,7 +84,7 @@ class VoiceAgentFunctionResult extends voice_1.default {
27
84
  commands: [{ name: 'agentFunctionResult', params }]
28
85
  }
29
86
  };
30
- this.log.info("Voice Agent Function Result", event);
87
+ this.log.info("VoiceAgentFunctionResult: sending", { functionId, functionName, callId });
31
88
  await this.thread.eventManager.emit(event, {
32
89
  target: callCallback,
33
90
  invocationType: 'async',
@@ -111,7 +111,7 @@ class VoiceAgent extends voice_1.default {
111
111
  this.throwError(new Error(`Voice Agent: unknown mode "${mode}"`));
112
112
  }
113
113
  }
114
- if (this.isStepFlagEnabled(tool_feedback?.enabled)) {
114
+ if (this.isStepFlagEnabled(tool_feedback?.enabled) && tools.length > 0) {
115
115
  const validatedFeedback = this.validateToolFeedback(tool_feedback);
116
116
  if (validatedFeedback) {
117
117
  config.tool_feedback = validatedFeedback;
@@ -142,48 +142,50 @@ class VoiceAgent extends voice_1.default {
142
142
  }
143
143
  validateToolFeedback(config) {
144
144
  if (config == null || typeof config !== 'object')
145
- return undefined;
145
+ return null;
146
146
  const cfg = config;
147
147
  // If enabled is not boolean true, silently return undefined (disabled case)
148
148
  if (typeof cfg.enabled !== 'boolean' || cfg.enabled !== true)
149
- return undefined;
149
+ return null;
150
150
  // Validate pre_tool_speech
151
151
  const validPreToolSpeech = ['auto', 'static', 'off'];
152
152
  if (!validPreToolSpeech.includes(cfg.pre_tool_speech)) {
153
153
  this.log.warn('Voice Agent: invalid tool_feedback.pre_tool_speech — feedback config excluded');
154
- return undefined;
154
+ return null;
155
155
  }
156
156
  // Validate tool_call_sound
157
157
  const validToolCallSound = ['typing', 'none'];
158
158
  if (!validToolCallSound.includes(cfg.tool_call_sound)) {
159
159
  this.log.warn('Voice Agent: invalid tool_feedback.tool_call_sound — feedback config excluded');
160
- return undefined;
160
+ return null;
161
161
  }
162
162
  // Validate tool_call_sound_behavior
163
163
  const validToolCallSoundBehavior = ['always', 'with_pre_speech'];
164
164
  if (!validToolCallSoundBehavior.includes(cfg.tool_call_sound_behavior)) {
165
165
  this.log.warn('Voice Agent: invalid tool_feedback.tool_call_sound_behavior — feedback config excluded');
166
- return undefined;
166
+ return null;
167
167
  }
168
168
  // Validate delay_seconds
169
- const delay = cfg.delay_seconds;
170
- if (typeof delay !== 'number' || !Number.isFinite(delay) || delay <= 0 || delay > 30) {
169
+ const delay = Number(cfg.delay_seconds);
170
+ if (delay <= 0 || delay > 30) {
171
171
  this.log.warn('Voice Agent: invalid tool_feedback.delay_seconds — feedback config excluded');
172
- return undefined;
172
+ return null;
173
173
  }
174
- // Validate pre_tool_speech_phrases
174
+ // Validate pre_tool_speech_phrases (only required when mode is 'static')
175
175
  const phrases = cfg.pre_tool_speech_phrases;
176
- if (!Array.isArray(phrases) ||
177
- phrases.length < 1 ||
178
- phrases.length > 20 ||
179
- !phrases.every((p) => typeof p === 'string' && p.trim().length > 0)) {
180
- this.log.warn('Voice Agent: invalid tool_feedback.pre_tool_speech_phrases feedback config excluded');
181
- return undefined;
176
+ if (cfg.pre_tool_speech === 'static') {
177
+ if (!Array.isArray(phrases) ||
178
+ phrases.length < 1 ||
179
+ phrases.length > 20 ||
180
+ !phrases.every((p) => typeof p === 'string' && p.trim().length > 0)) {
181
+ this.log.warn('Voice Agent: invalid tool_feedback.pre_tool_speech_phrases — feedback config excluded');
182
+ return null;
183
+ }
182
184
  }
183
185
  return {
184
186
  enabled: true,
185
187
  pre_tool_speech: cfg.pre_tool_speech,
186
- pre_tool_speech_phrases: phrases,
188
+ pre_tool_speech_phrases: (Array.isArray(phrases) ? phrases : []),
187
189
  tool_call_sound: cfg.tool_call_sound,
188
190
  tool_call_sound_behavior: cfg.tool_call_sound_behavior,
189
191
  delay_seconds: delay
@@ -216,7 +218,7 @@ class VoiceAgent extends voice_1.default {
216
218
  const result = {};
217
219
  if ('delay_seconds' in raw && raw.delay_seconds != null) {
218
220
  const delay = Number(raw.delay_seconds);
219
- if (!Number.isFinite(delay) || delay <= 0 || delay > 30) {
221
+ if (delay <= 0 || delay > 30) {
220
222
  this.log.warn(`Voice Agent: invalid feedback.delay_seconds for tool "${toolName}" — per-tool feedback excluded`);
221
223
  return null;
222
224
  }
@@ -252,8 +254,8 @@ class VoiceAgent extends voice_1.default {
252
254
  if (config == null || typeof config !== 'object' || Array.isArray(config))
253
255
  return null;
254
256
  const cfg = config;
255
- const timeoutSeconds = cfg.timeout_seconds;
256
- if (typeof timeoutSeconds !== 'number' || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0 || timeoutSeconds > 30) {
257
+ const timeoutSeconds = Number(cfg.timeout_seconds);
258
+ if (timeoutSeconds <= 0 || timeoutSeconds > 30) {
257
259
  this.log.warn('Voice Agent: invalid soft_timeout.timeout_seconds — soft timeout config excluded');
258
260
  return null;
259
261
  }
@@ -294,8 +296,22 @@ class VoiceAgent extends voice_1.default {
294
296
  return options;
295
297
  }
296
298
  async exitToThread() {
297
- await this.thread.set('__voiceAgentContext', this.state.result);
298
- this.thread.exitStep(this.state.exitStep, this.state.result);
299
+ const result = this.state.result;
300
+ this.log.info("VoiceAgent.exitToThread: writing context", {
301
+ threadId: this.thread.id,
302
+ functionId: result?.functionId,
303
+ callId: result?.callId,
304
+ name: result?.name,
305
+ });
306
+ await this.thread.set(`__voiceAgentContext_${this.thread.id}`, result);
307
+ await this.thread.set('__voiceAgentContext', result);
308
+ if (result?.functionId) {
309
+ await this.thread.set(`__voiceAgentContext_fn_${result.functionId}`, result);
310
+ this.log.info("VoiceAgent.exitToThread: wrote fn-keyed context", {
311
+ key: `__voiceAgentContext_fn_${result.functionId}`,
312
+ });
313
+ }
314
+ this.thread.exitStep(this.state.exitStep, result);
299
315
  }
300
316
  }
301
317
  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-agenttimeout.0",
3
+ "version": "7.0.26-agenttimeout.10",
4
4
  "author": "Roman Zolotarov <roman.zolotarov@onereach.com>",
5
5
  "contributors": [
6
6
  "Roman Zolotarov",