@memberjunction/ai-agents 6.2.0-edge.1 → 6.2.0-edge.3

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 (131) hide show
  1. package/README.md +18 -0
  2. package/dist/AgentDecisionService.d.ts +90 -0
  3. package/dist/AgentDecisionService.d.ts.map +1 -0
  4. package/dist/AgentDecisionService.js +159 -0
  5. package/dist/AgentDecisionService.js.map +1 -0
  6. package/dist/AgentRunner.d.ts +9 -0
  7. package/dist/AgentRunner.d.ts.map +1 -1
  8. package/dist/AgentRunner.js +23 -7
  9. package/dist/AgentRunner.js.map +1 -1
  10. package/dist/ConversationCompactionManager.d.ts +7 -1
  11. package/dist/ConversationCompactionManager.d.ts.map +1 -1
  12. package/dist/ConversationCompactionManager.js +4 -1
  13. package/dist/ConversationCompactionManager.js.map +1 -1
  14. package/dist/KnowledgeAgent.d.ts.map +1 -1
  15. package/dist/KnowledgeAgent.js +6 -6
  16. package/dist/KnowledgeAgent.js.map +1 -1
  17. package/dist/PayloadFeedbackManager.d.ts +116 -14
  18. package/dist/PayloadFeedbackManager.d.ts.map +1 -1
  19. package/dist/PayloadFeedbackManager.js +229 -88
  20. package/dist/PayloadFeedbackManager.js.map +1 -1
  21. package/dist/agent-context-injector.d.ts +47 -3
  22. package/dist/agent-context-injector.d.ts.map +1 -1
  23. package/dist/agent-context-injector.js +77 -10
  24. package/dist/agent-context-injector.js.map +1 -1
  25. package/dist/agent-memory-context-builder.d.ts +13 -0
  26. package/dist/agent-memory-context-builder.d.ts.map +1 -1
  27. package/dist/agent-memory-context-builder.js +24 -5
  28. package/dist/agent-memory-context-builder.js.map +1 -1
  29. package/dist/agent-types/flow-agent-type.d.ts +97 -0
  30. package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
  31. package/dist/agent-types/flow-agent-type.js +281 -24
  32. package/dist/agent-types/flow-agent-type.js.map +1 -1
  33. package/dist/agent-types/flow-graph-executor.d.ts +12 -1
  34. package/dist/agent-types/flow-graph-executor.d.ts.map +1 -1
  35. package/dist/agent-types/flow-graph-executor.js +25 -1
  36. package/dist/agent-types/flow-graph-executor.js.map +1 -1
  37. package/dist/agent-types/loop-agent-prompt-params.d.ts +239 -66
  38. package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
  39. package/dist/agent-types/loop-agent-prompt-params.js +47 -2
  40. package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
  41. package/dist/agent-types/loop-agent-response-type.d.ts +12 -1
  42. package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
  43. package/dist/agent-types/loop-agent-response-type.js.map +1 -1
  44. package/dist/agent-types/loop-agent-type.d.ts +26 -0
  45. package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
  46. package/dist/agent-types/loop-agent-type.js +115 -5
  47. package/dist/agent-types/loop-agent-type.js.map +1 -1
  48. package/dist/base-agent.d.ts +527 -10
  49. package/dist/base-agent.d.ts.map +1 -1
  50. package/dist/base-agent.js +1779 -110
  51. package/dist/base-agent.js.map +1 -1
  52. package/dist/catalog-narrowing.d.ts +136 -0
  53. package/dist/catalog-narrowing.d.ts.map +1 -0
  54. package/dist/catalog-narrowing.js +212 -0
  55. package/dist/catalog-narrowing.js.map +1 -0
  56. package/dist/decision-discovery.d.ts +401 -0
  57. package/dist/decision-discovery.d.ts.map +1 -0
  58. package/dist/decision-discovery.js +511 -0
  59. package/dist/decision-discovery.js.map +1 -0
  60. package/dist/finish-if-mode-warnings.d.ts +39 -0
  61. package/dist/finish-if-mode-warnings.d.ts.map +1 -0
  62. package/dist/finish-if-mode-warnings.js +78 -0
  63. package/dist/finish-if-mode-warnings.js.map +1 -0
  64. package/dist/finish-if-state.d.ts +72 -0
  65. package/dist/finish-if-state.d.ts.map +1 -0
  66. package/dist/finish-if-state.js +109 -0
  67. package/dist/finish-if-state.js.map +1 -0
  68. package/dist/index.d.ts +10 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +11 -0
  71. package/dist/index.js.map +1 -1
  72. package/dist/memory-manager-agent.d.ts +108 -2
  73. package/dist/memory-manager-agent.d.ts.map +1 -1
  74. package/dist/memory-manager-agent.js +172 -9
  75. package/dist/memory-manager-agent.js.map +1 -1
  76. package/dist/memory-note-gate.d.ts +208 -0
  77. package/dist/memory-note-gate.d.ts.map +1 -0
  78. package/dist/memory-note-gate.js +294 -0
  79. package/dist/memory-note-gate.js.map +1 -0
  80. package/dist/native-tools/action-tool-builder.d.ts +14 -0
  81. package/dist/native-tools/action-tool-builder.d.ts.map +1 -1
  82. package/dist/native-tools/action-tool-builder.js +62 -7
  83. package/dist/native-tools/action-tool-builder.js.map +1 -1
  84. package/dist/native-tools/control-tools.d.ts +17 -1
  85. package/dist/native-tools/control-tools.d.ts.map +1 -1
  86. package/dist/native-tools/control-tools.js +40 -3
  87. package/dist/native-tools/control-tools.js.map +1 -1
  88. package/dist/realtime/bridge-host-params.d.ts +22 -0
  89. package/dist/realtime/bridge-host-params.d.ts.map +1 -0
  90. package/dist/realtime/bridge-host-params.js +53 -0
  91. package/dist/realtime/bridge-host-params.js.map +1 -0
  92. package/dist/realtime/bridge-realtime-session-factory.d.ts +23 -2
  93. package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
  94. package/dist/realtime/bridge-realtime-session-factory.js +22 -6
  95. package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
  96. package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -1
  97. package/dist/realtime/bridge-room-transcript-sink.js +15 -9
  98. package/dist/realtime/bridge-room-transcript-sink.js.map +1 -1
  99. package/dist/realtime/bridge-session-transcript-sink.d.ts +37 -0
  100. package/dist/realtime/bridge-session-transcript-sink.d.ts.map +1 -0
  101. package/dist/realtime/bridge-session-transcript-sink.js +58 -0
  102. package/dist/realtime/bridge-session-transcript-sink.js.map +1 -0
  103. package/dist/realtime/realtime-client-session-service.d.ts +117 -12
  104. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
  105. package/dist/realtime/realtime-client-session-service.js +162 -39
  106. package/dist/realtime/realtime-client-session-service.js.map +1 -1
  107. package/dist/realtime/realtime-coagent-resolution.d.ts +90 -0
  108. package/dist/realtime/realtime-coagent-resolution.d.ts.map +1 -0
  109. package/dist/realtime/realtime-coagent-resolution.js +186 -0
  110. package/dist/realtime/realtime-coagent-resolution.js.map +1 -0
  111. package/dist/realtime/realtime-delegation-narrator.d.ts +97 -0
  112. package/dist/realtime/realtime-delegation-narrator.d.ts.map +1 -0
  113. package/dist/realtime/realtime-delegation-narrator.js +162 -0
  114. package/dist/realtime/realtime-delegation-narrator.js.map +1 -0
  115. package/dist/realtime/realtime-session-runner.d.ts +2 -82
  116. package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
  117. package/dist/realtime/realtime-session-runner.js +12 -187
  118. package/dist/realtime/realtime-session-runner.js.map +1 -1
  119. package/dist/realtime/realtime-vendor-resolution.d.ts +8 -3
  120. package/dist/realtime/realtime-vendor-resolution.d.ts.map +1 -1
  121. package/dist/realtime/realtime-vendor-resolution.js +3 -3
  122. package/dist/realtime/realtime-vendor-resolution.js.map +1 -1
  123. package/dist/rubric-evaluation-agent-runner.d.ts +2 -0
  124. package/dist/rubric-evaluation-agent-runner.d.ts.map +1 -0
  125. package/dist/rubric-evaluation-agent-runner.js +47 -0
  126. package/dist/rubric-evaluation-agent-runner.js.map +1 -0
  127. package/dist/self-check.d.ts +101 -0
  128. package/dist/self-check.d.ts.map +1 -0
  129. package/dist/self-check.js +74 -0
  130. package/dist/self-check.js.map +1 -0
  131. package/package.json +19 -18
