@memberjunction/ai-agents 6.2.0-edge.2 → 6.2.0-edge.4
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 +6 -0
- package/dist/AgentDecisionService.d.ts +8 -1
- package/dist/AgentDecisionService.d.ts.map +1 -1
- package/dist/AgentDecisionService.js +4 -0
- package/dist/AgentDecisionService.js.map +1 -1
- package/dist/AgentRunner.d.ts +51 -2
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +121 -23
- package/dist/AgentRunner.js.map +1 -1
- package/dist/ConversationCompactionManager.d.ts +9 -1
- package/dist/ConversationCompactionManager.d.ts.map +1 -1
- package/dist/ConversationCompactionManager.js +6 -3
- package/dist/ConversationCompactionManager.js.map +1 -1
- package/dist/ConversationToolManager.d.ts +13 -2
- package/dist/ConversationToolManager.d.ts.map +1 -1
- package/dist/ConversationToolManager.js +19 -3
- package/dist/ConversationToolManager.js.map +1 -1
- package/dist/KnowledgeAgent.d.ts.map +1 -1
- package/dist/KnowledgeAgent.js +6 -1
- package/dist/KnowledgeAgent.js.map +1 -1
- package/dist/MJAIAgentRequestEntityServer.d.ts +6 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts.map +1 -1
- package/dist/MJAIAgentRequestEntityServer.js +26 -1
- package/dist/MJAIAgentRequestEntityServer.js.map +1 -1
- package/dist/PayloadFeedbackManager.d.ts +3 -0
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +2 -1
- package/dist/PayloadFeedbackManager.js.map +1 -1
- package/dist/agent-run-watchdog.d.ts +65 -5
- package/dist/agent-run-watchdog.d.ts.map +1 -1
- package/dist/agent-run-watchdog.js +143 -28
- package/dist/agent-run-watchdog.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +7 -0
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +1 -0
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts +20 -0
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +73 -2
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/artifact-tools/SearchResultSetToolLibrary.js +4 -4
- package/dist/artifact-tools/SearchResultSetToolLibrary.js.map +1 -1
- package/dist/base-agent.d.ts +310 -26
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +867 -244
- package/dist/base-agent.js.map +1 -1
- package/dist/client-tool-results.d.ts +45 -0
- package/dist/client-tool-results.d.ts.map +1 -0
- package/dist/client-tool-results.js +94 -0
- package/dist/client-tool-results.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/native-tools/action-tool-builder.d.ts +14 -0
- package/dist/native-tools/action-tool-builder.d.ts.map +1 -1
- package/dist/native-tools/action-tool-builder.js +62 -7
- package/dist/native-tools/action-tool-builder.js.map +1 -1
- package/dist/native-tools/control-tools.d.ts +17 -1
- package/dist/native-tools/control-tools.d.ts.map +1 -1
- package/dist/native-tools/control-tools.js +40 -3
- package/dist/native-tools/control-tools.js.map +1 -1
- package/dist/prior-turn-tool-result-cache.d.ts +31 -12
- package/dist/prior-turn-tool-result-cache.d.ts.map +1 -1
- package/dist/prior-turn-tool-result-cache.js +59 -16
- package/dist/prior-turn-tool-result-cache.js.map +1 -1
- package/dist/realtime/bridge-host-params.d.ts +22 -0
- package/dist/realtime/bridge-host-params.d.ts.map +1 -0
- package/dist/realtime/bridge-host-params.js +53 -0
- package/dist/realtime/bridge-host-params.js.map +1 -0
- package/dist/realtime/bridge-realtime-session-factory.d.ts +63 -3
- package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
- package/dist/realtime/bridge-realtime-session-factory.js +83 -22
- package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
- package/dist/realtime/bridge-realtime-usage-recorder.d.ts +81 -0
- package/dist/realtime/bridge-realtime-usage-recorder.d.ts.map +1 -0
- package/dist/realtime/bridge-realtime-usage-recorder.js +194 -0
- package/dist/realtime/bridge-realtime-usage-recorder.js.map +1 -0
- package/dist/realtime/bridge-session-transcript-sink.d.ts +37 -0
- package/dist/realtime/bridge-session-transcript-sink.d.ts.map +1 -0
- package/dist/realtime/bridge-session-transcript-sink.js +58 -0
- package/dist/realtime/bridge-session-transcript-sink.js.map +1 -0
- package/dist/realtime/client-only-channel-server.d.ts +83 -0
- package/dist/realtime/client-only-channel-server.d.ts.map +1 -0
- package/dist/realtime/client-only-channel-server.js +107 -0
- package/dist/realtime/client-only-channel-server.js.map +1 -0
- package/dist/realtime/identity-verification-channel-server.d.ts +50 -0
- package/dist/realtime/identity-verification-channel-server.d.ts.map +1 -0
- package/dist/realtime/identity-verification-channel-server.js +66 -0
- package/dist/realtime/identity-verification-channel-server.js.map +1 -0
- package/dist/realtime/interactive-component-channel-server.d.ts +39 -0
- package/dist/realtime/interactive-component-channel-server.d.ts.map +1 -0
- package/dist/realtime/interactive-component-channel-server.js +55 -0
- package/dist/realtime/interactive-component-channel-server.js.map +1 -0
- package/dist/realtime/realtime-avatar-resolution.d.ts +194 -0
- package/dist/realtime/realtime-avatar-resolution.d.ts.map +1 -0
- package/dist/realtime/realtime-avatar-resolution.js +208 -0
- package/dist/realtime/realtime-avatar-resolution.js.map +1 -0
- package/dist/realtime/realtime-channel-policy.d.ts +81 -0
- package/dist/realtime/realtime-channel-policy.d.ts.map +1 -0
- package/dist/realtime/realtime-channel-policy.js +91 -0
- package/dist/realtime/realtime-channel-policy.js.map +1 -0
- package/dist/realtime/realtime-channel-server-host.d.ts +14 -0
- package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -1
- package/dist/realtime/realtime-channel-server-host.js +35 -0
- package/dist/realtime/realtime-channel-server-host.js.map +1 -1
- package/dist/realtime/realtime-client-session-service.d.ts +493 -30
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +825 -98
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +94 -15
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +109 -4
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-coagent-resolution.d.ts +90 -0
- package/dist/realtime/realtime-coagent-resolution.d.ts.map +1 -0
- package/dist/realtime/realtime-coagent-resolution.js +186 -0
- package/dist/realtime/realtime-coagent-resolution.js.map +1 -0
- package/dist/realtime/realtime-delegation-narrator.d.ts +97 -0
- package/dist/realtime/realtime-delegation-narrator.d.ts.map +1 -0
- package/dist/realtime/realtime-delegation-narrator.js +162 -0
- package/dist/realtime/realtime-delegation-narrator.js.map +1 -0
- package/dist/realtime/realtime-recording-store.d.ts +136 -16
- package/dist/realtime/realtime-recording-store.d.ts.map +1 -1
- package/dist/realtime/realtime-recording-store.js +505 -39
- package/dist/realtime/realtime-recording-store.js.map +1 -1
- package/dist/realtime/realtime-session-runner.d.ts +2 -82
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
- package/dist/realtime/realtime-session-runner.js +16 -191
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/realtime/realtime-vendor-resolution.d.ts +9 -0
- package/dist/realtime/realtime-vendor-resolution.d.ts.map +1 -1
- package/dist/realtime/realtime-vendor-resolution.js +16 -5
- package/dist/realtime/realtime-vendor-resolution.js.map +1 -1
- package/dist/realtime/realtime-video-output-gate.d.ts +83 -0
- package/dist/realtime/realtime-video-output-gate.d.ts.map +1 -0
- package/dist/realtime/realtime-video-output-gate.js +79 -0
- package/dist/realtime/realtime-video-output-gate.js.map +1 -0
- package/dist/rubric-evaluation-agent-runner.js +17 -8
- package/dist/rubric-evaluation-agent-runner.js.map +1 -1
- package/dist/self-check.d.ts +6 -3
- package/dist/self-check.d.ts.map +1 -1
- package/dist/self-check.js +3 -3
- package/dist/self-check.js.map +1 -1
- package/dist/tool-result-format.d.ts +14 -0
- package/dist/tool-result-format.d.ts.map +1 -1
- package/dist/tool-result-format.js.map +1 -1
- package/package.json +19 -19
package/dist/base-agent.js
CHANGED
|
@@ -10,20 +10,20 @@
|
|
|
10
10
|
* @author MemberJunction.com
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
|
-
import {
|
|
13
|
+
import { MJEnvironmentEntityExtended, ConversationEngine } from '@memberjunction/core-entities';
|
|
14
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';
|
|
15
|
+
import { BuildNativeToolSet, COMPLETE_TASK_TOOL, SUB_AGENT_TOOL_PREFIX } from './native-tools/control-tools.js';
|
|
16
16
|
import { BuildAssistantToolCallTurn, BuildToolResultTurn, CompactToolResultContent } from './native-tools/tool-result-turns.js';
|
|
17
17
|
import { LooksLikeLoopEnvelope } from './native-tools/dual-channel.js';
|
|
18
|
-
import { ResolvePromptRunUserID } from "@memberjunction/ai-core-plus";
|
|
19
|
-
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
18
|
+
import { MJAIAgentRunEntityExtended, ResolvePromptRunUserID } from "@memberjunction/ai-core-plus";
|
|
19
|
+
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase, ToPlainJSON } from '@memberjunction/core';
|
|
20
20
|
import { AgentRunWatchdog } from './agent-run-watchdog.js';
|
|
21
21
|
import { AIPromptRunner, GetToolCallingDecision } from '@memberjunction/ai-prompts';
|
|
22
|
-
import { BaseRealtimeModel, GetAIAPIKey, MakeAIAPIKeyResolver, IsPrefixPromptCache } from '@memberjunction/ai';
|
|
22
|
+
import { BaseRealtimeModel, GetAIAPIKey, CredentialScopeAllows, MakeAIAPIKeyResolver, IsPrefixPromptCache } from '@memberjunction/ai';
|
|
23
23
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
24
24
|
import { ProviderRubricEngine } from '@memberjunction/rubrics';
|
|
25
25
|
import { ExecuteSelfCheck, PickSelfCheckLink } from './self-check.js';
|
|
26
|
-
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, NormalizeUUID, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString, IsPlainObject, CleanAndParseJSON } from '@memberjunction/global';
|
|
26
|
+
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, NormalizeUUID, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString, IsPlainObject, CleanAndParseJSON, IsValidUUID } from '@memberjunction/global';
|
|
27
27
|
// token optimization via @memberjunction/context-crush (SmartCrusher/CacheAligner-inspired)
|
|
28
28
|
import { CrushJSON, DescribeCrush, PartitionStablePrefix } from '@memberjunction/context-crush';
|
|
29
29
|
// AST-aware code reduction (CodeCompressor-inspired) — opt-in per agent type
|
|
@@ -31,9 +31,12 @@ import { CrushCode } from '@memberjunction/context-crush/code';
|
|
|
31
31
|
import { RealtimeSessionRunner } from './realtime/realtime-session-runner.js';
|
|
32
32
|
import { ResolveNarrationInstructionsTemplate } from './realtime/realtime-narration.js';
|
|
33
33
|
import { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, GetSessionTuningSettings, GetModelCatalogSessionSettings, DeepMergeConfigs, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
|
|
34
|
-
import { SelectRealtimeVendorForModel } from './realtime/realtime-vendor-resolution.js';
|
|
34
|
+
import { ListRealtimeVendorsForModel, SelectRealtimeVendorForModel } from './realtime/realtime-vendor-resolution.js';
|
|
35
|
+
import { ChooseRealtimeVendorForAvatar, RealtimeModelShowsAvatar } from './realtime/realtime-video-output-gate.js';
|
|
36
|
+
import { RealtimeSessionCanShowAvatar } from './realtime/realtime-avatar-resolution.js';
|
|
35
37
|
import { RealtimeClientSessionService, WarnOnUnmatchedProviderVoice } from './realtime/realtime-client-session-service.js';
|
|
36
38
|
import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
|
|
39
|
+
import { ReadHostTools, ReadTrimmedString } from './realtime/bridge-host-params.js';
|
|
37
40
|
import { RealtimeRecordingController } from './realtime/realtime-recording-capture.js';
|
|
38
41
|
import { ResolveRecordingStorageAccountID, StoreRealtimeRecording } from './realtime/realtime-recording-store.js';
|
|
39
42
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
@@ -70,6 +73,7 @@ import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolIn
|
|
|
70
73
|
import { GetValueFromPath, SetMappedValue } from '@memberjunction/ai-core-plus';
|
|
71
74
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
72
75
|
import { ClientToolRequestManager } from './ClientToolRequestManager.js';
|
|
76
|
+
import { BuildClientToolResultMessage, ResolveClientToolResultMaxChars, ReplaceMediaBlocksWithStubs, IsMediaBlock, IMAGE_BLOCK_TOKEN_ESTIMATE } from './client-tool-results.js';
|
|
73
77
|
import { ConversationMessageResolver } from './utils/ConversationMessageResolver.js';
|
|
74
78
|
import _ from 'lodash';
|
|
75
79
|
/**
|
|
@@ -209,6 +213,13 @@ export class BaseAgent {
|
|
|
209
213
|
*/
|
|
210
214
|
this._generalValidationRetryCount = 0;
|
|
211
215
|
this._agentRun = null;
|
|
216
|
+
/**
|
|
217
|
+
* The abort controller that governs the current `Execute` call (the merged timeout/upstream
|
|
218
|
+
* signal). Held so {@link initializeAgentRun} can register it with the watchdog's stop relay:
|
|
219
|
+
* when someone marks this run's row `Cancelled`, the watchdog aborts it and the loop's
|
|
220
|
+
* existing cancellation checks end the run. Null outside an execution.
|
|
221
|
+
*/
|
|
222
|
+
this._runAbortController = null;
|
|
212
223
|
/**
|
|
213
224
|
* The task graph this run submitted and is now waiting on, or null.
|
|
214
225
|
*
|
|
@@ -316,6 +327,11 @@ export class BaseAgent {
|
|
|
316
327
|
* Armed only when the run has a conversationId (the cross-turn context gate).
|
|
317
328
|
*/
|
|
318
329
|
this._conversationToolManager = new ConversationToolManager();
|
|
330
|
+
/**
|
|
331
|
+
* The conversation's branch rows for the current run, loaded once when the run is on a
|
|
332
|
+
* branch (`ExecuteAgentParams.ConversationBranchID`). Empty on the trunk.
|
|
333
|
+
*/
|
|
334
|
+
this._conversationBranches = [];
|
|
319
335
|
/**
|
|
320
336
|
* Manages in-flight durable memory writes for the current agent run.
|
|
321
337
|
* Only consulted when the agent has AllowMemoryWrite enabled.
|
|
@@ -822,6 +838,12 @@ export class BaseAgent {
|
|
|
822
838
|
* on the right one, and neither shows up as an error.
|
|
823
839
|
*/
|
|
824
840
|
static { this.DEFAULT_ABSOLUTE_MAX_ITERATIONS = 5000; }
|
|
841
|
+
/**
|
|
842
|
+
* The prefix of the abort reason the wall-clock guard in {@link Execute} raises when a run
|
|
843
|
+
* outlives `maxExecutionTimeMs`. The guard appends the agent and the budget after it; a reader
|
|
844
|
+
* ({@link CancellationReasonForAbort}) checks the prefix, never the wording after it.
|
|
845
|
+
*/
|
|
846
|
+
static { this.AgentTimeoutAbortReason = 'Agent execution exceeded maxExecutionTimeMs'; }
|
|
825
847
|
/**
|
|
826
848
|
* The resolved FileStorageAccount ID for this agent run. Set during Execute()
|
|
827
849
|
* via the hierarchical resolution chain (Runtime → Agent → Category → Type → fallback).
|
|
@@ -1480,13 +1502,15 @@ export class BaseAgent {
|
|
|
1480
1502
|
}
|
|
1481
1503
|
const timeoutId = setTimeout(() => {
|
|
1482
1504
|
if (!timeoutController.signal.aborted) {
|
|
1483
|
-
timeoutController.abort(
|
|
1505
|
+
timeoutController.abort(`${BaseAgent.AgentTimeoutAbortReason}: agent '${params.agent.Name}' ran past ${agentTimeoutMS}ms`);
|
|
1484
1506
|
}
|
|
1485
1507
|
}, agentTimeoutMS);
|
|
1486
1508
|
// Route the merged signal back through `params` so the existing body of
|
|
1487
1509
|
// Execute (and downstream sub-agent invocations that propagate
|
|
1488
1510
|
// `cancellationToken`) observe it.
|
|
1489
1511
|
params.cancellationToken = timeoutController.signal;
|
|
1512
|
+
// Hold it for the watchdog's stop relay (registered once the run row exists).
|
|
1513
|
+
this._runAbortController = timeoutController;
|
|
1490
1514
|
try {
|
|
1491
1515
|
this.logStatus(`🤖 Starting execution of agent '${params.agent.Name}'`, true, params);
|
|
1492
1516
|
// =====================================================================================
|
|
@@ -1545,7 +1569,11 @@ export class BaseAgent {
|
|
|
1545
1569
|
// Arm conversation-history retrieval tools — available only when the run has a
|
|
1546
1570
|
// conversation to page against (the same gate as all cross-turn context features).
|
|
1547
1571
|
// A history floor holds here too: the tools page only rows from it onward.
|
|
1548
|
-
|
|
1572
|
+
// On a branch, the tools page only rows on that branch's path.
|
|
1573
|
+
this._conversationBranches = wrappedParams.conversationId && wrappedParams.ConversationBranchID
|
|
1574
|
+
? await ConversationEngine.LoadBranchesFresh(wrappedParams.conversationId, params.contextUser, this.ProviderToUse)
|
|
1575
|
+
: [];
|
|
1576
|
+
this._conversationToolManager.Initialize(wrappedParams.conversationId || null, params.contextUser, wrappedParams.ConversationHistoryFrom ?? null, wrappedParams.ConversationBranchID ?? null, this._conversationBranches);
|
|
1549
1577
|
this._conversationToolManager.SetSummaryHost(this.buildConversationSummaryHost(wrappedParams));
|
|
1550
1578
|
// Initialize artifact tools with any input artifacts attached to the run.
|
|
1551
1579
|
// Artifacts arrive as a typed first-class field on ExecuteAgentParams —
|
|
@@ -1619,6 +1647,7 @@ export class BaseAgent {
|
|
|
1619
1647
|
this._messageLifecycleCallback = params.onMessageLifecycle;
|
|
1620
1648
|
this._catalogNarrowing = undefined;
|
|
1621
1649
|
this._openingRequest = OpeningRequestText(params.conversationMessages);
|
|
1650
|
+
this._runExecutionScope = this.runPromptExecutionScope(params);
|
|
1622
1651
|
this._isOpeningTurn = IsOpeningTurn(params.conversationMessages);
|
|
1623
1652
|
// Resolve storage account for file artifacts
|
|
1624
1653
|
this._resolvedStorageAccountId = await this.getStorageAccountID(wrappedParams);
|
|
@@ -1673,7 +1702,11 @@ export class BaseAgent {
|
|
|
1673
1702
|
primaryScopeEntityId = primaryEntity.ID;
|
|
1674
1703
|
}
|
|
1675
1704
|
}
|
|
1676
|
-
|
|
1705
|
+
// The user's new message, captured by identity before anything below adds to the list, so a
|
|
1706
|
+
// resumed stopped run can be spliced in ahead of it once the parallel phase settles.
|
|
1707
|
+
const continuationMessage = wrappedParams.conversationMessages[wrappedParams.conversationMessages.length - 1];
|
|
1708
|
+
// Positions matter: [0] is the config and [5] the stopped-run continuation.
|
|
1709
|
+
const [config, , , , , stoppedRunContinuation] = await Promise.all([
|
|
1677
1710
|
this.loadAgentConfiguration(params.agent),
|
|
1678
1711
|
this.preloadAgentData(wrappedParams),
|
|
1679
1712
|
this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig),
|
|
@@ -1682,10 +1715,15 @@ export class BaseAgent {
|
|
|
1682
1715
|
// conversationId). Runs here so the results are in the messages before
|
|
1683
1716
|
// the pre-turn compaction check and the first prompt.
|
|
1684
1717
|
this.injectPriorTurnToolResults(wrappedParams),
|
|
1718
|
+
// If the previous turn was STOPPED by the user, load what it did so this turn continues
|
|
1719
|
+
// the work instead of redoing it. Applied below, after this phase settles.
|
|
1720
|
+
this.loadStoppedRunContinuation(wrappedParams),
|
|
1685
1721
|
// Decision discovery (plan Task 3.1), off unless the decisionsEnabled and decisionDiscovery prompt
|
|
1686
1722
|
// params are true: may add a <suggested_agent> system message, within DECISION_DISCOVERY_TIMEOUT_MS.
|
|
1687
1723
|
this.InjectDecisionDiscovery(params.agent, params.contextUser, wrappedParams.conversationMessages, params.data)
|
|
1688
1724
|
]);
|
|
1725
|
+
// Resume (or summarize) a run the user stopped, now that nothing else is writing the list.
|
|
1726
|
+
this.applyStoppedRunContinuation(wrappedParams, stoppedRunContinuation, continuationMessage);
|
|
1689
1727
|
// Inject scope-resolved prompt parts (role-faithful) for this agent's prompt, alongside
|
|
1690
1728
|
// memory/RAG. Synchronous — parts are cached on AIEngine. Uses the same run scope.
|
|
1691
1729
|
this.InjectScopedPromptParts(params.agent, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes);
|
|
@@ -1773,6 +1811,7 @@ export class BaseAgent {
|
|
|
1773
1811
|
// consumers that re-read `params` after the call see what they
|
|
1774
1812
|
// passed in, not our chained signal.
|
|
1775
1813
|
params.cancellationToken = upstreamToken;
|
|
1814
|
+
this._runAbortController = null;
|
|
1776
1815
|
// A cancellation, or an error thrown out of the step loop, leaves any held decision
|
|
1777
1816
|
// requests unasked. The terminate path has already logged and cleared its own.
|
|
1778
1817
|
this.skipHeldDecisions(params, this.runEndForHeldDecisions());
|
|
@@ -1951,14 +1990,26 @@ export class BaseAgent {
|
|
|
1951
1990
|
const selfNames = Array.isArray(params.data?.realtimeSelfNames)
|
|
1952
1991
|
? (params.data?.realtimeSelfNames).filter((n) => typeof n === 'string')
|
|
1953
1992
|
: undefined;
|
|
1993
|
+
const hostTools = ReadHostTools(params.data?.realtimeHostTools);
|
|
1994
|
+
// Note: resolveHostTools is an in-process callback function passed directly in params.data.
|
|
1995
|
+
// It will not survive process boundaries or cross-host job queue serialization.
|
|
1996
|
+
const resolveHostTools = typeof params.data?.resolveHostTools === 'function'
|
|
1997
|
+
? params.data.resolveHostTools
|
|
1998
|
+
: undefined;
|
|
1954
1999
|
return {
|
|
1955
2000
|
CoAgent: params.agent,
|
|
1956
2001
|
TargetAgentID: targetID,
|
|
2002
|
+
HostTools: hostTools,
|
|
2003
|
+
ResolveHostTools: resolveHostTools,
|
|
2004
|
+
HostFraming: ReadTrimmedString(params.data?.realtimeHostFraming, 'realtimeHostFraming'),
|
|
2005
|
+
PriorTranscript: ReadTrimmedString(params.data?.realtimePriorTranscript, 'realtimePriorTranscript'),
|
|
2006
|
+
ConversationID: ReadTrimmedString(params.data?.conversationId, 'conversationId'),
|
|
1957
2007
|
// The run's runtime API keys, so the bridged voice session mints on the caller's key when
|
|
1958
2008
|
// the run carries one for the realtime driver (the same run-key → platform order as
|
|
1959
2009
|
// GetAIAPIKey; realtime does not consult MJ Credentials). Absent ⇒ platform keys, as before.
|
|
1960
2010
|
// CreateBridgeRealtimeSession (the LiveKit / telephony factory) passes no apiKeys today.
|
|
1961
2011
|
APIKeys: params.apiKeys,
|
|
2012
|
+
CredentialScope: params.CredentialScope,
|
|
1962
2013
|
AgentSessionID: params.data?.agentSessionId ?? '',
|
|
1963
2014
|
PreferredModelID: modelID,
|
|
1964
2015
|
ConfigOverridesJson: BuildRealtimeOverridesJson(modelID, voice) ?? undefined,
|
|
@@ -1966,6 +2017,12 @@ export class BaseAgent {
|
|
|
1966
2017
|
UserID: params.contextUser?.ID,
|
|
1967
2018
|
DisableAutoResponse: meetingMode || undefined,
|
|
1968
2019
|
SelfNames: selfNames,
|
|
2020
|
+
// The bridge's bot can publish the agent's avatar into the room (set by the room coordinator after it asked).
|
|
2021
|
+
AvatarDelivery: params.data?.realtimeAvatarDelivery === 'room' ? 'room' : undefined,
|
|
2022
|
+
// A server-side session: without room delivery it can't show an avatar, so the model choice prefers none.
|
|
2023
|
+
ServerSide: true,
|
|
2024
|
+
// A phone call (set by the telephony hosts): the caller sees no video, so the session asks for no avatar.
|
|
2025
|
+
PhoneCall: params.data?.realtimePhoneCall === true || undefined,
|
|
1969
2026
|
// App awareness (Move 1/3/4): the app the session runs in (sources the app cascade layer +
|
|
1970
2027
|
// RelevantAgents → allowed-agent union) and the live app-context snapshot injected at mint.
|
|
1971
2028
|
// Both ride params.data, the same conduit async agents use for appContext.
|
|
@@ -1981,7 +2038,8 @@ export class BaseAgent {
|
|
|
1981
2038
|
* without provider SDKs or DB metadata.
|
|
1982
2039
|
*
|
|
1983
2040
|
* Production resolution: pick the highest-power active model of AIModelType `Realtime`; then
|
|
1984
|
-
* pick its highest-priority active vendor whose `DriverClass` has a resolvable API key
|
|
2041
|
+
* pick its highest-priority active vendor whose `DriverClass` has a resolvable API key, unless another keyed
|
|
2042
|
+
* vendor matches whether the run will show an avatar better ({@link ChooseRealtimeVendorForAvatar}); then
|
|
1985
2043
|
* instantiate the driver via the `ClassFactory`. Returns `null` (never throws) if any step
|
|
1986
2044
|
* can't be satisfied — the caller turns that into a clean FAILED result. (Per-agent realtime
|
|
1987
2045
|
* model preference can later be wired through the agent's prompt-model config, the same path
|
|
@@ -2004,32 +2062,63 @@ export class BaseAgent {
|
|
|
2004
2062
|
// candidates by priority and takes the first whose key resolves, so an organization that
|
|
2005
2063
|
// brings its own credential for a vendor we hold no platform key for now reaches that
|
|
2006
2064
|
// vendor — which is the point of bringing your own key, not a side effect of it.
|
|
2007
|
-
const resolveAPIKey = MakeAIAPIKeyResolver(params.apiKeys);
|
|
2065
|
+
const resolveAPIKey = MakeAIAPIKeyResolver(params.apiKeys, undefined, params.CredentialScope);
|
|
2066
|
+
// The vendor follows whether the run will show an avatar, by the co-agent session prep's rule: a vendor that
|
|
2067
|
+
// renders avatars (Vertex AI for Gemini 3.8 Live) only when it will, or when it is the only keyed vendor.
|
|
2068
|
+
const showsAvatar = this.realtimeRunShowsAvatar(params);
|
|
2008
2069
|
const candidates = this.selectRealtimeModelCandidates(params.agent, overrideModelID);
|
|
2009
2070
|
for (const model of candidates) {
|
|
2010
2071
|
const vendor = SelectRealtimeVendorForModel(model.ID, resolveAPIKey);
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
}
|
|
2014
|
-
const apiKey = resolveAPIKey(vendor.DriverClass);
|
|
2015
|
-
if (!apiKey) {
|
|
2072
|
+
const first = vendor ? this.instantiateRealtimeRunVendor(model.ID, vendor, resolveAPIKey) : null;
|
|
2073
|
+
if (!vendor || !first) {
|
|
2016
2074
|
continue;
|
|
2017
2075
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
}
|
|
2022
|
-
return {
|
|
2023
|
-
model: instance,
|
|
2024
|
-
modelID: model.ID,
|
|
2025
|
-
vendorID: vendor.VendorID,
|
|
2026
|
-
modelVendorID: vendor.ModelVendorID,
|
|
2027
|
-
apiName: vendor.APIName,
|
|
2028
|
-
driverClass: vendor.DriverClass
|
|
2029
|
-
};
|
|
2076
|
+
return ChooseRealtimeVendorForAvatar(vendor, first, ListRealtimeVendorsForModel(model.ID), showsAvatar, {
|
|
2077
|
+
Instantiate: (other) => this.instantiateRealtimeRunVendor(model.ID, other, resolveAPIKey),
|
|
2078
|
+
RendersAvatar: (resolution) => RealtimeModelShowsAvatar({ ModelID: resolution.modelID, APIName: resolution.apiName, Model: resolution.model }, AIEngine.Instance),
|
|
2079
|
+
});
|
|
2030
2080
|
}
|
|
2031
2081
|
return null;
|
|
2032
2082
|
}
|
|
2083
|
+
/**
|
|
2084
|
+
* Whether a session-driven run will show an avatar: the agent's video setting is on and the run could show one
|
|
2085
|
+
* ({@link RealtimeSessionCanShowAvatar}) — a server-side session, so only when its host publishes the avatar into a
|
|
2086
|
+
* room (`params.data.realtimeAvatarDelivery` `'room'`) and it is not a phone call (`params.data.realtimePhoneCall`).
|
|
2087
|
+
*
|
|
2088
|
+
* @param params The execution parameters.
|
|
2089
|
+
*/
|
|
2090
|
+
realtimeRunShowsAvatar(params) {
|
|
2091
|
+
if (this.resolveRealtimeEffectiveConfig(params.agent).realtime?.video?.enabled !== true) {
|
|
2092
|
+
return false;
|
|
2093
|
+
}
|
|
2094
|
+
return RealtimeSessionCanShowAvatar({
|
|
2095
|
+
ServerSide: true,
|
|
2096
|
+
AvatarDelivery: params.data?.realtimeAvatarDelivery === 'room' ? 'room' : undefined,
|
|
2097
|
+
PhoneCall: params.data?.realtimePhoneCall === true,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* A vendor's realtime driver on its key, with the model and vendor identifiers a run resolves to; `null` when the
|
|
2102
|
+
* key or the driver is missing.
|
|
2103
|
+
*/
|
|
2104
|
+
instantiateRealtimeRunVendor(modelID, vendor, resolveAPIKey) {
|
|
2105
|
+
const apiKey = resolveAPIKey(vendor.DriverClass);
|
|
2106
|
+
if (!apiKey) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeModel, vendor.DriverClass, apiKey);
|
|
2110
|
+
if (!instance) {
|
|
2111
|
+
return null;
|
|
2112
|
+
}
|
|
2113
|
+
return {
|
|
2114
|
+
model: instance,
|
|
2115
|
+
modelID,
|
|
2116
|
+
vendorID: vendor.VendorID,
|
|
2117
|
+
modelVendorID: vendor.ModelVendorID,
|
|
2118
|
+
apiName: vendor.APIName,
|
|
2119
|
+
driverClass: vendor.DriverClass
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2033
2122
|
/**
|
|
2034
2123
|
* The active `Realtime`-AIModelType models to try, in resolution order — the candidate list
|
|
2035
2124
|
* {@link resolveRealtimeModel} walks until one yields a usable vendor + key + driver. Returns ALL
|
|
@@ -2293,6 +2382,7 @@ export class BaseAgent {
|
|
|
2293
2382
|
parentDepth: this._depth,
|
|
2294
2383
|
configurationId: params.configurationId,
|
|
2295
2384
|
apiKeys: params.apiKeys,
|
|
2385
|
+
CredentialScope: params.CredentialScope,
|
|
2296
2386
|
data: params.data,
|
|
2297
2387
|
verbose: params.verbose,
|
|
2298
2388
|
// Progress streams BOTH to the runner's narration consumer (request.OnProgress —
|
|
@@ -3259,6 +3349,7 @@ export class BaseAgent {
|
|
|
3259
3349
|
AgentID: agent.ID,
|
|
3260
3350
|
PromptName: promptName,
|
|
3261
3351
|
CancellationToken: signal,
|
|
3352
|
+
ExecutionScope: this._runExecutionScope,
|
|
3262
3353
|
});
|
|
3263
3354
|
return { ...DecisionDiscoveryFromResult(result, options, DECISION_DISCOVERY_MIN_CONFIDENCE), ...sizes };
|
|
3264
3355
|
}
|
|
@@ -3857,15 +3948,22 @@ export class BaseAgent {
|
|
|
3857
3948
|
/**
|
|
3858
3949
|
* The `tool_choice` for this turn (§8.3).
|
|
3859
3950
|
*
|
|
3860
|
-
* `'auto'` normally
|
|
3951
|
+
* `'auto'` normally; on the last turn this run will be allowed, a forced `complete_task` call.
|
|
3861
3952
|
*
|
|
3862
3953
|
* **Why the final turn is special.** A tool call is a request to continue: the framework runs
|
|
3863
3954
|
* the action, feeds the result back, and the model decides again. On the last permitted
|
|
3864
3955
|
* iteration there is no "again" — the limit check fires the moment the turn returns, so the
|
|
3865
3956
|
* action is executed, paid for, and its result discarded, and the run ends with no answer for
|
|
3866
3957
|
* the user because the model spent its last turn asking a question instead of answering one.
|
|
3867
|
-
*
|
|
3868
|
-
*
|
|
3958
|
+
* The final turn is forced to what the framework actually needs from it: a terminal answer.
|
|
3959
|
+
*
|
|
3960
|
+
* **Why `complete_task` and not `'none'`.** Under implicit control flow, `'none'` forbids the
|
|
3961
|
+
* model's only way to store its result — `payload_change_request` and `complete_task` are tools
|
|
3962
|
+
* too — so a forced `'none'` turn can end the run but never deliver the payload. Naming
|
|
3963
|
+
* `complete_task` forces the one call that does both. A model that is NOT on implicit control
|
|
3964
|
+
* flow never receives `complete_task` (the runner strips control tools), and the runner then
|
|
3965
|
+
* downgrades the named choice to `'none'`: the envelope turn the hybrid needs. Providers also
|
|
3966
|
+
* enforce a named choice more reliably than `'none'`.
|
|
3869
3967
|
*
|
|
3870
3968
|
* **What this does NOT fix.** Models call a tool on a measurable share of turns whose right
|
|
3871
3969
|
* answer was chat, completion or delegation. Those are not predictable
|
|
@@ -3873,14 +3971,14 @@ export class BaseAgent {
|
|
|
3873
3971
|
* address them. That belongs to the prompt, and is why the native-mode Actions section names
|
|
3874
3972
|
* the cases explicitly.
|
|
3875
3973
|
*
|
|
3876
|
-
* Subclasses may narrow this further; the base contract is that a forced
|
|
3877
|
-
* relaxed to `'auto'` on a turn the framework has already decided is terminal.
|
|
3974
|
+
* Subclasses may narrow this further; the base contract is that a forced terminal choice must
|
|
3975
|
+
* never be relaxed to `'auto'` on a turn the framework has already decided is terminal.
|
|
3878
3976
|
*
|
|
3879
3977
|
* @param params The run parameters, for the per-run iteration override
|
|
3880
|
-
* @returns `'
|
|
3978
|
+
* @returns `{ name: 'complete_task' }` on the final permitted iteration, otherwise `'auto'`
|
|
3881
3979
|
*/
|
|
3882
3980
|
resolveToolChoiceForTurn(params) {
|
|
3883
|
-
return this.isFinalPermittedIteration(params) ?
|
|
3981
|
+
return this.isFinalPermittedIteration(params) ? { name: COMPLETE_TASK_TOOL } : 'auto';
|
|
3884
3982
|
}
|
|
3885
3983
|
/**
|
|
3886
3984
|
* Whether the turn currently being prepared is the last one this run will be allowed.
|
|
@@ -4061,6 +4159,7 @@ export class BaseAgent {
|
|
|
4061
4159
|
if (params.apiKeys && params.apiKeys.length > 0) {
|
|
4062
4160
|
childPromptParams.apiKeys = params.apiKeys;
|
|
4063
4161
|
}
|
|
4162
|
+
childPromptParams.CredentialScope = params.CredentialScope;
|
|
4064
4163
|
// Pass through configurationId to both parent and child prompts if provided
|
|
4065
4164
|
if (params.configurationId) {
|
|
4066
4165
|
promptParams.configurationId = params.configurationId;
|
|
@@ -4096,6 +4195,10 @@ export class BaseAgent {
|
|
|
4096
4195
|
promptParams.apiKeys = params.apiKeys;
|
|
4097
4196
|
this.logStatus(`🔑 Using ${params.apiKeys.length} API key(s) provided at runtime`, true, params);
|
|
4098
4197
|
}
|
|
4198
|
+
promptParams.CredentialScope = params.CredentialScope;
|
|
4199
|
+
if (!CredentialScopeAllows(params.CredentialScope, 'Environment')) {
|
|
4200
|
+
this.logStatus(`🔒 Credential scope is ${params.CredentialScope}: the platform's environment API keys will not be used`, true, params);
|
|
4201
|
+
}
|
|
4099
4202
|
// Thread the per-request provider so prompt run records are saved through the isolated provider
|
|
4100
4203
|
promptParams.provider = params.provider || this._activeProvider;
|
|
4101
4204
|
// Overlay scope-resolved run settings (model / vendor / configuration / effort / sampling
|
|
@@ -5168,6 +5271,7 @@ export class BaseAgent {
|
|
|
5168
5271
|
MaxAttempts: row.MaxSelfCheckAttempts ?? null,
|
|
5169
5272
|
rubricId: String(row.RubricID),
|
|
5170
5273
|
passThreshold: row.PassThreshold ?? null,
|
|
5274
|
+
evaluatorConfig: row.EvaluatorConfig ?? null,
|
|
5171
5275
|
};
|
|
5172
5276
|
const attempt = (this._selfCheckAttemptsByRun.get(agentRun.ID) ?? 0) + 1;
|
|
5173
5277
|
this._selfCheckAttemptsByRun.set(agentRun.ID, attempt);
|
|
@@ -5182,7 +5286,7 @@ export class BaseAgent {
|
|
|
5182
5286
|
};
|
|
5183
5287
|
try {
|
|
5184
5288
|
const outcome = await ExecuteSelfCheck({
|
|
5185
|
-
engine: ProviderRubricEngine(provider, params.contextUser),
|
|
5289
|
+
engine: ProviderRubricEngine(provider, params.contextUser, this.runPromptExecutionScope(params)),
|
|
5186
5290
|
link,
|
|
5187
5291
|
runId: agentRun.ID,
|
|
5188
5292
|
agentKind: this.AgentTypeInstance?.constructor?.name === 'LoopAgentType' ? 'loop' : 'flow',
|
|
@@ -5577,23 +5681,15 @@ export class BaseAgent {
|
|
|
5577
5681
|
}
|
|
5578
5682
|
/**
|
|
5579
5683
|
* Converts ChatMessageContent to a string representation.
|
|
5580
|
-
* Handles both simple strings and content block arrays.
|
|
5684
|
+
* Handles both simple strings and content block arrays. Text blocks keep their text;
|
|
5685
|
+
* image, audio, video and file blocks become a short stub instead of their base64.
|
|
5581
5686
|
*
|
|
5582
5687
|
* @param content - The message content to convert
|
|
5583
5688
|
* @returns String representation of the content
|
|
5584
5689
|
* @protected
|
|
5585
5690
|
*/
|
|
5586
5691
|
contentToString(content) {
|
|
5587
|
-
|
|
5588
|
-
return content;
|
|
5589
|
-
}
|
|
5590
|
-
// Content is an array of blocks - convert to string
|
|
5591
|
-
return content.map((block) => {
|
|
5592
|
-
if (block.type === 'text') {
|
|
5593
|
-
return block.content;
|
|
5594
|
-
}
|
|
5595
|
-
return `[${block.type}: ${block.content}]`;
|
|
5596
|
-
}).join('\n');
|
|
5692
|
+
return ReplaceMediaBlocksWithStubs(content);
|
|
5597
5693
|
}
|
|
5598
5694
|
/**
|
|
5599
5695
|
* Smart trimming of message content based on detected format.
|
|
@@ -5865,9 +5961,7 @@ export class BaseAgent {
|
|
|
5865
5961
|
break;
|
|
5866
5962
|
const originalTokens = candidate.tokens;
|
|
5867
5963
|
const originalMessage = candidate.message;
|
|
5868
|
-
const originalContent =
|
|
5869
|
-
? originalMessage.content
|
|
5870
|
-
: JSON.stringify(originalMessage.content);
|
|
5964
|
+
const originalContent = ReplaceMediaBlocksWithStubs(originalMessage.content);
|
|
5871
5965
|
if (originalMessage.role === 'tool') {
|
|
5872
5966
|
// compact each tool_result block's text; the block structure is what the provider needs.
|
|
5873
5967
|
const compacted = CompactToolResultContent(originalMessage, (t) => (t.length > 500 ? `${t.slice(0, 500)}… [compacted from ${t.length} chars]` : t));
|
|
@@ -5941,9 +6035,7 @@ export class BaseAgent {
|
|
|
5941
6035
|
break;
|
|
5942
6036
|
const originalTokens = candidate.tokens;
|
|
5943
6037
|
const originalMessage = candidate.message;
|
|
5944
|
-
const originalContent =
|
|
5945
|
-
? originalMessage.content
|
|
5946
|
-
: JSON.stringify(originalMessage.content);
|
|
6038
|
+
const originalContent = ReplaceMediaBlocksWithStubs(originalMessage.content);
|
|
5947
6039
|
// Aggressive compaction - keep only first 200 chars
|
|
5948
6040
|
const compactedContent = await this.compactMessage(originalMessage, {
|
|
5949
6041
|
compactMode: 'First N Chars',
|
|
@@ -6133,14 +6225,24 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6133
6225
|
this.appendNativeAssistantTurn(params, guardrailCheckedStep);
|
|
6134
6226
|
// if we need to retry make sure we add the retry message to the conversation messages
|
|
6135
6227
|
if (guardrailCheckedStep.step === 'Retry' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
6136
|
-
//
|
|
6228
|
+
// A payload-only turn, or a complete_task that failed Success validation, is answered as a
|
|
6229
|
+
// tool result for that call — the feedback is what the model reads next.
|
|
6137
6230
|
params.conversationMessages.push(BuildToolResultTurn([{
|
|
6138
6231
|
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
6139
|
-
toolName:
|
|
6232
|
+
toolName: this.nativeToolNameForCall(guardrailCheckedStep, guardrailCheckedStep.payloadToolCallId),
|
|
6140
6233
|
content: guardrailCheckedStep.retryInstructions || 'Payload change applied.',
|
|
6141
6234
|
isError: false
|
|
6142
6235
|
}], { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
6143
6236
|
}
|
|
6237
|
+
else if (guardrailCheckedStep.step === 'Success' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
6238
|
+
// complete_task ended the run: answer the call so the history never ends on a dangling tool_use.
|
|
6239
|
+
params.conversationMessages.push(BuildToolResultTurn([{
|
|
6240
|
+
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
6241
|
+
toolName: this.nativeToolNameForCall(guardrailCheckedStep, guardrailCheckedStep.payloadToolCallId),
|
|
6242
|
+
content: 'Task complete.',
|
|
6243
|
+
isError: false
|
|
6244
|
+
}], { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
6245
|
+
}
|
|
6144
6246
|
else if (guardrailCheckedStep.step === 'Retry' && (guardrailCheckedStep.message || guardrailCheckedStep.errorMessage || guardrailCheckedStep.retryInstructions)) {
|
|
6145
6247
|
params.conversationMessages.push({
|
|
6146
6248
|
role: 'user',
|
|
@@ -6150,6 +6252,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6150
6252
|
// Return the next step directly - execution handling is done in execute NextStep
|
|
6151
6253
|
return guardrailCheckedStep;
|
|
6152
6254
|
}
|
|
6255
|
+
/** The tool name the model used for a call on this turn — `payload_change_request` when it cannot be found. */
|
|
6256
|
+
nativeToolNameForCall(step, toolCallId) {
|
|
6257
|
+
return step.nativeTurn?.toolCalls.find((c) => c.id === toolCallId)?.name ?? 'payload_change_request';
|
|
6258
|
+
}
|
|
6153
6259
|
/**
|
|
6154
6260
|
* Executes a batch of artifact tool calls, recording each as its own
|
|
6155
6261
|
* `Tool` AIAgentRunStep (a sibling of the Prompt step that requested them)
|
|
@@ -6219,20 +6325,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6219
6325
|
if (!body) {
|
|
6220
6326
|
return;
|
|
6221
6327
|
}
|
|
6222
|
-
|
|
6223
|
-
role: 'user',
|
|
6224
|
-
content: body,
|
|
6225
|
-
metadata: {
|
|
6226
|
-
turnAdded: 0,
|
|
6227
|
-
messageType: BaseAgent.toolResultMessageType,
|
|
6228
|
-
expirationTurns: 2,
|
|
6229
|
-
expirationMode: 'Compact',
|
|
6230
|
-
compactMode: 'First N Chars',
|
|
6231
|
-
compactLength: 500,
|
|
6232
|
-
compactPromptId: '',
|
|
6233
|
-
},
|
|
6234
|
-
};
|
|
6235
|
-
params.conversationMessages.push(message);
|
|
6328
|
+
params.conversationMessages.push(this.buildCarryForwardMessage(body));
|
|
6236
6329
|
this.logStatus(`[PriorTurnToolResults] Carried ${steps.length} tool result(s) forward from the previous run`, true, params);
|
|
6237
6330
|
}
|
|
6238
6331
|
catch (error) {
|
|
@@ -6240,6 +6333,504 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6240
6333
|
this.logStatus(`[PriorTurnToolResults] Skipped (contained error): ${error instanceof Error ? error.message : error}`, true, params);
|
|
6241
6334
|
}
|
|
6242
6335
|
}
|
|
6336
|
+
/**
|
|
6337
|
+
* Carries a STOPPED previous turn's completed results forward into this run's context.
|
|
6338
|
+
*
|
|
6339
|
+
* {@link injectPriorTurnToolResults} covers the normal case: the previous run settled and
|
|
6340
|
+
* its inline tool results ride along one turn. A run the user stopped never settles, so
|
|
6341
|
+
* that path skips it, and the next turn would redo every action the stopped run had already
|
|
6342
|
+
* finished (observed: a stopped Sage run had five weather lookups and fifteen searches done;
|
|
6343
|
+
* "keep going" redid all of them). This path looks at the newest root run for this agent in
|
|
6344
|
+
* the conversation and, when it is one the user stopped, carries it forward per
|
|
6345
|
+
* {@link ExecuteAgentParams.stopContinuationMode}: by default its own message stack is resumed
|
|
6346
|
+
* so the agent picks up where it stopped; in summary mode its completed Actions and Tool results
|
|
6347
|
+
* go in as one transient message that says to continue, not restart. A settled newest run means
|
|
6348
|
+
* the normal path already has the context, so nothing is added here. During execution the two
|
|
6349
|
+
* halves run separately: {@link loadStoppedRunContinuation} in the parallel phase, then
|
|
6350
|
+
* {@link applyStoppedRunContinuation} once it settles.
|
|
6351
|
+
*
|
|
6352
|
+
* Same gates as the sibling: root depth, a conversation, no history floor. Fail-soft.
|
|
6353
|
+
* @protected
|
|
6354
|
+
*/
|
|
6355
|
+
async injectStoppedRunResults(params) {
|
|
6356
|
+
const continuationMessage = params.conversationMessages[params.conversationMessages.length - 1];
|
|
6357
|
+
const continuation = await this.loadStoppedRunContinuation(params);
|
|
6358
|
+
this.applyStoppedRunContinuation(params, continuation, continuationMessage);
|
|
6359
|
+
}
|
|
6360
|
+
/**
|
|
6361
|
+
* The I/O half of {@link injectStoppedRunResults}: finds a user-stopped predecessor and loads its
|
|
6362
|
+
* steps (every step for the full-stack resume, only completed results for the summary). Runs inside
|
|
6363
|
+
* the pre-execution parallel phase; nothing here touches the message list.
|
|
6364
|
+
* @protected
|
|
6365
|
+
*/
|
|
6366
|
+
async loadStoppedRunContinuation(params) {
|
|
6367
|
+
if (!params.conversationId || this._depth !== 0 || params.ConversationHistoryFrom) {
|
|
6368
|
+
return null;
|
|
6369
|
+
}
|
|
6370
|
+
try {
|
|
6371
|
+
// A caller that chained this run (the run resolver, after its own stopped-predecessor
|
|
6372
|
+
// lookup, or a client continuing a run) already named the predecessor: check that one
|
|
6373
|
+
// row rather than searching the conversation again. Only an unchained run searches.
|
|
6374
|
+
const runId = params.lastRunId
|
|
6375
|
+
? await this.userStoppedRunIdIfStopped(params.lastRunId, params)
|
|
6376
|
+
: await this.findUserStoppedPredecessorRunId(params);
|
|
6377
|
+
if (!runId) {
|
|
6378
|
+
return null;
|
|
6379
|
+
}
|
|
6380
|
+
const mode = params.stopContinuationMode ?? BaseAgent.DefaultStopContinuationMode;
|
|
6381
|
+
const steps = mode === 'full_stack'
|
|
6382
|
+
? await this.loadStoppedRunStack(runId, params)
|
|
6383
|
+
: await this.loadStoppedRunResultSteps(runId, params);
|
|
6384
|
+
return { RunID: runId, Mode: mode, Steps: steps };
|
|
6385
|
+
}
|
|
6386
|
+
catch (error) {
|
|
6387
|
+
this.logStatus(`[StoppedRunResults] Skipped (contained error): ${error instanceof Error ? error.message : error}`, true, params);
|
|
6388
|
+
return null;
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
/**
|
|
6392
|
+
* The message half of {@link injectStoppedRunResults}, applied once the pre-execution phase has
|
|
6393
|
+
* settled so it never races the memory and RAG injections running beside the load.
|
|
6394
|
+
*
|
|
6395
|
+
* Full stack: the stopped run's own turns are spliced in ahead of the user's new message, in the
|
|
6396
|
+
* shape the loop built while running, so the agent resumes where it stopped. When the stack cannot
|
|
6397
|
+
* be resumed (no recorded prompt, or its anchor is missing) this falls back to the summary.
|
|
6398
|
+
* Summary: the completed results go in as one carried-forward message. Fail-soft.
|
|
6399
|
+
* @protected
|
|
6400
|
+
*/
|
|
6401
|
+
applyStoppedRunContinuation(params, continuation, continuationMessage) {
|
|
6402
|
+
if (!continuation) {
|
|
6403
|
+
return;
|
|
6404
|
+
}
|
|
6405
|
+
try {
|
|
6406
|
+
if (continuation.Mode === 'full_stack' && this.tryResumeStoppedRunStack(params, continuation, continuationMessage)) {
|
|
6407
|
+
return;
|
|
6408
|
+
}
|
|
6409
|
+
const steps = continuation.Steps.filter(BaseAgent.isCarriedForwardResultStep);
|
|
6410
|
+
const body = BaseAgent.BuildStoppedRunResultsMessage(steps, BaseAgent.maxStoppedRunResultChars);
|
|
6411
|
+
if (!body) {
|
|
6412
|
+
return;
|
|
6413
|
+
}
|
|
6414
|
+
params.conversationMessages.push(this.buildCarryForwardMessage(body));
|
|
6415
|
+
this.logStatus(`[StoppedRunResults] Carried ${steps.length} completed step result(s) forward from stopped run ${continuation.RunID}`, true, params);
|
|
6416
|
+
}
|
|
6417
|
+
catch (error) {
|
|
6418
|
+
this.logStatus(`[StoppedRunResults] Skipped (contained error): ${error instanceof Error ? error.message : error}`, true, params);
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
/**
|
|
6422
|
+
* The full-stack half of {@link applyStoppedRunContinuation}: true when the stopped run's turns were
|
|
6423
|
+
* spliced in. Any failure leaves the messages untouched and returns false, so the caller carries
|
|
6424
|
+
* the results as a summary instead of losing them.
|
|
6425
|
+
* @private
|
|
6426
|
+
*/
|
|
6427
|
+
tryResumeStoppedRunStack(params, continuation, continuationMessage) {
|
|
6428
|
+
try {
|
|
6429
|
+
const resumed = this.buildResumedStoppedRunTurns(continuation.Steps, params);
|
|
6430
|
+
if (resumed && BaseAgent.SpliceResumedTurns(params.conversationMessages, resumed, continuationMessage)) {
|
|
6431
|
+
this.logStatus(`[StoppedRunResume] Resumed ${resumed.length} message(s) from stopped run ${continuation.RunID}`, true, params);
|
|
6432
|
+
return true;
|
|
6433
|
+
}
|
|
6434
|
+
this.logStatus(`[StoppedRunResume] Stack of stopped run ${continuation.RunID} could not be resumed; carrying its results as a summary`, true, params);
|
|
6435
|
+
}
|
|
6436
|
+
catch (error) {
|
|
6437
|
+
this.logStatus(`[StoppedRunResume] Resume failed (${error instanceof Error ? error.message : error}); carrying its results as a summary`, true, params);
|
|
6438
|
+
}
|
|
6439
|
+
return false;
|
|
6440
|
+
}
|
|
6441
|
+
/** The mode a run uses when the caller does not set {@link ExecuteAgentParams.stopContinuationMode}. */
|
|
6442
|
+
static { this.DefaultStopContinuationMode = 'full_stack'; }
|
|
6443
|
+
/**
|
|
6444
|
+
* Placed just before the user's new message when a stopped run's stack is resumed, so the model
|
|
6445
|
+
* knows the turns above it were interrupted and what follows is how to continue.
|
|
6446
|
+
*/
|
|
6447
|
+
static { this.StoppedRunResumeNotice = '[Note: The user stopped the previous execution at this point. The instruction that follows is the continuation request.]'; }
|
|
6448
|
+
/** Result code and message for an action the stopped run requested but never ran. */
|
|
6449
|
+
static { this.stoppedBeforeRunResultCode = 'NOT_EXECUTED'; }
|
|
6450
|
+
static { this.stoppedBeforeRunMessage = 'Not executed: the user stopped the run before this action finished.'; }
|
|
6451
|
+
/**
|
|
6452
|
+
* The stopped run's turns in the shape the loop builds, or null when they cannot be recovered.
|
|
6453
|
+
*
|
|
6454
|
+
* The last prompt step recorded the run's whole message stack as it went into that prompt; the
|
|
6455
|
+
* first prompt step recorded the stack the run started from (history plus this run's injections,
|
|
6456
|
+
* which the new run adds for itself). The difference is the run's own turns. If the last prompt
|
|
6457
|
+
* completed, its decision and the steps after it are rebuilt with the loop's own builders.
|
|
6458
|
+
* @protected
|
|
6459
|
+
*/
|
|
6460
|
+
buildResumedStoppedRunTurns(steps, params) {
|
|
6461
|
+
const prompts = steps.filter(s => s.StepType === 'Prompt');
|
|
6462
|
+
if (prompts.length === 0) {
|
|
6463
|
+
return null;
|
|
6464
|
+
}
|
|
6465
|
+
const lastPrompt = prompts[prompts.length - 1];
|
|
6466
|
+
const loopTurns = BaseAgent.ExtractStoppedRunLoopTurns(BaseAgent.readPromptStepStack(prompts[0]), BaseAgent.readPromptStepStack(lastPrompt));
|
|
6467
|
+
if (!loopTurns) {
|
|
6468
|
+
return null;
|
|
6469
|
+
}
|
|
6470
|
+
const decision = lastPrompt.Status === 'Completed' ? BaseAgent.readPromptStepDecision(lastPrompt) : null;
|
|
6471
|
+
const tail = this.buildStoppedRunTail(decision, steps.filter(s => s.StepNumber > lastPrompt.StepNumber), params);
|
|
6472
|
+
return [...loopTurns, ...tail].map(BaseAgent.restampResumedTurn);
|
|
6473
|
+
}
|
|
6474
|
+
/**
|
|
6475
|
+
* The run's own turns: what follows the run's starting message in its last recorded stack, minus
|
|
6476
|
+
* the per-request runtime-state fragments (the new run appends its own). The starting message is
|
|
6477
|
+
* matched at its original position first, so a repeated message earlier in the history ("keep
|
|
6478
|
+
* going" twice) cannot be mistaken for it. Null when it cannot be found.
|
|
6479
|
+
*/
|
|
6480
|
+
static ExtractStoppedRunLoopTurns(firstStack, lastStack) {
|
|
6481
|
+
if (!firstStack?.length || !lastStack?.length) {
|
|
6482
|
+
return null;
|
|
6483
|
+
}
|
|
6484
|
+
const key = (m) => `${m.role}\u0000${JSON.stringify(m.content)}`;
|
|
6485
|
+
const startKey = key(firstStack[firstStack.length - 1]);
|
|
6486
|
+
const expectedAt = firstStack.length - 1;
|
|
6487
|
+
const at = lastStack[expectedAt] && key(lastStack[expectedAt]) === startKey
|
|
6488
|
+
? expectedAt
|
|
6489
|
+
: lastStack.map(key).lastIndexOf(startKey);
|
|
6490
|
+
if (at < 0) {
|
|
6491
|
+
return null;
|
|
6492
|
+
}
|
|
6493
|
+
return lastStack.slice(at + 1).filter(m => m.metadata?.volatileState !== true);
|
|
6494
|
+
}
|
|
6495
|
+
/**
|
|
6496
|
+
* Inserts resumed turns ahead of the user's new message (and ahead of the stopped run's own reply
|
|
6497
|
+
* to the user, when that sits right before it), followed by {@link StoppedRunResumeNotice}. The new
|
|
6498
|
+
* message is found by identity, captured before the pre-execution phase added anything. Returns
|
|
6499
|
+
* false, changing nothing, when it is not a user message still in the list.
|
|
6500
|
+
*/
|
|
6501
|
+
static SpliceResumedTurns(messages, resumed, continuationMessage) {
|
|
6502
|
+
const at = continuationMessage ? messages.indexOf(continuationMessage) : -1;
|
|
6503
|
+
if (at < 0 || continuationMessage?.role !== 'user') {
|
|
6504
|
+
return false;
|
|
6505
|
+
}
|
|
6506
|
+
const notice = { role: 'user', content: BaseAgent.StoppedRunResumeNotice };
|
|
6507
|
+
const previous = messages[at - 1];
|
|
6508
|
+
const stoppedReplyAt = previous?.role === 'assistant' && !previous.toolCalls?.length ? at - 1 : at;
|
|
6509
|
+
messages.splice(at, 0, notice);
|
|
6510
|
+
messages.splice(stoppedReplyAt, 0, ...resumed);
|
|
6511
|
+
return true;
|
|
6512
|
+
}
|
|
6513
|
+
/**
|
|
6514
|
+
* What the stopped run's last completed prompt decided, rebuilt as the loop would have written it:
|
|
6515
|
+
* the model's native call turn when results go back natively, the invocation record and the results
|
|
6516
|
+
* of the actions that finished (and a not-executed result for the ones that did not) for an Actions
|
|
6517
|
+
* turn, otherwise the completed results as a carried-forward message. Any native call left without
|
|
6518
|
+
* a result is then answered, so the history is valid for every provider.
|
|
6519
|
+
* @protected
|
|
6520
|
+
*/
|
|
6521
|
+
buildStoppedRunTail(decision, tailSteps, params) {
|
|
6522
|
+
const tail = [];
|
|
6523
|
+
if (!decision) {
|
|
6524
|
+
return tail;
|
|
6525
|
+
}
|
|
6526
|
+
const scratch = { ...params, conversationMessages: tail };
|
|
6527
|
+
const native = decision.nativeTurn?.sendResultsNatively === true;
|
|
6528
|
+
if (native && decision.nativeTurn) {
|
|
6529
|
+
tail.push(BuildAssistantToolCallTurn(decision.nativeTurn));
|
|
6530
|
+
}
|
|
6531
|
+
if (decision.step === 'Actions' && decision.actions?.length) {
|
|
6532
|
+
if (!native) {
|
|
6533
|
+
tail.push({ role: 'user', content: this.buildActionInvocationMessage(decision.actions) });
|
|
6534
|
+
}
|
|
6535
|
+
const summaries = this.summarizeStoppedRunActions(decision.actions, tailSteps);
|
|
6536
|
+
this.appendActionResults(scratch, summaries, this.buildActionResultsMessage(summaries), undefined, decision);
|
|
6537
|
+
}
|
|
6538
|
+
else {
|
|
6539
|
+
const results = BaseAgent.BuildStoppedRunResultsMessage(tailSteps.filter(BaseAgent.isCarriedForwardResultStep), BaseAgent.maxStoppedRunResultChars);
|
|
6540
|
+
if (results) {
|
|
6541
|
+
tail.push(this.buildCarryForwardMessage(results));
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
this.reconcileUnansweredToolCalls(scratch);
|
|
6545
|
+
return tail;
|
|
6546
|
+
}
|
|
6547
|
+
/** One result per requested action, in request order: its completed step's result, or not executed. @private */
|
|
6548
|
+
summarizeStoppedRunActions(requested, tailSteps) {
|
|
6549
|
+
const completed = tailSteps.filter(s => s.StepType === 'Actions' && s.Status === 'Completed');
|
|
6550
|
+
const used = new Set();
|
|
6551
|
+
return requested.map(action => {
|
|
6552
|
+
const step = completed.find(s => !used.has(s) && BaseAgent.readStoppedActionName(s) === action.name);
|
|
6553
|
+
const summary = step ? this.summarizeStoppedActionStep(action.name, step) : null;
|
|
6554
|
+
if (!step || !summary) {
|
|
6555
|
+
return {
|
|
6556
|
+
actionName: action.name,
|
|
6557
|
+
success: false,
|
|
6558
|
+
params: [],
|
|
6559
|
+
resultCode: BaseAgent.stoppedBeforeRunResultCode,
|
|
6560
|
+
message: BaseAgent.stoppedBeforeRunMessage,
|
|
6561
|
+
};
|
|
6562
|
+
}
|
|
6563
|
+
used.add(step);
|
|
6564
|
+
return summary;
|
|
6565
|
+
});
|
|
6566
|
+
}
|
|
6567
|
+
/**
|
|
6568
|
+
* A completed action step as the loop summarizes a live action result: output parameters only,
|
|
6569
|
+
* with large media swapped for placeholders. Null when the step's output cannot be read.
|
|
6570
|
+
* @private
|
|
6571
|
+
*/
|
|
6572
|
+
summarizeStoppedActionStep(actionName, step) {
|
|
6573
|
+
let output;
|
|
6574
|
+
try {
|
|
6575
|
+
output = step.OutputData ? JSON.parse(step.OutputData) : {};
|
|
6576
|
+
}
|
|
6577
|
+
catch {
|
|
6578
|
+
return null;
|
|
6579
|
+
}
|
|
6580
|
+
const result = output.actionResult;
|
|
6581
|
+
if (!result) {
|
|
6582
|
+
return null;
|
|
6583
|
+
}
|
|
6584
|
+
const success = result.success === true;
|
|
6585
|
+
const outputParams = (result.parameters ?? []).filter(p => p.Type === 'Both' || p.Type === 'Output');
|
|
6586
|
+
const actionEntity = ActionEngineServer.Instance.Actions?.find(a => a.Name === actionName);
|
|
6587
|
+
return {
|
|
6588
|
+
actionName,
|
|
6589
|
+
success,
|
|
6590
|
+
params: this.interceptLargeBinaryContent(outputParams, actionEntity),
|
|
6591
|
+
resultCode: result.resultCode || (success ? 'SUCCESS' : 'ERROR'),
|
|
6592
|
+
message: result.message || (success ? 'Action completed' : 'Unknown error'),
|
|
6593
|
+
};
|
|
6594
|
+
}
|
|
6595
|
+
/** The message stack a prompt step recorded as its input, or null. @private */
|
|
6596
|
+
static readPromptStepStack(step) {
|
|
6597
|
+
try {
|
|
6598
|
+
const input = step.InputData ? JSON.parse(step.InputData) : {};
|
|
6599
|
+
return Array.isArray(input.conversationMessages) ? input.conversationMessages : null;
|
|
6600
|
+
}
|
|
6601
|
+
catch {
|
|
6602
|
+
return null;
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
/** The decision a completed prompt step recorded, or null. @private */
|
|
6606
|
+
static readPromptStepDecision(step) {
|
|
6607
|
+
try {
|
|
6608
|
+
const output = step.OutputData ? JSON.parse(step.OutputData) : {};
|
|
6609
|
+
return output.nextStep ?? null;
|
|
6610
|
+
}
|
|
6611
|
+
catch {
|
|
6612
|
+
return null;
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
/** The action an Actions step ran, from its recorded input, or null. @private */
|
|
6616
|
+
static readStoppedActionName(step) {
|
|
6617
|
+
try {
|
|
6618
|
+
const input = step.InputData ? JSON.parse(step.InputData) : {};
|
|
6619
|
+
return input.actionName ?? null;
|
|
6620
|
+
}
|
|
6621
|
+
catch {
|
|
6622
|
+
return null;
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
/**
|
|
6626
|
+
* A resumed turn re-dated to this run's first turn. The stopped run's turn numbers mean nothing
|
|
6627
|
+
* to this run's counter, and expiry compares the two, so a resumed result ages from now.
|
|
6628
|
+
* @private
|
|
6629
|
+
*/
|
|
6630
|
+
static restampResumedTurn(message) {
|
|
6631
|
+
return message.metadata?.turnAdded !== undefined
|
|
6632
|
+
? { ...message, metadata: { ...message.metadata, turnAdded: 0 } }
|
|
6633
|
+
: message;
|
|
6634
|
+
}
|
|
6635
|
+
/** A step whose result the summary carries: a completed, successful Actions or Tool step. @private */
|
|
6636
|
+
static isCarriedForwardResultStep(step) {
|
|
6637
|
+
const types = BaseAgent.stoppedRunCarryForwardPredicate.stepTypes;
|
|
6638
|
+
return types.includes(step.StepType) &&
|
|
6639
|
+
step.Status === BaseAgent.stoppedRunCarryForwardPredicate.stepStatus &&
|
|
6640
|
+
step.Success === true;
|
|
6641
|
+
}
|
|
6642
|
+
/**
|
|
6643
|
+
* A carried-forward results message: a user-role message that expires after two turns and
|
|
6644
|
+
* compacts to its head, so results reused once never compound across the conversation. Shared
|
|
6645
|
+
* by the settled-run and stopped-run carry-forwards so the two cannot age differently.
|
|
6646
|
+
* @private
|
|
6647
|
+
*/
|
|
6648
|
+
buildCarryForwardMessage(content) {
|
|
6649
|
+
return {
|
|
6650
|
+
role: 'user',
|
|
6651
|
+
content,
|
|
6652
|
+
metadata: {
|
|
6653
|
+
turnAdded: 0,
|
|
6654
|
+
messageType: BaseAgent.toolResultMessageType,
|
|
6655
|
+
expirationTurns: 2,
|
|
6656
|
+
expirationMode: 'Compact',
|
|
6657
|
+
compactMode: 'First N Chars',
|
|
6658
|
+
compactLength: 500,
|
|
6659
|
+
compactPromptId: '',
|
|
6660
|
+
},
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
/**
|
|
6664
|
+
* The ID of this agent's newest root run on this turn's branch path IF the user stopped it,
|
|
6665
|
+
* else null. One query over the runs that could be the predecessor (settled, or stopped by
|
|
6666
|
+
* the user): the newest decides. A stopped run older than a settled one is history the
|
|
6667
|
+
* settled run already built on, so it is not carried again. The path scopes it the same way
|
|
6668
|
+
* as {@link loadPriorTurnToolResultSteps}: a run stopped on a sibling branch is never resumed.
|
|
6669
|
+
* @private
|
|
6670
|
+
*/
|
|
6671
|
+
async findUserStoppedPredecessorRunId(params) {
|
|
6672
|
+
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
6673
|
+
const newest = await rv.RunView({
|
|
6674
|
+
EntityName: 'MJ: AI Agent Runs',
|
|
6675
|
+
ExtraFilter: MJAIAgentRunEntityExtended.BuildStoppedPredecessorFilter(params.conversationId, params.agent.ID, params.ConversationBranchID ?? null, this._conversationBranches),
|
|
6676
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
6677
|
+
MaxRows: 1,
|
|
6678
|
+
Fields: ['ID', 'Status', 'CancellationReason'],
|
|
6679
|
+
ResultType: 'simple',
|
|
6680
|
+
// The row this looks for was written by the Stop button moments ago; read the database, not a cache.
|
|
6681
|
+
BypassCache: true,
|
|
6682
|
+
}, params.contextUser);
|
|
6683
|
+
const run = newest.Success ? newest.Results?.[0] : undefined;
|
|
6684
|
+
return run && MJAIAgentRunEntityExtended.IsUserStopped(run.Status, run.CancellationReason) ? run.ID : null;
|
|
6685
|
+
}
|
|
6686
|
+
/**
|
|
6687
|
+
* `runId` if that run is one the user stopped, else null. The chained-run counterpart of
|
|
6688
|
+
* {@link findUserStoppedPredecessorRunId}: one primary-key read of the named run instead of a
|
|
6689
|
+
* search of the conversation, so a run the resolver already resolved is not looked up twice,
|
|
6690
|
+
* and a run chained to a settled predecessor (an ordinary continuation) resumes nothing.
|
|
6691
|
+
* @private
|
|
6692
|
+
*/
|
|
6693
|
+
async userStoppedRunIdIfStopped(runId, params) {
|
|
6694
|
+
if (!IsValidUUID(runId)) {
|
|
6695
|
+
return null;
|
|
6696
|
+
}
|
|
6697
|
+
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
6698
|
+
const result = await rv.RunView({
|
|
6699
|
+
EntityName: 'MJ: AI Agent Runs',
|
|
6700
|
+
ExtraFilter: `ID='${runId}'`,
|
|
6701
|
+
MaxRows: 1,
|
|
6702
|
+
Fields: ['ID', 'Status', 'CancellationReason'],
|
|
6703
|
+
ResultType: 'simple',
|
|
6704
|
+
// The row this checks was written by the Stop button moments ago; read the database, not a cache.
|
|
6705
|
+
BypassCache: true,
|
|
6706
|
+
}, params.contextUser);
|
|
6707
|
+
const run = result.Success ? result.Results?.[0] : undefined;
|
|
6708
|
+
return run && MJAIAgentRunEntityExtended.IsUserStopped(run.Status, run.CancellationReason) ? run.ID : null;
|
|
6709
|
+
}
|
|
6710
|
+
/**
|
|
6711
|
+
* Every step of a stopped run, in step order: the full-stack resume reads the prompt steps'
|
|
6712
|
+
* recorded stacks and which requested actions completed. Read from the database, since the run
|
|
6713
|
+
* was stopped moments ago.
|
|
6714
|
+
* @private
|
|
6715
|
+
*/
|
|
6716
|
+
async loadStoppedRunStack(runId, params) {
|
|
6717
|
+
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
6718
|
+
const steps = await rv.RunView({
|
|
6719
|
+
EntityName: 'MJ: AI Agent Run Steps',
|
|
6720
|
+
ExtraFilter: `AgentRunID='${runId}'`,
|
|
6721
|
+
OrderBy: 'StepNumber ASC',
|
|
6722
|
+
Fields: ['StepNumber', 'StepType', 'StepName', 'Status', 'Success', 'InputData', 'OutputData'],
|
|
6723
|
+
ResultType: 'simple',
|
|
6724
|
+
BypassCache: true,
|
|
6725
|
+
}, params.contextUser);
|
|
6726
|
+
return steps.Success ? (steps.Results || []) : [];
|
|
6727
|
+
}
|
|
6728
|
+
/** The completed, successful Actions and Tool steps of a stopped run, in step order. @private */
|
|
6729
|
+
async loadStoppedRunResultSteps(runId, params) {
|
|
6730
|
+
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
6731
|
+
const steps = await rv.RunView({
|
|
6732
|
+
EntityName: 'MJ: AI Agent Run Steps',
|
|
6733
|
+
ExtraFilter: `AgentRunID='${runId}' AND StepType IN (${BaseAgent.stoppedRunCarryForwardPredicate.stepTypes.map(t => `'${t}'`).join(', ')}) ` +
|
|
6734
|
+
`AND Status='${BaseAgent.stoppedRunCarryForwardPredicate.stepStatus}' AND Success=1`,
|
|
6735
|
+
OrderBy: 'StepNumber ASC',
|
|
6736
|
+
Fields: ['StepNumber', 'StepType', 'StepName', 'Status', 'Success', 'InputData', 'OutputData'],
|
|
6737
|
+
ResultType: 'simple',
|
|
6738
|
+
BypassCache: true,
|
|
6739
|
+
}, params.contextUser);
|
|
6740
|
+
return steps.Success ? (steps.Results || []) : [];
|
|
6741
|
+
}
|
|
6742
|
+
/**
|
|
6743
|
+
* Budget for the stopped-run carry-forward message. Smaller than a standalone tool result on
|
|
6744
|
+
* purpose: this rides at the front of a continuation turn's context, and a stopped run can
|
|
6745
|
+
* hold dozens of search results. ~7k tokens keeps the useful values and drops the long tails.
|
|
6746
|
+
*/
|
|
6747
|
+
static { this.maxStoppedRunResultChars = 28_000; }
|
|
6748
|
+
/**
|
|
6749
|
+
* Renders a stopped run's completed step results into the carried-forward message body.
|
|
6750
|
+
* Pure and static for testability. An Actions step renders as the action call (name and
|
|
6751
|
+
* params) with its output parameters, or its message when it produced none; a Tool step
|
|
6752
|
+
* renders under the same contract as {@link BuildPriorTurnToolResultsMessage}. Failed or
|
|
6753
|
+
* unparseable steps are skipped. Each section is capped at an even share of `maxChars`
|
|
6754
|
+
* (never below a readable floor) and the whole body at `maxChars`, with a note for anything
|
|
6755
|
+
* dropped. Returns null when nothing usable remains.
|
|
6756
|
+
*/
|
|
6757
|
+
static BuildStoppedRunResultsMessage(steps, maxChars) {
|
|
6758
|
+
const sections = [];
|
|
6759
|
+
const perSectionMax = Math.max(BaseAgent.minStoppedRunSectionChars, Math.floor(maxChars / Math.max(steps.length, 1)));
|
|
6760
|
+
let usedChars = 0;
|
|
6761
|
+
let dropped = 0;
|
|
6762
|
+
for (const step of steps) {
|
|
6763
|
+
const section = BaseAgent.renderStoppedRunStep(step);
|
|
6764
|
+
if (!section) {
|
|
6765
|
+
continue;
|
|
6766
|
+
}
|
|
6767
|
+
const capped = section.length > perSectionMax
|
|
6768
|
+
? `${section.slice(0, perSectionMax)}\n[truncated]`
|
|
6769
|
+
: section;
|
|
6770
|
+
if (usedChars + capped.length > maxChars && sections.length > 0) {
|
|
6771
|
+
dropped++;
|
|
6772
|
+
continue;
|
|
6773
|
+
}
|
|
6774
|
+
usedChars += capped.length;
|
|
6775
|
+
sections.push(capped);
|
|
6776
|
+
}
|
|
6777
|
+
if (sections.length === 0) {
|
|
6778
|
+
return null;
|
|
6779
|
+
}
|
|
6780
|
+
const header = 'Your previous turn was stopped by the user before it finished. The actions and tools listed below ' +
|
|
6781
|
+
'were ALREADY EXECUTED in that turn and their results are still valid. Treat each entry as a result you already ' +
|
|
6782
|
+
'have: do not run the action or call a tool again for it, and do not invoke the action names below as tools. ' +
|
|
6783
|
+
'Continue from where the stopped turn left off rather than starting over.';
|
|
6784
|
+
const droppedNote = dropped > 0 ? `\n\n[${dropped} additional result(s) omitted for size; re-call those if needed]` : '';
|
|
6785
|
+
return `${header}\n${sections.join('\n\n')}${droppedNote}`;
|
|
6786
|
+
}
|
|
6787
|
+
/** One stopped-run step as a result section, or null when it carries nothing reusable. @private */
|
|
6788
|
+
static renderStoppedRunStep(step) {
|
|
6789
|
+
if (step.StepType === 'Tool') {
|
|
6790
|
+
return BaseAgent.BuildPriorTurnToolResultsMessage([{ OutputData: step.OutputData }], Number.MAX_SAFE_INTEGER)
|
|
6791
|
+
?.split('\n').slice(1).join('\n') || null; // the sibling renderer's body without its header line
|
|
6792
|
+
}
|
|
6793
|
+
if (step.StepType !== 'Actions' || !step.InputData || !step.OutputData) {
|
|
6794
|
+
return null;
|
|
6795
|
+
}
|
|
6796
|
+
let input;
|
|
6797
|
+
let output;
|
|
6798
|
+
try {
|
|
6799
|
+
input = JSON.parse(step.InputData);
|
|
6800
|
+
output = JSON.parse(step.OutputData);
|
|
6801
|
+
}
|
|
6802
|
+
catch {
|
|
6803
|
+
return null;
|
|
6804
|
+
}
|
|
6805
|
+
const result = output.actionResult;
|
|
6806
|
+
if (!input.actionName || result?.success !== true) {
|
|
6807
|
+
return null;
|
|
6808
|
+
}
|
|
6809
|
+
const outputs = {};
|
|
6810
|
+
for (const param of result.parameters ?? []) {
|
|
6811
|
+
if (param.Type === BaseAgent.actionOutputParamType && param.Value !== undefined && param.Value !== null) {
|
|
6812
|
+
outputs[param.Name] = param.Value;
|
|
6813
|
+
}
|
|
6814
|
+
}
|
|
6815
|
+
const body = Object.keys(outputs).length > 0 ? RenderToolResultData(outputs) : (result.message ?? '');
|
|
6816
|
+
if (!body) {
|
|
6817
|
+
return null;
|
|
6818
|
+
}
|
|
6819
|
+
// Deliberately NOT the inline-tool heading (`### tool({...})`): an agent shown that shape for an
|
|
6820
|
+
// action tried to call the action as a conversation tool. Say what it is in words.
|
|
6821
|
+
return `### Already executed: action "${input.actionName}" with params ${JSON.stringify(input.actionParams ?? {})}\n` +
|
|
6822
|
+
`Result:\n\`\`\`json\n${body}\n\`\`\``;
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* The `MJ: AI Agent Runs` predicate for this agent's previous settled root run on one
|
|
6826
|
+
* branch path: the run's reply row must be on that path, so a run made on a sibling
|
|
6827
|
+
* branch is never carried forward, and not replaced by a rerun of its turn, so a replaced
|
|
6828
|
+
* answer's tool results are never carried forward.
|
|
6829
|
+
*/
|
|
6830
|
+
static BuildPriorTurnRunFilter(conversationId, agentId, branchId, branches, statusList) {
|
|
6831
|
+
return `ConversationID='${conversationId}' AND Status IN (${statusList}) AND ParentRunID IS NULL AND AgentID='${agentId}'`
|
|
6832
|
+
+ ` AND ${MJAIAgentRunEntityExtended.BuildReplyOnBranchPathFilter(conversationId, branchId, branches)}`;
|
|
6833
|
+
}
|
|
6243
6834
|
/**
|
|
6244
6835
|
* Loads this agent's previous settled root run's Tool steps for this conversation
|
|
6245
6836
|
* (settled = {@link settledRunStatuses}: Completed or AwaitingFeedback).
|
|
@@ -6255,7 +6846,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6255
6846
|
* @private
|
|
6256
6847
|
*/
|
|
6257
6848
|
async loadPriorTurnToolResultSteps(params) {
|
|
6258
|
-
const
|
|
6849
|
+
const branchId = params.ConversationBranchID ?? null;
|
|
6850
|
+
const cached = PriorTurnToolResultCache.Instance.Get(params.conversationId, params.agent.ID, branchId);
|
|
6259
6851
|
if (cached) {
|
|
6260
6852
|
this.logStatus(`[PriorTurnToolResults] Prior-run tool results served from cache (${cached.length} step(s), no DB lookup)`, true, params);
|
|
6261
6853
|
return cached;
|
|
@@ -6264,10 +6856,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6264
6856
|
const statusList = predicate.runStatuses.map(s => `'${s}'`).join(', ');
|
|
6265
6857
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
6266
6858
|
// AgentID scopes provenance: in a multi-agent conversation, agent B must never
|
|
6267
|
-
// inherit agent A's results labeled "your previous turn".
|
|
6859
|
+
// inherit agent A's results labeled "your previous turn". The branch path scopes it
|
|
6860
|
+
// the same way: a run on a sibling branch is never carried forward.
|
|
6268
6861
|
const priorRun = await rv.RunView({
|
|
6269
6862
|
EntityName: 'MJ: AI Agent Runs',
|
|
6270
|
-
ExtraFilter:
|
|
6863
|
+
ExtraFilter: BaseAgent.BuildPriorTurnRunFilter(params.conversationId, params.agent.ID, branchId, this._conversationBranches, statusList),
|
|
6271
6864
|
OrderBy: '__mj_CreatedAt DESC',
|
|
6272
6865
|
MaxRows: 1,
|
|
6273
6866
|
Fields: ['ID'],
|
|
@@ -6293,9 +6886,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6293
6886
|
* only when the run row settled as {@link settledRunStatuses} (Completed OR
|
|
6294
6887
|
* AwaitingFeedback — the normal chat-turn ending) — a failed run leaves the previous
|
|
6295
6888
|
* settled run's entry standing, just as the RunView filter would. Scoped to this
|
|
6296
|
-
* run's agent (cache key = conversation + agent) so parallel
|
|
6297
|
-
* conversation never cross-pollinate. An empty
|
|
6298
|
-
* negative-cache case that spares tool-free
|
|
6889
|
+
* run's agent and branch (cache key = conversation + agent + branch) so parallel
|
|
6890
|
+
* agents in one conversation, and sibling branches, never cross-pollinate. An empty
|
|
6891
|
+
* projection is cached too (the negative-cache case that spares tool-free
|
|
6892
|
+
* conversations the queries every turn). The run's reply row is stored with the entry,
|
|
6893
|
+
* so the entry is no longer served once a rerun of its turn replaces that row.
|
|
6299
6894
|
* Same-node edge semantics (failed step INSERTs, concurrent completions) are
|
|
6300
6895
|
* documented on the cache class. Called from {@link finalizeAgentRun}.
|
|
6301
6896
|
* @private
|
|
@@ -6310,7 +6905,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6310
6905
|
const records = (this._agentRun.Steps || [])
|
|
6311
6906
|
.filter(s => s.StepType === predicate.stepType && s.Status === predicate.stepStatus)
|
|
6312
6907
|
.map(s => ({ OutputData: s.OutputData || null }));
|
|
6313
|
-
PriorTurnToolResultCache.Instance.Set(conversationId, this._agentRun.AgentID, records);
|
|
6908
|
+
PriorTurnToolResultCache.Instance.Set(conversationId, this._agentRun.AgentID, this._executeParams?.ConversationBranchID ?? null, records, this._agentRun.ConversationDetailID ?? null);
|
|
6314
6909
|
}
|
|
6315
6910
|
/**
|
|
6316
6911
|
* Tool families whose step results are eligible for prior-turn carry-forward —
|
|
@@ -6336,7 +6931,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6336
6931
|
* ({@link carryForwardPredicate} → DB filter + cache-population gate) and the
|
|
6337
6932
|
* post-turn compaction gate ({@link startPostTurnCompaction}).
|
|
6338
6933
|
*/
|
|
6339
|
-
static { this.settledRunStatuses =
|
|
6934
|
+
static { this.settledRunStatuses = MJAIAgentRunEntityExtended.SettledStatuses; }
|
|
6340
6935
|
/**
|
|
6341
6936
|
* The carry-forward row predicate — the SINGLE source shared by the two places that
|
|
6342
6937
|
* must select the same rows or the cache diverges from the DB path: the RunView
|
|
@@ -6354,6 +6949,19 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6354
6949
|
stepStatus: 'Completed',
|
|
6355
6950
|
runStatuses: BaseAgent.settledRunStatuses,
|
|
6356
6951
|
}; }
|
|
6952
|
+
/**
|
|
6953
|
+
* The row predicate for the stopped-run carry-forward's step query: the completed, successful
|
|
6954
|
+
* Actions and Tool steps of the run the user stopped. Declared beside
|
|
6955
|
+
* {@link carryForwardPredicate} for the same reason it is: one place to change what counts.
|
|
6956
|
+
*/
|
|
6957
|
+
static { this.stoppedRunCarryForwardPredicate = {
|
|
6958
|
+
stepTypes: ['Actions', 'Tool'],
|
|
6959
|
+
stepStatus: 'Completed',
|
|
6960
|
+
}; }
|
|
6961
|
+
/** An action parameter's direction, as the action executor stamps it. */
|
|
6962
|
+
static { this.actionOutputParamType = 'Output'; }
|
|
6963
|
+
/** A stopped-run result section is never cut below this many characters, whatever the budget. */
|
|
6964
|
+
static { this.minStoppedRunSectionChars = 1_500; }
|
|
6357
6965
|
/**
|
|
6358
6966
|
* Display name of the seeded system prompt behind summarizeRange's recursive
|
|
6359
6967
|
* sub-call (see metadata/prompts/.summarize-range-prompt.json). Resolved with a
|
|
@@ -6444,10 +7052,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6444
7052
|
throw new Error(`The '${BaseAgent.SummarizeRangePromptName}' system prompt is not present in this environment`);
|
|
6445
7053
|
}
|
|
6446
7054
|
const promptParams = new AIPromptParams();
|
|
7055
|
+
Object.assign(promptParams, this.runPromptExecutionScope(params));
|
|
6447
7056
|
promptParams.prompt = prompt;
|
|
6448
7057
|
// Keys are the summarize-range.template.md contract ({{ lens }}, {{ messages }})
|
|
6449
7058
|
promptParams.data = { lens, messages: rangeText };
|
|
6450
|
-
promptParams.contextUser = params.contextUser;
|
|
6451
7059
|
promptParams.agentId = params.agent.ID;
|
|
6452
7060
|
promptParams.UserID = ResolvePromptRunUserID({
|
|
6453
7061
|
UserID: params.userId,
|
|
@@ -7093,6 +7701,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7093
7701
|
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
7094
7702
|
PromptName: promptName,
|
|
7095
7703
|
CancellationToken: params.cancellationToken,
|
|
7704
|
+
ExecutionScope: this.runPromptExecutionScope(params),
|
|
7096
7705
|
});
|
|
7097
7706
|
}
|
|
7098
7707
|
/** Asks the questions once, against the request's state. */
|
|
@@ -7637,6 +8246,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7637
8246
|
AgentID: agent.ID,
|
|
7638
8247
|
PromptName: promptName,
|
|
7639
8248
|
CancellationToken: controller.signal,
|
|
8249
|
+
ExecutionScope: this._runExecutionScope,
|
|
7640
8250
|
});
|
|
7641
8251
|
const result = await Promise.race([ask, stopped]);
|
|
7642
8252
|
if (result === stoppedResult) {
|
|
@@ -7749,13 +8359,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7749
8359
|
*/
|
|
7750
8360
|
buildAgentBaseCatalog(agent, engine) {
|
|
7751
8361
|
// Resolve sub-agents: direct ParentID children + active relationships, de-duped, ordered.
|
|
8362
|
+
// Only Active agents are kept, so a Disabled relationship target never reaches the catalog.
|
|
7752
8363
|
const activeSubAgents = engine.Agents.filter(a => UUIDsEqual(a.ParentID, agent.ID) && a.Status === 'Active')
|
|
7753
8364
|
.sort((a, b) => a.ExecutionOrder - b.ExecutionOrder);
|
|
7754
8365
|
const activeAgentRelationships = engine.AgentRelationships.filter(ar => UUIDsEqual(ar.AgentID, agent.ID) && ar.Status === 'Active');
|
|
7755
8366
|
const uniqueActiveSubAgentIDs = new Set();
|
|
7756
8367
|
activeSubAgents.forEach(a => uniqueActiveSubAgentIDs.add(a.ID));
|
|
7757
8368
|
activeAgentRelationships.forEach(ar => uniqueActiveSubAgentIDs.add(ar.SubAgentID));
|
|
7758
|
-
const uniqueActiveSubAgents = Array.from(uniqueActiveSubAgentIDs)
|
|
8369
|
+
const uniqueActiveSubAgents = Array.from(uniqueActiveSubAgentIDs)
|
|
8370
|
+
.map(id => engine.Agents.find(a => UUIDsEqual(a.ID, id)))
|
|
8371
|
+
.filter((a) => !!a && a.Status === 'Active');
|
|
7759
8372
|
// Resolve actions from the agent's active AIAgentAction junctions.
|
|
7760
8373
|
const agentActions = engine.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, agent.ID) && aa.Status === 'Active');
|
|
7761
8374
|
const baseActionsRaw = ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)));
|
|
@@ -8179,7 +8792,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8179
8792
|
ContextUser: params.contextUser,
|
|
8180
8793
|
AgentID: params.agent?.ID,
|
|
8181
8794
|
PromptName: settings.PromptName,
|
|
8182
|
-
CancellationToken: params.cancellationToken
|
|
8795
|
+
CancellationToken: params.cancellationToken,
|
|
8796
|
+
ExecutionScope: this.runPromptExecutionScope(params)
|
|
8183
8797
|
});
|
|
8184
8798
|
this.attachDecisionPromptRun(step, result);
|
|
8185
8799
|
if (!result.success) {
|
|
@@ -8332,7 +8946,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8332
8946
|
ChangeReasoning: nextStep.payloadChangeRequest?.reasoning,
|
|
8333
8947
|
Message: response.Message,
|
|
8334
8948
|
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
8335
|
-
CancellationToken: params.cancellationToken
|
|
8949
|
+
CancellationToken: params.cancellationToken,
|
|
8950
|
+
ExecutionScope: this.runPromptExecutionScope(params)
|
|
8336
8951
|
};
|
|
8337
8952
|
}
|
|
8338
8953
|
/**
|
|
@@ -8371,12 +8986,30 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8371
8986
|
};
|
|
8372
8987
|
params.conversationMessages.push(message);
|
|
8373
8988
|
}
|
|
8989
|
+
/**
|
|
8990
|
+
* The {@link AIPromptExecutionScope} of model work this run starts outside the agent's own turn —
|
|
8991
|
+
* summarizing a range, compacting a message or the conversation, decision calls (FinishIf,
|
|
8992
|
+
* discovery, catalog narrowing, decision requests, the payload change check) and self-check
|
|
8993
|
+
* rubrics. Each built its params with only `contextUser`, so it ran on the platform's keys and
|
|
8994
|
+
* default configuration inside a run on a customer's keys, and under a `'RuntimeOnly'` scope
|
|
8995
|
+
* would have bypassed it.
|
|
8996
|
+
*/
|
|
8997
|
+
runPromptExecutionScope(params) {
|
|
8998
|
+
return {
|
|
8999
|
+
contextUser: params.contextUser,
|
|
9000
|
+
provider: params.provider || this._activeProvider,
|
|
9001
|
+
configurationId: params.configurationId,
|
|
9002
|
+
apiKeys: params.apiKeys,
|
|
9003
|
+
CredentialScope: params.CredentialScope,
|
|
9004
|
+
};
|
|
9005
|
+
}
|
|
8374
9006
|
/**
|
|
8375
9007
|
* Whether THIS action may use the run's runtime API key for THIS driver class. The default is
|
|
8376
9008
|
* yes: the run was started on those keys, and an action that calls a vendor on the user's behalf
|
|
8377
9009
|
* (Generate Image) is doing what the prompts do. Override to narrow it — an agent that knows
|
|
8378
9010
|
* which of its actions talk to which vendor can refuse everything else, and a refusal costs the
|
|
8379
9011
|
* action nothing but the customer's key: it falls back to the platform key as if the run had none.
|
|
9012
|
+
* Under a `'RuntimeOnly'` credential scope a refusal leaves the action with no key for that class.
|
|
8380
9013
|
*/
|
|
8381
9014
|
actionMayUseRuntimeAPIKey(action, driverClass, params) {
|
|
8382
9015
|
return true;
|
|
@@ -8389,12 +9022,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8389
9022
|
buildRuntimeAPIKeyResolver(params, actionEntity) {
|
|
8390
9023
|
const runKeys = params.apiKeys;
|
|
8391
9024
|
return (driverClass) => {
|
|
9025
|
+
const platformAllowed = CredentialScopeAllows(params.CredentialScope, 'Environment');
|
|
8392
9026
|
if (!this.actionMayUseRuntimeAPIKey(actionEntity, driverClass, params)) {
|
|
8393
|
-
this.logStatus(`🔑 Runtime API key for '${driverClass}' refused to action '${actionEntity.Name}' by policy — platform key applies`, true, params);
|
|
9027
|
+
this.logStatus(`🔑 Runtime API key for '${driverClass}' refused to action '${actionEntity.Name}' by policy — ${platformAllowed ? 'platform key applies' : `credential scope is ${params.CredentialScope}, so no key applies`}`, true, params);
|
|
8394
9028
|
return undefined;
|
|
8395
9029
|
}
|
|
8396
|
-
const key = GetAIAPIKey(driverClass, runKeys);
|
|
8397
|
-
|
|
9030
|
+
const key = GetAIAPIKey(driverClass, runKeys, false, params.CredentialScope);
|
|
9031
|
+
const source = runKeys?.some((k) => k.driverClass === driverClass) ? 'run' : platformAllowed ? 'platform' : `none — credential scope is ${params.CredentialScope}`;
|
|
9032
|
+
this.logStatus(`🔑 Action '${actionEntity.Name}' resolved an API key for '${driverClass}' (${source})`, true, params);
|
|
8398
9033
|
return key || undefined;
|
|
8399
9034
|
};
|
|
8400
9035
|
}
|
|
@@ -8461,8 +9096,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8461
9096
|
// shared by every action in the run (parallel ones included) and copied into sub-agent
|
|
8462
9097
|
// runs, so anything stamped there would name the wrong action under parallel dispatch
|
|
8463
9098
|
// and travel further than the action it was meant for. Absent when the run has no keys,
|
|
8464
|
-
// so the action uses GetAIAPIKey(driverClass) exactly as before
|
|
9099
|
+
// so the action uses GetAIAPIKey(driverClass) exactly as before — unless the scope below
|
|
9100
|
+
// is RuntimeOnly, which tells the action the resolver's answer (or its absence) is final.
|
|
8465
9101
|
RuntimeAPIKeyResolver: params.apiKeys && params.apiKeys.length > 0 ? this.buildRuntimeAPIKeyResolver(params, actionEntity) : undefined,
|
|
9102
|
+
// AICredentialScope → actions-base's RuntimeCredentialScope mirror: a value added to the
|
|
9103
|
+
// former and not the latter fails to compile here, so the two cannot drift apart.
|
|
9104
|
+
CredentialScope: params.CredentialScope,
|
|
8466
9105
|
});
|
|
8467
9106
|
if (result.Success) {
|
|
8468
9107
|
this.logStatus(` ✅ Action '${action.name}' completed successfully`, true, params);
|
|
@@ -8713,6 +9352,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8713
9352
|
configurationId: params.configurationId, // propagate configuration ID to sub-agent
|
|
8714
9353
|
effortLevel: params.effortLevel, // propagate effort level to sub-agent
|
|
8715
9354
|
apiKeys: params.apiKeys, // propagate API keys to sub-agent
|
|
9355
|
+
CredentialScope: params.CredentialScope, // a sub-agent may not spend keys its parent could not
|
|
8716
9356
|
inputArtifacts: params.inputArtifacts, // propagate input artifacts so sub-agents inherit the parent's artifact manifest + tools (e.g. a Codesmith delegate can read a Data Snapshot the parent references)
|
|
8717
9357
|
data: {
|
|
8718
9358
|
...params.data,
|
|
@@ -8902,7 +9542,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8902
9542
|
lines.push('records. When you choose client tools, set nextStep.type to "ClientTools".');
|
|
8903
9543
|
lines.push('');
|
|
8904
9544
|
lines.push('NOTE: Do NOT use client tools for asking the user questions or collecting input.');
|
|
8905
|
-
lines.push('Use the "Chat" step for that. Client tools
|
|
9545
|
+
lines.push('Use the "Chat" step for that. Client tools read and change the user\'s UI; some return an image.');
|
|
8906
9546
|
lines.push('');
|
|
8907
9547
|
for (const tool of tools) {
|
|
8908
9548
|
const categoryTag = tool.Category ? ` [${tool.Category}]` : '';
|
|
@@ -8995,6 +9635,44 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8995
9635
|
const desc = param.Description ? ` — ${param.Description}` : '';
|
|
8996
9636
|
return `\`${param.Name}\`${requiredMarker}${suffix}${desc}${defaultStr}`;
|
|
8997
9637
|
}
|
|
9638
|
+
/**
|
|
9639
|
+
* The `user`-role record of what the model asked to run on an Actions turn ("[You invoked ...]").
|
|
9640
|
+
* Shared by the live Actions step and the stopped-run resume, so a resumed turn reads exactly
|
|
9641
|
+
* like one the loop wrote. See the Actions step for why it is a user-role annotation.
|
|
9642
|
+
* @protected
|
|
9643
|
+
*/
|
|
9644
|
+
buildActionInvocationMessage(actions) {
|
|
9645
|
+
if (actions.length === 1) {
|
|
9646
|
+
const aa = actions[0];
|
|
9647
|
+
if (!aa.params || Object.keys(aa.params).length === 0) {
|
|
9648
|
+
return `[You invoked the **${aa.name}** action.]`;
|
|
9649
|
+
}
|
|
9650
|
+
const paramsList = Object.entries(aa.params)
|
|
9651
|
+
.map(([key, value]) => `• **${key}**: ${this.formatParamValueForMessage(value)}`)
|
|
9652
|
+
.join('\n');
|
|
9653
|
+
return `[You invoked the **${aa.name}** action with parameters:\n${paramsList}\n]`;
|
|
9654
|
+
}
|
|
9655
|
+
return `[You invoked **${actions.length} actions** in parallel:\n\n` + actions.map((aa, index) => {
|
|
9656
|
+
const actionText = `${index + 1}. **${aa.name}**`;
|
|
9657
|
+
if (!aa.params || Object.keys(aa.params).length === 0) {
|
|
9658
|
+
return actionText;
|
|
9659
|
+
}
|
|
9660
|
+
const paramsList = Object.entries(aa.params)
|
|
9661
|
+
.map(([key, value]) => ` • **${key}**: ${this.formatParamValueForMessage(value)}`)
|
|
9662
|
+
.join('\n');
|
|
9663
|
+
return `${actionText}\n${paramsList}`;
|
|
9664
|
+
}).join('\n\n') + '\n]';
|
|
9665
|
+
}
|
|
9666
|
+
/**
|
|
9667
|
+
* The action-results message body: a header naming any failures, then each result in the
|
|
9668
|
+
* compact markdown shape. Shared by the live Actions step and the stopped-run resume.
|
|
9669
|
+
* @protected
|
|
9670
|
+
*/
|
|
9671
|
+
buildActionResultsMessage(actionSummaries) {
|
|
9672
|
+
const failed = actionSummaries.filter(a => !a.success).length;
|
|
9673
|
+
const header = failed > 0 ? `${failed} of ${actionSummaries.length} action(s) failed:` : 'Action results:';
|
|
9674
|
+
return `${header}\n${this.formatActionResultsAsMarkdown(actionSummaries)}`;
|
|
9675
|
+
}
|
|
8998
9676
|
/**
|
|
8999
9677
|
* Formats an array of action result summaries as compact markdown instead of
|
|
9000
9678
|
* pretty-printed JSON. This typically saves 60-70 % of the tokens that the
|
|
@@ -9364,6 +10042,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9364
10042
|
// For now, returning a placeholder
|
|
9365
10043
|
return 'General';
|
|
9366
10044
|
}
|
|
10045
|
+
/**
|
|
10046
|
+
* Throws unless `lastRunId` is absent or a UUID. The value can come from a client, and the run
|
|
10047
|
+
* initialization places it in `ExtraFilter` clauses (the payload lookup, {@link validateRunChain}
|
|
10048
|
+
* and {@link resolvePlanModeGate}).
|
|
10049
|
+
*/
|
|
10050
|
+
assertLastRunIdIsUUID(lastRunId) {
|
|
10051
|
+
if (lastRunId && !IsValidUUID(lastRunId)) {
|
|
10052
|
+
throw new Error('Invalid lastRunId: expected the UUID of a prior AI Agent Run.');
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
9367
10055
|
/**
|
|
9368
10056
|
* Initializes the agent run tracking by creating MJAIAgentRunEntityExtended and setting up context.
|
|
9369
10057
|
*
|
|
@@ -9371,6 +10059,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9371
10059
|
* @param {ExecuteAgentParams} params - The execution parameters
|
|
9372
10060
|
*/
|
|
9373
10061
|
async initializeAgentRun(params) {
|
|
10062
|
+
this.assertLastRunIdIsUUID(params.lastRunId);
|
|
9374
10063
|
// Handle autoPopulateLastRunPayload if requested
|
|
9375
10064
|
let modifiedParams = params;
|
|
9376
10065
|
if (params.lastRunId && params.autoPopulateLastRunPayload) {
|
|
@@ -9379,7 +10068,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9379
10068
|
const lastRunResult = await rv.RunView({
|
|
9380
10069
|
EntityName: 'MJ: AI Agent Runs',
|
|
9381
10070
|
ExtraFilter: `ID='${params.lastRunId}'`,
|
|
9382
|
-
ResultType: 'simple' // Avoid recursive loading
|
|
10071
|
+
ResultType: 'simple', // Avoid recursive loading
|
|
10072
|
+
// A stopped predecessor wrote its FinalPayload moments ago; read the database, not a cache.
|
|
10073
|
+
BypassCache: true
|
|
9383
10074
|
}, params.contextUser);
|
|
9384
10075
|
if (lastRunResult.Success && lastRunResult.Results.length > 0) {
|
|
9385
10076
|
const lastRun = lastRunResult.Results[0];
|
|
@@ -9540,7 +10231,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9540
10231
|
// server-side DB provider can heartbeat via SQL; client/non-DB providers simply opt out.
|
|
9541
10232
|
const runProvider = params.provider || this._activeProvider;
|
|
9542
10233
|
if (runProvider instanceof DatabaseProviderBase && params.contextUser) {
|
|
9543
|
-
AgentRunWatchdog.Instance.Track(this._agentRun.ID, runProvider, params.contextUser);
|
|
10234
|
+
AgentRunWatchdog.Instance.Track(this._agentRun.ID, runProvider, params.contextUser, this._runAbortController ?? undefined);
|
|
9544
10235
|
}
|
|
9545
10236
|
// Invoke callback if provided
|
|
9546
10237
|
if (modifiedParams.onAgentRunCreated) {
|
|
@@ -11150,8 +11841,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11150
11841
|
* by one in-flight sub-agent would race the others' reads.
|
|
11151
11842
|
*
|
|
11152
11843
|
* Uses `structuredClone` (Node 17+) where available; falls back to a JSON
|
|
11153
|
-
* round-trip for environments without it
|
|
11154
|
-
*
|
|
11844
|
+
* round-trip (`ToPlainJSON`) for environments without it AND when
|
|
11845
|
+
* `structuredClone` throws (a Proxy such as a live JSONType `<Field>Object`
|
|
11846
|
+
* view, or functions). Returns the original value — with an error logged —
|
|
11847
|
+
* only when even the JSON clone fails.
|
|
11155
11848
|
*
|
|
11156
11849
|
* **JSON fallback caveats** — the round-trip is *not* shape-preserving:
|
|
11157
11850
|
* - `Date` → ISO string
|
|
@@ -11173,13 +11866,22 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
11173
11866
|
return payload;
|
|
11174
11867
|
if (typeof payload !== 'object')
|
|
11175
11868
|
return payload;
|
|
11176
|
-
|
|
11177
|
-
|
|
11869
|
+
if (typeof globalThis.structuredClone === 'function') {
|
|
11870
|
+
try {
|
|
11178
11871
|
return globalThis.structuredClone(payload);
|
|
11179
11872
|
}
|
|
11180
|
-
|
|
11873
|
+
catch {
|
|
11874
|
+
// DataCloneError — typically a Proxy (a live JSONType `<Field>Object` view) or an
|
|
11875
|
+
// object holding functions. Fall through to the JSON clone below; returning the
|
|
11876
|
+
// original here would silently give the sub-agent the caller's LIVE object and lose
|
|
11877
|
+
// payload isolation.
|
|
11878
|
+
}
|
|
11181
11879
|
}
|
|
11182
|
-
|
|
11880
|
+
try {
|
|
11881
|
+
return ToPlainJSON(payload);
|
|
11882
|
+
}
|
|
11883
|
+
catch (error) {
|
|
11884
|
+
LogError(`BaseAgent.cloneSubAgentPayload: payload could not be cloned (${error instanceof Error ? error.message : String(error)}); sub-agent will share the caller's object`);
|
|
11183
11885
|
return payload;
|
|
11184
11886
|
}
|
|
11185
11887
|
}
|
|
@@ -12046,40 +12748,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12046
12748
|
// which (pre-guardrail) looped forever. Phrasing it in second person under the `user`
|
|
12047
12749
|
// role keeps the memory while removing the false assistant-prose exemplar. The
|
|
12048
12750
|
// human-facing narration is emitted separately via onProgress above.
|
|
12049
|
-
|
|
12050
|
-
if (actions.length === 1) {
|
|
12051
|
-
const aa = actions[0];
|
|
12052
|
-
actionMessage = `[You invoked the **${aa.name}** action`;
|
|
12053
|
-
// Add parameters if they exist
|
|
12054
|
-
if (aa.params && Object.keys(aa.params).length > 0) {
|
|
12055
|
-
const paramsList = Object.entries(aa.params)
|
|
12056
|
-
.map(([key, value]) => {
|
|
12057
|
-
const displayValue = this.formatParamValueForMessage(value);
|
|
12058
|
-
return `• **${key}**: ${displayValue}`;
|
|
12059
|
-
})
|
|
12060
|
-
.join('\n');
|
|
12061
|
-
actionMessage += ` with parameters:\n${paramsList}\n]`;
|
|
12062
|
-
}
|
|
12063
|
-
else {
|
|
12064
|
-
actionMessage += '.]';
|
|
12065
|
-
}
|
|
12066
|
-
}
|
|
12067
|
-
else {
|
|
12068
|
-
actionMessage = `[You invoked **${actions.length} actions** in parallel:\n\n` + actions.map((aa, index) => {
|
|
12069
|
-
let actionText = `${index + 1}. **${aa.name}**`;
|
|
12070
|
-
// Add parameters if they exist
|
|
12071
|
-
if (aa.params && Object.keys(aa.params).length > 0) {
|
|
12072
|
-
const paramsList = Object.entries(aa.params)
|
|
12073
|
-
.map(([key, value]) => {
|
|
12074
|
-
const displayValue = this.formatParamValueForMessage(value);
|
|
12075
|
-
return ` • **${key}**: ${displayValue}`;
|
|
12076
|
-
})
|
|
12077
|
-
.join('\n');
|
|
12078
|
-
actionText += `\n${paramsList}`;
|
|
12079
|
-
}
|
|
12080
|
-
return actionText;
|
|
12081
|
-
}).join('\n\n') + '\n]';
|
|
12082
|
-
}
|
|
12751
|
+
const actionMessage = this.buildActionInvocationMessage(actions);
|
|
12083
12752
|
if (addConversationMessage) {
|
|
12084
12753
|
// Record as a `user`-role environment annotation (no metadata - permanent record).
|
|
12085
12754
|
// See the note above on why this is NOT an `assistant` turn.
|
|
@@ -12198,10 +12867,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12198
12867
|
// Check if any actions failed
|
|
12199
12868
|
const failedActions = actionSummaries.filter(a => !a.success);
|
|
12200
12869
|
// Add user message with the results — compact markdown instead of JSON
|
|
12201
|
-
const
|
|
12202
|
-
? `${failedActions.length} of ${actionSummaries.length} action(s) failed:`
|
|
12203
|
-
: `Action results:`;
|
|
12204
|
-
const resultsMessage = `${header}\n${this.formatActionResultsAsMarkdown(actionSummaries)}`;
|
|
12870
|
+
const resultsMessage = this.buildActionResultsMessage(actionSummaries);
|
|
12205
12871
|
// Build metadata from AI Agent Actions configuration
|
|
12206
12872
|
// If multiple actions, use the most restrictive (shortest) expiration settings
|
|
12207
12873
|
let metadata;
|
|
@@ -12403,24 +13069,20 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12403
13069
|
});
|
|
12404
13070
|
const timeoutMs = tool.TimeoutMs ?? defaultTimeout;
|
|
12405
13071
|
const response = await ClientToolRequestManager.Instance.RequestClientTool(`ct_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, tool.Name, tool.Params, params.sessionID, agentRunID, timeoutMs, tool.Description);
|
|
12406
|
-
await this.finalizeStepEntity(stepEntity, response.Success, response.ErrorMessage, {
|
|
13072
|
+
await this.finalizeStepEntity(stepEntity, response.Success, response.ErrorMessage, {
|
|
13073
|
+
result: response.Result,
|
|
13074
|
+
media: response.Media?.map(m => ({ mimeType: m.MimeType, bytes: Math.floor((m.Base64.length * 3) / 4), width: m.Width, height: m.Height }))
|
|
13075
|
+
});
|
|
12407
13076
|
results.push({
|
|
12408
13077
|
ToolName: tool.Name,
|
|
12409
13078
|
Success: response.Success,
|
|
12410
13079
|
Result: response.Result,
|
|
13080
|
+
Media: response.Media,
|
|
12411
13081
|
ErrorMessage: response.ErrorMessage
|
|
12412
13082
|
});
|
|
12413
13083
|
}
|
|
12414
13084
|
// Format results as conversation message
|
|
12415
|
-
|
|
12416
|
-
params.conversationMessages.push({
|
|
12417
|
-
role: 'user',
|
|
12418
|
-
content: resultsMarkdown,
|
|
12419
|
-
metadata: {
|
|
12420
|
-
turnAdded: this._promptTurnCount,
|
|
12421
|
-
messageType: 'client-tool-result'
|
|
12422
|
-
}
|
|
12423
|
-
});
|
|
13085
|
+
params.conversationMessages.push(BuildClientToolResultMessage(results, this._promptTurnCount, ResolveClientToolResultMaxChars(this._agentTypePromptParams)));
|
|
12424
13086
|
// If the LLM already declared taskComplete=true alongside the client tools,
|
|
12425
13087
|
// honor that intent now that tools have executed — no need for another LLM call.
|
|
12426
13088
|
if (previousDecision.terminateAfterExecution) {
|
|
@@ -12441,29 +13103,6 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12441
13103
|
}
|
|
12442
13104
|
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
12443
13105
|
}
|
|
12444
|
-
/**
|
|
12445
|
-
* Format client tool results as a compact markdown summary for the conversation.
|
|
12446
|
-
*/
|
|
12447
|
-
formatClientToolResultsAsMarkdown(results) {
|
|
12448
|
-
const failedCount = results.filter(r => !r.Success).length;
|
|
12449
|
-
const header = failedCount > 0
|
|
12450
|
-
? `${failedCount} of ${results.length} client tool(s) failed:`
|
|
12451
|
-
: 'Client tool results:';
|
|
12452
|
-
const lines = results.map(r => {
|
|
12453
|
-
const icon = r.Success ? '✓' : '✗';
|
|
12454
|
-
let line = `${icon} **${r.ToolName}**: ${r.Success ? 'succeeded' : 'failed'}`;
|
|
12455
|
-
if (r.ErrorMessage)
|
|
12456
|
-
line += ` — ${r.ErrorMessage}`;
|
|
12457
|
-
if (r.Success && r.Result != null) {
|
|
12458
|
-
const resultStr = typeof r.Result === 'string' ? r.Result : JSON.stringify(r.Result);
|
|
12459
|
-
if (resultStr.length <= 500) {
|
|
12460
|
-
line += `\n Result: ${resultStr}`;
|
|
12461
|
-
}
|
|
12462
|
-
}
|
|
12463
|
-
return line;
|
|
12464
|
-
});
|
|
12465
|
-
return `${header}\n${lines.join('\n')}`;
|
|
12466
|
-
}
|
|
12467
13106
|
/**
|
|
12468
13107
|
* Executes a 'Skill' next step: activates one or more skills the LLM requested by name.
|
|
12469
13108
|
* Activating a skill (1) appends its full `Instructions` to the conversation so they take
|
|
@@ -13370,12 +14009,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
13370
14009
|
/**
|
|
13371
14010
|
* Resolves the file storage account ID for this agent's file artifacts.
|
|
13372
14011
|
*
|
|
13373
|
-
*
|
|
13374
|
-
*
|
|
13375
|
-
*
|
|
13376
|
-
*
|
|
13377
|
-
* 4. Agent Type-level (`agentType.DefaultStorageAccountID`)
|
|
13378
|
-
* 5. System fallback — single active storage account, if only one exists
|
|
14012
|
+
* Delegates to {@link AIEngineBase.ResolveAgentStorageAccountID}, the shared golden path:
|
|
14013
|
+
* runtime override (`params.override?.storageAccountId`) → agent → category tree → agent type →
|
|
14014
|
+
* system fallback (the single active account, or the most recently updated one with a
|
|
14015
|
+
* once-per-agent warning when several are active).
|
|
13379
14016
|
*
|
|
13380
14017
|
* This method is `protected` so subclasses can override the resolution logic
|
|
13381
14018
|
* for custom storage routing (e.g., routing by file type or tenant).
|
|
@@ -13384,68 +14021,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
13384
14021
|
* @returns The resolved FileStorageAccount ID, or null if none configured
|
|
13385
14022
|
*/
|
|
13386
14023
|
async getStorageAccountID(params) {
|
|
13387
|
-
|
|
13388
|
-
if (params.override?.storageAccountId) {
|
|
13389
|
-
return params.override.storageAccountId;
|
|
13390
|
-
}
|
|
13391
|
-
// 2. Agent-level override
|
|
13392
|
-
if (params.agent.DefaultStorageAccountID) {
|
|
13393
|
-
return params.agent.DefaultStorageAccountID;
|
|
13394
|
-
}
|
|
13395
|
-
// 3. Category tree walk
|
|
13396
|
-
const categoryId = params.agent.CategoryID;
|
|
13397
|
-
if (categoryId) {
|
|
13398
|
-
try {
|
|
13399
|
-
const categories = AIEngine.Instance.AgentCategories;
|
|
13400
|
-
let currentId = categoryId;
|
|
13401
|
-
const visited = new Set();
|
|
13402
|
-
while (currentId && !visited.has(currentId)) {
|
|
13403
|
-
visited.add(currentId);
|
|
13404
|
-
const cat = categories.find(c => UUIDsEqual(c.ID, currentId));
|
|
13405
|
-
if (!cat)
|
|
13406
|
-
break;
|
|
13407
|
-
if (cat.DefaultStorageAccountID)
|
|
13408
|
-
return cat.DefaultStorageAccountID;
|
|
13409
|
-
currentId = cat.ParentID;
|
|
13410
|
-
}
|
|
13411
|
-
}
|
|
13412
|
-
catch (error) {
|
|
13413
|
-
LogError(`Error resolving category storage account: ${error.message}`);
|
|
13414
|
-
}
|
|
13415
|
-
}
|
|
13416
|
-
// 4. Agent Type-level default
|
|
13417
|
-
const agentTypeId = params.agent.TypeID;
|
|
13418
|
-
if (agentTypeId) {
|
|
13419
|
-
const agentType = AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, agentTypeId));
|
|
13420
|
-
if (agentType?.DefaultStorageAccountID) {
|
|
13421
|
-
return agentType.DefaultStorageAccountID;
|
|
13422
|
-
}
|
|
13423
|
-
}
|
|
13424
|
-
// 5. System fallback — use cached metadata (already loaded during engine Config)
|
|
13425
|
-
const activeAccounts = FileStorageEngineBase.Instance.AccountsWithProviders
|
|
13426
|
-
.filter(a => a.provider.IsActive);
|
|
13427
|
-
if (activeAccounts.length === 0) {
|
|
13428
|
-
// No storage configured — return null, inline base64 fallback handles it downstream
|
|
13429
|
-
return null;
|
|
13430
|
-
}
|
|
13431
|
-
if (activeAccounts.length === 1) {
|
|
13432
|
-
return activeAccounts[0].account.ID;
|
|
13433
|
-
}
|
|
13434
|
-
// 2+ active accounts but nothing configured at any level
|
|
13435
|
-
const agentName = params.agent.Name || params.agent.ID;
|
|
13436
|
-
const typeName = params.agent.TypeID
|
|
13437
|
-
? AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, params.agent.TypeID))?.Name || params.agent.TypeID
|
|
13438
|
-
: 'unknown';
|
|
13439
|
-
const categoryName = params.agent.CategoryID
|
|
13440
|
-
? AIEngine.Instance.AgentCategories.find(c => UUIDsEqual(c.ID, params.agent.CategoryID))?.Name || params.agent.CategoryID
|
|
13441
|
-
: 'none';
|
|
13442
|
-
const accountNames = activeAccounts.map(a => `'${a.account.Name}' (${a.provider.Name})`).join(', ');
|
|
13443
|
-
throw new Error(`Multiple active file storage accounts detected (${accountNames}) but no DefaultStorageAccountID is configured ` +
|
|
13444
|
-
`for agent '${agentName}', category '${categoryName}', or agent type '${typeName}'.\n` +
|
|
13445
|
-
`To fix: Set DefaultStorageAccountID on one of the following (in order of priority):\n` +
|
|
13446
|
-
` 1. Agent '${agentName}' — for this specific agent\n` +
|
|
13447
|
-
` 2. Agent Category '${categoryName}' — for all agents in this category\n` +
|
|
13448
|
-
` 3. Agent Type '${typeName}' — for all agents of this type`);
|
|
14024
|
+
return AIEngine.Instance.ResolveAgentStorageAccountID(params.agent, params.override?.storageAccountId);
|
|
13449
14025
|
}
|
|
13450
14026
|
/**
|
|
13451
14027
|
* Resolves the target user ID from a resolved assignment strategy.
|
|
@@ -14238,15 +14814,59 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
14238
14814
|
this._agentRun.CompletedAt = new Date();
|
|
14239
14815
|
this._agentRun.Success = false;
|
|
14240
14816
|
this._agentRun.ErrorMessage = message;
|
|
14817
|
+
// Save() writes every column, so an in-memory null here would overwrite the reason the
|
|
14818
|
+
// Stop button wrote on the row. Record the reason the abort signal carried instead. The
|
|
14819
|
+
// signal is consulted first because most call sites pass a fixed message naming WHERE
|
|
14820
|
+
// the run was cancelled ('during prompt execution'), not WHY.
|
|
14821
|
+
const signalReason = this._runAbortController?.signal.reason;
|
|
14822
|
+
this._agentRun.CancellationReason = BaseAgent.CancellationReasonForAbort(typeof signalReason === 'string' ? signalReason : message);
|
|
14823
|
+
// A stop is not supposed to lose the work. The run row only learns its FinalPayload on a
|
|
14824
|
+
// normal finish, but every completed step already recorded the payload as it stood, so
|
|
14825
|
+
// the last of those is what the next turn (chained by LastRunID) starts from.
|
|
14826
|
+
if (!this._agentRun.FinalPayload) {
|
|
14827
|
+
const lastPayload = BaseAgent.LastStepPayload(this._agentRun.Steps);
|
|
14828
|
+
if (lastPayload) {
|
|
14829
|
+
this._agentRun.FinalPayload = lastPayload;
|
|
14830
|
+
}
|
|
14831
|
+
}
|
|
14241
14832
|
// Calculate total tokens even for cancelled runs
|
|
14242
14833
|
this.applyTokenStatsToRun(this._agentRun, this.calculateTokenStats());
|
|
14243
14834
|
await this._agentRun.Save();
|
|
14835
|
+
AgentRunWatchdog.Instance.Untrack(this._agentRun.ID);
|
|
14244
14836
|
}
|
|
14245
14837
|
return {
|
|
14246
14838
|
success: false,
|
|
14247
14839
|
agentRun: this._agentRun
|
|
14248
14840
|
};
|
|
14249
14841
|
}
|
|
14842
|
+
/**
|
|
14843
|
+
* Maps the reason an abort signal carried to the run's `CancellationReason` value: the
|
|
14844
|
+
* watchdog's stop relay raises {@link AgentRunWatchdog.UserCancelAbortReason} for a row marked
|
|
14845
|
+
* `User Request`; the wall-clock guard's reason starts with {@link AgentTimeoutAbortReason};
|
|
14846
|
+
* anything else (an upstream caller's token, a shutdown) is `System`.
|
|
14847
|
+
*/
|
|
14848
|
+
static CancellationReasonForAbort(reason) {
|
|
14849
|
+
if (reason === AgentRunWatchdog.UserCancelAbortReason) {
|
|
14850
|
+
return 'User Request';
|
|
14851
|
+
}
|
|
14852
|
+
if (reason && reason.startsWith(BaseAgent.AgentTimeoutAbortReason)) {
|
|
14853
|
+
return 'Timeout';
|
|
14854
|
+
}
|
|
14855
|
+
return 'System';
|
|
14856
|
+
}
|
|
14857
|
+
/**
|
|
14858
|
+
* The payload recorded at the end of the latest step that recorded one, as its serialized
|
|
14859
|
+
* string, or null when no step has. Steps are examined from the highest `StepNumber` down
|
|
14860
|
+
* (array order is spawn order, which a sub-step can interleave).
|
|
14861
|
+
*/
|
|
14862
|
+
static LastStepPayload(steps) {
|
|
14863
|
+
if (!steps || steps.length === 0) {
|
|
14864
|
+
return null;
|
|
14865
|
+
}
|
|
14866
|
+
const ordered = [...steps].sort((a, b) => (b.StepNumber ?? 0) - (a.StepNumber ?? 0));
|
|
14867
|
+
const withPayload = ordered.find(s => typeof s.PayloadAtEnd === 'string' && s.PayloadAtEnd.length > 0);
|
|
14868
|
+
return withPayload?.PayloadAtEnd ?? null;
|
|
14869
|
+
}
|
|
14250
14870
|
/**
|
|
14251
14871
|
* Finalizes the agent run with success.
|
|
14252
14872
|
*
|
|
@@ -14763,11 +15383,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
14763
15383
|
}
|
|
14764
15384
|
const outcome = await ConversationCompactionManager.CompactIfNeeded({
|
|
14765
15385
|
ConversationId: params.conversationId,
|
|
15386
|
+
BranchID: params.ConversationBranchID ?? null,
|
|
14766
15387
|
Agent: params.agent,
|
|
14767
15388
|
AgentType: config?.agentType || null,
|
|
14768
15389
|
Budget: budget,
|
|
14769
15390
|
ContextUser: params.contextUser,
|
|
14770
15391
|
Provider: this.ProviderToUse,
|
|
15392
|
+
ExecutionScope: this.runPromptExecutionScope(params),
|
|
14771
15393
|
EstimateTokens: (messages) => this.estimateConversationTokens(messages),
|
|
14772
15394
|
Verbose: params.verbose,
|
|
14773
15395
|
// The in-flight agent-response placeholder row: a post-turn pass runs while
|
|
@@ -14962,9 +15584,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
14962
15584
|
* @protected
|
|
14963
15585
|
*/
|
|
14964
15586
|
async compactMessage(message, metadata, params) {
|
|
14965
|
-
const originalContent =
|
|
14966
|
-
? message.content
|
|
14967
|
-
: JSON.stringify(message.content);
|
|
15587
|
+
const originalContent = ReplaceMediaBlocksWithStubs(message.content);
|
|
14968
15588
|
switch (metadata.compactMode) {
|
|
14969
15589
|
case 'First N Chars': {
|
|
14970
15590
|
const length = metadata.compactLength;
|
|
@@ -14992,6 +15612,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
14992
15612
|
const step = await this.createCompactionStep(prompt, message, params);
|
|
14993
15613
|
// Execute summarization prompt
|
|
14994
15614
|
const promptParams = new AIPromptParams();
|
|
15615
|
+
Object.assign(promptParams, this.runPromptExecutionScope(params));
|
|
14995
15616
|
promptParams.prompt = prompt;
|
|
14996
15617
|
promptParams.data = {
|
|
14997
15618
|
originalContent,
|
|
@@ -15000,7 +15621,6 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
15000
15621
|
messageType: message.metadata?.messageType || 'unknown',
|
|
15001
15622
|
turnAdded: message.metadata?.turnAdded || 0
|
|
15002
15623
|
};
|
|
15003
|
-
promptParams.contextUser = params.contextUser;
|
|
15004
15624
|
promptParams.agentId = params.agent.ID;
|
|
15005
15625
|
promptParams.UserID = ResolvePromptRunUserID({
|
|
15006
15626
|
UserID: params.userId,
|
|
@@ -15075,12 +15695,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
15075
15695
|
|| messageType === 'loop-result';
|
|
15076
15696
|
}
|
|
15077
15697
|
estimateTokens(content, modelName) {
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
//
|
|
15082
|
-
|
|
15083
|
-
|
|
15698
|
+
if (typeof content === 'string') {
|
|
15699
|
+
return this.heuristicTokenCount(content);
|
|
15700
|
+
}
|
|
15701
|
+
// Each media block counts as IMAGE_BLOCK_TOKEN_ESTIMATE, not by its base64 length.
|
|
15702
|
+
let tokens = 0;
|
|
15703
|
+
for (const block of content) {
|
|
15704
|
+
tokens += IsMediaBlock(block) ? IMAGE_BLOCK_TOKEN_ESTIMATE : this.heuristicTokenCount(block.content);
|
|
15705
|
+
}
|
|
15706
|
+
return tokens;
|
|
15084
15707
|
}
|
|
15085
15708
|
/**
|
|
15086
15709
|
* Provides an improved heuristic token count when tokenizer is unavailable.
|