@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.cjs CHANGED
@@ -823,27 +823,33 @@ function getNavaiRoutePromptLines(routes = []) {
823
823
  // src/agent.ts
824
824
  var RESERVED_TOOL_NAMES = /* @__PURE__ */ new Set(["navigate_to", "execute_app_function"]);
825
825
  var TOOL_NAME_REGEXP = /^[a-zA-Z0-9_-]{1,64}$/;
826
+ var DEBUG_PREFIX = "[navai debug]";
826
827
  function toErrorMessage2(error) {
827
828
  return error instanceof Error ? error.message : String(error);
828
829
  }
829
- async function buildNavaiAgent(options) {
830
- const functionsRegistry = await loadNavaiFunctions(options.functionModuleLoaders ?? {});
831
- const backendWarnings = [];
830
+ function debugLog(message, details) {
831
+ if (details === void 0) {
832
+ console.log(`${DEBUG_PREFIX} ${message}`);
833
+ return;
834
+ }
835
+ console.log(`${DEBUG_PREFIX} ${message}`, details);
836
+ }
837
+ function normalizeBackendFunctions(backendFunctions, functionsRegistry, warnings) {
832
838
  const backendFunctionsByName = /* @__PURE__ */ new Map();
833
839
  const backendFunctionsOrdered = [];
834
- for (const backendFunction of options.backendFunctions ?? []) {
840
+ for (const backendFunction of backendFunctions ?? []) {
835
841
  const name = backendFunction.name.trim().toLowerCase();
836
842
  if (!name) {
837
843
  continue;
838
844
  }
839
845
  if (functionsRegistry.byName.has(name)) {
840
- backendWarnings.push(
846
+ warnings.push(
841
847
  `[navai] Ignored backend function "${backendFunction.name}": name conflicts with a frontend function.`
842
848
  );
843
849
  continue;
844
850
  }
845
851
  if (backendFunctionsByName.has(name)) {
846
- backendWarnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
852
+ warnings.push(`[navai] Ignored duplicated backend function "${backendFunction.name}".`);
847
853
  continue;
848
854
  }
849
855
  const normalizedDefinition = {
@@ -853,94 +859,119 @@ async function buildNavaiAgent(options) {
853
859
  backendFunctionsByName.set(name, normalizedDefinition);
854
860
  backendFunctionsOrdered.push(normalizedDefinition);
855
861
  }
862
+ return backendFunctionsOrdered;
863
+ }
864
+ function createExecuteAppFunction(input) {
865
+ const backendFunctionsByName = new Map(input.backendFunctions.map((item) => [item.name, item]));
856
866
  const availableFunctionNames = [
857
- ...functionsRegistry.ordered.map((item) => item.name),
858
- ...backendFunctionsOrdered.map((item) => item.name)
867
+ ...input.functionsRegistry.ordered.map((item) => item.name),
868
+ ...input.backendFunctions.map((item) => item.name)
859
869
  ];
860
- const aliasWarnings = [];
861
- const directFunctionToolNames = [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
862
- if (!name) {
863
- return false;
864
- }
865
- if (RESERVED_TOOL_NAMES.has(name)) {
866
- aliasWarnings.push(
867
- `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
868
- );
869
- return false;
870
- }
871
- if (!TOOL_NAME_REGEXP.test(name)) {
872
- aliasWarnings.push(
873
- `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
874
- );
875
- return false;
876
- }
877
- return true;
878
- });
879
870
  const executeAppFunction = async (requestedName, payload) => {
880
871
  const requested = requestedName.trim().toLowerCase();
881
- const frontendDefinition = functionsRegistry.byName.get(requested);
872
+ debugLog("execute_app_function called", { requestedName, requested, payload });
873
+ const frontendDefinition = input.functionsRegistry.byName.get(requested);
882
874
  if (frontendDefinition) {
883
875
  try {
884
- const result = await frontendDefinition.run(payload ?? {}, options);
885
- return { ok: true, function_name: frontendDefinition.name, source: frontendDefinition.source, result };
876
+ debugLog("executing frontend function", {
877
+ functionName: frontendDefinition.name,
878
+ source: frontendDefinition.source
879
+ });
880
+ const result = await frontendDefinition.run(payload ?? {}, input.context);
881
+ const response = {
882
+ ok: true,
883
+ function_name: frontendDefinition.name,
884
+ source: frontendDefinition.source,
885
+ result
886
+ };
887
+ debugLog("frontend function completed", response);
888
+ return response;
886
889
  } catch (error) {
887
- return {
890
+ const failure = {
888
891
  ok: false,
889
892
  function_name: frontendDefinition.name,
890
893
  error: "Function execution failed.",
891
894
  details: toErrorMessage2(error)
892
895
  };
896
+ debugLog("frontend function failed", failure);
897
+ return failure;
893
898
  }
894
899
  }
895
900
  const backendDefinition = backendFunctionsByName.get(requested);
896
901
  if (!backendDefinition) {
897
- return {
902
+ const failure = {
898
903
  ok: false,
899
904
  error: "Unknown or disallowed function.",
900
905
  available_functions: availableFunctionNames
901
906
  };
907
+ debugLog("execute_app_function rejected unknown function", failure);
908
+ return failure;
902
909
  }
903
- if (!options.executeBackendFunction) {
904
- return {
910
+ if (!input.executeBackendFunction) {
911
+ const failure = {
905
912
  ok: false,
906
913
  function_name: backendDefinition.name,
907
914
  error: "Backend function execution is not configured."
908
915
  };
916
+ debugLog("backend function execution unavailable", failure);
917
+ return failure;
909
918
  }
910
919
  try {
911
- const result = await options.executeBackendFunction({
920
+ debugLog("executing backend function", {
921
+ functionName: backendDefinition.name,
922
+ source: backendDefinition.source ?? "backend"
923
+ });
924
+ const result = await input.executeBackendFunction({
912
925
  functionName: backendDefinition.name,
913
926
  payload: payload ?? null
914
927
  });
915
- return {
928
+ const response = {
916
929
  ok: true,
917
930
  function_name: backendDefinition.name,
918
931
  source: backendDefinition.source ?? "backend",
919
932
  result
920
933
  };
934
+ debugLog("backend function completed", response);
935
+ return response;
921
936
  } catch (error) {
922
- return {
937
+ const failure = {
923
938
  ok: false,
924
939
  function_name: backendDefinition.name,
925
940
  error: "Function execution failed.",
926
941
  details: toErrorMessage2(error)
927
942
  };
943
+ debugLog("backend function failed", failure);
944
+ return failure;
928
945
  }
929
946
  };
930
- const navigateTool = (0, import_realtime.tool)({
931
- name: "navigate_to",
932
- description: "Navigate to an allowed route in the current app.",
933
- parameters: import_zod.z.object({
934
- target: import_zod.z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
935
- }),
936
- execute: async ({ target }) => {
937
- const path = resolveNavaiRoute(target, options.routes);
938
- if (!path) {
939
- return { ok: false, error: "Unknown or disallowed route." };
940
- }
941
- options.navigate(path);
942
- return { ok: true, path };
947
+ return {
948
+ availableFunctionNames,
949
+ executeAppFunction
950
+ };
951
+ }
952
+ function createFunctionTools(input) {
953
+ const aliasWarnings = [];
954
+ const availableFunctionNames = [
955
+ ...input.functionsRegistry.ordered.map((item) => item.name),
956
+ ...input.backendFunctions.map((item) => item.name)
957
+ ];
958
+ const directFunctionToolNames = input.includeDirectAliases === false ? [] : [...new Set(availableFunctionNames)].map((name) => name.trim().toLowerCase()).filter((name) => {
959
+ if (!name) {
960
+ return false;
961
+ }
962
+ if (RESERVED_TOOL_NAMES.has(name)) {
963
+ aliasWarnings.push(
964
+ `[navai] Function "${name}" is available only via execute_app_function because its name conflicts with a built-in tool.`
965
+ );
966
+ return false;
967
+ }
968
+ if (!TOOL_NAME_REGEXP.test(name)) {
969
+ aliasWarnings.push(
970
+ `[navai] Function "${name}" is available only via execute_app_function because its name is not a valid tool id.`
971
+ );
972
+ return false;
943
973
  }
974
+ return true;
944
975
  });
945
976
  const executeFunctionTool = (0, import_realtime.tool)({
946
977
  name: "execute_app_function",
@@ -951,36 +982,146 @@ async function buildNavaiAgent(options) {
951
982
  "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."
952
983
  )
953
984
  }),
954
- execute: async ({ function_name, payload }) => await executeAppFunction(function_name, payload)
985
+ execute: async ({ function_name, payload }) => await input.executeAppFunction(function_name, payload)
955
986
  });
956
987
  const directFunctionTools = directFunctionToolNames.map(
957
988
  (functionName) => (0, import_realtime.tool)({
958
989
  name: functionName,
959
990
  description: `Direct alias for execute_app_function("${functionName}").`,
960
991
  parameters: import_zod.z.object({
961
- payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).nullable().optional().describe(
992
+ payload: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).nullable().describe(
962
993
  "Payload object. Optional. Use payload.args as array for function args, payload.constructorArgs for class constructors, payload.methodArgs for class methods."
963
994
  )
964
995
  }),
965
- execute: async ({ payload }) => await executeAppFunction(functionName, payload ?? null)
996
+ execute: async ({ payload }) => await input.executeAppFunction(functionName, payload ?? null)
966
997
  })
967
998
  );
968
- const routeLines = getNavaiRoutePromptLines(options.routes);
969
- const functionLines = functionsRegistry.ordered.length + backendFunctionsOrdered.length > 0 ? [
999
+ return {
1000
+ aliasWarnings,
1001
+ availableFunctionNames,
1002
+ executeFunctionTool,
1003
+ directFunctionTools
1004
+ };
1005
+ }
1006
+ function buildFunctionLines(functionsRegistry, backendFunctions) {
1007
+ return functionsRegistry.ordered.length + backendFunctions.length > 0 ? [
970
1008
  ...functionsRegistry.ordered.map((item) => `- ${item.name}: ${item.description}`),
971
- ...backendFunctionsOrdered.map(
972
- (item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`
973
- )
1009
+ ...backendFunctions.map((item) => `- ${item.name}: ${item.description ?? "Execute backend function."}`)
974
1010
  ] : ["- none"];
1011
+ }
1012
+ async function buildNavaiAgent(options) {
1013
+ const aggregatedWarnings = [];
1014
+ const configuredAgents = (options.agents ?? []).filter(
1015
+ (agent2) => Object.keys(agent2.functionModuleLoaders ?? {}).length > 0
1016
+ );
1017
+ const primaryAgentConfig = configuredAgents.find((agent2) => agent2.key === options.primaryAgentKey) ?? configuredAgents.find((agent2) => agent2.isPrimary) ?? configuredAgents[0];
1018
+ const primaryFunctionLoaders = primaryAgentConfig?.functionModuleLoaders ?? options.functionModuleLoaders ?? {};
1019
+ const functionsRegistry = await loadNavaiFunctions(primaryFunctionLoaders);
1020
+ const backendFunctionsOrdered = normalizeBackendFunctions(options.backendFunctions, functionsRegistry, aggregatedWarnings);
1021
+ const primaryExecutionSurface = createExecuteAppFunction({
1022
+ functionsRegistry,
1023
+ backendFunctions: backendFunctionsOrdered,
1024
+ executeBackendFunction: options.executeBackendFunction,
1025
+ context: options
1026
+ });
1027
+ const primaryFunctionTools = createFunctionTools({
1028
+ functionsRegistry,
1029
+ backendFunctions: backendFunctionsOrdered,
1030
+ executeAppFunction: primaryExecutionSurface.executeAppFunction
1031
+ });
1032
+ aggregatedWarnings.push(...functionsRegistry.warnings, ...primaryFunctionTools.aliasWarnings);
1033
+ const navigateTool = (0, import_realtime.tool)({
1034
+ name: "navigate_to",
1035
+ description: "Navigate to an allowed route in the current app.",
1036
+ parameters: import_zod.z.object({
1037
+ target: import_zod.z.string().min(1).describe("Route name or route path. Example: perfil, ajustes, /profile, /settings")
1038
+ }),
1039
+ execute: async ({ target }) => {
1040
+ debugLog("navigate_to called", { target });
1041
+ const path = resolveNavaiRoute(target, options.routes);
1042
+ if (!path) {
1043
+ const failure = { ok: false, error: "Unknown or disallowed route." };
1044
+ debugLog("navigate_to rejected", failure);
1045
+ return failure;
1046
+ }
1047
+ options.navigate(path);
1048
+ const response = { ok: true, path };
1049
+ debugLog("navigate_to completed", response);
1050
+ return response;
1051
+ }
1052
+ });
1053
+ const routeLines = getNavaiRoutePromptLines(options.routes);
1054
+ const functionLines = buildFunctionLines(functionsRegistry, backendFunctionsOrdered);
1055
+ const specialistAgents = [];
1056
+ const specialistLines = [];
1057
+ for (const runtimeAgent of configuredAgents) {
1058
+ if (primaryAgentConfig && runtimeAgent.key === primaryAgentConfig.key) {
1059
+ continue;
1060
+ }
1061
+ const specialistRegistry = await loadNavaiFunctions(runtimeAgent.functionModuleLoaders);
1062
+ const specialistWarnings = [...specialistRegistry.warnings];
1063
+ const specialistBackendFunctions = normalizeBackendFunctions(
1064
+ options.backendFunctions,
1065
+ specialistRegistry,
1066
+ specialistWarnings
1067
+ );
1068
+ const specialistExecutionSurface = createExecuteAppFunction({
1069
+ functionsRegistry: specialistRegistry,
1070
+ backendFunctions: specialistBackendFunctions,
1071
+ executeBackendFunction: options.executeBackendFunction,
1072
+ context: options
1073
+ });
1074
+ const specialistFunctionTools = createFunctionTools({
1075
+ functionsRegistry: specialistRegistry,
1076
+ backendFunctions: specialistBackendFunctions,
1077
+ executeAppFunction: specialistExecutionSurface.executeAppFunction,
1078
+ includeDirectAliases: false
1079
+ });
1080
+ specialistWarnings.push(...specialistFunctionTools.aliasWarnings);
1081
+ aggregatedWarnings.push(...specialistWarnings);
1082
+ const specialistInstructions = [
1083
+ runtimeAgent.instructions ?? `You are the ${runtimeAgent.name} specialist agent for this web app.`,
1084
+ "Allowed app functions:",
1085
+ ...buildFunctionLines(specialistRegistry, specialistBackendFunctions),
1086
+ "Rules:",
1087
+ "- Always use execute_app_function for app actions.",
1088
+ "- When no arguments are needed, call execute_app_function with payload set to null.",
1089
+ "- Use only the functions available to this specialist agent.",
1090
+ "- Do not navigate unless one of your allowed functions explicitly does so.",
1091
+ "- Return a concise result to the main NAVAI agent."
1092
+ ].join("\n");
1093
+ debugLog("creating specialist agent", {
1094
+ key: runtimeAgent.key,
1095
+ name: runtimeAgent.name,
1096
+ functions: [
1097
+ ...specialistRegistry.ordered.map((item) => item.name),
1098
+ ...specialistBackendFunctions.map((item) => item.name)
1099
+ ]
1100
+ });
1101
+ const specialistAgent = new import_realtime.RealtimeAgent({
1102
+ name: runtimeAgent.name,
1103
+ handoffDescription: runtimeAgent.handoffDescription ?? runtimeAgent.description ?? `Delegate specialist work to ${runtimeAgent.name}.`,
1104
+ instructions: specialistInstructions,
1105
+ tools: [specialistFunctionTools.executeFunctionTool]
1106
+ });
1107
+ specialistAgents.push(specialistAgent);
1108
+ specialistLines.push(
1109
+ `- ${runtimeAgent.name}: ${runtimeAgent.description ?? runtimeAgent.handoffDescription ?? "Specialist agent available by delegation."}`
1110
+ );
1111
+ }
975
1112
  const instructions = [
976
- options.baseInstructions ?? "You are a voice assistant embedded in a web app.",
1113
+ primaryAgentConfig?.instructions ?? options.baseInstructions ?? "You are the main NAVAI voice agent embedded in a web app.",
977
1114
  "Allowed routes:",
978
1115
  ...routeLines,
979
1116
  "Allowed app functions:",
980
1117
  ...functionLines,
1118
+ "Available specialist agents:",
1119
+ ...specialistLines.length > 0 ? specialistLines : ["- none"],
981
1120
  "Rules:",
982
1121
  "- If user asks to go/open a section, always call navigate_to.",
983
- "- If user asks to run an internal action, call execute_app_function or the matching direct function tool.",
1122
+ "- If user asks to run an internal action that belongs to you, call execute_app_function or the matching direct function tool.",
1123
+ "- If the task clearly belongs to a specialist agent, hand off to that specialist agent.",
1124
+ "- Food recommendations, fast food, hamburgers, pizza, tacos, snacks, and meal suggestions belong to the food specialist.",
984
1125
  "- Always include payload in execute_app_function. Use null when no arguments are needed.",
985
1126
  "- For execute_app_function, pass arguments using payload.args (array).",
986
1127
  "- For class methods, pass payload.constructorArgs and payload.methodArgs.",
@@ -988,11 +1129,23 @@ async function buildNavaiAgent(options) {
988
1129
  "- If destination/action is unclear, ask a brief clarifying question."
989
1130
  ].join("\n");
990
1131
  const agent = new import_realtime.RealtimeAgent({
991
- name: options.agentName ?? "Navai Voice Agent",
1132
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
992
1133
  instructions,
993
- tools: [navigateTool, executeFunctionTool, ...directFunctionTools]
1134
+ handoffs: specialistAgents,
1135
+ tools: [
1136
+ navigateTool,
1137
+ primaryFunctionTools.executeFunctionTool,
1138
+ ...primaryFunctionTools.directFunctionTools
1139
+ ]
1140
+ });
1141
+ debugLog("created primary agent", {
1142
+ name: primaryAgentConfig?.name ?? options.agentName ?? "Navai Voice Agent",
1143
+ primaryAgentKey: primaryAgentConfig?.key ?? null,
1144
+ directFunctions: functionsRegistry.ordered.map((item) => item.name),
1145
+ backendFunctions: backendFunctionsOrdered.map((item) => item.name),
1146
+ specialistDelegates: configuredAgents.filter((runtimeAgent) => runtimeAgent.key !== primaryAgentConfig?.key).map((runtimeAgent) => runtimeAgent.key)
994
1147
  });
995
- return { agent, warnings: [...functionsRegistry.warnings, ...backendWarnings, ...aliasWarnings] };
1148
+ return { agent, warnings: aggregatedWarnings };
996
1149
  }
997
1150
 
998
1151
  // src/backend.ts
@@ -1115,6 +1268,7 @@ function createNavaiBackendClient(options = {}) {
1115
1268
  // src/runtime.ts
1116
1269
  var ROUTES_ENV_KEYS = ["NAVAI_ROUTES_FILE"];
1117
1270
  var FUNCTIONS_ENV_KEYS = ["NAVAI_FUNCTIONS_FOLDERS"];
1271
+ var AGENTS_ENV_KEYS = ["NAVAI_AGENTS_FOLDERS"];
1118
1272
  var MODEL_ENV_KEYS = ["NAVAI_REALTIME_MODEL"];
1119
1273
  async function resolveNavaiFrontendRuntimeConfig(options) {
1120
1274
  const warnings = [];
@@ -1124,6 +1278,7 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
1124
1278
  const defaultFunctionsFolder = options.defaultFunctionsFolder ?? "src/ai/functions-modules";
1125
1279
  const routesFile = readOptional2(options.routesFile) ?? readFirstOptionalEnv(options.env, ROUTES_ENV_KEYS) ?? defaultRoutesFile;
1126
1280
  const functionsFolders = readOptional2(options.functionsFolders) ?? readFirstOptionalEnv(options.env, FUNCTIONS_ENV_KEYS) ?? defaultFunctionsFolder;
1281
+ const agentsFolders = readOptional2(options.agentsFolders) ?? readFirstOptionalEnv(options.env, AGENTS_ENV_KEYS);
1127
1282
  const modelOverride = readOptional2(options.modelOverride) ?? readFirstOptionalEnv(options.env, MODEL_ENV_KEYS);
1128
1283
  const routes = await resolveRoutes({
1129
1284
  routesFile,
@@ -1135,12 +1290,23 @@ async function resolveNavaiFrontendRuntimeConfig(options) {
1135
1290
  const functionModuleLoaders = resolveFunctionModuleLoaders({
1136
1291
  indexedLoaders,
1137
1292
  functionsFolders,
1293
+ agentsFolders,
1138
1294
  defaultFunctionsFolder,
1139
1295
  warnings
1140
1296
  });
1297
+ const agents = await resolveRuntimeAgents({
1298
+ indexedLoaders,
1299
+ functionModuleLoaders,
1300
+ functionsFolders,
1301
+ agentsFolders,
1302
+ defaultFunctionsFolder
1303
+ });
1304
+ const primaryAgentKey = agents.find((agent) => agent.isPrimary)?.key;
1141
1305
  return {
1142
1306
  routes,
1143
1307
  functionModuleLoaders,
1308
+ agents,
1309
+ primaryAgentKey,
1144
1310
  modelOverride,
1145
1311
  warnings
1146
1312
  };
@@ -1176,10 +1342,11 @@ async function resolveRoutes(input) {
1176
1342
  }
1177
1343
  function resolveFunctionModuleLoaders(input) {
1178
1344
  const configuredTokens = input.functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
1345
+ const agentFolders = parseCsvList(input.agentsFolders);
1179
1346
  const tokens = configuredTokens.length > 0 ? configuredTokens : [input.defaultFunctionsFolder];
1180
- const matchers = tokens.map((token) => createPathMatcher(token));
1347
+ const matchers = tokens.map((token) => createPathMatcher(token, agentFolders));
1181
1348
  const matchedEntries = input.indexedLoaders.filter(
1182
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && matchers.some((matcher) => matcher(entry.normalizedPath))
1349
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && matchers.some((matcher) => matcher(entry.normalizedPath))
1183
1350
  );
1184
1351
  if (matchedEntries.length > 0) {
1185
1352
  return Object.fromEntries(matchedEntries.map((entry) => [entry.rawPath, entry.load]));
@@ -1189,9 +1356,9 @@ function resolveFunctionModuleLoaders(input) {
1189
1356
  `[navai] NAVAI_FUNCTIONS_FOLDERS did not match any module: "${input.functionsFolders}". Falling back to "${input.defaultFunctionsFolder}".`
1190
1357
  );
1191
1358
  }
1192
- const fallbackMatcher = createPathMatcher(input.defaultFunctionsFolder);
1359
+ const fallbackMatcherWithAgents = createPathMatcher(input.defaultFunctionsFolder, agentFolders);
1193
1360
  const fallbackEntries = input.indexedLoaders.filter(
1194
- (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && fallbackMatcher(entry.normalizedPath)
1361
+ (entry) => !entry.normalizedPath.endsWith(".d.ts") && !entry.normalizedPath.startsWith("src/node_modules/") && !isAgentConfigPath(entry.normalizedPath) && fallbackMatcherWithAgents(entry.normalizedPath)
1195
1362
  );
1196
1363
  return Object.fromEntries(fallbackEntries.map((entry) => [entry.rawPath, entry.load]));
1197
1364
  }
@@ -1234,7 +1401,7 @@ function buildModuleCandidates(inputPath) {
1234
1401
  }
1235
1402
  return [srcPrefixed, `${srcPrefixed}.ts`, `${srcPrefixed}.js`, `${srcPrefixed}/index.ts`, `${srcPrefixed}/index.js`];
1236
1403
  }
1237
- function createPathMatcher(input) {
1404
+ function createPathMatcher(input, agentFolders = []) {
1238
1405
  const raw = normalizePath(input);
1239
1406
  if (!raw) {
1240
1407
  return () => false;
@@ -1252,8 +1419,141 @@ function createPathMatcher(input) {
1252
1419
  return (path) => path === normalized;
1253
1420
  }
1254
1421
  const base = normalized.replace(/\/+$/, "");
1422
+ const normalizedAgents = agentFolders.map(normalizePathSegment).filter(Boolean);
1423
+ if (normalizedAgents.length > 0) {
1424
+ return (path) => {
1425
+ if (!path.startsWith(`${base}/`)) {
1426
+ return false;
1427
+ }
1428
+ const suffix = path.slice(base.length + 1);
1429
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
1430
+ return normalizedAgents.includes(firstSegment);
1431
+ };
1432
+ }
1255
1433
  return (path) => path === base || path.startsWith(`${base}/`);
1256
1434
  }
1435
+ async function resolveRuntimeAgents(input) {
1436
+ const configuredAgents = parseCsvList(input.agentsFolders);
1437
+ if (configuredAgents.length === 0) {
1438
+ return [];
1439
+ }
1440
+ const loaderByPath = new Map(input.indexedLoaders.map((entry) => [entry.normalizedPath, entry]));
1441
+ const baseDirectories = resolveAgentBaseDirectories(input.functionsFolders, input.defaultFunctionsFolder);
1442
+ const groupedLoaders = /* @__PURE__ */ new Map();
1443
+ for (const [rawPath, load] of Object.entries(input.functionModuleLoaders)) {
1444
+ const agentKey = extractAgentKeyFromPath(rawPath, baseDirectories, configuredAgents);
1445
+ if (!agentKey) {
1446
+ continue;
1447
+ }
1448
+ const current = groupedLoaders.get(agentKey) ?? {};
1449
+ current[rawPath] = load;
1450
+ groupedLoaders.set(agentKey, current);
1451
+ }
1452
+ const configuredPrimaryKey = configuredAgents[0];
1453
+ const agents = [];
1454
+ for (const agentKey of configuredAgents) {
1455
+ const functionLoaders = groupedLoaders.get(agentKey);
1456
+ if (!functionLoaders || Object.keys(functionLoaders).length === 0) {
1457
+ continue;
1458
+ }
1459
+ const config = await loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath);
1460
+ agents.push({
1461
+ key: config.key?.trim() || agentKey,
1462
+ name: readOptional2(config.name) ?? humanizeAgentKey(agentKey),
1463
+ description: readOptional2(config.description),
1464
+ handoffDescription: readOptional2(config.handoffDescription) ?? readOptional2(config.description),
1465
+ instructions: readOptional2(config.instructions),
1466
+ isPrimary: config.isPrimary === true || agentKey === configuredPrimaryKey,
1467
+ functionModuleLoaders: functionLoaders
1468
+ });
1469
+ }
1470
+ if (agents.filter((agent) => agent.isPrimary).length === 0 && agents[0]) {
1471
+ agents[0].isPrimary = true;
1472
+ }
1473
+ if (agents.filter((agent) => agent.isPrimary).length > 1) {
1474
+ let primaryAssigned = false;
1475
+ for (const agent of agents) {
1476
+ if (agent.isPrimary && !primaryAssigned) {
1477
+ primaryAssigned = true;
1478
+ continue;
1479
+ }
1480
+ agent.isPrimary = false;
1481
+ }
1482
+ }
1483
+ return agents;
1484
+ }
1485
+ async function loadAgentModuleConfig(agentKey, baseDirectories, loaderByPath) {
1486
+ for (const baseDirectory of baseDirectories) {
1487
+ const configBase = `${baseDirectory}/${agentKey}/agent.config`;
1488
+ const matchedLoader = buildModuleCandidates(configBase).map((candidate) => loaderByPath.get(candidate)).find(Boolean);
1489
+ if (!matchedLoader) {
1490
+ continue;
1491
+ }
1492
+ try {
1493
+ const imported = await matchedLoader.load();
1494
+ return readAgentModuleConfig(imported);
1495
+ } catch {
1496
+ return {};
1497
+ }
1498
+ }
1499
+ return {};
1500
+ }
1501
+ function readAgentModuleConfig(moduleShape) {
1502
+ const candidate = readRecord(moduleShape.NAVAI_AGENT) ?? readRecord(moduleShape.agent) ?? readRecord(moduleShape.default) ?? {};
1503
+ return {
1504
+ key: readOptionalString(candidate.key),
1505
+ name: readOptionalString(candidate.name),
1506
+ description: readOptionalString(candidate.description),
1507
+ handoffDescription: readOptionalString(candidate.handoffDescription),
1508
+ instructions: readOptionalString(candidate.instructions),
1509
+ isPrimary: candidate.isPrimary === true
1510
+ };
1511
+ }
1512
+ function readRecord(value) {
1513
+ return value && typeof value === "object" ? value : null;
1514
+ }
1515
+ function readOptionalString(value) {
1516
+ return typeof value === "string" ? readOptional2(value) : void 0;
1517
+ }
1518
+ function resolveAgentBaseDirectories(functionsFolders, defaultFunctionsFolder) {
1519
+ const configuredTokens = functionsFolders.split(",").map((value) => value.trim()).filter(Boolean);
1520
+ const tokens = configuredTokens.length > 0 ? configuredTokens : [defaultFunctionsFolder];
1521
+ return [...new Set(tokens.map(toAgentBaseDirectory).filter(Boolean))];
1522
+ }
1523
+ function toAgentBaseDirectory(input) {
1524
+ const raw = normalizePath(input);
1525
+ if (!raw) {
1526
+ return null;
1527
+ }
1528
+ const normalized = raw.startsWith("src/") ? raw : `src/${raw}`;
1529
+ if (normalized.includes("*") || /\.[cm]?[jt]s$/.test(normalized)) {
1530
+ return null;
1531
+ }
1532
+ if (normalized.endsWith("/...")) {
1533
+ return normalized.slice(0, -4).replace(/\/+$/, "") || null;
1534
+ }
1535
+ return normalized.replace(/\/+$/, "") || null;
1536
+ }
1537
+ function extractAgentKeyFromPath(pathValue, baseDirectories, configuredAgents) {
1538
+ const normalized = normalizePath(pathValue);
1539
+ for (const baseDirectory of baseDirectories) {
1540
+ if (!normalized.startsWith(`${baseDirectory}/`)) {
1541
+ continue;
1542
+ }
1543
+ const suffix = normalized.slice(baseDirectory.length + 1);
1544
+ const firstSegment = suffix.split("/", 1)[0] ?? "";
1545
+ if (configuredAgents.includes(firstSegment)) {
1546
+ return firstSegment;
1547
+ }
1548
+ }
1549
+ return void 0;
1550
+ }
1551
+ function humanizeAgentKey(value) {
1552
+ return value.split(/[_-]+/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
1553
+ }
1554
+ function isAgentConfigPath(pathValue) {
1555
+ return /\/agent\.config\.[cm]?[jt]s$/i.test(pathValue);
1556
+ }
1257
1557
  function globToRegExp(pattern) {
1258
1558
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1259
1559
  const wildcardSafe = escaped.replace(/\*\*/g, "___DOUBLE_STAR___");
@@ -1264,6 +1564,12 @@ function globToRegExp(pattern) {
1264
1564
  function normalizePath(input) {
1265
1565
  return input.trim().replace(/\\/g, "/").replace(/^\/+/, "").replace(/^(\.\/)+/, "").replace(/^(\.\.\/)+/, "");
1266
1566
  }
1567
+ function normalizePathSegment(input) {
1568
+ return normalizePath(input).replace(/\//g, "");
1569
+ }
1570
+ function parseCsvList(input) {
1571
+ return (input ?? "").split(",").map((value) => normalizePathSegment(value)).filter(Boolean);
1572
+ }
1267
1573
  function readFirstOptionalEnv(env, keys) {
1268
1574
  if (!env) {
1269
1575
  return void 0;
@@ -1294,6 +1600,7 @@ function toErrorMessage3(error) {
1294
1600
  // src/useWebVoiceAgent.ts
1295
1601
  var import_realtime2 = require("@openai/agents/realtime");
1296
1602
  var import_react = require("react");
1603
+ var DEBUG_PREFIX2 = "[navai debug]";
1297
1604
  function formatError(error) {
1298
1605
  if (error instanceof Error) {
1299
1606
  return error.message;
@@ -1307,6 +1614,13 @@ function emitWarnings(warnings) {
1307
1614
  }
1308
1615
  }
1309
1616
  }
1617
+ function debugLog2(message, details) {
1618
+ if (details === void 0) {
1619
+ console.log(`${DEBUG_PREFIX2} ${message}`);
1620
+ return;
1621
+ }
1622
+ console.log(`${DEBUG_PREFIX2} ${message}`, details);
1623
+ }
1310
1624
  function useWebVoiceAgent(options) {
1311
1625
  const sessionRef = (0, import_react.useRef)(null);
1312
1626
  const attachedRealtimeSessionRef = (0, import_react.useRef)(null);
@@ -1317,6 +1631,7 @@ function useWebVoiceAgent(options) {
1317
1631
  env: options.env,
1318
1632
  routesFile: options.routesFile,
1319
1633
  functionsFolders: options.functionsFolders,
1634
+ agentsFolders: options.agentsFolders,
1320
1635
  modelOverride: options.modelOverride,
1321
1636
  defaultRoutesFile: options.defaultRoutesFile,
1322
1637
  defaultFunctionsFolder: options.defaultFunctionsFolder
@@ -1325,6 +1640,7 @@ function useWebVoiceAgent(options) {
1325
1640
  options.defaultFunctionsFolder,
1326
1641
  options.defaultRoutes,
1327
1642
  options.defaultRoutesFile,
1643
+ options.agentsFolders,
1328
1644
  options.env,
1329
1645
  options.functionsFolders,
1330
1646
  options.modelOverride,
@@ -1371,6 +1687,45 @@ function useWebVoiceAgent(options) {
1371
1687
  const attachSessionAudioListeners = (0, import_react.useCallback)(
1372
1688
  (session) => {
1373
1689
  detachSessionAudioListeners();
1690
+ session.on("agent_start", (_context, agent, turnInput) => {
1691
+ debugLog2("session agent_start", {
1692
+ agent: agent.name,
1693
+ turnInputCount: Array.isArray(turnInput) ? turnInput.length : 0
1694
+ });
1695
+ });
1696
+ session.on("agent_end", (_context, agent, output) => {
1697
+ debugLog2("session agent_end", {
1698
+ agent: agent.name,
1699
+ output
1700
+ });
1701
+ });
1702
+ session.on("agent_handoff", (_context, fromAgent, toAgent) => {
1703
+ debugLog2("session agent_handoff", {
1704
+ from: fromAgent.name,
1705
+ to: toAgent.name
1706
+ });
1707
+ });
1708
+ session.on("agent_tool_start", (_context, agent, tool2, details) => {
1709
+ debugLog2("session agent_tool_start", {
1710
+ agent: agent.name,
1711
+ tool: tool2.name,
1712
+ toolCall: details.toolCall
1713
+ });
1714
+ });
1715
+ session.on("agent_tool_end", (_context, agent, tool2, result, details) => {
1716
+ debugLog2("session agent_tool_end", {
1717
+ agent: agent.name,
1718
+ tool: tool2.name,
1719
+ result,
1720
+ toolCall: details.toolCall
1721
+ });
1722
+ });
1723
+ session.on("history_added", (item) => {
1724
+ debugLog2("session history_added", item);
1725
+ });
1726
+ session.on("error", (sessionError) => {
1727
+ debugLog2("session error", sessionError);
1728
+ });
1374
1729
  session.on("audio_start", handleSessionAudioStart);
1375
1730
  session.on("audio_stopped", handleSessionAudioStopped);
1376
1731
  session.on("audio_interrupted", handleSessionAudioInterrupted);
@@ -1409,6 +1764,17 @@ function useWebVoiceAgent(options) {
1409
1764
  setAgentVoiceStateIfChanged("idle");
1410
1765
  try {
1411
1766
  const runtimeConfig = await runtimeConfigPromise;
1767
+ debugLog2("resolved runtime config", {
1768
+ routes: runtimeConfig.routes.map((route) => route.path),
1769
+ functionModules: Object.keys(runtimeConfig.functionModuleLoaders),
1770
+ agents: runtimeConfig.agents.map((agent2) => ({
1771
+ key: agent2.key,
1772
+ name: agent2.name,
1773
+ isPrimary: agent2.isPrimary,
1774
+ functionModules: Object.keys(agent2.functionModuleLoaders)
1775
+ })),
1776
+ warnings: runtimeConfig.warnings
1777
+ });
1412
1778
  const requestPayload = runtimeConfig.modelOverride ? { model: runtimeConfig.modelOverride } : {};
1413
1779
  const secretPayload = await backendClient.createClientSecret(requestPayload);
1414
1780
  const backendFunctionsResult = await backendClient.listFunctions();
@@ -1416,6 +1782,8 @@ function useWebVoiceAgent(options) {
1416
1782
  navigate: options.navigate,
1417
1783
  routes: runtimeConfig.routes,
1418
1784
  functionModuleLoaders: runtimeConfig.functionModuleLoaders,
1785
+ agents: runtimeConfig.agents,
1786
+ primaryAgentKey: runtimeConfig.primaryAgentKey,
1419
1787
  backendFunctions: backendFunctionsResult.functions,
1420
1788
  executeBackendFunction: backendClient.executeFunction
1421
1789
  });
@@ -1431,6 +1799,7 @@ function useWebVoiceAgent(options) {
1431
1799
  setStatus("connected");
1432
1800
  } catch (startError) {
1433
1801
  const message = formatError(startError);
1802
+ debugLog2("session start failed", { message, error: startError });
1434
1803
  setError(message);
1435
1804
  setStatus("error");
1436
1805
  setAgentVoiceStateIfChanged("idle");