@memberjunction/ai-agents 5.37.0 → 5.38.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 +47 -0
- package/dist/AgentDataPreloader.d.ts +8 -17
- package/dist/AgentDataPreloader.d.ts.map +1 -1
- package/dist/AgentDataPreloader.js +53 -8
- package/dist/AgentDataPreloader.js.map +1 -1
- package/dist/AgentRunner.d.ts +19 -23
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +73 -167
- package/dist/AgentRunner.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +9 -0
- 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 +22 -12
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/artifact-tools/DataSnapshotToolLibrary.d.ts +4 -0
- package/dist/artifact-tools/DataSnapshotToolLibrary.d.ts.map +1 -1
- package/dist/artifact-tools/DataSnapshotToolLibrary.js +507 -12
- package/dist/artifact-tools/DataSnapshotToolLibrary.js.map +1 -1
- package/dist/base-agent.d.ts +279 -80
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +638 -150
- package/dist/base-agent.js.map +1 -1
- package/package.json +17 -17
package/dist/base-agent.js
CHANGED
|
@@ -73,6 +73,12 @@ import _ from 'lodash';
|
|
|
73
73
|
* const result = await agent.Execute(params);
|
|
74
74
|
* ```
|
|
75
75
|
*/
|
|
76
|
+
/**
|
|
77
|
+
* Maximum number of sub-agents to dispatch concurrently when a Loop agent
|
|
78
|
+
* returns a `subAgents` array. Prevents a misbehaving LLM (or an over-eager
|
|
79
|
+
* one) from saturating the model API / DB pool with N concurrent runs.
|
|
80
|
+
*/
|
|
81
|
+
const PARALLEL_SUBAGENT_CONCURRENCY_LIMIT = 5;
|
|
76
82
|
export class BaseAgent {
|
|
77
83
|
constructor() {
|
|
78
84
|
/**
|
|
@@ -80,6 +86,16 @@ export class BaseAgent {
|
|
|
80
86
|
* @private
|
|
81
87
|
*/
|
|
82
88
|
this._promptRunner = new AIPromptRunner();
|
|
89
|
+
/**
|
|
90
|
+
* List of pending database save Promises for observability step records.
|
|
91
|
+
* Awaited concurrently in finalizeAgentRun() to prevent blocking.
|
|
92
|
+
*/
|
|
93
|
+
this._pendingSaves = [];
|
|
94
|
+
/**
|
|
95
|
+
* Queue map to chain database saves sequentially per step entity.
|
|
96
|
+
* Prevents UPDATE queries running before INSERT queries on quick steps.
|
|
97
|
+
*/
|
|
98
|
+
this._stepSavePromises = new Map();
|
|
83
99
|
/**
|
|
84
100
|
* Active per-request metadata provider, set at the start of Execute().
|
|
85
101
|
* Defaults to the global Metadata.Provider; overridden when a per-request
|
|
@@ -418,7 +434,12 @@ export class BaseAgent {
|
|
|
418
434
|
* This prevents context overflow when action results contain large base64 data (images, audio, video).
|
|
419
435
|
*
|
|
420
436
|
* Uses generic ValueType=MediaOutput detection from action metadata to identify media output params.
|
|
421
|
-
*
|
|
437
|
+
*
|
|
438
|
+
* Intercepted media is stored in `_mediaOutputs` with a generated `refId` and is **always
|
|
439
|
+
* persisted** by `AgentRunner` — all media outputs are saved to `AIAgentRunMedia` +
|
|
440
|
+
* `ConversationDetailAttachment` (which auto-pairs to an artifact via the server hook).
|
|
441
|
+
* The `${media:<refId>}` placeholder injected into the action result keeps the LLM's
|
|
442
|
+
* context window small; the LLM is told the media will be displayed automatically.
|
|
422
443
|
*
|
|
423
444
|
* @param actionParams - The output parameters from an action result
|
|
424
445
|
* @param actionEntity - Optional action entity metadata for ValueType checking
|
|
@@ -454,11 +475,10 @@ export class BaseAgent {
|
|
|
454
475
|
if (media.data && media.data.length > BaseAgent.LARGE_BINARY_THRESHOLD) {
|
|
455
476
|
// Generate unique reference ID
|
|
456
477
|
const refId = `media-${Date.now().toString(36)}-${i}-${Math.random().toString(36).substring(2, 8)}`;
|
|
457
|
-
// Store in unified media outputs
|
|
478
|
+
// Store in unified media outputs — always persisted by AgentRunner.
|
|
458
479
|
this._mediaOutputs.push({
|
|
459
480
|
...media,
|
|
460
481
|
refId,
|
|
461
|
-
persist: false // Not persisted unless placeholder is resolved in final output
|
|
462
482
|
});
|
|
463
483
|
references.push(`\${media:${refId}}`);
|
|
464
484
|
extractedCount++;
|
|
@@ -472,7 +492,7 @@ export class BaseAgent {
|
|
|
472
492
|
Value: {
|
|
473
493
|
mediaReferences: references,
|
|
474
494
|
count: mediaItems.length,
|
|
475
|
-
note: `${extractedCount} media item(s) extracted
|
|
495
|
+
note: `${extractedCount} media item(s) extracted and will be displayed to the user automatically.`
|
|
476
496
|
}
|
|
477
497
|
});
|
|
478
498
|
this.logStatus(`📦 Extracted ${extractedCount} ${param.Name} item(s) to media references`, true);
|
|
@@ -485,14 +505,13 @@ export class BaseAgent {
|
|
|
485
505
|
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
486
506
|
if (isMediaOutputParam || base64Pattern.test(param.Value.substring(0, 1000))) {
|
|
487
507
|
const refId = `data-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
|
|
488
|
-
// Store in unified media outputs
|
|
508
|
+
// Store in unified media outputs — always persisted by AgentRunner.
|
|
489
509
|
this._mediaOutputs.push({
|
|
490
510
|
modality: 'Image', // Default to image, could be enhanced with mime detection
|
|
491
511
|
mimeType: 'application/octet-stream',
|
|
492
512
|
data: param.Value,
|
|
493
513
|
label: `Media data from ${param.Name}`,
|
|
494
514
|
refId,
|
|
495
|
-
persist: false
|
|
496
515
|
});
|
|
497
516
|
sanitizedParams.push({
|
|
498
517
|
Name: param.Name,
|
|
@@ -509,40 +528,50 @@ export class BaseAgent {
|
|
|
509
528
|
return sanitizedParams;
|
|
510
529
|
}
|
|
511
530
|
/**
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
531
|
+
* Substitutes `${media:<refId>}` placeholders in a string with the actual
|
|
532
|
+
* data URI (`data:<mime>;base64,<bytes>`) of the matching intercepted media item.
|
|
533
|
+
*
|
|
534
|
+
* Used for payload / actionable-command resolution at the terminal step, where the
|
|
535
|
+
* LLM wants to embed image (or other media) data inline at a specific position in
|
|
536
|
+
* its structured output rather than as a trailing attachment card. The string
|
|
537
|
+
* variant of placeholder resolution; recursive walker lives in
|
|
538
|
+
* {@link resolveMediaPlaceholdersInPayload}.
|
|
539
|
+
*
|
|
540
|
+
* This function only does substitution — it has no persistence side effects.
|
|
515
541
|
*
|
|
516
542
|
* @param text - The string that may contain media placeholders
|
|
517
|
-
* @returns String with placeholders resolved to actual data URIs
|
|
543
|
+
* @returns String with placeholders resolved to actual data URIs (or the original
|
|
544
|
+
* placeholder if the refId is unknown — defensive, shouldn't happen)
|
|
518
545
|
* @private
|
|
519
546
|
* @since 3.1.0
|
|
520
547
|
*/
|
|
521
548
|
resolveMediaPlaceholdersInString(text) {
|
|
522
|
-
//
|
|
549
|
+
// Fast path: nothing to resolve if there are no intercepted media items.
|
|
523
550
|
const hasRefIds = this._mediaOutputs.some(m => m.refId);
|
|
524
551
|
if (!text || !hasRefIds) {
|
|
525
552
|
return text;
|
|
526
553
|
}
|
|
527
|
-
// Match ${media:ref-id} pattern
|
|
554
|
+
// Match ${media:ref-id} pattern (lowercase letters, digits, dashes only —
|
|
555
|
+
// matches the IDs generated by interceptLargeBinaryContent).
|
|
528
556
|
const placeholderRegex = /\$\{media:([a-z0-9-]+)\}/g;
|
|
529
557
|
return text.replace(placeholderRegex, (match, refId) => {
|
|
530
558
|
const media = this._mediaOutputs.find(m => m.refId === refId);
|
|
531
559
|
if (media?.data) {
|
|
532
|
-
// Mark for persistence since it's being used in final output
|
|
533
|
-
media.persist = true;
|
|
534
560
|
return `data:${media.mimeType};base64,${media.data}`;
|
|
535
561
|
}
|
|
536
|
-
//
|
|
562
|
+
// Unknown refId — leave the placeholder in place rather than emit a broken
|
|
563
|
+
// data URI. Defensive; this branch should not fire in normal flow.
|
|
537
564
|
this.logStatus(`⚠️ Media reference '${refId}' not found in registry`, true);
|
|
538
565
|
return match;
|
|
539
566
|
});
|
|
540
567
|
}
|
|
541
568
|
/**
|
|
542
|
-
* Resolves media placeholders
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
569
|
+
* Resolves `${media:<refId>}` placeholders anywhere inside an arbitrary payload.
|
|
570
|
+
* - Strings: resolves placeholders directly
|
|
571
|
+
* - Objects: recursively processes every string property
|
|
572
|
+
* - Arrays: recursively processes every element
|
|
573
|
+
*
|
|
574
|
+
* Pure resolution — no persistence side effects.
|
|
546
575
|
*
|
|
547
576
|
* @param payload - The payload that may contain media placeholders in string values
|
|
548
577
|
* @returns Payload with all placeholders resolved to actual data URIs
|
|
@@ -550,21 +579,12 @@ export class BaseAgent {
|
|
|
550
579
|
* @since 3.1.0
|
|
551
580
|
*/
|
|
552
581
|
resolveMediaPlaceholdersInPayload(payload) {
|
|
553
|
-
//
|
|
582
|
+
// Fast path: nothing to resolve if no intercepted media exists.
|
|
554
583
|
const hasRefIds = this._mediaOutputs.some(m => m.refId);
|
|
555
584
|
if (!hasRefIds) {
|
|
556
585
|
return payload;
|
|
557
586
|
}
|
|
558
|
-
|
|
559
|
-
const unpersisted = this._mediaOutputs.filter(m => m.refId && m.persist === false).length;
|
|
560
|
-
const resolved = this.resolveMediaPlaceholdersRecursive(payload);
|
|
561
|
-
// Count how many were marked for persistence (persist changed from false to true)
|
|
562
|
-
const persistedAfter = this._mediaOutputs.filter(m => m.refId && m.persist === true).length;
|
|
563
|
-
const resolvedCount = persistedAfter - (unpersisted - this._mediaOutputs.filter(m => m.refId && m.persist === false).length);
|
|
564
|
-
if (resolvedCount > 0) {
|
|
565
|
-
this.logStatus(`✅ Resolved ${resolvedCount} media placeholder(s) in final payload`, true);
|
|
566
|
-
}
|
|
567
|
-
return resolved;
|
|
587
|
+
return this.resolveMediaPlaceholdersRecursive(payload);
|
|
568
588
|
}
|
|
569
589
|
/**
|
|
570
590
|
* Recursively resolves media placeholders in any value.
|
|
@@ -593,54 +613,26 @@ export class BaseAgent {
|
|
|
593
613
|
// Return primitives (numbers, booleans) as-is
|
|
594
614
|
return value;
|
|
595
615
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
616
|
+
// ───────────────────────── Sub-class state accessors ──────────────────────────
|
|
617
|
+
// Read-only `protected` getters so driver sub-classes (e.g. Skip) can inspect
|
|
618
|
+
// the current run's state without being able to corrupt internal invariants.
|
|
619
|
+
// Mutations still flow through the framework's own methods (createStepEntity,
|
|
620
|
+
// queueStepSave, incrementExecutionCount, etc.).
|
|
621
|
+
// (`AgentRun` and `MediaOutputs` are already public getters above; the
|
|
622
|
+
// accessors below cover state that previously had no external surface.)
|
|
623
|
+
/** Depth of this agent in the execution hierarchy (0 = root). @protected */
|
|
624
|
+
get Depth() { return this._depth; }
|
|
625
|
+
/** Agent name hierarchy from root to current (e.g. `['Sage', 'Skip', 'Researcher']`). @protected */
|
|
626
|
+
get AgentHierarchy() { return this._agentHierarchy; }
|
|
627
|
+
/** Parent step counts used to build the `2.1.3` hierarchical step label. @protected */
|
|
628
|
+
get ParentStepCounts() { return this._parentStepCounts; }
|
|
629
|
+
/**
|
|
630
|
+
* Accumulated file outputs (PDF, Excel, Word, etc.) produced this run.
|
|
631
|
+
* Mirrors the existing `MediaOutputs` accessor pattern but is scoped to
|
|
632
|
+
* driver sub-classes since it's a more internal collection.
|
|
633
|
+
* @protected
|
|
609
634
|
*/
|
|
610
|
-
|
|
611
|
-
if (!message) {
|
|
612
|
-
return message;
|
|
613
|
-
}
|
|
614
|
-
// Check if any media has a refId (meaning we have intercepted media)
|
|
615
|
-
const hasRefIds = this._mediaOutputs.some(m => m.refId);
|
|
616
|
-
if (!hasRefIds) {
|
|
617
|
-
return message;
|
|
618
|
-
}
|
|
619
|
-
// Find all ${media:xxx} placeholders and mark referenced media for persistence
|
|
620
|
-
const placeholderRegex = /\$\{media:([a-zA-Z0-9_-]+)\}/g;
|
|
621
|
-
let match;
|
|
622
|
-
let promotedCount = 0;
|
|
623
|
-
while ((match = placeholderRegex.exec(message)) !== null) {
|
|
624
|
-
const refId = match[1];
|
|
625
|
-
const media = this._mediaOutputs.find(m => m.refId === refId);
|
|
626
|
-
if (media && media.persist !== true) {
|
|
627
|
-
media.persist = true; // Triggers save to AIAgentRunMedia
|
|
628
|
-
promotedCount++;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
if (promotedCount > 0) {
|
|
632
|
-
this.logStatus(`📎 Auto-promoted ${promotedCount} media output(s) from message placeholders`, true);
|
|
633
|
-
}
|
|
634
|
-
// Strip <img>, <audio>, <video> tags containing media placeholders
|
|
635
|
-
// The media will display via ConversationDetailAttachment instead
|
|
636
|
-
let cleanedMessage = message
|
|
637
|
-
.replace(/<img[^>]*src=["']\$\{media:[^}]+\}["'][^>]*\/?>/gi, '')
|
|
638
|
-
.replace(/<audio[^>]*src=["']\$\{media:[^}]+\}["'][^>]*>.*?<\/audio>/gi, '')
|
|
639
|
-
.replace(/<video[^>]*src=["']\$\{media:[^}]+\}["'][^>]*>.*?<\/video>/gi, '')
|
|
640
|
-
.replace(/\n\s*\n\s*\n/g, '\n\n') // Clean up excessive newlines
|
|
641
|
-
.trim();
|
|
642
|
-
return cleanedMessage;
|
|
643
|
-
}
|
|
635
|
+
get FileOutputs() { return this._fileOutputs; }
|
|
644
636
|
/**
|
|
645
637
|
* Gets the current validation retry count for the agent run.
|
|
646
638
|
* This count tracks how many times the agent has retried validation
|
|
@@ -1977,51 +1969,85 @@ export class BaseAgent {
|
|
|
1977
1969
|
* @param nextStep
|
|
1978
1970
|
* @returns
|
|
1979
1971
|
*/
|
|
1972
|
+
/**
|
|
1973
|
+
* Returns the list of sub-agent requests on a next-step decision, normalizing
|
|
1974
|
+
* the singular (`subAgent`) and plural (`subAgents`) forms. Plural takes
|
|
1975
|
+
* precedence when both are present (parallel fan-out); otherwise the singular
|
|
1976
|
+
* form is wrapped into a single-element array. Empty if neither is set.
|
|
1977
|
+
*
|
|
1978
|
+
* Use this anywhere code needs to enumerate the sub-agents an LLM requested
|
|
1979
|
+
* — keeps validation and execution paths consistent and avoids the regression
|
|
1980
|
+
* where one path read `.subAgent?.name` and missed parallel requests.
|
|
1981
|
+
*/
|
|
1982
|
+
getRequestedSubAgents(nextStep) {
|
|
1983
|
+
if (!nextStep)
|
|
1984
|
+
return [];
|
|
1985
|
+
if (nextStep.subAgents && nextStep.subAgents.length > 0) {
|
|
1986
|
+
return nextStep.subAgents;
|
|
1987
|
+
}
|
|
1988
|
+
return nextStep.subAgent ? [nextStep.subAgent] : [];
|
|
1989
|
+
}
|
|
1980
1990
|
async validateSubAgentNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
1981
|
-
// check to make sure the current agent can execute the specified sub-agent
|
|
1982
|
-
const name = nextStep.subAgent?.name;
|
|
1983
1991
|
const curAgentSubAgents = AIEngine.Instance.GetSubAgents(params.agent.ID, 'Active');
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1992
|
+
// Collect requested sub-agents. Prefer plural `subAgents` (parallel fan-out);
|
|
1993
|
+
// fall back to singular `subAgent` for the classic single-sub-agent next step.
|
|
1994
|
+
const requested = this.getRequestedSubAgents(nextStep);
|
|
1995
|
+
if (requested.length === 0) {
|
|
1996
|
+
this.logError(`Sub-agent 'undefined' not found or not active for agent '${params.agent.Name}'`, {
|
|
1987
1997
|
agent: params.agent,
|
|
1988
1998
|
category: 'SubAgentExecution'
|
|
1989
1999
|
});
|
|
1990
|
-
// Increment validation retry count since we're changing to Retry
|
|
1991
2000
|
if (nextStep.step !== 'Retry') {
|
|
1992
2001
|
this._generalValidationRetryCount++;
|
|
1993
2002
|
}
|
|
1994
2003
|
return {
|
|
1995
2004
|
step: 'Retry',
|
|
1996
|
-
terminate: false,
|
|
1997
|
-
errorMessage: `Sub-agent '
|
|
2005
|
+
terminate: false,
|
|
2006
|
+
errorMessage: `Sub-agent 'undefined' not found or not active`
|
|
1998
2007
|
};
|
|
1999
2008
|
}
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2009
|
+
// Validate each requested sub-agent: existence + MaxExecutionsPerRun
|
|
2010
|
+
for (const req of requested) {
|
|
2011
|
+
const name = req?.name;
|
|
2012
|
+
const subAgent = curAgentSubAgents.find(a => a.Name.trim().toLowerCase() === name?.trim().toLowerCase());
|
|
2013
|
+
if (!name || !subAgent) {
|
|
2014
|
+
this.logError(`Sub-agent '${name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
2005
2015
|
agent: params.agent,
|
|
2006
|
-
category: 'SubAgentExecution'
|
|
2007
|
-
metadata: {
|
|
2008
|
-
subAgentName: name,
|
|
2009
|
-
executionCount,
|
|
2010
|
-
maxExecutions: subAgent.MaxExecutionsPerRun
|
|
2011
|
-
}
|
|
2016
|
+
category: 'SubAgentExecution'
|
|
2012
2017
|
});
|
|
2013
|
-
// Increment validation retry count since we're changing to Retry
|
|
2014
2018
|
if (nextStep.step !== 'Retry') {
|
|
2015
2019
|
this._generalValidationRetryCount++;
|
|
2016
2020
|
}
|
|
2017
2021
|
return {
|
|
2018
2022
|
step: 'Retry',
|
|
2019
2023
|
terminate: false,
|
|
2020
|
-
errorMessage: `Sub-agent '${name}'
|
|
2024
|
+
errorMessage: `Sub-agent '${name}' not found or not active`
|
|
2021
2025
|
};
|
|
2022
2026
|
}
|
|
2027
|
+
if (subAgent.MaxExecutionsPerRun != null) {
|
|
2028
|
+
const executionCount = await this.getSubAgentExecutionCount(agentRun.ID, subAgent.ID);
|
|
2029
|
+
if (executionCount >= subAgent.MaxExecutionsPerRun) {
|
|
2030
|
+
this.logError(`Sub-agent '${name}' has reached its maximum execution limit of ${subAgent.MaxExecutionsPerRun}`, {
|
|
2031
|
+
agent: params.agent,
|
|
2032
|
+
category: 'SubAgentExecution',
|
|
2033
|
+
metadata: {
|
|
2034
|
+
subAgentName: name,
|
|
2035
|
+
executionCount,
|
|
2036
|
+
maxExecutions: subAgent.MaxExecutionsPerRun
|
|
2037
|
+
}
|
|
2038
|
+
});
|
|
2039
|
+
if (nextStep.step !== 'Retry') {
|
|
2040
|
+
this._generalValidationRetryCount++;
|
|
2041
|
+
}
|
|
2042
|
+
return {
|
|
2043
|
+
step: 'Retry',
|
|
2044
|
+
terminate: false,
|
|
2045
|
+
errorMessage: `Sub-agent '${name}' has reached its maximum execution limit of ${subAgent.MaxExecutionsPerRun}`
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2023
2049
|
}
|
|
2024
|
-
//
|
|
2050
|
+
// All requested sub-agents are valid
|
|
2025
2051
|
return nextStep;
|
|
2026
2052
|
}
|
|
2027
2053
|
/**
|
|
@@ -3760,6 +3786,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3760
3786
|
configurationId: params.configurationId, // propagate configuration ID to sub-agent
|
|
3761
3787
|
effortLevel: params.effortLevel, // propagate effort level to sub-agent
|
|
3762
3788
|
apiKeys: params.apiKeys, // propagate API keys to sub-agent
|
|
3789
|
+
inputArtifacts: params.inputArtifacts, // propagate input artifacts so sub-agents inherit the parent's artifact manifest + tools (e.g. a Codesmith delegate can read a Data Snapshot the parent references)
|
|
3763
3790
|
data: {
|
|
3764
3791
|
...params.data,
|
|
3765
3792
|
...subAgentRequest.templateParameters,
|
|
@@ -3770,10 +3797,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3770
3797
|
PrimaryScopeEntityName: params.PrimaryScopeEntityName, // propagate scope to sub-agent
|
|
3771
3798
|
PrimaryScopeRecordID: params.PrimaryScopeRecordID,
|
|
3772
3799
|
SecondaryScopes: params.SecondaryScopes,
|
|
3773
|
-
// Add callback to link AgentRun ID immediately when created
|
|
3774
3800
|
onAgentRunCreated: async (agentRunId) => {
|
|
3775
3801
|
stepEntity.TargetLogID = agentRunId;
|
|
3776
|
-
|
|
3802
|
+
this.queueStepSave(stepEntity);
|
|
3777
3803
|
}
|
|
3778
3804
|
});
|
|
3779
3805
|
// Check if execution was successful
|
|
@@ -4497,7 +4523,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4497
4523
|
/**
|
|
4498
4524
|
* Creates a step entity for tracking.
|
|
4499
4525
|
*
|
|
4500
|
-
*
|
|
4526
|
+
* Exposed as `protected` so driver sub-classes (e.g. Skip) can author custom
|
|
4527
|
+
* `AIAgentRunStep` records with the same setup correctness — `StepNumber`,
|
|
4528
|
+
* hierarchy breadcrumb, UUID validation, payload serialization, and the
|
|
4529
|
+
* `queueStepSave` coupling that keeps INSERT-then-UPDATE ordering safe.
|
|
4530
|
+
*
|
|
4531
|
+
* @protected
|
|
4501
4532
|
* @param params - Step creation parameters
|
|
4502
4533
|
* @returns {Promise<MJAIAgentRunStepEntityExtended>} - The created step entity
|
|
4503
4534
|
*/
|
|
@@ -4534,9 +4565,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4534
4565
|
}
|
|
4535
4566
|
});
|
|
4536
4567
|
}
|
|
4537
|
-
|
|
4538
|
-
throw new Error(`Failed to create agent run step record: ${JSON.stringify(stepEntity.LatestResult)}`);
|
|
4539
|
-
}
|
|
4568
|
+
this.queueStepSave(stepEntity);
|
|
4540
4569
|
// Add the step to the agent run's Steps array
|
|
4541
4570
|
if (this._agentRun) {
|
|
4542
4571
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -4592,6 +4621,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4592
4621
|
} : undefined
|
|
4593
4622
|
};
|
|
4594
4623
|
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Finalizes a step entity with completion status. Pairs with `createStepEntity`
|
|
4626
|
+
* — drivers that create custom steps should also finalize them through this
|
|
4627
|
+
* method so `Status`/`CompletedAt`/`Success`/`ErrorMessage`/`OutputData` are
|
|
4628
|
+
* populated consistently and the UPDATE is sequenced behind the INSERT via
|
|
4629
|
+
* `queueStepSave`.
|
|
4630
|
+
*
|
|
4631
|
+
* @protected
|
|
4632
|
+
*/
|
|
4595
4633
|
async finalizeStepEntity(stepEntity, success, errorMessage, outputData) {
|
|
4596
4634
|
try {
|
|
4597
4635
|
stepEntity.Status = success ? 'Completed' : 'Failed';
|
|
@@ -4609,14 +4647,73 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4609
4647
|
}
|
|
4610
4648
|
});
|
|
4611
4649
|
}
|
|
4612
|
-
|
|
4613
|
-
console.error('Failed to update agent run step record');
|
|
4614
|
-
}
|
|
4650
|
+
this.queueStepSave(stepEntity);
|
|
4615
4651
|
}
|
|
4616
4652
|
catch (e) {
|
|
4617
|
-
|
|
4653
|
+
LogError(`Failed to update agent run step record: ${e?.message ?? e}`, undefined, e);
|
|
4618
4654
|
}
|
|
4619
4655
|
}
|
|
4656
|
+
/**
|
|
4657
|
+
* Queues a database save for a step entity.
|
|
4658
|
+
*
|
|
4659
|
+
* - Saves on the same step record are chained (sequenced) to prevent an UPDATE
|
|
4660
|
+
* from racing the original INSERT.
|
|
4661
|
+
* - Saves on different step records run concurrently.
|
|
4662
|
+
* - Failures are not thrown — they're logged via `LogError` (with the entity's
|
|
4663
|
+
* `LatestResult.CompleteMessage` per the BaseEntity convention) so the
|
|
4664
|
+
* agent loop isn't blocked by observability writes — but they ARE surfaced
|
|
4665
|
+
* in `finalizeAgentRun` so callers see step-record drift.
|
|
4666
|
+
*
|
|
4667
|
+
* Exposed as `protected` so driver sub-classes (e.g. Skip) that author
|
|
4668
|
+
* custom `AIAgentRunStep` records can fire-and-forget saves through the
|
|
4669
|
+
* same chained/non-blocking machinery instead of awaiting `entity.Save()`
|
|
4670
|
+
* inline and blocking the agent loop.
|
|
4671
|
+
*
|
|
4672
|
+
* @protected
|
|
4673
|
+
*/
|
|
4674
|
+
queueStepSave(stepEntity) {
|
|
4675
|
+
const id = stepEntity.ID;
|
|
4676
|
+
const previousSave = this._stepSavePromises.get(id) ?? Promise.resolve();
|
|
4677
|
+
const currentSave = previousSave.then(() => stepEntity.Save()).then((ok) => {
|
|
4678
|
+
if (!ok) {
|
|
4679
|
+
LogError(`Failed to save agent run step record ${id}: ${stepEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
4680
|
+
}
|
|
4681
|
+
return ok;
|
|
4682
|
+
});
|
|
4683
|
+
this._stepSavePromises.set(id, currentSave);
|
|
4684
|
+
this._pendingSaves.push(currentSave);
|
|
4685
|
+
}
|
|
4686
|
+
/**
|
|
4687
|
+
* Maps an array through an async worker with bounded concurrency.
|
|
4688
|
+
* Preserves input order in the output. Used to cap parallel sub-agent and
|
|
4689
|
+
* preload dispatches so a misbehaving LLM (or a runaway data source) can't
|
|
4690
|
+
* exhaust the model API or DB pool.
|
|
4691
|
+
*
|
|
4692
|
+
* Exposed as `protected` so driver sub-classes performing custom parallel
|
|
4693
|
+
* work get the same bounded-fan-out + ordered-results contract for free.
|
|
4694
|
+
*
|
|
4695
|
+
* @protected
|
|
4696
|
+
*/
|
|
4697
|
+
async mapWithConcurrency(items, limit, worker) {
|
|
4698
|
+
if (items.length === 0)
|
|
4699
|
+
return [];
|
|
4700
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
4701
|
+
const results = new Array(items.length);
|
|
4702
|
+
let next = 0;
|
|
4703
|
+
const runners = [];
|
|
4704
|
+
for (let i = 0; i < effectiveLimit; i++) {
|
|
4705
|
+
runners.push((async () => {
|
|
4706
|
+
while (true) {
|
|
4707
|
+
const idx = next++;
|
|
4708
|
+
if (idx >= items.length)
|
|
4709
|
+
return;
|
|
4710
|
+
results[idx] = await worker(items[idx], idx);
|
|
4711
|
+
}
|
|
4712
|
+
})());
|
|
4713
|
+
}
|
|
4714
|
+
await Promise.all(runners);
|
|
4715
|
+
return results;
|
|
4716
|
+
}
|
|
4620
4717
|
/**
|
|
4621
4718
|
* Default parameter resolution for loop body parameters (used by Flow agents)
|
|
4622
4719
|
* Resolves item.field, payload.field, item, index, or static values
|
|
@@ -4651,7 +4748,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4651
4748
|
/**
|
|
4652
4749
|
* Formats a message with agent hierarchy for streaming/progress updates.
|
|
4653
4750
|
*
|
|
4654
|
-
*
|
|
4751
|
+
* Exposed as `protected` so driver sub-classes emit progress events whose
|
|
4752
|
+
* breadcrumbs line up with the framework's own — keeps the Explorer tree
|
|
4753
|
+
* view consistent across custom and built-in dispatch.
|
|
4754
|
+
*
|
|
4755
|
+
* @protected
|
|
4655
4756
|
* @param {string} baseMessage - The base message to format
|
|
4656
4757
|
* @returns {string} - The formatted message with hierarchy breadcrumb
|
|
4657
4758
|
*/
|
|
@@ -4674,10 +4775,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4674
4775
|
* - Nested sub-agent step 3: buildHierarchicalStep(3, [2, 1]) => "2.1.3"
|
|
4675
4776
|
* - Deep nesting: buildHierarchicalStep(5, [1, 2, 3, 4]) => "1.2.3.4.5"
|
|
4676
4777
|
*
|
|
4778
|
+
* Exposed as `protected` so driver sub-classes can emit step labels that
|
|
4779
|
+
* match the framework's `2.1.3` nesting convention.
|
|
4780
|
+
*
|
|
4677
4781
|
* @param currentStep - Current agent's step number (1-based)
|
|
4678
4782
|
* @param parentSteps - Array of parent step counts from root to immediate parent
|
|
4679
4783
|
* @returns Formatted hierarchical step string, or undefined if currentStep is undefined/null
|
|
4680
|
-
* @
|
|
4784
|
+
* @protected
|
|
4681
4785
|
*/
|
|
4682
4786
|
buildHierarchicalStep(currentStep, parentSteps) {
|
|
4683
4787
|
if (currentStep == null)
|
|
@@ -4948,10 +5052,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4948
5052
|
stepEntityId: stepEntity.ID
|
|
4949
5053
|
});
|
|
4950
5054
|
} : undefined;
|
|
4951
|
-
// Add callback to link PromptRun ID immediately when created
|
|
4952
5055
|
promptParams.onPromptRunCreated = async (promptRunId) => {
|
|
4953
5056
|
stepEntity.TargetLogID = promptRunId;
|
|
4954
|
-
|
|
5057
|
+
this.queueStepSave(stepEntity);
|
|
4955
5058
|
};
|
|
4956
5059
|
// Execute the prompt
|
|
4957
5060
|
const promptResult = await this.executePrompt(promptParams);
|
|
@@ -5596,37 +5699,31 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5596
5699
|
* @param subAgentPayloadOverride - Optional payload override for sub-agent execution, if provided the normal payload computation is skipped
|
|
5597
5700
|
*/
|
|
5598
5701
|
async processSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
5599
|
-
|
|
5702
|
+
// Multiple sub-agents → parallel fan-out
|
|
5703
|
+
if (previousDecision.subAgents && previousDecision.subAgents.length > 0) {
|
|
5704
|
+
return await this.executeParallelSubAgents(params, previousDecision.subAgents, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5705
|
+
}
|
|
5706
|
+
// Single sub-agent path. Use the helper so callers that populated `subAgents`
|
|
5707
|
+
// with a single entry (instead of `subAgent`) still resolve correctly.
|
|
5708
|
+
const requested = this.getRequestedSubAgents(previousDecision);
|
|
5709
|
+
const subAgentRequest = (requested[0] ?? previousDecision.subAgent);
|
|
5600
5710
|
const name = subAgentRequest?.name;
|
|
5601
5711
|
if (!name) {
|
|
5602
5712
|
return {
|
|
5603
5713
|
step: 'Failed',
|
|
5604
5714
|
terminate: false,
|
|
5605
5715
|
errorMessage: 'Sub-agent name is required',
|
|
5606
|
-
previousPayload: previousDecision
|
|
5607
|
-
newPayload: previousDecision
|
|
5716
|
+
previousPayload: previousDecision.newPayload,
|
|
5717
|
+
newPayload: previousDecision.newPayload
|
|
5608
5718
|
};
|
|
5609
5719
|
}
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
const childAgent = childAgents.find(a => a.Name.trim().toLowerCase() === name.trim().toLowerCase());
|
|
5614
|
-
if (childAgent) {
|
|
5615
|
-
// This is a child agent - use direct payload coupling
|
|
5616
|
-
return await this.executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5720
|
+
const resolved = this.resolveSubAgentByName(params, name);
|
|
5721
|
+
if (resolved?.relationship) {
|
|
5722
|
+
return await this.executeRelatedSubAgentStep(params, previousDecision, resolved.subAgentEntity, resolved.relationship, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5617
5723
|
}
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
ar.Status === 'Active');
|
|
5621
|
-
for (const relationship of activeRelationships) {
|
|
5622
|
-
const relatedAgent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, relationship.SubAgentID) &&
|
|
5623
|
-
a.Status === 'Active');
|
|
5624
|
-
if (relatedAgent && relatedAgent.Name.trim().toLowerCase() === name.trim().toLowerCase()) {
|
|
5625
|
-
// This is a related agent - use message-based coupling
|
|
5626
|
-
return await this.executeRelatedSubAgentStep(params, previousDecision, relatedAgent, relationship, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5627
|
-
}
|
|
5724
|
+
if (resolved) {
|
|
5725
|
+
return await this.executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5628
5726
|
}
|
|
5629
|
-
// Sub-agent not found
|
|
5630
5727
|
this.logError(`Sub-agent '${name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
5631
5728
|
agent: params.agent,
|
|
5632
5729
|
category: 'SubAgentExecution'
|
|
@@ -5635,8 +5732,381 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5635
5732
|
step: 'Retry',
|
|
5636
5733
|
terminate: false,
|
|
5637
5734
|
errorMessage: `Sub-agent '${name}' not found or not active`,
|
|
5638
|
-
previousPayload: previousDecision
|
|
5639
|
-
newPayload: previousDecision
|
|
5735
|
+
previousPayload: previousDecision.newPayload,
|
|
5736
|
+
newPayload: previousDecision.newPayload
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
/**
|
|
5740
|
+
* Finds a sub-agent by name, checking child agents (ParentID) first, then
|
|
5741
|
+
* related agents (AgentRelationships). Returns `undefined` when the name
|
|
5742
|
+
* doesn't resolve to an active agent reachable from `params.agent`.
|
|
5743
|
+
*
|
|
5744
|
+
* Used by both the single and parallel sub-agent dispatch paths so name
|
|
5745
|
+
* resolution is consistent and there's one place to fix lookup bugs.
|
|
5746
|
+
*
|
|
5747
|
+
* Exposed as `protected` so driver sub-classes with custom routing logic
|
|
5748
|
+
* still resolve names through the same case-insensitive child-then-related
|
|
5749
|
+
* lookup the framework uses internally.
|
|
5750
|
+
*
|
|
5751
|
+
* @protected
|
|
5752
|
+
*/
|
|
5753
|
+
resolveSubAgentByName(params, name) {
|
|
5754
|
+
const normalized = name.trim().toLowerCase();
|
|
5755
|
+
const childAgent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ParentID, params.agent.ID) &&
|
|
5756
|
+
a.Status === 'Active' &&
|
|
5757
|
+
a.Name.trim().toLowerCase() === normalized);
|
|
5758
|
+
if (childAgent) {
|
|
5759
|
+
return { subAgentEntity: childAgent };
|
|
5760
|
+
}
|
|
5761
|
+
const activeRelationships = AIEngine.Instance.AgentRelationships.filter(ar => UUIDsEqual(ar.AgentID, params.agent.ID) && ar.Status === 'Active');
|
|
5762
|
+
for (const rel of activeRelationships) {
|
|
5763
|
+
const relatedAgent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, rel.SubAgentID) &&
|
|
5764
|
+
a.Status === 'Active' &&
|
|
5765
|
+
a.Name.trim().toLowerCase() === normalized);
|
|
5766
|
+
if (relatedAgent) {
|
|
5767
|
+
return { subAgentEntity: relatedAgent, relationship: rel };
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
return undefined;
|
|
5771
|
+
}
|
|
5772
|
+
/**
|
|
5773
|
+
* Best-effort deep clone for sub-agent payloads. We need this so two parallel
|
|
5774
|
+
* sub-agents can each receive their own working copy — without it, mutations
|
|
5775
|
+
* by one in-flight sub-agent would race the others' reads.
|
|
5776
|
+
*
|
|
5777
|
+
* Uses `structuredClone` (Node 17+) where available; falls back to a JSON
|
|
5778
|
+
* round-trip for environments without it. Returns the original value on
|
|
5779
|
+
* non-cloneable inputs.
|
|
5780
|
+
*
|
|
5781
|
+
* **JSON fallback caveats** — the round-trip is *not* shape-preserving:
|
|
5782
|
+
* - `Date` → ISO string
|
|
5783
|
+
* - `Map`, `Set`, `RegExp`, typed arrays → `{}`
|
|
5784
|
+
* - `undefined` values and function-valued properties → dropped
|
|
5785
|
+
* - `BigInt` → throws (caught by the outer try/catch, returns original)
|
|
5786
|
+
* - circular refs → throws (returns original)
|
|
5787
|
+
* If payloads ever carry those shapes, behavior diverges between the
|
|
5788
|
+
* structuredClone path (Node 17+) and the JSON path. Keep sub-agent
|
|
5789
|
+
* payloads to plain JSON-safe shapes to avoid this skew.
|
|
5790
|
+
*
|
|
5791
|
+
* Exposed as `protected` so driver sub-classes performing their own parallel
|
|
5792
|
+
* dispatch get the same payload-isolation guarantee.
|
|
5793
|
+
*
|
|
5794
|
+
* @protected
|
|
5795
|
+
*/
|
|
5796
|
+
cloneSubAgentPayload(payload) {
|
|
5797
|
+
if (payload === null || payload === undefined)
|
|
5798
|
+
return payload;
|
|
5799
|
+
if (typeof payload !== 'object')
|
|
5800
|
+
return payload;
|
|
5801
|
+
try {
|
|
5802
|
+
if (typeof globalThis.structuredClone === 'function') {
|
|
5803
|
+
return globalThis.structuredClone(payload);
|
|
5804
|
+
}
|
|
5805
|
+
return JSON.parse(JSON.stringify(payload));
|
|
5806
|
+
}
|
|
5807
|
+
catch {
|
|
5808
|
+
return payload;
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
/**
|
|
5812
|
+
* Pre-flight for one parallel sub-agent: resolve the entity, push the
|
|
5813
|
+
* delegation message, emit progress. Runs synchronously (no awaits) so the
|
|
5814
|
+
* conversation transcript order matches the `subAgents` array's source order
|
|
5815
|
+
* regardless of which dispatch races to the front.
|
|
5816
|
+
*
|
|
5817
|
+
* Returns `undefined` (and pushes a sentinel message) when the sub-agent
|
|
5818
|
+
* can't be resolved — the dispatch loop later records this as a failed
|
|
5819
|
+
* execution rather than throwing inside `Promise.all`.
|
|
5820
|
+
*
|
|
5821
|
+
* @private
|
|
5822
|
+
*/
|
|
5823
|
+
prepareParallelSubAgentDispatch(params, request, stepCount) {
|
|
5824
|
+
const resolved = this.resolveSubAgentByName(params, request.name);
|
|
5825
|
+
if (!resolved) {
|
|
5826
|
+
this.logError(`Sub-agent '${request.name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
5827
|
+
agent: params.agent,
|
|
5828
|
+
category: 'SubAgentExecution'
|
|
5829
|
+
});
|
|
5830
|
+
return undefined;
|
|
5831
|
+
}
|
|
5832
|
+
const { subAgentEntity, relationship } = resolved;
|
|
5833
|
+
params.onProgress?.({
|
|
5834
|
+
step: 'subagent_execution',
|
|
5835
|
+
message: this.formatHierarchicalMessage(`Delegating to parallel sub-agent ${request.name}`),
|
|
5836
|
+
metadata: {
|
|
5837
|
+
agentName: params.agent.Name,
|
|
5838
|
+
subAgentName: request.name,
|
|
5839
|
+
reason: request.message,
|
|
5840
|
+
relationshipType: relationship ? 'related' : 'child',
|
|
5841
|
+
stepCount: stepCount + 1,
|
|
5842
|
+
hierarchicalStep: this.buildHierarchicalStep(stepCount + 1, this._parentStepCounts)
|
|
5843
|
+
}
|
|
5844
|
+
});
|
|
5845
|
+
params.conversationMessages.push({
|
|
5846
|
+
role: 'assistant',
|
|
5847
|
+
content: `I'm delegating this task to the parallel sub-agent "${request.name}".\n\nReason: ${request.message}`
|
|
5848
|
+
});
|
|
5849
|
+
return { request: request, subAgentEntity, relationship };
|
|
5850
|
+
}
|
|
5851
|
+
/**
|
|
5852
|
+
* Computes the per-sub-agent input payload + context message based on whether
|
|
5853
|
+
* this is a child (PayloadScope / paths) or related (input/output mapping)
|
|
5854
|
+
* sub-agent. The parent payload is deep-cloned for child agents so two
|
|
5855
|
+
* parallel sub-agents can't see each other's in-flight mutations.
|
|
5856
|
+
*
|
|
5857
|
+
* @private
|
|
5858
|
+
*/
|
|
5859
|
+
async buildSubAgentInputs(params, dispatch, previousDecision, subAgentPayloadOverride) {
|
|
5860
|
+
const { subAgentEntity, relationship, request } = dispatch;
|
|
5861
|
+
const parentPayload = previousDecision.newPayload;
|
|
5862
|
+
if (relationship) {
|
|
5863
|
+
// Related agent path — input/output mapping handles the structural transform.
|
|
5864
|
+
let initialPayload = subAgentPayloadOverride;
|
|
5865
|
+
if (!initialPayload && relationship.SubAgentInputMapping) {
|
|
5866
|
+
initialPayload = this.applySubAgentInputMapping(parentPayload, relationship.SubAgentInputMapping);
|
|
5867
|
+
}
|
|
5868
|
+
const contextPaths = this.parseSubAgentContextPaths(relationship, request.name);
|
|
5869
|
+
const contextMessage = this.prepareRelatedSubAgentContextMessage(parentPayload, contextPaths, params);
|
|
5870
|
+
return { initialPayload, contextMessage, upstreamPaths: [] };
|
|
5871
|
+
}
|
|
5872
|
+
// Child agent path — scoping + downstream/upstream paths, with deep clone
|
|
5873
|
+
// so siblings can't mutate each other's input.
|
|
5874
|
+
const { downstreamPaths, upstreamPaths } = this.computeUpstreamDownstreamPaths(params, subAgentEntity, request);
|
|
5875
|
+
let initialPayload = subAgentPayloadOverride;
|
|
5876
|
+
if (!initialPayload) {
|
|
5877
|
+
initialPayload = await this.computeChildSubAgentPayload(params, subAgentEntity, downstreamPaths, request, previousDecision);
|
|
5878
|
+
}
|
|
5879
|
+
return { initialPayload: this.cloneSubAgentPayload(initialPayload), contextMessage: null, upstreamPaths };
|
|
5880
|
+
}
|
|
5881
|
+
/**
|
|
5882
|
+
* Safely parses the `SubAgentContextPaths` JSON field on a relationship.
|
|
5883
|
+
* @private
|
|
5884
|
+
*/
|
|
5885
|
+
parseSubAgentContextPaths(relationship, subAgentName) {
|
|
5886
|
+
if (!relationship.SubAgentContextPaths)
|
|
5887
|
+
return [];
|
|
5888
|
+
try {
|
|
5889
|
+
return JSON.parse(relationship.SubAgentContextPaths);
|
|
5890
|
+
}
|
|
5891
|
+
catch (parseError) {
|
|
5892
|
+
LogError(`Failed to parse SubAgentContextPaths for sub-agent ${subAgentName}: ${parseError.message}`);
|
|
5893
|
+
return [];
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
/**
|
|
5897
|
+
* Merges one parallel sub-agent's result back into the running parent payload.
|
|
5898
|
+
* Returns the new payload AND the payload that should be persisted on this
|
|
5899
|
+
* specific sub-agent's step record (the *delta* applied for this sub-agent,
|
|
5900
|
+
* not the cumulative state, so audit logs can distinguish each sibling's
|
|
5901
|
+
* contribution).
|
|
5902
|
+
*
|
|
5903
|
+
* @private
|
|
5904
|
+
*/
|
|
5905
|
+
mergeParallelSubAgentResult(params, execution, runningPayload) {
|
|
5906
|
+
if (!execution.result.success) {
|
|
5907
|
+
// Failures don't contribute to the merged payload, but we still record
|
|
5908
|
+
// the sub-agent's own result on its step for forensic visibility.
|
|
5909
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
5910
|
+
}
|
|
5911
|
+
if (execution.relationship) {
|
|
5912
|
+
if (!execution.relationship.SubAgentOutputMapping) {
|
|
5913
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
5914
|
+
}
|
|
5915
|
+
const payloadChange = this.applySubAgentOutputMapping(execution.result.payload, runningPayload, execution.relationship.SubAgentOutputMapping);
|
|
5916
|
+
if (!payloadChange || !payloadChange.updateElements) {
|
|
5917
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
5918
|
+
}
|
|
5919
|
+
const mergeResult = this._payloadManager.applyAgentChangeRequest(runningPayload, payloadChange, {
|
|
5920
|
+
validateChanges: true,
|
|
5921
|
+
logChanges: true,
|
|
5922
|
+
analyzeChanges: true,
|
|
5923
|
+
generateDiff: true,
|
|
5924
|
+
agentName: `${execution.request.name} (related agent mapping)`,
|
|
5925
|
+
verbose: params.verbose === true || IsVerboseLoggingEnabled()
|
|
5926
|
+
});
|
|
5927
|
+
return { mergedPayload: mergeResult.result, stepPayloadAtEnd: execution.result.payload };
|
|
5928
|
+
}
|
|
5929
|
+
// Child agent merge — reverse-scope then merge along upstream paths.
|
|
5930
|
+
let resultPayloadForMerge = execution.result.payload;
|
|
5931
|
+
if (execution.subAgentEntity.PayloadScope) {
|
|
5932
|
+
resultPayloadForMerge = this._payloadManager.reversePayloadScope(execution.result.payload, execution.subAgentEntity.PayloadScope);
|
|
5933
|
+
}
|
|
5934
|
+
const mergeResult = this._payloadManager.mergeUpstreamPayload(execution.request.name, runningPayload, resultPayloadForMerge, execution.upstreamPaths, params.verbose === true || IsVerboseLoggingEnabled());
|
|
5935
|
+
return { mergedPayload: mergeResult.result, stepPayloadAtEnd: resultPayloadForMerge };
|
|
5936
|
+
}
|
|
5937
|
+
/**
|
|
5938
|
+
* Builds the aggregated markdown summary of parallel sub-agent results that
|
|
5939
|
+
* gets appended to the parent's conversation as a `user` message — gives the
|
|
5940
|
+
* Loop agent a single deterministic record of what fanned out and what came
|
|
5941
|
+
* back, regardless of completion order.
|
|
5942
|
+
*
|
|
5943
|
+
* @private
|
|
5944
|
+
*/
|
|
5945
|
+
buildParallelSubAgentSummary(executions) {
|
|
5946
|
+
return executions
|
|
5947
|
+
.map(execution => {
|
|
5948
|
+
const statusEmoji = execution.result.success ? '✅' : '❌';
|
|
5949
|
+
const baseInfo = `${statusEmoji} **Sub-Agent: ${execution.request.name}**\n` +
|
|
5950
|
+
`* Message: "${execution.request.message}"\n` +
|
|
5951
|
+
`* Status: ${execution.result.agentRun?.FinalStep || 'Failed'}`;
|
|
5952
|
+
if (execution.result.agentRun?.ErrorMessage) {
|
|
5953
|
+
return `${baseInfo}\n* Error: ${execution.result.agentRun.ErrorMessage}`;
|
|
5954
|
+
}
|
|
5955
|
+
return baseInfo;
|
|
5956
|
+
})
|
|
5957
|
+
.join('\n\n---\n\n');
|
|
5958
|
+
}
|
|
5959
|
+
/**
|
|
5960
|
+
* Executes multiple sub-agents in parallel (with a concurrency cap) and
|
|
5961
|
+
* merges their output payloads back into the parent sequentially.
|
|
5962
|
+
*
|
|
5963
|
+
* Pipeline:
|
|
5964
|
+
* 1. **Synchronously** prepare each dispatch (resolve entity, push delegation
|
|
5965
|
+
* message, emit progress) so conversation order is deterministic.
|
|
5966
|
+
* 2. Create step entities and run sub-agents with bounded concurrency.
|
|
5967
|
+
* 3. Merge each result into the parent payload sequentially in source order.
|
|
5968
|
+
* 4. Finalize each step entity with its own contribution recorded.
|
|
5969
|
+
* 5. Append an aggregated `user` summary message to the parent conversation.
|
|
5970
|
+
*
|
|
5971
|
+
* Termination semantics: matches the single sub-agent path — if any
|
|
5972
|
+
* dispatched child requested `terminateAfter: true`, the parent terminates
|
|
5973
|
+
* regardless of whether that child succeeded. The parent's reported step is
|
|
5974
|
+
* `Failed` when any child failed, `Success` when terminating cleanly, and
|
|
5975
|
+
* `Retry` otherwise.
|
|
5976
|
+
*
|
|
5977
|
+
* @private
|
|
5978
|
+
*/
|
|
5979
|
+
/**
|
|
5980
|
+
* Worker for one parallel sub-agent dispatch: creates the step entity, builds
|
|
5981
|
+
* the (isolated) input payload, and invokes `ExecuteSubAgent`. Returns
|
|
5982
|
+
* `undefined` for an empty dispatch slot (unresolved sub-agent name) so the
|
|
5983
|
+
* caller can record a synthetic failure in source order.
|
|
5984
|
+
*
|
|
5985
|
+
* @private
|
|
5986
|
+
*/
|
|
5987
|
+
async runSingleParallelSubAgent(params, dispatch, previousDecision, currentPayload, parentStepId, subAgentPayloadOverride, stepCount) {
|
|
5988
|
+
if (!dispatch)
|
|
5989
|
+
return undefined;
|
|
5990
|
+
const { request, subAgentEntity, relationship } = dispatch;
|
|
5991
|
+
const stepEntity = await this.createStepEntity({
|
|
5992
|
+
stepType: 'Sub-Agent',
|
|
5993
|
+
stepName: `Execute Parallel Sub-Agent: ${request.name}`,
|
|
5994
|
+
contextUser: params.contextUser,
|
|
5995
|
+
targetId: subAgentEntity.ID,
|
|
5996
|
+
inputData: {
|
|
5997
|
+
agentName: params.agent.Name,
|
|
5998
|
+
subAgentName: request.name,
|
|
5999
|
+
message: request.message,
|
|
6000
|
+
terminateAfter: request.terminateAfter,
|
|
6001
|
+
conversationMessages: params.conversationMessages,
|
|
6002
|
+
parentAgentHierarchy: this._agentHierarchy,
|
|
6003
|
+
relationshipType: relationship ? 'related' : 'child'
|
|
6004
|
+
},
|
|
6005
|
+
payloadAtStart: currentPayload,
|
|
6006
|
+
parentId: parentStepId
|
|
6007
|
+
});
|
|
6008
|
+
this.incrementExecutionCount(subAgentEntity.ID);
|
|
6009
|
+
const { initialPayload, contextMessage, upstreamPaths } = await this.buildSubAgentInputs(params, dispatch, previousDecision, subAgentPayloadOverride);
|
|
6010
|
+
const result = await this.ExecuteSubAgent(params, request, subAgentEntity, stepEntity, initialPayload, contextMessage, stepCount);
|
|
6011
|
+
return { request, result, subAgentEntity, relationship, stepEntity, upstreamPaths };
|
|
6012
|
+
}
|
|
6013
|
+
/**
|
|
6014
|
+
* Builds a synthetic execution record for an unresolved sub-agent so we can
|
|
6015
|
+
* keep source-order alignment between `subAgentRequests` and `executions`
|
|
6016
|
+
* without throwing inside `Promise.all`.
|
|
6017
|
+
*
|
|
6018
|
+
* @private
|
|
6019
|
+
*/
|
|
6020
|
+
synthesizeUnresolvedSubAgentExecution(request, runningPayload) {
|
|
6021
|
+
return {
|
|
6022
|
+
request,
|
|
6023
|
+
result: {
|
|
6024
|
+
success: false,
|
|
6025
|
+
payload: runningPayload,
|
|
6026
|
+
agentRun: {
|
|
6027
|
+
ErrorMessage: `Sub-agent '${request.name}' not found or not active`,
|
|
6028
|
+
FinalStep: 'Failed'
|
|
6029
|
+
}
|
|
6030
|
+
},
|
|
6031
|
+
subAgentEntity: { ID: '', Name: request.name },
|
|
6032
|
+
upstreamPaths: []
|
|
6033
|
+
};
|
|
6034
|
+
}
|
|
6035
|
+
/**
|
|
6036
|
+
* Sequentially merges each sub-agent's result into the parent payload and
|
|
6037
|
+
* finalizes its step entity with its own contribution recorded.
|
|
6038
|
+
*
|
|
6039
|
+
* @private
|
|
6040
|
+
*/
|
|
6041
|
+
async mergeParallelExecutionsIntoParent(params, subAgentRequests, executions, startingPayload) {
|
|
6042
|
+
let mergedPayload = startingPayload;
|
|
6043
|
+
let anyFailure = false;
|
|
6044
|
+
const allExecutions = [];
|
|
6045
|
+
for (let idx = 0; idx < subAgentRequests.length; idx++) {
|
|
6046
|
+
const execution = executions[idx];
|
|
6047
|
+
if (!execution) {
|
|
6048
|
+
anyFailure = true;
|
|
6049
|
+
allExecutions.push(this.synthesizeUnresolvedSubAgentExecution(subAgentRequests[idx], mergedPayload));
|
|
6050
|
+
continue;
|
|
6051
|
+
}
|
|
6052
|
+
if (!execution.result.success)
|
|
6053
|
+
anyFailure = true;
|
|
6054
|
+
if (execution.result.mediaOutputs?.length)
|
|
6055
|
+
this._mediaOutputs.push(...execution.result.mediaOutputs);
|
|
6056
|
+
if (execution.result.fileOutputs?.length)
|
|
6057
|
+
this._fileOutputs.push(...execution.result.fileOutputs);
|
|
6058
|
+
const { mergedPayload: newMerged, stepPayloadAtEnd } = this.mergeParallelSubAgentResult(params, execution, mergedPayload);
|
|
6059
|
+
mergedPayload = newMerged;
|
|
6060
|
+
allExecutions.push(execution);
|
|
6061
|
+
await this.recordParallelStepCompletion(execution, stepPayloadAtEnd);
|
|
6062
|
+
}
|
|
6063
|
+
return { mergedPayload, anyFailure, allExecutions };
|
|
6064
|
+
}
|
|
6065
|
+
/**
|
|
6066
|
+
* Persists per-sibling step state (`PayloadAtEnd` is THIS sub-agent's
|
|
6067
|
+
* contribution, not the cumulative parent state) and finalizes its step
|
|
6068
|
+
* entity.
|
|
6069
|
+
*
|
|
6070
|
+
* @private
|
|
6071
|
+
*/
|
|
6072
|
+
async recordParallelStepCompletion(execution, stepPayloadAtEnd) {
|
|
6073
|
+
if (!execution.stepEntity)
|
|
6074
|
+
return;
|
|
6075
|
+
execution.stepEntity.PayloadAtEnd = this.serializePayloadAtEnd(stepPayloadAtEnd);
|
|
6076
|
+
await this.finalizeStepEntity(execution.stepEntity, execution.result.success, execution.result.agentRun?.ErrorMessage, {
|
|
6077
|
+
subAgentResult: {
|
|
6078
|
+
success: execution.result.success,
|
|
6079
|
+
finalStep: execution.result.agentRun?.FinalStep,
|
|
6080
|
+
errorMessage: execution.result.agentRun?.ErrorMessage,
|
|
6081
|
+
stepCount: execution.result.agentRun?.Steps?.length || 0,
|
|
6082
|
+
},
|
|
6083
|
+
shouldTerminate: execution.request.terminateAfter === true,
|
|
6084
|
+
nextStep: execution.request.terminateAfter === true ? 'success' : 'retry'
|
|
6085
|
+
});
|
|
6086
|
+
}
|
|
6087
|
+
async executeParallelSubAgents(params, subAgentRequests, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
6088
|
+
const currentPayload = previousDecision.newPayload;
|
|
6089
|
+
// Synchronous pre-flight — order-stable transcript + progress events.
|
|
6090
|
+
const dispatches = subAgentRequests.map(req => this.prepareParallelSubAgentDispatch(params, req, stepCount));
|
|
6091
|
+
// Bounded parallel dispatch.
|
|
6092
|
+
const executions = await this.mapWithConcurrency(dispatches, PARALLEL_SUBAGENT_CONCURRENCY_LIMIT, (dispatch) => this.runSingleParallelSubAgent(params, dispatch, previousDecision, currentPayload, parentStepId, subAgentPayloadOverride, stepCount));
|
|
6093
|
+
// Sequential merge + per-sibling step finalization.
|
|
6094
|
+
const { mergedPayload, anyFailure, allExecutions } = await this.mergeParallelExecutionsIntoParent(params, subAgentRequests, executions, currentPayload);
|
|
6095
|
+
// Aggregated summary appended to the parent transcript.
|
|
6096
|
+
params.conversationMessages.push({
|
|
6097
|
+
role: 'user',
|
|
6098
|
+
content: `Parallel Sub-Agents Completed:\n\n${this.buildParallelSubAgentSummary(allExecutions)}`
|
|
6099
|
+
});
|
|
6100
|
+
// Termination semantics: matches the single sub-agent path —
|
|
6101
|
+
// `terminateAfter` triggers parent termination regardless of the child's
|
|
6102
|
+
// success/failure. The parent's step reflects whether any child failed:
|
|
6103
|
+
// Failed if any did, Success if terminating cleanly, otherwise Retry.
|
|
6104
|
+
const shouldTerminateParent = allExecutions.some(e => e.request.terminateAfter === true);
|
|
6105
|
+
return {
|
|
6106
|
+
step: anyFailure ? 'Failed' : (shouldTerminateParent ? 'Success' : 'Retry'),
|
|
6107
|
+
terminate: shouldTerminateParent,
|
|
6108
|
+
newPayload: mergedPayload,
|
|
6109
|
+
previousPayload: previousDecision.newPayload
|
|
5640
6110
|
};
|
|
5641
6111
|
}
|
|
5642
6112
|
/**
|
|
@@ -6199,7 +6669,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6199
6669
|
// Update step entity with ActionExecutionLog ID if available
|
|
6200
6670
|
if (actionResult.LogEntry?.ID) {
|
|
6201
6671
|
stepEntity.TargetLogID = actionResult.LogEntry.ID;
|
|
6202
|
-
|
|
6672
|
+
this.queueStepSave(stepEntity);
|
|
6203
6673
|
}
|
|
6204
6674
|
// Prepare output data with action result
|
|
6205
6675
|
const outputData = {
|
|
@@ -7471,6 +7941,28 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7471
7941
|
* @private
|
|
7472
7942
|
*/
|
|
7473
7943
|
async finalizeAgentRun(finalStep, payload, contextUser) {
|
|
7944
|
+
// Await every pending step save (success OR failure) and accumulate diagnostics.
|
|
7945
|
+
// We use allSettled so a single failure doesn't shadow the rest, and we drain
|
|
7946
|
+
// both queues afterwards so an instance reused for another run doesn't leak
|
|
7947
|
+
// settled promises.
|
|
7948
|
+
const pending = this._pendingSaves;
|
|
7949
|
+
this._pendingSaves = [];
|
|
7950
|
+
this._stepSavePromises.clear();
|
|
7951
|
+
if (pending.length > 0) {
|
|
7952
|
+
const settled = await Promise.allSettled(pending);
|
|
7953
|
+
const rejections = settled.filter(s => s.status === 'rejected');
|
|
7954
|
+
const falses = settled.filter(s => s.status === 'fulfilled' && s.value === false).length;
|
|
7955
|
+
for (const r of rejections) {
|
|
7956
|
+
LogError(`Pending step save rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
7957
|
+
}
|
|
7958
|
+
const totalFailures = rejections.length + falses;
|
|
7959
|
+
if (totalFailures > 0 && this._agentRun) {
|
|
7960
|
+
const note = `${totalFailures} step record save(s) failed during this run; see logs for details.`;
|
|
7961
|
+
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
7962
|
+
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
7963
|
+
: note;
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7474
7966
|
// Only resolve media placeholders for ROOT agents (depth === 0)
|
|
7475
7967
|
// Sub-agents keep placeholders intact so parent agents don't get huge base64 in their context
|
|
7476
7968
|
// The root agent resolves all placeholders when returning the final result to the UI
|
|
@@ -7483,12 +7975,6 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7483
7975
|
const resolvedActionableCommands = (finalStep.actionableCommands && isRootAgent)
|
|
7484
7976
|
? this.resolveMediaPlaceholdersInPayload(finalStep.actionableCommands)
|
|
7485
7977
|
: finalStep.actionableCommands;
|
|
7486
|
-
// For root agents: process message for media placeholders
|
|
7487
|
-
// This promotes referenced media (sets persist=true) and strips media HTML tags
|
|
7488
|
-
// so images display via ConversationDetailAttachment instead of embedded in message
|
|
7489
|
-
const processedMessage = (finalStep.message && isRootAgent)
|
|
7490
|
-
? this.processMessageMediaPlaceholders(finalStep.message)
|
|
7491
|
-
: finalStep.message;
|
|
7492
7978
|
if (this._agentRun) {
|
|
7493
7979
|
this._agentRun.CompletedAt = new Date();
|
|
7494
7980
|
this._agentRun.Success = finalStep.step === 'Success' || finalStep.step === 'Chat';
|
|
@@ -7513,7 +7999,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7513
7999
|
}
|
|
7514
8000
|
this._agentRun.Result = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
7515
8001
|
this._agentRun.FinalStep = finalStep.step;
|
|
7516
|
-
this._agentRun.Message =
|
|
8002
|
+
this._agentRun.Message = finalStep.message;
|
|
7517
8003
|
// Set the FinalPayloadObject - this will automatically stringify for the DB
|
|
7518
8004
|
this._agentRun.FinalPayloadObject = resolvedPayload;
|
|
7519
8005
|
this._agentRun.FinalPayload = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
@@ -7532,10 +8018,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7532
8018
|
if (finalStep.promoteMediaOutputs && finalStep.promoteMediaOutputs.length > 0) {
|
|
7533
8019
|
this.promoteMediaOutputs(finalStep.promoteMediaOutputs);
|
|
7534
8020
|
}
|
|
7535
|
-
// Return unified media outputs
|
|
7536
|
-
// -
|
|
7537
|
-
// - Intercepted binary with refIds (persist=false unless placeholder was resolved)
|
|
7538
|
-
// Sub-agents pass their full mediaOutputs to parent for merging and placeholder resolution.
|
|
8021
|
+
// Return unified media outputs — all items are persisted by AgentRunner.
|
|
8022
|
+
// Sub-agents pass their mediaOutputs to parent for merging and placeholder resolution.
|
|
7539
8023
|
return {
|
|
7540
8024
|
success: finalStep.step === 'Success' || finalStep.step === 'Chat',
|
|
7541
8025
|
payload: resolvedPayload,
|
|
@@ -7607,8 +8091,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7607
8091
|
/**
|
|
7608
8092
|
* Increments the execution count for an item (action or sub-agent).
|
|
7609
8093
|
*
|
|
8094
|
+
* Exposed as `protected` so driver sub-classes performing custom dispatch
|
|
8095
|
+
* bump the same per-item counter the framework checks against execution
|
|
8096
|
+
* guardrails — without this, custom dispatch silently bypasses limits.
|
|
8097
|
+
*
|
|
7610
8098
|
* @param itemId - The item ID to increment (action ID or sub-agent ID)
|
|
7611
|
-
* @
|
|
8099
|
+
* @protected
|
|
7612
8100
|
*/
|
|
7613
8101
|
incrementExecutionCount(itemId) {
|
|
7614
8102
|
const currentCount = this._executionCounts.get(itemId) || 0;
|
|
@@ -7619,7 +8107,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7619
8107
|
*
|
|
7620
8108
|
* @param itemId - The item ID to get count for
|
|
7621
8109
|
* @returns The execution count (0 if never executed)
|
|
7622
|
-
* @
|
|
8110
|
+
* @protected
|
|
7623
8111
|
*/
|
|
7624
8112
|
getExecutionCount(itemId) {
|
|
7625
8113
|
return this._executionCounts.get(itemId) || 0;
|