@memberjunction/ai-agents 5.20.0 → 5.22.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.
@@ -81,6 +81,13 @@ export class BaseAgent {
81
81
  * @private
82
82
  */
83
83
  this._metadata = new Metadata();
84
+ /**
85
+ * Active per-request metadata provider, set at the start of Execute().
86
+ * Defaults to the global Metadata.Provider; overridden when a per-request
87
+ * provider is passed through ExecuteAgentParams.provider for server isolation.
88
+ * @private
89
+ */
90
+ this._activeProvider = Metadata.Provider;
84
91
  /**
85
92
  * This is state information that is specific to the agent type. BaseAgent doesn't know what
86
93
  * this contains or care, it is just responsible for keeping this, giving the Agent Type the
@@ -173,6 +180,17 @@ export class BaseAgent {
173
180
  * @since 2.123.0
174
181
  */
175
182
  this._effectiveActions = [];
183
+ /**
184
+ * Counts only prompt (LLM) executions, NOT all agent steps.
185
+ * Used for message expiration age calculations so that `expirationTurns`
186
+ * semantically means "number of LLM calls" rather than "number of steps"
187
+ * (which includes actions, loops, sub-agents, etc.).
188
+ *
189
+ * Without this, a ForEach loop over 100 items would bump the step counter
190
+ * by 100, causing messages to expire far too early even though only one
191
+ * prompt execution occurred after them.
192
+ */
193
+ this._promptTurnCount = 0;
176
194
  /**
177
195
  * Execution limits for dynamically added actions.
178
196
  * Maps action IDs to their MaxExecutionsPerRun limit.
@@ -808,6 +826,9 @@ export class BaseAgent {
808
826
  * ```
809
827
  */
810
828
  async Execute(params) {
829
+ // Capture per-request provider for the duration of this execution so all entity
830
+ // saves go through the isolated provider, never the global singleton's transaction.
831
+ this._activeProvider = params.provider || Metadata.Provider;
811
832
  try {
812
833
  this.logStatus(`🤖 Starting execution of agent '${params.agent.Name}'`, true, params);
813
834
  // Check permissions - user must have run permission or be the owner
@@ -1003,8 +1024,10 @@ export class BaseAgent {
1003
1024
  if (params.cancellationToken?.aborted) {
1004
1025
  throw new Error('Cancelled during execution');
1005
1026
  }
1006
- // Prune and compact expired messages before executing the next step
1007
- await this.pruneAndCompactExpiredMessages(params, stepCount);
1027
+ // Prune and compact expired messages before executing the next step.
1028
+ // Uses _promptTurnCount (not stepCount) so expiration is measured in prompt
1029
+ // executions, not all steps (which include actions, loops, sub-agents, etc.)
1030
+ await this.pruneAndCompactExpiredMessages(params, this._promptTurnCount);
1008
1031
  // Execute the current step based on previous decision or initial prompt
1009
1032
  this.logStatus(`🔄 Executing step ${stepCount + 1} for agent '${params.agent.Name}'`, true, params);
1010
1033
  const nextStep = await this.executeNextStep(params, config, currentNextStep, stepCount);
@@ -1584,6 +1607,8 @@ export class BaseAgent {
1584
1607
  promptParams.apiKeys = params.apiKeys;
1585
1608
  this.logStatus(`🔑 Using ${params.apiKeys.length} API key(s) provided at runtime`, true, params);
1586
1609
  }
1610
+ // Thread the per-request provider so prompt run records are saved through the isolated provider
1611
+ promptParams.provider = params.provider || this._activeProvider;
1587
1612
  return promptParams;
1588
1613
  }
1589
1614
  /**
@@ -2815,13 +2840,13 @@ export class BaseAgent {
2815
2840
  newPayload: payload
2816
2841
  };
2817
2842
  }
2818
- // Get current step count for age calculations
2819
- const currentStepCount = this._agentRun?.Steps?.length || 0;
2843
+ // Use prompt turn count for age calculations (not step count)
2844
+ const currentPromptTurn = this._promptTurnCount;
2820
2845
  // Try multiple recovery strategies in order
2821
2846
  const strategies = [
2822
- () => this.recoveryStrategy_RemoveOldestActionResults(params, tokensToSave, currentStepCount, 5),
2823
- () => this.recoveryStrategy_CompactOldActionResults(params, tokensToSave, currentStepCount, 3),
2824
- () => this.recoveryStrategy_RemoveOldestActionResults(params, tokensToSave, currentStepCount, 2),
2847
+ () => this.recoveryStrategy_RemoveOldestActionResults(params, tokensToSave, currentPromptTurn, 5),
2848
+ () => this.recoveryStrategy_CompactOldActionResults(params, tokensToSave, currentPromptTurn, 3),
2849
+ () => this.recoveryStrategy_RemoveOldestActionResults(params, tokensToSave, currentPromptTurn, 2),
2825
2850
  () => this.recoveryStrategy_CompactAllActionResults(params, tokensToSave),
2826
2851
  () => Promise.resolve(this.recoveryStrategy_TrimLastUserMessage(params, tokensToSave))
2827
2852
  ];
@@ -3314,8 +3339,8 @@ The context is now within limits. Please retry your request with the recovered c
3314
3339
  async ExecuteSubAgent(params, subAgentRequest, subAgent, stepEntity, payload, contextMessage, stepCount = 0) {
3315
3340
  try {
3316
3341
  this.logStatus(`🤖 Executing sub-agent '${subAgentRequest.name}'`, true, params);
3317
- // Create a new AgentRunner instance
3318
- const runner = new AgentRunner();
3342
+ // Create a new AgentRunner instance with the same isolated provider
3343
+ const runner = new AgentRunner(params.provider || this._activeProvider);
3319
3344
  // Prepare messages for sub-agent using database-configured message mode
3320
3345
  const subAgentMessages = this.prepareSubAgentMessages(params, subAgentRequest, subAgent, contextMessage);
3321
3346
  // Set parent run ID in the sub-agent's execution
@@ -3561,18 +3586,15 @@ The context is now within limits. Please retry your request with the recovered c
3561
3586
  if (!result.success && result.agentRun?.ErrorMessage) {
3562
3587
  lines.push(`**Error:** ${result.agentRun.ErrorMessage}`);
3563
3588
  }
3564
- // Payload — the actual result data the parent agent cares about
3589
+ // Payload — the actual result data the parent agent cares about.
3590
+ // Not truncated by default — the expiration/compaction lifecycle handles
3591
+ // context window management. Truncating here permanently discards data
3592
+ // before the LLM ever sees it.
3565
3593
  if (result.payload != null) {
3566
3594
  const payloadStr = typeof result.payload === 'string'
3567
3595
  ? result.payload
3568
3596
  : JSON.stringify(result.payload);
3569
- // For very large payloads, truncate to keep context manageable
3570
- if (payloadStr.length > 4000) {
3571
- lines.push(`**Payload** (truncated):\n${payloadStr.substring(0, 4000)}…`);
3572
- }
3573
- else {
3574
- lines.push(`**Payload:**\n${payloadStr}`);
3575
- }
3597
+ lines.push(`**Payload:**\n${payloadStr}`);
3576
3598
  }
3577
3599
  return lines.join('\n');
3578
3600
  }
@@ -3580,14 +3602,19 @@ The context is now within limits. Please retry your request with the recovered c
3580
3602
  * Formats a single output parameter value for inclusion in action result
3581
3603
  * markdown. Scalars are shown inline with backtick formatting; objects and
3582
3604
  * arrays use compact (single-line) JSON to avoid the indentation overhead
3583
- * of pretty-printed JSON. Very long values are truncated.
3605
+ * of pretty-printed JSON.
3606
+ *
3607
+ * By default, values are **not truncated** — the expiration/compaction lifecycle
3608
+ * handles context window management. Truncating at formatting time permanently
3609
+ * discards data before the LLM ever sees it. Pass an explicit maxLength only
3610
+ * when you have a specific reason to limit output size.
3584
3611
  *
3585
3612
  * @param value - The parameter value (any type)
3586
- * @param maxLength - Maximum character length before truncation (default 500)
3613
+ * @param maxLength - Maximum character length before truncation (0 = no limit, default 0)
3587
3614
  * @returns Formatted string
3588
3615
  * @private
3589
3616
  */
3590
- formatParamValueForResult(value, maxLength = 500) {
3617
+ formatParamValueForResult(value, maxLength = 0) {
3591
3618
  if (value === null || value === undefined) {
3592
3619
  return '`null`';
3593
3620
  }
@@ -3602,7 +3629,7 @@ The context is now within limits. Please retry your request with the recovered c
3602
3629
  // Compact JSON (no pretty-printing) for objects/arrays
3603
3630
  stringValue = JSON.stringify(value);
3604
3631
  }
3605
- if (stringValue.length > maxLength) {
3632
+ if (maxLength > 0 && stringValue.length > maxLength) {
3606
3633
  return `${stringValue.substring(0, maxLength)}…`;
3607
3634
  }
3608
3635
  return stringValue;
@@ -3816,8 +3843,10 @@ The context is now within limits. Please retry your request with the recovered c
3816
3843
  }
3817
3844
  }
3818
3845
  }
3846
+ // Reset prompt turn counter for this execution
3847
+ this._promptTurnCount = 0;
3819
3848
  // Create MJAIAgentRunEntity
3820
- this._agentRun = await this._metadata.GetEntityObject('MJ: AI Agent Runs', params.contextUser);
3849
+ this._agentRun = await (params.provider || this._activeProvider).GetEntityObject('MJ: AI Agent Runs', params.contextUser);
3821
3850
  this._agentRun.AgentID = params.agent.ID;
3822
3851
  if (params.conversationDetailId) {
3823
3852
  this._agentRun.ConversationDetailID = params.conversationDetailId;
@@ -3970,7 +3999,7 @@ The context is now within limits. Please retry your request with the recovered c
3970
3999
  * @returns {Promise<MJAIAgentRunStepEntityExtended>} - The created step entity
3971
4000
  */
3972
4001
  async createStepEntity(params) {
3973
- const stepEntity = await this._metadata.GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
4002
+ const stepEntity = await this._activeProvider.GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
3974
4003
  stepEntity.AgentRunID = this._agentRun.ID;
3975
4004
  // Step number is based on current count of steps + 1
3976
4005
  stepEntity.StepNumber = (this._agentRun.Steps?.length || 0) + 1;
@@ -4211,8 +4240,7 @@ The context is now within limits. Please retry your request with the recovered c
4211
4240
  // Check if this is a message expansion request
4212
4241
  if (previousDecision.messageIndex !== undefined) {
4213
4242
  // Handle message expansion before retrying
4214
- const currentStepCount = this._agentRun?.Steps?.length || 0;
4215
- this.executeExpandMessageStep(previousDecision, params, currentStepCount);
4243
+ this.executeExpandMessageStep(previousDecision, params, this._promptTurnCount);
4216
4244
  }
4217
4245
  return await this.executePromptStep(params, config, previousDecision, stepCount);
4218
4246
  case 'Sub-Agent':
@@ -4420,12 +4448,11 @@ The context is now within limits. Please retry your request with the recovered c
4420
4448
  };
4421
4449
  // Execute the prompt
4422
4450
  const promptResult = await this.executePrompt(promptParams);
4423
- // Remove temporary messages before processing prompt results
4424
- // This includes loop results and sub-agent completion messages
4425
- params.conversationMessages = params.conversationMessages.filter(m => {
4426
- const metadata = m.metadata;
4427
- return !metadata?._loopResults && !metadata?._subAgentResult;
4428
- });
4451
+ // Increment prompt-specific turn counter (used for expiration age calculations)
4452
+ this._promptTurnCount++;
4453
+ // Loop and sub-agent results now use the standard expiration/compaction lifecycle
4454
+ // (messageType: 'loop-result' / 'sub-agent-result') instead of being deleted after
4455
+ // one prompt turn. The processMessageExpiration() method handles their lifecycle.
4429
4456
  // Update step entity with AIPromptRun ID if available
4430
4457
  if (promptResult.promptRun?.ID) {
4431
4458
  stepEntity.TargetLogID = promptResult.promptRun.ID;
@@ -4940,31 +4967,33 @@ The context is now within limits. Please retry your request with the recovered c
4940
4967
  // This ensures parent agents can override child Chat steps
4941
4968
  return await this.validateChatNextStep(params, chatStep, mergedPayload, this._agentRun, stepEntity);
4942
4969
  }
4943
- // Build a clean summary of sub-agent result
4944
- const subAgentSummary = {
4945
- agentName: params.agent.Name,
4970
+ // Add user message with the sub-agent results using markdown format
4971
+ // and expiration metadata so it persists across multiple prompt turns
4972
+ const resultMessage = this.formatSubAgentResultAsMarkdown(subAgentRequest.name, subAgentResult);
4973
+ const subAgentMetadata = {
4974
+ turnAdded: this._promptTurnCount,
4975
+ messageType: 'sub-agent-result',
4946
4976
  subAgentName: subAgentRequest.name,
4947
4977
  subAgentId: subAgentEntity.ID,
4948
- success: subAgentResult.success,
4949
- finalStep: subAgentResult.agentRun?.FinalStep,
4950
- errorMessage: subAgentResult.agentRun?.ErrorMessage || null
4951
- // do NOT include payload here as this goes to the LLM and
4952
- // we don't need that there, too many tokens and LLM already gets
4953
- // payload the normal way
4978
+ // Default: keep sub-agent results for 3 turns then remove
4979
+ expirationTurns: 3,
4980
+ expirationMode: 'Remove'
4954
4981
  };
4955
- // Add user message with the sub-agent results
4956
- const resultMessage = subAgentResult.success
4957
- ? `Sub-agent completed successfully:\n${JSON.stringify(subAgentSummary, null, 2)}`
4958
- : `Sub-agent failed:\n${JSON.stringify(subAgentSummary, null, 2)}`;
4982
+ // Apply global override if configured
4983
+ if (params.messageExpirationOverride) {
4984
+ const override = params.messageExpirationOverride;
4985
+ if (override.expirationTurns != null) {
4986
+ subAgentMetadata.expirationTurns = override.expirationTurns;
4987
+ subAgentMetadata.expirationMode = override.expirationMode || 'Remove';
4988
+ subAgentMetadata.compactMode = override.compactMode;
4989
+ subAgentMetadata.compactLength = override.compactLength;
4990
+ subAgentMetadata.compactPromptId = override.compactPromptId;
4991
+ }
4992
+ }
4959
4993
  params.conversationMessages.push({
4960
4994
  role: 'user',
4961
4995
  content: resultMessage,
4962
- metadata: {
4963
- _temporary: true,
4964
- _subAgentResult: true,
4965
- subAgentName: subAgentRequest.name,
4966
- subAgentId: subAgentEntity.ID
4967
- }
4996
+ metadata: subAgentMetadata
4968
4997
  });
4969
4998
  // Set PayloadAtEnd with the merged payload
4970
4999
  if (stepEntity) {
@@ -5217,17 +5246,31 @@ The context is now within limits. Please retry your request with the recovered c
5217
5246
  // This ensures parent agents can override child Chat steps
5218
5247
  return await this.validateChatNextStep(params, chatStep, mergedPayload, this._agentRun, stepEntity);
5219
5248
  }
5220
- // Add sub-agent result to conversation as user message
5221
- const subAgentSummary = {
5222
- agentName: params.agent.Name,
5249
+ // Add sub-agent result to conversation as user message using markdown format
5250
+ // and expiration metadata so it persists across multiple prompt turns
5251
+ const relatedResultMessage = this.formatSubAgentResultAsMarkdown(subAgentRequest.name, subAgentResult);
5252
+ const relatedMetadata = {
5253
+ turnAdded: this._promptTurnCount,
5254
+ messageType: 'sub-agent-result',
5223
5255
  subAgentName: subAgentRequest.name,
5224
- success: subAgentResult.success,
5225
- payload: subAgentResult.payload,
5226
- errorMessage: subAgentResult.agentRun?.ErrorMessage
5256
+ subAgentId: subAgentEntity.ID,
5257
+ expirationTurns: 3,
5258
+ expirationMode: 'Remove'
5227
5259
  };
5260
+ if (params.messageExpirationOverride) {
5261
+ const override = params.messageExpirationOverride;
5262
+ if (override.expirationTurns != null) {
5263
+ relatedMetadata.expirationTurns = override.expirationTurns;
5264
+ relatedMetadata.expirationMode = override.expirationMode || 'Remove';
5265
+ relatedMetadata.compactMode = override.compactMode;
5266
+ relatedMetadata.compactLength = override.compactLength;
5267
+ relatedMetadata.compactPromptId = override.compactPromptId;
5268
+ }
5269
+ }
5228
5270
  params.conversationMessages.push({
5229
5271
  role: 'user',
5230
- content: `Related sub-agent "${subAgentRequest.name}" completed:\n${JSON.stringify(subAgentSummary, null, 2)}`
5272
+ content: relatedResultMessage,
5273
+ metadata: relatedMetadata
5231
5274
  });
5232
5275
  // Update the agent run's current payload
5233
5276
  if (this._agentRun) {
@@ -5676,9 +5719,8 @@ The context is now within limits. Please retry your request with the recovered c
5676
5719
  }
5677
5720
  // Only add metadata if we have expiration settings
5678
5721
  if (minExpirationTurns !== null && expirationMode !== 'None') {
5679
- const currentStepCount = this._agentRun?.Steps?.length || 0;
5680
5722
  metadata = {
5681
- turnAdded: currentStepCount,
5723
+ turnAdded: this._promptTurnCount,
5682
5724
  messageType: 'action-result',
5683
5725
  expirationTurns: minExpirationTurns,
5684
5726
  expirationMode: expirationMode,
@@ -5820,7 +5862,7 @@ The context is now within limits. Please retry your request with the recovered c
5820
5862
  const requestForUserId = this.resolveUserFromStrategy(resolvedStrategy, params);
5821
5863
  const priority = resolvedStrategy?.priority ?? 50;
5822
5864
  const expirationMinutes = resolvedStrategy?.expirationMinutes;
5823
- const request = await this._metadata.GetEntityObject('MJ: AI Agent Requests', params.contextUser);
5865
+ const request = await (params.provider || this._activeProvider).GetEntityObject('MJ: AI Agent Requests', params.contextUser);
5824
5866
  request.NewRecord();
5825
5867
  request.AgentID = params.agent.ID;
5826
5868
  request.RequestedAt = new Date();
@@ -6297,6 +6339,8 @@ The context is now within limits. Please retry your request with the recovered c
6297
6339
  else if (forEach.subAgent) {
6298
6340
  const subAgentStep = { step: 'Sub-Agent', subAgent: forEach.subAgent, newPayload: currentPayload, previousPayload: currentPayload };
6299
6341
  result = await this.processSubAgentStep(params, subAgentStep, parentStepId, item);
6342
+ // Attach formatted summary as priorStepResult so formatLoopResultsAsMarkdown can render it
6343
+ result.priorStepResult = this.formatSubAgentResultAsMarkdown(forEach.subAgent.name, result);
6300
6344
  }
6301
6345
  else {
6302
6346
  throw new Error('ForEach missing action/subAgent');
@@ -6318,7 +6362,7 @@ The context is now within limits. Please retry your request with the recovered c
6318
6362
  loopStepEntity.PayloadAtEnd = this.serializePayloadAtEnd(loopResults.finalPayload);
6319
6363
  await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, loopResults.errors.join('\n\n'), loopResults);
6320
6364
  if (this.AgentTypeInstance.InjectLoopResultsAsMessage) {
6321
- this.injectLoopResultsMessage('ForEach', forEach.collectionPath, loopResults.results, loopResults.errors, params);
6365
+ this.injectLoopResultsMessage('ForEach', forEach.collectionPath, loopResults.results, loopResults.errors, params, forEach.action?.name);
6322
6366
  }
6323
6367
  return {
6324
6368
  step: 'Retry',
@@ -6329,20 +6373,121 @@ The context is now within limits. Please retry your request with the recovered c
6329
6373
  };
6330
6374
  }
6331
6375
  /**
6332
- * Helper: Inject loop results as temporary message
6376
+ * Helper: Inject loop results into conversation with expiration/compaction metadata.
6377
+ * Uses the same lifecycle as non-loop action results so results persist for N turns
6378
+ * rather than being deleted after a single prompt turn.
6333
6379
  */
6334
- injectLoopResultsMessage(loopType, collectionOrCondition, results, errors, params) {
6335
- // grab the priorStepResult from within each result item and put that into a new array
6336
- const extractedResults = results.map(r => r.priorStepResult);
6380
+ injectLoopResultsMessage(loopType, collectionOrCondition, results, errors, params, actionName) {
6337
6381
  const label = loopType === 'ForEach' ? 'Collection' : 'Condition';
6382
+ const content = `## Loop Completed\n**Type:** ${loopType}\n**${label}:** ${collectionOrCondition}\n` +
6383
+ `**Processed:** ${results.length}, **Errors:** ${errors.length}\n\n` +
6384
+ this.formatLoopResultsAsMarkdown(results, errors);
6385
+ // Resolve expiration metadata from the loop's action config (most-restrictive-wins)
6386
+ const metadata = this.resolveLoopExpirationMetadata(params, actionName);
6338
6387
  params.conversationMessages.push({
6339
6388
  role: 'user',
6340
- content: `## Loop Completed\n**Type:** ${loopType}\n**${label}:** ${collectionOrCondition}\n` +
6341
- `**Processed:** ${results.length}, **Errors:** ${errors.length}\n\n` +
6342
- `**Results:**\n\`\`\`json\n${JSON.stringify(extractedResults, null, 2)}\n\`\`\``,
6343
- metadata: { _temporary: true, _loopResults: true }
6389
+ content,
6390
+ metadata
6344
6391
  });
6345
6392
  }
6393
+ /**
6394
+ * Formats loop iteration results as markdown. Handles two distinct result shapes
6395
+ * depending on whether the loop body executed actions or sub-agents:
6396
+ *
6397
+ * - **Action iterations**: `priorStepResult` is an `ActionResultSummary[]`, set by
6398
+ * `executeActionsStep()` (line ~7003). These are formatted via `formatActionResultsAsMarkdown()`.
6399
+ *
6400
+ * - **Sub-agent iterations**: `priorStepResult` is a **pre-formatted markdown string**,
6401
+ * set by the loop handlers (`executeSingleForEachIteration` / `executeSingleWhileIteration`)
6402
+ * which call `formatSubAgentResultAsMarkdown()` and assign the result to `priorStepResult`.
6403
+ * Since the markdown is already rendered upstream, this method simply includes it as-is.
6404
+ *
6405
+ * This two-path design means sub-agent formatting happens at assignment time (in the loop
6406
+ * handler), while action formatting happens at render time (here). Both produce the same
6407
+ * markdown style used by non-loop results, ensuring consistency across the codebase.
6408
+ */
6409
+ formatLoopResultsAsMarkdown(results, errors) {
6410
+ const lines = [];
6411
+ for (let i = 0; i < results.length; i++) {
6412
+ const iterResult = results[i].priorStepResult;
6413
+ // Action iterations: priorStepResult is ActionResultSummary[] — format via shared helper
6414
+ if (Array.isArray(iterResult)) {
6415
+ lines.push(`### Iteration ${i + 1}`);
6416
+ lines.push(this.formatActionResultsAsMarkdown(iterResult));
6417
+ }
6418
+ else if (iterResult != null) {
6419
+ // Sub-agent iterations: priorStepResult is already a markdown string from
6420
+ // formatSubAgentResultAsMarkdown() called in the loop handler. Non-string
6421
+ // results (edge cases) are JSON-stringified as a fallback.
6422
+ lines.push(`### Iteration ${i + 1}`);
6423
+ const text = typeof iterResult === 'string'
6424
+ ? iterResult
6425
+ : JSON.stringify(iterResult);
6426
+ lines.push(text);
6427
+ }
6428
+ // null/undefined priorStepResult: iteration produced no output — skip silently
6429
+ }
6430
+ if (errors.length > 0) {
6431
+ lines.push(`### Errors`);
6432
+ for (const err of errors) {
6433
+ const errMsg = typeof err === 'string' ? err : err?.message || JSON.stringify(err);
6434
+ lines.push(`• ✗ ${errMsg}`);
6435
+ }
6436
+ }
6437
+ return lines.join('\n');
6438
+ }
6439
+ /**
6440
+ * Resolves expiration/compaction metadata for loop result messages by looking up
6441
+ * the loop's action config. Uses the same most-restrictive-wins logic as
6442
+ * non-loop action result metadata resolution.
6443
+ */
6444
+ resolveLoopExpirationMetadata(params, actionName) {
6445
+ const baseMetadata = {
6446
+ turnAdded: this._promptTurnCount,
6447
+ messageType: 'loop-result'
6448
+ };
6449
+ // Check for global override first
6450
+ if (params.messageExpirationOverride) {
6451
+ const override = params.messageExpirationOverride;
6452
+ if (override.expirationTurns != null && override.expirationMode !== 'None') {
6453
+ return {
6454
+ ...baseMetadata,
6455
+ expirationTurns: override.expirationTurns,
6456
+ expirationMode: override.expirationMode || 'Remove',
6457
+ compactMode: override.compactMode,
6458
+ compactLength: override.compactLength,
6459
+ compactPromptId: override.compactPromptId
6460
+ };
6461
+ }
6462
+ }
6463
+ // Look up the loop's action config for expiration settings
6464
+ if (actionName) {
6465
+ const agentActions = AIEngine.Instance.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, params.agent.ID));
6466
+ const effectiveActions = this._effectiveActions.length > 0
6467
+ ? this._effectiveActions
6468
+ : ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)));
6469
+ const matchedAction = effectiveActions.find(a => a.Name.trim().toLowerCase() === actionName.trim().toLowerCase());
6470
+ if (matchedAction) {
6471
+ const agentAction = agentActions.find(aa => UUIDsEqual(aa.ActionID, matchedAction.ID));
6472
+ if (agentAction?.ResultExpirationTurns != null && agentAction.ResultExpirationMode !== 'None') {
6473
+ return {
6474
+ ...baseMetadata,
6475
+ expirationTurns: agentAction.ResultExpirationTurns,
6476
+ expirationMode: agentAction.ResultExpirationMode,
6477
+ compactMode: agentAction.CompactMode,
6478
+ compactLength: agentAction.CompactLength ?? undefined,
6479
+ compactPromptId: agentAction.CompactPromptID ?? undefined
6480
+ };
6481
+ }
6482
+ }
6483
+ }
6484
+ // Default: keep for 3 turns then remove (safe fallback so results don't vanish after 1 turn)
6485
+ return {
6486
+ ...baseMetadata,
6487
+ expirationTurns: 3,
6488
+ expirationMode: 'Remove'
6489
+ };
6490
+ }
6346
6491
  /**
6347
6492
  * Helper: Find action with fuzzy matching for loops.
6348
6493
  * Uses effective actions which includes both database-configured and dynamically added actions.
@@ -6482,6 +6627,8 @@ The context is now within limits. Please retry your request with the recovered c
6482
6627
  else if (whileOp.subAgent) {
6483
6628
  const subAgentStep = { step: 'Sub-Agent', subAgent: whileOp.subAgent, newPayload: currentPayload, previousPayload: currentPayload };
6484
6629
  result = await this.processSubAgentStep(params, subAgentStep, parentStepId, attemptContext);
6630
+ // Attach formatted summary as priorStepResult so formatLoopResultsAsMarkdown can render it
6631
+ result.priorStepResult = this.formatSubAgentResultAsMarkdown(whileOp.subAgent.name, result);
6485
6632
  }
6486
6633
  else {
6487
6634
  throw new Error('While missing action/subAgent');
@@ -6502,7 +6649,7 @@ The context is now within limits. Please retry your request with the recovered c
6502
6649
  loopStepEntity.PayloadAtEnd = this.serializePayloadAtEnd(loopResults.finalPayload);
6503
6650
  await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, loopResults.errors.join('\n\n'), loopResults);
6504
6651
  if (this.AgentTypeInstance.InjectLoopResultsAsMessage) {
6505
- this.injectLoopResultsMessage('While', whileOp.condition, loopResults.results, loopResults.errors, params);
6652
+ this.injectLoopResultsMessage('While', whileOp.condition, loopResults.results, loopResults.errors, params, whileOp.action?.name);
6506
6653
  }
6507
6654
  return {
6508
6655
  step: 'Retry',
@@ -6881,8 +7028,7 @@ The context is now within limits. Please retry your request with the recovered c
6881
7028
  if (!this._agentRun) {
6882
7029
  throw new Error('Cannot create compaction step: agent run not initialized');
6883
7030
  }
6884
- const md = new Metadata();
6885
- const step = await md.GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
7031
+ const step = await (params.provider || this._activeProvider).GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
6886
7032
  step.NewRecord();
6887
7033
  step.AgentRunID = this._agentRun.ID;
6888
7034
  step.StepType = 'Prompt';