@memberjunction/ai-agents 5.23.0 → 5.25.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.
@@ -10,6 +10,7 @@
10
10
  * @author MemberJunction.com
11
11
  * @since 2.49.0
12
12
  */
13
+ import { FileStorageEngineBase } from '@memberjunction/core-entities';
13
14
  import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled } from '@memberjunction/core';
14
15
  import { AIPromptRunner } from '@memberjunction/ai-prompts';
15
16
  import { BaseAgentType } from './agent-types/base-agent-type.js';
@@ -19,7 +20,7 @@ import { ActionEngineServer } from '@memberjunction/actions';
19
20
  import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
20
21
  import { AgentContextInjector } from './agent-context-injector.js';
21
22
  import { RerankerService } from '@memberjunction/ai-reranker';
22
- import { AIPromptParams, ChildPromptParam, ConversationUtility, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
23
+ import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
23
24
  import { AgentRunner } from './AgentRunner.js';
24
25
  import { PayloadManager } from './PayloadManager.js';
25
26
  import { ScratchpadManager } from './ScratchpadManager.js';
@@ -120,6 +121,7 @@ export class BaseAgent {
120
121
  * @private
121
122
  */
122
123
  this._feedbackRequestId = null;
124
+ this._resolvedStorageAccountId = null;
123
125
  /**
124
126
  * Agent hierarchy for display purposes (e.g., ["Marketing Agent", "Copywriter Agent"]).
125
127
  * Tracked separately as it's display-only and doesn't need persistence.
@@ -162,6 +164,14 @@ export class BaseAgent {
162
164
  * @since 3.1.0
163
165
  */
164
166
  this._mediaOutputs = [];
167
+ /**
168
+ * Accumulated file outputs (PDF, Excel, Word, etc.) produced by file-generation actions.
169
+ * Detected from the FileOutput output param after each action executes.
170
+ * Returned in ExecuteAgentResult.fileOutputs for processing by AgentRunner into MJ: Artifacts.
171
+ * @private
172
+ * @since 5.22.0
173
+ */
174
+ this._fileOutputs = [];
165
175
  /**
166
176
  * Payload manager for handling payload access control.
167
177
  * @private
@@ -226,12 +236,6 @@ export class BaseAgent {
226
236
  * Storage for injected notes and examples to include in result
227
237
  */
228
238
  this._injectedMemory = { notes: [], examples: [] };
229
- /**
230
- * Walks up the agent's category hierarchy looking for an AssignmentStrategy.
231
- * Loads categories via RunView and caches them for the duration of this run.
232
- * @private
233
- */
234
- this._categoryCache = null;
235
239
  /**
236
240
  * Determines the request type ID based on the Chat step's context.
237
241
  * - If the responseForm has only approve/reject-style buttons, uses "Approval"
@@ -267,6 +271,16 @@ export class BaseAgent {
267
271
  get AgentTypeInstance() {
268
272
  return this._agentTypeInstance;
269
273
  }
274
+ /**
275
+ * The resolved FileStorageAccount ID for this agent run. Set during Execute()
276
+ * via the hierarchical resolution chain (Runtime → Agent → Category → Type → fallback).
277
+ * Included in the ExecuteAgentResult so AgentRunner can route file artifact uploads.
278
+ *
279
+ * Subclasses can read this to customize storage behavior based on the resolved account.
280
+ */
281
+ get ResolvedStorageAccountId() {
282
+ return this._resolvedStorageAccountId;
283
+ }
270
284
  /**
271
285
  * Access the current run for the agent
272
286
  */
@@ -358,6 +372,29 @@ export class BaseAgent {
358
372
  * @private
359
373
  */
360
374
  static { this.LARGE_BINARY_THRESHOLD = 10000; }
375
+ /**
376
+ * Inspects a set of action output params for any value matching the FileOutputRef shape
377
+ * (an object with `fileName`, `mimeType`, and either `fileData` or `fileId`).
378
+ * Returns all matching FileOutputRef values found across all output params.
379
+ *
380
+ * Detection is shape-based, not name-based — actions can name their file output
381
+ * parameter anything and it will still be detected.
382
+ *
383
+ * @param outputParams - The output parameters from an action result
384
+ * @private
385
+ * @since 5.22.0
386
+ */
387
+ detectFileOutputs(outputParams) {
388
+ const results = [];
389
+ for (const param of outputParams) {
390
+ if (param.Value == null)
391
+ continue;
392
+ const ref = ParseFileOutputRef(param.Value);
393
+ if (ref)
394
+ results.push(ref);
395
+ }
396
+ return results;
397
+ }
361
398
  /**
362
399
  * Intercepts large media content in action results and replaces with placeholder references.
363
400
  * This prevents context overflow when action results contain large base64 data (images, audio, video).
@@ -832,13 +869,36 @@ export class BaseAgent {
832
869
  this._activeProvider = params.provider || Metadata.Provider;
833
870
  try {
834
871
  this.logStatus(`🤖 Starting execution of agent '${params.agent.Name}'`, true, params);
835
- // Check permissions - user must have run permission or be the owner
836
- const canRun = await AIAgentPermissionHelper.HasPermission(params.agent.ID, params.contextUser, 'run');
837
- if (!canRun) {
838
- const errorMessage = `User ${params.contextUser.Email} does not have permission to run agent '${params.agent.Name}' (ID: ${params.agent.ID})`;
839
- this.logStatus(`🚫 ${errorMessage}`, false, params);
840
- throw new Error(errorMessage);
841
- }
872
+ // =====================================================================================
873
+ // LATENCY OPTIMIZATION (Opt #4): Parallelized initialization sequence.
874
+ //
875
+ // The agent initialization pipeline was originally fully sequential: each operation
876
+ // awaited before the next began, even when there were no data dependencies between
877
+ // them. This added ~150-200ms of unnecessary serial wait time.
878
+ //
879
+ // The restructured pipeline uses 4 phases:
880
+ //
881
+ // PRE-WORK (sync/fast): Parameter wrapping, markup conversion, payload init,
882
+ // cancellation check, payload validation (may exit early).
883
+ //
884
+ // PHASE 1 (parallel): Permission check + Engine init + AgentRun creation.
885
+ // These three are mutually independent. If permission fails, we mark the
886
+ // already-created AgentRun as failed and terminate — the wasted run record
887
+ // is a negligible cost vs. the ~150ms saved by not serializing these.
888
+ //
889
+ // PHASE 2 (parallel): Config load + Data preload + Context memory injection.
890
+ // All three depend on Phase 1 completing (engines loaded, agentRun exists)
891
+ // but NOT on each other. Config load reads from AIEngine's in-memory cache.
892
+ // Data preload fetches agent data sources. Context memory loads notes/examples
893
+ // and injects them into the conversation messages.
894
+ //
895
+ // PHASE 3 (sequential): Agent type initialization — must wait for config from
896
+ // Phase 2 because it needs the resolved agent type and prompt configuration.
897
+ //
898
+ // Original total init time: ~sum of all operations (~400-500ms)
899
+ // Optimized: ~max(Phase1) + max(Phase2) + Phase3 (~200-300ms)
900
+ // =====================================================================================
901
+ // --- PRE-WORK: Fast synchronous setup and early-exit checks ---
842
902
  // Wrap the progress callback to capture all events
843
903
  const wrappedParams = {
844
904
  ...params,
@@ -850,12 +910,20 @@ export class BaseAgent {
850
910
  }
851
911
  // Reset scratchpad for each new execution (ephemeral per run)
852
912
  this._scratchpadManager.Clear();
913
+ // Initialize starting payload — must complete before AgentRun creation since the
914
+ // run record stores the starting payload snapshot.
853
915
  await this.initializeStartingPayload(wrappedParams);
854
916
  // Check for cancellation at start
855
917
  if (params.cancellationToken?.aborted) {
856
918
  this.logStatus(`⚠️ Agent '${params.agent.Name}' execution cancelled before start`, true, params);
857
919
  return await this.createCancelledResult('Cancelled before execution started', params.contextUser);
858
920
  }
921
+ // Handle starting payload validation if configured — may return early with a
922
+ // validation failure result, so we run this before launching expensive parallel work.
923
+ const startingValidationResult = await this.handleStartingPayloadValidation(wrappedParams);
924
+ if (startingValidationResult) {
925
+ return startingValidationResult;
926
+ }
859
927
  // Report initialization progress
860
928
  wrappedParams.onProgress?.({
861
929
  step: 'initialization',
@@ -865,31 +933,46 @@ export class BaseAgent {
865
933
  hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
866
934
  }
867
935
  });
868
- // Initialize execution tracking
869
- await this.initializeAgentRun(wrappedParams);
870
- // Reset validation retry counters for this run
936
+ // --- PHASE 1: Permission check + Engine init + AgentRun creation (parallel) ---
937
+ // These three operations have zero data dependencies on each other:
938
+ // - Permission check queries the AIAgentPermission entity
939
+ // - Engine init calls AIEngine.Instance.Config() and ActionEngineServer.Instance.Config()
940
+ // - AgentRun creation inserts a new AIAgentRun record
941
+ //
942
+ // If permission check fails, we mark the AgentRun as failed. This is acceptable:
943
+ // an orphaned "failed" run record is harmless and far cheaper than serializing these
944
+ // three operations (~150ms savings).
945
+ const [canRun] = await Promise.all([
946
+ AIAgentPermissionHelper.HasPermission(params.agent.ID, params.contextUser, 'run'),
947
+ this.initializeEngines(params.contextUser),
948
+ this.initializeAgentRun(wrappedParams)
949
+ ]);
950
+ if (!canRun) {
951
+ // Permission denied — mark the already-created AgentRun as failed so it doesn't
952
+ // appear as a phantom "running" record in the UI.
953
+ const errorMessage = `User ${params.contextUser.Email} does not have permission to run agent '${params.agent.Name}' (ID: ${params.agent.ID})`;
954
+ this.logStatus(`🚫 ${errorMessage}`, false, params);
955
+ if (this._agentRun) {
956
+ this._agentRun.Status = 'Failed';
957
+ this._agentRun.ErrorMessage = errorMessage;
958
+ await this._agentRun.Save();
959
+ }
960
+ throw new Error(errorMessage);
961
+ }
962
+ // Reset per-run state (sync, instant — no parallelization needed)
871
963
  this._validationRetryCount = 0;
872
964
  this._generalValidationRetryCount = 0;
873
965
  this._contextRecoveryAttempts = 0;
874
- // Reset effective actions and dynamic limits for this run
875
966
  this._effectiveActions = [];
876
967
  this._dynamicActionLimits = {};
877
- // Reset media outputs accumulator for this run
878
- // (unified array now includes both promoted media and intercepted binary with refIds)
879
968
  this._mediaOutputs = [];
880
- // Store message lifecycle callback if provided
881
969
  this._messageLifecycleCallback = params.onMessageLifecycle;
882
- // Initialize engines
883
- await this.initializeEngines(params.contextUser);
884
- // Check for cancellation after initialization
970
+ // Resolve storage account for file artifacts
971
+ this._resolvedStorageAccountId = await this.getStorageAccountID(wrappedParams);
972
+ // Check for cancellation after Phase 1
885
973
  if (params.cancellationToken?.aborted) {
886
974
  return await this.createCancelledResult('Cancelled during initialization', params.contextUser);
887
975
  }
888
- // Handle starting payload validation if configured
889
- const startingValidationResult = await this.handleStartingPayloadValidation(wrappedParams);
890
- if (startingValidationResult) {
891
- return startingValidationResult;
892
- }
893
976
  // Report validation progress
894
977
  wrappedParams.onProgress?.({
895
978
  step: 'validation',
@@ -899,34 +982,26 @@ export class BaseAgent {
899
982
  hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
900
983
  }
901
984
  });
902
- // Create and track validation step
985
+ // Validate agent — may return early with a failure result. Runs after engines are
986
+ // initialized (Phase 1) since validation may inspect AIEngine metadata.
903
987
  const validationResult = await this.validateAgentWithTracking(params.agent, params.contextUser);
904
988
  if (validationResult)
905
989
  return validationResult;
906
- // Load agent configuration
990
+ // --- PHASE 2: Config load + Data preload + Context memory injection (parallel) ---
991
+ // All three depend on Phase 1 completing (engines initialized, agentRun exists) but
992
+ // have no dependencies on each other:
993
+ // - Config load reads agent type, prompts, and agent prompts from AIEngine's in-memory
994
+ // cache — typically < 5ms but async due to potential Config() refresh.
995
+ // - Data preload fetches agent data sources and creates a tracking step entity.
996
+ // - Context memory resolves scope configuration (pure computation), then loads notes
997
+ // and examples from the DB and injects them into conversation messages.
907
998
  this.logStatus(`📋 Loading configuration for agent '${params.agent.Name}'`, true, params);
908
- const config = await this.loadAgentConfiguration(params.agent);
909
- if (!config.success) {
910
- this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
911
- agent: params.agent,
912
- category: 'AgentConfiguration'
913
- });
914
- return await this.createFailureResult(config.errorMessage || 'Failed to load agent configuration', params.contextUser);
915
- }
916
- // Preload agent data sources unless disabled
917
- await this.preloadAgentData(wrappedParams);
918
- // now initialize the agent type which gets us the instance setup in our class plus also gets the agent type to initialize
919
- // its state
920
- await this.initializeAgentType(wrappedParams, config);
921
- // Inject context memory (notes and examples) before execution
999
+ // Pre-compute scope configuration for context memory injection (pure computation,
1000
+ // no I/O — safe to do before launching the parallel phase).
922
1001
  const userId = params.userId || params.contextUser?.ID;
923
1002
  const companyId = params.companyId;
924
- // Extract input text from conversation messages (last user message)
925
- const lastUserMessage = params.conversationMessages
926
- .filter(m => m.role === 'user')
927
- .pop();
1003
+ const lastUserMessage = params.conversationMessages.filter(m => m.role === 'user').pop();
928
1004
  const inputText = lastUserMessage?.content || '';
929
- // Parse agent-level scope config for note/example filtering
930
1005
  const scopeConfigJson = params.agent.ScopeConfig;
931
1006
  let scopeConfig = null;
932
1007
  if (scopeConfigJson) {
@@ -935,11 +1010,9 @@ export class BaseAgent {
935
1010
  }
936
1011
  catch { /* ignore bad JSON */ }
937
1012
  }
938
- // Resolve scope params from top-level params or data fallback (for GraphQL callers)
939
1013
  const primaryScopeEntityName = params.PrimaryScopeEntityName ?? params.data?.PrimaryScopeEntityName;
940
1014
  const primaryScopeRecordId = params.PrimaryScopeRecordID ?? params.data?.PrimaryScopeRecordID;
941
1015
  const secondaryScopes = params.SecondaryScopes ?? params.data?.SecondaryScopes;
942
- // Resolve entity name to entity ID for scope filtering
943
1016
  let primaryScopeEntityId;
944
1017
  if (primaryScopeEntityName) {
945
1018
  const primaryEntity = this._metadata.Entities.find(e => e.Name === primaryScopeEntityName);
@@ -947,8 +1020,22 @@ export class BaseAgent {
947
1020
  primaryScopeEntityId = primaryEntity.ID;
948
1021
  }
949
1022
  }
950
- // Inject context memory (notes and examples) into conversation messages
951
- await this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig);
1023
+ const [config] = await Promise.all([
1024
+ this.loadAgentConfiguration(params.agent),
1025
+ this.preloadAgentData(wrappedParams),
1026
+ this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig)
1027
+ ]);
1028
+ if (!config.success) {
1029
+ this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
1030
+ agent: params.agent,
1031
+ category: 'AgentConfiguration'
1032
+ });
1033
+ return await this.createFailureResult(config.errorMessage || 'Failed to load agent configuration', params.contextUser);
1034
+ }
1035
+ // --- PHASE 3: Agent type initialization (sequential) ---
1036
+ // Must wait for config from Phase 2 because it needs the resolved agent type and
1037
+ // prompt configuration to initialize the type-specific state machine.
1038
+ await this.initializeAgentType(wrappedParams, config);
952
1039
  // Execute the agent's internal logic with wrapped parameters
