@navai/voice-frontend 0.1.3 → 0.1.6

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/index.js CHANGED
@@ -247,27 +247,33 @@ function getNavaiRoutePromptLines(routes = []) {
247
247
  // src/agent.ts
248
248
  var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set(["navigate_to", "execute_app_function"]);
249
249
  var TOOL_NAME_REGEXP = /^[a-zA-Z0-9_-]{1,64}$/;
250
+ var DEBUG_PREFIX = "[navai debug]";
250
251
  function toErrorMessage2(error) {
251
252
  return error instanceof Error ? error.message : String(error);
252
253
  }
253
- async function buildNavaiAgent(options) {
254
- const functionsRegistry = await loadNavaiFunctions(options.functionModuleLoaders ?? {});
255
- const backendWarnings = [];
254
+ function debugLog(message, details) {
255
+ if (details === void 0) {
256
+ console.log(`${DEBUG_PREFIX} ${message}`);
257
+ return;
258
+ }
259
+ console.log(`${DEBUG_PREFIX} ${message}`, details);
260
+ }
261
+ function normalizeBackendFunctions(backendFunctions, functionsRegistry, warnings) {
256
262
  const backendFunctionsByName = /* @__PURE__ */ new Map();
257
263
  const backendFunctionsOrdered = [];
258
- for (const backendFunction of options.backendFunctions ?? []) {
264
+ for (const backendFunction of backendFunctions ?? []) {
259
265
  const name = backendFunction.name.trim().toLowerCase();
260
266
  if (!name) {
261
267
  continue;
262
268
  }
263
269
  if (functionsRegistry.byName.has(name)) {
264
- backendWarnings.push(
270
+ warnings.push(
265
271
  `[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
266
272
  );
267
273
  continue;
268
274
  }
269
275
  if (backendFunctionsByName.has(name)) {
270
- backendWarnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
276
+ warnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
271
277
  continue;
272
278
  }
273
279
  const normalizedDefinition = {
@@ -277,94 +283,119 @@ async function buildNavaiAgent(options) {
277
283
  backendFunctionsByName.set(name, normalizedDefinition);
278
284
  backendFunctionsOrdered.push(normalizedDefinition);
279
285
  }
286
+ return backendFunctionsOrdered;
287
+ }
288
+ function createExecuteAppFunction(input) {
289
+ const backendFunctionsByName = new Map(input.backendFunctions.map((item) => [item.name, item]));
280
290
  const availableFunctionNames = [
281
- ...functionsRegistry.ordered.map((item) => item.name),
282
- ...backendFunctionsOrdered.map((item) => item.name)
291
+ ...input.functionsRegistry.ordered.map((item) => item.name),
292
+ ...input.backendFunctions.map((item) => item.name)
283
293
  ];
284
- const aliasWarnings = [];
285
- const directFunctionToolNames = [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
286
- if (!name) {
287
- return false;
288
- }
289
- if (RESERVED_TOOL_NAMES.has(name)) {
290
- aliasWarnings.push(
291
- `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
292
- );
293
- return false;
294
- }
295
- if (!TOOL_NAME_REGEXP.test(name)) {
296
- aliasWarnings.push(
297
- `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
298
- );
299
- return false;
300
- }
301
- return true;
302
- });
303
294
  const executeAppFunction = async (requestedName, payload) => {
304
295
  const requested = requestedName.trim().toLowerCase();
305
- const frontendDefinition = functionsRegistry.byName.get(requested);
296
+ debugLog("execute_app_function called", { requestedName, requested, payload });
297
+ const frontendDefinition = input.functionsRegistry.byName.get(requested);
306
298
  if (frontendDefinition) {
307
299
  try {
308
- const result = await frontendDefinition.run(payload ?? {}, options);
309
- return { ok: true, function_name: frontendDefinition.name, source: frontendDefinition.source, result };
300
+ debugLog("executing frontend function", {
301
+ functionName: frontendDefinition.name,
302
+ source: frontendDefinition.source
303
+ });
304
+ const result = await frontendDefinition.run(payload ?? {}, input.context);
305
+ const response = {
306
+ ok: true,
307
+ function_name: frontendDefinition.name,
308
+ source: frontendDefinition.source,
309
+ result
310
+ };
311
+ debugLog("frontend function completed", response);
312
+ return response;
310
313
  } catch (error) {
311
- return {
314
+ const failure = {
312
315
  ok: false,
313
316
  function_name: frontendDefinition.name,
314
317
  error: "Function execution failed.",
315
318
  details: toErrorMessage2(error)
316
319
  };
320
+ debugLog("frontend function failed", failure);
321
+ return failure;
317
322
  }
318
323
  }
319
324
  const backendDefinition = backendFunctionsByName.get(requested);
320
325
  if (!backendDefinition) {
321
- return {
326
+ const failure = {
322
327
  ok: false,
323
328
  error: "Unknown or disallowed function.",
324
329
  available_functions: availableFunctionNames
325
330
  };
331
+ debugLog("execute_app_function rejected unknown function", failure);
332
+ return failure;
326
333
  }
327
- if (!options.executeBackendFunction) {
328
- return {
334
+ if (!input.executeBackendFunction) {
335
+ const failure = {
329
336
  ok: false,
330
337
  function_name: backendDefinition.name,
331
338
  error: "Backend function execution is not configured."
332
339
  };
340
+ debugLog("backend function execution unavailable", failure);
341
+ return failure;
333
342
  }
334
343
  try {
335
- const result = await options.executeBackendFunction({
344
+ debugLog("executing backend function", {
345
+ functionName: backendDefinition.name,
346
+ source: backendDefinition.source ?? "backend"
347
+ });
348
+ const result = await input.executeBackendFunction({
336
349
  functionName: backendDefinition.name,
337
350
  payload: payload ?? null
338
351
  });
339
- return {
352
+ const response = {
340
353
  ok: true,
341
354
  function_name: backendDefinition.name,
342
355
  source: backendDefinition.source ?? "backend",
343
356
  result
344
357
  };
358
+ debugLog("backend function completed", response);
359
+ return response;
345
360
  } catch (error) {
346
- return {
361
+ const failure = {
347
362
  ok: false,
348
363
  function_name: backendDefinition.name,
349
364
  error: "Function execution failed.",
350
365
  details: toErrorMessage2(error)
351
366
  };
367
+ debugLog("backend function failed", failure);
368
+ return failure;
352
369
  }
353
370
  };
354
- const navigateTool = tool({
355
- name: "navigate_to",
356
- description: "Navigate to an allowed route in the current app.",
357
- parameters: z.object({
358
- target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
359
- }),
360
- execute: async ({ target }) => {
361
- const path = resolveNavaiRoute(target, options.routes);
362
- if (!path) {
363
- return { ok: false, error: "Unknown or disallowed route." };
364
- }
365
- options.navigate(path);
366
- return { ok: true, path };
371
+ return {
372
+ availableFunctionNames,
373
+ executeAppFunction
374
+ };
375
+ }
376
+ function createFunctionTools(input) {
377
+ const aliasWarnings = [];
378
+ const availableFunctionNames = [
379
+ ...input.functionsRegistry.ordered.map((item) => item.name),
380
+ ...input.backendFunctions.map((item) => item.name)
381
+ ];
382
+ const directFunctionToolNames = input.includeDirectAliases === false ? [] : [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
383
+ if (!name) {
384
+ return false;
385
+ }
386
+ if (RESERVED_TOOL_NAMES.has(name)) {
387
+ aliasWarnings.push(
388
+ `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
389
+ );
390
+ return false;
391
+ }
392
+ if (!TOOL_NAME_REGEXP.test(name)) {
393
+ aliasWarnings.push(
394
+ `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
395
+ );
396
+ return false;
367
397
  }
398
+ return true;
368
399
  });
369
400
  const executeFunctionTool = tool({
370
401
  name: "execute_app_function",
@@ -375,36 +406,146 @@ async function buildNavaiAgent(options) {
375
406
  "Payload object. Use null when no arguments are needed. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
376
407
  )
377
408
  }),
378
- execute: async ({ function_name, payload }) => await executeAppFunction(function_name, payload)
409
+ execute: async ({ function_name, payload }) => await input.executeAppFunction(function_name, payload)
379
410
  });
380
411
  const directFunctionTools = directFunctionToolNames.map(
381
412
  (functionName) => tool({
382
413
  name: functionName,
383
414
  description: `Direct alias for execute_app_function("${functionName}").`,
384
415
  parameters: z.object({
385
- payload: z.record(z.string(), z.unknown()).nullable().optional().describe(
416
+ payload: z.record(z.string(), z.unknown()).nullable().describe(
386
417
  "Payload object. Optional. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
387
418
  )
388
419
  }),
389
- execute: async ({ payload }) => await executeAppFunction(functionName, payload ?? null)
420
+ execute: async ({ payload }) => await input.executeAppFunction(functionName, payload ?? null)
390
421
  })
391
422
  );
392
- const routeLines = getNavaiRoutePromptLines(options.routes);
393
- const functionLines = functionsRegistry.ordered.length + backendFunctionsOrdered.length > 0 ? [
423
+ return {
424
+ aliasWarnings,
425
+ availableFunctionNames,
426
+ executeFunctionTool,
427
+ directFunctionTools
428
+ };
429
+ }
430
+ function buildFunctionLines(functionsRegistry, backendFunctions) {
431
+ return functionsRegistry.ordered.length + backendFunctions.length > 0 ? [
394
432
  ...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
395
- ...backendFunctionsOrdered.map(
396
- (item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
397
- )
433
+ ...backendFunctions.map((item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`)
398
434
  ] : ["- none"];
435
+ }
436
+ async function buildNavaiAgent(options) {
437
+ const aggregatedWarnings = [];
438
+ const configuredAgents = (options.agents ?? []).filter(
439
+ (agent2) => Object.keys(agent2.functionModuleLoaders ?? {}).length > 0
440
+ );
441
+ const primaryAgentConfig = configuredAgents.find((agent2) => agent2.key === options.primaryAgentKey) ?? configuredAgents.find((agent2) => agent2.isPrimary) ?? configuredAgents[0];
442
+ const primaryFunctionLoaders = primaryAgentConfig?.functionModuleLoaders ?? options.functionModuleLoaders ?? {};
443
+ const functionsRegistry = await loadNavaiFunctions(primaryFunctionLoaders);
444
+ const backendFunctionsOrdered = normalizeBackendFunctions(options.backendFunctions, functionsRegistry, aggregatedWarnings);
445
+ const primaryExecutionSurface = createExecuteAppFunction({
446
+ functionsRegistry,
447
+ backendFunctions: backendFunctionsOrdered,
448
+ executeBackendFunction: options.executeBackendFunction,
449
+ context: options
450
+ });
451
+ const primaryFunctionTools = createFunctionTools({
452
+ functionsRegistry,
453
+ backendFunctions: backendFunctionsOrdered,
454
+ executeAppFunction: primaryExecutionSurface.executeAppFunction
455
+ });
456
+ aggregatedWarnings.push(...functionsRegistry.warnings, ...primaryFunctionTools.aliasWarnings);
457
+ const navigateTool = tool({
458
+ name: "navigate_to",
459
+ description: "Navigate to an allowed route in the current app.",
460
+ parameters: z.object({
461
+ target: z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
462
+ }),
463
+ execute: async ({ target }) => {
464
+ debugLog("navigate_to called", { target });
465
+ const path = resolveNavaiRoute(target, options.routes);
466
+ if (!path) {
467
+ const failure = { ok: false, error: "Unknown or disallowed route." };
468
+ debugLog("navigate_to rejected", failure);
469
+ return failure;
470
+ }
471
+ options.navigate(path);
472
+ const response = { ok: true, path };
473
+ debugLog("navigate_to completed", response);
474
+ return response;
475
+ }
476
+ });
477
+ const routeLines = getNavaiRoutePromptLines(options.routes);
478
+ const functionLines = buildFunctionLines(functionsRegistry, backendFunctionsOrdered);
479
+ const specialistAgents = [];
480
+ const specialistLines = [];
481
+ for (const runtimeAgent of configuredAgents) {
482
+ if (primaryAgentConfig && runtimeAgent.key === primaryAgentConfig.key) {
483
+ continue;
484
+ }
485
+ const specialistRegistry = await loadNavaiFunctions(runtimeAgent.functionModuleLoaders);
486
+ const specialistWarnings = [...specialistRegistry.warnings];
487
+ const specialistBackendFunctions = normalizeBackendFunctions(
488
+ options.backendFunctions,
489
+ specialistRegistry,
490
+ specialistWarnings
491
+ );
492
+ const specialistExecutionSurface = createExecuteAppFunction({
493
+ functionsRegistry: specialistRegistry,
494
+ backendFunctions: specialistBackendFunctions,
495
+ executeBackendFunction: options.executeBackendFunction,
496
+ context: options
497
+ });
498
+ const specialistFunctionTools = createFunctionTools({
499
+ functionsRegistry: specialistRegistry,
500
+ backendFunctions: specialistBackendFunctions,
501
+ executeAppFunction: specialistExecutionSurface.executeAppFunction,
502
+ includeDirectAliases: false
503
+ });
504
+ specialistWarnings.push(...specialistFunctionTools.aliasWarnings);
505
+ aggregatedWarnings.push(...specialistWarnings);
506
+ const specialistInstructions = [
507
+ runtimeAgent.instructions ?? `You are the ${runtimeAgent.name} specialist agent for this web app.`,
508
+ "Allowed app functions:",
509
+ ...buildFunctionLines(specialistRegistry, specialistBackendFunctions),
510
+ "Rules:",
511
+ "- Always use execute_app_function for app actions.",
512
+ "- When no arguments are needed, call execute_app_function with payload set to null.",
513
+ "- Use only the functions available to this specialist agent.",
514
+ "- Do not navigate unless one of your allowed functions explicitly does so.",
515
+ "- Return a concise result to the main NAVAI agent."
516
+ ].join("\n");
517
+ debugLog("creating specialist agent", {
518
+ key: runtimeAgent.key,
519
+ name: runtimeAgent.name,
520
+ functions: [
521
+ ...specialistRegistry.ordered.map((item) => item.name),
522
+ ...specialistBackendFunctions.map((item) => item.name)
523
+ ]
524
+ });
525
+ const specialistAgent = new RealtimeAgent({
526
+ name: runtimeAgent.name,
527
+ handoffDescription: runtimeAgent.handoffDescription ?? runtimeAgent.description ?? `Delegate specialist work to ${runtimeAgent.name}.`,
528
+ instructions: specialistInstructions,
529
+ tools: [specialistFunctionTools.executeFunctionTool]
530
+ });
531
+ specialistAgents.push(specialistAgent);
532
+ specialistLines.push(
533
+ `- ${runtimeAgent.name}: ${runtimeAgent.description ?? runtimeAgent.handoffDescription ?? "Specialist agent available by delegation."}`
534
+ );
535
+ }
399
536
  const instructions = [
400
- options.baseInstructions ?? "You are a voice assistant embedded in a web app.",
537
+ primaryAgentConfig?.instructions ?? options.baseInstructions ?? "You are the main NAVAI voice agent embedded in a web app.",
401
538
  "Allowed routes:",
402
539
  ...routeLines,
403
540
  "Allowed app functions:",
404
541
  ...functionLines,
542
+ "Available specialist agents:",
543
+ ...specialistLines.length > 0 ? specialistLines : ["- none"],
405
544
  "Rules:",
406
545
  "- If user asks to go/open a section, always call navigate_to.",
407
- "- If user asks to run an internal action, call execute_app_function or the matching direct function tool.",
546
+ "- If user asks to run an internal action that belongs to you, call execute_app_function or the matching direct function tool.",
547
+ "- If the task clearly belongs to a specialist agent, hand off to that specialist agent.",
548
+ "- Food recommendations, fast food, hamburgers, pizza, tacos, snacks, and meal suggestions belong to the food specialist.",
408
549
  "- Always include payload in execute_app_function. Use null when no arguments are needed.",
409
550
  "- For execute_app_function, pass arguments using payload.args (array).",
410
551
  "- For class methods, pass payload.constructorArgs and payload.methodArgs.",
@@ -412,11 +553,23 @@ async function buildNavaiAgent(options) {
412
553
  "- If destination/action is unclear, ask a brief clarifying question."
413
554
  ].join("\n");
414
555
  const agent = new RealtimeAgent({
415
- name: options.agentName ?? "Navai Voice Agent",
556
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
416
557
  instructions,
417
- tools: [navigateTool, executeFunctionTool, ...directFunctionTools]
558
+ handoffs: specialistAgents,
559
+ tools: [
560
+ navigateTool,
561
+ primaryFunctionTools.executeFunctionTool,
562
+ ...primaryFunctionTools.directFunctionTools
563
+ ]
564
+ });
565
+ debugLog("created primary agent", {
566
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
567
+ primaryAgentKey: primaryAgentConfig?.key ?? null,
568
+ directFunctions: functionsRegistry.ordered.map((item) => item.name),
569
+ backendFunctions: backendFunctionsOrdered.map((item) => item.name),
570
+ specialistDelegates: configuredAgents.filter((runtimeAgent) => runtimeAgent.key !== primaryAgentConfig?.key).map((runtimeAgent) => runtimeAgent.key)
418
571
  });
419
- return { agent, warnings: [...functionsRegistry.warnings, ...backendWarnings, ...aliasWarnings] };
572
+ return { agent, warnings: aggregatedWarnings };
420
573
  }
421
574
 
422
575
  // src/backend.ts
@@ -539,6 +692,7 @@ function createNavaiBackendClient(options = {}) {
539
692
  // src/runtime.ts
540
693
  var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
541
694
  var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
695
+ var AGENTS_ENV_KEYS = ["NAVAI_AGENTS_FOLDERS"];
542
696
  var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
543
697
  async function resolveNavaiFrontendRuntimeConfig(options) {
544
698
  const warnings = [];
@@ -548,6 +702,7 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
548
702
  const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
549
703
  const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
550
704
  const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
705
+ const agentsFolders = readOptional2(options.agentsFolders) ?? readFirstOptionalEnv(options.env, AGENTS_ENV_KEYS);
551
706
  const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
552
707
  const routes = await resolveRoutes({
553
708
  routesFile,
@@ -559,12 +714,23 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
559
714
  const functionModuleLoaders = resolveFunctionModuleLoaders({
560
715
  indexedLoaders,
561
716
  functionsFolders,
717
+ agentsFolders,
562
718
  defaultFunctionsFolder,
563
719
  warnings
564
720
  });
721
+ const agents = await resolveRuntimeAgents({
722
+ indexedLoaders,
723
+ functionModuleLoaders,
724
+ functionsFolders,
725
+ agentsFolders,
726
+ defaultFunctionsFolder
727
+ });
728
+ const primaryAgentKey = agents.find((agent) => agent.isPrimary)?.key;
565
729
  return {
566
730
  routes,
567
731
  functionModuleLoaders,
732
+ agents,
733
+ primaryAgentKey,
568
734
  modelOverride,
569
735
  warnings
570
736
  };
@@ -600,10 +766,11 @@ async function resolveRoutes(input) {
600
766
  }
601
767
  function resolveFunctionModuleLoaders(input) {
602
768
  const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
769
+ const agentFolders = parseCsvList(input.agentsFolders);
603
770
  const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
604
- const matchers = tokens.map((token) => createPathMatcher(token));
771
+ const matchers = tokens.map((token) => createPathMatcher(token, agentFolders));
605
772
  const matchedEntries = input.indexedLoaders.filter(
606
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
773
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && matchers.some((matcher) => matcher(entry.normalizedPath))
607
774
  );
608
775
  if (matchedEntries.length > 0) {
609
776
  return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
@@ -613,9 +780,9 @@ function resolveFunctionModuleLoaders(input) {
613
780
  `[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
614
781
  );
615
782
  }
616
- const fallbackMatcher = createPathMatcher(input.defaultFunctionsFolder);
783
+ const fallbackMatcherWithAgents = createPathMatcher(input.defaultFunctionsFolder, agentFolders);
617
784
  const fallbackEntries = input.indexedLoaders.filter(
618
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && fallbackMatcher(entry.normalizedPath)
785
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && fallbackMatcherWithAgents(entry.normalizedPath)
619
786
  );
620
787
  return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
621
788
  }
@@ -658,7 +825,7 @@ function buildModuleCandidates(inputPath) {
658
825
  }
659
826
  return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
660
827
  }
661
- function createPathMatcher(input) {
828
+ function createPathMatcher(input, agentFolders = []) {
662
829
  const raw = normalizePath(input);
663
830
  if (!raw) {
664
831
  return () => false;
@@ -676,8 +843,141 @@ function createPathMatcher(input) {
676
843
  return (path) => path === normalized;
677
844
  }
678
845
  const base = normalized.replace(/\/+$/, "");
846
+ const normalizedAgents = agentFolders.map(normalizePathSegment).filter(Boolean);
847
+ if (normalizedAgents.length > 0) {
848
+ return (path) => {
849
+ if (!path.startsWith(`${base}/`)) {
850
+ return false;
851
+ }
852
+ const suffix = path.slice(base.length + 1);
853
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
854
+ return normalizedAgents.includes(firstSegment);
855
+ };
856
+ }
679
857
  return (path) => path === base || path.startsWith(`${base}/`);
680
858
  }
859
+ async function resolveRuntimeAgents(input) {
860
+ const configuredAgents = parseCsvList(input.agentsFolders);
861
+ if (configuredAgents.length === 0) {
862
+ return [];
863
+ }
864
+ const loaderByPath = new Map(input.indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
865
+ const baseDirectories = resolveAgentBaseDirectories(input.functionsFolders, input.defaultFunctionsFolder);
866
+ const groupedLoaders = /* @__PURE__ */ new Map();
867
+ for (const [rawPath, load] of Object.entries(input.functionModuleLoaders)) {
868
+ const agentKey = extractAgentKeyFromPath(rawPath, baseDirectories, configuredAgents);
869
+ if (!agentKey) {
870
+ continue;
871
+ }
872
+ const current = groupedLoaders.get(agentKey) ?? {};
873
+ current[rawPath] = load;
874
+ groupedLoaders.set(agentKey, current);
875
+ }
876
+ const configuredPrimaryKey = configuredAgents[0];
877
+ const agents = [];
878
+ for (const agentKey of configuredAgents) {
879
+ const functionLoaders = groupedLoaders.get(agentKey);
880
+ if (!functionLoaders || Object.keys(functionLoaders).length === 0) {
881
+ continue;
882
+ }
883
+ const config = await loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath);
884
+ agents.push({
885
+ key: config.key?.trim() || agentKey,
886
+ name: readOptional2(config.name) ?? humanizeAgentKey(agentKey),
887
+ description: readOptional2(config.description),
888
+ handoffDescription: readOptional2(config.handoffDescription) ?? readOptional2(config.description),
889
+ instructions: readOptional2(config.instructions),
890
+ isPrimary: config.isPrimary === true || agentKey === configuredPrimaryKey,
891
+ functionModuleLoaders: functionLoaders
892
+ });
893
+ }
894
+ if (agents.filter((agent) => agent.isPrimary).length === 0 && agents[0]) {
895
+ agents[0].isPrimary = true;
896
+ }
897
+ if (agents.filter((agent) => agent.isPrimary).length > 1) {
898
+ let primaryAssigned = false;
899
+ for (const agent of agents) {
900
+ if (agent.isPrimary && !primaryAssigned) {
901
+ primaryAssigned = true;
902
+ continue;
903
+ }
904
+ agent.isPrimary = false;
905
+ }
906
+ }
907
+ return agents;
908
+ }
909
+ async function loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath) {
910
+ for (const baseDirectory of baseDirectories) {
911
+ const configBase = `${baseDirectory}/${agentKey}/agent.config`;
912
+ const matchedLoader = buildModuleCandidates(configBase).map((candidate) => loaderByPath.get(candidate)).find(Boolean);
913
+ if (!matchedLoader) {
914
+ continue;
915
+ }
916
+ try {
917
+ const imported = await matchedLoader.load();
918
+ return readAgentModuleConfig(imported);
919
+ } catch {
920
+ return {};
921
+ }
922
+ }
923
+ return {};
924
+ }
925
+ function readAgentModuleConfig(moduleShape) {
926
+ const candidate = readRecord(moduleShape.NAVAI_AGENT) ?? readRecord(moduleShape.agent) ?? readRecord(moduleShape.default) ?? {};
927
+ return {
928
+ key: readOptionalString(candidate.key),
929
+ name: readOptionalString(candidate.name),
930
+ description: readOptionalString(candidate.description),
931
+ handoffDescription: readOptionalString(candidate.handoffDescription),
932
+ instructions: readOptionalString(candidate.instructions),
933
+ isPrimary: candidate.isPrimary === true
934
+ };
935
+ }
936
+ function readRecord(value) {
937
+ return value && typeof value === "object" ? value : null;
938
+ }
939
+ function readOptionalString(value) {
940
+ return typeof value === "string" ? readOptional2(value) : void 0;
941
+ }
942
+ function resolveAgentBaseDirectories(functionsFolders, defaultFunctionsFolder) {
943
+ const configuredTokens = functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
944
+ const tokens = configuredTokens.length > 0 ? configuredTokens : [defaultFunctionsFolder];
945
+ return [...new Set(tokens.map(toAgentBaseDirectory).filter(Boolean))];
946
+ }
947
+ function toAgentBaseDirectory(input) {
948
+ const raw = normalizePath(input);
949
+ if (!raw) {
950
+ return null;
951
+ }
952
+ const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
953
+ if (normalized.includes("*") || /\.[cm]?[jt]s$/.test(normalized)) {
954
+ return null;
955
+ }
956
+ if (normalized.endsWith("/...")) {
957
+ return normalized.slice(0, -4).replace(/\/+$/, "") || null;
958
+ }
959
+ return normalized.replace(/\/+$/, "") || null;
960
+ }
961
+ function extractAgentKeyFromPath(pathValue, baseDirectories, configuredAgents) {
962
+ const normalized = normalizePath(pathValue);
963
+ for (const baseDirectory of baseDirectories) {
964
+ if (!normalized.startsWith(`${baseDirectory}/`)) {
965
+ continue;
966
+ }
967
+ const suffix = normalized.slice(baseDirectory.length + 1);
968
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
969
+ if (configuredAgents.includes(firstSegment)) {
970
+ return firstSegment;
971
+ }
972
+ }
973
+ return void 0;
974
+ }
975
+ function humanizeAgentKey(value) {
976
+ return value.split(/[_-]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
977
+ }
978
+ function isAgentConfigPath(pathValue) {
979
+ return /\/agent\.config\.[cm]?[jt]s$/i.test(pathValue);
980
+ }
681
981
  function globToRegExp(pattern) {
682
982
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
683
983
  const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
@@ -688,6 +988,12 @@ function globToRegExp(pattern) {
688
988
  function normalizePath(input) {
689
989
  return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
690
990
  }
991
+ function normalizePathSegment(input) {
992
+ return normalizePath(input).replace(/\//g, "");
993
+ }
994
+ function parseCsvList(input) {
995
+ return (input ?? "").split(",").map((value) => normalizePathSegment(value)).filter(Boolean);
996
+ }
691
997
  function readFirstOptionalEnv(env, keys) {
692
998
  if (!env) {
693
999
  return void 0;
@@ -718,6 +1024,7 @@ function toErrorMessage3(error) {
718
1024
  // src/useWebVoiceAgent.ts
719
1025
  import { RealtimeSession } from "@openai/agents/realtime";
720
1026
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1027
+ var DEBUG_PREFIX2 = "[navai debug]";
721
1028
  function formatError(error) {
722
1029
  if (error instanceof Error) {
723
1030
  return error.message;
@@ -731,6 +1038,13 @@ function emitWarnings(warnings) {
731
1038
  }
732
1039
  }
733
1040
  }
1041
+ function debugLog2(message, details) {
1042
+ if (details === void 0) {
1043
+ console.log(`${DEBUG_PREFIX2} ${message}`);
1044
+ return;
1045
+ }
1046
+ console.log(`${DEBUG_PREFIX2} ${message}`, details);
1047
+ }
734
1048
  function useWebVoiceAgent(options) {
735
1049
  const sessionRef = useRef(null);
736
1050
  const attachedRealtimeSessionRef = useRef(null);
@@ -741,6 +1055,7 @@ function useWebVoiceAgent(options) {
741
1055
  env: options.env,
742
1056
  routesFile: options.routesFile,
743
1057
  functionsFolders: options.functionsFolders,
1058
+ agentsFolders: options.agentsFolders,
744
1059
  modelOverride: options.modelOverride,
745
1060
  defaultRoutesFile: options.defaultRoutesFile,
746
1061
  defaultFunctionsFolder: options.defaultFunctionsFolder
@@ -749,6 +1064,7 @@ function useWebVoiceAgent(options) {
749
1064
  options.defaultFunctionsFolder,
750
1065
  options.defaultRoutes,
751
1066
  options.defaultRoutesFile,
1067
+ options.agentsFolders,
752
1068
  options.env,
753
1069
  options.functionsFolders,
754
1070
  options.modelOverride,
@@ -795,6 +1111,45 @@ function useWebVoiceAgent(options) {
795
1111
  const attachSessionAudioListeners = useCallback(
796
1112
  (session) => {
797
1113
  detachSessionAudioListeners();
1114
+ session.on("agent_start", (_context, agent, turnInput) => {
1115
+ debugLog2("session agent_start", {
1116
+ agent: agent.name,
1117
+ turnInputCount: Array.isArray(turnInput) ? turnInput.length : 0
1118
+ });
1119
+ });
1120
+ session.on("agent_end", (_context, agent, output) => {
1121
+ debugLog2("session agent_end", {
1122
+ agent: agent.name,
1123
+ output
1124
+ });
1125
+ });
1126
+ session.on("agent_handoff", (_context, fromAgent, toAgent) => {
1127
+ debugLog2("session agent_handoff", {
1128
+ from: fromAgent.name,
1129
+ to: toAgent.name
1130
+ });
1131
+ });
1132
+ session.on("agent_tool_start", (_context, agent, tool2, details) => {
1133
+ debugLog2("session agent_tool_start", {
1134
+ agent: agent.name,
1135
+ tool: tool2.name,
1136
+ toolCall: details.toolCall
1137
+ });
1138
+ });
1139
+ session.on("agent_tool_end", (_context, agent, tool2, result, details) => {
1140
+ debugLog2("session agent_tool_end", {
1141
+ agent: agent.name,
1142
+ tool: tool2.name,
1143
+ result,
1144
+ toolCall: details.toolCall
1145
+ });
1146
+ });
1147
+ session.on("history_added", (item) => {
1148
+ debugLog2("session history_added", item);
1149
+ });
1150
+ session.on("error", (sessionError) => {
1151
+ debugLog2("session error", sessionError);
1152
+ });
798
1153
  session.on("audio_start", handleSessionAudioStart);
799
1154
  session.on("audio_stopped", handleSessionAudioStopped);
800
1155
  session.on("audio_interrupted", handleSessionAudioInterrupted);
@@ -833,6 +1188,17 @@ function useWebVoiceAgent(options) {
833
1188
  setAgentVoiceStateIfChanged("idle");
834
1189
  try {
835
1190
  const runtimeConfig = await runtimeConfigPromise;
1191
+ debugLog2("resolved runtime config", {
1192
+ routes: runtimeConfig.routes.map((route) => route.path),
1193
+ functionModules: Object.keys(runtimeConfig.functionModuleLoaders),
1194
+ agents: runtimeConfig.agents.map((agent2) => ({
1195
+ key: agent2.key,
1196
+ name: agent2.name,
1197
+ isPrimary: agent2.isPrimary,
1198
+ functionModules: Object.keys(agent2.functionModuleLoaders)
1199
+ })),
1200
+ warnings: runtimeConfig.warnings
1201
+ });
836
1202
  const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
837
1203
  const secretPayload = await backendClient.createClientSecret(requestPayload);
838
1204
  const backendFunctionsResult = await backendClient.listFunctions();
@@ -840,6 +1206,8 @@ function useWebVoiceAgent(options) {
840
1206
  navigate: options.navigate,
841
1207
  routes: runtimeConfig.routes,
842
1208
  functionModuleLoaders: runtimeConfig.functionModuleLoaders,
1209
+ agents: runtimeConfig.agents,
1210
+ primaryAgentKey: runtimeConfig.primaryAgentKey,
843
1211
  backendFunctions: backendFunctionsResult.functions,
844
1212
  executeBackendFunction: backendClient.executeFunction
845
1213
  });
@@ -855,6 +1223,7 @@ function useWebVoiceAgent(options) {
855
1223
  setStatus("connected");
856
1224
  } catch (startError) {
857
1225
  const message = formatError(startError);
1226
+ debugLog2("session start failed", { message, error: startError });
858
1227
  setError(message);
859
1228
  setStatus("error");
860
1229
  setAgentVoiceStateIfChanged("idle");