@memberjunction/ai-agents 5.37.0 → 5.39.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/ArtifactToolManager.d.ts +13 -0
- package/dist/ArtifactToolManager.d.ts.map +1 -1
- package/dist/ArtifactToolManager.js +60 -1
- package/dist/ArtifactToolManager.js.map +1 -1
- package/dist/agent-run-watchdog.d.ts +109 -0
- package/dist/agent-run-watchdog.d.ts.map +1 -0
- package/dist/agent-run-watchdog.js +278 -0
- package/dist/agent-run-watchdog.js.map +1 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts +13 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +3 -1
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +21 -3
- 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 +51 -13
- 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/artifact-tools/JSONToolLibrary.d.ts.map +1 -1
- package/dist/artifact-tools/JSONToolLibrary.js +7 -1
- package/dist/artifact-tools/JSONToolLibrary.js.map +1 -1
- package/dist/base-agent.d.ts +331 -80
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +843 -155
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/coerce.d.ts +43 -0
- package/dist/pipeline/coerce.d.ts.map +1 -0
- package/dist/pipeline/coerce.js +92 -0
- package/dist/pipeline/coerce.js.map +1 -0
- package/dist/pipeline/index.d.ts +20 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +20 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/jsonpath-eval.d.ts +29 -0
- package/dist/pipeline/jsonpath-eval.d.ts.map +1 -0
- package/dist/pipeline/jsonpath-eval.js +144 -0
- package/dist/pipeline/jsonpath-eval.js.map +1 -0
- package/dist/pipeline/operators.d.ts +15 -0
- package/dist/pipeline/operators.d.ts.map +1 -0
- package/dist/pipeline/operators.js +332 -0
- package/dist/pipeline/operators.js.map +1 -0
- package/dist/pipeline/path.d.ts +16 -0
- package/dist/pipeline/path.d.ts.map +1 -0
- package/dist/pipeline/path.js +34 -0
- package/dist/pipeline/path.js.map +1 -0
- package/dist/pipeline/pipeline-docs.d.ts +6 -0
- package/dist/pipeline/pipeline-docs.d.ts.map +1 -0
- package/dist/pipeline/pipeline-docs.js +137 -0
- package/dist/pipeline/pipeline-docs.js.map +1 -0
- package/dist/pipeline/pipeline-executor.d.ts +72 -0
- package/dist/pipeline/pipeline-executor.d.ts.map +1 -0
- package/dist/pipeline/pipeline-executor.js +326 -0
- package/dist/pipeline/pipeline-executor.js.map +1 -0
- package/dist/pipeline/pipeline-registry.d.ts +24 -0
- package/dist/pipeline/pipeline-registry.d.ts.map +1 -0
- package/dist/pipeline/pipeline-registry.js +28 -0
- package/dist/pipeline/pipeline-registry.js.map +1 -0
- package/dist/pipeline/pipeline.types.d.ts +89 -0
- package/dist/pipeline/pipeline.types.d.ts.map +1 -0
- package/dist/pipeline/pipeline.types.js +13 -0
- package/dist/pipeline/pipeline.types.js.map +1 -0
- package/dist/pipeline/predicate.d.ts +23 -0
- package/dist/pipeline/predicate.d.ts.map +1 -0
- package/dist/pipeline/predicate.js +306 -0
- package/dist/pipeline/predicate.js.map +1 -0
- package/dist/pipeline/providers/action-provider.d.ts +21 -0
- package/dist/pipeline/providers/action-provider.d.ts.map +1 -0
- package/dist/pipeline/providers/action-provider.js +24 -0
- package/dist/pipeline/providers/action-provider.js.map +1 -0
- package/dist/pipeline/providers/artifact-tool-provider.d.ts +20 -0
- package/dist/pipeline/providers/artifact-tool-provider.d.ts.map +1 -0
- package/dist/pipeline/providers/artifact-tool-provider.js +27 -0
- package/dist/pipeline/providers/artifact-tool-provider.js.map +1 -0
- package/dist/pipeline/providers/serialize.d.ts +23 -0
- package/dist/pipeline/providers/serialize.d.ts.map +1 -0
- package/dist/pipeline/providers/serialize.js +99 -0
- package/dist/pipeline/providers/serialize.js.map +1 -0
- package/dist/pipeline/template.d.ts +18 -0
- package/dist/pipeline/template.d.ts.map +1 -0
- package/dist/pipeline/template.js +47 -0
- package/dist/pipeline/template.js.map +1 -0
- package/package.json +19 -17
package/dist/base-agent.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase } from '@memberjunction/core-entities';
|
|
14
|
-
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled } from '@memberjunction/core';
|
|
14
|
+
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
15
|
+
import { AgentRunWatchdog } from './agent-run-watchdog.js';
|
|
15
16
|
import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
16
17
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
17
18
|
import { CopyScalarsAndArrays, JSONValidator, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
|
|
@@ -26,6 +27,7 @@ import { AgentRunner } from './AgentRunner.js';
|
|
|
26
27
|
import { PayloadManager } from './PayloadManager.js';
|
|
27
28
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
28
29
|
import { ArtifactToolManager } from './ArtifactToolManager.js';
|
|
30
|
+
import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
|
|
29
31
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
30
32
|
import { ClientToolRequestManager } from './ClientToolRequestManager.js';
|
|
31
33
|
import { ConversationMessageResolver } from './utils/ConversationMessageResolver.js';
|
|
@@ -73,6 +75,12 @@ import _ from 'lodash';
|
|
|
73
75
|
* const result = await agent.Execute(params);
|
|
74
76
|
* ```
|
|
75
77
|
*/
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of sub-agents to dispatch concurrently when a Loop agent
|
|
80
|
+
* returns a `subAgents` array. Prevents a misbehaving LLM (or an over-eager
|
|
81
|
+
* one) from saturating the model API / DB pool with N concurrent runs.
|
|
82
|
+
*/
|
|
83
|
+
const PARALLEL_SUBAGENT_CONCURRENCY_LIMIT = 5;
|
|
76
84
|
export class BaseAgent {
|
|
77
85
|
constructor() {
|
|
78
86
|
/**
|
|
@@ -80,6 +88,21 @@ export class BaseAgent {
|
|
|
80
88
|
* @private
|
|
81
89
|
*/
|
|
82
90
|
this._promptRunner = new AIPromptRunner();
|
|
91
|
+
/**
|
|
92
|
+
* List of pending database save Promises for observability step records.
|
|
93
|
+
* Awaited concurrently in finalizeAgentRun() to prevent blocking.
|
|
94
|
+
*/
|
|
95
|
+
this._pendingSaves = [];
|
|
96
|
+
/**
|
|
97
|
+
* Queue map to chain database saves sequentially per step entity.
|
|
98
|
+
* Prevents UPDATE queries running before INSERT queries on quick steps.
|
|
99
|
+
*
|
|
100
|
+
* Keyed by the step ENTITY INSTANCE, not its `ID`: a new step's `ID` is empty at create time and
|
|
101
|
+
* only gets populated during its INSERT `Save()`, so keying by `ID` would file the create and the
|
|
102
|
+
* finalize under different buckets and defeat the chain — letting the UPDATE race ahead of the
|
|
103
|
+
* INSERT on millisecond-fast steps (e.g. pipelines), which left them stuck at `Running`.
|
|
104
|
+
*/
|
|
105
|
+
this._stepSavePromises = new Map();
|
|
83
106
|
/**
|
|
84
107
|
* Active per-request metadata provider, set at the start of Execute().
|
|
85
108
|
* Defaults to the global Metadata.Provider; overridden when a per-request
|
|
@@ -418,7 +441,12 @@ export class BaseAgent {
|
|
|
418
441
|
* This prevents context overflow when action results contain large base64 data (images, audio, video).
|
|
419
442
|
*
|
|
420
443
|
* Uses generic ValueType=MediaOutput detection from action metadata to identify media output params.
|
|
421
|
-
*
|
|
444
|
+
*
|
|
445
|
+
* Intercepted media is stored in `_mediaOutputs` with a generated `refId` and is **always
|
|
446
|
+
* persisted** by `AgentRunner` — all media outputs are saved to `AIAgentRunMedia` +
|
|
447
|
+
* `ConversationDetailAttachment` (which auto-pairs to an artifact via the server hook).
|
|
448
|
+
* The `${media:<refId>}` placeholder injected into the action result keeps the LLM's
|
|
449
|
+
* context window small; the LLM is told the media will be displayed automatically.
|
|
422
450
|
*
|
|
423
451
|
* @param actionParams - The output parameters from an action result
|
|
424
452
|
* @param actionEntity - Optional action entity metadata for ValueType checking
|
|
@@ -454,11 +482,10 @@ export class BaseAgent {
|
|
|
454
482
|
if (media.data && media.data.length > BaseAgent.LARGE_BINARY_THRESHOLD) {
|
|
455
483
|
// Generate unique reference ID
|
|
456
484
|
const refId = `media-${Date.now().toString(36)}-${i}-${Math.random().toString(36).substring(2, 8)}`;
|
|
457
|
-
// Store in unified media outputs
|
|
485
|
+
// Store in unified media outputs — always persisted by AgentRunner.
|
|
458
486
|
this._mediaOutputs.push({
|
|
459
487
|
...media,
|
|
460
488
|
refId,
|
|
461
|
-
persist: false // Not persisted unless placeholder is resolved in final output
|
|
462
489
|
});
|
|
463
490
|
references.push(`\${media:${refId}}`);
|
|
464
491
|
extractedCount++;
|
|
@@ -472,7 +499,7 @@ export class BaseAgent {
|
|
|
472
499
|
Value: {
|
|
473
500
|
mediaReferences: references,
|
|
474
501
|
count: mediaItems.length,
|
|
475
|
-
note: `${extractedCount} media item(s) extracted
|
|
502
|
+
note: `${extractedCount} media item(s) extracted and will be displayed to the user automatically.`
|
|
476
503
|
}
|
|
477
504
|
});
|
|
478
505
|
this.logStatus(`📦 Extracted ${extractedCount} ${param.Name} item(s) to media references`, true);
|
|
@@ -485,14 +512,13 @@ export class BaseAgent {
|
|
|
485
512
|
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
486
513
|
if (isMediaOutputParam || base64Pattern.test(param.Value.substring(0, 1000))) {
|
|
487
514
|
const refId = `data-${Date.now().toString(36)}-${Math.random().toString(36).substring(2, 8)}`;
|
|
488
|
-
// Store in unified media outputs
|
|
515
|
+
// Store in unified media outputs — always persisted by AgentRunner.
|
|
489
516
|
this._mediaOutputs.push({
|
|
490
517
|
modality: 'Image', // Default to image, could be enhanced with mime detection
|
|
491
518
|
mimeType: 'application/octet-stream',
|
|
492
519
|
data: param.Value,
|
|
493
520
|
label: `Media data from ${param.Name}`,
|
|
494
521
|
refId,
|
|
495
|
-
persist: false
|
|
496
522
|
});
|
|
497
523
|
sanitizedParams.push({
|
|
498
524
|
Name: param.Name,
|
|
@@ -509,40 +535,50 @@ export class BaseAgent {
|
|
|
509
535
|
return sanitizedParams;
|
|
510
536
|
}
|
|
511
537
|
/**
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
538
|
+
* Substitutes `${media:<refId>}` placeholders in a string with the actual
|
|
539
|
+
* data URI (`data:<mime>;base64,<bytes>`) of the matching intercepted media item.
|
|
540
|
+
*
|
|
541
|
+
* Used for payload / actionable-command resolution at the terminal step, where the
|
|
542
|
+
* LLM wants to embed image (or other media) data inline at a specific position in
|
|
543
|
+
* its structured output rather than as a trailing attachment card. The string
|
|
544
|
+
* variant of placeholder resolution; recursive walker lives in
|
|
545
|
+
* {@link resolveMediaPlaceholdersInPayload}.
|
|
546
|
+
*
|
|
547
|
+
* This function only does substitution — it has no persistence side effects.
|
|
515
548
|
*
|
|
516
549
|
* @param text - The string that may contain media placeholders
|
|
517
|
-
* @returns String with placeholders resolved to actual data URIs
|
|
550
|
+
* @returns String with placeholders resolved to actual data URIs (or the original
|
|
551
|
+
* placeholder if the refId is unknown — defensive, shouldn't happen)
|
|
518
552
|
* @private
|
|
519
553
|
* @since 3.1.0
|
|
520
554
|
*/
|
|
521
555
|
resolveMediaPlaceholdersInString(text) {
|
|
522
|
-
//
|
|
556
|
+
// Fast path: nothing to resolve if there are no intercepted media items.
|
|
523
557
|
const hasRefIds = this._mediaOutputs.some(m => m.refId);
|
|
524
558
|
if (!text || !hasRefIds) {
|
|
525
559
|
return text;
|
|
526
560
|
}
|
|
527
|
-
// Match ${media:ref-id} pattern
|
|
561
|
+
// Match ${media:ref-id} pattern (lowercase letters, digits, dashes only —
|
|
562
|
+
// matches the IDs generated by interceptLargeBinaryContent).
|
|
528
563
|
const placeholderRegex = /\$\{media:([a-z0-9-]+)\}/g;
|
|
529
564
|
return text.replace(placeholderRegex, (match, refId) => {
|
|
530
565
|
const media = this._mediaOutputs.find(m => m.refId === refId);
|
|
531
566
|
if (media?.data) {
|
|
532
|
-
// Mark for persistence since it's being used in final output
|
|
533
|
-
media.persist = true;
|
|
534
567
|
return `data:${media.mimeType};base64,${media.data}`;
|
|
535
568
|
}
|
|
536
|
-
//
|
|
569
|
+
// Unknown refId — leave the placeholder in place rather than emit a broken
|
|
570
|
+
// data URI. Defensive; this branch should not fire in normal flow.
|
|
537
571
|
this.logStatus(`⚠️ Media reference '${refId}' not found in registry`, true);
|
|
538
572
|
return match;
|
|
539
573
|
});
|
|
540
574
|
}
|
|
541
575
|
/**
|
|
542
|
-
* Resolves media placeholders
|
|
543
|
-
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
576
|
+
* Resolves `${media:<refId>}` placeholders anywhere inside an arbitrary payload.
|
|
577
|
+
* - Strings: resolves placeholders directly
|
|
578
|
+
* - Objects: recursively processes every string property
|
|
579
|
+
* - Arrays: recursively processes every element
|
|
580
|
+
*
|
|
581
|
+
* Pure resolution — no persistence side effects.
|
|
546
582
|
*
|
|
547
583
|
* @param payload - The payload that may contain media placeholders in string values
|
|
548
584
|
* @returns Payload with all placeholders resolved to actual data URIs
|
|
@@ -550,21 +586,12 @@ export class BaseAgent {
|
|
|
550
586
|
* @since 3.1.0
|
|
551
587
|
*/
|
|
552
588
|
resolveMediaPlaceholdersInPayload(payload) {
|
|
553
|
-
//
|
|
589
|
+
// Fast path: nothing to resolve if no intercepted media exists.
|
|
554
590
|
const hasRefIds = this._mediaOutputs.some(m => m.refId);
|
|
555
591
|
if (!hasRefIds) {
|
|
556
592
|
return payload;
|
|
557
593
|
}
|
|
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;
|
|
594
|
+
return this.resolveMediaPlaceholdersRecursive(payload);
|
|
568
595
|
}
|
|
569
596
|
/**
|
|
570
597
|
* Recursively resolves media placeholders in any value.
|
|
@@ -593,54 +620,26 @@ export class BaseAgent {
|
|
|
593
620
|
// Return primitives (numbers, booleans) as-is
|
|
594
621
|
return value;
|
|
595
622
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
623
|
+
// ───────────────────────── Sub-class state accessors ──────────────────────────
|
|
624
|
+
// Read-only `protected` getters so driver sub-classes (e.g. Skip) can inspect
|
|
625
|
+
// the current run's state without being able to corrupt internal invariants.
|
|
626
|
+
// Mutations still flow through the framework's own methods (createStepEntity,
|
|
627
|
+
// queueStepSave, incrementExecutionCount, etc.).
|
|
628
|
+
// (`AgentRun` and `MediaOutputs` are already public getters above; the
|
|
629
|
+
// accessors below cover state that previously had no external surface.)
|
|
630
|
+
/** Depth of this agent in the execution hierarchy (0 = root). @protected */
|
|
631
|
+
get Depth() { return this._depth; }
|
|
632
|
+
/** Agent name hierarchy from root to current (e.g. `['Sage', 'Skip', 'Researcher']`). @protected */
|
|
633
|
+
get AgentHierarchy() { return this._agentHierarchy; }
|
|
634
|
+
/** Parent step counts used to build the `2.1.3` hierarchical step label. @protected */
|
|
635
|
+
get ParentStepCounts() { return this._parentStepCounts; }
|
|
636
|
+
/**
|
|
637
|
+
* Accumulated file outputs (PDF, Excel, Word, etc.) produced this run.
|
|
638
|
+
* Mirrors the existing `MediaOutputs` accessor pattern but is scoped to
|
|
639
|
+
* driver sub-classes since it's a more internal collection.
|
|
640
|
+
* @protected
|
|
609
641
|
*/
|
|
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
|
-
}
|
|
642
|
+
get FileOutputs() { return this._fileOutputs; }
|
|
644
643
|
/**
|
|
645
644
|
* Gets the current validation retry count for the agent run.
|
|
646
645
|
* This count tracks how many times the agent has retried validation
|
|
@@ -1821,6 +1820,21 @@ export class BaseAgent {
|
|
|
1821
1820
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
1822
1821
|
this.logStatus(`[ArtifactTools] Artifacts present but tools disabled by agent config (includeArtifactToolsDocs=false)`, true, params);
|
|
1823
1822
|
}
|
|
1823
|
+
// Inject pipeline tool docs when pipelines are enabled and at least one source exists.
|
|
1824
|
+
// A pipeline's first step must be a source (Action or artifact tool); with none
|
|
1825
|
+
// available pipelines are impossible, so BuildPipelineToolDocs returns '' and the
|
|
1826
|
+
// template's `{{ _PIPELINE_TOOLS }}` block stays empty.
|
|
1827
|
+
const pipelineDocsEnabled = agentTypePromptParams?.includePipelineDocs !== false;
|
|
1828
|
+
if (pipelineDocsEnabled) {
|
|
1829
|
+
const sourceNames = [
|
|
1830
|
+
...this.getEffectiveActionsForValidation(params.agent.ID).map((a) => a.Name),
|
|
1831
|
+
...this._artifactToolManager.GetAvailableToolNames(),
|
|
1832
|
+
];
|
|
1833
|
+
const pipelineDocs = BuildPipelineToolDocs(sourceNames);
|
|
1834
|
+
if (pipelineDocs) {
|
|
1835
|
+
promptParams.data['_PIPELINE_TOOLS'] = pipelineDocs;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1824
1838
|
// Pass file artifacts as candidate native file inputs.
|
|
1825
1839
|
// The AIPromptRunner will check these against the resolved driver's
|
|
1826
1840
|
// FileCapabilities and attach qualifying files as native content blocks.
|
|
@@ -1977,51 +1991,85 @@ export class BaseAgent {
|
|
|
1977
1991
|
* @param nextStep
|
|
1978
1992
|
* @returns
|
|
1979
1993
|
*/
|
|
1994
|
+
/**
|
|
1995
|
+
* Returns the list of sub-agent requests on a next-step decision, normalizing
|
|
1996
|
+
* the singular (`subAgent`) and plural (`subAgents`) forms. Plural takes
|
|
1997
|
+
* precedence when both are present (parallel fan-out); otherwise the singular
|
|
1998
|
+
* form is wrapped into a single-element array. Empty if neither is set.
|
|
1999
|
+
*
|
|
2000
|
+
* Use this anywhere code needs to enumerate the sub-agents an LLM requested
|
|
2001
|
+
* — keeps validation and execution paths consistent and avoids the regression
|
|
2002
|
+
* where one path read `.subAgent?.name` and missed parallel requests.
|
|
2003
|
+
*/
|
|
2004
|
+
getRequestedSubAgents(nextStep) {
|
|
2005
|
+
if (!nextStep)
|
|
2006
|
+
return [];
|
|
2007
|
+
if (nextStep.subAgents && nextStep.subAgents.length > 0) {
|
|
2008
|
+
return nextStep.subAgents;
|
|
2009
|
+
}
|
|
2010
|
+
return nextStep.subAgent ? [nextStep.subAgent] : [];
|
|
2011
|
+
}
|
|
1980
2012
|
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
2013
|
const curAgentSubAgents = AIEngine.Instance.GetSubAgents(params.agent.ID, 'Active');
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2014
|
+
// Collect requested sub-agents. Prefer plural `subAgents` (parallel fan-out);
|
|
2015
|
+
// fall back to singular `subAgent` for the classic single-sub-agent next step.
|
|
2016
|
+
const requested = this.getRequestedSubAgents(nextStep);
|
|
2017
|
+
if (requested.length === 0) {
|
|
2018
|
+
this.logError(`Sub-agent 'undefined' not found or not active for agent '${params.agent.Name}'`, {
|
|
1987
2019
|
agent: params.agent,
|
|
1988
2020
|
category: 'SubAgentExecution'
|
|
1989
2021
|
});
|
|
1990
|
-
// Increment validation retry count since we're changing to Retry
|
|
1991
2022
|
if (nextStep.step !== 'Retry') {
|
|
1992
2023
|
this._generalValidationRetryCount++;
|
|
1993
2024
|
}
|
|
1994
2025
|
return {
|
|
1995
2026
|
step: 'Retry',
|
|
1996
|
-
terminate: false,
|
|
1997
|
-
errorMessage: `Sub-agent '
|
|
2027
|
+
terminate: false,
|
|
2028
|
+
errorMessage: `Sub-agent 'undefined' not found or not active`
|
|
1998
2029
|
};
|
|
1999
2030
|
}
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
const
|
|
2003
|
-
|
|
2004
|
-
|
|
2031
|
+
// Validate each requested sub-agent: existence + MaxExecutionsPerRun
|
|
2032
|
+
for (const req of requested) {
|
|
2033
|
+
const name = req?.name;
|
|
2034
|
+
const subAgent = curAgentSubAgents.find(a => a.Name.trim().toLowerCase() === name?.trim().toLowerCase());
|
|
2035
|
+
if (!name || !subAgent) {
|
|
2036
|
+
this.logError(`Sub-agent '${name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
2005
2037
|
agent: params.agent,
|
|
2006
|
-
category: 'SubAgentExecution'
|
|
2007
|
-
metadata: {
|
|
2008
|
-
subAgentName: name,
|
|
2009
|
-
executionCount,
|
|
2010
|
-
maxExecutions: subAgent.MaxExecutionsPerRun
|
|
2011
|
-
}
|
|
2038
|
+
category: 'SubAgentExecution'
|
|
2012
2039
|
});
|
|
2013
|
-
// Increment validation retry count since we're changing to Retry
|
|
2014
2040
|
if (nextStep.step !== 'Retry') {
|
|
2015
2041
|
this._generalValidationRetryCount++;
|
|
2016
2042
|
}
|
|
2017
2043
|
return {
|
|
2018
2044
|
step: 'Retry',
|
|
2019
2045
|
terminate: false,
|
|
2020
|
-
errorMessage: `Sub-agent '${name}'
|
|
2046
|
+
errorMessage: `Sub-agent '${name}' not found or not active`
|
|
2021
2047
|
};
|
|
2022
2048
|
}
|
|
2049
|
+
if (subAgent.MaxExecutionsPerRun != null) {
|
|
2050
|
+
const executionCount = await this.getSubAgentExecutionCount(agentRun.ID, subAgent.ID);
|
|
2051
|
+
if (executionCount >= subAgent.MaxExecutionsPerRun) {
|
|
2052
|
+
this.logError(`Sub-agent '${name}' has reached its maximum execution limit of ${subAgent.MaxExecutionsPerRun}`, {
|
|
2053
|
+
agent: params.agent,
|
|
2054
|
+
category: 'SubAgentExecution',
|
|
2055
|
+
metadata: {
|
|
2056
|
+
subAgentName: name,
|
|
2057
|
+
executionCount,
|
|
2058
|
+
maxExecutions: subAgent.MaxExecutionsPerRun
|
|
2059
|
+
}
|
|
2060
|
+
});
|
|
2061
|
+
if (nextStep.step !== 'Retry') {
|
|
2062
|
+
this._generalValidationRetryCount++;
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
step: 'Retry',
|
|
2066
|
+
terminate: false,
|
|
2067
|
+
errorMessage: `Sub-agent '${name}' has reached its maximum execution limit of ${subAgent.MaxExecutionsPerRun}`
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2023
2071
|
}
|
|
2024
|
-
//
|
|
2072
|
+
// All requested sub-agents are valid
|
|
2025
2073
|
return nextStep;
|
|
2026
2074
|
}
|
|
2027
2075
|
/**
|
|
@@ -3279,9 +3327,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3279
3327
|
const body = toolResults.map((r, i) => {
|
|
3280
3328
|
const heading = `### ${i + 1}. ${r.artifactId}.${r.tool}(${JSON.stringify(r.input)})`;
|
|
3281
3329
|
if (r.result.success) {
|
|
3282
|
-
const
|
|
3330
|
+
const raw = typeof r.result.data === 'string'
|
|
3283
3331
|
? r.result.data
|
|
3284
3332
|
: JSON.stringify(r.result.data, null, 2);
|
|
3333
|
+
const data = this.capStandaloneToolResultText(raw);
|
|
3285
3334
|
return `${heading}\n\`\`\`json\n${data}\n\`\`\``;
|
|
3286
3335
|
}
|
|
3287
3336
|
return `${heading}\n**Error:** ${r.result.errorMessage}`;
|
|
@@ -3305,6 +3354,154 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3305
3354
|
};
|
|
3306
3355
|
params.conversationMessages.push(message);
|
|
3307
3356
|
}
|
|
3357
|
+
/**
|
|
3358
|
+
* Character budget (~4 chars/token) for a SINGLE standalone artifact-tool result injected into
|
|
3359
|
+
* the conversation. A `get_full` on a large artifact can otherwise dump the whole thing into
|
|
3360
|
+
* context and overflow the model's window — the exact failure pipelines exist to avoid. Override
|
|
3361
|
+
* in a subclass to tune. Pipelines are unaffected: their intermediate results never flow through
|
|
3362
|
+
* here, and the executor already caps a pipeline's final output.
|
|
3363
|
+
*
|
|
3364
|
+
* @protected
|
|
3365
|
+
*/
|
|
3366
|
+
get maxStandaloneToolResultChars() {
|
|
3367
|
+
return 100_000; // ~25k tokens
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Bound a standalone tool result to {@link maxStandaloneToolResultChars}: return a head slice
|
|
3371
|
+
* plus a redirect that teaches the agent to page (`get_rows`) or reduce (`pipeline`) instead of
|
|
3372
|
+
* reading a whole large artifact. Mirrors how read/search tools cap output at the tool boundary.
|
|
3373
|
+
*
|
|
3374
|
+
* @protected
|
|
3375
|
+
*/
|
|
3376
|
+
capStandaloneToolResultText(text) {
|
|
3377
|
+
const budget = this.maxStandaloneToolResultChars;
|
|
3378
|
+
if (text.length <= budget) {
|
|
3379
|
+
return text;
|
|
3380
|
+
}
|
|
3381
|
+
const omitted = text.length - budget;
|
|
3382
|
+
return (text.slice(0, budget) +
|
|
3383
|
+
`\n\n…[truncated ${omitted.toLocaleString()} chars. This artifact is too large to read whole ` +
|
|
3384
|
+
`(~${Math.round(text.length / 4000)}k tokens) — reading it in full overflows the context window. ` +
|
|
3385
|
+
`Instead: page it with get_rows(start, count), or run a pipeline that filters/aggregates it ` +
|
|
3386
|
+
`server-side (where / select / groupBy → only the small final result returns to you).]`);
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Builds a per-run {@link PipelineToolRegistry} that unifies the three pipeline-able
|
|
3390
|
+
* substrates behind one namespace: built-in transforms, the agent's effective Actions, and
|
|
3391
|
+
* the run's artifact tools. Transforms register first so their reserved names win; a source
|
|
3392
|
+
* whose name collides with a transform is skipped for pipeline use (still callable normally)
|
|
3393
|
+
* and logged, rather than aborting the whole pipeline.
|
|
3394
|
+
*
|
|
3395
|
+
* @protected
|
|
3396
|
+
*/
|
|
3397
|
+
buildPipelineRegistry(params) {
|
|
3398
|
+
const registry = new PipelineToolRegistry();
|
|
3399
|
+
const register = (invocable) => {
|
|
3400
|
+
try {
|
|
3401
|
+
registry.Register(invocable);
|
|
3402
|
+
}
|
|
3403
|
+
catch (e) {
|
|
3404
|
+
this.logStatus(`[Pipeline] Skipped tool "${invocable.toolName}": ${e.message}`, true, params);
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
// Operators (where/select/map/…) are pure code-defined verbs, not registry tools — only
|
|
3408
|
+
// capabilities (Actions + artifact tools) live here as pipeline sources/stages.
|
|
3409
|
+
// Actions — each wrapped to run via the existing single-action execution path.
|
|
3410
|
+
this.getEffectiveActionsForValidation(params.agent.ID).forEach((actionEntity) => register(new ActionInvocable(actionEntity.Name, (p) => this.ExecuteSingleAction(params, { name: actionEntity.Name, params: p }, actionEntity, params.contextUser))));
|
|
3411
|
+
// Artifact tools — one invocable per distinct tool name; `artifactId` is supplied as a
|
|
3412
|
+
// call-time param so the same `{ tool, params }` step shape works across all substrates.
|
|
3413
|
+
this._artifactToolManager.GetAvailableToolNames().forEach((toolName) => register(new ArtifactToolInvocable(toolName, async (tool, p) => {
|
|
3414
|
+
const stored = await this._artifactToolManager.ExecuteSingleToolCall({
|
|
3415
|
+
artifactId: String(p.artifactId ?? ''),
|
|
3416
|
+
tool,
|
|
3417
|
+
input: p,
|
|
3418
|
+
});
|
|
3419
|
+
return stored.result;
|
|
3420
|
+
})));
|
|
3421
|
+
return registry;
|
|
3422
|
+
}
|
|
3423
|
+
/**
|
|
3424
|
+
* Runs a tool pipeline as a single `Tool` step in the run tree (sibling of the prompt step that
|
|
3425
|
+
* requested it, matching artifact-tool steps). ALL pipeline observability lives in this step's
|
|
3426
|
+
* `OutputData` — the per-stage breakdown, totals, bytes saved, and the tool chain — so there are
|
|
3427
|
+
* no dedicated pipeline entities and no extra SQL I/O; the run tree alone carries everything a
|
|
3428
|
+
* debug UI needs.
|
|
3429
|
+
*
|
|
3430
|
+
* @protected
|
|
3431
|
+
*/
|
|
3432
|
+
async executePipelineAsStep(pipeline, params) {
|
|
3433
|
+
const stepEntity = await this.createStepEntity({
|
|
3434
|
+
stepType: 'Tool',
|
|
3435
|
+
stepName: `Pipeline: ${pipeline.steps.length} step(s)`,
|
|
3436
|
+
contextUser: params.contextUser,
|
|
3437
|
+
inputData: { steps: pipeline.steps },
|
|
3438
|
+
});
|
|
3439
|
+
const registry = this.buildPipelineRegistry(params);
|
|
3440
|
+
// The executor converts stage-level errors into a failed RESULT (it doesn't throw for those),
|
|
3441
|
+
// but an unexpected throw — e.g. a tool returning a non-serializable value (BigInt/circular)
|
|
3442
|
+
// that trips JSON.stringify in the executor's byte-accounting — must NEVER leave this step
|
|
3443
|
+
// stuck on 'Running'. Catch it and materialize a failed result so finalize always runs and the
|
|
3444
|
+
// failure surfaces as a 'Failed' step (answering "do pipeline errors show as errors?": yes).
|
|
3445
|
+
let result;
|
|
3446
|
+
try {
|
|
3447
|
+
result = await new PipelineExecutor(registry).Execute(pipeline.steps);
|
|
3448
|
+
}
|
|
3449
|
+
catch (e) {
|
|
3450
|
+
result = {
|
|
3451
|
+
success: false,
|
|
3452
|
+
finalOutput: null,
|
|
3453
|
+
steps: [],
|
|
3454
|
+
error: `Pipeline crashed: ${e?.message ?? String(e)}`,
|
|
3455
|
+
contextBytesSaved: 0,
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
// A pipeline is ONE run-step — not a parent + a child step per stage. It runs server-side in a
|
|
3459
|
+
// single fast pass, so the full per-stage breakdown + totals live in this step's OutputData for
|
|
3460
|
+
// a debug UI to visualize — no separate entities, no extra DB writes.
|
|
3461
|
+
await this.finalizeStepEntity(stepEntity, result.success, result.success ? undefined : result.error, {
|
|
3462
|
+
success: result.success,
|
|
3463
|
+
toolChain: summarizePipelineStages(result.steps),
|
|
3464
|
+
steps: result.steps,
|
|
3465
|
+
contextBytesSaved: result.contextBytesSaved,
|
|
3466
|
+
totalBytesStreamed: result.steps.reduce((sum, s) => sum + s.outputSize, 0),
|
|
3467
|
+
totalDurationMs: result.steps.reduce((sum, s) => sum + s.durationMs, 0),
|
|
3468
|
+
failedStepIndex: result.failedStepIndex,
|
|
3469
|
+
});
|
|
3470
|
+
return result;
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Pushes the pipeline's final output (or its failure message) into the conversation for the
|
|
3474
|
+
* LLM's next turn, mirroring the artifact-tool "inject once, then expire" pattern. Only the
|
|
3475
|
+
* final output is surfaced — intermediate step outputs never enter the context window.
|
|
3476
|
+
*
|
|
3477
|
+
* @protected
|
|
3478
|
+
*/
|
|
3479
|
+
injectPipelineResultMessage(params, result) {
|
|
3480
|
+
const diagnostic = result.success && result.diagnostic
|
|
3481
|
+
? `\n⚠ Empty result — ${result.diagnostic}`
|
|
3482
|
+
: '';
|
|
3483
|
+
// Identify which pipeline this result belongs to (stage chain, e.g. `get_rows → where →
|
|
3484
|
+
// select`). Without it, multiple pipeline results across turns are indistinguishable once
|
|
3485
|
+
// compacted — mirrors how artifact-tool results name their tool/artifact.
|
|
3486
|
+
const label = summarizePipelineStages(result.steps);
|
|
3487
|
+
const content = result.success
|
|
3488
|
+
? `Pipeline result [${label}] (final stage value — intermediate stages stayed out of context, ~${result.contextBytesSaved} bytes saved):\n\`\`\`\n${formatFinalOutput(result.finalOutput)}\n\`\`\`${diagnostic}`
|
|
3489
|
+
: `Pipeline failed [${label}].\n${result.error}`;
|
|
3490
|
+
const message = {
|
|
3491
|
+
role: 'user',
|
|
3492
|
+
content,
|
|
3493
|
+
metadata: {
|
|
3494
|
+
turnAdded: this._promptTurnCount,
|
|
3495
|
+
messageType: 'tool-result',
|
|
3496
|
+
expirationTurns: 3,
|
|
3497
|
+
expirationMode: 'Compact',
|
|
3498
|
+
compactMode: 'First N Chars',
|
|
3499
|
+
compactLength: 500,
|
|
3500
|
+
compactPromptId: '',
|
|
3501
|
+
},
|
|
3502
|
+
};
|
|
3503
|
+
params.conversationMessages.push(message);
|
|
3504
|
+
}
|
|
3308
3505
|
/**
|
|
3309
3506
|
* Creates a chat message containing sub-agent execution results.
|
|
3310
3507
|
*
|
|
@@ -3497,7 +3694,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3497
3694
|
{ docsFlag: 'includeForEachDocs', responseTypeKey: 'forEach' },
|
|
3498
3695
|
{ docsFlag: 'includeWhileDocs', responseTypeKey: 'while' },
|
|
3499
3696
|
{ docsFlag: 'includeScratchpadDocs', responseTypeKey: 'scratchpad' },
|
|
3500
|
-
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' }
|
|
3697
|
+
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' },
|
|
3698
|
+
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' }
|
|
3501
3699
|
];
|
|
3502
3700
|
for (const { docsFlag, responseTypeKey } of alignmentMappings) {
|
|
3503
3701
|
// Check if the user explicitly set this response type property
|
|
@@ -3760,6 +3958,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3760
3958
|
configurationId: params.configurationId, // propagate configuration ID to sub-agent
|
|
3761
3959
|
effortLevel: params.effortLevel, // propagate effort level to sub-agent
|
|
3762
3960
|
apiKeys: params.apiKeys, // propagate API keys to sub-agent
|
|
3961
|
+
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
3962
|
data: {
|
|
3764
3963
|
...params.data,
|
|
3765
3964
|
...subAgentRequest.templateParameters,
|
|
@@ -3770,10 +3969,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3770
3969
|
PrimaryScopeEntityName: params.PrimaryScopeEntityName, // propagate scope to sub-agent
|
|
3771
3970
|
PrimaryScopeRecordID: params.PrimaryScopeRecordID,
|
|
3772
3971
|
SecondaryScopes: params.SecondaryScopes,
|
|
3773
|
-
// Add callback to link AgentRun ID immediately when created
|
|
3774
3972
|
onAgentRunCreated: async (agentRunId) => {
|
|
3775
3973
|
stepEntity.TargetLogID = agentRunId;
|
|
3776
|
-
|
|
3974
|
+
this.queueStepSave(stepEntity);
|
|
3777
3975
|
}
|
|
3778
3976
|
});
|
|
3779
3977
|
// Check if execution was successful
|
|
@@ -4442,6 +4640,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4442
4640
|
const errorMessage = JSON.stringify(CopyScalarsAndArrays(this._agentRun.LatestResult));
|
|
4443
4641
|
throw new Error(`Failed to create agent run record: Details: ${errorMessage}`);
|
|
4444
4642
|
}
|
|
4643
|
+
// Hand the now-persisted run (it has a stable ID) to the watchdog so a process restart,
|
|
4644
|
+
// crash, or failed terminal-state write can't leave it stuck 'Running' forever. Only the
|
|
4645
|
+
// server-side DB provider can heartbeat via SQL; client/non-DB providers simply opt out.
|
|
4646
|
+
const runProvider = params.provider || this._activeProvider;
|
|
4647
|
+
if (runProvider instanceof DatabaseProviderBase && params.contextUser) {
|
|
4648
|
+
AgentRunWatchdog.Instance.Track(this._agentRun.ID, runProvider, params.contextUser);
|
|
4649
|
+
}
|
|
4445
4650
|
// Invoke callback if provided
|
|
4446
4651
|
if (modifiedParams.onAgentRunCreated) {
|
|
4447
4652
|
try {
|
|
@@ -4497,7 +4702,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4497
4702
|
/**
|
|
4498
4703
|
* Creates a step entity for tracking.
|
|
4499
4704
|
*
|
|
4500
|
-
*
|
|
4705
|
+
* Exposed as `protected` so driver sub-classes (e.g. Skip) can author custom
|
|
4706
|
+
* `AIAgentRunStep` records with the same setup correctness — `StepNumber`,
|
|
4707
|
+
* hierarchy breadcrumb, UUID validation, payload serialization, and the
|
|
4708
|
+
* `queueStepSave` coupling that keeps INSERT-then-UPDATE ordering safe.
|
|
4709
|
+
*
|
|
4710
|
+
* @protected
|
|
4501
4711
|
* @param params - Step creation parameters
|
|
4502
4712
|
* @returns {Promise<MJAIAgentRunStepEntityExtended>} - The created step entity
|
|
4503
4713
|
*/
|
|
@@ -4534,9 +4744,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4534
4744
|
}
|
|
4535
4745
|
});
|
|
4536
4746
|
}
|
|
4537
|
-
|
|
4538
|
-
throw new Error(`Failed to create agent run step record: ${JSON.stringify(stepEntity.LatestResult)}`);
|
|
4539
|
-
}
|
|
4747
|
+
this.queueStepSave(stepEntity);
|
|
4540
4748
|
// Add the step to the agent run's Steps array
|
|
4541
4749
|
if (this._agentRun) {
|
|
4542
4750
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -4592,6 +4800,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4592
4800
|
} : undefined
|
|
4593
4801
|
};
|
|
4594
4802
|
}
|
|
4803
|
+
/**
|
|
4804
|
+
* Finalizes a step entity with completion status. Pairs with `createStepEntity`
|
|
4805
|
+
* — drivers that create custom steps should also finalize them through this
|
|
4806
|
+
* method so `Status`/`CompletedAt`/`Success`/`ErrorMessage`/`OutputData` are
|
|
4807
|
+
* populated consistently and the UPDATE is sequenced behind the INSERT via
|
|
4808
|
+
* `queueStepSave`.
|
|
4809
|
+
*
|
|
4810
|
+
* @protected
|
|
4811
|
+
*/
|
|
4595
4812
|
async finalizeStepEntity(stepEntity, success, errorMessage, outputData) {
|
|
4596
4813
|
try {
|
|
4597
4814
|
stepEntity.Status = success ? 'Completed' : 'Failed';
|
|
@@ -4609,14 +4826,74 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4609
4826
|
}
|
|
4610
4827
|
});
|
|
4611
4828
|
}
|
|
4612
|
-
|
|
4613
|
-
console.error('Failed to update agent run step record');
|
|
4614
|
-
}
|
|
4829
|
+
this.queueStepSave(stepEntity);
|
|
4615
4830
|
}
|
|
4616
4831
|
catch (e) {
|
|
4617
|
-
|
|
4832
|
+
LogError(`Failed to update agent run step record: ${e?.message ?? e}`, undefined, e);
|
|
4618
4833
|
}
|
|
4619
4834
|
}
|
|
4835
|
+
/**
|
|
4836
|
+
* Queues a database save for a step entity.
|
|
4837
|
+
*
|
|
4838
|
+
* - Saves on the same step record are chained (sequenced) to prevent an UPDATE
|
|
4839
|
+
* from racing the original INSERT.
|
|
4840
|
+
* - Saves on different step records run concurrently.
|
|
4841
|
+
* - Failures are not thrown — they're logged via `LogError` (with the entity's
|
|
4842
|
+
* `LatestResult.CompleteMessage` per the BaseEntity convention) so the
|
|
4843
|
+
* agent loop isn't blocked by observability writes — but they ARE surfaced
|
|
4844
|
+
* in `finalizeAgentRun` so callers see step-record drift.
|
|
4845
|
+
*
|
|
4846
|
+
* Exposed as `protected` so driver sub-classes (e.g. Skip) that author
|
|
4847
|
+
* custom `AIAgentRunStep` records can fire-and-forget saves through the
|
|
4848
|
+
* same chained/non-blocking machinery instead of awaiting `entity.Save()`
|
|
4849
|
+
* inline and blocking the agent loop.
|
|
4850
|
+
*
|
|
4851
|
+
* @protected
|
|
4852
|
+
*/
|
|
4853
|
+
queueStepSave(stepEntity) {
|
|
4854
|
+
// Chain on the entity INSTANCE (stable), NOT stepEntity.ID — the ID is empty until the INSERT
|
|
4855
|
+
// Save() assigns it, so an ID-keyed chain breaks for fast create→finalize sequences.
|
|
4856
|
+
const previousSave = this._stepSavePromises.get(stepEntity) ?? Promise.resolve();
|
|
4857
|
+
const currentSave = previousSave.then(() => stepEntity.Save()).then((ok) => {
|
|
4858
|
+
if (!ok) {
|
|
4859
|
+
LogError(`Failed to save agent run step record ${stepEntity.ID || '(unsaved)'}: ${stepEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
4860
|
+
}
|
|
4861
|
+
return ok;
|
|
4862
|
+
});
|
|
4863
|
+
this._stepSavePromises.set(stepEntity, currentSave);
|
|
4864
|
+
this._pendingSaves.push(currentSave);
|
|
4865
|
+
}
|
|
4866
|
+
/**
|
|
4867
|
+
* Maps an array through an async worker with bounded concurrency.
|
|
4868
|
+
* Preserves input order in the output. Used to cap parallel sub-agent and
|
|
4869
|
+
* preload dispatches so a misbehaving LLM (or a runaway data source) can't
|
|
4870
|
+
* exhaust the model API or DB pool.
|
|
4871
|
+
*
|
|
4872
|
+
* Exposed as `protected` so driver sub-classes performing custom parallel
|
|
4873
|
+
* work get the same bounded-fan-out + ordered-results contract for free.
|
|
4874
|
+
*
|
|
4875
|
+
* @protected
|
|
4876
|
+
*/
|
|
4877
|
+
async mapWithConcurrency(items, limit, worker) {
|
|
4878
|
+
if (items.length === 0)
|
|
4879
|
+
return [];
|
|
4880
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
4881
|
+
const results = new Array(items.length);
|
|
4882
|
+
let next = 0;
|
|
4883
|
+
const runners = [];
|
|
4884
|
+
for (let i = 0; i < effectiveLimit; i++) {
|
|
4885
|
+
runners.push((async () => {
|
|
4886
|
+
while (true) {
|
|
4887
|
+
const idx = next++;
|
|
4888
|
+
if (idx >= items.length)
|
|
4889
|
+
return;
|
|
4890
|
+
results[idx] = await worker(items[idx], idx);
|
|
4891
|
+
}
|
|
4892
|
+
})());
|
|
4893
|
+
}
|
|
4894
|
+
await Promise.all(runners);
|
|
4895
|
+
return results;
|
|
4896
|
+
}
|
|
4620
4897
|
/**
|
|
4621
4898
|
* Default parameter resolution for loop body parameters (used by Flow agents)
|
|
4622
4899
|
* Resolves item.field, payload.field, item, index, or static values
|
|
@@ -4651,7 +4928,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4651
4928
|
/**
|
|
4652
4929
|
* Formats a message with agent hierarchy for streaming/progress updates.
|
|
4653
4930
|
*
|
|
4654
|
-
*
|
|
4931
|
+
* Exposed as `protected` so driver sub-classes emit progress events whose
|
|
4932
|
+
* breadcrumbs line up with the framework's own — keeps the Explorer tree
|
|
4933
|
+
* view consistent across custom and built-in dispatch.
|
|
4934
|
+
*
|
|
4935
|
+
* @protected
|
|
4655
4936
|
* @param {string} baseMessage - The base message to format
|
|
4656
4937
|
* @returns {string} - The formatted message with hierarchy breadcrumb
|
|
4657
4938
|
*/
|
|
@@ -4674,10 +4955,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4674
4955
|
* - Nested sub-agent step 3: buildHierarchicalStep(3, [2, 1]) => "2.1.3"
|
|
4675
4956
|
* - Deep nesting: buildHierarchicalStep(5, [1, 2, 3, 4]) => "1.2.3.4.5"
|
|
4676
4957
|
*
|
|
4958
|
+
* Exposed as `protected` so driver sub-classes can emit step labels that
|
|
4959
|
+
* match the framework's `2.1.3` nesting convention.
|
|
4960
|
+
*
|
|
4677
4961
|
* @param currentStep - Current agent's step number (1-based)
|
|
4678
4962
|
* @param parentSteps - Array of parent step counts from root to immediate parent
|
|
4679
4963
|
* @returns Formatted hierarchical step string, or undefined if currentStep is undefined/null
|
|
4680
|
-
* @
|
|
4964
|
+
* @protected
|
|
4681
4965
|
*/
|
|
4682
4966
|
buildHierarchicalStep(currentStep, parentSteps) {
|
|
4683
4967
|
if (currentStep == null)
|
|
@@ -4948,10 +5232,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4948
5232
|
stepEntityId: stepEntity.ID
|
|
4949
5233
|
});
|
|
4950
5234
|
} : undefined;
|
|
4951
|
-
// Add callback to link PromptRun ID immediately when created
|
|
4952
5235
|
promptParams.onPromptRunCreated = async (promptRunId) => {
|
|
4953
5236
|
stepEntity.TargetLogID = promptRunId;
|
|
4954
|
-
|
|
5237
|
+
this.queueStepSave(stepEntity);
|
|
4955
5238
|
};
|
|
4956
5239
|
// Execute the prompt
|
|
4957
5240
|
const promptResult = await this.executePrompt(promptParams);
|
|
@@ -5080,6 +5363,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5080
5363
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
5081
5364
|
this.logStatus(`[ArtifactTools] LLM did not use artifact tools this turn (artifacts available but not accessed)`, true, params);
|
|
5082
5365
|
}
|
|
5366
|
+
// Execute a tool pipeline if provided (zero turn cost — processed inline). Each step's
|
|
5367
|
+
// output is threaded into the next server-side; only the final step's output returns to
|
|
5368
|
+
// the LLM, so intermediate payloads never enter the context window.
|
|
5369
|
+
if (initialNextStep.pipeline?.steps?.length) {
|
|
5370
|
+
this.logStatus(`[Pipeline] LLM requested a ${initialNextStep.pipeline.steps.length}-stage pipeline: ${initialNextStep.pipeline.steps.map(s => s.tool ?? Object.keys(s)[0]).join(' | ')}`, true, params);
|
|
5371
|
+
const pipelineResult = await this.executePipelineAsStep(initialNextStep.pipeline, params);
|
|
5372
|
+
this.injectPipelineResultMessage(params, pipelineResult);
|
|
5373
|
+
}
|
|
5083
5374
|
// now that we have processed the payload, we can process the next step which does validation and changes the next step if
|
|
5084
5375
|
// validation fails
|
|
5085
5376
|
const updatedNextStep = await this.processNextStep(initialNextStep, params, config.agentType, promptResult, finalPayload, stepEntity);
|
|
@@ -5596,37 +5887,31 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5596
5887
|
* @param subAgentPayloadOverride - Optional payload override for sub-agent execution, if provided the normal payload computation is skipped
|
|
5597
5888
|
*/
|
|
5598
5889
|
async processSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
5599
|
-
|
|
5890
|
+
// Multiple sub-agents → parallel fan-out
|
|
5891
|
+
if (previousDecision.subAgents && previousDecision.subAgents.length > 0) {
|
|
5892
|
+
return await this.executeParallelSubAgents(params, previousDecision.subAgents, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5893
|
+
}
|
|
5894
|
+
// Single sub-agent path. Use the helper so callers that populated `subAgents`
|
|
5895
|
+
// with a single entry (instead of `subAgent`) still resolve correctly.
|
|
5896
|
+
const requested = this.getRequestedSubAgents(previousDecision);
|
|
5897
|
+
const subAgentRequest = (requested[0] ?? previousDecision.subAgent);
|
|
5600
5898
|
const name = subAgentRequest?.name;
|
|
5601
5899
|
if (!name) {
|
|
5602
5900
|
return {
|
|
5603
5901
|
step: 'Failed',
|
|
5604
5902
|
terminate: false,
|
|
5605
5903
|
errorMessage: 'Sub-agent name is required',
|
|
5606
|
-
previousPayload: previousDecision
|
|
5607
|
-
newPayload: previousDecision
|
|
5904
|
+
previousPayload: previousDecision.newPayload,
|
|
5905
|
+
newPayload: previousDecision.newPayload
|
|
5608
5906
|
};
|
|
5609
5907
|
}
|
|
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);
|
|
5908
|
+
const resolved = this.resolveSubAgentByName(params, name);
|
|
5909
|
+
if (resolved?.relationship) {
|
|
5910
|
+
return await this.executeRelatedSubAgentStep(params, previousDecision, resolved.subAgentEntity, resolved.relationship, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5617
5911
|
}
|
|
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
|
-
}
|
|
5912
|
+
if (resolved) {
|
|
5913
|
+
return await this.executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
5628
5914
|
}
|
|
5629
|
-
// Sub-agent not found
|
|
5630
5915
|
this.logError(`Sub-agent '${name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
5631
5916
|
agent: params.agent,
|
|
5632
5917
|
category: 'SubAgentExecution'
|
|
@@ -5635,8 +5920,381 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5635
5920
|
step: 'Retry',
|
|
5636
5921
|
terminate: false,
|
|
5637
5922
|
errorMessage: `Sub-agent '${name}' not found or not active`,
|
|
5638
|
-
previousPayload: previousDecision
|
|
5639
|
-
newPayload: previousDecision
|
|
5923
|
+
previousPayload: previousDecision.newPayload,
|
|
5924
|
+
newPayload: previousDecision.newPayload
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
/**
|
|
5928
|
+
* Finds a sub-agent by name, checking child agents (ParentID) first, then
|
|
5929
|
+
* related agents (AgentRelationships). Returns `undefined` when the name
|
|
5930
|
+
* doesn't resolve to an active agent reachable from `params.agent`.
|
|
5931
|
+
*
|
|
5932
|
+
* Used by both the single and parallel sub-agent dispatch paths so name
|
|
5933
|
+
* resolution is consistent and there's one place to fix lookup bugs.
|
|
5934
|
+
*
|
|
5935
|
+
* Exposed as `protected` so driver sub-classes with custom routing logic
|
|
5936
|
+
* still resolve names through the same case-insensitive child-then-related
|
|
5937
|
+
* lookup the framework uses internally.
|
|
5938
|
+
*
|
|
5939
|
+
* @protected
|
|
5940
|
+
*/
|
|
5941
|
+
resolveSubAgentByName(params, name) {
|
|
5942
|
+
const normalized = name.trim().toLowerCase();
|
|
5943
|
+
const childAgent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ParentID, params.agent.ID) &&
|
|
5944
|
+
a.Status === 'Active' &&
|
|
5945
|
+
a.Name.trim().toLowerCase() === normalized);
|
|
5946
|
+
if (childAgent) {
|
|
5947
|
+
return { subAgentEntity: childAgent };
|
|
5948
|
+
}
|
|
5949
|
+
const activeRelationships = AIEngine.Instance.AgentRelationships.filter(ar => UUIDsEqual(ar.AgentID, params.agent.ID) && ar.Status === 'Active');
|
|
5950
|
+
for (const rel of activeRelationships) {
|
|
5951
|
+
const relatedAgent = AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, rel.SubAgentID) &&
|
|
5952
|
+
a.Status === 'Active' &&
|
|
5953
|
+
a.Name.trim().toLowerCase() === normalized);
|
|
5954
|
+
if (relatedAgent) {
|
|
5955
|
+
return { subAgentEntity: relatedAgent, relationship: rel };
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
return undefined;
|
|
5959
|
+
}
|
|
5960
|
+
/**
|
|
5961
|
+
* Best-effort deep clone for sub-agent payloads. We need this so two parallel
|
|
5962
|
+
* sub-agents can each receive their own working copy — without it, mutations
|
|
5963
|
+
* by one in-flight sub-agent would race the others' reads.
|
|
5964
|
+
*
|
|
5965
|
+
* Uses `structuredClone` (Node 17+) where available; falls back to a JSON
|
|
5966
|
+
* round-trip for environments without it. Returns the original value on
|
|
5967
|
+
* non-cloneable inputs.
|
|
5968
|
+
*
|
|
5969
|
+
* **JSON fallback caveats** — the round-trip is *not* shape-preserving:
|
|
5970
|
+
* - `Date` → ISO string
|
|
5971
|
+
* - `Map`, `Set`, `RegExp`, typed arrays → `{}`
|
|
5972
|
+
* - `undefined` values and function-valued properties → dropped
|
|
5973
|
+
* - `BigInt` → throws (caught by the outer try/catch, returns original)
|
|
5974
|
+
* - circular refs → throws (returns original)
|
|
5975
|
+
* If payloads ever carry those shapes, behavior diverges between the
|
|
5976
|
+
* structuredClone path (Node 17+) and the JSON path. Keep sub-agent
|
|
5977
|
+
* payloads to plain JSON-safe shapes to avoid this skew.
|
|
5978
|
+
*
|
|
5979
|
+
* Exposed as `protected` so driver sub-classes performing their own parallel
|
|
5980
|
+
* dispatch get the same payload-isolation guarantee.
|
|
5981
|
+
*
|
|
5982
|
+
* @protected
|
|
5983
|
+
*/
|
|
5984
|
+
cloneSubAgentPayload(payload) {
|
|
5985
|
+
if (payload === null || payload === undefined)
|
|
5986
|
+
return payload;
|
|
5987
|
+
if (typeof payload !== 'object')
|
|
5988
|
+
return payload;
|
|
5989
|
+
try {
|
|
5990
|
+
if (typeof globalThis.structuredClone === 'function') {
|
|
5991
|
+
return globalThis.structuredClone(payload);
|
|
5992
|
+
}
|
|
5993
|
+
return JSON.parse(JSON.stringify(payload));
|
|
5994
|
+
}
|
|
5995
|
+
catch {
|
|
5996
|
+
return payload;
|
|
5997
|
+
}
|
|
5998
|
+
}
|
|
5999
|
+
/**
|
|
6000
|
+
* Pre-flight for one parallel sub-agent: resolve the entity, push the
|
|
6001
|
+
* delegation message, emit progress. Runs synchronously (no awaits) so the
|
|
6002
|
+
* conversation transcript order matches the `subAgents` array's source order
|
|
6003
|
+
* regardless of which dispatch races to the front.
|
|
6004
|
+
*
|
|
6005
|
+
* Returns `undefined` (and pushes a sentinel message) when the sub-agent
|
|
6006
|
+
* can't be resolved — the dispatch loop later records this as a failed
|
|
6007
|
+
* execution rather than throwing inside `Promise.all`.
|
|
6008
|
+
*
|
|
6009
|
+
* @private
|
|
6010
|
+
*/
|
|
6011
|
+
prepareParallelSubAgentDispatch(params, request, stepCount) {
|
|
6012
|
+
const resolved = this.resolveSubAgentByName(params, request.name);
|
|
6013
|
+
if (!resolved) {
|
|
6014
|
+
this.logError(`Sub-agent '${request.name}' not found or not active for agent '${params.agent.Name}'`, {
|
|
6015
|
+
agent: params.agent,
|
|
6016
|
+
category: 'SubAgentExecution'
|
|
6017
|
+
});
|
|
6018
|
+
return undefined;
|
|
6019
|
+
}
|
|
6020
|
+
const { subAgentEntity, relationship } = resolved;
|
|
6021
|
+
params.onProgress?.({
|
|
6022
|
+
step: 'subagent_execution',
|
|
6023
|
+
message: this.formatHierarchicalMessage(`Delegating to parallel sub-agent ${request.name}`),
|
|
6024
|
+
metadata: {
|
|
6025
|
+
agentName: params.agent.Name,
|
|
6026
|
+
subAgentName: request.name,
|
|
6027
|
+
reason: request.message,
|
|
6028
|
+
relationshipType: relationship ? 'related' : 'child',
|
|
6029
|
+
stepCount: stepCount + 1,
|
|
6030
|
+
hierarchicalStep: this.buildHierarchicalStep(stepCount + 1, this._parentStepCounts)
|
|
6031
|
+
}
|
|
6032
|
+
});
|
|
6033
|
+
params.conversationMessages.push({
|
|
6034
|
+
role: 'assistant',
|
|
6035
|
+
content: `I'm delegating this task to the parallel sub-agent "${request.name}".\n\nReason: ${request.message}`
|
|
6036
|
+
});
|
|
6037
|
+
return { request: request, subAgentEntity, relationship };
|
|
6038
|
+
}
|
|
6039
|
+
/**
|
|
6040
|
+
* Computes the per-sub-agent input payload + context message based on whether
|
|
6041
|
+
* this is a child (PayloadScope / paths) or related (input/output mapping)
|
|
6042
|
+
* sub-agent. The parent payload is deep-cloned for child agents so two
|
|
6043
|
+
* parallel sub-agents can't see each other's in-flight mutations.
|
|
6044
|
+
*
|
|
6045
|
+
* @private
|
|
6046
|
+
*/
|
|
6047
|
+
async buildSubAgentInputs(params, dispatch, previousDecision, subAgentPayloadOverride) {
|
|
6048
|
+
const { subAgentEntity, relationship, request } = dispatch;
|
|
6049
|
+
const parentPayload = previousDecision.newPayload;
|
|
6050
|
+
if (relationship) {
|
|
6051
|
+
// Related agent path — input/output mapping handles the structural transform.
|
|
6052
|
+
let initialPayload = subAgentPayloadOverride;
|
|
6053
|
+
if (!initialPayload && relationship.SubAgentInputMapping) {
|
|
6054
|
+
initialPayload = this.applySubAgentInputMapping(parentPayload, relationship.SubAgentInputMapping);
|
|
6055
|
+
}
|
|
6056
|
+
const contextPaths = this.parseSubAgentContextPaths(relationship, request.name);
|
|
6057
|
+
const contextMessage = this.prepareRelatedSubAgentContextMessage(parentPayload, contextPaths, params);
|
|
6058
|
+
return { initialPayload, contextMessage, upstreamPaths: [] };
|
|
6059
|
+
}
|
|
6060
|
+
// Child agent path — scoping + downstream/upstream paths, with deep clone
|
|
6061
|
+
// so siblings can't mutate each other's input.
|
|
6062
|
+
const { downstreamPaths, upstreamPaths } = this.computeUpstreamDownstreamPaths(params, subAgentEntity, request);
|
|
6063
|
+
let initialPayload = subAgentPayloadOverride;
|
|
6064
|
+
if (!initialPayload) {
|
|
6065
|
+
initialPayload = await this.computeChildSubAgentPayload(params, subAgentEntity, downstreamPaths, request, previousDecision);
|
|
6066
|
+
}
|
|
6067
|
+
return { initialPayload: this.cloneSubAgentPayload(initialPayload), contextMessage: null, upstreamPaths };
|
|
6068
|
+
}
|
|
6069
|
+
/**
|
|
6070
|
+
* Safely parses the `SubAgentContextPaths` JSON field on a relationship.
|
|
6071
|
+
* @private
|
|
6072
|
+
*/
|
|
6073
|
+
parseSubAgentContextPaths(relationship, subAgentName) {
|
|
6074
|
+
if (!relationship.SubAgentContextPaths)
|
|
6075
|
+
return [];
|
|
6076
|
+
try {
|
|
6077
|
+
return JSON.parse(relationship.SubAgentContextPaths);
|
|
6078
|
+
}
|
|
6079
|
+
catch (parseError) {
|
|
6080
|
+
LogError(`Failed to parse SubAgentContextPaths for sub-agent ${subAgentName}: ${parseError.message}`);
|
|
6081
|
+
return [];
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
/**
|
|
6085
|
+
* Merges one parallel sub-agent's result back into the running parent payload.
|
|
6086
|
+
* Returns the new payload AND the payload that should be persisted on this
|
|
6087
|
+
* specific sub-agent's step record (the *delta* applied for this sub-agent,
|
|
6088
|
+
* not the cumulative state, so audit logs can distinguish each sibling's
|
|
6089
|
+
* contribution).
|
|
6090
|
+
*
|
|
6091
|
+
* @private
|
|
6092
|
+
*/
|
|
6093
|
+
mergeParallelSubAgentResult(params, execution, runningPayload) {
|
|
6094
|
+
if (!execution.result.success) {
|
|
6095
|
+
// Failures don't contribute to the merged payload, but we still record
|
|
6096
|
+
// the sub-agent's own result on its step for forensic visibility.
|
|
6097
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
6098
|
+
}
|
|
6099
|
+
if (execution.relationship) {
|
|
6100
|
+
if (!execution.relationship.SubAgentOutputMapping) {
|
|
6101
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
6102
|
+
}
|
|
6103
|
+
const payloadChange = this.applySubAgentOutputMapping(execution.result.payload, runningPayload, execution.relationship.SubAgentOutputMapping);
|
|
6104
|
+
if (!payloadChange || !payloadChange.updateElements) {
|
|
6105
|
+
return { mergedPayload: runningPayload, stepPayloadAtEnd: execution.result.payload };
|
|
6106
|
+
}
|
|
6107
|
+
const mergeResult = this._payloadManager.applyAgentChangeRequest(runningPayload, payloadChange, {
|
|
6108
|
+
validateChanges: true,
|
|
6109
|
+
logChanges: true,
|
|
6110
|
+
analyzeChanges: true,
|
|
6111
|
+
generateDiff: true,
|
|
6112
|
+
agentName: `${execution.request.name} (related agent mapping)`,
|
|
6113
|
+
verbose: params.verbose === true || IsVerboseLoggingEnabled()
|
|
6114
|
+
});
|
|
6115
|
+
return { mergedPayload: mergeResult.result, stepPayloadAtEnd: execution.result.payload };
|
|
6116
|
+
}
|
|
6117
|
+
// Child agent merge — reverse-scope then merge along upstream paths.
|
|
6118
|
+
let resultPayloadForMerge = execution.result.payload;
|
|
6119
|
+
if (execution.subAgentEntity.PayloadScope) {
|
|
6120
|
+
resultPayloadForMerge = this._payloadManager.reversePayloadScope(execution.result.payload, execution.subAgentEntity.PayloadScope);
|
|
6121
|
+
}
|
|
6122
|
+
const mergeResult = this._payloadManager.mergeUpstreamPayload(execution.request.name, runningPayload, resultPayloadForMerge, execution.upstreamPaths, params.verbose === true || IsVerboseLoggingEnabled());
|
|
6123
|
+
return { mergedPayload: mergeResult.result, stepPayloadAtEnd: resultPayloadForMerge };
|
|
6124
|
+
}
|
|
6125
|
+
/**
|
|
6126
|
+
* Builds the aggregated markdown summary of parallel sub-agent results that
|
|
6127
|
+
* gets appended to the parent's conversation as a `user` message — gives the
|
|
6128
|
+
* Loop agent a single deterministic record of what fanned out and what came
|
|
6129
|
+
* back, regardless of completion order.
|
|
6130
|
+
*
|
|
6131
|
+
* @private
|
|
6132
|
+
*/
|
|
6133
|
+
buildParallelSubAgentSummary(executions) {
|
|
6134
|
+
return executions
|
|
6135
|
+
.map(execution => {
|
|
6136
|
+
const statusEmoji = execution.result.success ? '✅' : '❌';
|
|
6137
|
+
const baseInfo = `${statusEmoji} **Sub-Agent: ${execution.request.name}**\n` +
|
|
6138
|
+
`* Message: "${execution.request.message}"\n` +
|
|
6139
|
+
`* Status: ${execution.result.agentRun?.FinalStep || 'Failed'}`;
|
|
6140
|
+
if (execution.result.agentRun?.ErrorMessage) {
|
|
6141
|
+
return `${baseInfo}\n* Error: ${execution.result.agentRun.ErrorMessage}`;
|
|
6142
|
+
}
|
|
6143
|
+
return baseInfo;
|
|
6144
|
+
})
|
|
6145
|
+
.join('\n\n---\n\n');
|
|
6146
|
+
}
|
|
6147
|
+
/**
|
|
6148
|
+
* Executes multiple sub-agents in parallel (with a concurrency cap) and
|
|
6149
|
+
* merges their output payloads back into the parent sequentially.
|
|
6150
|
+
*
|
|
6151
|
+
* Pipeline:
|
|
6152
|
+
* 1. **Synchronously** prepare each dispatch (resolve entity, push delegation
|
|
6153
|
+
* message, emit progress) so conversation order is deterministic.
|
|
6154
|
+
* 2. Create step entities and run sub-agents with bounded concurrency.
|
|
6155
|
+
* 3. Merge each result into the parent payload sequentially in source order.
|
|
6156
|
+
* 4. Finalize each step entity with its own contribution recorded.
|
|
6157
|
+
* 5. Append an aggregated `user` summary message to the parent conversation.
|
|
6158
|
+
*
|
|
6159
|
+
* Termination semantics: matches the single sub-agent path — if any
|
|
6160
|
+
* dispatched child requested `terminateAfter: true`, the parent terminates
|
|
6161
|
+
* regardless of whether that child succeeded. The parent's reported step is
|
|
6162
|
+
* `Failed` when any child failed, `Success` when terminating cleanly, and
|
|
6163
|
+
* `Retry` otherwise.
|
|
6164
|
+
*
|
|
6165
|
+
* @private
|
|
6166
|
+
*/
|
|
6167
|
+
/**
|
|
6168
|
+
* Worker for one parallel sub-agent dispatch: creates the step entity, builds
|
|
6169
|
+
* the (isolated) input payload, and invokes `ExecuteSubAgent`. Returns
|
|
6170
|
+
* `undefined` for an empty dispatch slot (unresolved sub-agent name) so the
|
|
6171
|
+
* caller can record a synthetic failure in source order.
|
|
6172
|
+
*
|
|
6173
|
+
* @private
|
|
6174
|
+
*/
|
|
6175
|
+
async runSingleParallelSubAgent(params, dispatch, previousDecision, currentPayload, parentStepId, subAgentPayloadOverride, stepCount) {
|
|
6176
|
+
if (!dispatch)
|
|
6177
|
+
return undefined;
|
|
6178
|
+
const { request, subAgentEntity, relationship } = dispatch;
|
|
6179
|
+
const stepEntity = await this.createStepEntity({
|
|
6180
|
+
stepType: 'Sub-Agent',
|
|
6181
|
+
stepName: `Execute Parallel Sub-Agent: ${request.name}`,
|
|
6182
|
+
contextUser: params.contextUser,
|
|
6183
|
+
targetId: subAgentEntity.ID,
|
|
6184
|
+
inputData: {
|
|
6185
|
+
agentName: params.agent.Name,
|
|
6186
|
+
subAgentName: request.name,
|
|
6187
|
+
message: request.message,
|
|
6188
|
+
terminateAfter: request.terminateAfter,
|
|
6189
|
+
conversationMessages: params.conversationMessages,
|
|
6190
|
+
parentAgentHierarchy: this._agentHierarchy,
|
|
6191
|
+
relationshipType: relationship ? 'related' : 'child'
|
|
6192
|
+
},
|
|
6193
|
+
payloadAtStart: currentPayload,
|
|
6194
|
+
parentId: parentStepId
|
|
6195
|
+
});
|
|
6196
|
+
this.incrementExecutionCount(subAgentEntity.ID);
|
|
6197
|
+
const { initialPayload, contextMessage, upstreamPaths } = await this.buildSubAgentInputs(params, dispatch, previousDecision, subAgentPayloadOverride);
|
|
6198
|
+
const result = await this.ExecuteSubAgent(params, request, subAgentEntity, stepEntity, initialPayload, contextMessage, stepCount);
|
|
6199
|
+
return { request, result, subAgentEntity, relationship, stepEntity, upstreamPaths };
|
|
6200
|
+
}
|
|
6201
|
+
/**
|
|
6202
|
+
* Builds a synthetic execution record for an unresolved sub-agent so we can
|
|
6203
|
+
* keep source-order alignment between `subAgentRequests` and `executions`
|
|
6204
|
+
* without throwing inside `Promise.all`.
|
|
6205
|
+
*
|
|
6206
|
+
* @private
|
|
6207
|
+
*/
|
|
6208
|
+
synthesizeUnresolvedSubAgentExecution(request, runningPayload) {
|
|
6209
|
+
return {
|
|
6210
|
+
request,
|
|
6211
|
+
result: {
|
|
6212
|
+
success: false,
|
|
6213
|
+
payload: runningPayload,
|
|
6214
|
+
agentRun: {
|
|
6215
|
+
ErrorMessage: `Sub-agent '${request.name}' not found or not active`,
|
|
6216
|
+
FinalStep: 'Failed'
|
|
6217
|
+
}
|
|
6218
|
+
},
|
|
6219
|
+
subAgentEntity: { ID: '', Name: request.name },
|
|
6220
|
+
upstreamPaths: []
|
|
6221
|
+
};
|
|
6222
|
+
}
|
|
6223
|
+
/**
|
|
6224
|
+
* Sequentially merges each sub-agent's result into the parent payload and
|
|
6225
|
+
* finalizes its step entity with its own contribution recorded.
|
|
6226
|
+
*
|
|
6227
|
+
* @private
|
|
6228
|
+
*/
|
|
6229
|
+
async mergeParallelExecutionsIntoParent(params, subAgentRequests, executions, startingPayload) {
|
|
6230
|
+
let mergedPayload = startingPayload;
|
|
6231
|
+
let anyFailure = false;
|
|
6232
|
+
const allExecutions = [];
|
|
6233
|
+
for (let idx = 0; idx < subAgentRequests.length; idx++) {
|
|
6234
|
+
const execution = executions[idx];
|
|
6235
|
+
if (!execution) {
|
|
6236
|
+
anyFailure = true;
|
|
6237
|
+
allExecutions.push(this.synthesizeUnresolvedSubAgentExecution(subAgentRequests[idx], mergedPayload));
|
|
6238
|
+
continue;
|
|
6239
|
+
}
|
|
6240
|
+
if (!execution.result.success)
|
|
6241
|
+
anyFailure = true;
|
|
6242
|
+
if (execution.result.mediaOutputs?.length)
|
|
6243
|
+
this._mediaOutputs.push(...execution.result.mediaOutputs);
|
|
6244
|
+
if (execution.result.fileOutputs?.length)
|
|
6245
|
+
this._fileOutputs.push(...execution.result.fileOutputs);
|
|
6246
|
+
const { mergedPayload: newMerged, stepPayloadAtEnd } = this.mergeParallelSubAgentResult(params, execution, mergedPayload);
|
|
6247
|
+
mergedPayload = newMerged;
|
|
6248
|
+
allExecutions.push(execution);
|
|
6249
|
+
await this.recordParallelStepCompletion(execution, stepPayloadAtEnd);
|
|
6250
|
+
}
|
|
6251
|
+
return { mergedPayload, anyFailure, allExecutions };
|
|
6252
|
+
}
|
|
6253
|
+
/**
|
|
6254
|
+
* Persists per-sibling step state (`PayloadAtEnd` is THIS sub-agent's
|
|
6255
|
+
* contribution, not the cumulative parent state) and finalizes its step
|
|
6256
|
+
* entity.
|
|
6257
|
+
*
|
|
6258
|
+
* @private
|
|
6259
|
+
*/
|
|
6260
|
+
async recordParallelStepCompletion(execution, stepPayloadAtEnd) {
|
|
6261
|
+
if (!execution.stepEntity)
|
|
6262
|
+
return;
|
|
6263
|
+
execution.stepEntity.PayloadAtEnd = this.serializePayloadAtEnd(stepPayloadAtEnd);
|
|
6264
|
+
await this.finalizeStepEntity(execution.stepEntity, execution.result.success, execution.result.agentRun?.ErrorMessage, {
|
|
6265
|
+
subAgentResult: {
|
|
6266
|
+
success: execution.result.success,
|
|
6267
|
+
finalStep: execution.result.agentRun?.FinalStep,
|
|
6268
|
+
errorMessage: execution.result.agentRun?.ErrorMessage,
|
|
6269
|
+
stepCount: execution.result.agentRun?.Steps?.length || 0,
|
|
6270
|
+
},
|
|
6271
|
+
shouldTerminate: execution.request.terminateAfter === true,
|
|
6272
|
+
nextStep: execution.request.terminateAfter === true ? 'success' : 'retry'
|
|
6273
|
+
});
|
|
6274
|
+
}
|
|
6275
|
+
async executeParallelSubAgents(params, subAgentRequests, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
6276
|
+
const currentPayload = previousDecision.newPayload;
|
|
6277
|
+
// Synchronous pre-flight — order-stable transcript + progress events.
|
|
6278
|
+
const dispatches = subAgentRequests.map(req => this.prepareParallelSubAgentDispatch(params, req, stepCount));
|
|
6279
|
+
// Bounded parallel dispatch.
|
|
6280
|
+
const executions = await this.mapWithConcurrency(dispatches, PARALLEL_SUBAGENT_CONCURRENCY_LIMIT, (dispatch) => this.runSingleParallelSubAgent(params, dispatch, previousDecision, currentPayload, parentStepId, subAgentPayloadOverride, stepCount));
|
|
6281
|
+
// Sequential merge + per-sibling step finalization.
|
|
6282
|
+
const { mergedPayload, anyFailure, allExecutions } = await this.mergeParallelExecutionsIntoParent(params, subAgentRequests, executions, currentPayload);
|
|
6283
|
+
// Aggregated summary appended to the parent transcript.
|
|
6284
|
+
params.conversationMessages.push({
|
|
6285
|
+
role: 'user',
|
|
6286
|
+
content: `Parallel Sub-Agents Completed:\n\n${this.buildParallelSubAgentSummary(allExecutions)}`
|
|
6287
|
+
});
|
|
6288
|
+
// Termination semantics: matches the single sub-agent path —
|
|
6289
|
+
// `terminateAfter` triggers parent termination regardless of the child's
|
|
6290
|
+
// success/failure. The parent's step reflects whether any child failed:
|
|
6291
|
+
// Failed if any did, Success if terminating cleanly, otherwise Retry.
|
|
6292
|
+
const shouldTerminateParent = allExecutions.some(e => e.request.terminateAfter === true);
|
|
6293
|
+
return {
|
|
6294
|
+
step: anyFailure ? 'Failed' : (shouldTerminateParent ? 'Success' : 'Retry'),
|
|
6295
|
+
terminate: shouldTerminateParent,
|
|
6296
|
+
newPayload: mergedPayload,
|
|
6297
|
+
previousPayload: previousDecision.newPayload
|
|
5640
6298
|
};
|
|
5641
6299
|
}
|
|
5642
6300
|
/**
|
|
@@ -6199,7 +6857,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6199
6857
|
// Update step entity with ActionExecutionLog ID if available
|
|
6200
6858
|
if (actionResult.LogEntry?.ID) {
|
|
6201
6859
|
stepEntity.TargetLogID = actionResult.LogEntry.ID;
|
|
6202
|
-
|
|
6860
|
+
this.queueStepSave(stepEntity);
|
|
6203
6861
|
}
|
|
6204
6862
|
// Prepare output data with action result
|
|
6205
6863
|
const outputData = {
|
|
@@ -7431,6 +8089,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7431
8089
|
this._agentRun.TotalTokensUsed = tokenStats.totalTokens;
|
|
7432
8090
|
this._agentRun.TotalPromptTokensUsed = tokenStats.promptTokens;
|
|
7433
8091
|
this._agentRun.TotalCompletionTokensUsed = tokenStats.completionTokens;
|
|
8092
|
+
this._agentRun.TotalCacheReadTokensUsed = tokenStats.cacheReadTokens;
|
|
8093
|
+
this._agentRun.TotalCacheWriteTokensUsed = tokenStats.cacheWriteTokens;
|
|
7434
8094
|
this._agentRun.TotalCost = tokenStats.totalCost;
|
|
7435
8095
|
await this._agentRun.Save();
|
|
7436
8096
|
}
|
|
@@ -7457,6 +8117,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7457
8117
|
this._agentRun.TotalTokensUsed = tokenStats.totalTokens;
|
|
7458
8118
|
this._agentRun.TotalPromptTokensUsed = tokenStats.promptTokens;
|
|
7459
8119
|
this._agentRun.TotalCompletionTokensUsed = tokenStats.completionTokens;
|
|
8120
|
+
this._agentRun.TotalCacheReadTokensUsed = tokenStats.cacheReadTokens;
|
|
8121
|
+
this._agentRun.TotalCacheWriteTokensUsed = tokenStats.cacheWriteTokens;
|
|
7460
8122
|
this._agentRun.TotalCost = tokenStats.totalCost;
|
|
7461
8123
|
await this._agentRun.Save();
|
|
7462
8124
|
}
|
|
@@ -7471,6 +8133,28 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7471
8133
|
* @private
|
|
7472
8134
|
*/
|
|
7473
8135
|
async finalizeAgentRun(finalStep, payload, contextUser) {
|
|
8136
|
+
// Await every pending step save (success OR failure) and accumulate diagnostics.
|
|
8137
|
+
// We use allSettled so a single failure doesn't shadow the rest, and we drain
|
|
8138
|
+
// both queues afterwards so an instance reused for another run doesn't leak
|
|
8139
|
+
// settled promises.
|
|
8140
|
+
const pending = this._pendingSaves;
|
|
8141
|
+
this._pendingSaves = [];
|
|
8142
|
+
this._stepSavePromises.clear();
|
|
8143
|
+
if (pending.length > 0) {
|
|
8144
|
+
const settled = await Promise.allSettled(pending);
|
|
8145
|
+
const rejections = settled.filter(s => s.status === 'rejected');
|
|
8146
|
+
const falses = settled.filter(s => s.status === 'fulfilled' && s.value === false).length;
|
|
8147
|
+
for (const r of rejections) {
|
|
8148
|
+
LogError(`Pending step save rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
8149
|
+
}
|
|
8150
|
+
const totalFailures = rejections.length + falses;
|
|
8151
|
+
if (totalFailures > 0 && this._agentRun) {
|
|
8152
|
+
const note = `${totalFailures} step record save(s) failed during this run; see logs for details.`;
|
|
8153
|
+
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
8154
|
+
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
8155
|
+
: note;
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
7474
8158
|
// Only resolve media placeholders for ROOT agents (depth === 0)
|
|
7475
8159
|
// Sub-agents keep placeholders intact so parent agents don't get huge base64 in their context
|
|
7476
8160
|
// The root agent resolves all placeholders when returning the final result to the UI
|
|
@@ -7483,12 +8167,6 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7483
8167
|
const resolvedActionableCommands = (finalStep.actionableCommands && isRootAgent)
|
|
7484
8168
|
? this.resolveMediaPlaceholdersInPayload(finalStep.actionableCommands)
|
|
7485
8169
|
: 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
8170
|
if (this._agentRun) {
|
|
7493
8171
|
this._agentRun.CompletedAt = new Date();
|
|
7494
8172
|
this._agentRun.Success = finalStep.step === 'Success' || finalStep.step === 'Chat';
|
|
@@ -7513,7 +8191,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7513
8191
|
}
|
|
7514
8192
|
this._agentRun.Result = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
7515
8193
|
this._agentRun.FinalStep = finalStep.step;
|
|
7516
|
-
this._agentRun.Message =
|
|
8194
|
+
this._agentRun.Message = finalStep.message;
|
|
7517
8195
|
// Set the FinalPayloadObject - this will automatically stringify for the DB
|
|
7518
8196
|
this._agentRun.FinalPayloadObject = resolvedPayload;
|
|
7519
8197
|
this._agentRun.FinalPayload = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
@@ -7522,6 +8200,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7522
8200
|
this._agentRun.TotalTokensUsed = tokenStats.totalTokens;
|
|
7523
8201
|
this._agentRun.TotalPromptTokensUsed = tokenStats.promptTokens;
|
|
7524
8202
|
this._agentRun.TotalCompletionTokensUsed = tokenStats.completionTokens;
|
|
8203
|
+
this._agentRun.TotalCacheReadTokensUsed = tokenStats.cacheReadTokens;
|
|
8204
|
+
this._agentRun.TotalCacheWriteTokensUsed = tokenStats.cacheWriteTokens;
|
|
7525
8205
|
this._agentRun.TotalCost = tokenStats.totalCost;
|
|
7526
8206
|
const ok = await this._agentRun.Save();
|
|
7527
8207
|
if (!ok) {
|
|
@@ -7532,10 +8212,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7532
8212
|
if (finalStep.promoteMediaOutputs && finalStep.promoteMediaOutputs.length > 0) {
|
|
7533
8213
|
this.promoteMediaOutputs(finalStep.promoteMediaOutputs);
|
|
7534
8214
|
}
|
|
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.
|
|
8215
|
+
// Return unified media outputs — all items are persisted by AgentRunner.
|
|
8216
|
+
// Sub-agents pass their mediaOutputs to parent for merging and placeholder resolution.
|
|
7539
8217
|
return {
|
|
7540
8218
|
success: finalStep.step === 'Success' || finalStep.step === 'Chat',
|
|
7541
8219
|
payload: resolvedPayload,
|
|
@@ -7562,15 +8240,19 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7562
8240
|
let totalTokens = 0;
|
|
7563
8241
|
let promptTokens = 0;
|
|
7564
8242
|
let completionTokens = 0;
|
|
8243
|
+
let cacheReadTokens = 0;
|
|
8244
|
+
let cacheWriteTokens = 0;
|
|
7565
8245
|
let totalCost = 0;
|
|
7566
8246
|
// Iterate through the agent run's steps to sum up tokens
|
|
7567
8247
|
if (this._agentRun?.Steps) {
|
|
7568
8248
|
for (const step of this._agentRun.Steps) {
|
|
7569
8249
|
if (step.StepType === 'Prompt' && step.PromptRun) {
|
|
7570
|
-
// Add tokens from prompt runs
|
|
8250
|
+
// Add tokens from prompt runs (rollup fields include any nested child prompt runs)
|
|
7571
8251
|
totalTokens += step.PromptRun.TokensUsedRollup || 0;
|
|
7572
8252
|
promptTokens += step.PromptRun.TokensPromptRollup || 0;
|
|
7573
8253
|
completionTokens += step.PromptRun.TokensCompletionRollup || 0;
|
|
8254
|
+
cacheReadTokens += step.PromptRun.TokensCacheReadRollup || 0;
|
|
8255
|
+
cacheWriteTokens += step.PromptRun.TokensCacheWriteRollup || 0;
|
|
7574
8256
|
totalCost += step.PromptRun.TotalCost || 0;
|
|
7575
8257
|
}
|
|
7576
8258
|
else if (step.StepType === 'Sub-Agent' && step.SubAgentRun) {
|
|
@@ -7578,11 +8260,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7578
8260
|
totalTokens += step.SubAgentRun.TotalTokensUsed || 0;
|
|
7579
8261
|
promptTokens += step.SubAgentRun.TotalPromptTokensUsed || 0;
|
|
7580
8262
|
completionTokens += step.SubAgentRun.TotalCompletionTokensUsed || 0;
|
|
8263
|
+
cacheReadTokens += step.SubAgentRun.TotalCacheReadTokensUsed || 0;
|
|
8264
|
+
cacheWriteTokens += step.SubAgentRun.TotalCacheWriteTokensUsed || 0;
|
|
7581
8265
|
totalCost += step.SubAgentRun.TotalCost || 0;
|
|
7582
8266
|
}
|
|
7583
8267
|
}
|
|
7584
8268
|
}
|
|
7585
|
-
return { totalTokens, promptTokens, completionTokens, totalCost };
|
|
8269
|
+
return { totalTokens, promptTokens, completionTokens, cacheReadTokens, cacheWriteTokens, totalCost };
|
|
7586
8270
|
}
|
|
7587
8271
|
/**
|
|
7588
8272
|
* Gets the count of how many times a specific action has been executed in this agent run.
|
|
@@ -7607,8 +8291,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7607
8291
|
/**
|
|
7608
8292
|
* Increments the execution count for an item (action or sub-agent).
|
|
7609
8293
|
*
|
|
8294
|
+
* Exposed as `protected` so driver sub-classes performing custom dispatch
|
|
8295
|
+
* bump the same per-item counter the framework checks against execution
|
|
8296
|
+
* guardrails — without this, custom dispatch silently bypasses limits.
|
|
8297
|
+
*
|
|
7610
8298
|
* @param itemId - The item ID to increment (action ID or sub-agent ID)
|
|
7611
|
-
* @
|
|
8299
|
+
* @protected
|
|
7612
8300
|
*/
|
|
7613
8301
|
incrementExecutionCount(itemId) {
|
|
7614
8302
|
const currentCount = this._executionCounts.get(itemId) || 0;
|
|
@@ -7619,7 +8307,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7619
8307
|
*
|
|
7620
8308
|
* @param itemId - The item ID to get count for
|
|
7621
8309
|
* @returns The execution count (0 if never executed)
|
|
7622
|
-
* @
|
|
8310
|
+
* @protected
|
|
7623
8311
|
*/
|
|
7624
8312
|
getExecutionCount(itemId) {
|
|
7625
8313
|
return this._executionCounts.get(itemId) || 0;
|