@@ -33,15 +33,28 @@ import { LogError, LogStatus, RunView } from '@memberjunction/core';
33
33
  import { MJGlobal, MJLruCache, UUIDsEqual, EscapeSQLString } from '@memberjunction/global';
34
34
  import { ActionEngineServer } from '@memberjunction/actions';
35
35
  import { RunActionParams } from '@memberjunction/actions-base';
36
- import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
36
+ import { BaseRealtimeModel, GetAIAPIKey, CredentialScopeAllows } from '@memberjunction/ai';
37
37
  import { FormatAppContextNote, ResolvePromptRunUserID } from '@memberjunction/ai-core-plus';
38
38
  import { AIEngine } from '@memberjunction/aiengine';
39
39
  import { AgentMemoryContextBuilder } from '../agent-memory-context-builder.js';
40
40
  import { AgentRunner } from '../AgentRunner.js';
41
+ import { DelegationNarrator } from './realtime-delegation-narrator.js';
42
+ import { FilterAllowedAgentsByCanRun } from './realtime-coagent-resolution.js';
41
43
  import { RealtimeToolBroker, INVOKE_TARGET_AGENT_TOOL_NAME, INVOKE_TARGET_AGENT_DESCRIPTION, BuildRealtimeAgentFraming } from './realtime-tool-broker.js';