953
1040
  this.logStatus(`🚀 Executing agent '${params.agent.Name}' internal logic`, true, params);
954
1041
  const executionResult = await this.executeAgentInternal(wrappedParams, config);
@@ -3207,6 +3294,10 @@ The context is now within limits. Please retry your request with the recovered c
3207
3294
  Value: value,
3208
3295
  Type: 'Input'
3209
3296
  }));
3297
+ // Build action context: preserve the agent's context and inject resolved storage account ID
3298
+ const actionContext = this._resolvedStorageAccountId
3299
+ ? { ...(typeof params.context === 'object' && params.context ? params.context : {}), __resolvedStorageAccountId: this._resolvedStorageAccountId }
3300
+ : params.context;
3210
3301
  // Execute the action and return the full ActionResult
3211
3302
  const result = await actionEngine.RunAction({
3212
3303
  Action: actionEntity,
@@ -3214,7 +3305,7 @@ The context is now within limits. Please retry your request with the recovered c
3214
3305
  ContextUser: contextUser,
3215
3306
  Filters: [],
3216
3307
  SkipActionLog: false,
3217
- Context: params.context // pass along our context to actions so they can use it however they need
3308
+ Context: actionContext
3218
3309
  });
3219
3310
  if (result.Success) {
3220
3311
  this.logStatus(` ✅ Action '${action.name}' completed successfully`, true, params);
@@ -3618,6 +3709,18 @@ The context is now within limits. Please retry your request with the recovered c
3618
3709
  lines.push('');
3619
3710
  lines.push(`**User:** ${user.Name}${user.Roles?.length ? ` (Roles: ${user.Roles.join(', ')})` : ''}`);
3620
3711
  }
