@memberjunction/ai-agents 6.1.0-edge.6 → 6.1.0

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.
Files changed (67) hide show
  1. package/dist/SkillImportExportService.d.ts +17 -1
  2. package/dist/SkillImportExportService.d.ts.map +1 -1
  3. package/dist/SkillImportExportService.js +57 -5
  4. package/dist/SkillImportExportService.js.map +1 -1
  5. package/dist/SkillMarkdownConverter.d.ts +17 -1
  6. package/dist/SkillMarkdownConverter.d.ts.map +1 -1
  7. package/dist/SkillMarkdownConverter.js +40 -5
  8. package/dist/SkillMarkdownConverter.js.map +1 -1
  9. package/dist/agent-types/base-agent-type.d.ts +26 -1
  10. package/dist/agent-types/base-agent-type.d.ts.map +1 -1
  11. package/dist/agent-types/base-agent-type.js +17 -0
  12. package/dist/agent-types/base-agent-type.js.map +1 -1
  13. package/dist/agent-types/loop-agent-response-type.d.ts +16 -0
  14. package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
  15. package/dist/agent-types/loop-agent-response-type.js +19 -1
  16. package/dist/agent-types/loop-agent-response-type.js.map +1 -1
  17. package/dist/agent-types/loop-agent-type.d.ts +32 -1
  18. package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
  19. package/dist/agent-types/loop-agent-type.js +190 -3
  20. package/dist/agent-types/loop-agent-type.js.map +1 -1
  21. package/dist/base-agent.d.ts +186 -3
  22. package/dist/base-agent.d.ts.map +1 -1
  23. package/dist/base-agent.js +564 -53
  24. package/dist/base-agent.js.map +1 -1
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/native-tools/action-tool-builder.d.ts +92 -0
  30. package/dist/native-tools/action-tool-builder.d.ts.map +1 -0
  31. package/dist/native-tools/action-tool-builder.js +226 -0
  32. package/dist/native-tools/action-tool-builder.js.map +1 -0
  33. package/dist/native-tools/control-tools.d.ts +52 -0
  34. package/dist/native-tools/control-tools.d.ts.map +1 -0
  35. package/dist/native-tools/control-tools.js +113 -0
  36. package/dist/native-tools/control-tools.js.map +1 -0
  37. package/dist/native-tools/dual-channel.d.ts +16 -0
  38. package/dist/native-tools/dual-channel.d.ts.map +1 -0
  39. package/dist/native-tools/dual-channel.js +44 -0
  40. package/dist/native-tools/dual-channel.js.map +1 -0
  41. package/dist/native-tools/tool-result-turns.d.ts +32 -0
  42. package/dist/native-tools/tool-result-turns.d.ts.map +1 -0
  43. package/dist/native-tools/tool-result-turns.js +29 -0
  44. package/dist/native-tools/tool-result-turns.js.map +1 -0
  45. package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
  46. package/dist/realtime/bridge-realtime-session-factory.js +22 -2
  47. package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
  48. package/dist/realtime/realtime-client-session-service.d.ts +104 -17
  49. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
  50. package/dist/realtime/realtime-client-session-service.js +325 -28
  51. package/dist/realtime/realtime-client-session-service.js.map +1 -1
  52. package/dist/realtime/realtime-coagent-config.d.ts +61 -1
  53. package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
  54. package/dist/realtime/realtime-coagent-config.js +93 -1
  55. package/dist/realtime/realtime-coagent-config.js.map +1 -1
  56. package/dist/realtime/realtime-session-runner.d.ts +10 -8
  57. package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
  58. package/dist/realtime/realtime-session-runner.js +11 -9
  59. package/dist/realtime/realtime-session-runner.js.map +1 -1
  60. package/dist/realtime/realtime-tool-broker.d.ts +11 -5
  61. package/dist/realtime/realtime-tool-broker.d.ts.map +1 -1
  62. package/dist/realtime/realtime-tool-broker.js +19 -7
  63. package/dist/realtime/realtime-tool-broker.js.map +1 -1
  64. package/dist/scoped-prompt-config-resolver.d.ts +6 -1
  65. package/dist/scoped-prompt-config-resolver.d.ts.map +1 -1
  66. package/dist/scoped-prompt-config-resolver.js.map +1 -1
  67. package/package.json +18 -18