42
44
  import { NARRATION_PROMPT_NAME, LEGACY_NARRATION_PROMPT_NAME, ResolveNarrationInstructionsTemplate } from './realtime-narration.js';
43
45
  import { BuildVoiceMannerSection, BuildAppRealtimeOverridesJson, DeepMergeConfigs, GetModelCatalogSessionSettings, GetDisclosureForTarget, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, ResolveEffectiveRealtimeConfig, MatchProviderVoiceSettings, GetDirectActionsConfig, IsActionAllowedForDirectInvocation } from './realtime-coagent-config.js';
44
46
  import { SelectRealtimeVendorForModel } from './realtime-vendor-resolution.js';
47
+ /** Runtime handles by their realtime session, so the layer that only holds the session can reach its runtime. */
48
+ const bridgeRuntimes = new WeakMap();
49
+ /**
50
+ * Returns the runtime wired onto a bridged realtime session by
51
+ * {@link RealtimeClientSessionService.WireBridgeRealtimeSession}, or `undefined` for a session that was never
52
+ * wired. Lets a host that only holds the {@link IRealtimeSession} (the telephony services) cancel delegations on
53
+ * barge-in and install its local tool handler.
54
+ */
55
+ export function GetBridgeRealtimeRuntime(session) {
56
+ return bridgeRuntimes.get(session);
57
+ }
45
58
  /**
46
59
  * Logs when a configuration authored per-provider voice settings but NONE matched the vendor that
47
60
  * actually ran — those settings are dropped, and pre-#3530 that happened with no trace at all: the
@@ -115,12 +128,27 @@ export class ToolArgumentsError extends Error {
115
128
  */
