@memberjunction/ai-agents 5.22.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.
- package/README.md +4 -1
- package/dist/AgentRunner.d.ts +36 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +174 -70
- package/dist/AgentRunner.js.map +1 -1
- package/dist/ClientToolRequestManager.d.ts +62 -0
- package/dist/ClientToolRequestManager.d.ts.map +1 -0
- package/dist/ClientToolRequestManager.js +107 -0
- package/dist/ClientToolRequestManager.js.map +1 -0
- package/dist/KnowledgeAgent.d.ts +5 -3
- package/dist/KnowledgeAgent.d.ts.map +1 -1
- package/dist/KnowledgeAgent.js +49 -58
- package/dist/KnowledgeAgent.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +21 -10
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +15 -6
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +16 -2
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +28 -3
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +30 -0
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +380 -54
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
package/dist/base-agent.js
CHANGED
|
@@ -24,6 +24,7 @@ import { AgentRunner } from './AgentRunner.js';
|
|
|
24
24
|
import { PayloadManager } from './PayloadManager.js';
|
|
25
25
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
26
26
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
27
|
+
import { ClientToolRequestManager } from './ClientToolRequestManager.js';
|
|
27
28
|
import { ConversationMessageResolver } from './utils/ConversationMessageResolver.js';
|
|
28
29
|
import _ from 'lodash';
|
|
29
30
|
/**
|
|
@@ -831,13 +832,36 @@ export class BaseAgent {
|
|
|
831
832
|
this._activeProvider = params.provider || Metadata.Provider;
|
|
832
833
|
try {
|
|
833
834
|
this.logStatus(`🤖 Starting execution of agent '${params.agent.Name}'`, true, params);
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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 ---
|
|
841
865
|
// Wrap the progress callback to capture all events
|
|
842
866
|
const wrappedParams = {
|
|
843
867
|
...params,
|
|
@@ -849,12 +873,20 @@ export class BaseAgent {
|
|
|
849
873
|
}
|
|
850
874
|
// Reset scratchpad for each new execution (ephemeral per run)
|
|
851
875
|
this._scratchpadManager.Clear();
|
|
876
|
+
// Initialize starting payload — must complete before AgentRun creation since the
|
|
877
|
+
// run record stores the starting payload snapshot.
|
|
852
878
|
await this.initializeStartingPayload(wrappedParams);
|
|
853
879
|
// Check for cancellation at start
|
|
854
880
|
if (params.cancellationToken?.aborted) {
|
|
855
881
|
this.logStatus(`⚠️ Agent '${params.agent.Name}' execution cancelled before start`, true, params);
|
|
856
882
|
return await this.createCancelledResult('Cancelled before execution started', params.contextUser);
|
|
857
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
|
+
}
|
|
858
890
|
// Report initialization progress
|
|
859
891
|
wrappedParams.onProgress?.({
|
|
860
892
|
step: 'initialization',
|
|
@@ -864,31 +896,44 @@ export class BaseAgent {
|
|
|
864
896
|
hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
|
|
865
897
|
}
|
|
866
898
|
});
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
//
|
|
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)
|
|
870
926
|
this._validationRetryCount = 0;
|
|
871
927
|
this._generalValidationRetryCount = 0;
|
|
872
928
|
this._contextRecoveryAttempts = 0;
|
|
873
|
-
// Reset effective actions and dynamic limits for this run
|
|
874
929
|
this._effectiveActions = [];
|
|
875
930
|
this._dynamicActionLimits = {};
|
|
876
|
-
// Reset media outputs accumulator for this run
|
|
877
|
-
// (unified array now includes both promoted media and intercepted binary with refIds)
|
|
878
931
|
this._mediaOutputs = [];
|
|
879
|
-
// Store message lifecycle callback if provided
|
|
880
932
|
this._messageLifecycleCallback = params.onMessageLifecycle;
|
|
881
|
-
//
|
|
882
|
-
await this.initializeEngines(params.contextUser);
|
|
883
|
-
// Check for cancellation after initialization
|
|
933
|
+
// Check for cancellation after Phase 1
|
|
884
934
|
if (params.cancellationToken?.aborted) {
|
|
885
935
|
return await this.createCancelledResult('Cancelled during initialization', params.contextUser);
|
|
886
936
|
}
|
|
887
|
-
// Handle starting payload validation if configured
|
|
888
|
-
const startingValidationResult = await this.handleStartingPayloadValidation(wrappedParams);
|
|
889
|
-
if (startingValidationResult) {
|
|
890
|
-
return startingValidationResult;
|
|
891
|
-
}
|
|
892
937
|
// Report validation progress
|
|
893
938
|
wrappedParams.onProgress?.({
|
|
894
939
|
step: 'validation',
|
|
@@ -898,34 +943,26 @@ export class BaseAgent {
|
|
|
898
943
|
hierarchicalStep: this.buildHierarchicalStep(0, this._parentStepCounts)
|
|
899
944
|
}
|
|
900
945
|
});
|
|
901
|
-
//
|
|
946
|
+
// Validate agent — may return early with a failure result. Runs after engines are
|
|
947
|
+
// initialized (Phase 1) since validation may inspect AIEngine metadata.
|
|
902
948
|
const validationResult = await this.validateAgentWithTracking(params.agent, params.contextUser);
|
|
903
949
|
if (validationResult)
|
|
904
950
|
return validationResult;
|
|
905
|
-
//
|
|
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.
|
|
906
959
|
this.logStatus(`📋 Loading configuration for agent '${params.agent.Name}'`, true, params);
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
|
|
910
|
-
agent: params.agent,
|
|
911
|
-
category: 'AgentConfiguration'
|
|
912
|
-
});
|
|
913
|
-
return await this.createFailureResult(config.errorMessage || 'Failed to load agent configuration', params.contextUser);
|
|
914
|
-
}
|
|
915
|
-
// Preload agent data sources unless disabled
|
|
916
|
-
await this.preloadAgentData(wrappedParams);
|
|
917
|
-
// now initialize the agent type which gets us the instance setup in our class plus also gets the agent type to initialize
|
|
918
|
-
// its state
|
|
919
|
-
await this.initializeAgentType(wrappedParams, config);
|
|
920
|
-
// 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).
|
|
921
962
|
const userId = params.userId || params.contextUser?.ID;
|
|
922
963
|
const companyId = params.companyId;
|
|
923
|
-
|
|
924
|
-
const lastUserMessage = params.conversationMessages
|
|
925
|
-
.filter(m => m.role === 'user')
|
|
926
|
-
.pop();
|
|
964
|
+
const lastUserMessage = params.conversationMessages.filter(m => m.role === 'user').pop();
|
|
927
965
|
const inputText = lastUserMessage?.content || '';
|
|
928
|
-
// Parse agent-level scope config for note/example filtering
|
|
929
966
|
const scopeConfigJson = params.agent.ScopeConfig;
|
|
930
967
|
let scopeConfig = null;
|
|
931
968
|
if (scopeConfigJson) {
|
|
@@ -934,11 +971,9 @@ export class BaseAgent {
|
|
|
934
971
|
}
|
|
935
972
|
catch { /* ignore bad JSON */ }
|
|
936
973
|
}
|
|
937
|
-
// Resolve scope params from top-level params or data fallback (for GraphQL callers)
|
|
938
974
|
const primaryScopeEntityName = params.PrimaryScopeEntityName ?? params.data?.PrimaryScopeEntityName;
|
|
939
975
|
const primaryScopeRecordId = params.PrimaryScopeRecordID ?? params.data?.PrimaryScopeRecordID;
|
|
940
976
|
const secondaryScopes = params.SecondaryScopes ?? params.data?.SecondaryScopes;
|
|
941
|
-
// Resolve entity name to entity ID for scope filtering
|
|
942
977
|
let primaryScopeEntityId;
|
|
943
978
|
if (primaryScopeEntityName) {
|
|
944
979
|
const primaryEntity = this._metadata.Entities.find(e => e.Name === primaryScopeEntityName);
|
|
@@ -946,8 +981,22 @@ export class BaseAgent {
|
|
|
946
981
|
primaryScopeEntityId = primaryEntity.ID;
|
|
947
982
|
}
|
|
948
983
|
}
|
|
949
|
-
|
|
950
|
-
|
|
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);
|
|
951
1000
|
// Execute the agent's internal logic with wrapped parameters
|
|
952
1001
|
this.logStatus(`🚀 Executing agent '${params.agent.Name}' internal logic`, true, params);
|
|
953
1002
|
const executionResult = await this.executeAgentInternal(wrappedParams, config);
|
|
@@ -1673,6 +1722,9 @@ export class BaseAgent {
|
|
|
1673
1722
|
case 'While':
|
|
1674
1723
|
// While loops are valid - no additional validation needed
|
|
1675
1724
|
return nextStep;
|
|
1725
|
+
case 'ClientTools':
|
|
1726
|
+
// Client tools are valid - execution handled by executeClientToolsStep
|
|
1727
|
+
return nextStep;
|
|
1676
1728
|
default:
|
|
1677
1729
|
// if we get here, the next step is not recognized, we can return a retry step
|
|
1678
1730
|
this.logError(`Invalid next step '${nextStep.step}' for agent '${params.agent.Name}'`, {
|
|
@@ -2598,9 +2650,9 @@ export class BaseAgent {
|
|
|
2598
2650
|
? currentStepCount - msg.metadata.turnAdded
|
|
2599
2651
|
: 0,
|
|
2600
2652
|
tokens: this.estimateTokens(msg.content),
|
|
2601
|
-
|
|
2653
|
+
isToolResult: this.IsToolResultMessage(msg)
|
|
2602
2654
|
}))
|
|
2603
|
-
.filter(c => c.
|
|
2655
|
+
.filter(c => c.isToolResult && c.age >= minAge)
|
|
2604
2656
|
.sort((a, b) => b.age - a.age); // Oldest first
|
|
2605
2657
|
// Remove messages until we've saved enough
|
|
2606
2658
|
for (const candidate of candidates) {
|
|
@@ -2651,10 +2703,10 @@ export class BaseAgent {
|
|
|
2651
2703
|
? currentStepCount - msg.metadata.turnAdded
|
|
2652
2704
|
: 0,
|
|
2653
2705
|
tokens: this.estimateTokens(msg.content),
|
|
2654
|
-
|
|
2706
|
+
isToolResult: this.IsToolResultMessage(msg),
|
|
2655
2707
|
alreadyCompacted: msg.metadata?.wasCompacted === true
|
|
2656
2708
|
}))
|
|
2657
|
-
.filter(c => c.
|
|
2709
|
+
.filter(c => c.isToolResult && c.age >= minAge && !c.alreadyCompacted)
|
|
2658
2710
|
.sort((a, b) => b.age - a.age); // Oldest first
|
|
2659
2711
|
for (const candidate of candidates) {
|
|
2660
2712
|
if (tokensSaved >= tokensToSave)
|
|
@@ -2713,10 +2765,10 @@ export class BaseAgent {
|
|
|
2713
2765
|
message: msg,
|
|
2714
2766
|
index: index,
|
|
2715
2767
|
tokens: this.estimateTokens(msg.content),
|
|
2716
|
-
|
|
2768
|
+
isToolResult: this.IsToolResultMessage(msg),
|
|
2717
2769
|
alreadyCompacted: msg.metadata?.wasCompacted === true
|
|
2718
2770
|
}))
|
|
2719
|
-
.filter(c => c.
|
|
2771
|
+
.filter(c => c.isToolResult && !c.alreadyCompacted && c.tokens > 200)
|
|
2720
2772
|
.sort((a, b) => b.tokens - a.tokens); // Largest first
|
|
2721
2773
|
for (const candidate of candidates) {
|
|
2722
2774
|
if (tokensSaved >= tokensToSave)
|
|
@@ -3004,6 +3056,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3004
3056
|
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
3005
3057
|
const runtimePromptParamOverrides = extraData?.__agentTypePromptParams;
|
|
3006
3058
|
const agentTypePromptParams = this.buildAgentTypePromptParams(agentType, agent, runtimePromptParamOverrides);
|
|
3059
|
+
// Build client tool details for the prompt
|
|
3060
|
+
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
3061
|
+
// Build app context section if provided in extraData
|
|
3062
|
+
const appContext = this.buildAppContextSection(extraData);
|
|
3007
3063
|
const contextData = {
|
|
3008
3064
|
agentName: agent.Name,
|
|
3009
3065
|
agentDescription: agent.Description,
|
|
@@ -3012,6 +3068,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3012
3068
|
subAgentDetails: this.formatSubAgentDetails(uniqueActiveSubAgents),
|
|
3013
3069
|
actionCount: activeActions.length,
|
|
3014
3070
|
actionDetails: this.formatActionDetails(activeActions),
|
|
3071
|
+
clientToolDetails: clientToolDetails,
|
|
3072
|
+
appContext: appContext,
|
|
3015
3073
|
};
|
|
3016
3074
|
// Build the final result with __agentTypePromptParams injected
|
|
3017
3075
|
// Note: extraData can override contextData properties, but __agentTypePromptParams
|
|
@@ -3023,7 +3081,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3023
3081
|
if (extraData) {
|
|
3024
3082
|
// Spread extraData but don't let it override __agentTypePromptParams
|
|
3025
3083
|
// (which was already built with runtime overrides included)
|
|
3026
|
-
|
|
3084
|
+
// Spread extraData into the result but exclude properties that were already
|
|
3085
|
+
// processed into formatted prompt sections above. Without this exclusion,
|
|
3086
|
+
// the raw objects from extraData would overwrite the formatted markdown strings
|
|
3087
|
+
// in contextData (e.g., appContext object would replace the markdown string).
|
|
3088
|
+
const { __agentTypePromptParams: _ignored, appContext: _ignoredAppContext, ...restExtraData } = extraData;
|
|
3027
3089
|
return {
|
|
3028
3090
|
...result,
|
|
3029
3091
|
...restExtraData
|
|
@@ -3501,6 +3563,123 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3501
3563
|
return lines.join('\n');
|
|
3502
3564
|
}).join('\n\n');
|
|
3503
3565
|
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Build the client tool prompt section for system prompt injection.
|
|
3568
|
+
*
|
|
3569
|
+
* Tool sources (checked in order, all merged — first registration wins):
|
|
3570
|
+
* 1. Metadata tools from AI Agent Client Tools junction table
|
|
3571
|
+
* 2. Session-level enriched tools from ClientToolRequestManager (set by client SDK)
|
|
3572
|
+
* 3. Tools provided directly in extraData.clientTools (runtime override)
|
|
3573
|
+
*/
|
|
3574
|
+
buildClientToolPromptSection(agent, extraData) {
|
|
3575
|
+
const toolMap = new Map();
|
|
3576
|
+
// 1. Metadata tools from junction table (authoritative source)
|
|
3577
|
+
const engine = AIEngine.Instance;
|
|
3578
|
+
const metadataTools = engine.GetClientToolsForAgent(agent.ID);
|
|
3579
|
+
for (const tool of metadataTools) {
|
|
3580
|
+
toolMap.set(tool.Name, {
|
|
3581
|
+
Name: tool.Name,
|
|
3582
|
+
Description: tool.Description,
|
|
3583
|
+
InputSchema: tool.InputSchemaJSON ? JSON.parse(tool.InputSchemaJSON) : {},
|
|
3584
|
+
OutputSchema: tool.OutputSchemaJSON ? JSON.parse(tool.OutputSchemaJSON) : undefined,
|
|
3585
|
+
Category: tool.Category || undefined,
|
|
3586
|
+
DefaultTimeoutMs: tool.DefaultTimeoutMs || undefined
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
3589
|
+
// 2. Session-level enriched tools (client SDK decorated tools)
|
|
3590
|
+
const sessionID = extraData?.sessionID;
|
|
3591
|
+
if (sessionID) {
|
|
3592
|
+
for (const tool of ClientToolRequestManager.Instance.GetSessionTools(sessionID)) {
|
|
3593
|
+
if (!toolMap.has(tool.Name)) {
|
|
3594
|
+
toolMap.set(tool.Name, tool);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
// 3. Runtime extraData override
|
|
3599
|
+
if (extraData?.clientTools) {
|
|
3600
|
+
for (const tool of extraData.clientTools) {
|
|
3601
|
+
if (!toolMap.has(tool.Name)) {
|
|
3602
|
+
toolMap.set(tool.Name, tool);
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
const tools = Array.from(toolMap.values());
|
|
3607
|
+
if (tools.length === 0) {
|
|
3608
|
+
return ''; // No client tools available
|
|
3609
|
+
}
|
|
3610
|
+
const lines = [];
|
|
3611
|
+
lines.push('### Client Tools (execute in the user\'s browser)');
|
|
3612
|
+
lines.push('Client tools run in the user\'s browser and interact with the UI. Use these when you need');
|
|
3613
|
+
lines.push('to navigate the user somewhere, display a specific view, switch dashboard tabs, or show');
|
|
3614
|
+
lines.push('records. When you choose client tools, set nextStep.type to "ClientTools".');
|
|
3615
|
+
lines.push('');
|
|
3616
|
+
lines.push('NOTE: Do NOT use client tools for asking the user questions or collecting input.');
|
|
3617
|
+
lines.push('Use the "Chat" step for that. Client tools are for programmatic UI interaction only.');
|
|
3618
|
+
lines.push('');
|
|
3619
|
+
for (const tool of tools) {
|
|
3620
|
+
const categoryTag = tool.Category ? ` [${tool.Category}]` : '';
|
|
3621
|
+
lines.push(`- **${tool.Name}**${categoryTag}: ${tool.Description}`);
|
|
3622
|
+
// Show input parameters from InputSchema
|
|
3623
|
+
const props = tool.InputSchema?.properties;
|
|
3624
|
+
const required = tool.InputSchema?.required;
|
|
3625
|
+
if (props) {
|
|
3626
|
+
const paramParts = Object.entries(props).map(([name, schema]) => {
|
|
3627
|
+
const req = required?.includes(name) ? '\\*' : '';
|
|
3628
|
+
const desc = schema.description ? ` — ${schema.description}` : '';
|
|
3629
|
+
return `\`${name}\`${req}${desc}`;
|
|
3630
|
+
});
|
|
3631
|
+
lines.push(` Inputs: ${paramParts.join(', ')}`);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
return lines.join('\n');
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Build the app context section for system prompt injection.
|
|
3638
|
+
* Reads the AppContextSnapshot from extraData.appContext and formats
|
|
3639
|
+
* it as a concise markdown section the LLM can reference.
|
|
3640
|
+
*/
|
|
3641
|
+
buildAppContextSection(extraData) {
|
|
3642
|
+
const ctx = extraData?.appContext;
|
|
3643
|
+
if (!ctx)
|
|
3644
|
+
return '';
|
|
3645
|
+
const app = ctx['App'];
|
|
3646
|
+
const activeNav = ctx['ActiveNavItem'];
|
|
3647
|
+
const otherNavs = ctx['OtherNavItems'];
|
|
3648
|
+
const user = ctx['User'];
|
|
3649
|
+
if (!app?.Name)
|
|
3650
|
+
return '';
|
|
3651
|
+
const lines = [];
|
|
3652
|
+
lines.push('### Current Application Context');
|
|
3653
|
+
lines.push(`The user is currently in the **${app.Name}** application${app.Description ? ` — ${app.Description}` : ''}.`);
|
|
3654
|
+
if (activeNav?.Name) {
|
|
3655
|
+
lines.push('');
|
|
3656
|
+
lines.push(`**Active view:** ${activeNav.Name}${activeNav.Description ? ` — ${activeNav.Description}` : ''}${activeNav.ResourceType ? ` (${activeNav.ResourceType})` : ''}`);
|
|
3657
|
+
}
|
|
3658
|
+
if (otherNavs && otherNavs.length > 0) {
|
|
3659
|
+
lines.push('');
|
|
3660
|
+
lines.push('**Other views available in this app:**');
|
|
3661
|
+
for (const nav of otherNavs) {
|
|
3662
|
+
lines.push(`- ${nav.Name}${nav.Description ? ` — ${nav.Description}` : ''}`);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
if (user?.Name) {
|
|
3666
|
+
lines.push('');
|
|
3667
|
+
lines.push(`**User:** ${user.Name}${user.Roles?.length ? ` (Roles: ${user.Roles.join(', ')})` : ''}`);
|
|
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
|
+
}
|
|
3681
|
+
return lines.join('\n');
|
|
3682
|
+
}
|
|
3504
3683
|
/**
|
|
3505
3684
|
* Formats a single action parameter as a compact inline string.
|
|
3506
3685
|
* Uses \* suffix for required, (array) when IsArray, and only shows
|
|
@@ -4247,6 +4426,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4247
4426
|
return await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
4248
4427
|
case 'Actions':
|
|
4249
4428
|
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount);
|
|
4429
|
+
// Type assertion required because 'ClientTools' is not yet in the DB StepType value list.
|
|
4430
|
+
// The LoopAgentType.DetermineNextStep() emits this value when the LLM chooses client tools.
|
|
4431
|
+
case 'ClientTools':
|
|
4432
|
+
return await this.executeClientToolsStep(params, config, previousDecision, stepCount);
|
|
4250
4433
|
case 'Chat':
|
|
4251
4434
|
return await this.executeChatStep(params, previousDecision);
|
|
4252
4435
|
case 'Success':
|
|
@@ -4620,6 +4803,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4620
4803
|
else if (updatedNextStep.step === 'Success' || updatedNextStep.step === 'Failed') {
|
|
4621
4804
|
return { ...updatedNextStep, terminate: true };
|
|
4622
4805
|
}
|
|
4806
|
+
else if (updatedNextStep.step === 'ClientTools') {
|
|
4807
|
+
// ClientTools must return terminate: false so the main loop continues
|
|
4808
|
+
// to the next iteration where executeClientToolsStep actually dispatches
|
|
4809
|
+
// and awaits the tool. The LLM's original terminate intent is preserved in
|
|
4810
|
+
// terminateAfterExecution so executeClientToolsStep can honor it post-execution.
|
|
4811
|
+
return {
|
|
4812
|
+
...updatedNextStep,
|
|
4813
|
+
terminate: false,
|
|
4814
|
+
terminateAfterExecution: updatedNextStep.terminate
|
|
4815
|
+
};
|
|
4816
|
+
}
|
|
4623
4817
|
else {
|
|
4624
4818
|
return { ...updatedNextStep, terminate: false };
|
|
4625
4819
|
}
|
|
@@ -5821,6 +6015,130 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5821
6015
|
*
|
|
5822
6016
|
* @private
|
|
5823
6017
|
*/
|
|
6018
|
+
// ================================================================
|
|
6019
|
+
// Client Tools Step Execution
|
|
6020
|
+
// ================================================================
|
|
6021
|
+
/**
|
|
6022
|
+
* Execute client-side tools requested by the agent.
|
|
6023
|
+
* Sends each tool invocation via PubSub, awaits the client's response
|
|
6024
|
+
* (or timeout), then adds results to the conversation and continues.
|
|
6025
|
+
*/
|
|
6026
|
+
async executeClientToolsStep(params, config, previousDecision, stepCount = 0) {
|
|
6027
|
+
const clientTools = previousDecision.clientTools ?? [];
|
|
6028
|
+
if (clientTools.length === 0) {
|
|
6029
|
+
// No tools to execute — continue with next prompt
|
|
6030
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
6031
|
+
}
|
|
6032
|
+
if (!params.sessionID) {
|
|
6033
|
+
// No session ID — can't communicate with client
|
|
6034
|
+
const errorMsg = 'Cannot execute client tools: no sessionID provided in ExecuteAgentParams';
|
|
6035
|
+
LogError(errorMsg);
|
|
6036
|
+
params.conversationMessages.push({
|
|
6037
|
+
role: 'user',
|
|
6038
|
+
content: `Client tool execution skipped: ${errorMsg}`
|
|
6039
|
+
});
|
|
6040
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
6041
|
+
}
|
|
6042
|
+
const currentPayload = previousDecision?.newPayload || previousDecision?.previousPayload || params.payload;
|
|
6043
|
+
// Build assistant message describing the tool invocations
|
|
6044
|
+
const toolMessage = clientTools.length === 1
|
|
6045
|
+
? `I'm invoking the **${clientTools[0].Name}** client tool${clientTools[0].Description ? ` — ${clientTools[0].Description}` : ''}.`
|
|
6046
|
+
: `I'm invoking **${clientTools.length} client tools**:\n\n` +
|
|
6047
|
+
clientTools.map((t, i) => `${i + 1}. **${t.Name}**${t.Description ? ` — ${t.Description}` : ''}`).join('\n');
|
|
6048
|
+
params.conversationMessages.push({
|
|
6049
|
+
role: 'assistant',
|
|
6050
|
+
content: toolMessage
|
|
6051
|
+
});
|
|
6052
|
+
// Report progress
|
|
6053
|
+
params.onProgress?.({
|
|
6054
|
+
step: 'action_execution', // Reuse action_execution step type for progress reporting
|
|
6055
|
+
message: this.formatHierarchicalMessage(toolMessage),
|
|
6056
|
+
metadata: {
|
|
6057
|
+
toolCount: clientTools.length,
|
|
6058
|
+
toolNames: clientTools.map(t => t.Name),
|
|
6059
|
+
stepCount: stepCount + 1,
|
|
6060
|
+
hierarchicalStep: this.buildHierarchicalStep(stepCount + 1, this._parentStepCounts)
|
|
6061
|
+
},
|
|
6062
|
+
displayMode: 'live'
|
|
6063
|
+
});
|
|
6064
|
+
// Resolve default timeout: per-tool > params > agent config > 30s
|
|
6065
|
+
const defaultTimeout = params.clientToolTimeoutMs ?? 30_000;
|
|
6066
|
+
const results = [];
|
|
6067
|
+
const agentRunID = this._agentRun?.ID ?? 'unknown';
|
|
6068
|
+
// Execute tools sequentially (client may not support parallel UI operations)
|
|
6069
|
+
for (const tool of clientTools) {
|
|
6070
|
+
const stepEntity = await this.createStepEntity({
|
|
6071
|
+
// INTENTIONAL: We use 'Actions' as the DB step type because the MJ: AI Agent Run Steps
|
|
6072
|
+
// entity's StepType value list does not yet include 'ClientTools'. A future database
|
|
6073
|
+
// migration will add 'ClientTools' to the allowed values in the StepType CHECK constraint
|
|
6074
|
+
// and CodeGen will regenerate the types. Until then, client tool steps are recorded under
|
|
6075
|
+
// 'Actions' in the run history. The step name ("Client Tool: {name}") distinguishes them.
|
|
6076
|
+
stepType: 'Actions',
|
|
6077
|
+
stepName: `Client Tool: ${tool.Name}`,
|
|
6078
|
+
inputData: { toolName: tool.Name, params: tool.Params },
|
|
6079
|
+
contextUser: params.contextUser,
|
|
6080
|
+
payloadAtStart: currentPayload,
|
|
6081
|
+
payloadAtEnd: currentPayload
|
|
6082
|
+
});
|
|
6083
|
+
const timeoutMs = tool.TimeoutMs ?? defaultTimeout;
|
|
6084
|
+
const response = await ClientToolRequestManager.Instance.RequestClientTool(`ct_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, tool.Name, tool.Params, params.sessionID, agentRunID, timeoutMs, tool.Description);
|
|
6085
|
+
await this.finalizeStepEntity(stepEntity, response.Success, response.ErrorMessage, { result: response.Result });
|
|
6086
|
+
results.push({
|
|
6087
|
+
ToolName: tool.Name,
|
|
6088
|
+
Success: response.Success,
|
|
6089
|
+
Result: response.Result,
|
|
6090
|
+
ErrorMessage: response.ErrorMessage
|
|
6091
|
+
});
|
|
6092
|
+
}
|
|
6093
|
+
// Format results as conversation message
|
|
6094
|
+
const resultsMarkdown = this.formatClientToolResultsAsMarkdown(results);
|
|
6095
|
+
params.conversationMessages.push({
|
|
6096
|
+
role: 'user',
|
|
6097
|
+
content: resultsMarkdown,
|
|
6098
|
+
metadata: {
|
|
6099
|
+
turnAdded: this._promptTurnCount,
|
|
6100
|
+
messageType: 'client-tool-result'
|
|
6101
|
+
}
|
|
6102
|
+
});
|
|
6103
|
+
// If the LLM already declared taskComplete=true alongside the client tools,
|
|
6104
|
+
// honor that intent now that tools have executed — no need for another LLM call.
|
|
6105
|
+
if (previousDecision.terminateAfterExecution) {
|
|
6106
|
+
return {
|
|
6107
|
+
step: 'Success',
|
|
6108
|
+
terminate: true,
|
|
6109
|
+
message: previousDecision.message || 'Client tools executed successfully.',
|
|
6110
|
+
payloadChangeRequest: previousDecision.payloadChangeRequest,
|
|
6111
|
+
scratchpad: previousDecision.scratchpad,
|
|
6112
|
+
responseForm: previousDecision.responseForm,
|
|
6113
|
+
actionableCommands: previousDecision.actionableCommands,
|
|
6114
|
+
automaticCommands: previousDecision.automaticCommands
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
6118
|
+
}
|
|
6119
|
+
/**
|
|
6120
|
+
* Format client tool results as a compact markdown summary for the conversation.
|
|
6121
|
+
*/
|
|
6122
|
+
formatClientToolResultsAsMarkdown(results) {
|
|
6123
|
+
const failedCount = results.filter(r => !r.Success).length;
|
|
6124
|
+
const header = failedCount > 0
|
|
6125
|
+
? `${failedCount} of ${results.length} client tool(s) failed:`
|
|
6126
|
+
: 'Client tool results:';
|
|
6127
|
+
const lines = results.map(r => {
|
|
6128
|
+
const icon = r.Success ? '✓' : '✗';
|
|
6129
|
+
let line = `${icon} **${r.ToolName}**: ${r.Success ? 'succeeded' : 'failed'}`;
|
|
6130
|
+
if (r.ErrorMessage)
|
|
6131
|
+
line += ` — ${r.ErrorMessage}`;
|
|
6132
|
+
if (r.Success && r.Result != null) {
|
|
6133
|
+
const resultStr = typeof r.Result === 'string' ? r.Result : JSON.stringify(r.Result);
|
|
6134
|
+
if (resultStr.length <= 500) {
|
|
6135
|
+
line += `\n Result: ${resultStr}`;
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
return line;
|
|
6139
|
+
});
|
|
6140
|
+
return `${header}\n${lines.join('\n')}`;
|
|
6141
|
+
}
|
|
5824
6142
|
async executeChatStep(params, previousDecision) {
|
|
5825
6143
|
const stepEntity = await this.createStepEntity({ stepType: 'Chat', stepName: 'User Interaction', contextUser: params.contextUser });
|
|
5826
6144
|
// Chat steps are successful - they indicate a need for user interaction
|
|
@@ -7165,6 +7483,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7165
7483
|
* @param modelName - Optional model name for accurate tokenization
|
|
7166
7484
|
* @protected
|
|
7167
7485
|
*/
|
|
7486
|
+
/**
|
|
7487
|
+
* Returns true if the message is a tool result (action or client tool).
|
|
7488
|
+
* Used by recovery strategies to identify compactable result messages.
|
|
7489
|
+
*/
|
|
7490
|
+
IsToolResultMessage(msg) {
|
|
7491
|
+
const messageType = msg.metadata?.messageType;
|
|
7492
|
+
return messageType === 'action-result' || messageType === 'client-tool-result';
|
|
7493
|
+
}
|
|
7168
7494
|
estimateTokens(content, modelName) {
|
|
7169
7495
|
const text = typeof content === 'string'
|
|
7170
7496
|
? content
|