@@ -9,14 +9,14 @@
9
9
  *
10
10
  * 1. {@link RealtimeClientSessionService.PrepareClientSession} — resolves the Realtime model,
11
11
  * assembles the companion system prompt (co-agent prompt + target identity + history + memory),
12
- * builds the stable, target-independent tool set (always including `invoke-target-agent`), and
12
+ * builds the realtime tool set (always including `invoke-target-agent`, plus allowed direct actions on dynamic-toolset drivers), and
13
13
  * asks the model to mint a {@link ClientRealtimeSessionConfig} (ephemeral token + provider
14
14
  * session config) the browser applies verbatim.
15
15
  * 2. {@link RealtimeClientSessionService.ExecuteRelayedTool} — executes a single tool call the
16
16
  * browser relayed, routing it through the shared {@link RealtimeToolBroker} so the result is
17
17
  * byte-for-byte identical to the server-bridged path. `invoke-target-agent` delegates to the
18
- * target agent via {@link AgentRunner.RunAgent}; every other tool returns a structured
19
- * "not available" result for now (action wiring is a later phase).
18
+ * target agent via {@link AgentRunner.RunAgent}; allowed direct actions execute via
19
+ * {@link ActionEngineServer.Instance.RunAction}; other tools return a structured "not available" result.
20
20
  *
21
21
  * **Why this duplicates BaseAgent.** The private helpers in `BaseAgent.executeRealtimeSession`
22
22
  * (model resolution, companion-prompt assembly, target-agent resolution, delegation) are the
@@ -31,14 +31,16 @@
31
31
  */
32
32
  import { LogError, LogStatus, RunView } from '@memberjunction/core';
33
33
  import { MJGlobal, MJLruCache, UUIDsEqual, EscapeSQLString } from '@memberjunction/global';
34
+ import { ActionEngineServer } from '@memberjunction/actions';
35
+ import { RunActionParams } from '@memberjunction/actions-base';
34
36
  import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
35
37
  import { FormatAppContextNote } from '@memberjunction/ai-core-plus';
36
38
  import { AIEngine } from '@memberjunction/aiengine';
37
39
  import { AgentMemoryContextBuilder } from '../agent-memory-context-builder.js';
38
40
  import { AgentRunner } from '../AgentRunner.js';
39
- import { RealtimeToolBroker, INVOKE_TARGET_AGENT_TOOL_NAME, BuildRealtimeAgentFraming } from './realtime-tool-broker.js';
41
+ import { RealtimeToolBroker, INVOKE_TARGET_AGENT_TOOL_NAME, INVOKE_TARGET_AGENT_DESCRIPTION, BuildRealtimeAgentFraming } from './realtime-tool-broker.js';
40
42
  import { NARRATION_PROMPT_NAME, LEGACY_NARRATION_PROMPT_NAME, ResolveNarrationInstructionsTemplate } from './realtime-narration.js';
41
- import { BuildVoiceMannerSection, BuildAppRealtimeOverridesJson, DeepMergeConfigs, GetModelCatalogSessionSettings, GetDisclosureForTarget, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, ResolveEffectiveRealtimeConfig, MatchProviderVoiceSettings } from './realtime-coagent-config.js';
43
+ import { BuildVoiceMannerSection, BuildAppRealtimeOverridesJson, DeepMergeConfigs, GetModelCatalogSessionSettings, GetDisclosureForTarget, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, ResolveEffectiveRealtimeConfig, MatchProviderVoiceSettings, GetDirectActionsConfig, IsActionAllowedForDirectInvocation } from './realtime-coagent-config.js';
42
44
  import { SelectRealtimeVendorForModel } from './realtime-vendor-resolution.js';
