@memberjunction/ai-agents 5.23.0 → 5.24.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.
@@ -832,13 +832,36 @@ export class BaseAgent {
832
832
  this._activeProvider = params.provider || Metadata.Provider;
833
833
  try {
834
834
  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
- }
835
+ // =====================================================================================
836
+ // LATENCY OPTIMIZATION (Opt #4): Parallelized initialization sequence.
837
+ //
838
+ // The agent initialization pipeline was originally fully sequential: each operation
839
+ // awaited before the next began, even when there were no data dependencies between
840
+ // them. This added ~150-200ms of unnecessary serial wait time.
841
+ //
842
+ // The restructured pipeline uses 4 phases:
843
+ //
844
+ // PRE-WORK (sync/fast): Parameter wrapping, markup conversion, payload init,
845
+ // cancellation check, payload validation (may exit early).
846
+ //
847
+ // PHASE 1 (parallel): Permission check + Engine init + AgentRun creation.
848
+ // These three are mutually independent. If permission fails, we mark the
849
+ // already-created AgentRun as failed and terminate — the wasted run record
850
+ // is a negligible cost vs. the ~150ms saved by not serializing these.
851
+ //
852
+ // PHASE 2 (parallel): Config load + Data preload + Context memory injection.
853
+ // All three depend on Phase 1 completing (engines loaded, agentRun exists)
854
+ // but NOT on each other. Config load reads from AIEngine's in-memory cache.
855
+ // Data preload fetches agent data sources. Context memory loads notes/examples
856
+ // and injects them into the conversation messages.
857
+ //
858
+ // PHASE 3 (sequential): Agent type initialization — must wait for config from
859
+ // Phase 2 because it needs the resolved agent type and prompt configuration.
860
+ //
861
+ // Original total init time: ~sum of all operations (~400-500ms)
862
+ // Optimized: ~max(Phase1) + max(Phase2) + Phase3 (~200-300ms)
863
+ // =====================================================================================
864
+ // --- PRE-WORK: Fast synchronous setup and early-exit checks ---
842
865
  // Wrap the progress callback to capture all events
843
866
  const wrappedParams = {
844
867
  ...params,
@@ -850,12 +873,20 @@ export class BaseAgent {
850
873
  }
851
874
  // Reset scratchpad for each new execution (ephemeral per run)
852
875
  this._scratchpadManager.Clear();
876
+ // Initialize starting payload — must complete before AgentRun creation since the
877
+ // run record stores the starting payload snapshot.
853
878
  await this.initializeStartingPayload(wrappedParams);
854
879
  // Check for cancellation at start
855
880
  if (params.cancellationToken?.aborted) {
856
881
  this.logStatus(`⚠️ Agent '${params.agent.Name}' execution cancelled before start`, true, params);
857
882
  return await this.createCancelledResult('Cancelled before execution started', params.contextUser);
858
883
  }
884
+ // Handle starting payload validation if configured — may return early with a
885
+ // validation failure result, so we run this before launching expensive parallel work.
886
+ const startingValidationResult = await this.handleStartingPayloadValidation(wrappedParams);
887
+ if (startingValidationResult) {
888
+ return startingValidationResult;
889
+ }
859
890
  // Report initialization progress
860
891
  wrappedParams.onProgress?.({
861
892
  step: 'initialization',
@@ -865,31 +896,44 @@ export class BaseAgent {
865
896
  hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
866
897
  }
867
898
  });
868
- // Initialize execution tracking
869
- await this.initializeAgentRun(wrappedParams);
870
- // Reset validation retry counters for this run
899
+ // --- PHASE 1: Permission check + Engine init + AgentRun creation (parallel) ---
900
+ // These three operations have zero data dependencies on each other:
901
+ // - Permission check queries the AIAgentPermission entity
902
+ // - Engine init calls AIEngine.Instance.Config() and ActionEngineServer.Instance.Config()
903
+ // - AgentRun creation inserts a new AIAgentRun record
904
+ //
905
+ // If permission check fails, we mark the AgentRun as failed. This is acceptable:
906
+ // an orphaned "failed" run record is harmless and far cheaper than serializing these
907
+ // three operations (~150ms savings).
908
+ const [canRun] = await Promise.all([
909
+ AIAgentPermissionHelper.HasPermission(params.agent.ID, params.contextUser, 'run'),
910
+ this.initializeEngines(params.contextUser),
911
+ this.initializeAgentRun(wrappedParams)
912
+ ]);
913
+ if (!canRun) {
914
+ // Permission denied — mark the already-created AgentRun as failed so it doesn't
915
+ // appear as a phantom "running" record in the UI.
916
+ const errorMessage = `User ${params.contextUser.Email} does not have permission to run agent '${params.agent.Name}' (ID: ${params.agent.ID})`;
917
+ this.logStatus(`🚫 ${errorMessage}`, false, params);
918
+ if (this._agentRun) {
919
+ this._agentRun.Status = 'Failed';
920
+ this._agentRun.ErrorMessage = errorMessage;
921
+ await this._agentRun.Save();
922
+ }
923
+ throw new Error(errorMessage);
924
+ }
925
+ // Reset per-run state (sync, instant — no parallelization needed)
871
926
  this._validationRetryCount = 0;
872
927
  this._generalValidationRetryCount = 0;
873
928
  this._contextRecoveryAttempts = 0;
874
- // Reset effective actions and dynamic limits for this run
875
929
  this._effectiveActions = [];
876
930
  this._dynamicActionLimits = {};
877
- // Reset media outputs accumulator for this run
878
- // (unified array now includes both promoted media and intercepted binary with refIds)
879
931
  this._mediaOutputs = [];
880
- // Store message lifecycle callback if provided
881
932
  this._messageLifecycleCallback = params.onMessageLifecycle;
882
- // Initialize engines
883
- await this.initializeEngines(params.contextUser);
884
- // Check for cancellation after initialization
933
+ // Check for cancellation after Phase 1
885
934
  if (params.cancellationToken?.aborted) {
886
935
  return await this.createCancelledResult('Cancelled during initialization', params.contextUser);
887
936
  }
888
- // Handle starting payload validation if configured
889
- const startingValidationResult = await this.handleStartingPayloadValidation(wrappedParams);
890
- if (startingValidationResult) {
891
- return startingValidationResult;
892
- }
893
937
  // Report validation progress
894
938
  wrappedParams.onProgress?.({
895
939
  step: 'validation',
@@ -899,34 +943,26 @@ export class BaseAgent {
899
943
  hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
900
944
  }
901
945
  });
902
- // Create and track validation step
946
+ // Validate agent — may return early with a failure result. Runs after engines are
947
+ // initialized (Phase 1) since validation may inspect AIEngine metadata.
903
948
  const validationResult = await this.validateAgentWithTracking(params.agent, params.contextUser);
904
949
  if (validationResult)
905
950
  return validationResult;
906
- // Load agent configuration
951
+ // --- PHASE 2: Config load + Data preload + Context memory injection (parallel) ---
952
+ // All three depend on Phase 1 completing (engines initialized, agentRun exists) but
953
+ // have no dependencies on each other:
954
+ // - Config load reads agent type, prompts, and agent prompts from AIEngine's in-memory
955
+ // cache — typically < 5ms but async due to potential Config() refresh.
956
+ // - Data preload fetches agent data sources and creates a tracking step entity.
957
+ // - Context memory resolves scope configuration (pure computation), then loads notes
958
+ // and examples from the DB and injects them into conversation messages.
907
959
  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
960
+ // Pre-compute scope configuration for context memory injection (pure computation,
961
+ // no I/O — safe to do before launching the parallel phase).
922
962
  const userId = params.userId || params.contextUser?.ID;
923
963
  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();
964
+ const lastUserMessage = params.conversationMessages.filter(m => m.role === 'user').pop();
928
965
  const inputText = lastUserMessage?.content || '';
929
- // Parse agent-level scope config for note/example filtering
930
966
  const scopeConfigJson = params.agent.ScopeConfig;
931
967
  let scopeConfig = null;
932
968
  if (scopeConfigJson) {
@@ -935,11 +971,9 @@ export class BaseAgent {
935
971
  }
936
972
  catch { /* ignore bad JSON */ }
937
973
  }
938
- // Resolve scope params from top-level params or data fallback (for GraphQL callers)
939
974
  const primaryScopeEntityName = params.PrimaryScopeEntityName ?? params.data?.PrimaryScopeEntityName;
940
975
  const primaryScopeRecordId = params.PrimaryScopeRecordID ?? params.data?.PrimaryScopeRecordID;
941
976
  const secondaryScopes = params.SecondaryScopes ?? params.data?.SecondaryScopes;
942
- // Resolve entity name to entity ID for scope filtering
943
977
  let primaryScopeEntityId;
944
978
  if (primaryScopeEntityName) {
945
979
  const primaryEntity = this._metadata.Entities.find(e => e.Name === primaryScopeEntityName);
@@ -947,8 +981,22 @@ export class BaseAgent {
947
981
  primaryScopeEntityId = primaryEntity.ID;
948
982
  }
949
983
  }
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);
984
+ const [config] = await Promise.all([
985
+ this.loadAgentConfiguration(params.agent),
986
+ this.preloadAgentData(wrappedParams),
987
+ this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig)
988
+ ]);
989
+ if (!config.success) {
990
+ this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
991
+ agent: params.agent,
992
+ category: 'AgentConfiguration'
993
+ });
994
+ return await this.createFailureResult(config.errorMessage || 'Failed to load agent configuration', params.contextUser);
995
+ }
996
+ // --- PHASE 3: Agent type initialization (sequential) ---
997
+ // Must wait for config from Phase 2 because it needs the resolved agent type and
998
+ // prompt configuration to initialize the type-specific state machine.
999
+ await this.initializeAgentType(wrappedParams, config);
952
1000
  // Execute the agent's internal logic with wrapped parameters
953
1001
  this.logStatus(`🚀 Executing agent '${params.agent.Name}' internal logic`, true, params);
954
1002
  const executionResult = await this.executeAgentInternal(wrappedParams, config);
@@ -3618,6 +3666,18 @@ The context is now within limits. Please retry your request with the recovered c
3618
3666
  lines.push('');
3619
3667
  lines.push(`**User:** ${user.Name}${user.Roles?.length ? ` (Roles: ${user.Roles.join(', ')})` : ''}`);
3620
3668
  }
3669
+ // Additional context reported by the active view/component
3670
+ const dashboardCtx = ctx['AdditionalContext'];
3671
+ if (dashboardCtx && Object.keys(dashboardCtx).length > 0) {
3672
+ lines.push('');
3673
+ lines.push('**Dashboard state:**');
3674
+ for (const [key, value] of Object.entries(dashboardCtx)) {
3675
+ if (value === null || value === undefined)
3676
+ continue;
3677
+ const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
3678
+ lines.push(`- ${key}: ${displayValue}`);
3679
+ }
3680
+ }
3621
3681
  return lines.join('\n');
3622
3682
  }
3623
3683
  /**