116
129
  export class RealtimeClientSessionService {
117
130
  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();
131
+ /**
132
+ * Maps session id -> (wireName -> MJActionEntityExtended) built during tool projection.
133
+ * Bounded with `MJLruCache` (rather than a plain `Map`): the resolver holds ONE shared service
134
+ * instance for the process lifetime (see class doc above), and there is no "session ended" hook
135
+ * this service can key eviction off — every voice session ever prepared would otherwise leave an
136
+ * entry here forever. Same TTL/maxSize rationale as {@link promptRunWriteChains} below.
137
+ */
138
+ this.sessionWireActionMaps = new MJLruCache({
139
+ maxSize: 5_000,
140
+ ttlMs: 4 * 60 * 60 * 1000, // 4h — generous vs. any realistic voice-session duration
141
+ });
142
+ /** Maps targetAgentID -> (wireName -> MJActionEntityExtended) fallback built during tool projection. Bounded for the same reason as {@link sessionWireActionMaps}. */
143
+ this.targetWireActionMaps = new MJLruCache({
144
+ maxSize: 5_000,
145
+ ttlMs: 4 * 60 * 60 * 1000,
146
+ });
147
+ /** Maps session id -> direct actions config resolved during session prep. Bounded for the same reason as {@link sessionWireActionMaps}. */
148
+ this.sessionDirectConfigs = new MJLruCache({
149
+ maxSize: 5_000,
150
+ ttlMs: 4 * 60 * 60 * 1000,
151
+ });
124
152
  /**
125
153
  * IN-FLIGHT DELEGATION REGISTRY — the server half of the client-direct CANCEL channel.
126
154
  *
@@ -300,6 +328,16 @@ export class RealtimeClientSessionService {
300
328
  // Prep must have succeeded before wiring; degrade to a tool-error fallback rather than throw.
301
329
  return this.wireBridgeFallbackRuntime(session);
302
330
  }
331
+ // The delegation set the model may reach, narrowed to what THIS run-as user may run (the browser path
332
+ // applies the same filter; without it a bridged call would reach colleagues the caller cannot).
333
+ const allowedAgents = await FilterAllowedAgentsByCanRun(prep.EffectiveConfig?.realtime?.allowedAgents, contextUser);
334
+ // Spoken progress while delegated work runs — the same pacing/wording the generic session runner uses.
335
+ const narrator = new DelegationNarrator({
336
+ GetSession: () => session,
337
+ NarrationInstructionsTemplate: this.resolveNarrationInstructionsTemplate(),
338
+ NarrationPaceMs: GetNarrationPaceMs(prep.EffectiveConfig) ?? undefined,
339
+ });
340
+ let localToolHandler;
303
341
  const promptID = this.resolveCoAgentSystemPrompt(coAgent).PromptID;
304
342
  const obs = await this.createCoAgentObservabilityRun(coAgent, promptID, resolution.ModelID, resolution.VendorID, input.UserID || contextUser?.ID, input.AgentSessionID, contextUser, provider, input.ConversationID);
305
343
  let finalized = false;
@@ -313,15 +351,18 @@ export class RealtimeClientSessionService {
313
351
  // Tool calls → the shared delegation entry point, then hand the serialized result back to the model.
314
352
  session.OnToolCall(async (call) => {
315
353
  try {
316
- const result = await this.ExecuteRelayedTool({
317
- AgentSessionID: input.AgentSessionID,
318
- ParentRunID: obs?.CoAgentRunID,
319
- TargetAgentID: input.TargetAgentID,
320
- AllowedAgents: prep.EffectiveConfig?.realtime?.allowedAgents,
321
- DirectActions: prep.EffectiveConfig?.realtime?.directActions,
322
- Call: call,
323
- }, contextUser, provider);
324
- await session.SendToolResult(call.CallID, result.ResultJson);
354
+ const resultJson = localToolHandler?.Handles(call.ToolName)
355
+ ? await localToolHandler.Execute(call)
356
+ : (await narrator.Track(() => this.ExecuteRelayedTool({
357
+ AgentSessionID: input.AgentSessionID,
358
+ ParentRunID: obs?.CoAgentRunID,
359
+ TargetAgentID: input.TargetAgentID,
360
+ AllowedAgents: allowedAgents,
361
+ DirectActions: prep.EffectiveConfig?.realtime?.directActions,
362
+ OnProgress: (progress) => narrator.HandleProgress(progress),
363
+ Call: call,
364
+ }, contextUser, provider))).ResultJson;
365
+ await session.SendToolResult(call.CallID, resultJson);
325
366
  }
326
367
  catch (error) {
327
368
  const message = error instanceof Error ? error.message : String(error);
@@ -333,11 +374,24 @@ export class RealtimeClientSessionService {
333
374
  // through the idempotent finalizer, so double-fire is harmless.
334
375
  const originalClose = session.Close.bind(session);
335
376
  session.Close = async () => {
377
+ narrator.Cancel();
336
378
  await finalize(true);
337
379
  await originalClose();
338
380
  };
339
381
  session.OnClose?.(() => { void finalize(true); });
340
- return { CoAgentRunID: obs?.CoAgentRunID, PromptRunID: obs?.PromptRunID, Finalize: finalize };
382
+ const runtime = {
383
+ CoAgentRunID: obs?.CoAgentRunID,
384
+ PromptRunID: obs?.PromptRunID,
385
+ Finalize: finalize,
386
+ CancelInFlightDelegations: () => {
387
+ narrator.Cancel(); // a stale "still working on it" line must not be spoken over the caller
388
+ return this.CancelInFlightDelegations(input.AgentSessionID);
389
+ },
390
+ CancelPendingNarration: () => narrator.Cancel(),
391
+ SetLocalToolHandler: (handler) => { localToolHandler = handler; },
392
+ };
393
+ bridgeRuntimes.set(session, runtime);
394
+ return runtime;
341
395
  }
342
396
  /**
343
397
  * Degenerate {@link BridgeRealtimeRuntime} for the rare case wiring is attempted without a resolved
@@ -348,7 +402,14 @@ export class RealtimeClientSessionService {
348
402
  session.OnToolCall((call) => {
349
403
  void session.SendToolResult(call.CallID, JSON.stringify({ success: false, error: 'Tool execution is unavailable — the co-agent did not resolve. Let the user know.' }));
350
404
  });
351
- return { Finalize: async () => { } };
405
+ const runtime = {
406
+ Finalize: async () => { },
407
+ CancelInFlightDelegations: () => 0,
408
+ CancelPendingNarration: () => { },
409
+ SetLocalToolHandler: () => { },
410
+ };
411
+ bridgeRuntimes.set(session, runtime);
412
+ return runtime;
352
413
  }
353
414
  /**
354
415
  * **The single source of truth for realtime session prep.** Builds the {@link RealtimeSessionParams}
@@ -1024,16 +1085,36 @@ export class RealtimeClientSessionService {
1024
1085
  * @returns The resolution outcome (resolution or failure reason).
1025
1086
  */
1026
1087
  async resolveModelForSession(input, coAgent, effectiveConfig) {
1088
+ // One resolver for every branch below, built here rather than per branch so the requested-model,
1089
+ // configured-preference and default paths cannot disagree about which credentials the session
1090
+ // may use. It answers with the session's RUN keys only: resolveVendorAndInstantiate layers this
1091
+ // service's getAPIKeyForDriver seam under it, giving run key → seam → (the seam's default) env.
1092
+ // A resolver that fell back to the environment itself would answer before the seam, so a
1093
+ // subclass that overrides the seam would lose to AI_VENDOR_API_KEY__<driver>.
1094
+ const resolveRunKey = this.buildRunKeyResolver(input.APIKeys);
1095
+ const scope = input.CredentialScope ?? 'Any';
1027
1096
  if (input.PreferredModelID) {
1028
- return this.resolvePreferredRealtimeModel(input.PreferredModelID);
1097
+ return this.resolvePreferredRealtimeModel(input.PreferredModelID, resolveRunKey, scope);
1029
1098
  }
1030
- const fromConfig = this.resolveConfiguredModelPreference(effectiveConfig);
1099
+ const fromConfig = this.resolveConfiguredModelPreference(effectiveConfig, resolveRunKey, scope);
1031
1100
  if (fromConfig) {
1032
1101
  return { Resolution: fromConfig };
1033
1102
  }
1034
- const resolution = await this.resolveRealtimeModel(coAgent);
1103
+ const resolution = await this.resolveRealtimeModel(coAgent, resolveRunKey, scope);
1035
1104
  return resolution ? { Resolution: resolution } : { ErrorMessage: this.noModelMessage() };
1036
1105
  }
1106
+ /**
1107
+ * The session's run-scoped keys as a resolver: the key for a driver class when the run carries
1108
+ * one, else `undefined` — no platform fallback. The fallback belongs to
1109
+ * {@link getAPIKeyForDriver}, which {@link resolveVendorAndInstantiate} consults for any class
1110
+ * this does not key.
1111
+ *
1112
+ * @param apiKeys The session's run keys ({@link PrepareClientSessionInput.APIKeys}), if any.
1113
+ * @returns A resolver over those keys only.
1114
+ */
1115
+ buildRunKeyResolver(apiKeys) {
1116
+ return (driverClass) => apiKeys?.find(k => k.driverClass === driverClass)?.apiKey || undefined;
1117
+ }
1037
1118
  /**
1038
1119
  * Resolves the effective config's `realtime.modelPreference` (an `MJ: AI Models` Name OR ID)
1039
1120
  * into a usable realtime model. TOLERANT by design — this is a METADATA preference, so any
@@ -1042,9 +1123,11 @@ export class RealtimeClientSessionService {
1042
1123
  * explicit runtime choice ({@link resolvePreferredRealtimeModel}), which fails loud.
1043
1124
  *
1044
1125
  * @param effectiveConfig The resolved effective configuration.
1126
+ * @param resolve The session's run-scoped key resolver (run keys only; see {@link resolveVendorAndInstantiate}).
1127
+ * @param credentialScope The session's credential scope; see {@link resolveVendorAndInstantiate}.
1045
1128
  * @returns The resolution, or `null` when no preference is configured or it can't be satisfied.
1046
1129
  */
1047
- resolveConfiguredModelPreference(effectiveConfig) {
1130
+ resolveConfiguredModelPreference(effectiveConfig, resolve, credentialScope = 'Any') {
1048
1131
  const preference = effectiveConfig?.realtime?.modelPreference;
1049
1132
  if (!preference) {
1050
1133
  return null;
@@ -1060,7 +1143,7 @@ export class RealtimeClientSessionService {
1060
1143
  `'${model.Name}' but it is not an Active Realtime model — falling through to default resolution.`);
1061
1144
  return null;
1062
1145
  }
1063
- const resolution = this.resolveVendorAndInstantiate(model);
1146
+ const resolution = this.resolveVendorAndInstantiate(model, resolve, credentialScope);
1064
1147
  if (!resolution) {
1065
1148
  LogError(`RealtimeClientSessionService: configured realtime model preference '${model.Name}' has no usable ` +
1066
1149
  'vendor DriverClass/API key — falling through to default resolution.');
@@ -1092,9 +1175,11 @@ export class RealtimeClientSessionService {
1092
1175
  * the caller's user explicitly chose this one.
1093
1176
  *
1094
1177
  * @param preferredModelID The `MJ: AI Models.ID` the user chose.
1178
+ * @param resolve The session's run-scoped key resolver (run keys only; see {@link resolveVendorAndInstantiate}).
1179
+ * @param credentialScope The session's credential scope; see {@link resolveVendorAndInstantiate}.
1095
1180
  * @returns The resolution outcome (resolution or a specific failure reason).
1096
1181
  */
1097
- resolvePreferredRealtimeModel(preferredModelID) {
1182
+ resolvePreferredRealtimeModel(preferredModelID, resolve, credentialScope = 'Any') {
1098
1183
  const model = this.findModelByID(preferredModelID);
1099
1184
  if (!model) {
1100
1185
  return { ErrorMessage: `The requested realtime model (id '${preferredModelID}') was not found in AI model metadata.` };
@@ -1105,7 +1190,7 @@ export class RealtimeClientSessionService {
1105
1190
  if (!this.isRealtimeModel(model)) {
1106
1191
  return { ErrorMessage: `The requested model '${model.Name}' is not a Realtime model (its type is '${model.AIModelType}').` };
1107
1192
  }
1108
- const resolution = this.resolveVendorAndInstantiate(model);
1193
+ const resolution = this.resolveVendorAndInstantiate(model, resolve, credentialScope);
1109
1194
  if (!resolution) {
1110
1195
  return {
1111
1196
  ErrorMessage: `The requested model '${model.Name}' has no active vendor with a usable DriverClass/API key ` +
@@ -1137,9 +1222,11 @@ export class RealtimeClientSessionService {
1137
1222
  * step can't be satisfied.
1138
1223
  *
1139
1224
  * @param coAgent The co-agent being voiced (reserved for future per-agent model preference).
1225
+ * @param resolve The session's run-scoped key resolver (run keys only; see {@link resolveVendorAndInstantiate}).
1226
+ * @param credentialScope The session's credential scope; see {@link resolveVendorAndInstantiate}.
1140
1227
  * @returns The resolved model + identifiers, or `null`.
1141
1228
  */
1142
- async resolveRealtimeModel(coAgent) {
1229
+ async resolveRealtimeModel(coAgent, resolve, credentialScope = 'Any') {
1143
1230
  // Walk candidates in descending PowerRank, returning the FIRST that fully resolves to a usable
1144
1231
  // client-direct driver (active vendor + API key + ClassFactory driver + SupportsClientDirect).
1145
1232
  // Single-pick dead-ended whenever the highest-power model lacked a key or client-direct support
@@ -1147,7 +1234,7 @@ export class RealtimeClientSessionService {
1147
1234
  // surfaced "No usable Realtime model" instead of falling through to a model that works.
1148
1235
  const candidates = this.selectRealtimeModelCandidates(coAgent);
1149
1236
  for (const model of candidates) {
1150
- const resolution = this.resolveVendorAndInstantiate(model);
1237
+ const resolution = this.resolveVendorAndInstantiate(model, resolve, credentialScope);
1151
1238
  if (resolution && resolution.Model.SupportsClientDirect) {
1152
1239
  return resolution;
1153
1240
  }
@@ -1158,15 +1245,31 @@ export class RealtimeClientSessionService {
1158
1245
  * Shared tail of model resolution: picks the vendor (with a usable API key) for an
1159
1246
  * already-chosen model entity and instantiates its realtime driver.
1160
1247
  *
1248
+ * Key precedence, per driver class: `resolve` (the session's run keys), then this service's
1249
+ * {@link getAPIKeyForDriver} seam, whose default is the environment key. Vendor selection and the
1250
+ * mint use that one chain, so a session is never routed to a vendor it cannot then be minted on.
1251
+ *
1161
1252
  * @param model The chosen model entity.
1253
+ * @param resolve The session's run-scoped key resolver. Expected to answer with run keys only:
1254
+ * one that falls back to the environment itself answers before {@link getAPIKeyForDriver} and
1255
+ * so bypasses an override of it.
1256
+ * @param credentialScope A scope that rules out the `'Environment'` source (`'RuntimeOnly'`) drops the
1257
+ * {@link getAPIKeyForDriver} seam: the run's keys are the whole chain, so a vendor they do not key
1258
+ * is never selected on the platform's key.
1162
1259
  * @returns The full resolution, or `null` when no vendor/key/driver can be satisfied.
1163
1260
  */
1164
- resolveVendorAndInstantiate(model) {
1165
- const vendor = this.selectRealtimeVendor(model.ID);
1261
+ resolveVendorAndInstantiate(model, resolve, credentialScope = 'Any') {
1262
+ // The run's keys first, then this service's own seam (which subclasses and tests override) —
1263
+ // so a run-scoped credential wins without taking that seam away from anyone who replaced it.
1264
+ // A scope that rules out the environment has no second step: the seam's default is the platform key.
1265
+ const resolveKey = CredentialScopeAllows(credentialScope, 'Environment')
1266
+ ? (driverClass) => resolve?.(driverClass) ?? this.getAPIKeyForDriver(driverClass)
1267
+ : (driverClass) => resolve?.(driverClass);
1268
+ const vendor = this.selectRealtimeVendor(model.ID, resolveKey);
1166
1269
  if (!vendor) {
1167
1270
  return null;
1168
1271
  }
1169
- const apiKey = this.getAPIKeyForDriver(vendor.DriverClass);
1272
+ const apiKey = resolveKey(vendor.DriverClass);
1170
1273
  if (!apiKey) {
1171
1274
  return null;
1172
1275
  }
@@ -1187,6 +1290,9 @@ export class RealtimeClientSessionService {
1187
1290
  /**
1188
1291
  * Resolves the API key for a vendor driver class. **Overridable seam** (wraps the module-level
1189
1292
  * {@link GetAIAPIKey}) so tests can simulate present/absent keys without environment setup.
1293
+ * Consulted for every driver class the session's run keys do not cover, so an override (a vault,
1294
+ * a different key, or `undefined` to disable a vendor) applies whether or not the environment
1295
+ * also holds a key.
1190
1296
  *
1191
1297
  * @param driverClass The vendor's `DriverClass`.
1192
1298
  * @returns The API key, or a falsy value when none is configured.
@@ -1225,10 +1331,11 @@ export class RealtimeClientSessionService {
1225
1331
  * THIS service's {@link getAPIKeyForDriver} so subclasses and tests keep their key-resolution seam.
1226
1332
  *
1227
1333
  * @param modelID The chosen model's id.
1334
+ * @param resolve The complete key chain to select against; defaults to {@link getAPIKeyForDriver}.
1228
1335
  * @returns The vendor driver/api identifiers, or `null` when none has a usable key.
1229
1336
  */
1230
- selectRealtimeVendor(modelID) {
1231
- return SelectRealtimeVendorForModel(modelID, (driverClass) => this.getAPIKeyForDriver(driverClass));
1337
+ selectRealtimeVendor(modelID, resolve) {
1338
+ return SelectRealtimeVendorForModel(modelID, resolve ?? ((driverClass) => this.getAPIKeyForDriver(driverClass)));
1232
1339
  }
1233
1340
  /**
1234
1341
  * Resolves the DB-driven progress-narration instruction template: the Active `MJ: AI Prompts`
@@ -1267,7 +1374,7 @@ export class RealtimeClientSessionService {
1267
1374
  const combinedExtra = directTools.length > 0
1268
1375
  ? [...(input.ExtraTools ?? []), ...directTools]
1269
1376
  : input.ExtraTools;
1270
- const tools = this.buildStableToolSet(combinedExtra);
1377
+ const tools = this.appendHostTools(this.buildStableToolSet(combinedExtra), input.HostTools);
1271
1378
  // Hoisted (rather than built inline at the return) so the mint log below can report the voice
1272
1379
  // that ACTUALLY reached the driver — see the `voice=` field. Same bag, built once.
1273
1380
  const configBag = this.buildSessionConfigBag(input, effectiveConfig, driverClass, modelID, modelVendorID);
@@ -1362,6 +1469,7 @@ export class RealtimeClientSessionService {
1362
1469
  const colleagues = this.buildColleaguesFromConfig(effectiveConfig, input.TargetAgentID);
1363
1470
  const framing = BuildRealtimeAgentFraming(targetName, this.buildInteractiveSurfaceFraming(input.ExtraTools), colleagues, hasDirectTools);
1364
1471
  const meetingFraming = this.buildMeetingFraming(input);
1472
+ const hostFraming = input.HostFraming?.trim() ?? '';
1365
1473
  const coAgentPrompt = this.getCoAgentSystemPromptText(coAgent);
1366
1474
  const voiceManner = BuildVoiceMannerSection(effectiveConfig);
1367
1475
  const targetIdentity = this.formatTargetIdentity(target);
@@ -1369,7 +1477,7 @@ export class RealtimeClientSessionService {
1369
1477
  const priorTranscript = this.formatPriorTranscript(input.PriorTranscript);
1370
1478
  const history = this.formatConversationHistory(input.ConversationMessages);
1371
1479
  const memoryContext = await this.assembleMemoryContext(input, coAgent, contextUser, provider);
1372
- return [framing, meetingFraming, coAgentPrompt, voiceManner, targetIdentity, appContextSection, priorTranscript, history, memoryContext]
1480
+ return [framing, meetingFraming, hostFraming, coAgentPrompt, voiceManner, targetIdentity, appContextSection, priorTranscript, history, memoryContext]
1373
1481
  .filter(part => part && part.trim().length > 0)
1374
1482
  .join('\n\n');
1375
1483
  }
@@ -1695,6 +1803,21 @@ export class RealtimeClientSessionService {
1695
1803
  }
1696
1804
  return result;
1697
1805
  }
1806
+ /** Appends the host-declared tools to a stable tool set, dropping any whose name is already taken. */
1807
+ appendHostTools(tools, hostTools) {
1808
+ if (!hostTools || hostTools.length === 0) {
1809
+ return tools;
1810
+ }
1811
+ const taken = new Set(tools.map((t) => t.Name.toLowerCase()));
1812
+ const merged = [...tools];
1813
+ for (const tool of hostTools) {
1814
+ if (!taken.has(tool.Name.toLowerCase())) {
1815
+ taken.add(tool.Name.toLowerCase());
1816
+ merged.push(tool);
1817
+ }
1818
+ }
1819
+ return merged;
1820
+ }
1698
1821
  /**
1699
1822
  * Builds the {@link RealtimeToolBroker} for a relayed tool call, wiring `DelegateToTarget` to a
1700
1823
  * target-agent run and `ExecuteTool` to direct-action execution or a structured "not available" placeholder.
@@ -2017,12 +2140,12 @@ export class RealtimeClientSessionService {
2017
2140
  const allowedActions = candidateActions.filter(action => IsActionAllowedForDirectInvocation(action.Name, directConfig));
2018
2141
  const wireMap = this.BuildWireActionMap(allowedActions);
2019
2142
  if (agentSessionID) {
2020
- this.sessionWireActionMaps.set(agentSessionID, wireMap);
2143
+ this.sessionWireActionMaps.Set(agentSessionID, wireMap);
2021
2144
  if (directConfig) {
2022
- this.sessionDirectConfigs.set(agentSessionID, directConfig);
2145
+ this.sessionDirectConfigs.Set(agentSessionID, directConfig);
2023
2146
  }
2024
2147
  }
2025
- this.targetWireActionMaps.set(targetAgentID, wireMap);
2148
+ this.targetWireActionMaps.Set(targetAgentID, wireMap);
2026
2149
  const tools = [];
2027
2150
  for (const [wireName, action] of wireMap.entries()) {
2028
2151
  tools.push(this.mapActionToToolDefinition(action, wireName));
@@ -2107,7 +2230,7 @@ export class RealtimeClientSessionService {
2107
2230
  }
2108
2231
  const candidateActions = this.getTargetAgentActions(target.ID);
2109
2232
  const candidateWireMap = this.BuildWireActionMap(candidateActions);
2110
- const action = (input?.AgentSessionID ? this.sessionWireActionMaps.get(input.AgentSessionID)?.get(call.ToolName) : undefined)
2233
+ const action = (input?.AgentSessionID ? this.sessionWireActionMaps.Get(input.AgentSessionID)?.get(call.ToolName) : undefined)
2111
2234
  ?? candidateWireMap.get(call.ToolName)
2112
2235
  ?? Array.from(candidateWireMap.entries()).find(([w]) => w.toLowerCase() === call.ToolName.trim().toLowerCase())?.[1];
2113
2236
  if (!action) {
@@ -2118,7 +2241,7 @@ export class RealtimeClientSessionService {
2118
2241
  };
2119
2242
  }
2120
2243
  const directConfig = input?.DirectActions
2121
- ?? (input?.AgentSessionID ? this.sessionDirectConfigs.get(input.AgentSessionID) : undefined)
2244
+ ?? (input?.AgentSessionID ? this.sessionDirectConfigs.Get(input.AgentSessionID) : undefined)
2122
2245
  ?? GetDirectActionsConfig(this.resolveEffectiveConfig(target, undefined, target));
2123
2246
  if (!IsActionAllowedForDirectInvocation(action.Name, directConfig)) {
2124
2247
  return {