43
45
  /**
44
46
  * Logs when a configuration authored per-provider voice settings but NONE matched the vendor that
@@ -77,6 +79,32 @@ export function WarnOnUnmatchedProviderVoice(effectiveConfig, driverClass, surfa
77
79
  `driver '${driverClass ?? 'unknown'}' — none matched, so those settings were dropped. Author ` +
78
80
  'realtime.voice.default.voice to carry a voice to whichever vendor runs.');
79
81
  }
82
+ /**
83
+ * Sanitizes an action or tool name to conform to provider wire constraints
84
+ * (e.g. OpenAI function naming: ^[a-zA-Z0-9_-]{1,64}$).
85
+ *
86
+ * Replaces non-alphanumeric/hyphen/underscore characters with underscores,
87
+ * collapses consecutive runs of underscores into a single underscore, and
88
+ * truncates to 64 characters.
89
+ *
90
+ * @param name The original action or tool name.
91
+ * @returns The sanitized wire-safe name.
92
+ */
93
+ export function SanitizeWireToolName(name) {
94
+ return name
95
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
96
+ .replace(/_+/g, '_')
97
+ .slice(0, 64);
98
+ }
99
+ /**
100
+ * Sentinel error thrown when action tool arguments are present but cannot be parsed or are not a JSON object.
101
+ */
102
+ export class ToolArgumentsError extends Error {
103
+ constructor(message) {
104
+ super(message);
105
+ this.name = 'ToolArgumentsError';
106
+ }
107
+ }
80
108
  /**
81
109
  * Server-agnostic service that prepares a client-direct realtime session and executes the tool
82
110
  * calls the browser relays back. Constructed per-request (a normal injectable service — NOT a
@@ -87,6 +115,12 @@ export function WarnOnUnmatchedProviderVoice(effectiveConfig, driverClass, surfa
87
115
  */
