@memberjunction/ai-agents 6.1.4 → 6.2.0-edge.1
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 +25 -0
- package/dist/AgentDataPreloader.d.ts +4 -0
- package/dist/AgentDataPreloader.d.ts.map +1 -1
- package/dist/AgentDataPreloader.js +10 -2
- package/dist/AgentDataPreloader.js.map +1 -1
- package/dist/AgentRunner.d.ts +9 -4
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +39 -27
- package/dist/AgentRunner.js.map +1 -1
- package/dist/ArtifactToolManager.d.ts +4 -4
- package/dist/ArtifactToolManager.js +10 -10
- package/dist/ConversationToolManager.d.ts +16 -3
- package/dist/ConversationToolManager.d.ts.map +1 -1
- package/dist/ConversationToolManager.js +28 -5
- package/dist/ConversationToolManager.js.map +1 -1
- package/dist/PayloadChangeAnalyzer.d.ts +4 -0
- package/dist/PayloadChangeAnalyzer.d.ts.map +1 -1
- package/dist/PayloadChangeAnalyzer.js +10 -2
- package/dist/PayloadChangeAnalyzer.js.map +1 -1
- package/dist/PayloadFeedbackManager.d.ts +8 -0
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +20 -4
- package/dist/PayloadFeedbackManager.js.map +1 -1
- package/dist/PayloadManager.d.ts +27 -0
- package/dist/PayloadManager.d.ts.map +1 -1
- package/dist/PayloadManager.js +47 -15
- package/dist/PayloadManager.js.map +1 -1
- package/dist/agent-types/base-agent-type.d.ts +16 -0
- package/dist/agent-types/base-agent-type.d.ts.map +1 -1
- package/dist/agent-types/base-agent-type.js +20 -1
- package/dist/agent-types/base-agent-type.js.map +1 -1
- package/dist/agent-types/flow-agent-type.d.ts +9 -0
- package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
- package/dist/agent-types/flow-agent-type.js +18 -5
- package/dist/agent-types/flow-agent-type.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +74 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +2 -0
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/artifact-target-plan.d.ts +2 -0
- package/dist/artifact-target-plan.d.ts.map +1 -1
- package/dist/artifact-target-plan.js +5 -1
- package/dist/artifact-target-plan.js.map +1 -1
- package/dist/base-agent.d.ts +408 -3
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +924 -78
- package/dist/base-agent.js.map +1 -1
- package/dist/constants.d.ts +57 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +70 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +12 -12
- package/dist/memory-manager-agent.js +65 -65
- package/dist/native-tools/action-tool-builder.d.ts +8 -0
- package/dist/native-tools/action-tool-builder.d.ts.map +1 -1
- package/dist/native-tools/action-tool-builder.js +22 -6
- package/dist/native-tools/action-tool-builder.js.map +1 -1
- package/dist/native-tools/control-tools.d.ts +8 -0
- package/dist/native-tools/control-tools.d.ts.map +1 -1
- package/dist/native-tools/control-tools.js +25 -9
- package/dist/native-tools/control-tools.js.map +1 -1
- package/dist/native-tools/dual-channel.d.ts +2 -0
- package/dist/native-tools/dual-channel.d.ts.map +1 -1
- package/dist/native-tools/dual-channel.js +5 -1
- package/dist/native-tools/dual-channel.js.map +1 -1
- package/dist/native-tools/tool-result-turns.d.ts +9 -0
- package/dist/native-tools/tool-result-turns.d.ts.map +1 -1
- package/dist/native-tools/tool-result-turns.js +15 -3
- package/dist/native-tools/tool-result-turns.js.map +1 -1
- package/dist/pipeline/coerce.d.ts +14 -0
- package/dist/pipeline/coerce.d.ts.map +1 -1
- package/dist/pipeline/coerce.js +37 -9
- package/dist/pipeline/coerce.js.map +1 -1
- package/dist/pipeline/jsonpath-eval.d.ts +4 -0
- package/dist/pipeline/jsonpath-eval.d.ts.map +1 -1
- package/dist/pipeline/jsonpath-eval.js +10 -2
- package/dist/pipeline/jsonpath-eval.js.map +1 -1
- package/dist/pipeline/operators.js +16 -16
- package/dist/pipeline/path.d.ts +6 -0
- package/dist/pipeline/path.d.ts.map +1 -1
- package/dist/pipeline/path.js +17 -5
- package/dist/pipeline/path.js.map +1 -1
- package/dist/pipeline/pipeline-executor.js +10 -10
- package/dist/pipeline/predicate.d.ts +4 -0
- package/dist/pipeline/predicate.d.ts.map +1 -1
- package/dist/pipeline/predicate.js +15 -7
- package/dist/pipeline/predicate.js.map +1 -1
- package/dist/pipeline/providers/action-provider.js +2 -2
- package/dist/pipeline/providers/artifact-tool-provider.js +2 -2
- package/dist/pipeline/providers/serialize.d.ts +4 -0
- package/dist/pipeline/providers/serialize.d.ts.map +1 -1
- package/dist/pipeline/providers/serialize.js +10 -2
- package/dist/pipeline/providers/serialize.js.map +1 -1
- package/dist/pipeline/template.d.ts +4 -0
- package/dist/pipeline/template.d.ts.map +1 -1
- package/dist/pipeline/template.js +15 -7
- package/dist/pipeline/template.js.map +1 -1
- package/dist/realtime/agent-media-library.d.ts +10 -0
- package/dist/realtime/agent-media-library.d.ts.map +1 -1
- package/dist/realtime/agent-media-library.js +29 -9
- package/dist/realtime/agent-media-library.js.map +1 -1
- package/dist/realtime/media-channel-server.js +2 -2
- package/dist/realtime/meeting-controls-state.d.ts +2 -0
- package/dist/realtime/meeting-controls-state.d.ts.map +1 -1
- package/dist/realtime/meeting-controls-state.js +6 -2
- package/dist/realtime/meeting-controls-state.js.map +1 -1
- package/dist/realtime/realtime-channel-server-data-context.d.ts +2 -0
- package/dist/realtime/realtime-channel-server-data-context.d.ts.map +1 -1
- package/dist/realtime/realtime-channel-server-data-context.js +5 -1
- package/dist/realtime/realtime-channel-server-data-context.js.map +1 -1
- package/dist/realtime/realtime-channel-server-host.js +2 -2
- package/dist/realtime/realtime-client-session-service.d.ts +4 -0
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +15 -6
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +2 -0
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +6 -2
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-recording-store.d.ts +15 -0
- package/dist/realtime/realtime-recording-store.d.ts.map +1 -1
- package/dist/realtime/realtime-recording-store.js +31 -7
- package/dist/realtime/realtime-recording-store.js.map +1 -1
- package/dist/realtime/realtime-session-runner.d.ts +4 -4
- package/dist/realtime/realtime-session-runner.js +9 -9
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/runtime-state-fragment.d.ts +101 -0
- package/dist/runtime-state-fragment.d.ts.map +1 -0
- package/dist/runtime-state-fragment.js +173 -0
- package/dist/runtime-state-fragment.js.map +1 -0
- package/dist/types/payload-operations.d.ts +8 -0
- package/dist/types/payload-operations.d.ts.map +1 -1
- package/dist/types/payload-operations.js +21 -5
- package/dist/types/payload-operations.js.map +1 -1
- package/dist/utils/ConversationMessageResolver.d.ts +4 -0
- package/dist/utils/ConversationMessageResolver.d.ts.map +1 -1
- package/dist/utils/ConversationMessageResolver.js +10 -2
- package/dist/utils/ConversationMessageResolver.js.map +1 -1
- package/dist/volatile-child-prompt.d.ts +33 -0
- package/dist/volatile-child-prompt.d.ts.map +1 -0
- package/dist/volatile-child-prompt.js +76 -0
- package/dist/volatile-child-prompt.js.map +1 -0
- package/package.json +20 -18
package/dist/base-agent.js
CHANGED
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase, MJEnvironmentEntityExtended } from '@memberjunction/core-entities';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
14
|
+
import { BuildActionToolSet, FilterDeclarableActions, SanitizeToolName } from './native-tools/action-tool-builder.js';
|
|
15
|
+
import { BuildNativeToolSet, SUB_AGENT_TOOL_PREFIX } from './native-tools/control-tools.js';
|
|
16
|
+
import { BuildAssistantToolCallTurn, BuildToolResultTurn, CompactToolResultContent } from './native-tools/tool-result-turns.js';
|
|
17
|
+
import { LooksLikeLoopEnvelope } from './native-tools/dual-channel.js';
|
|
18
|
+
import { ResolvePromptRunUserID } from "@memberjunction/ai-core-plus";
|
|
18
19
|
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
19
20
|
import { AgentRunWatchdog } from './agent-run-watchdog.js';
|
|
20
21
|
import { AIPromptRunner, GetToolCallingDecision } from '@memberjunction/ai-prompts';
|
|
21
|
-
import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
|
|
22
|
+
import { BaseRealtimeModel, GetAIAPIKey, IsPrefixPromptCache } from '@memberjunction/ai';
|
|
22
23
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
23
|
-
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString } from '@memberjunction/global';
|
|
24
|
+
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, NormalizeUUID, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString } from '@memberjunction/global';
|
|
24
25
|
// token optimization via @memberjunction/context-crush (SmartCrusher/CacheAligner-inspired)
|
|
25
26
|
import { CrushJSON, DescribeCrush, PartitionStablePrefix } from '@memberjunction/context-crush';
|
|
26
27
|
// AST-aware code reduction (CodeCompressor-inspired) — opt-in per agent type
|
|
@@ -32,7 +33,7 @@ import { SelectRealtimeVendorForModel } from './realtime/realtime-vendor-resolut
|
|
|
32
33
|
import { RealtimeClientSessionService, WarnOnUnmatchedProviderVoice } from './realtime/realtime-client-session-service.js';
|
|
33
34
|
import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
|
|
34
35
|
import { RealtimeRecordingController } from './realtime/realtime-recording-capture.js';
|
|
35
|
-
import {
|
|
36
|
+
import { ResolveRecordingStorageAccountID, StoreRealtimeRecording } from './realtime/realtime-recording-store.js';
|
|
36
37
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
37
38
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
38
39
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
@@ -43,7 +44,12 @@ import { FormatToolResultSection, FormatToolErrorSection, RenderToolResultData,
|
|
|
43
44
|
import { PriorTurnToolResultCache } from './prior-turn-tool-result-cache.js';
|
|
44
45
|
import { PromptComponentResolver, InjectScopedPromptParts } from './prompt-component-resolver.js';
|
|
45
46
|
import { ScopedPromptConfigResolver, ApplyScopedPromptConfig } from './scoped-prompt-config-resolver.js';
|
|
46
|
-
import { StringifyForPersistence, AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter } from '@memberjunction/ai-core-plus';
|
|
47
|
+
import { StringifyForPersistence, AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter, SystemPlaceholderManager } from '@memberjunction/ai-core-plus';
|
|
48
|
+
import { ActionResult, RunActionParams } from '@memberjunction/actions-base';
|
|
49
|
+
import { TemplateEngineServer } from '@memberjunction/templates';
|
|
50
|
+
import { RuntimeStateFragmentBuilder, EscapeRuntimeStateTagsInMessage } from './runtime-state-fragment.js';
|
|
51
|
+
import { ResolveSpecializationPlacement } from './volatile-child-prompt.js';
|
|
52
|
+
import { CURRENT_DATE_PLACEHOLDER, CURRENT_DAY_OF_WEEK_PLACEHOLDER, CURRENT_TIME_PLACEHOLDER, RUNTIME_STATE_TAG, SCRATCHPAD_NOTES_PLACEHOLDER, SCRATCHPAD_TASK_SUMMARY_PLACEHOLDER, SCRATCHPAD_TASKS_PLACEHOLDER, VOLATILE_TEMPLATE_MARKERS, } from './constants.js';
|
|
47
53
|
import { AgentRunner } from './AgentRunner.js';
|
|
48
54
|
import { PayloadManager } from './PayloadManager.js';
|
|
49
55
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
@@ -106,6 +112,21 @@ import _ from 'lodash';
|
|
|
106
112
|
* one) from saturating the model API / DB pool with N concurrent runs.
|
|
107
113
|
*/
|
|
108
114
|
const PARALLEL_SUBAGENT_CONCURRENCY_LIMIT = 5;
|
|
115
|
+
/** Identical-arguments rule: this many failures with the same arguments block further identical calls. */
|
|
116
|
+
export const IDENTICAL_FAILURE_THRESHOLD = 2;
|
|
117
|
+
/** Attempt budget: this many consecutive failures, across any arguments, disable the action for the run. */
|
|
118
|
+
export const ACTION_FAILURE_BUDGET = 5;
|
|
119
|
+
/**
|
|
120
|
+
* The {@link ActionResult} returned when the run-scoped circuit breaker blocks a call before it
|
|
121
|
+
* reaches the action engine. Carries the rule that fired so the failure directive can name it
|
|
122
|
+
* directly instead of re-deriving it from the failure history, which a blocked call never updates.
|
|
123
|
+
*/
|
|
124
|
+
export class CircuitBreakerActionResult extends ActionResult {
|
|
125
|
+
constructor(Reason) {
|
|
126
|
+
super();
|
|
127
|
+
this.Reason = Reason;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
109
130
|
export class BaseAgent {
|
|
110
131
|
constructor() {
|
|
111
132
|
/**
|
|
@@ -128,6 +149,24 @@ export class BaseAgent {
|
|
|
128
149
|
* @private
|
|
129
150
|
*/
|
|
130
151
|
this._activeProvider = Metadata.Provider; // global-provider-ok: default until Execute() captures per-request provider
|
|
152
|
+
/**
|
|
153
|
+
* Index in conversationMessages where this agent run began, used to accurately restore
|
|
154
|
+
* turn 1's trailing state message in append-only mode without corrupting prior chat turns.
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
this._turn1InsertionIndex = -1;
|
|
158
|
+
/**
|
|
159
|
+
* Actions that have failed fatally (e.g., missing API key, unauthorized, or repeated unrecoverable errors)
|
|
160
|
+
* during the current agent run. Subsequent attempts to execute these actions are short-circuited in 0ms.
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
this._fatalActionFailures = new Set();
|
|
164
|
+
/**
|
|
165
|
+
* Parameter-aware failure tracking per action name for the current agent run.
|
|
166
|
+
* Differentiates identical retries (which trip quickly) from parameter modifications (which allow self-correction).
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
this._actionFailureHistory = new Map();
|
|
131
170
|
/**
|
|
132
171
|
* This is state information that is specific to the agent type. BaseAgent doesn't know what
|
|
133
172
|
* this contains or care, it is just responsible for keeping this, giving the Agent Type the
|
|
@@ -460,6 +499,242 @@ export class BaseAgent {
|
|
|
460
499
|
* @private
|
|
461
500
|
*/
|
|
462
501
|
static { this.MAX_CONSECUTIVE_UNPRODUCTIVE_RETRIES = 10; }
|
|
502
|
+
/**
|
|
503
|
+
* The identity of one action call's arguments, for the circuit breaker's identical-arguments
|
|
504
|
+
* rule: two calls with the same normalized string are "the same call", whatever order the model
|
|
505
|
+
* wrote the keys in.
|
|
506
|
+
*
|
|
507
|
+
* Algorithm, top to bottom:
|
|
508
|
+
* 1. Null, undefined or a non-object yields `''` (a call with no arguments).
|
|
509
|
+
* 2. The top-level keys are sorted, and each `(key, value)` pair passes through
|
|
510
|
+
* {@link normalizeActionParamEntry}, which may rename it, rewrite its value, or drop it.
|
|
511
|
+
* 3. Every value passes through {@link normalizeActionParamValue}: plain objects are rebuilt with
|
|
512
|
+
* sorted keys at EVERY depth, arrays keep their order but normalize each element, and
|
|
513
|
+
* anything else (strings, numbers, booleans, null, Dates, entity instances) is kept as is.
|
|
514
|
+
* 4. The result is serialized with `JSON.stringify`. Should that throw (a circular reference,
|
|
515
|
+
* a BigInt), the fallback is a sorted list of the top-level keys — still deterministic, still
|
|
516
|
+
* distinguishes differently-shaped calls, and never throws.
|
|
517
|
+
*
|
|
518
|
+
* Three protected layers so a subclass can change one part without re-implementing the rest:
|
|
519
|
+
* override {@link normalizeActionParamEntry} to ignore a key (a trace id, a timestamp the model
|
|
520
|
+
* regenerates on every call), or {@link normalizeActionParamValue} to canonicalize values
|
|
521
|
+
* (case-fold a search query, trim whitespace) so near-identical retries count as identical.
|
|
522
|
+
*/
|
|
523
|
+
normalizeActionParams(params) {
|
|
524
|
+
if (!params || typeof params !== 'object') {
|
|
525
|
+
return '';
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
const normalized = {};
|
|
529
|
+
for (const key of Object.keys(params).sort()) {
|
|
530
|
+
const entry = this.normalizeActionParamEntry(key, params[key]);
|
|
531
|
+
if (entry) {
|
|
532
|
+
normalized[entry.key] = entry.value;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return JSON.stringify(normalized);
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return `[unserializable:${Object.keys(params).sort().join(',')}]`;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Normalizes one top-level `(key, value)` pair of an action's arguments. The default keeps the
|
|
543
|
+
* key and normalizes the value through {@link normalizeActionParamValue}. Return `null` to drop
|
|
544
|
+
* the pair from the call's identity — the seam for ignoring arguments that legitimately differ
|
|
545
|
+
* between otherwise identical retries.
|
|
546
|
+
*/
|
|
547
|
+
normalizeActionParamEntry(key, value) {
|
|
548
|
+
return { key, value: this.normalizeActionParamValue(value) };
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Normalizes one value, recursively: a plain object is rebuilt with its keys sorted, an array
|
|
552
|
+
* keeps its order with each element normalized, and any other value is returned unchanged. The
|
|
553
|
+
* seam for canonicalizing values before they are compared.
|
|
554
|
+
*
|
|
555
|
+
* "Plain" here is by prototype (`Object.prototype` or none), not the structural
|
|
556
|
+
* `IsPlainObject` from `@memberjunction/global`: a Date, Map or entity instance must pass through
|
|
557
|
+
* as an opaque leaf and serialize as itself, not be rebuilt as an empty bag of sorted keys.
|
|
558
|
+
*/
|
|
559
|
+
normalizeActionParamValue(value) {
|
|
560
|
+
if (Array.isArray(value)) {
|
|
561
|
+
return value.map(item => this.normalizeActionParamValue(item));
|
|
562
|
+
}
|
|
563
|
+
if (value !== null && typeof value === 'object' && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
564
|
+
const source = value;
|
|
565
|
+
const sorted = {};
|
|
566
|
+
for (const key of Object.keys(source).sort()) {
|
|
567
|
+
sorted[key] = this.normalizeActionParamValue(source[key]);
|
|
568
|
+
}
|
|
569
|
+
return sorted;
|
|
570
|
+
}
|
|
571
|
+
return value;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Detects whether an action error message represents a fatal configuration or credential
|
|
575
|
+
* problem: the tool cannot work in this environment no matter what arguments it is given,
|
|
576
|
+
* so retrying is pointless and the action is locked out for the rest of the run.
|
|
577
|
+
*
|
|
578
|
+
* Deliberately NOT fatal: HTTP 401/403, "unauthorized" and "forbidden". Those are usually
|
|
579
|
+
* per-resource (one site blocking a fetch, one record the user cannot read) or transient
|
|
580
|
+
* (a search provider using 403 as a rate limit), so they fall through to the parameter-aware
|
|
581
|
+
* failure history where the identical-arguments rule and the consecutive-attempt budget
|
|
582
|
+
* bound them without disabling the tool for every other resource.
|
|
583
|
+
*
|
|
584
|
+
* Also NOT fatal, for the same reason: a failure the model can fix by changing its arguments.
|
|
585
|
+
* A message that names a parameter is treated as an argument problem whatever else it says,
|
|
586
|
+
* and a call that itself carried credential-shaped arguments (password, API key, token) is
|
|
587
|
+
* never fatal even on "authentication failed", because the credential came from the model,
|
|
588
|
+
* not the environment. Demoting a message from fatal costs at most the attempt budget.
|
|
589
|
+
*
|
|
590
|
+
* @param message The action's failure message.
|
|
591
|
+
* @param actionParams The arguments the call was made with, when known.
|
|
592
|
+
*/
|
|
593
|
+
isFatalActionError(message, actionParams) {
|
|
594
|
+
if (!message) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
// 1. Parameter/argument/input problems are recoverable (the agent can adjust inputs), so they
|
|
598
|
+
// never trip the fatal breaker, however the rest of the message is phrased.
|
|
599
|
+
const isParamError = /\b(?:parameter|argument|param|input|field|option|property|value|column|filter|header)\b/i.test(message);
|
|
600
|
+
if (isParamError || this.hasCredentialShapedArguments(actionParams)) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
// 2. Missing or invalid credentials, API keys, or authentication failures are unrecoverable in this run
|
|
604
|
+
const fatalCredentialPattern = /(?:api[\s_-]?key\s+(?:is\s+)?(?:not\s+found|missing|required|invalid)|(?:missing|invalid)\s+api[\s_-]?key|no\s+api[\s_-]?key|credentials?\s+(?:not\s+found|missing)|authentication\s+failed)/i;
|
|
605
|
+
if (fatalCredentialPattern.test(message)) {
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
// 3. Action/service/provider-level configuration problems where the tool itself cannot execute in this environment
|
|
609
|
+
const fatalConfigPattern = /(?:(?:action|tool|service|provider|integration|driver|client|api|extension|engine|server)\s+(?:is\s+)?not\s+configured|not\s+configured\s+(?:for\s+(?:this\s+)?tenant|in\s+(?:this\s+)?environment|on\s+this\s+server|in\s+(?:config|mj\.config))|^\s*(?:action\s+)?(?:is\s+)?not\s+configured[.!]*\s*$)/i;
|
|
610
|
+
return fatalConfigPattern.test(message);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* True when any top-level argument name looks like a credential the model supplied itself
|
|
614
|
+
* (password, secret, credential, API key, or an access / auth / bearer / refresh / ID token).
|
|
615
|
+
* `maxTokens`-style names are deliberately not matched.
|
|
616
|
+
*/
|
|
617
|
+
hasCredentialShapedArguments(actionParams) {
|
|
618
|
+
if (!actionParams || typeof actionParams !== 'object') {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
const credentialKey = /password|passwd|secret|credential|api[_-]?key|(?:access|auth|bearer|refresh|id)[_-]?token|^token$/i;
|
|
622
|
+
return Object.keys(actionParams).some(key => credentialKey.test(key));
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Records a non-successful outcome for the run-scoped action circuit breaker. A fatal
|
|
626
|
+
* configuration error locks the action out for the rest of the run; any other failure
|
|
627
|
+
* updates the parameter-aware history behind the identical-arguments rule and the
|
|
628
|
+
* consecutive-attempt budget.
|
|
629
|
+
*/
|
|
630
|
+
recordActionFailure(action, actionEntity, message, normalizedParams) {
|
|
631
|
+
if (this.isFatalActionError(message, action.params)) {
|
|
632
|
+
this._fatalActionFailures.add(action.name);
|
|
633
|
+
if (actionEntity?.Name) {
|
|
634
|
+
this._fatalActionFailures.add(actionEntity.Name);
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
const existing = this._actionFailureHistory.get(action.name) || (actionEntity?.Name ? this._actionFailureHistory.get(actionEntity.Name) : undefined);
|
|
639
|
+
const isIdentical = existing !== undefined && existing.lastParamsString === normalizedParams;
|
|
640
|
+
const record = {
|
|
641
|
+
lastParamsString: normalizedParams,
|
|
642
|
+
identicalFailures: isIdentical && existing ? existing.identicalFailures + 1 : 1,
|
|
643
|
+
totalConsecutiveFailures: (existing?.totalConsecutiveFailures ?? 0) + 1
|
|
644
|
+
};
|
|
645
|
+
this._actionFailureHistory.set(action.name, record);
|
|
646
|
+
if (actionEntity?.Name) {
|
|
647
|
+
this._actionFailureHistory.set(actionEntity.Name, record);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Clears the parameter-aware failure history for an action after it succeeds, so the
|
|
652
|
+
* identical-arguments rule and the consecutive-attempt budget start over.
|
|
653
|
+
*/
|
|
654
|
+
clearActionFailureRecord(action, actionEntity) {
|
|
655
|
+
this._actionFailureHistory.delete(action.name);
|
|
656
|
+
if (actionEntity?.Name) {
|
|
657
|
+
this._actionFailureHistory.delete(actionEntity.Name);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Applies the three circuit-breaker rules to a call that is about to be dispatched. Returns a
|
|
662
|
+
* blocked result, with the rule that fired, when the call must not go to the action engine;
|
|
663
|
+
* null when it may proceed. Rules are checked fatal → identical-arguments → budget.
|
|
664
|
+
*/
|
|
665
|
+
checkActionCircuitBreaker(params, action, actionEntity, normalizedParams) {
|
|
666
|
+
const entityKey = actionEntity?.Name;
|
|
667
|
+
if (this._fatalActionFailures.has(action.name) || (entityKey && this._fatalActionFailures.has(entityKey))) {
|
|
668
|
+
this.logStatus(` ⚡ Circuit breaker: Action '${action.name}' short-circuited (0ms): fatal configuration or credential error earlier in this run`, false, params);
|
|
669
|
+
return this.buildBlockedActionResult(actionEntity, 'fatal', `Action '${action.name}' is disabled for this run because it previously failed with an unrecoverable configuration or credential error. You must select an alternative action.`);
|
|
670
|
+
}
|
|
671
|
+
const record = this._actionFailureHistory.get(action.name) || (entityKey ? this._actionFailureHistory.get(entityKey) : undefined);
|
|
672
|
+
if (!record) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
if (record.lastParamsString === normalizedParams && record.identicalFailures >= IDENTICAL_FAILURE_THRESHOLD) {
|
|
676
|
+
this.logStatus(` ⚡ Circuit breaker: Action '${action.name}' short-circuited on identical retry loop (0ms)`, false, params);
|
|
677
|
+
return this.buildBlockedActionResult(actionEntity, 'identical-arguments', `Action '${action.name}' is disabled for these inputs because it already failed ${record.identicalFailures} times with identical arguments. You must modify your parameters or select an alternative tool.`);
|
|
678
|
+
}
|
|
679
|
+
if (record.totalConsecutiveFailures >= ACTION_FAILURE_BUDGET) {
|
|
680
|
+
this.logStatus(` ⚡ Circuit breaker: Action '${action.name}' short-circuited on max retry attempts (0ms)`, false, params);
|
|
681
|
+
return this.buildBlockedActionResult(actionEntity, 'attempts-exhausted', `Action '${action.name}' is disabled for this run after ${ACTION_FAILURE_BUDGET} consecutive failures across parameter attempts. You must select an alternative tool or proceed with available data.`);
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
/** The failed {@link ActionResult} a blocked call returns in place of dispatching. */
|
|
686
|
+
buildBlockedActionResult(actionEntity, reason, message) {
|
|
687
|
+
const blocked = new CircuitBreakerActionResult(reason);
|
|
688
|
+
blocked.Success = false;
|
|
689
|
+
blocked.Message = message;
|
|
690
|
+
blocked.Params = [];
|
|
691
|
+
blocked.RunParams = new RunActionParams();
|
|
692
|
+
blocked.RunParams.Action = actionEntity;
|
|
693
|
+
return blocked;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Names the rule behind a failed action summary so the directive matches what actually
|
|
697
|
+
* happened. A blocked call reports the rule that blocked it. A dispatched failure is fatal if
|
|
698
|
+
* `recordActionFailure` locked the action out (the summary has no access to the call's
|
|
699
|
+
* arguments, so the decision is read back rather than re-derived from the message); otherwise
|
|
700
|
+
* the budget is checked BEFORE the identical-arguments rule, because once the budget is spent
|
|
701
|
+
* the action is blocked whatever the arguments are, and telling the model to change them
|
|
702
|
+
* would send it in circles.
|
|
703
|
+
*/
|
|
704
|
+
classifyActionFailure(summary) {
|
|
705
|
+
if (summary.breakerReason) {
|
|
706
|
+
return summary.breakerReason;
|
|
707
|
+
}
|
|
708
|
+
if (this._fatalActionFailures.has(summary.actionName)) {
|
|
709
|
+
return 'fatal';
|
|
710
|
+
}
|
|
711
|
+
const record = this._actionFailureHistory.get(summary.actionName);
|
|
712
|
+
if (!record) {
|
|
713
|
+
return 'warning';
|
|
714
|
+
}
|
|
715
|
+
if (record.totalConsecutiveFailures >= ACTION_FAILURE_BUDGET) {
|
|
716
|
+
return 'attempts-exhausted';
|
|
717
|
+
}
|
|
718
|
+
if (record.identicalFailures >= IDENTICAL_FAILURE_THRESHOLD) {
|
|
719
|
+
return 'identical-arguments';
|
|
720
|
+
}
|
|
721
|
+
return 'warning';
|
|
722
|
+
}
|
|
723
|
+
/** The guidance line appended to the history for one failed action. */
|
|
724
|
+
formatActionFailureDirective(summary) {
|
|
725
|
+
const name = summary.actionName;
|
|
726
|
+
const record = this._actionFailureHistory.get(name);
|
|
727
|
+
switch (this.classifyActionFailure(summary)) {
|
|
728
|
+
case 'fatal':
|
|
729
|
+
return `[CRITICAL/ACTION_UNAVAILABLE] Action '${name}' failed with an unrecoverable configuration or credential error: "${summary.message}". This action cannot execute in this environment. DO NOT call '${name}' again during this run. You MUST select an alternative tool or proceed with available data.`;
|
|
730
|
+
case 'attempts-exhausted':
|
|
731
|
+
return `[CRITICAL/ATTEMPTS_EXHAUSTED] Action '${name}' has failed ${record?.totalConsecutiveFailures ?? ACTION_FAILURE_BUDGET} consecutive times: "${summary.message}". Retries for this action are exhausted. You MUST pivot to an alternative tool or continue with available data.`;
|
|
732
|
+
case 'identical-arguments':
|
|
733
|
+
return `[CRITICAL/REPEATED_IDENTICAL_CALL] Action '${name}' failed again with the EXACT SAME arguments: "${summary.message}". Calling '${name}' with these parameters will not work. You MUST either adjust your parameters or pivot to an alternative tool.`;
|
|
734
|
+
default:
|
|
735
|
+
return `[WARNING/ACTION_FAILURE] Action '${name}' failed: "${summary.message}". Review the error and adjust your input parameters (attempt ${record?.totalConsecutiveFailures ?? 1} of ${ACTION_FAILURE_BUDGET}). DO NOT retry calling '${name}' with identical arguments.`;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
463
738
|
/**
|
|
464
739
|
* Returns the active metadata provider for this agent run. Subclasses MUST
|
|
465
740
|
* use this getter (rather than `new Metadata()` or `Metadata.Provider`) so
|
|
@@ -575,12 +850,16 @@ export class BaseAgent {
|
|
|
575
850
|
* }]);
|
|
576
851
|
* ```
|
|
577
852
|
*/
|
|
578
|
-
|
|
853
|
+
PromoteMediaOutputs(mediaOutputs) {
|
|
579
854
|
if (mediaOutputs && mediaOutputs.length > 0) {
|
|
580
855
|
this._mediaOutputs.push(...mediaOutputs);
|
|
581
856
|
this.logStatus(`📎 Promoted ${mediaOutputs.length} media output(s) to agent results`, true);
|
|
582
857
|
}
|
|
583
858
|
}
|
|
859
|
+
/** @deprecated Use {@link PromoteMediaOutputs}. */
|
|
860
|
+
promoteMediaOutputs(mediaOutputs) {
|
|
861
|
+
return this.PromoteMediaOutputs(mediaOutputs);
|
|
862
|
+
}
|
|
584
863
|
/**
|
|
585
864
|
* Gets the currently accumulated media outputs for this agent run.
|
|
586
865
|
* @returns Array of promoted media outputs
|
|
@@ -1196,6 +1475,11 @@ export class BaseAgent {
|
|
|
1196
1475
|
this._executeParams = wrappedParams;
|
|
1197
1476
|
this._agentConfig = undefined;
|
|
1198
1477
|
this._lastModelSelectionInfo = undefined;
|
|
1478
|
+
this._lastVolatileStateMessage = undefined;
|
|
1479
|
+
this._resolvedTrailingStateMode = undefined;
|
|
1480
|
+
this._turn1InsertionIndex = -1;
|
|
1481
|
+
this._fatalActionFailures.clear();
|
|
1482
|
+
this._actionFailureHistory.clear();
|
|
1199
1483
|
// Convert UI markup in conversation messages to plain text if requested (default: true)
|
|
1200
1484
|
if (params.convertUIMarkupToPlainText !== false) {
|
|
1201
1485
|
this.convertUIMarkupInMessages(wrappedParams.conversationMessages);
|
|
@@ -1206,7 +1490,8 @@ export class BaseAgent {
|
|
|
1206
1490
|
this._memoryWriteManager.Clear();
|
|
1207
1491
|
// Arm conversation-history retrieval tools — available only when the run has a
|
|
1208
1492
|
// conversation to page against (the same gate as all cross-turn context features).
|
|
1209
|
-
|
|
1493
|
+
// A history floor holds here too: the tools page only rows from it onward.
|
|
1494
|
+
this._conversationToolManager.Initialize(wrappedParams.conversationId || null, params.contextUser, wrappedParams.ConversationHistoryFrom ?? null);
|
|
1210
1495
|
this._conversationToolManager.SetSummaryHost(this.buildConversationSummaryHost(wrappedParams));
|
|
1211
1496
|
// Initialize artifact tools with any input artifacts attached to the run.
|
|
1212
1497
|
// Artifacts arrive as a typed first-class field on ExecuteAgentParams —
|
|
@@ -1753,6 +2038,11 @@ export class BaseAgent {
|
|
|
1753
2038
|
promptRun.ModelID = modelResolution.modelID;
|
|
1754
2039
|
promptRun.VendorID = modelResolution.vendorID || null;
|
|
1755
2040
|
promptRun.AgentID = params.agent.ID;
|
|
2041
|
+
promptRun.UserID = ResolvePromptRunUserID({
|
|
2042
|
+
UserID: params.userId,
|
|
2043
|
+
AgentRun: this._agentRun,
|
|
2044
|
+
ContextUser: params.contextUser,
|
|
2045
|
+
});
|
|
1756
2046
|
promptRun.Status = 'Running';
|
|
1757
2047
|
promptRun.RunAt = new Date();
|
|
1758
2048
|
promptRun.StreamingEnabled = true;
|
|
@@ -2260,7 +2550,7 @@ export class BaseAgent {
|
|
|
2260
2550
|
}
|
|
2261
2551
|
// Storage: recording provider, else attachment provider; then that provider's first account.
|
|
2262
2552
|
const storageAccountId = params.contextUser
|
|
2263
|
-
? await
|
|
2553
|
+
? await ResolveRecordingStorageAccountID(agent, params.contextUser, params.provider || this._activeProvider)
|
|
2264
2554
|
: null;
|
|
2265
2555
|
if (!storageAccountId) {
|
|
2266
2556
|
this.logStatus('🔴 Realtime recording on but no resolvable storage account (RecordingStorageProviderID/AttachmentStorageProviderID) — recording disabled.', false, params);
|
|
@@ -2311,7 +2601,7 @@ export class BaseAgent {
|
|
|
2311
2601
|
// same mixed PCM as the WAV — persisted as a peaks.json sidecar so the player renders the
|
|
2312
2602
|
// real waveform without re-decoding the audio. Best-effort: an empty array writes no sidecar.
|
|
2313
2603
|
const peaks = controller.GetPeaks();
|
|
2314
|
-
const stored = await
|
|
2604
|
+
const stored = await StoreRealtimeRecording({
|
|
2315
2605
|
Audio: encoded.Buffer,
|
|
2316
2606
|
MimeType: 'audio/wav',
|
|
2317
2607
|
Media: controller.Media,
|
|
@@ -2451,7 +2741,7 @@ export class BaseAgent {
|
|
|
2451
2741
|
stepCount++;
|
|
2452
2742
|
// Promote any media outputs from this step to the agent's outputs
|
|
2453
2743
|
if (nextStep.promoteMediaOutputs && nextStep.promoteMediaOutputs.length > 0) {
|
|
2454
|
-
this.
|
|
2744
|
+
this.PromoteMediaOutputs(nextStep.promoteMediaOutputs);
|
|
2455
2745
|
}
|
|
2456
2746
|
// Track consecutive failed steps to prevent infinite retry loops.
|
|
2457
2747
|
// Any non-Failed step resets the counter.
|
|
@@ -2976,7 +3266,7 @@ export class BaseAgent {
|
|
|
2976
3266
|
stepEntity.NativeToolCallCount = callCount;
|
|
2977
3267
|
// A tool call wins, but a turn that ALSO carried a valid
|
|
2978
3268
|
// envelope gave two answers, and the one we discard has to be counted somewhere.
|
|
2979
|
-
stepEntity.NativeDualChannel = callCount > 0 ?
|
|
3269
|
+
stepEntity.NativeDualChannel = callCount > 0 ? LooksLikeLoopEnvelope(message?.content) : null;
|
|
2980
3270
|
// whether this step's results went back as native tool-result turns.
|
|
2981
3271
|
stepEntity.NativeToolResultsSent = GetToolCallingDecision(promptResult?.chatResult)?.toolResults === true;
|
|
2982
3272
|
if (stepEntity.NativeDualChannel) {
|
|
@@ -3006,7 +3296,7 @@ export class BaseAgent {
|
|
|
3006
3296
|
if (!turn?.sendResultsNatively || this._lastNativeTurnAppended === turn) {
|
|
3007
3297
|
return;
|
|
3008
3298
|
}
|
|
3009
|
-
params.conversationMessages.push(
|
|
3299
|
+
params.conversationMessages.push(BuildAssistantToolCallTurn(turn));
|
|
3010
3300
|
this._lastNativeTurnAppended = turn;
|
|
3011
3301
|
}
|
|
3012
3302
|
/**
|
|
@@ -3051,13 +3341,13 @@ export class BaseAgent {
|
|
|
3051
3341
|
}
|
|
3052
3342
|
results.push({
|
|
3053
3343
|
toolCallId: action.toolCallId,
|
|
3054
|
-
toolName:
|
|
3344
|
+
toolName: SanitizeToolName(summary.actionName),
|
|
3055
3345
|
content: this.formatActionResultsAsMarkdown([summary]),
|
|
3056
3346
|
isError: !summary.success
|
|
3057
3347
|
});
|
|
3058
3348
|
}
|
|
3059
3349
|
if (results.length > 0) {
|
|
3060
|
-
params.conversationMessages.push(
|
|
3350
|
+
params.conversationMessages.push(BuildToolResultTurn(results, metadata));
|
|
3061
3351
|
}
|
|
3062
3352
|
if (orphans.length > 0) {
|
|
3063
3353
|
params.conversationMessages.push({ role: 'user', content: `Action results:\n${this.formatActionResultsAsMarkdown(orphans)}`, metadata });
|
|
@@ -3117,7 +3407,7 @@ export class BaseAgent {
|
|
|
3117
3407
|
if (unanswered.length === 0) {
|
|
3118
3408
|
return;
|
|
3119
3409
|
}
|
|
3120
|
-
params.conversationMessages.push(
|
|
3410
|
+
params.conversationMessages.push(BuildToolResultTurn(unanswered.map((call) => ({
|
|
3121
3411
|
toolCallId: call.id,
|
|
3122
3412
|
toolName: call.name,
|
|
3123
3413
|
content: 'Not executed — the agent did not run this call on this turn. See the message that follows.',
|
|
@@ -3129,7 +3419,7 @@ export class BaseAgent {
|
|
|
3129
3419
|
* answers, which every provider rejects. Expiry and recovery stub its blocks instead.
|
|
3130
3420
|
*/
|
|
3131
3421
|
stubToolTurn(message, note) {
|
|
3132
|
-
return
|
|
3422
|
+
return CompactToolResultContent(message, () => note);
|
|
3133
3423
|
}
|
|
3134
3424
|
applyNativeTools(promptParams, params) {
|
|
3135
3425
|
this._nativeToolBindings = undefined;
|
|
@@ -3147,18 +3437,18 @@ export class BaseAgent {
|
|
|
3147
3437
|
return;
|
|
3148
3438
|
}
|
|
3149
3439
|
// ...and a per-agent-ACTION gate: rows that opt out are removed before the tool set is built.
|
|
3150
|
-
const actions =
|
|
3440
|
+
const actions = FilterDeclarableActions(this.getEffectiveActionsForValidation(params.agent.ID), AIEngine.Instance.AgentActions.filter((aa) => UUIDsEqual(aa.AgentID, params.agent.ID)));
|
|
3151
3441
|
const subAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
3152
3442
|
if (actions.length === 0 && subAgents.length === 0) {
|
|
3153
3443
|
return;
|
|
3154
3444
|
}
|
|
3155
3445
|
try {
|
|
3156
|
-
const actionSet =
|
|
3446
|
+
const actionSet = BuildActionToolSet(actions, new Map(actions.map((a) => [a.ID, a.Params.Items])));
|
|
3157
3447
|
// Under implicit control flow the agent cannot know which model will answer, so it declares the full
|
|
3158
3448
|
// set — Actions plus the control-flow tools (one per sub-agent, payload_change_request,
|
|
3159
3449
|
// ask_user) — and NAMES the control ones. The runner keeps them only when the selected
|
|
3160
3450
|
// model's LLM.NativeControlFlow resolves to 'implicit'; a hybrid model never sees them.
|
|
3161
|
-
const toolSet =
|
|
3451
|
+
const toolSet = BuildNativeToolSet(actionSet, subAgents);
|
|
3162
3452
|
promptParams.tools = toolSet.tools;
|
|
3163
3453
|
promptParams.controlFlowToolNames = toolSet.controlToolNames;
|
|
3164
3454
|
promptParams.toolChoice = this.resolveToolChoiceForTurn(params);
|
|
@@ -3239,6 +3529,11 @@ export class BaseAgent {
|
|
|
3239
3529
|
// Attribute the resulting AIPromptRun to this agent. Agents share agent-type-level system
|
|
3240
3530
|
// prompts, so without this a parent's inference and its sub-agent's are indistinguishable.
|
|
3241
3531
|
promptParams.agentId = params.agent.ID;
|
|
3532
|
+
promptParams.UserID = ResolvePromptRunUserID({
|
|
3533
|
+
UserID: params.userId,
|
|
3534
|
+
AgentRun: this._agentRun,
|
|
3535
|
+
ContextUser: params.contextUser,
|
|
3536
|
+
}) ?? undefined;
|
|
3242
3537
|
// Handle case where systemPrompt is optional (e.g., Flow Agent Type)
|
|
3243
3538
|
if (systemPrompt) {
|
|
3244
3539
|
promptParams.prompt = systemPrompt;
|
|
@@ -3294,9 +3589,9 @@ export class BaseAgent {
|
|
|
3294
3589
|
const agentTypePromptParams = promptParams.data.__agentTypePromptParams;
|
|
3295
3590
|
const scratchpadEnabled = agentTypePromptParams?.includeScratchpadDocs !== false;
|
|
3296
3591
|
if (scratchpadEnabled && this._scratchpadManager) {
|
|
3297
|
-
promptParams.data[
|
|
3298
|
-
promptParams.data[
|
|
3299
|
-
promptParams.data[
|
|
3592
|
+
promptParams.data[SCRATCHPAD_NOTES_PLACEHOLDER] = this._scratchpadManager.GetNotes() || '_(no notes yet)_';
|
|
3593
|
+
promptParams.data[SCRATCHPAD_TASKS_PLACEHOLDER] = this._scratchpadManager.ToPromptString();
|
|
3594
|
+
promptParams.data[SCRATCHPAD_TASK_SUMMARY_PLACEHOLDER] = this._scratchpadManager.GetTaskSummary();
|
|
3300
3595
|
}
|
|
3301
3596
|
// Inject artifact tools template variables if enabled and artifacts are present.
|
|
3302
3597
|
// Note: prior tool results are NO LONGER injected via a per-turn template var.
|
|
@@ -3432,8 +3727,334 @@ export class BaseAgent {
|
|
|
3432
3727
|
params.data?.SecondaryScopes,
|
|
3433
3728
|
}, promptParams);
|
|
3434
3729
|
}
|
|
3730
|
+
// Prompt-cache layout. The per-iteration state (and, when the child prompt is volatile, the
|
|
3731
|
+
// specialization) never lives in the system prompt; it rides as the FINAL message of THIS request.
|
|
3732
|
+
// In append-only mode (OpenAI prompt caching), prior runtime-state fragments are retained in history
|
|
3733
|
+
// so each turn extends the exact byte prefix of the previous request, maintaining ~93% cache hits.
|
|
3734
|
+
// In replace-in-place mode (Gemini/Cerebras), only the latest fragment is attached, keeping history lean.
|
|
3735
|
+
const volatileStateMessage = await this.buildVolatileStateMessage(params, promptParams, payload, childPrompt, agentType, systemPrompt);
|
|
3736
|
+
if (volatileStateMessage) {
|
|
3737
|
+
const isAppendOnly = this.shouldUseAppendOnlyTrailingState(promptParams);
|
|
3738
|
+
this.restoreTurn1VolatileStateIfNeeded(params, isAppendOnly);
|
|
3739
|
+
promptParams.conversationMessages = this.assembleOutgoingMessages(params.conversationMessages, volatileStateMessage, isAppendOnly);
|
|
3740
|
+
if (isAppendOnly) {
|
|
3741
|
+
params.conversationMessages.push(volatileStateMessage);
|
|
3742
|
+
}
|
|
3743
|
+
this._lastVolatileStateMessage = volatileStateMessage;
|
|
3744
|
+
}
|
|
3435
3745
|
return promptParams;
|
|
3436
3746
|
}
|
|
3747
|
+
/**
|
|
3748
|
+
* In append-only mode, restores turn 1's volatile state fragment if mode resolution
|
|
3749
|
+
* was deferred until after turn 1 (e.g. dynamic model selection).
|
|
3750
|
+
*
|
|
3751
|
+
* Restores the fragment at the exact message boundary where turn 1 executed, ensuring
|
|
3752
|
+
* earlier turns in multi-turn conversations are not corrupted.
|
|
3753
|
+
*
|
|
3754
|
+
* This is only correct for a replace → append-only flip between turn 1 and turn 2, when
|
|
3755
|
+
* `_lastVolatileStateMessage` still holds turn 1's fragment. `shouldUseAppendOnlyTrailingState`
|
|
3756
|
+
* freezes the mode at the first model selection precisely so that no later flip can occur.
|
|
3757
|
+
*/
|
|
3758
|
+
restoreTurn1VolatileStateIfNeeded(params, isAppendOnly) {
|
|
3759
|
+
if (this._turn1InsertionIndex < 0) {
|
|
3760
|
+
// Record the message boundary at Turn 1 before any loop messages are added
|
|
3761
|
+
this._turn1InsertionIndex = params.conversationMessages.length;
|
|
3762
|
+
}
|
|
3763
|
+
else if (isAppendOnly && this._lastVolatileStateMessage && !this.runHasVolatileStateMessage(params)) {
|
|
3764
|
+
// If append-only was resolved after turn 1 (via _lastModelSelectionInfo),
|
|
3765
|
+
// restore turn 1's fragment at the exact position where turn 1 executed it (the turn 1 boundary)
|
|
3766
|
+
// to ensure exact prefix match without corrupting pre-existing conversation history.
|
|
3767
|
+
const insertIdx = Math.min(this._turn1InsertionIndex, params.conversationMessages.length);
|
|
3768
|
+
params.conversationMessages.splice(insertIdx, 0, this._lastVolatileStateMessage);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
/**
|
|
3772
|
+
* Whether THIS run has already placed a volatile-state fragment in the history, i.e. at or after
|
|
3773
|
+
* the turn-1 boundary. Fragments before the boundary belong to an earlier run whose history the
|
|
3774
|
+
* caller reused; they must not suppress this run's turn-1 restore.
|
|
3775
|
+
*/
|
|
3776
|
+
runHasVolatileStateMessage(params) {
|
|
3777
|
+
const start = Math.max(0, this._turn1InsertionIndex);
|
|
3778
|
+
return params.conversationMessages.slice(start).some(m => m.metadata?.volatileState === true);
|
|
3779
|
+
}
|
|
3780
|
+
/**
|
|
3781
|
+
* The message array sent for ONE request under `'trailingMessage'` placement: a copy of the history
|
|
3782
|
+
* with every non-system message's fragment tag literals escaped, then the real fragment last.
|
|
3783
|
+
*
|
|
3784
|
+
* Escaping at send time (rather than where text enters the history) covers every source at once —
|
|
3785
|
+
* user turns, action results, sub-agent results, skill activations — without rewriting stored data,
|
|
3786
|
+
* and it is deterministic, so the cached prefix stays byte-stable across iterations. System messages
|
|
3787
|
+
* and framework-authored volatile state messages are left alone.
|
|
3788
|
+
*/
|
|
3789
|
+
assembleOutgoingMessages(history, fragment, isAppendOnly = false) {
|
|
3790
|
+
const source = isAppendOnly ? history : history.filter(m => !m.metadata?.volatileState);
|
|
3791
|
+
const sanitized = source.map(m => (m.role === 'system' || m.metadata?.volatileState ? m : EscapeRuntimeStateTagsInMessage(m)));
|
|
3792
|
+
return [...sanitized, fragment];
|
|
3793
|
+
}
|
|
3794
|
+
/**
|
|
3795
|
+
* Determines whether the current prompt execution should use append-only trailing state retention.
|
|
3796
|
+
*
|
|
3797
|
+
* Why: OpenAI and xAI prompt caching operate on an exact byte prefix match from token 0. Replacing the
|
|
3798
|
+
* trailing runtime-state fragment turn-over-turn breaks the byte prefix after the system prompt,
|
|
3799
|
+
* dropping cache hit rates significantly. In append-only mode, prior runtime state messages are
|
|
3800
|
+
* retained in the message history so each turn is an exact prefix extension of the prior turn,
|
|
3801
|
+
* achieving ~93% cache hit rate. Providers with block-level or sliding caching (Gemini, Cerebras)
|
|
3802
|
+
* use replace-in-place to keep context compact.
|
|
3803
|
+
*
|
|
3804
|
+
* Which providers are which is METADATA, not code: the `PrefixPromptCache` flag in the model
|
|
3805
|
+
* catalog's `ModelConfiguration` cascade (Model Types < Models < Vendors' `Configuration.ModelDefaults`
|
|
3806
|
+
* < Model Vendors), read through {@link resolvePrefixPromptCache}. `true` means append-only;
|
|
3807
|
+
* anything else means replace.
|
|
3808
|
+
*
|
|
3809
|
+
* Decided ONCE per run. An explicit `trailingStateMode` or a runtime model override answers
|
|
3810
|
+
* immediately. Otherwise the answer is frozen at the first model selection and reused for every
|
|
3811
|
+
* later turn, so a failover to another vendor cannot flip the layout mid-run — a flip after turn 2
|
|
3812
|
+
* would leave stale fragments in the history or, worse, restore the wrong turn's fragment. On turn 1,
|
|
3813
|
+
* before any selection is known, the answer is replace-in-place: turn 1's fragment is kept and, if
|
|
3814
|
+
* turn 2 resolves to append-only, spliced back at the turn-1 boundary, which reproduces exactly the
|
|
3815
|
+
* bytes an append-only turn 1 would have sent. Nothing is lost by deferring, so the prompt's bound
|
|
3816
|
+
* models are deliberately NOT consulted — prompts commonly bind several vendors for failover, and
|
|
3817
|
+
* guessing from them mis-pins runs that end up selecting another vendor.
|
|
3818
|
+
*/
|
|
3819
|
+
shouldUseAppendOnlyTrailingState(promptParams) {
|
|
3820
|
+
const data = promptParams.data ?? {};
|
|
3821
|
+
// An explicit trailingStateMode wins; 'auto' (the default) or an absent key falls through to
|
|
3822
|
+
// vendor/model detection below. See TrailingStateMode for when to force either mode.
|
|
3823
|
+
const agentTypePromptParams = data.__agentTypePromptParams;
|
|
3824
|
+
if (agentTypePromptParams?.trailingStateMode === 'appendOnly') {
|
|
3825
|
+
return true;
|
|
3826
|
+
}
|
|
3827
|
+
if (agentTypePromptParams?.trailingStateMode === 'replace') {
|
|
3828
|
+
return false;
|
|
3829
|
+
}
|
|
3830
|
+
// A model override pins the serving path for the whole run, so it answers now. A vendor-only
|
|
3831
|
+
// override cannot: the strategy lives on the model-vendor row, which needs the model too, so
|
|
3832
|
+
// that case is decided at the first selection like any other run.
|
|
3833
|
+
if (promptParams.override?.modelId) {
|
|
3834
|
+
const model = AIEngine.Instance?.ModelsByID?.get(NormalizeUUID(promptParams.override.modelId));
|
|
3835
|
+
const vendor = promptParams.override.vendorId ? AIEngine.Instance?.VendorsByID?.get(NormalizeUUID(promptParams.override.vendorId)) : undefined;
|
|
3836
|
+
return this.resolvePrefixPromptCache(model, vendor);
|
|
3837
|
+
}
|
|
3838
|
+
if (this._resolvedTrailingStateMode !== undefined) {
|
|
3839
|
+
return this._resolvedTrailingStateMode;
|
|
3840
|
+
}
|
|
3841
|
+
if (this._lastModelSelectionInfo) {
|
|
3842
|
+
const model = this._lastModelSelectionInfo.ModelSelected;
|
|
3843
|
+
const vendor = this._lastModelSelectionInfo.vendorSelected;
|
|
3844
|
+
this._resolvedTrailingStateMode = this.resolvePrefixPromptCache(model, vendor);
|
|
3845
|
+
return this._resolvedTrailingStateMode;
|
|
3846
|
+
}
|
|
3847
|
+
// Turn 1, nothing known yet: replace-in-place, resolved for good on turn 2 (see above).
|
|
3848
|
+
return false;
|
|
3849
|
+
}
|
|
3850
|
+
/**
|
|
3851
|
+
* Whether a model, as served by a vendor, sits behind a byte-prefix prompt cache
|
|
3852
|
+
* (`LLM.PrefixPromptCache`), read from the model catalog's `ModelConfiguration` cascade —
|
|
3853
|
+
* `AIModelType < AIModel < AIVendor.Configuration.ModelDefaults < AIModelVendor` — via
|
|
3854
|
+
* `AIEngine.GetEffectiveModelConfiguration`. The most specific layer is the INFERENCE-PROVIDER
|
|
3855
|
+
* model-vendor row for `vendor`, whose vendor row supplies the host-wide default that beats the
|
|
3856
|
+
* model's own bag; when the vendor is unknown, or has no inference row for this model, the model
|
|
3857
|
+
* and type layers still answer. False when no layer declares it, which callers
|
|
3858
|
+
* treat as a block cache (replace-in-place).
|
|
3859
|
+
*
|
|
3860
|
+
* Extension point: a subclass with out-of-catalog knowledge (an OpenAI-compatible gateway whose
|
|
3861
|
+
* rows carry no flag, say) can override this rather than the mode decision above.
|
|
3862
|
+
*/
|
|
3863
|
+
resolvePrefixPromptCache(model, vendor) {
|
|
3864
|
+
if (!model) {
|
|
3865
|
+
return false;
|
|
3866
|
+
}
|
|
3867
|
+
const engine = AIEngine.Instance;
|
|
3868
|
+
const modelVendor = vendor
|
|
3869
|
+
? (engine.ModelVendorsByModelID?.get(NormalizeUUID(model.ID)) ?? []).find(mv => UUIDsEqual(mv.VendorID, vendor.ID) && engine.IsInferenceProvider(mv))
|
|
3870
|
+
: undefined;
|
|
3871
|
+
return IsPrefixPromptCache(engine.GetEffectiveModelConfiguration(model.ID, modelVendor?.ID));
|
|
3872
|
+
}
|
|
3873
|
+
/**
|
|
3874
|
+
* Builds the framework-authored `user` message that carries the loop agent's volatile state as the
|
|
3875
|
+
* final message of the request; returns null only when every block is turned off, or when the
|
|
3876
|
+
* system prompt template in this database has not yet synced and still embeds the state itself
|
|
3877
|
+
* (see the guard below). The blocks mirror the sections the template used to render (see
|
|
3878
|
+
* {@link RuntimeStateFragmentBuilder}), and each honors the same include flag the template did.
|
|
3879
|
+
*
|
|
3880
|
+
* Why this exists: provider prompt caching is a prefix match over tools → system → messages, so
|
|
3881
|
+
* state that changes every iteration INSIDE the system prompt invalidates the entire history each
|
|
3882
|
+
* call. Measured on Sage: 36% → 83% cached on Gemini 2.5 Flash, 12% → 96% on Claude Opus 5 (with the
|
|
3883
|
+
* Anthropic adapter placing its breakpoint before this message), output quality unchanged.
|
|
3884
|
+
*
|
|
3885
|
+
* The message carries STATE only — never rules. It is marked `metadata.volatileState` so adapters
|
|
3886
|
+
* can recognize it without depending on this package's tag names.
|
|
3887
|
+
*/
|
|
3888
|
+
async buildVolatileStateMessage(params, promptParams, payload, childPrompt, agentType, systemPrompt) {
|
|
3889
|
+
const data = promptParams.data ?? {};
|
|
3890
|
+
const agentTypePromptParams = data.__agentTypePromptParams;
|
|
3891
|
+
// Delivery gate: the fragment is emitted only for a system prompt whose template points the model
|
|
3892
|
+
// at it. See resolveRuntimeStateDelivery for the two ways a template can fail that test.
|
|
3893
|
+
const effectiveSystemPrompt = systemPrompt ?? promptParams.prompt;
|
|
3894
|
+
const delivery = await this.resolveRuntimeStateDelivery(effectiveSystemPrompt, params.contextUser);
|
|
3895
|
+
if (delivery === 'embedded') {
|
|
3896
|
+
this.logStatus('⚠️ System prompt template still contains volatile blocks (database template unsynced); skipping trailing runtime-state fragment to avoid duplicate state.', true, params);
|
|
3897
|
+
return null;
|
|
3898
|
+
}
|
|
3899
|
+
if (delivery === 'unsupported') {
|
|
3900
|
+
this.logStatus(`System prompt template has no <${RUNTIME_STATE_TAG}> pointer (not a Loop agent system prompt); skipping trailing runtime-state fragment.`, true, params);
|
|
3901
|
+
return null;
|
|
3902
|
+
}
|
|
3903
|
+
const includeDateTime = agentTypePromptParams?.includeDateTimeInPrompt !== false;
|
|
3904
|
+
const includeScratchpad = agentTypePromptParams?.includeScratchpadDocs !== false;
|
|
3905
|
+
const includePayload = agentTypePromptParams?.includePayloadInPrompt !== false;
|
|
3906
|
+
const specialization = await this.resolveRelocatedSpecialization(promptParams, childPrompt, agentType, params.contextUser);
|
|
3907
|
+
const fragment = new RuntimeStateFragmentBuilder().Build({
|
|
3908
|
+
DateTime: includeDateTime ? await this.resolveFragmentDateTime(promptParams) : null,
|
|
3909
|
+
Scratchpad: includeScratchpad ? this.readScratchpadFromTemplateData(data) : null,
|
|
3910
|
+
// Same value the agent type injects for the template (`payload || {}`), so both placements agree.
|
|
3911
|
+
Payload: includePayload ? { Value: payload || {} } : null,
|
|
3912
|
+
Specialization: specialization,
|
|
3913
|
+
});
|
|
3914
|
+
if (!fragment) {
|
|
3915
|
+
return null;
|
|
3916
|
+
}
|
|
3917
|
+
this.logStatus(`📦 Volatile state → trailing message (${fragment.length} chars${specialization ? ', specialization relocated' : ''})`, true, params);
|
|
3918
|
+
return { role: 'user', content: fragment, metadata: { volatileState: true, turnAdded: this._promptTurnCount } };
|
|
3919
|
+
}
|
|
3920
|
+
/**
|
|
3921
|
+
* Decides whether this run's specialization (child prompt) rides in the trailing message, and if so
|
|
3922
|
+
* pre-renders it and flags the template to render a stub in its place. Decided from the child
|
|
3923
|
+
* template's UNRENDERED text via {@link ResolveSpecializationPlacement}, so the answer is the same on
|
|
3924
|
+
* every iteration and the layout never flips mid-run. Returns the rendered specialization, or null
|
|
3925
|
+
* when it stays in the system prompt.
|
|
3926
|
+
*/
|
|
3927
|
+
async resolveRelocatedSpecialization(promptParams, childPrompt, agentType, contextUser) {
|
|
3928
|
+
const placeholder = agentType.AgentPromptPlaceholder;
|
|
3929
|
+
if (!childPrompt || !placeholder || !promptParams.childPrompts || promptParams.childPrompts.length === 0) {
|
|
3930
|
+
return null;
|
|
3931
|
+
}
|
|
3932
|
+
const templateText = await this.loadChildPromptTemplateText(childPrompt, contextUser);
|
|
3933
|
+
const agentTypePromptParams = promptParams.data?.__agentTypePromptParams;
|
|
3934
|
+
if (ResolveSpecializationPlacement(agentTypePromptParams, templateText) !== 'trailingMessage') {
|
|
3935
|
+
return null;
|
|
3936
|
+
}
|
|
3937
|
+
const rendered = await this._promptRunner.RenderChildPromptTemplates(promptParams.childPrompts, promptParams);
|
|
3938
|
+
const text = rendered.renderedTemplates[placeholder];
|
|
3939
|
+
if (!text || text.trim().length === 0) {
|
|
3940
|
+
return null;
|
|
3941
|
+
}
|
|
3942
|
+
// Cache pre-rendered child templates so AIPromptRunner.ExecutePrompt does not re-render them
|
|
3943
|
+
promptParams.PreRenderedChildTemplates = rendered.renderedTemplates;
|
|
3944
|
+
// The same data object the parent template renders against — this switches the `## Specialization`
|
|
3945
|
+
// block to its stub and extends the Runtime State pointer.
|
|
3946
|
+
if (promptParams.data) {
|
|
3947
|
+
promptParams.data._SPECIALIZATION_RELOCATED = true;
|
|
3948
|
+
}
|
|
3949
|
+
return text;
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Decides, from the system prompt's UNRENDERED template text, whether the trailing runtime-state
|
|
3953
|
+
* fragment belongs on this request:
|
|
3954
|
+
*
|
|
3955
|
+
* - `'trailing'` — the template carries the `<mj-runtime-state>` pointer, so the model is told where
|
|
3956
|
+
* the state lives. The Loop agent system prompt.
|
|
3957
|
+
* - `'embedded'` — the template still renders the state blocks itself (an environment whose
|
|
3958
|
+
* TemplateContent has not synced the new Loop template, or the Flow template, which embeds the
|
|
3959
|
+
* payload). Emitting the fragment would deliver the same state twice.
|
|
3960
|
+
* - `'unsupported'` — the template has neither. The Harness system prompt, or a custom prompt run
|
|
3961
|
+
* without the Loop system prompt. The model would receive an unexplained block.
|
|
3962
|
+
* - `'unknown'` — no template text to inspect (no TemplateID, or the lookup failed). The caller
|
|
3963
|
+
* fails OPEN here: a Loop agent losing its payload from the model's view is far worse than a
|
|
3964
|
+
* non-Loop agent receiving an unexplained fragment, and in practice every agent type's system
|
|
3965
|
+
* prompt has a template, so this arises only from a lookup failure.
|
|
3966
|
+
*/
|
|
3967
|
+
async resolveRuntimeStateDelivery(systemPrompt, contextUser) {
|
|
3968
|
+
const templateText = await this.loadPromptTemplateText(systemPrompt, contextUser);
|
|
3969
|
+
if (templateText === null) {
|
|
3970
|
+
return 'unknown';
|
|
3971
|
+
}
|
|
3972
|
+
if (this.templateTextEmbedsVolatileState(templateText)) {
|
|
3973
|
+
return 'embedded';
|
|
3974
|
+
}
|
|
3975
|
+
return templateText.includes(`<${RUNTIME_STATE_TAG}>`) ? 'trailing' : 'unsupported';
|
|
3976
|
+
}
|
|
3977
|
+
/**
|
|
3978
|
+
* The strings whose presence in a system prompt's unrendered template text means the template
|
|
3979
|
+
* still renders the volatile state itself, so the trailing fragment must be suppressed. Defaults
|
|
3980
|
+
* to {@link VOLATILE_TEMPLATE_MARKERS}: the three block headings plus the date and payload
|
|
3981
|
+
* placeholders. Extension point — an agent type whose template lays the state out under other
|
|
3982
|
+
* headings overrides this to return its own markers.
|
|
3983
|
+
*/
|
|
3984
|
+
get volatileTemplateMarkers() {
|
|
3985
|
+
return VOLATILE_TEMPLATE_MARKERS;
|
|
3986
|
+
}
|
|
3987
|
+
/**
|
|
3988
|
+
* True when unrendered template text contains any of {@link volatileTemplateMarkers} — the legacy
|
|
3989
|
+
* Loop layout, or any template that embeds the payload.
|
|
3990
|
+
*/
|
|
3991
|
+
templateTextEmbedsVolatileState(templateText) {
|
|
3992
|
+
return this.volatileTemplateMarkers.some(marker => templateText.includes(marker));
|
|
3993
|
+
}
|
|
3994
|
+
/**
|
|
3995
|
+
* Raw template text (placeholders intact) for an AI prompt from the cached template engine.
|
|
3996
|
+
* Null when the prompt has no template or the lookup fails.
|
|
3997
|
+
*/
|
|
3998
|
+
async loadPromptTemplateText(prompt, contextUser) {
|
|
3999
|
+
if (!prompt?.TemplateID) {
|
|
4000
|
+
return null;
|
|
4001
|
+
}
|
|
4002
|
+
try {
|
|
4003
|
+
await TemplateEngineServer.Instance.Config(false, contextUser);
|
|
4004
|
+
const template = TemplateEngineServer.Instance.Templates?.find(t => UUIDsEqual(t.ID, prompt.TemplateID));
|
|
4005
|
+
return template?.GetHighestPriorityContent()?.TemplateText ?? null;
|
|
4006
|
+
}
|
|
4007
|
+
catch (e) {
|
|
4008
|
+
this.logError(e instanceof Error ? e : String(e), { category: 'RuntimeStateFragment', severity: 'warning' });
|
|
4009
|
+
return null;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* The child prompt's raw template text (placeholders intact), from the cached template engine.
|
|
4014
|
+
* Null when the prompt has no template or the lookup fails — which fails CLOSED: with no text to
|
|
4015
|
+
* inspect, {@link ResolveSpecializationPlacement} keeps the specialization in the system prompt.
|
|
4016
|
+
*/
|
|
4017
|
+
async loadChildPromptTemplateText(childPrompt, contextUser) {
|
|
4018
|
+
return this.loadPromptTemplateText(childPrompt, contextUser);
|
|
4019
|
+
}
|
|
4020
|
+
/**
|
|
4021
|
+
* The date/time strings exactly as the system placeholders would render them into the template.
|
|
4022
|
+
* Resolves only the three temporal placeholders (by name, through the same registry the template
|
|
4023
|
+
* uses, so a registered override applies here too) rather than every system placeholder — this runs
|
|
4024
|
+
* on every loop iteration.
|
|
4025
|
+
*/
|
|
4026
|
+
async resolveFragmentDateTime(promptParams) {
|
|
4027
|
+
const resolve = async (name) => {
|
|
4028
|
+
const placeholder = SystemPlaceholderManager.getPlaceholders().find(p => p.name === name);
|
|
4029
|
+
if (!placeholder) {
|
|
4030
|
+
return null;
|
|
4031
|
+
}
|
|
4032
|
+
try {
|
|
4033
|
+
const value = await placeholder.getValue(promptParams);
|
|
4034
|
+
return value == null ? null : String(value);
|
|
4035
|
+
}
|
|
4036
|
+
catch (e) {
|
|
4037
|
+
this.logError(e instanceof Error ? e : String(e), { category: 'RuntimeStateFragment', severity: 'warning', metadata: { placeholder: name } });
|
|
4038
|
+
return null;
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
4041
|
+
const [date, dayOfWeek, time] = await Promise.all([resolve(CURRENT_DATE_PLACEHOLDER), resolve(CURRENT_DAY_OF_WEEK_PLACEHOLDER), resolve(CURRENT_TIME_PLACEHOLDER)]);
|
|
4042
|
+
if (!date || !dayOfWeek || !time) {
|
|
4043
|
+
return null;
|
|
4044
|
+
}
|
|
4045
|
+
return { Date: date, DayOfWeek: dayOfWeek, Time: time };
|
|
4046
|
+
}
|
|
4047
|
+
/**
|
|
4048
|
+
* The scratchpad strings already placed in the template data by the prep step (so the fragment shows
|
|
4049
|
+
* exactly what the template would have). Null when the scratchpad is disabled or absent.
|
|
4050
|
+
*/
|
|
4051
|
+
readScratchpadFromTemplateData(data) {
|
|
4052
|
+
const notes = data[SCRATCHPAD_NOTES_PLACEHOLDER], tasks = data[SCRATCHPAD_TASKS_PLACEHOLDER], summary = data[SCRATCHPAD_TASK_SUMMARY_PLACEHOLDER];
|
|
4053
|
+
if (typeof notes !== 'string' || typeof tasks !== 'string' || typeof summary !== 'string') {
|
|
4054
|
+
return null;
|
|
4055
|
+
}
|
|
4056
|
+
return { Notes: notes, Tasks: tasks, TaskSummary: summary };
|
|
4057
|
+
}
|
|
3437
4058
|
/**
|
|
3438
4059
|
* Executes the configured prompt. Always uses the attemptJSONRepair option to try to fix LLM
|
|
3439
4060
|
* JSON syntax issues if they arise.
|
|
@@ -4211,7 +4832,7 @@ export class BaseAgent {
|
|
|
4211
4832
|
}
|
|
4212
4833
|
// Check absolute maximum iterations (safety net to prevent infinite loops)
|
|
4213
4834
|
const absoluteMaxIterations = params.absoluteMaxIterations ?? BaseAgent.DEFAULT_ABSOLUTE_MAX_ITERATIONS;
|
|
4214
|
-
if (agentRun.TotalPromptIterations && agentRun.TotalPromptIterations >= absoluteMaxIterations) {
|
|
4835
|
+
if (agentRun.TotalPromptIterations != null && agentRun.TotalPromptIterations >= absoluteMaxIterations) {
|
|
4215
4836
|
return {
|
|
4216
4837
|
exceeded: true,
|
|
4217
4838
|
type: 'iterations',
|
|
@@ -4221,7 +4842,7 @@ export class BaseAgent {
|
|
|
4221
4842
|
};
|
|
4222
4843
|
}
|
|
4223
4844
|
// Check cost limit
|
|
4224
|
-
if (agent.MaxCostPerRun && agentRun.TotalCost) {
|
|
4845
|
+
if (agent.MaxCostPerRun != null && agentRun.TotalCost != null) {
|
|
4225
4846
|
if (agentRun.TotalCost >= agent.MaxCostPerRun) {
|
|
4226
4847
|
return {
|
|
4227
4848
|
exceeded: true,
|
|
@@ -4233,7 +4854,7 @@ export class BaseAgent {
|
|
|
4233
4854
|
}
|
|
4234
4855
|
}
|
|
4235
4856
|
// Check token limit
|
|
4236
|
-
if (agent.MaxTokensPerRun && agentRun.TotalTokensUsed) {
|
|
4857
|
+
if (agent.MaxTokensPerRun != null && agentRun.TotalTokensUsed != null) {
|
|
4237
4858
|
if (agentRun.TotalTokensUsed >= agent.MaxTokensPerRun) {
|
|
4238
4859
|
return {
|
|
4239
4860
|
exceeded: true,
|
|
@@ -4245,7 +4866,7 @@ export class BaseAgent {
|
|
|
4245
4866
|
}
|
|
4246
4867
|
}
|
|
4247
4868
|
// Check iteration limit
|
|
4248
|
-
if (agent.MaxIterationsPerRun && agentRun.TotalPromptIterations) {
|
|
4869
|
+
if (agent.MaxIterationsPerRun != null && agentRun.TotalPromptIterations != null) {
|
|
4249
4870
|
if (agentRun.TotalPromptIterations >= agent.MaxIterationsPerRun) {
|
|
4250
4871
|
return {
|
|
4251
4872
|
exceeded: true,
|
|
@@ -4257,7 +4878,7 @@ export class BaseAgent {
|
|
|
4257
4878
|
}
|
|
4258
4879
|
}
|
|
4259
4880
|
// Check time limit
|
|
4260
|
-
if (agent.MaxTimePerRun && agentRun.StartedAt) {
|
|
4881
|
+
if (agent.MaxTimePerRun != null && agentRun.StartedAt) {
|
|
4261
4882
|
const elapsedSeconds = Math.floor((Date.now() - new Date(agentRun.StartedAt).getTime()) / 1000);
|
|
4262
4883
|
if (elapsedSeconds >= agent.MaxTimePerRun) {
|
|
4263
4884
|
return {
|
|
@@ -4625,6 +5246,60 @@ export class BaseAgent {
|
|
|
4625
5246
|
serializePayloadAtEnd(payload) {
|
|
4626
5247
|
return payload ? JSON.stringify(payload) : null;
|
|
4627
5248
|
}
|
|
5249
|
+
/**
|
|
5250
|
+
* Recovery Strategy 0: drop stale runtime-state fragments.
|
|
5251
|
+
*
|
|
5252
|
+
* Under append-only trailing-state retention (prefix-cache providers: OpenAI, xAI) every
|
|
5253
|
+
* iteration leaves its `<mj-runtime-state>` message in the history so the next request is an
|
|
5254
|
+
* exact prefix extension of the last. Those copies are cached tokens on the wire, but they are
|
|
5255
|
+
* context all the same, and they carry nothing the model needs: the CURRENT state always rides
|
|
5256
|
+
* as the fresh fragment appended to the outgoing request. So when the context overflows they
|
|
5257
|
+
* are the first thing to go, oldest first, all but the most recent. Keeping the newest one
|
|
5258
|
+
* matters for two reasons: it is the history's only fragment after this pass, so
|
|
5259
|
+
* {@link restoreTurn1VolatileStateIfNeeded} does not splice a turn-1 copy back in, and it
|
|
5260
|
+
* keeps the prefix intact from that point forward. The cost is one cache miss on the next call;
|
|
5261
|
+
* the alternative was a failed run.
|
|
5262
|
+
*
|
|
5263
|
+
* A no-op under replace-in-place retention, where the history never holds a fragment.
|
|
5264
|
+
*
|
|
5265
|
+
* @param params - Agent execution parameters
|
|
5266
|
+
* @param tokensToSave - Target number of tokens to free
|
|
5267
|
+
* @param currentStepCount - Current turn number, for the lifecycle event
|
|
5268
|
+
* @returns Result with tokens saved and strategy description
|
|
5269
|
+
* @protected
|
|
5270
|
+
*/
|
|
5271
|
+
recoveryStrategy_DropStaleVolatileState(params, tokensToSave, currentStepCount) {
|
|
5272
|
+
const fragmentIndices = params.conversationMessages
|
|
5273
|
+
.map((msg, index) => (msg.metadata?.volatileState === true ? index : -1))
|
|
5274
|
+
.filter(index => index >= 0);
|
|
5275
|
+
// All but the most recent, oldest first.
|
|
5276
|
+
const stale = fragmentIndices.slice(0, -1);
|
|
5277
|
+
if (stale.length === 0) {
|
|
5278
|
+
return { tokensSaved: 0, strategyName: 'No stale runtime-state fragments to drop' };
|
|
5279
|
+
}
|
|
5280
|
+
let tokensSaved = 0;
|
|
5281
|
+
const removedIndices = [];
|
|
5282
|
+
for (const index of stale) {
|
|
5283
|
+
if (tokensSaved >= tokensToSave)
|
|
5284
|
+
break;
|
|
5285
|
+
removedIndices.push(index);
|
|
5286
|
+
tokensSaved += this.estimateTokens(params.conversationMessages[index].content);
|
|
5287
|
+
}
|
|
5288
|
+
// Remove in reverse order to keep the remaining indices valid.
|
|
5289
|
+
removedIndices.sort((a, b) => b - a).forEach(index => {
|
|
5290
|
+
const removed = params.conversationMessages.splice(index, 1)[0];
|
|
5291
|
+
this.emitMessageLifecycleEvent({
|
|
5292
|
+
type: 'message-removed',
|
|
5293
|
+
turn: currentStepCount,
|
|
5294
|
+
messageIndex: index,
|
|
5295
|
+
message: removed,
|
|
5296
|
+
reason: 'Context recovery - stale runtime-state fragment (append-only retention)',
|
|
5297
|
+
tokensSaved: this.estimateTokens(removed.content)
|
|
5298
|
+
});
|
|
5299
|
+
});
|
|
5300
|
+
this.logStatus(`Dropped ${removedIndices.length} stale runtime-state fragment(s) (${tokensSaved} tokens); ${fragmentIndices.length - removedIndices.length} retained`, true, params);
|
|
5301
|
+
return { tokensSaved, strategyName: `Dropped ${removedIndices.length} stale runtime-state fragment(s) retained for prefix caching` };
|
|
5302
|
+
}
|
|
4628
5303
|
/**
|
|
4629
5304
|
* Recovery Strategy 1: Remove oldest tool-result messages.
|
|
4630
5305
|
* Targets messages older than minAge turns for removal.
|
|
@@ -4730,7 +5405,7 @@ export class BaseAgent {
|
|
|
4730
5405
|
: JSON.stringify(originalMessage.content);
|
|
4731
5406
|
if (originalMessage.role === 'tool') {
|
|
4732
5407
|
// compact each tool_result block's text; the block structure is what the provider needs.
|
|
4733
|
-
const compacted =
|
|
5408
|
+
const compacted = CompactToolResultContent(originalMessage, (t) => (t.length > 500 ? `${t.slice(0, 500)}… [compacted from ${t.length} chars]` : t));
|
|
4734
5409
|
const saved = originalTokens - this.estimateTokens(compacted.content);
|
|
4735
5410
|
if (saved > 0) {
|
|
4736
5411
|
params.conversationMessages[candidate.index] = {
|
|
@@ -4845,10 +5520,13 @@ export class BaseAgent {
|
|
|
4845
5520
|
* @protected
|
|
4846
5521
|
*/
|
|
4847
5522
|
recoveryStrategy_TrimLastUserMessage(params, tokensToSave) {
|
|
4848
|
-
// Find the last user message (reverse search for compatibility)
|
|
5523
|
+
// Find the last user message (reverse search for compatibility). A retained runtime-state
|
|
5524
|
+
// fragment is user-role but is framework state, not the user's request: trimming it would
|
|
5525
|
+
// leave a damaged copy in the history and spare the message this strategy is meant to trim.
|
|
4849
5526
|
let lastUserMessageIndex = -1;
|
|
4850
5527
|
for (let i = params.conversationMessages.length - 1; i >= 0; i--) {
|
|
4851
|
-
|
|
5528
|
+
const candidate = params.conversationMessages[i];
|
|
5529
|
+
if (candidate.role === 'user' && candidate.metadata?.volatileState !== true) {
|
|
4852
5530
|
lastUserMessageIndex = i;
|
|
4853
5531
|
break;
|
|
4854
5532
|
}
|
|
@@ -4922,6 +5600,9 @@ export class BaseAgent {
|
|
|
4922
5600
|
const currentPromptTurn = this._promptTurnCount;
|
|
4923
5601
|
// Try multiple recovery strategies in order
|
|
4924
5602
|
const strategies = [
|
|
5603
|
+
// Retained runtime-state fragments (append-only mode) are pure cache filler: stale copies of
|
|
5604
|
+
// state the next request re-sends anyway. Freeing them costs one cache miss, never content.
|
|
5605
|
+
() => this.recoveryStrategy_DropStaleVolatileState(params, tokensToSave, currentPromptTurn),
|
|
4925
5606
|
() => this.recoveryStrategy_RemoveOldestToolResults(params, tokensToSave, currentPromptTurn, 5),
|
|
4926
5607
|
() => this.recoveryStrategy_CompactOldToolResults(params, tokensToSave, currentPromptTurn, 3),
|
|
4927
5608
|
() => this.recoveryStrategy_RemoveOldestToolResults(params, tokensToSave, currentPromptTurn, 2),
|
|
@@ -4988,7 +5669,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4988
5669
|
// if we need to retry make sure we add the retry message to the conversation messages
|
|
4989
5670
|
if (guardrailCheckedStep.step === 'Retry' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
4990
5671
|
// the payload-only turn is answered as a tool result for the payload_change_request call.
|
|
4991
|
-
params.conversationMessages.push(
|
|
5672
|
+
params.conversationMessages.push(BuildToolResultTurn([{
|
|
4992
5673
|
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
4993
5674
|
toolName: 'payload_change_request',
|
|
4994
5675
|
content: guardrailCheckedStep.retryInstructions || 'Payload change applied.',
|
|
@@ -5059,10 +5740,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5059
5740
|
* its direct predecessor's results, so context never compounds.
|
|
5060
5741
|
*
|
|
5061
5742
|
* Gated on conversationId + root depth — programmatic runs and sub-agents skip it.
|
|
5743
|
+
* Skipped under a history floor (`ConversationHistoryFrom`): the previous run's tool
|
|
5744
|
+
* results can quote messages from before the floor.
|
|
5062
5745
|
* @protected
|
|
5063
5746
|
*/
|
|
5064
5747
|
async injectPriorTurnToolResults(params) {
|
|
5065
|
-
if (!params.conversationId || this._depth !== 0) {
|
|
5748
|
+
if (!params.conversationId || this._depth !== 0 || params.ConversationHistoryFrom) {
|
|
5066
5749
|
return;
|
|
5067
5750
|
}
|
|
5068
5751
|
try {
|
|
@@ -5301,6 +5984,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5301
5984
|
promptParams.data = { lens, messages: rangeText };
|
|
5302
5985
|
promptParams.contextUser = params.contextUser;
|
|
5303
5986
|
promptParams.agentId = params.agent.ID;
|
|
5987
|
+
promptParams.UserID = ResolvePromptRunUserID({
|
|
5988
|
+
UserID: params.userId,
|
|
5989
|
+
AgentRun: this._agentRun,
|
|
5990
|
+
ContextUser: params.contextUser,
|
|
5991
|
+
}) ?? undefined;
|
|
5304
5992
|
const result = await this._promptRunner.ExecutePrompt(promptParams);
|
|
5305
5993
|
const text = ExtractPromptResultText(result);
|
|
5306
5994
|
if (!result.success || text.length === 0) {
|
|
@@ -5625,8 +6313,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5625
6313
|
};
|
|
5626
6314
|
// Operators (where/select/map/…) are pure code-defined verbs, not registry tools — only
|
|
5627
6315
|
// capabilities (Actions + artifact tools) live here as pipeline sources/stages.
|
|
5628
|
-
// Actions — each wrapped to run via the existing single-action execution path.
|
|
5629
|
-
|
|
6316
|
+
// Actions — each wrapped to run via the existing single-action execution path. The
|
|
6317
|
+
// run-scoped circuit breaker is bypassed here: the pipeline executor's `map` stage does
|
|
6318
|
+
// its own per-element failure accounting and expects elements to be independent, and
|
|
6319
|
+
// there is no model in that loop to act on the breaker's guidance.
|
|
6320
|
+
this.getEffectiveActionsForValidation(params.agent.ID).forEach((actionEntity) => register(new ActionInvocable(actionEntity.Name, (p) => this.ExecuteSingleAction(params, { name: actionEntity.Name, params: p }, actionEntity, params.contextUser, { skipCircuitBreaker: true }))));
|
|
5630
6321
|
// Artifact tools — one invocable per distinct tool name; `artifactId` is supplied as a
|
|
5631
6322
|
// call-time param so the same `{ tool, params }` step shape works across all substrates.
|
|
5632
6323
|
this._artifactToolManager.GetAvailableToolNames().forEach((toolName) => register(new ArtifactToolInvocable(toolName, async (tool, p) => {
|
|
@@ -6120,6 +6811,33 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6120
6811
|
return {};
|
|
6121
6812
|
}
|
|
6122
6813
|
}
|
|
6814
|
+
/**
|
|
6815
|
+
* Whether THIS action may use the run's runtime API key for THIS driver class. The default is
|
|
6816
|
+
* yes: the run was started on those keys, and an action that calls a vendor on the user's behalf
|
|
6817
|
+
* (Generate Image) is doing what the prompts do. Override to narrow it — an agent that knows
|
|
6818
|
+
* which of its actions talk to which vendor can refuse everything else, and a refusal costs the
|
|
6819
|
+
* action nothing but the customer's key: it falls back to the platform key as if the run had none.
|
|
6820
|
+
*/
|
|
6821
|
+
actionMayUseRuntimeAPIKey(action, driverClass, params) {
|
|
6822
|
+
return true;
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* The {@link RuntimeAPIKeyResolver} handed to one action dispatch: one driver class in, one key
|
|
6826
|
+
* out, the list itself never leaves this closure. Every answer is logged by action and driver
|
|
6827
|
+
* class (never the key), so a run's log shows which action drew which credential.
|
|
6828
|
+
*/
|
|
6829
|
+
buildRuntimeAPIKeyResolver(params, actionEntity) {
|
|
6830
|
+
const runKeys = params.apiKeys;
|
|
6831
|
+
return (driverClass) => {
|
|
6832
|
+
if (!this.actionMayUseRuntimeAPIKey(actionEntity, driverClass, params)) {
|
|
6833
|
+
this.logStatus(`🔑 Runtime API key for '${driverClass}' refused to action '${actionEntity.Name}' by policy — platform key applies`, true, params);
|
|
6834
|
+
return undefined;
|
|
6835
|
+
}
|
|
6836
|
+
const key = GetAIAPIKey(driverClass, runKeys);
|
|
6837
|
+
this.logStatus(`🔑 Action '${actionEntity.Name}' resolved an API key for '${driverClass}' (${runKeys?.some((k) => k.driverClass === driverClass) ? 'run' : 'platform'})`, true, params);
|
|
6838
|
+
return key || undefined;
|
|
6839
|
+
};
|
|
6840
|
+
}
|
|
6123
6841
|
/**
|
|
6124
6842
|
* This method executes one action using the MemberJunction Actions framework.
|
|
6125
6843
|
* The full ActionResult objects are returned, allowing the caller to access result codes, output parameters,
|
|
@@ -6128,12 +6846,24 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6128
6846
|
* @param {ExecuteAgentParams} params - Parameters from agent execution for context passing
|
|
6129
6847
|
* @param {AgentAction} action - Action to execute
|
|
6130
6848
|
* @param {UserInfo} [contextUser] - Optional user context for permissions
|
|
6849
|
+
* @param {ExecuteSingleActionOptions} [options] - `skipCircuitBreaker` bypasses the run-scoped
|
|
6850
|
+
* circuit breaker for callers that do their own failure accounting (the pipeline executor)
|
|
6131
6851
|
*
|
|
6132
6852
|
* @returns {Promise<ActionResult>} ActionResult object from the action execution
|
|
6133
6853
|
*
|
|
6134
6854
|
* @throws {Error} If the action fails to execute
|
|
6135
6855
|
*/
|
|
6136
|
-
async ExecuteSingleAction(params, action, actionEntity, contextUser) {
|
|
6856
|
+
async ExecuteSingleAction(params, action, actionEntity, contextUser, options) {
|
|
6857
|
+
const skipBreaker = options?.skipCircuitBreaker === true;
|
|
6858
|
+
const normalizedParams = this.normalizeActionParams(action.params);
|
|
6859
|
+
// Run-scoped circuit breaker: each rule short-circuits in 0ms with a result that carries the
|
|
6860
|
+
// rule that fired, so the failure directive can name it without consulting the history.
|
|
6861
|
+
if (!skipBreaker) {
|
|
6862
|
+
const blocked = this.checkActionCircuitBreaker(params, action, actionEntity, normalizedParams);
|
|
6863
|
+
if (blocked) {
|
|
6864
|
+
return blocked;
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6137
6867
|
try {
|
|
6138
6868
|
const actionEngine = ActionEngineServer.Instance;
|
|
6139
6869
|
// Convert params object to ActionParam array
|
|
@@ -6165,17 +6895,34 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6165
6895
|
ContextUser: contextUser,
|
|
6166
6896
|
Filters: [],
|
|
6167
6897
|
SkipActionLog: false,
|
|
6168
|
-
Context: actionContext
|
|
6898
|
+
Context: actionContext,
|
|
6899
|
+
// The run's RUNTIME API KEYS, as a RESOLVER bound to this one action — see
|
|
6900
|
+
// buildRuntimeAPIKeyResolver(). Per dispatch on purpose: actionContext IS params.context,
|
|
6901
|
+
// shared by every action in the run (parallel ones included) and copied into sub-agent
|
|
6902
|
+
// runs, so anything stamped there would name the wrong action under parallel dispatch
|
|
6903
|
+
// and travel further than the action it was meant for. Absent when the run has no keys,
|
|
6904
|
+
// so the action uses GetAIAPIKey(driverClass) exactly as before.
|
|
6905
|
+
RuntimeAPIKeyResolver: params.apiKeys && params.apiKeys.length > 0 ? this.buildRuntimeAPIKeyResolver(params, actionEntity) : undefined,
|
|
6169
6906
|
});
|
|
6170
6907
|
if (result.Success) {
|
|
6171
6908
|
this.logStatus(` ✅ Action '${action.name}' completed successfully`, true, params);
|
|
6909
|
+
if (!skipBreaker) {
|
|
6910
|
+
this.clearActionFailureRecord(action, actionEntity);
|
|
6911
|
+
}
|
|
6172
6912
|
}
|
|
6173
6913
|
else {
|
|
6174
6914
|
this.logStatus(` ❌ Action '${action.name}' failed: ${result.Message || 'Unknown error'}`, false, params);
|
|
6915
|
+
if (!skipBreaker) {
|
|
6916
|
+
this.recordActionFailure(action, actionEntity, result.Message, normalizedParams);
|
|
6917
|
+
}
|
|
6175
6918
|
}
|
|
6176
6919
|
return result;
|
|
6177
6920
|
}
|
|
6178
6921
|
catch (error) {
|
|
6922
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
6923
|
+
if (!skipBreaker) {
|
|
6924
|
+
this.recordActionFailure(action, actionEntity, errorMsg, normalizedParams);
|
|
6925
|
+
}
|
|
6179
6926
|
this.logError(error, {
|
|
6180
6927
|
category: 'ActionExecution',
|
|
6181
6928
|
metadata: {
|
|
@@ -6256,6 +7003,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6256
7003
|
* **Priority:** AIAgentRelationship.MessageMode takes precedence over AIAgent.MessageMode
|
|
6257
7004
|
* to allow different parent agents to pass messages differently to the same sub-agent.
|
|
6258
7005
|
*
|
|
7006
|
+
* **Runtime state never crosses the boundary.** The parent's trailing runtime-state messages
|
|
7007
|
+
* (`metadata.volatileState` — its payload, scratchpad and, when relocated, its specialization;
|
|
7008
|
+
* retained in the history under append-only mode) are dropped BEFORE any mode slices the
|
|
7009
|
+
* history, so a sub-agent never sees the parent's state, never has it counted against
|
|
7010
|
+
* `MaxMessages`, and never receives it unescaped when it builds no fragment of its own. The
|
|
7011
|
+
* sub-agent builds its own fragment at its prompt step.
|
|
7012
|
+
*
|
|
6259
7013
|
* Subclasses can override this method to implement custom message preparation logic
|
|
6260
7014
|
* specific to their domain (e.g., Skip agents adding special context).
|
|
6261
7015
|
*
|
|
@@ -6275,6 +7029,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6275
7029
|
// Get MessageMode and MaxMessages from either relationship or child agent
|
|
6276
7030
|
let messageMode = relationship?.MessageMode || subAgent.MessageMode || 'None';
|
|
6277
7031
|
let maxMessages = relationship?.MaxMessages || subAgent.MaxMessages || null;
|
|
7032
|
+
// The parent's history minus its runtime-state messages (see the doc comment): every mode
|
|
7033
|
+
// slices THIS, so no fragment reaches the sub-agent and none spends a MaxMessages slot.
|
|
7034
|
+
const history = params.conversationMessages.filter(m => m.metadata?.volatileState !== true);
|
|
6278
7035
|
// Apply message mode
|
|
6279
7036
|
switch (messageMode) {
|
|
6280
7037
|
case 'None':
|
|
@@ -6283,23 +7040,23 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6283
7040
|
break;
|
|
6284
7041
|
case 'All':
|
|
6285
7042
|
// Pass all parent conversation history
|
|
6286
|
-
messages = [...
|
|
7043
|
+
messages = [...history];
|
|
6287
7044
|
break;
|
|
6288
7045
|
case 'Latest':
|
|
6289
7046
|
// Pass most recent N messages
|
|
6290
7047
|
if (maxMessages && maxMessages > 0) {
|
|
6291
|
-
messages = this.makeToolTurnsSelfConsistent(
|
|
7048
|
+
messages = this.makeToolTurnsSelfConsistent(history.slice(-maxMessages));
|
|
6292
7049
|
}
|
|
6293
7050
|
else {
|
|
6294
|
-
messages = [...
|
|
7051
|
+
messages = [...history];
|
|
6295
7052
|
}
|
|
6296
7053
|
break;
|
|
6297
7054
|
case 'Bookend':
|
|
6298
7055
|
// Pass first 2 + most recent (N-2) with indicator message between
|
|
6299
|
-
if (maxMessages && maxMessages > 2 &&
|
|
6300
|
-
const firstTwo =
|
|
6301
|
-
const remaining =
|
|
6302
|
-
const omittedCount =
|
|
7056
|
+
if (maxMessages && maxMessages > 2 && history.length > maxMessages) {
|
|
7057
|
+
const firstTwo = history.slice(0, 2);
|
|
7058
|
+
const remaining = history.slice(-(maxMessages - 2));
|
|
7059
|
+
const omittedCount = history.length - maxMessages;
|
|
6303
7060
|
messages = this.makeToolTurnsSelfConsistent([
|
|
6304
7061
|
...firstTwo,
|
|
6305
7062
|
{
|
|
@@ -6310,7 +7067,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6310
7067
|
]);
|
|
6311
7068
|
}
|
|
6312
7069
|
else {
|
|
6313
|
-
messages = [...
|
|
7070
|
+
messages = [...history];
|
|
6314
7071
|
}
|
|
6315
7072
|
break;
|
|
6316
7073
|
default:
|
|
@@ -6407,6 +7164,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6407
7164
|
subAgentChanges: subAgentSubAgentChanges, // propagate filtered sub-agent changes to sub-agent
|
|
6408
7165
|
PrimaryScopeEntityName: params.PrimaryScopeEntityName, // propagate scope to sub-agent
|
|
6409
7166
|
PrimaryScopeRecordID: params.PrimaryScopeRecordID,
|
|
7167
|
+
companyId: params.companyId,
|
|
6410
7168
|
SecondaryScopes: params.SecondaryScopes,
|
|
6411
7169
|
onAgentRunCreated: async (agentRunId) => {
|
|
6412
7170
|
stepEntity.TargetLogID = agentRunId;
|
|
@@ -7759,7 +8517,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7759
8517
|
await this.recordFoldedTaskGraph(params, previousDecision);
|
|
7760
8518
|
return await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
7761
8519
|
case 'Actions':
|
|
7762
|
-
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount);
|
|
8520
|
+
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount, this.actionOptionsForAgentType());
|
|
7763
8521
|
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
7764
8522
|
// union (non-terminal, like 'ClientTools') — LoopAgentType.DetermineNextStep() emits it
|
|
7765
8523
|
// when the LLM chooses to activate a skill.
|
|
@@ -8650,9 +9408,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8650
9408
|
}
|
|
8651
9409
|
if (previousDecision?.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId) {
|
|
8652
9410
|
// the delegate_to_* call is answered as a tool result.
|
|
8653
|
-
params.conversationMessages.push(
|
|
9411
|
+
params.conversationMessages.push(BuildToolResultTurn([...this.payloadToolResult(previousDecision), {
|
|
8654
9412
|
toolCallId: subAgentRequest.toolCallId,
|
|
8655
|
-
toolName: `${SUB_AGENT_TOOL_PREFIX}${
|
|
9413
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${SanitizeToolName(subAgentRequest.name)}`,
|
|
8656
9414
|
content: resultMessage,
|
|
8657
9415
|
isError: !subAgentResult.success
|
|
8658
9416
|
}], subAgentMetadata));
|
|
@@ -9144,11 +9902,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9144
9902
|
const pairable = nativeResults ? allExecutions.filter((e) => !!e.request.toolCallId) : [];
|
|
9145
9903
|
const unpairable = allExecutions.filter((e) => !pairable.includes(e));
|
|
9146
9904
|
if (pairable.length > 0) {
|
|
9147
|
-
params.conversationMessages.push(
|
|
9905
|
+
params.conversationMessages.push(BuildToolResultTurn([
|
|
9148
9906
|
...this.payloadToolResult(previousDecision),
|
|
9149
9907
|
...pairable.map((e) => ({
|
|
9150
9908
|
toolCallId: e.request.toolCallId,
|
|
9151
|
-
toolName: `${SUB_AGENT_TOOL_PREFIX}${
|
|
9909
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${SanitizeToolName(e.request.name)}`,
|
|
9152
9910
|
content: this.buildParallelSubAgentSummary([e]),
|
|
9153
9911
|
isError: !e.result.success
|
|
9154
9912
|
}))
|
|
@@ -9372,9 +10130,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9372
10130
|
}
|
|
9373
10131
|
if (previousDecision.nativeTurn?.sendResultsNatively && subAgentRequest.toolCallId) {
|
|
9374
10132
|
// the delegate_to_* call is answered as a tool result (same as the child path).
|
|
9375
|
-
params.conversationMessages.push(
|
|
10133
|
+
params.conversationMessages.push(BuildToolResultTurn([...this.payloadToolResult(previousDecision), {
|
|
9376
10134
|
toolCallId: subAgentRequest.toolCallId,
|
|
9377
|
-
toolName: `${SUB_AGENT_TOOL_PREFIX}${
|
|
10135
|
+
toolName: `${SUB_AGENT_TOOL_PREFIX}${SanitizeToolName(subAgentRequest.name)}`,
|
|
9378
10136
|
content: relatedResultMessage,
|
|
9379
10137
|
isError: !subAgentResult.success
|
|
9380
10138
|
}], relatedMetadata));
|
|
@@ -9623,12 +10381,21 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9623
10381
|
* Supports both dot notation (obj.prop) and array indexing (arr[0]).
|
|
9624
10382
|
*
|
|
9625
10383
|
|
|
10384
|
+
/**
|
|
10385
|
+
* The {@link ExecuteSingleActionOptions} the main loop passes for this run's agent type: the
|
|
10386
|
+
* circuit-breaker exemption when the type has opted out (`BaseAgentType.UsesActionCircuitBreaker`
|
|
10387
|
+
* is false — Flow), otherwise none. Kept as a seam so a subclass can widen or narrow the
|
|
10388
|
+
* exemption without touching the loop.
|
|
10389
|
+
*/
|
|
10390
|
+
actionOptionsForAgentType() {
|
|
10391
|
+
return this.AgentTypeInstance?.UsesActionCircuitBreaker === false ? { skipCircuitBreaker: true } : undefined;
|
|
10392
|
+
}
|
|
9626
10393
|
/**
|
|
9627
10394
|
* Executes actions step and tracks it.
|
|
9628
10395
|
*
|
|
9629
10396
|
* @private
|
|
9630
10397
|
*/
|
|
9631
|
-
async executeActionsStep(params, previousDecision, parentStepId, addConversationMessage = true, stepCount = 0) {
|
|
10398
|
+
async executeActionsStep(params, previousDecision, parentStepId, addConversationMessage = true, stepCount = 0, actionOptions) {
|
|
9632
10399
|
try {
|
|
9633
10400
|
const currentPayload = previousDecision?.newPayload || previousDecision?.previousPayload || params.payload;
|
|
9634
10401
|
const actions = previousDecision.actions || [];
|
|
@@ -9788,7 +10555,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9788
10555
|
let actionResult;
|
|
9789
10556
|
try {
|
|
9790
10557
|
// Execute the action
|
|
9791
|
-
actionResult = await this.ExecuteSingleAction(params, aa, actionEntity, params.contextUser);
|
|
10558
|
+
actionResult = await this.ExecuteSingleAction(params, aa, actionEntity, params.contextUser, actionOptions);
|
|
9792
10559
|
// Update step entity with ActionExecutionLog ID if available
|
|
9793
10560
|
if (actionResult.LogEntry?.ID) {
|
|
9794
10561
|
const logId = actionResult.LogEntry.ID;
|
|
@@ -9808,7 +10575,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9808
10575
|
};
|
|
9809
10576
|
// Finalize step entity with output data
|
|
9810
10577
|
await this.finalizeStepEntity(stepEntity, actionResult.Success, actionResult.Success ? undefined : actionResult.Message, outputData);
|
|
9811
|
-
return { success:
|
|
10578
|
+
return { success: actionResult.Success, result: actionResult, action: aa, actionEntity, stepEntity, error: actionResult.Success ? undefined : actionResult.Message };
|
|
9812
10579
|
}
|
|
9813
10580
|
catch (error) {
|
|
9814
10581
|
await this.finalizeStepEntity(stepEntity, false, error.message);
|
|
@@ -9824,9 +10591,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9824
10591
|
// Build a clean summary of action results
|
|
9825
10592
|
// Apply large binary content interception to prevent context overflow
|
|
9826
10593
|
const actionSummaries = actionResults.map(result => {
|
|
9827
|
-
const actionResult = result.
|
|
10594
|
+
const actionResult = result.result;
|
|
10595
|
+
const isActionSuccess = Boolean(result.success && (actionResult ? actionResult.Success : true));
|
|
9828
10596
|
// Filter to output params only
|
|
9829
|
-
const outputParams =
|
|
10597
|
+
const outputParams = actionResult?.Params?.filter(p => p.Type === 'Both' || p.Type === 'Output') || [];
|
|
9830
10598
|
// Intercept large media content (images, audio, video) and replace with placeholders
|
|
9831
10599
|
// This prevents context overflow from base64 data (~700K tokens per 1024x1024 image)
|
|
9832
10600
|
// Pass actionEntity for generic ValueType=MediaOutput detection from metadata
|
|
@@ -9836,11 +10604,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9836
10604
|
this._fileOutputs.push(...fileOutputs);
|
|
9837
10605
|
return {
|
|
9838
10606
|
actionName: result.action.name,
|
|
9839
|
-
success:
|
|
10607
|
+
success: isActionSuccess,
|
|
9840
10608
|
params: sanitizedParams,
|
|
9841
|
-
resultCode: actionResult?.Result?.ResultCode || (
|
|
9842
|
-
message:
|
|
9843
|
-
aiDirectives:
|
|
10609
|
+
resultCode: actionResult?.Result?.ResultCode || (isActionSuccess ? 'SUCCESS' : 'ERROR'),
|
|
10610
|
+
message: actionResult?.Message || (isActionSuccess ? 'Action completed' : result.error || 'Unknown error'),
|
|
10611
|
+
aiDirectives: isActionSuccess ? actionResult?.AIDirectives : undefined,
|
|
10612
|
+
breakerReason: actionResult instanceof CircuitBreakerActionResult ? actionResult.Reason : undefined
|
|
9844
10613
|
};
|
|
9845
10614
|
});
|
|
9846
10615
|
// Check if any actions failed
|
|
@@ -9906,6 +10675,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9906
10675
|
content: `IMPORTANT — Follow these directives from the action results:\n\n${directiveText}`
|
|
9907
10676
|
});
|
|
9908
10677
|
}
|
|
10678
|
+
// Surface failure guidance for failed actions so the model does not repeatedly loop on broken
|
|
10679
|
+
// tools. Not when the breaker is bypassed for this step: there is then no model in the loop to
|
|
10680
|
+
// act on it (Flow, ForEach, While, pipeline), and the directive would only pollute the history.
|
|
10681
|
+
if (failedActions.length > 0 && actionOptions?.skipCircuitBreaker !== true) {
|
|
10682
|
+
const failureText = failedActions.map(f => this.formatActionFailureDirective(f)).join('\n\n');
|
|
10683
|
+
params.conversationMessages.push({
|
|
10684
|
+
role: 'user',
|
|
10685
|
+
content: `IMPORTANT — Action Execution Failure Guidance:\n\n${failureText}`
|
|
10686
|
+
});
|
|
10687
|
+
}
|
|
9909
10688
|
}
|
|
9910
10689
|
// Call agent type's post-processing for actions
|
|
9911
10690
|
let finalPayload = currentPayload;
|
|
@@ -11174,16 +11953,18 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11174
11953
|
async executeForEachLoop(params, config, previousDecision) {
|
|
11175
11954
|
const forEach = previousDecision.forEach;
|
|
11176
11955
|
if (!forEach) {
|
|
11956
|
+
// Not reported to the model: a Loop agent never gets here, because LoopAgentType turns a
|
|
11957
|
+
// ForEach step with no details into a Retry whose errorMessage the model is shown.
|
|
11177
11958
|
return this.createFailedStep('ForEach configuration missing', previousDecision);
|
|
11178
11959
|
}
|
|
11179
11960
|
const validationMessage = this.validateForEachOperation(forEach);
|
|
11180
11961
|
if (validationMessage) {
|
|
11181
|
-
return this.
|
|
11962
|
+
return this.failLoopBeforeFirstIteration('ForEach', forEach.collectionPath, `ForEach configuration invalid: ${validationMessage}`, previousDecision, params, forEach.action?.name);
|
|
11182
11963
|
}
|
|
11183
11964
|
const currentPayload = previousDecision.newPayload || previousDecision.previousPayload;
|
|
11184
11965
|
const collection = this.getCollectionFromPayload(currentPayload, forEach.collectionPath);
|
|
11185
11966
|
if (!collection) {
|
|
11186
|
-
return this.
|
|
11967
|
+
return this.failLoopBeforeFirstIteration('ForEach', forEach.collectionPath, `Collection path "${forEach.collectionPath}" not an array`, previousDecision, params, forEach.action?.name);
|
|
11187
11968
|
}
|
|
11188
11969
|
const loopStepEntity = await this.createForEachLoopStep(forEach, collection, currentPayload, params);
|
|
11189
11970
|
const loopResults = await this.executeForEachIterations(forEach, collection, currentPayload, loopStepEntity.ID, params, config);
|
|
@@ -11461,7 +12242,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11461
12242
|
params: resolvedParams
|
|
11462
12243
|
};
|
|
11463
12244
|
const actionStep = { step: 'Actions', actions: [resolvedAction], newPayload: currentPayload, previousPayload: currentPayload, terminate: false };
|
|
11464
|
-
|
|
12245
|
+
// Loop iterations bypass the circuit breaker: the loop does its own per-item accounting and
|
|
12246
|
+
// there is no model between items to act on the breaker's guidance.
|
|
12247
|
+
result = await this.executeActionsStep(params, actionStep, parentStepId, false, 0, { skipCircuitBreaker: true });
|
|
11465
12248
|
}
|
|
11466
12249
|
else if (forEach.subAgent) {
|
|
11467
12250
|
const subAgentStep = { step: 'Sub-Agent', subAgent: forEach.subAgent, newPayload: currentPayload, previousPayload: currentPayload };
|
|
@@ -11487,7 +12270,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11487
12270
|
async completeForEachLoop(forEach, loopStepEntity, loopResults, previousDecision, params) {
|
|
11488
12271
|
// Finalize the loop step now that loop is complete
|
|
11489
12272
|
loopStepEntity.PayloadAtEnd = this.serializePayloadAtEnd(loopResults.finalPayload);
|
|
11490
|
-
await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, loopResults.errors
|
|
12273
|
+
await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, this.formatLoopErrors(loopResults.errors), loopResults);
|
|
11491
12274
|
if (this.AgentTypeInstance.InjectLoopResultsAsMessage) {
|
|
11492
12275
|
this.injectLoopResultsMessage('ForEach', forEach.collectionPath, loopResults.results, loopResults.errors, params, forEach.action?.name);
|
|
11493
12276
|
}
|
|
@@ -11517,6 +12300,32 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11517
12300
|
metadata
|
|
11518
12301
|
});
|
|
11519
12302
|
}
|
|
12303
|
+
/**
|
|
12304
|
+
* Fails a loop that never ran an iteration, and tells the model why.
|
|
12305
|
+
*
|
|
12306
|
+
* A Loop agent answers a Failed step by prompting again (`HandleStepFallback` returns null), and
|
|
12307
|
+
* nothing on that path reads the step's `errorMessage`. Without the injected message the model
|
|
12308
|
+
* gets another turn with no idea its loop failed, and is likely to emit the same loop again.
|
|
12309
|
+
* Flow agents don't inject loop results, so for them this is just the Failed step.
|
|
12310
|
+
*/
|
|
12311
|
+
failLoopBeforeFirstIteration(loopType, collectionOrCondition, errorMessage, previousDecision, params, actionName) {
|
|
12312
|
+
if (this.AgentTypeInstance.InjectLoopResultsAsMessage) {
|
|
12313
|
+
this.injectLoopResultsMessage(loopType, collectionOrCondition, [], [{ index: 0, message: errorMessage }], params, actionName);
|
|
12314
|
+
}
|
|
12315
|
+
return this.createFailedStep(errorMessage, previousDecision);
|
|
12316
|
+
}
|
|
12317
|
+
/**
|
|
12318
|
+
* One loop error as readable text. Joining the error objects directly wrote "[object Object]"
|
|
12319
|
+
* into the step's ErrorMessage. Falls back to JSON when an iteration threw something with no
|
|
12320
|
+
* message (a thrown non-Error).
|
|
12321
|
+
*/
|
|
12322
|
+
describeLoopError(err) {
|
|
12323
|
+
return err.message ? err.message : JSON.stringify(err);
|
|
12324
|
+
}
|
|
12325
|
+
/** All loop errors as text for a step's ErrorMessage. */
|
|
12326
|
+
formatLoopErrors(errors) {
|
|
12327
|
+
return errors.map(err => this.describeLoopError(err)).join('\n\n');
|
|
12328
|
+
}
|
|
11520
12329
|
/**
|
|
11521
12330
|
* Formats loop iteration results as markdown. Handles two distinct result shapes
|
|
11522
12331
|
* depending on whether the loop body executed actions or sub-agents:
|
|
@@ -11557,8 +12366,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11557
12366
|
if (errors.length > 0) {
|
|
11558
12367
|
lines.push(`### Errors`);
|
|
11559
12368
|
for (const err of errors) {
|
|
11560
|
-
|
|
11561
|
-
lines.push(`• ✗ ${errMsg}`);
|
|
12369
|
+
lines.push(`• ✗ ${this.describeLoopError(err)}`);
|
|
11562
12370
|
}
|
|
11563
12371
|
}
|
|
11564
12372
|
return lines.join('\n');
|
|
@@ -11664,11 +12472,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11664
12472
|
async executeWhileLoop(params, config, previousDecision) {
|
|
11665
12473
|
const whileOp = previousDecision.while;
|
|
11666
12474
|
if (!whileOp) {
|
|
12475
|
+
// Not reported to the model: a Loop agent never gets here, because LoopAgentType turns a
|
|
12476
|
+
// While step with no details into a Retry whose errorMessage the model is shown.
|
|
11667
12477
|
return this.createFailedStep('While configuration missing', previousDecision);
|
|
11668
12478
|
}
|
|
11669
12479
|
const validationMessage = this.validateWhileOperation(whileOp);
|
|
11670
12480
|
if (validationMessage) {
|
|
11671
|
-
return this.
|
|
12481
|
+
return this.failLoopBeforeFirstIteration('While', whileOp.condition, `While configuration invalid: ${validationMessage}`, previousDecision, params, whileOp.action?.name);
|
|
11672
12482
|
}
|
|
11673
12483
|
const currentPayload = previousDecision.newPayload || previousDecision.previousPayload;
|
|
11674
12484
|
const loopStepEntity = await this.createWhileLoopStep(whileOp, currentPayload, params);
|
|
@@ -11697,6 +12507,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11697
12507
|
const results = [];
|
|
11698
12508
|
const errors = [];
|
|
11699
12509
|
let iterationCount = 0;
|
|
12510
|
+
let conditionError;
|
|
11700
12511
|
const evaluator = new SafeExpressionEvaluator();
|
|
11701
12512
|
// ACTUAL WHILE LOOP - simple and clear!
|
|
11702
12513
|
while (iterationCount < maxIterations) {
|
|
@@ -11704,7 +12515,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11704
12515
|
await new Promise(resolve => setTimeout(resolve, whileOp.delayBetweenIterationsMs));
|
|
11705
12516
|
}
|
|
11706
12517
|
const evalResult = evaluator.evaluate(whileOp.condition, { payload: currentPayload, results, errors });
|
|
11707
|
-
if (!evalResult.success
|
|
12518
|
+
if (!evalResult.success) {
|
|
12519
|
+
// "Could not evaluate" is not "evaluated false". Treating it as false used to end the
|
|
12520
|
+
// loop silently and finalize it as a success — a malformed condition produced a green,
|
|
12521
|
+
// zero-iteration loop with the evaluator's error discarded.
|
|
12522
|
+
conditionError = `While condition '${whileOp.condition}' could not be evaluated: ${evalResult.error ?? 'unknown error'}`;
|
|
12523
|
+
errors.push({ index: iterationCount, message: conditionError });
|
|
12524
|
+
break;
|
|
12525
|
+
}
|
|
12526
|
+
if (!evalResult.value) {
|
|
11708
12527
|
break;
|
|
11709
12528
|
}
|
|
11710
12529
|
const attemptContext = { attemptNumber: iterationCount + 1, totalAttempts: iterationCount };
|
|
@@ -11720,7 +12539,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11720
12539
|
}
|
|
11721
12540
|
iterationCount++;
|
|
11722
12541
|
}
|
|
11723
|
-
return { results, errors, finalPayload: currentPayload, iterations: iterationCount };
|
|
12542
|
+
return { results, errors, finalPayload: currentPayload, iterations: iterationCount, conditionError };
|
|
11724
12543
|
}
|
|
11725
12544
|
/**
|
|
11726
12545
|
* Helper: Execute single While iteration
|
|
@@ -11749,7 +12568,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11749
12568
|
params: resolvedParams
|
|
11750
12569
|
};
|
|
11751
12570
|
const actionStep = { step: 'Actions', actions: [resolvedAction], newPayload: currentPayload, previousPayload: currentPayload, terminate: false };
|
|
11752
|
-
|
|
12571
|
+
// Same exemption as ForEach: the loop owns per-iteration accounting.
|
|
12572
|
+
result = await this.executeActionsStep(params, actionStep, parentStepId, false, 0, { skipCircuitBreaker: true });
|
|
11753
12573
|
}
|
|
11754
12574
|
else if (whileOp.subAgent) {
|
|
11755
12575
|
const subAgentStep = { step: 'Sub-Agent', subAgent: whileOp.subAgent, newPayload: currentPayload, previousPayload: currentPayload };
|
|
@@ -11774,13 +12594,23 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11774
12594
|
*/
|
|
11775
12595
|
async completeWhileLoop(whileOp, loopStepEntity, loopResults, previousDecision, params) {
|
|
11776
12596
|
loopStepEntity.PayloadAtEnd = this.serializePayloadAtEnd(loopResults.finalPayload);
|
|
11777
|
-
await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, loopResults.errors
|
|
12597
|
+
await this.finalizeStepEntity(loopStepEntity, loopResults.errors.length === 0, this.formatLoopErrors(loopResults.errors), loopResults);
|
|
12598
|
+
// Inject before the early return below: a Loop agent re-prompts after a Failed step, and the
|
|
12599
|
+
// loop-results message (the condition error, under Errors) is the only way the model learns why.
|
|
11778
12600
|
if (this.AgentTypeInstance.InjectLoopResultsAsMessage) {
|
|
11779
12601
|
this.injectLoopResultsMessage('While', whileOp.condition, loopResults.results, loopResults.errors, params, whileOp.action?.name);
|
|
11780
12602
|
}
|
|
12603
|
+
// A condition that never evaluated means the loop never ran: fail the step rather than
|
|
12604
|
+
// report a completed zero-iteration loop.
|
|
12605
|
+
if (loopResults.conditionError && loopResults.iterations === 0) {
|
|
12606
|
+
return this.createFailedStep(loopResults.conditionError, previousDecision);
|
|
12607
|
+
}
|
|
12608
|
+
const retryInstructions = loopResults.conditionError
|
|
12609
|
+
? `While loop request using condition '${whileOp.condition}' stopped after ${loopResults.iterations} iteration(s): ${loopResults.conditionError}`
|
|
12610
|
+
: `Completed While loop request using condition '${whileOp.condition}' after ${loopResults.iterations} iteration(s)`;
|
|
11781
12611
|
return {
|
|
11782
12612
|
step: 'Retry',
|
|
11783
|
-
retryInstructions
|
|
12613
|
+
retryInstructions,
|
|
11784
12614
|
terminate: false,
|
|
11785
12615
|
newPayload: loopResults.finalPayload,
|
|
11786
12616
|
previousPayload: previousDecision.previousPayload
|
|
@@ -11927,7 +12757,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11927
12757
|
}
|
|
11928
12758
|
// Also promote any media from the final step's promoteMediaOutputs
|
|
11929
12759
|
if (finalStep.promoteMediaOutputs && finalStep.promoteMediaOutputs.length > 0) {
|
|
11930
|
-
this.
|
|
12760
|
+
this.PromoteMediaOutputs(finalStep.promoteMediaOutputs);
|
|
11931
12761
|
}
|
|
11932
12762
|
// Return unified media outputs — all items are persisted by AgentRunner.
|
|
11933
12763
|
// Sub-agents pass their mediaOutputs to parent for merging and placeholder resolution.
|
|
@@ -12170,7 +13000,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12170
13000
|
if (item.message.role === 'tool') {
|
|
12171
13001
|
// compact per block so the tool turn keeps answering its call.
|
|
12172
13002
|
const limit = item.metadata.compactLength || 500;
|
|
12173
|
-
const compactedTurn =
|
|
13003
|
+
const compactedTurn = CompactToolResultContent(item.message, (t) => (t.length > limit ? `${t.slice(0, limit)}… [compacted from ${t.length} chars]` : t));
|
|
12174
13004
|
const saved = this.estimateTokens(originalContent) - this.estimateTokens(compactedTurn.content);
|
|
12175
13005
|
params.conversationMessages[item.index] = {
|
|
12176
13006
|
...compactedTurn,
|
|
@@ -12270,10 +13100,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12270
13100
|
* (agent or type ContextWindowMaxTokens) — before the first prompt the model is
|
|
12271
13101
|
* unknown, and compacting against the conservative default would over-trigger on
|
|
12272
13102
|
* large-context models. The post-turn hook (real model known) covers those.
|
|
13103
|
+
* Skipped under a history floor (`ConversationHistoryFrom`): a summary folds in the
|
|
13104
|
+
* conversation from its first message, which is what the floor excludes.
|
|
12273
13105
|
* @protected
|
|
12274
13106
|
*/
|
|
12275
13107
|
async checkPreTurnCompaction(params, config) {
|
|
12276
|
-
if (!params.conversationId || this._depth !== 0) {
|
|
13108
|
+
if (!params.conversationId || this._depth !== 0 || params.ConversationHistoryFrom) {
|
|
12277
13109
|
return;
|
|
12278
13110
|
}
|
|
12279
13111
|
const budget = this.resolveCompactionBudget(params, config);
|
|
@@ -12298,10 +13130,19 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12298
13130
|
* final step (→ AwaitingFeedback) is the NORMAL ending of a conversational turn;
|
|
12299
13131
|
* gating on 'Completed' alone silently disabled post-turn compaction for exactly
|
|
12300
13132
|
* the long-chat scenario this feature targets.
|
|
13133
|
+
*
|
|
13134
|
+
* Skipped under a history floor (`ConversationHistoryFrom`). A run with a floor must not
|
|
13135
|
+
* write the conversation's summary: the summary covers every row below its boundary, and
|
|
13136
|
+
* a run that may not read the rows before the floor can't produce that — nor, once reads
|
|
13137
|
+
* are narrowed to what the asker can see, can it tell which rows it was not shown.
|
|
12301
13138
|
* @protected
|
|
12302
13139
|
*/
|
|
12303
13140
|
startPostTurnCompaction() {
|
|
12304
13141
|
const params = this._executeParams;
|
|
13142
|
+
if (params?.ConversationHistoryFrom) {
|
|
13143
|
+
this.logStatus('Post-turn compaction skipped — the run has a history floor', true, params);
|
|
13144
|
+
return;
|
|
13145
|
+
}
|
|
12305
13146
|
if (!params?.conversationId || this._depth !== 0 || !this._agentRun
|
|
12306
13147
|
|| !BaseAgent.settledRunStatuses.includes(this._agentRun.Status)) {
|
|
12307
13148
|
// A quiet return here is indistinguishable from "the pass ran and found nothing to do":
|
|
@@ -12570,6 +13411,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12570
13411
|
};
|
|
12571
13412
|
promptParams.contextUser = params.contextUser;
|
|
12572
13413
|
promptParams.agentId = params.agent.ID;
|
|
13414
|
+
promptParams.UserID = ResolvePromptRunUserID({
|
|
13415
|
+
UserID: params.userId,
|
|
13416
|
+
AgentRun: this._agentRun,
|
|
13417
|
+
ContextUser: params.contextUser,
|
|
13418
|
+
}) ?? undefined;
|
|
12573
13419
|
const runner = new AIPromptRunner();
|
|
12574
13420
|
const result = await runner.ExecutePrompt(promptParams);
|
|
12575
13421
|
// Update step with result
|