@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.
- package/README.md +32 -0
- package/dist/AgentRunner.d.ts +58 -3
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +348 -62
- package/dist/AgentRunner.js.map +1 -1
- package/dist/base-agent.d.ts +48 -3
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +260 -67
- package/dist/base-agent.js.map +1 -1
- package/package.json +13 -13
package/dist/base-agent.js
CHANGED
|
@@ -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
|
-
//
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
//
|
|
869
|
-
|
|
870
|
-
//
|
|
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
|
-
//
|
|
883
|
-
await this.
|
|
884
|
-
// Check for cancellation after
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
909
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
/**
|