88
116
  export class RealtimeClientSessionService {
89
117
  constructor() {
118
+ /** Maps session id -> (wireName -> MJActionEntityExtended) built during tool projection. */
119
+ this.sessionWireActionMaps = new Map();
120
+ /** Maps targetAgentID -> (wireName -> MJActionEntityExtended) fallback built during tool projection. */
121
+ this.targetWireActionMaps = new Map();
122
+ /** Maps session id -> direct actions config resolved during session prep. */
123
+ this.sessionDirectConfigs = new Map();
90
124
  /**
91
125
  * IN-FLIGHT DELEGATION REGISTRY — the server half of the client-direct CANCEL channel.
92
126
  *
@@ -125,6 +159,33 @@ export class RealtimeClientSessionService {
125
159
  ttlMs: 4 * 60 * 60 * 1000, // 4h backstop — generous vs. any realistic session duration
126
160
  });
127
161
  }
162
+ /**
163
+ * Builds a wire-name to action map from candidate actions, sanitizing each
164
+ * action's name and deduplicating collisions (first action wins).
165
+ *
166
+ * @param actions The action entities to index by wire name.
167
+ * @returns Map of wire-name to action entity.
168
+ */
169
+ buildWireActionMap(actions) {
170
+ const map = new Map();
171
+ const seen = new Map();
172
+ for (const action of actions) {
173
+ if (!action.Name) {
174
+ continue;
175
+ }
176
+ const wireName = SanitizeWireToolName(action.Name);
177
+ const lower = wireName.toLowerCase();
178
+ const existing = seen.get(lower);
179
+ if (existing) {
180
+ // Colliding action name after sanitization — first action wins, log collision
181
+ LogError(`RealtimeClientSessionService.buildWireActionMap: wire name collision for '${wireName}' between action '${existing.Name}' and action '${action.Name}'. Keeping '${existing.Name}' (first wins).`);
182
+ continue;
183
+ }
184
+ seen.set(lower, action);
185
+ map.set(wireName, action);
186
+ }
187
+ return map;
188
+ }
128
189
  /**
129
190
  * The seeded name of the `MJ: AI Prompts` row whose `TemplateText` carries the first-person
130
191
  * progress-narration instructions (with a `{{ progressMessage }}` placeholder). Resolved at
@@ -253,6 +314,7 @@ export class RealtimeClientSessionService {
253
314
  ParentRunID: obs?.CoAgentRunID,
254
315
  TargetAgentID: input.TargetAgentID,
255
316
  AllowedAgents: prep.EffectiveConfig?.realtime?.allowedAgents,
317
+ DirectActions: prep.EffectiveConfig?.realtime?.directActions,
256
318
  Call: call,
257
319
  }, contextUser, provider);
258
320
  await session.SendToolResult(call.CallID, result.ResultJson);
@@ -920,6 +982,7 @@ export class RealtimeClientSessionService {
920
982
  */
921
983
  async configureEngine(contextUser, provider) {
922
984
  await AIEngine.Instance.Config(false, contextUser, provider);
985
+ await ActionEngineServer.Instance.Config(false, contextUser);
923
986
  }
924
987
  /**
925
988
  * Resolves the co-agent from either the supplied entity or its id (from cached metadata).
@@ -1178,7 +1241,7 @@ export class RealtimeClientSessionService {
1178
1241
  }
1179
1242
  /**
1180
1243
  * Builds the {@link RealtimeSessionParams} for the client-direct session: the companion system
1181
- * prompt plus the stable, target-independent tool set.
1244
+ * prompt plus the registered realtime tool set.
1182
1245
  *
1183
1246
  * @param input The prepare-session input.
1184
1247
  * @param coAgent The resolved co-agent.
@@ -1192,9 +1255,13 @@ export class RealtimeClientSessionService {
1192
1255
  * @returns The assembled session params.
1193
1256
  */
1194
1257
  async buildSessionParams(input, coAgent, modelApiName, contextUser, provider, effectiveConfig, driverClass, modelID, modelVendorID) {
1195
- const systemPrompt = await this.buildCompanionSystemPrompt(input, coAgent, contextUser, provider, effectiveConfig);
1258
+ const directTools = this.buildDirectActionTools(input.TargetAgentID, effectiveConfig, driverClass, input.AgentSessionID);
1259
+ const systemPrompt = await this.buildCompanionSystemPrompt(input, coAgent, contextUser, provider, effectiveConfig, directTools.length > 0);
1196
1260
  const memoryContext = await this.assembleMemoryContext(input, coAgent, contextUser, provider);
1197
- const tools = this.buildStableToolSet(input.ExtraTools);
1261
+ const combinedExtra = directTools.length > 0
1262
+ ? [...(input.ExtraTools ?? []), ...directTools]
1263
+ : input.ExtraTools;
1264
+ const tools = this.buildStableToolSet(combinedExtra);
1198
1265
  // Hoisted (rather than built inline at the return) so the mint log below can report the voice
1199
1266
  // that ACTUALLY reached the driver — see the `voice=` field. Same bag, built once.
1200
1267
  const configBag = this.buildSessionConfigBag(input, effectiveConfig, driverClass, modelID, modelVendorID);
@@ -1218,6 +1285,7 @@ export class RealtimeClientSessionService {
1218
1285
  // bound the provider session/token apply min(default, this); the janitor enforces it
1219
1286
  // regardless of driver support via the session deadline stamped by the transport layer.
1220
1287
  MaxSessionSeconds: input.MaxSessionSeconds,
1288
+ UserID: contextUser?.ID,
1221
1289
  };
1222
1290
  }
1223
1291
  /**
@@ -1277,7 +1345,7 @@ export class RealtimeClientSessionService {
1277
1345
  * @param effectiveConfig The resolved effective configuration (voice persona source).
1278
1346
  * @returns The concatenated system prompt (never empty — the framing is always present).
1279
1347
  */
1280
- async buildCompanionSystemPrompt(input, coAgent, contextUser, provider, effectiveConfig) {
1348
+ async buildCompanionSystemPrompt(input, coAgent, contextUser, provider, effectiveConfig, hasDirectTools = false) {
1281
1349
  const target = this.resolveTargetAgent(input.TargetAgentID);
1282
1350
  const targetName = target?.Name ?? 'the configured target agent';
1283
1351
  // Identity framing comes from the ONE shared producer (see BuildRealtimeAgentFraming) so the agent
@@ -1285,7 +1353,7 @@ export class RealtimeClientSessionService {
1285
1353
  // browser/whiteboard); bridges pass none. Colleagues come from the effective allowed-agent union
1286
1354
  // (Move 4) so the lead knows who it can delegate to and how to narrate each handoff.
1287
1355
  const colleagues = this.buildColleaguesFromConfig(effectiveConfig, input.TargetAgentID);
1288
- const framing = BuildRealtimeAgentFraming(targetName, this.buildInteractiveSurfaceFraming(input.ExtraTools), colleagues);
1356
+ const framing = BuildRealtimeAgentFraming(targetName, this.buildInteractiveSurfaceFraming(input.ExtraTools), colleagues, hasDirectTools);
1289
1357
  const meetingFraming = this.buildMeetingFraming(input);
1290
1358
  const coAgentPrompt = this.getCoAgentSystemPromptText(coAgent);
1291
1359
  const voiceManner = BuildVoiceMannerSection(effectiveConfig);
@@ -1580,19 +1648,17 @@ export class RealtimeClientSessionService {
1580
1648
  }
1581
1649
  }
1582
1650
  /**
1583
- * Builds the stable, target-independent tool set every voice session exposes: the single
1584
- * `invoke-target-agent` tool plus any caller-supplied extra tools. The target is a runtime
1585
- * argument *inside* the call, never a per-target tool — this keeps the provider contract
1586
- * identical across targets.
1651
+ * Builds the tool set every voice session exposes: the core `invoke-target-agent` tool plus
1652
+ * any caller-supplied extra tools or projected direct actions. The target is a runtime
1653
+ * argument *inside* the delegation call, keeping the provider delegation contract identical.
1587
1654
  *
1588
- * @param extraTools Optional additional target-independent tools.
1655
+ * @param extraTools Optional additional tools to register.
1589
1656
  * @returns The tools to register at session start.
1590
1657
  */
1591
1658
  buildStableToolSet(extraTools) {
1592
1659
  const invokeTarget = {
1593
1660
  Name: INVOKE_TARGET_AGENT_TOOL_NAME,
1594
- Description: 'Hand the user\'s request to the target agent to perform the actual work. Call this whenever ' +
1595
- 'real work (data lookup, analysis, actions) is required, then narrate progress while it runs.',
1661
+ Description: INVOKE_TARGET_AGENT_DESCRIPTION,
1596
1662
  ParametersSchema: {
1597
1663
  type: 'object',
1598
1664
  properties: {
@@ -1609,11 +1675,22 @@ export class RealtimeClientSessionService {
1609
1675
  required: ['request']
1610
1676
  }
1611
1677
  };
1612
- return extraTools && extraTools.length > 0 ? [invokeTarget, ...extraTools] : [invokeTarget];
1678
+ const result = [invokeTarget];
1679
+ const seenNames = new Set([INVOKE_TARGET_AGENT_TOOL_NAME.toLowerCase()]);
1680
+ if (extraTools && extraTools.length > 0) {
1681
+ for (const tool of extraTools) {
1682
+ const key = tool.Name.toLowerCase();
1683
+ if (!seenNames.has(key)) {
1684
+ seenNames.add(key);
1685
+ result.push(tool);
1686
+ }
1687
+ }
1688
+ }
1689
+ return result;
1613
1690
  }
1614
1691
  /**
1615
1692
  * Builds the {@link RealtimeToolBroker} for a relayed tool call, wiring `DelegateToTarget` to a
1616
- * target-agent run and `ExecuteTool` to a structured "not available" placeholder.
1693
+ * target-agent run and `ExecuteTool` to direct-action execution or a structured "not available" placeholder.
1617
1694
  *
1618
1695
  * @param input The relayed tool input.
1619
1696
  * @param contextUser The calling user.
@@ -1623,7 +1700,7 @@ export class RealtimeClientSessionService {
1623
1700
  buildToolBroker(input, contextUser, provider) {
1624
1701
  const deps = {
1625
1702
  DelegateToTarget: (request) => this.delegateToTarget(input, request, contextUser, provider),
1626
- ExecuteTool: (call) => this.executeNonTargetTool(call)
1703
+ ExecuteTool: (call) => this.executeNonTargetTool(call, input, contextUser)
1627
1704
  };
1628
1705
  return new RealtimeToolBroker(deps);
1629
1706
  }
@@ -1885,19 +1962,239 @@ export class RealtimeClientSessionService {
1885
1962
  return (await run.Load(parentRunID)) ? run : null;
1886
1963
  }
1887
1964
  /**
1888
- * Routes a non-target tool call. For now this returns a structured "not available" result —
1889
- * the richer client/UI/action routing is wired in a later phase. Documented minimal seam.
1965
+ * Inspects whether the resolved realtime driver class supports dynamic tool registration.
1966
+ * Dynamic tool sets (e.g. per-agent direct action projection) are registered on the provider
1967
+ * socket only when the driver explicitly declares `SupportsDynamicToolSet === true`.
1890
1968
  *
1891
- * @param call The non-target tool call.
1892
- * @returns A failed {@link ToolExecutionResult} the model can narrate.
1969
+ * @param driverClass The vendor's DriverClass (the ClassFactory key).
1970
+ * @returns True if the driver supports dynamic tool sets, false otherwise.
1893
1971
  */
1894
- async executeNonTargetTool(call) {
1972
+ driverSupportsDynamicToolSet(driverClass) {
1973
+ if (!driverClass) {
1974
+ return false;
1975
+ }
1976
+ const reg = MJGlobal.Instance.ClassFactory.GetRegistration(BaseRealtimeModel, driverClass);
1977
+ const subClass = reg?.SubClass;
1978
+ return subClass?.SupportsDynamicToolSet === true;
1979
+ }
1980
+ /**
1981
+ * Resolves the active database actions assigned to the target agent.
1982
+ * **Overridable seam** for tests to provide mock action sets without an engine cache.
1983
+ *
1984
+ * @param targetAgentID The target agent ID.
1985
+ * @returns The active action entities assigned to the agent.
1986
+ */
1987
+ getTargetAgentActions(targetAgentID) {
1988
+ const agentActions = (AIEngine.Instance.AgentActions ?? []).filter(aa => UUIDsEqual(aa.AgentID, targetAgentID) && aa.Status === 'Active');
1989
+ return (ActionEngineServer.Instance.Actions ?? []).filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)) && a.Status === 'Active');
1990
+ }
1991
+ /**
1992
+ * Builds the projected direct action tool definitions for a target agent if supported by the driver.
1993
+ * Gated by driver capability (`SupportsDynamicToolSet`) and explicit configuration opt-in
1994
+ * (`Configuration.realtime.directActions.enabled = true`).
1995
+ *
1996
+ * @param targetAgentID The target agent ID being voiced.
1997
+ * @param effectiveConfig The resolved effective configuration.
1998
+ * @param driverClass The resolved vendor's DriverClass.
1999
+ * @returns The array of projected direct action tools (empty if unsupported or disabled).
2000
+ */
2001
+ buildDirectActionTools(targetAgentID, effectiveConfig, driverClass, agentSessionID) {
2002
+ if (!targetAgentID || !this.driverSupportsDynamicToolSet(driverClass)) {
2003
+ return [];
2004
+ }
2005
+ const directConfig = GetDirectActionsConfig(effectiveConfig);
2006
+ if (!directConfig || !directConfig.enabled) {
2007
+ return [];
2008
+ }
2009
+ const candidateActions = this.getTargetAgentActions(targetAgentID);
2010
+ const allowedActions = candidateActions.filter(action => IsActionAllowedForDirectInvocation(action.Name, directConfig));
2011
+ const wireMap = this.buildWireActionMap(allowedActions);
2012
+ if (agentSessionID) {
2013
+ this.sessionWireActionMaps.set(agentSessionID, wireMap);
2014
+ if (directConfig) {
2015
+ this.sessionDirectConfigs.set(agentSessionID, directConfig);
2016
+ }
2017
+ }
2018
+ this.targetWireActionMaps.set(targetAgentID, wireMap);
2019
+ const tools = [];
2020
+ for (const [wireName, action] of wireMap.entries()) {
2021
+ tools.push(this.mapActionToToolDefinition(action, wireName));
2022
+ }
2023
+ return tools;
2024
+ }
2025
+ /**
2026
+ * Maps an action entity and its metadata parameters to a RealtimeToolDefinition.
2027
+ *
2028
+ * @param action The action entity to map.
2029
+ * @param wireName Optional pre-sanitized wire name. If omitted, derives via {@link SanitizeWireToolName}.
2030
+ * @returns The constructed tool definition.
2031
+ */
2032
+ mapActionToToolDefinition(action, wireName) {
2033
+ const rawParams = (action.Params?.Items && action.Params.Items.length > 0)
2034
+ ? action.Params.Items
2035
+ : (ActionEngineServer.Instance.ActionParams ?? []).filter(p => UUIDsEqual(p.ActionID, action.ID));
2036
+ const inputParams = rawParams.filter(p => {
2037
+ const dir = (p.Type ?? 'Input').trim().toLowerCase();
2038
+ return dir === 'input' || dir === 'both';
2039
+ });
2040
+ const properties = {};
2041
+ const required = [];
2042
+ for (const p of inputParams) {
2043
+ let schemaType = 'string';
2044
+ if (p.IsArray) {
2045
+ schemaType = 'array';
2046
+ }
2047
+ else if (p.ValueType === 'Simple Object' || p.ValueType === 'BaseEntity Sub-Class') {
2048
+ schemaType = 'object';
2049
+ }
2050
+ const propSchema = {
2051
+ type: schemaType,
2052
+ description: p.Description || p.Name
2053
+ };
2054
+ properties[p.Name] = propSchema;
2055
+ if (p.IsRequired) {
2056
+ required.push(p.Name);
2057
+ }
2058
+ }
2059
+ const parametersSchema = {
2060
+ type: 'object',
2061
+ properties: properties,
2062
+ ...(required.length > 0 ? { required } : {})
2063
+ };
1895
2064
  return {
1896
- CallID: call.CallID,
1897
- Success: false,
1898
- Output: `Tool '${call.ToolName}' is not available in this voice session.`
2065
+ Name: wireName ?? SanitizeWireToolName(action.Name),
2066
+ Description: action.Description || `Execute the ${action.Name} action.`,
2067
+ ParametersSchema: parametersSchema
1899
2068
  };
1900
2069
  }
2070
+ /**
2071
+ * Routes a non-target tool call by resolving the action through the wire map, checking the
2072
+ * direct-actions allowlist, and executing via `ActionEngineServer.Instance.RunAction` under a
2073
+ * configured timeout. If the tool is unrecognized, disallowed, or target resolution fails,
2074
+ * returns a structured "not available" result.
2075
+ *
2076
+ * @param call The non-target tool call.
2077
+ * @param input The optional relayed tool input context.
2078
+ * @param contextUser The calling user context.
2079
+ * @returns A {@link ToolExecutionResult} for the model's tool_response.
2080
+ */
2081
+ async executeNonTargetTool(call, input, contextUser) {
2082
+ if (!input?.TargetAgentID) {
2083
+ return {
2084
+ CallID: call.CallID,
2085
+ Success: false,
2086
+ Output: `Tool '${call.ToolName}' is not available in this voice session.`
2087
+ };
2088
+ }
2089
+ const target = this.resolveTargetAgent(input.TargetAgentID);
2090
+ if (!target) {
2091
+ return {
2092
+ CallID: call.CallID,
2093
+ Success: false,
2094
+ Output: `Target agent is not available to execute '${call.ToolName}'.`
2095
+ };
2096
+ }
2097
+ const candidateActions = this.getTargetAgentActions(target.ID);
2098
+ const candidateWireMap = this.buildWireActionMap(candidateActions);
2099
+ const action = (input?.AgentSessionID ? this.sessionWireActionMaps.get(input.AgentSessionID)?.get(call.ToolName) : undefined)
2100
+ ?? candidateWireMap.get(call.ToolName)
2101
+ ?? Array.from(candidateWireMap.entries()).find(([w]) => w.toLowerCase() === call.ToolName.trim().toLowerCase())?.[1];
2102
+ if (!action) {
2103
+ return {
2104
+ CallID: call.CallID,
2105
+ Success: false,
2106
+ Output: `Tool '${call.ToolName}' is not available to this agent.`
2107
+ };
2108
+ }
2109
+ const directConfig = input?.DirectActions
2110
+ ?? (input?.AgentSessionID ? this.sessionDirectConfigs.get(input.AgentSessionID) : undefined)
2111
+ ?? GetDirectActionsConfig(this.resolveEffectiveConfig(target, undefined, target));
2112
+ if (!IsActionAllowedForDirectInvocation(action.Name, directConfig)) {
2113
+ return {
2114
+ CallID: call.CallID,
2115
+ Success: false,
2116
+ Output: `Action '${action.Name}' is not enabled for direct voice invocation.`
2117
+ };
2118
+ }
2119
+ if (!contextUser) {
2120
+ return {
2121
+ CallID: call.CallID,
2122
+ Success: false,
2123
+ Output: `Execution of action '${action.Name}' refused: authenticated user context is required.`
2124
+ };
2125
+ }
2126
+ const timeoutMs = directConfig?.timeoutMs ?? 10_000;
2127
+ const executingUser = contextUser;
2128
+ try {
2129
+ const rawParams = this.parseActionParams(call.Arguments);
2130
+ const actionParams = Object.entries(rawParams).map(([name, value]) => ({
2131
+ Name: name,
2132
+ Value: value,
2133
+ Type: 'Input'
2134
+ }));
2135
+ const runParams = new RunActionParams();
2136
+ runParams.Action = action;
2137
+ runParams.ContextUser = executingUser;
2138
+ runParams.Params = actionParams;
2139
+ runParams.Filters = [];
2140
+ let timerHandle;
2141
+ const timeoutPromise = new Promise((_, reject) => {
2142
+ timerHandle = setTimeout(() => {
2143
+ reject(new Error(`Action '${action.Name}' execution timed out after ${timeoutMs}ms`));
2144
+ }, timeoutMs);
2145
+ if (typeof timerHandle.unref === 'function') {
2146
+ timerHandle.unref();
2147
+ }
2148
+ });
2149
+ const result = await Promise.race([
2150
+ ActionEngineServer.Instance.RunAction(runParams),
2151
+ timeoutPromise
2152
+ ]).finally(() => {
2153
+ if (timerHandle) {
2154
+ clearTimeout(timerHandle);
2155
+ }
2156
+ });
2157
+ const outputText = result.Message || (result.Success ? 'Action completed successfully.' : 'Action failed.');
2158
+ return {
2159
+ CallID: call.CallID,
2160
+ Success: result.Success,
2161
+ Output: outputText
2162
+ };
2163
+ }
2164
+ catch (error) {
2165
+ const message = error instanceof Error ? error.message : String(error);
2166
+ return {
2167
+ CallID: call.CallID,
2168
+ Success: false,
2169
+ Output: `Action '${action.Name}' failed: ${message}`
2170
+ };
2171
+ }
2172
+ }
2173
+ /**
2174
+ * Parses JSON arguments string into a key-value record of parameters.
2175
+ * Distinguishes "no arguments" (valid for zero-param action) from "unparseable JSON".
2176
+ *
2177
+ * @param argumentsJson The raw arguments string emitted by the model.
2178
+ * @returns The parsed arguments record.
2179
+ * @throws ToolArgumentsError with actionable message when arguments are present but unparseable.
2180
+ */
2181
+ parseActionParams(argumentsJson) {
2182
+ if (!argumentsJson || argumentsJson.trim() === '') {
2183
+ return {};
2184
+ }
2185
+ let parsed;
2186
+ try {
2187
+ parsed = JSON.parse(argumentsJson);
2188
+ }
2189
+ catch (err) {
2190
+ const syntaxMsg = err instanceof Error ? err.message : String(err);
2191
+ throw new ToolArgumentsError(`Unparseable JSON arguments: ${syntaxMsg}. Please provide valid JSON formatted arguments.`);
2192
+ }
2193
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2194
+ return parsed;
2195
+ }
2196
+ throw new ToolArgumentsError(`Tool arguments must be a JSON object, but received ${Array.isArray(parsed) ? 'an array' : typeof parsed}. Please format arguments as a JSON object.`);
2197
+ }
1901
2198
  /**
1902
2199
  * Parses the natural-language request text out of an `invoke-target-agent` call's arguments.
1903
2200
  * Falls back to the raw argument string when it is not the expected `{ request: string }` JSON.