3712
+ // Additional context reported by the active view/component
3713
+ const dashboardCtx = ctx['AdditionalContext'];
3714
+ if (dashboardCtx && Object.keys(dashboardCtx).length > 0) {
3715
+ lines.push('');
3716
+ lines.push('**Dashboard state:**');
3717
+ for (const [key, value] of Object.entries(dashboardCtx)) {
3718
+ if (value === null || value === undefined)
3719
+ continue;
3720
+ const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
3721
+ lines.push(`- ${key}: ${displayValue}`);
3722
+ }
3723
+ }
3621
3724
  return lines.join('\n');
3622
3725
  }
3623
3726
  /**
@@ -5060,6 +5163,11 @@ The context is now within limits. Please retry your request with the recovered c
5060
5163
  this.logStatus(`📦 Collected ${refCount} media reference(s) from sub-agent '${subAgentRequest.name}'`, true);
5061
5164
  }
5062
5165
  }
5166
+ // Merge sub-agent's file outputs into parent's array for unified artifact creation.
5167
+ if (subAgentResult.fileOutputs?.length) {
5168
+ this._fileOutputs.push(...subAgentResult.fileOutputs);
5169
+ this.logStatus(`📄 Collected ${subAgentResult.fileOutputs.length} file output(s) from sub-agent '${subAgentRequest.name}'`, true);
5170
+ }
5063
5171
  // Determine if we should terminate after sub-agent
5064
5172
  const shouldTerminate = subAgentRequest.terminateAfter;
5065
5173
  // Prepare output data
@@ -5316,6 +5424,11 @@ The context is now within limits. Please retry your request with the recovered c
5316
5424
  this.logStatus(`📦 Collected ${refCount} media reference(s) from related sub-agent '${subAgentRequest.name}'`, true);
5317
5425
  }
5318
5426
  }
5427
+ // Merge sub-agent's file outputs into parent's array for unified artifact creation.
5428
+ if (subAgentResult.fileOutputs?.length) {
5429
+ this._fileOutputs.push(...subAgentResult.fileOutputs);
5430
+ this.logStatus(`📄 Collected ${subAgentResult.fileOutputs.length} file output(s) from related sub-agent '${subAgentRequest.name}'`, true);
5431
+ }
5319
5432
  let mergedPayload = previousDecision.newPayload; // Start with parent's payload
5320
5433
  let currentStepPayloadChangeResult = undefined;
5321
5434
  if (subAgentResult.success && relationship.SubAgentOutputMapping) {
@@ -5810,6 +5923,9 @@ The context is now within limits. Please retry your request with the recovered c
5810
5923
  // This prevents context overflow from base64 data (~700K tokens per 1024x1024 image)
5811
5924
  // Pass actionEntity for generic ValueType=MediaOutput detection from metadata
5812
5925
  const sanitizedParams = this.interceptLargeBinaryContent(outputParams, result.actionEntity);
5926
+ // Collect file outputs (PDF, Excel, Word, etc.) for post-run artifact processing
5927
+ const fileOutputs = this.detectFileOutputs(outputParams);
5928
+ this._fileOutputs.push(...fileOutputs);
5813
5929
  return {
5814
5930
  actionName: result.action.name,
5815
5931
  success: result.success,
@@ -6188,27 +6304,22 @@ The context is now within limits. Please retry your request with the recovered c
6188
6304
  }
6189
6305
  return null;
6190
6306
  }
6307
+ /**
6308
+ * Walks up the agent's category hierarchy looking for an AssignmentStrategy.
6309
+ * Uses AIEngine.Instance.AgentCategories (cached during engine Config).
6310
+ */
6191
6311
  async resolveCategoryAssignmentStrategy(params) {
6192
6312
  const categoryId = params.agent.CategoryID;
6193
6313
  if (!categoryId)
6194
6314
  return null;
6195
6315
  try {
6196
- // Load all categories if not cached
6197
- if (!this._categoryCache) {
6198
- const rv = new RunView();
6199
- const result = await rv.RunView({
6200
- EntityName: 'MJ: AI Agent Categories',
6201
- Fields: ['ID', 'ParentID', 'AssignmentStrategy'],
6202
- ResultType: 'simple'
6203
- }, params.contextUser);
6204
- this._categoryCache = result.Success ? result.Results : [];
6205
- }
6316
+ const categories = AIEngine.Instance.AgentCategories;
6206
6317
  // Walk up the tree from the agent's category to the root
6207
6318
  let currentId = categoryId;
6208
6319
  const visited = new Set(); // prevent infinite loops
6209
6320
  while (currentId && !visited.has(currentId)) {
6210
6321
  visited.add(currentId);
6211
- const cat = this._categoryCache.find(c => UUIDsEqual(c.ID, currentId));
6322
+ const cat = categories.find(c => UUIDsEqual(c.ID, currentId));
6212
6323
  if (!cat)
6213
6324
  break;
6214
6325
  const strategy = parseAssignmentStrategy(cat.AssignmentStrategy);
@@ -6222,6 +6333,86 @@ The context is now within limits. Please retry your request with the recovered c
6222
6333
  }
6223
6334
  return null;
6224
6335
  }
6336
+ /**
6337
+ * Resolves the file storage account ID for this agent's file artifacts.
6338
+ *
6339
+ * Resolution chain (first non-null wins):
6340
+ * 1. Runtime override (`params.override?.storageAccountId`)
6341
+ * 2. Agent-level (`params.agent.DefaultStorageAccountID`)
6342
+ * 3. Category hierarchy — walks up the agent's category tree via `ParentID`
6343
+ * 4. Agent Type-level (`agentType.DefaultStorageAccountID`)
6344
+ * 5. System fallback — single active storage account, if only one exists
6345
+ *
6346
+ * This method is `protected` so subclasses can override the resolution logic
6347
+ * for custom storage routing (e.g., routing by file type or tenant).
6348
+ *
6349
+ * @param params - The current agent execution parameters
6350
+ * @returns The resolved FileStorageAccount ID, or null if none configured
6351
+ */
6352
+ async getStorageAccountID(params) {
6353
+ // 1. Runtime override — highest priority
6354
+ if (params.override?.storageAccountId) {
6355
+ return params.override.storageAccountId;
6356
+ }
6357
+ // 2. Agent-level override
6358
+ if (params.agent.DefaultStorageAccountID) {
6359
+ return params.agent.DefaultStorageAccountID;
6360
+ }
6361
+ // 3. Category tree walk
6362
+ const categoryId = params.agent.CategoryID;
6363
+ if (categoryId) {
6364
+ try {
6365
+ const categories = AIEngine.Instance.AgentCategories;
6366
+ let currentId = categoryId;
6367
+ const visited = new Set();
6368
+ while (currentId && !visited.has(currentId)) {
6369
+ visited.add(currentId);
6370
+ const cat = categories.find(c => UUIDsEqual(c.ID, currentId));
6371
+ if (!cat)
6372
+ break;
6373
+ if (cat.DefaultStorageAccountID)
6374
+ return cat.DefaultStorageAccountID;
6375
+ currentId = cat.ParentID;
6376
+ }
6377
+ }
6378
+ catch (error) {
6379
+ LogError(`Error resolving category storage account: ${error.message}`);
6380
+ }
6381
+ }
6382
+ // 4. Agent Type-level default
6383
+ const agentTypeId = params.agent.TypeID;
6384
+ if (agentTypeId) {
6385
+ const agentType = AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, agentTypeId));
6386
+ if (agentType?.DefaultStorageAccountID) {
6387
+ return agentType.DefaultStorageAccountID;
6388
+ }
6389
+ }
6390
+ // 5. System fallback — use cached metadata (already loaded during engine Config)
6391
+ const activeAccounts = FileStorageEngineBase.Instance.AccountsWithProviders
6392
+ .filter(a => a.provider.IsActive);
6393
+ if (activeAccounts.length === 0) {
6394
+ // No storage configured — return null, inline base64 fallback handles it downstream
6395
+ return null;
6396
+ }
6397
+ if (activeAccounts.length === 1) {
6398
+ return activeAccounts[0].account.ID;
6399
+ }
6400
+ // 2+ active accounts but nothing configured at any level
6401
+ const agentName = params.agent.Name || params.agent.ID;
6402
+ const typeName = params.agent.TypeID
6403
+ ? AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, params.agent.TypeID))?.Name || params.agent.TypeID
6404
+ : 'unknown';
6405
+ const categoryName = params.agent.CategoryID
6406
+ ? AIEngine.Instance.AgentCategories.find(c => UUIDsEqual(c.ID, params.agent.CategoryID))?.Name || params.agent.CategoryID
6407
+ : 'none';
6408
+ const accountNames = activeAccounts.map(a => `'${a.account.Name}' (${a.provider.Name})`).join(', ');
6409
+ throw new Error(`Multiple active file storage accounts detected (${accountNames}) but no DefaultStorageAccountID is configured ` +
6410
+ `for agent '${agentName}', category '${categoryName}', or agent type '${typeName}'.\n` +
6411
+ `To fix: Set DefaultStorageAccountID on one of the following (in order of priority):\n` +
6412
+ ` 1. Agent '${agentName}' — for this specific agent\n` +
6413
+ ` 2. Agent Category '${categoryName}' — for all agents in this category\n` +
6414
+ ` 3. Agent Type '${typeName}' — for all agents of this type`);
6415
+ }
6225
6416
  /**
6226
6417
  * Resolves the target user ID from a resolved assignment strategy.
6227
6418
  * For simple strategies (RunUser, AgentOwner, SpecificUser), resolves immediately.
@@ -7049,7 +7240,9 @@ The context is now within limits. Please retry your request with the recovered c
7049
7240
  ? this._injectedMemory
7050
7241
  : undefined,
7051
7242
  mediaOutputs: this._mediaOutputs.length > 0 ? this._mediaOutputs : undefined,
7052
- feedbackRequestId: this._feedbackRequestId || undefined
7243
+ fileOutputs: this._fileOutputs.length > 0 ? this._fileOutputs : undefined,
7244
+ feedbackRequestId: this._feedbackRequestId || undefined,
7245
+ resolvedStorageAccountId: this._resolvedStorageAccountId || undefined
7053
7246
  };
7054
7247
  }
7055
7248
  /**