@memberjunction/ai-agents 6.2.0-edge.1 → 6.2.0-edge.3
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 +18 -0
- package/dist/AgentDecisionService.d.ts +90 -0
- package/dist/AgentDecisionService.d.ts.map +1 -0
- package/dist/AgentDecisionService.js +159 -0
- package/dist/AgentDecisionService.js.map +1 -0
- package/dist/AgentRunner.d.ts +9 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +23 -7
- package/dist/AgentRunner.js.map +1 -1
- package/dist/ConversationCompactionManager.d.ts +7 -1
- package/dist/ConversationCompactionManager.d.ts.map +1 -1
- package/dist/ConversationCompactionManager.js +4 -1
- package/dist/ConversationCompactionManager.js.map +1 -1
- package/dist/KnowledgeAgent.d.ts.map +1 -1
- package/dist/KnowledgeAgent.js +6 -6
- package/dist/KnowledgeAgent.js.map +1 -1
- package/dist/PayloadFeedbackManager.d.ts +116 -14
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +229 -88
- package/dist/PayloadFeedbackManager.js.map +1 -1
- package/dist/agent-context-injector.d.ts +47 -3
- package/dist/agent-context-injector.d.ts.map +1 -1
- package/dist/agent-context-injector.js +77 -10
- package/dist/agent-context-injector.js.map +1 -1
- package/dist/agent-memory-context-builder.d.ts +13 -0
- package/dist/agent-memory-context-builder.d.ts.map +1 -1
- package/dist/agent-memory-context-builder.js +24 -5
- package/dist/agent-memory-context-builder.js.map +1 -1
- package/dist/agent-types/flow-agent-type.d.ts +97 -0
- package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
- package/dist/agent-types/flow-agent-type.js +281 -24
- package/dist/agent-types/flow-agent-type.js.map +1 -1
- package/dist/agent-types/flow-graph-executor.d.ts +12 -1
- package/dist/agent-types/flow-graph-executor.d.ts.map +1 -1
- package/dist/agent-types/flow-graph-executor.js +25 -1
- package/dist/agent-types/flow-graph-executor.js.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.d.ts +239 -66
- package/dist/agent-types/loop-agent-prompt-params.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-prompt-params.js +47 -2
- package/dist/agent-types/loop-agent-prompt-params.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +12 -1
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts +26 -0
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +115 -5
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +527 -10
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1779 -110
- package/dist/base-agent.js.map +1 -1
- package/dist/catalog-narrowing.d.ts +136 -0
- package/dist/catalog-narrowing.d.ts.map +1 -0
- package/dist/catalog-narrowing.js +212 -0
- package/dist/catalog-narrowing.js.map +1 -0
- package/dist/decision-discovery.d.ts +401 -0
- package/dist/decision-discovery.d.ts.map +1 -0
- package/dist/decision-discovery.js +511 -0
- package/dist/decision-discovery.js.map +1 -0
- package/dist/finish-if-mode-warnings.d.ts +39 -0
- package/dist/finish-if-mode-warnings.d.ts.map +1 -0
- package/dist/finish-if-mode-warnings.js +78 -0
- package/dist/finish-if-mode-warnings.js.map +1 -0
- package/dist/finish-if-state.d.ts +72 -0
- package/dist/finish-if-state.d.ts.map +1 -0
- package/dist/finish-if-state.js +109 -0
- package/dist/finish-if-state.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +108 -2
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +172 -9
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/memory-note-gate.d.ts +208 -0
- package/dist/memory-note-gate.d.ts.map +1 -0
- package/dist/memory-note-gate.js +294 -0
- package/dist/memory-note-gate.js.map +1 -0
- 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/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 +23 -2
- package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
- package/dist/realtime/bridge-realtime-session-factory.js +22 -6
- package/dist/realtime/bridge-realtime-session-factory.js.map +1 -1
- package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -1
- package/dist/realtime/bridge-room-transcript-sink.js +15 -9
- package/dist/realtime/bridge-room-transcript-sink.js.map +1 -1
- 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/realtime-client-session-service.d.ts +117 -12
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +162 -39
- package/dist/realtime/realtime-client-session-service.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-session-runner.d.ts +2 -82
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
- package/dist/realtime/realtime-session-runner.js +12 -187
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/realtime/realtime-vendor-resolution.d.ts +8 -3
- package/dist/realtime/realtime-vendor-resolution.d.ts.map +1 -1
- package/dist/realtime/realtime-vendor-resolution.js +3 -3
- package/dist/realtime/realtime-vendor-resolution.js.map +1 -1
- package/dist/rubric-evaluation-agent-runner.d.ts +2 -0
- package/dist/rubric-evaluation-agent-runner.d.ts.map +1 -0
- package/dist/rubric-evaluation-agent-runner.js +47 -0
- package/dist/rubric-evaluation-agent-runner.js.map +1 -0
- package/dist/self-check.d.ts +101 -0
- package/dist/self-check.d.ts.map +1 -0
- package/dist/self-check.js +74 -0
- package/dist/self-check.js.map +1 -0
- package/package.json +19 -18
package/dist/base-agent.js
CHANGED
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase, MJEnvironmentEntityExtended } 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
18
|
import { ResolvePromptRunUserID } from "@memberjunction/ai-core-plus";
|
|
19
|
-
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
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, 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
|
-
import {
|
|
24
|
+
import { ProviderRubricEngine } from '@memberjunction/rubrics';
|
|
25
|
+
import { ExecuteSelfCheck, PickSelfCheckLink } from './self-check.js';
|
|
26
|
+
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, NormalizeUUID, SafeExpressionEvaluator, UUIDsEqual, EscapeSQLString, IsPlainObject, CleanAndParseJSON } from '@memberjunction/global';
|
|
25
27
|
// token optimization via @memberjunction/context-crush (SmartCrusher/CacheAligner-inspired)
|
|
26
28
|
import { CrushJSON, DescribeCrush, PartitionStablePrefix } from '@memberjunction/context-crush';
|
|
27
29
|
// AST-aware code reduction (CodeCompressor-inspired) — opt-in per agent type
|
|
@@ -32,6 +34,7 @@ import { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs
|
|
|
32
34
|
import { SelectRealtimeVendorForModel } from './realtime/realtime-vendor-resolution.js';
|
|
33
35
|
import { RealtimeClientSessionService, WarnOnUnmatchedProviderVoice } from './realtime/realtime-client-session-service.js';
|
|
34
36
|
import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
|
|
37
|
+
import { ReadHostTools, ReadTrimmedString } from './realtime/bridge-host-params.js';
|
|
35
38
|
import { RealtimeRecordingController } from './realtime/realtime-recording-capture.js';
|
|
36
39
|
import { ResolveRecordingStorageAccountID, StoreRealtimeRecording } from './realtime/realtime-recording-store.js';
|
|
37
40
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
@@ -44,7 +47,7 @@ import { FormatToolResultSection, FormatToolErrorSection, RenderToolResultData,
|
|
|
44
47
|
import { PriorTurnToolResultCache } from './prior-turn-tool-result-cache.js';
|
|
45
48
|
import { PromptComponentResolver, InjectScopedPromptParts } from './prompt-component-resolver.js';
|
|
46
49
|
import { ScopedPromptConfigResolver, ApplyScopedPromptConfig } from './scoped-prompt-config-resolver.js';
|
|
47
|
-
import { StringifyForPersistence, AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter, SystemPlaceholderManager } from '@memberjunction/ai-core-plus';
|
|
50
|
+
import { StringifyForPersistence, AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter, SummarizeDecisionAnswers, SystemPlaceholderManager } from '@memberjunction/ai-core-plus';
|
|
48
51
|
import { ActionResult, RunActionParams } from '@memberjunction/actions-base';
|
|
49
52
|
import { TemplateEngineServer } from '@memberjunction/templates';
|
|
50
53
|
import { RuntimeStateFragmentBuilder, EscapeRuntimeStateTagsInMessage } from './runtime-state-fragment.js';
|
|
@@ -55,6 +58,13 @@ import { PayloadManager } from './PayloadManager.js';
|
|
|
55
58
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
56
59
|
import { ArtifactToolManager } from './ArtifactToolManager.js';
|
|
57
60
|
import { MemoryWriteManager } from './MemoryWriteManager.js';
|
|
61
|
+
import { AgentDecisionService } from './AgentDecisionService.js';
|
|
62
|
+
import { PayloadFeedbackManager } from './PayloadFeedbackManager.js';
|
|
63
|
+
import { DEFAULT_LOOP_AGENT_PROMPT_PARAMS, MAX_DECISION_CALLS_PER_TURN, MAX_DECISION_REQUESTS_PER_TURN, ResolveFinishIfMode } from './agent-types/loop-agent-prompt-params.js';
|
|
64
|
+
import { FinishIfModeWarnings } from './finish-if-mode-warnings.js';
|
|
65
|
+
import { ApplyCatalogNarrowing, BuildCatalogNarrowingQuestions, CatalogNarrowingCandidatesToAsk, CatalogNarrowingNote, CATALOG_NARROWING_FIND_ACTIONS, IsAlwaysShownAction, IsCatalogListNarrowed, NoCatalogNarrowing, OpeningRequestText, ReachableCatalogNarrowingLimits, ResolveCatalogNarrowingLimits, SelectCatalogNarrowing, } from './catalog-narrowing.js';
|
|
66
|
+
import { BuildDecisionDiscoveryOptionSet, BuildDecisionDiscoveryQuestions, CanSearchEntities, DECISION_DISCOVERY_MAX_RECORDED_IDS, DECISION_DISCOVERY_MIN_CONFIDENCE, DECISION_DISCOVERY_MIN_OPTIONS, DECISION_DISCOVERY_TIMEOUT_MS, DecisionDiscoveryAgentSearch, DecisionDiscoveryFromResult, DecisionDiscoveryRunnableAgents, DecisionPromptOptionCap, FailedDecisionDiscovery, HostAllowedAgentIDs, IsDecisionDiscoveryOn, IsOpeningTurn, MentionsAgent, } from './decision-discovery.js';
|
|
67
|
+
import { BuildFinishIfQuestions, CapFinishIfState, FINISH_IF_STATE_EXCERPT, FormatActionForFinishIf, FormatSubAgentForFinishIf, JudgeFinishIf } from './finish-if-state.js';
|
|
58
68
|
import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
|
|
59
69
|
// The ONE payload-mapping dialect. Loop agents and the task-graph dispatcher read the same authored
|
|
60
70
|
// mapping strings, so these must be the same functions rather than two copies that agree today.
|
|
@@ -127,6 +137,19 @@ export class CircuitBreakerActionResult extends ActionResult {
|
|
|
127
137
|
this.Reason = Reason;
|
|
128
138
|
}
|
|
129
139
|
}
|
|
140
|
+
/** The top-level `reasoning` and `message` of a prompt's JSON response, when it has them. */
|
|
141
|
+
function responseReasoningAndMessage(result) {
|
|
142
|
+
const parsed = typeof result === 'string' ? CleanAndParseJSON(result) : result;
|
|
143
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
const reasoning = 'reasoning' in parsed ? parsed.reasoning : undefined;
|
|
147
|
+
const message = 'message' in parsed ? parsed.message : undefined;
|
|
148
|
+
return {
|
|
149
|
+
Reasoning: typeof reasoning === 'string' ? reasoning : undefined,
|
|
150
|
+
Message: typeof message === 'string' ? message : undefined
|
|
151
|
+
};
|
|
152
|
+
}
|
|
130
153
|
export class BaseAgent {
|
|
131
154
|
constructor() {
|
|
132
155
|
/**
|
|
@@ -285,6 +308,10 @@ export class BaseAgent {
|
|
|
285
308
|
* Allows agents to explore input artifacts on demand.
|
|
286
309
|
*/
|
|
287
310
|
this._artifactToolManager = new ArtifactToolManager();
|
|
311
|
+
/**
|
|
312
|
+
* Service for evaluating agent decisions inline via fast decision models.
|
|
313
|
+
*/
|
|
314
|
+
this._agentDecisionService = new AgentDecisionService();
|
|
288
315
|
/**
|
|
289
316
|
* Manages conversation-history retrieval tools for the current agent run.
|
|
290
317
|
* Armed only when the run has a conversationId (the cross-turn context gate).
|
|
@@ -311,6 +338,18 @@ export class BaseAgent {
|
|
|
311
338
|
* @private
|
|
312
339
|
*/
|
|
313
340
|
this._effectiveSubAgents = [];
|
|
341
|
+
/**
|
|
342
|
+
* The request that opened the run: the last user message when it started, before the framework
|
|
343
|
+
* added anything. Catalog narrowing judges the catalog against it.
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
this._openingRequest = '';
|
|
347
|
+
/**
|
|
348
|
+
* Whether the run answers its conversation's opening request ({@link IsOpeningTurn}), read from
|
|
349
|
+
* the messages it started with. Decision discovery asks about no other turn.
|
|
350
|
+
* @private
|
|
351
|
+
*/
|
|
352
|
+
this._isOpeningTurn = false;
|
|
314
353
|
/**
|
|
315
354
|
* IDs of skills already activated during this run. Prevents re-activation from re-appending
|
|
316
355
|
* the same instructions to context / re-pushing duplicate actionChanges/subAgentChanges entries
|
|
@@ -460,6 +499,20 @@ export class BaseAgent {
|
|
|
460
499
|
* unshifted onto `conversationMessages` by the shared {@link AgentMemoryContextBuilder}.
|
|
461
500
|
*/
|
|
462
501
|
this._injectedRAG = null;
|
|
502
|
+
/**
|
|
503
|
+
* Checks execution guardrails and modifies next step if limits are exceeded.
|
|
504
|
+
* This method is called after validation but before execution of non-terminal steps.
|
|
505
|
+
*
|
|
506
|
+
* @param params - Execution parameters
|
|
507
|
+
* @param nextStep - The validated next step
|
|
508
|
+
* @param currentPayload - Current payload
|
|
509
|
+
* @param agentRun - Current agent run
|
|
510
|
+
* @param currentStep - Current execution step
|
|
511
|
+
* @returns Modified next step if guardrails exceeded, or original next step
|
|
512
|
+
* @protected
|
|
513
|
+
*/
|
|
514
|
+
this._selfCheckAttemptsByRun = new Map();
|
|
515
|
+
this._selfCheckLinkByAgent = new Map();
|
|
463
516
|
/**
|
|
464
517
|
* Determines the request type ID based on the Chat step's context.
|
|
465
518
|
* - If the responseForm has only approve/reject-style buttons, uses "Approval"
|
|
@@ -1109,6 +1162,8 @@ export class BaseAgent {
|
|
|
1109
1162
|
* @protected
|
|
1110
1163
|
*/
|
|
1111
1164
|
get FileOutputs() { return this._fileOutputs; }
|
|
1165
|
+
/** The longest the catalog narrowing decision may take before the run shows the full catalog. */
|
|
1166
|
+
static { this.CATALOG_NARROWING_TIMEOUT_MS = 30000; }
|
|
1112
1167
|
/** The skills activated so far in this run (requested or self-activated), in activation order. */
|
|
1113
1168
|
get ActivatedSkillIDs() {
|
|
1114
1169
|
return this._activatedSkillIDs;
|
|
@@ -1563,6 +1618,10 @@ export class BaseAgent {
|
|
|
1563
1618
|
this._dynamicActionLimits = {};
|
|
1564
1619
|
this._mediaOutputs = [];
|
|
1565
1620
|
this._messageLifecycleCallback = params.onMessageLifecycle;
|
|
1621
|
+
this._catalogNarrowing = undefined;
|
|
1622
|
+
this._openingRequest = OpeningRequestText(params.conversationMessages);
|
|
1623
|
+
this._runExecutionScope = this.runPromptExecutionScope(params);
|
|
1624
|
+
this._isOpeningTurn = IsOpeningTurn(params.conversationMessages);
|
|
1566
1625
|
// Resolve storage account for file artifacts
|
|
1567
1626
|
this._resolvedStorageAccountId = await this.getStorageAccountID(wrappedParams);
|
|
1568
1627
|
// Check for cancellation after Phase 1
|
|
@@ -1624,7 +1683,10 @@ export class BaseAgent {
|
|
|
1624
1683
|
// Carry the previous turn's tool results forward (no-op without a
|
|
1625
1684
|
// conversationId). Runs here so the results are in the messages before
|
|
1626
1685
|
// the pre-turn compaction check and the first prompt.
|
|
1627
|
-
this.injectPriorTurnToolResults(wrappedParams)
|
|
1686
|
+
this.injectPriorTurnToolResults(wrappedParams),
|
|
1687
|
+
// Decision discovery (plan Task 3.1), off unless the decisionsEnabled and decisionDiscovery prompt
|
|
1688
|
+
// params are true: may add a <suggested_agent> system message, within DECISION_DISCOVERY_TIMEOUT_MS.
|
|
1689
|
+
this.InjectDecisionDiscovery(params.agent, params.contextUser, wrappedParams.conversationMessages, params.data)
|
|
1628
1690
|
]);
|
|
1629
1691
|
// Inject scope-resolved prompt parts (role-faithful) for this agent's prompt, alongside
|
|
1630
1692
|
// memory/RAG. Synchronous — parts are cached on AIEngine. Uses the same run scope.
|
|
@@ -1713,6 +1775,9 @@ export class BaseAgent {
|
|
|
1713
1775
|
// consumers that re-read `params` after the call see what they
|
|
1714
1776
|
// passed in, not our chained signal.
|
|
1715
1777
|
params.cancellationToken = upstreamToken;
|
|
1778
|
+
// A cancellation, or an error thrown out of the step loop, leaves any held decision
|
|
1779
|
+
// requests unasked. The terminate path has already logged and cleared its own.
|
|
1780
|
+
this.skipHeldDecisions(params, this.runEndForHeldDecisions());
|
|
1716
1781
|
this.releasePerRunDataCache();
|
|
1717
1782
|
await this.finalizeRun(this.deriveRunOutcome());
|
|
1718
1783
|
}
|
|
@@ -1888,9 +1953,20 @@ export class BaseAgent {
|
|
|
1888
1953
|
const selfNames = Array.isArray(params.data?.realtimeSelfNames)
|
|
1889
1954
|
? (params.data?.realtimeSelfNames).filter((n) => typeof n === 'string')
|
|
1890
1955
|
: undefined;
|
|
1956
|
+
const hostTools = ReadHostTools(params.data?.realtimeHostTools);
|
|
1891
1957
|
return {
|
|
1892
1958
|
CoAgent: params.agent,
|
|
1893
1959
|
TargetAgentID: targetID,
|
|
1960
|
+
HostTools: hostTools,
|
|
1961
|
+
HostFraming: ReadTrimmedString(params.data?.realtimeHostFraming, 'realtimeHostFraming'),
|
|
1962
|
+
PriorTranscript: ReadTrimmedString(params.data?.realtimePriorTranscript, 'realtimePriorTranscript'),
|
|
1963
|
+
ConversationID: ReadTrimmedString(params.data?.conversationId, 'conversationId'),
|
|
1964
|
+
// The run's runtime API keys, so the bridged voice session mints on the caller's key when
|
|
1965
|
+
// the run carries one for the realtime driver (the same run-key → platform order as
|
|
1966
|
+
// GetAIAPIKey; realtime does not consult MJ Credentials). Absent ⇒ platform keys, as before.
|
|
1967
|
+
// CreateBridgeRealtimeSession (the LiveKit / telephony factory) passes no apiKeys today.
|
|
1968
|
+
APIKeys: params.apiKeys,
|
|
1969
|
+
CredentialScope: params.CredentialScope,
|
|
1894
1970
|
AgentSessionID: params.data?.agentSessionId ?? '',
|
|
1895
1971
|
PreferredModelID: modelID,
|
|
1896
1972
|
ConfigOverridesJson: BuildRealtimeOverridesJson(modelID, voice) ?? undefined,
|
|
@@ -1928,13 +2004,22 @@ export class BaseAgent {
|
|
|
1928
2004
|
// would dead-end whenever the top model lacked a key — e.g. a power-11 model with no env key
|
|
1929
2005
|
// (Inworld/AssemblyAI) outranking GPT Realtime — and surface "No usable Realtime model" even though
|
|
1930
2006
|
// a usable model exists. This mirrors the same fix in RealtimeClientSessionService.
|
|
2007
|
+
// The run's runtime API keys, in GetAIAPIKey's order: the run's key for a driver class, else
|
|
2008
|
+
// the platform's. That is the legacy tier of prompt key resolution — a prompt consults MJ
|
|
2009
|
+
// Credentials / AICredentialBindings first, and realtime does not consult them at all.
|
|
2010
|
+
//
|
|
2011
|
+
// This also affects WHICH vendor runs the session, deliberately. Vendor selection walks
|
|
2012
|
+
// candidates by priority and takes the first whose key resolves, so an organization that
|
|
2013
|
+
// brings its own credential for a vendor we hold no platform key for now reaches that
|
|
2014
|
+
// vendor — which is the point of bringing your own key, not a side effect of it.
|
|
2015
|
+
const resolveAPIKey = MakeAIAPIKeyResolver(params.apiKeys, undefined, params.CredentialScope);
|
|
1931
2016
|
const candidates = this.selectRealtimeModelCandidates(params.agent, overrideModelID);
|
|
1932
2017
|
for (const model of candidates) {
|
|
1933
|
-
const vendor = SelectRealtimeVendorForModel(model.ID);
|
|
2018
|
+
const vendor = SelectRealtimeVendorForModel(model.ID, resolveAPIKey);
|
|
1934
2019
|
if (!vendor) {
|
|
1935
2020
|
continue;
|
|
1936
2021
|
}
|
|
1937
|
-
const apiKey =
|
|
2022
|
+
const apiKey = resolveAPIKey(vendor.DriverClass);
|
|
1938
2023
|
if (!apiKey) {
|
|
1939
2024
|
continue;
|
|
1940
2025
|
}
|
|
@@ -2216,6 +2301,7 @@ export class BaseAgent {
|
|
|
2216
2301
|
parentDepth: this._depth,
|
|
2217
2302
|
configurationId: params.configurationId,
|
|
2218
2303
|
apiKeys: params.apiKeys,
|
|
2304
|
+
CredentialScope: params.CredentialScope,
|
|
2219
2305
|
data: params.data,
|
|
2220
2306
|
verbose: params.verbose,
|
|
2221
2307
|
// Progress streams BOTH to the runner's narration consumer (request.OnProgress —
|
|
@@ -2799,6 +2885,7 @@ export class BaseAgent {
|
|
|
2799
2885
|
// Check if we should continue or terminate
|
|
2800
2886
|
if (nextStep.terminate) {
|
|
2801
2887
|
continueExecution = false;
|
|
2888
|
+
this.skipHeldDecisions(params, 'the run ended after the step that carried them');
|
|
2802
2889
|
this.logStatus(`🏁 Agent '${params.agent.Name}' terminating after ${stepCount} steps with result: ${nextStep.step}`, true, params);
|
|
2803
2890
|
}
|
|
2804
2891
|
else {
|
|
@@ -2873,6 +2960,25 @@ export class BaseAgent {
|
|
|
2873
2960
|
}
|
|
2874
2961
|
return 'combined';
|
|
2875
2962
|
}
|
|
2963
|
+
/**
|
|
2964
|
+
* The observability context memory injection runs under for an agent run: the run's ID, the next
|
|
2965
|
+
* step number, and a callback that adds each rerank step to the run's steps. A rerank step carries
|
|
2966
|
+
* the rerank's prompt run, whose cost and token rollups include a prompt-backed reranker's own
|
|
2967
|
+
* prompt runs, so the run's totals and its `MaxCostPerRun` / `MaxTokensPerRun` guardrails count
|
|
2968
|
+
* them. Undefined when there is no run.
|
|
2969
|
+
*
|
|
2970
|
+
* @param run - The current agent run, or null before one exists
|
|
2971
|
+
*/
|
|
2972
|
+
MemoryObservability(run) {
|
|
2973
|
+
if (!run) {
|
|
2974
|
+
return undefined;
|
|
2975
|
+
}
|
|
2976
|
+
return {
|
|
2977
|
+
agentRunID: run.ID,
|
|
2978
|
+
stepNumber: (run.Steps?.length || 0) + 1,
|
|
2979
|
+
OnStepCreated: step => run.Steps.push(step),
|
|
2980
|
+
};
|
|
2981
|
+
}
|
|
2876
2982
|
/**
|
|
2877
2983
|
* Inject notes and examples into agent context memory.
|
|
2878
2984
|
* Called automatically before agent execution if injection is enabled on the agent.
|
|
@@ -2894,9 +3000,7 @@ export class BaseAgent {
|
|
|
2894
3000
|
// Delegate the orchestration to the shared, reusable builder so both BaseAgent and the
|
|
2895
3001
|
// Realtime agent type inject memory identically. The observability context and verbose
|
|
2896
3002
|
// status logging are derived from this instance and passed through.
|
|
2897
|
-
const observability = this._agentRun
|
|
2898
|
-
? { agentRunID: this._agentRun.ID, stepNumber: (this._agentRun.Steps?.length || 0) + 1 }
|
|
2899
|
-
: undefined;
|
|
3003
|
+
const observability = this.MemoryObservability(this._agentRun);
|
|
2900
3004
|
const result = await new AgentMemoryContextBuilder().InjectContextMemory(input, agent, userId, companyId, contextUser, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, secondaryScopeConfig, observability, (message, verboseOnly) => this.logStatus(message, verboseOnly));
|
|
2901
3005
|
// Store for inclusion in result (externally observable behavior preserved)
|
|
2902
3006
|
this._injectedMemory = result;
|
|
@@ -2961,6 +3065,302 @@ export class BaseAgent {
|
|
|
2961
3065
|
this._injectedRAG = result;
|
|
2962
3066
|
return result;
|
|
2963
3067
|
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Decision discovery (plan Task 3.1): suggests the agent to delegate to before the first prompt.
|
|
3070
|
+
*
|
|
3071
|
+
* Runs in parallel with the rest of Phase 2 of `Execute()`, and only when the merged agent-type
|
|
3072
|
+
* prompt params set `decisionsEnabled: true` (the master switch, {@link DecisionsEnabled}) and
|
|
3073
|
+
* `decisionDiscovery: true`, the run answers its conversation's opening request
|
|
3074
|
+
* ({@link IsOpeningTurn}), and that request @mentions no agent. A follow-up turn is never asked
|
|
3075
|
+
* about: it may be continuing work with an agent already engaged, and a suggestion must not pull
|
|
3076
|
+
* the agent away from it. One decision asks which of the agents the user may run (and the host
|
|
3077
|
+
* allows) should handle the request, and whether a specialist should. When both answers are
|
|
3078
|
+
* confident, a `<suggested_agent>` system message is unshifted onto `conversationMessages`, the
|
|
3079
|
+
* way pre-execution RAG adds `<retrieved_context>`. Each discovery is recorded as one
|
|
3080
|
+
* `Agent discovery` Decision step.
|
|
3081
|
+
*
|
|
3082
|
+
* Fails safe: an error, a timeout, a cancelled run, an unusable answer or an unsure one adds
|
|
3083
|
+
* nothing, so the agent behaves as it would without discovery. So do fewer than
|
|
3084
|
+
* {@link DECISION_DISCOVERY_MIN_OPTIONS} options. Never delays the first prompt by more than
|
|
3085
|
+
* {@link DECISION_DISCOVERY_TIMEOUT_MS}, and never throws.
|
|
3086
|
+
*
|
|
3087
|
+
* The any-applies question does not name the options, so the decision cannot say that none of
|
|
3088
|
+
* them fits: see {@link DECISION_DISCOVERY_APPLIES_INSTRUCTIONS} for why it stays that way for now.
|
|
3089
|
+
*
|
|
3090
|
+
* @param agent - The agent being executed.
|
|
3091
|
+
* @param contextUser - The user whose run permission decides the options.
|
|
3092
|
+
* @param conversationMessages - The mutated message array that flows to the LLM (the suggestion is unshifted here).
|
|
3093
|
+
* @param data - The run's `ExecuteAgentParams.data`. Its `__agentTypePromptParams` overrides the agent's prompt
|
|
3094
|
+
* params, and its `ALL_AVAILABLE_AGENTS`, when an array, is the host's allow-list: no other agent is suggested.
|
|
3095
|
+
*/
|
|
3096
|
+
async InjectDecisionDiscovery(agent, contextUser, conversationMessages, data) {
|
|
3097
|
+
try {
|
|
3098
|
+
const override = data?.__agentTypePromptParams;
|
|
3099
|
+
const overrides = IsPlainObject(override) ? override : undefined;
|
|
3100
|
+
if (!this.isDecisionDiscoveryOnFor(agent, overrides) || !conversationMessages) {
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
if (!this._openingRequest || !contextUser) {
|
|
3104
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': the run has no ${contextUser ? 'user message' : 'context user'}`, true);
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
if (!this._isOpeningTurn) {
|
|
3108
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': a follow-up turn, which may continue with an agent already engaged`, true);
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
if (MentionsAgent(this._openingRequest, AIEngine.Instance.Agents, agent.ID)) {
|
|
3112
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': the request @mentions an agent`, true);
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const promptParams = this.decisionDiscoveryPromptParams(agent, overrides);
|
|
3116
|
+
const outcome = await this.runDecisionDiscovery(agent, contextUser, promptParams, HostAllowedAgentIDs(data));
|
|
3117
|
+
if (outcome.Injected && outcome.Message) {
|
|
3118
|
+
conversationMessages.unshift({ role: 'system', content: outcome.Message });
|
|
3119
|
+
this.logStatus(`Decision discovery suggested '${outcome.Answer?.Agent.Name}' to '${agent.Name}'`, true);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
catch (error) {
|
|
3123
|
+
this.warnDecisionDiscovery(agent, error instanceof Error ? error.message : String(error));
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
/**
|
|
3127
|
+
* Whether discovery is on for this run: the master switch ({@link DecisionsEnabled}) and
|
|
3128
|
+
* `decisionDiscovery` are both on. Read without rebuilding the merged prompt params, since it is
|
|
3129
|
+
* asked on every run of every agent: each setting from the per-run override when it sets one,
|
|
3130
|
+
* otherwise from the agent's base params, cached on AIEngine with its base catalog. Only a cold
|
|
3131
|
+
* cache builds them.
|
|
3132
|
+
*/
|
|
3133
|
+
isDecisionDiscoveryOnFor(agent, overrides) {
|
|
3134
|
+
let base;
|
|
3135
|
+
const paramsSetting = (key) => {
|
|
3136
|
+
if (overrides && key in overrides) {
|
|
3137
|
+
return overrides;
|
|
3138
|
+
}
|
|
3139
|
+
base ??= this.cachedBasePromptParams(agent) ?? this.buildDecisionDiscoveryPromptParams(agent, undefined);
|
|
3140
|
+
return base;
|
|
3141
|
+
};
|
|
3142
|
+
return this.DecisionsEnabled(paramsSetting('decisionsEnabled')) && IsDecisionDiscoveryOn(paramsSetting('decisionDiscovery'));
|
|
3143
|
+
}
|
|
3144
|
+
/**
|
|
3145
|
+
* The run's merged agent-type prompt params (schema defaults < agent < per-run overrides), once
|
|
3146
|
+
* discovery is on: the cached base params when there is no override, otherwise built.
|
|
3147
|
+
*/
|
|
3148
|
+
decisionDiscoveryPromptParams(agent, overrides) {
|
|
3149
|
+
const cached = overrides ? undefined : this.cachedBasePromptParams(agent);
|
|
3150
|
+
return cached ?? this.buildDecisionDiscoveryPromptParams(agent, overrides);
|
|
3151
|
+
}
|
|
3152
|
+
/** The agent's base prompt params (no per-run overrides), from the base catalog AIEngine caches, when it holds one. */
|
|
3153
|
+
cachedBasePromptParams(agent) {
|
|
3154
|
+
return AIEngine.Instance.GetAgentBaseCatalog(agent.ID)?.baseAgentTypePromptParams;
|
|
3155
|
+
}
|
|
3156
|
+
/** Builds the merged prompt params, as `gatherPromptTemplateData` does. */
|
|
3157
|
+
buildDecisionDiscoveryPromptParams(agent, overrides) {
|
|
3158
|
+
const agentType = AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
3159
|
+
return this.buildAgentTypePromptParams(agentType, agent, overrides);
|
|
3160
|
+
}
|
|
3161
|
+
/**
|
|
3162
|
+
* Records one `Agent discovery` step around a bounded discovery, and returns what it found. A
|
|
3163
|
+
* failed discovery is logged as a warning; an unsure one only in verbose logs, except that an
|
|
3164
|
+
* answer from a model with no discovery calibration is warned about once per model, because
|
|
3165
|
+
* then discovery can never suggest anything.
|
|
3166
|
+
*/
|
|
3167
|
+
async runDecisionDiscovery(agent, contextUser, promptParams, hostAllowedIDs) {
|
|
3168
|
+
const promptName = typeof promptParams.decisionPromptName === 'string'
|
|
3169
|
+
? promptParams.decisionPromptName
|
|
3170
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
3171
|
+
const step = await this.startDecisionDiscoveryStep(contextUser, promptName);
|
|
3172
|
+
const outcome = await this.boundDecisionDiscovery(signal => this.discoverAgent(agent, contextUser, promptName, hostAllowedIDs, signal), step);
|
|
3173
|
+
await this.finishDecisionDiscoveryStep(step, outcome);
|
|
3174
|
+
if (!outcome.Succeeded) {
|
|
3175
|
+
this.warnDecisionDiscovery(agent, outcome.Reason ?? 'unknown error');
|
|
3176
|
+
}
|
|
3177
|
+
else if (!outcome.Injected) {
|
|
3178
|
+
if (outcome.UncalibratedModel) {
|
|
3179
|
+
BaseAgent.warnUncalibratedDiscoveryModel(outcome.UncalibratedModel);
|
|
3180
|
+
}
|
|
3181
|
+
this.logStatus(`Decision discovery for '${agent.Name}' suggested no agent: ${outcome.Reason}`, true);
|
|
3182
|
+
}
|
|
3183
|
+
return outcome;
|
|
3184
|
+
}
|
|
3185
|
+
/** The answering models already warned about as having no discovery calibration, so each is warned about once per process. */
|
|
3186
|
+
static { this._uncalibratedDiscoveryModelsWarned = new Set(); }
|
|
3187
|
+
/**
|
|
3188
|
+
* Warns, once per model, that discovery answered by a model with no discovery calibration
|
|
3189
|
+
* suggests nothing. That happens when a decision model is added or renamed, when a vendor model
|
|
3190
|
+
* moves to another version (Jev's `APIName`), or when LLM Decision answers through a chat model
|
|
3191
|
+
* other than the one its calibration was fitted on; without the warning the only trace is each
|
|
3192
|
+
* `Agent discovery` step's reason.
|
|
3193
|
+
*/
|
|
3194
|
+
static warnUncalibratedDiscoveryModel(model) {
|
|
3195
|
+
if (BaseAgent._uncalibratedDiscoveryModelsWarned.has(model)) {
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
BaseAgent._uncalibratedDiscoveryModelsWarned.add(model);
|
|
3199
|
+
LogErrorEx({
|
|
3200
|
+
message: `Decision discovery answered by ${model} suggests nothing: the model has no discovery calibration `
|
|
3201
|
+
+ '(DECISION_DISCOVERY_CALIBRATION), so its answers are treated as unsure. Fit one with the agent-discovery '
|
|
3202
|
+
+ 'Decision Eval, or check that the model was not renamed or moved to another version.',
|
|
3203
|
+
severity: 'warning',
|
|
3204
|
+
category: 'DecisionDiscovery',
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
/**
|
|
3208
|
+
* Runs `discover` until it finishes, {@link DECISION_DISCOVERY_TIMEOUT_MS} passes, or the run is
|
|
3209
|
+
* cancelled. A timeout or a cancellation aborts the signal `discover` was given, which aborts the
|
|
3210
|
+
* decision call, and returns a failed discovery at once. What `discover` returns after that
|
|
3211
|
+
* suggests nothing, but the prompt run of its decision call is still linked to `step` once it
|
|
3212
|
+
* settles ({@link linkLateDecisionPromptRun}), so the run counts the call's cost.
|
|
3213
|
+
*/
|
|
3214
|
+
async boundDecisionDiscovery(discover, step) {
|
|
3215
|
+
const controller = new AbortController();
|
|
3216
|
+
let stoppedOutcome;
|
|
3217
|
+
const stopped = new Promise(resolve => controller.signal.addEventListener('abort', () => {
|
|
3218
|
+
stoppedOutcome = FailedDecisionDiscovery(String(controller.signal.reason));
|
|
3219
|
+
resolve(stoppedOutcome);
|
|
3220
|
+
}, { once: true }));
|
|
3221
|
+
const runToken = this._executeParams?.cancellationToken;
|
|
3222
|
+
const relayRunAbort = () => controller.abort('the run was cancelled');
|
|
3223
|
+
if (runToken?.aborted) {
|
|
3224
|
+
relayRunAbort();
|
|
3225
|
+
}
|
|
3226
|
+
else {
|
|
3227
|
+
runToken?.addEventListener('abort', relayRunAbort, { once: true });
|
|
3228
|
+
}
|
|
3229
|
+
const timeoutMS = DECISION_DISCOVERY_TIMEOUT_MS;
|
|
3230
|
+
const timer = setTimeout(() => controller.abort(`the decision timed out after ${timeoutMS}ms`), timeoutMS);
|
|
3231
|
+
try {
|
|
3232
|
+
const discovering = discover(controller.signal);
|
|
3233
|
+
const outcome = await Promise.race([discovering, stopped]);
|
|
3234
|
+
if (outcome === stoppedOutcome) {
|
|
3235
|
+
this.linkLateDecisionPromptRun(step, discovering.then(late => late.Result));
|
|
3236
|
+
}
|
|
3237
|
+
return outcome;
|
|
3238
|
+
}
|
|
3239
|
+
catch (error) {
|
|
3240
|
+
return FailedDecisionDiscovery(error instanceof Error ? error.message : String(error));
|
|
3241
|
+
}
|
|
3242
|
+
finally {
|
|
3243
|
+
clearTimeout(timer);
|
|
3244
|
+
runToken?.removeEventListener('abort', relayRunAbort);
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* The discovery itself: the options, rebuilt from the permitted catalog, then one decision about
|
|
3249
|
+
* the opening request, and what its answer means. Never throws.
|
|
3250
|
+
*/
|
|
3251
|
+
async discoverAgent(agent, contextUser, promptName, hostAllowedIDs, signal) {
|
|
3252
|
+
try {
|
|
3253
|
+
const { Options: options, Error: optionsError, ...rest } = await this.decisionDiscoveryOptionSet(agent, contextUser, promptName, hostAllowedIDs);
|
|
3254
|
+
const sizes = { ...rest, OptionCount: options.length };
|
|
3255
|
+
if (optionsError) {
|
|
3256
|
+
return { ...FailedDecisionDiscovery(optionsError), ...sizes };
|
|
3257
|
+
}
|
|
3258
|
+
if (options.length < DECISION_DISCOVERY_MIN_OPTIONS) {
|
|
3259
|
+
return { Injected: false, Succeeded: true, Reason: `there are ${options.length} agents to choose from, fewer than the ${DECISION_DISCOVERY_MIN_OPTIONS} a suggestion needs, so nothing was asked`, ...sizes };
|
|
3260
|
+
}
|
|
3261
|
+
if (signal.aborted) {
|
|
3262
|
+
return { ...FailedDecisionDiscovery(String(signal.reason)), ...sizes };
|
|
3263
|
+
}
|
|
3264
|
+
const result = await this._agentDecisionService.Ask({
|
|
3265
|
+
State: this._openingRequest,
|
|
3266
|
+
Questions: BuildDecisionDiscoveryQuestions(options),
|
|
3267
|
+
ContextUser: contextUser,
|
|
3268
|
+
AgentID: agent.ID,
|
|
3269
|
+
PromptName: promptName,
|
|
3270
|
+
CancellationToken: signal,
|
|
3271
|
+
ExecutionScope: this._runExecutionScope,
|
|
3272
|
+
});
|
|
3273
|
+
return { ...DecisionDiscoveryFromResult(result, options, DECISION_DISCOVERY_MIN_CONFIDENCE), ...sizes };
|
|
3274
|
+
}
|
|
3275
|
+
catch (error) {
|
|
3276
|
+
return FailedDecisionDiscovery(error instanceof Error ? error.message : String(error));
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
/**
|
|
3280
|
+
* The Choice's options, rebuilt on every call and never cached ({@link BuildDecisionDiscoveryOptionSet}):
|
|
3281
|
+
* from AIEngine's current catalog, the agents the user may run, the decision prompt's option cap,
|
|
3282
|
+
* and the semantic search over the opening request when the provider can run it.
|
|
3283
|
+
*/
|
|
3284
|
+
async decisionDiscoveryOptionSet(agent, contextUser, promptName, hostAllowedIDs) {
|
|
3285
|
+
const provider = this.ProviderToUse;
|
|
3286
|
+
return BuildDecisionDiscoveryOptionSet({
|
|
3287
|
+
Agents: await DecisionDiscoveryRunnableAgents(AIEngine.Instance.Agents, contextUser),
|
|
3288
|
+
RunningAgentID: agent.ID,
|
|
3289
|
+
HostAllowedIDs: hostAllowedIDs,
|
|
3290
|
+
DeclaredCap: DecisionPromptOptionCap(AIEngine.Instance, promptName),
|
|
3291
|
+
Search: CanSearchEntities(provider) ? DecisionDiscoveryAgentSearch(provider, this._openingRequest, contextUser) : undefined,
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
3294
|
+
/** Opens the `Agent discovery` Decision step. Without a run there is no step. */
|
|
3295
|
+
async startDecisionDiscoveryStep(contextUser, promptName) {
|
|
3296
|
+
if (!this._agentRun) {
|
|
3297
|
+
return undefined;
|
|
3298
|
+
}
|
|
3299
|
+
try {
|
|
3300
|
+
return await this.createStepEntity({
|
|
3301
|
+
stepType: 'Decision',
|
|
3302
|
+
stepName: 'Agent discovery',
|
|
3303
|
+
contextUser,
|
|
3304
|
+
inputData: {
|
|
3305
|
+
request: this._openingRequest,
|
|
3306
|
+
promptName,
|
|
3307
|
+
minConfidence: DECISION_DISCOVERY_MIN_CONFIDENCE,
|
|
3308
|
+
timeoutMS: DECISION_DISCOVERY_TIMEOUT_MS,
|
|
3309
|
+
},
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
catch (error) {
|
|
3313
|
+
LogError(`Could not record the agent discovery step: ${error instanceof Error ? error.message : String(error)}`);
|
|
3314
|
+
return undefined;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Finalizes the `Agent discovery` step: the answer, whether it was injected, the host allow-list,
|
|
3319
|
+
* the agents left out for having no description (at most {@link DECISION_DISCOVERY_MAX_RECORDED_IDS}
|
|
3320
|
+
* IDs), any narrowing, and the call's usage.
|
|
3321
|
+
*/
|
|
3322
|
+
async finishDecisionDiscoveryStep(step, outcome) {
|
|
3323
|
+
if (!step) {
|
|
3324
|
+
return;
|
|
3325
|
+
}
|
|
3326
|
+
const result = outcome.Result;
|
|
3327
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
3328
|
+
if (result) {
|
|
3329
|
+
this.attachDecisionPromptRun(step, result);
|
|
3330
|
+
}
|
|
3331
|
+
const answer = outcome.Answer;
|
|
3332
|
+
const withoutDescription = outcome.WithoutDescription ?? [];
|
|
3333
|
+
await this.finalizeStepEntity(step, outcome.Succeeded, outcome.Succeeded ? undefined : outcome.Reason, {
|
|
3334
|
+
injected: outcome.Injected,
|
|
3335
|
+
reason: outcome.Reason,
|
|
3336
|
+
catalogSize: outcome.CatalogSize,
|
|
3337
|
+
hostAllowList: outcome.HostAllowListSize === undefined ? false : { size: outcome.HostAllowListSize },
|
|
3338
|
+
withoutDescription: { count: withoutDescription.length, agentIds: withoutDescription.slice(0, DECISION_DISCOVERY_MAX_RECORDED_IDS) },
|
|
3339
|
+
options: outcome.OptionCount,
|
|
3340
|
+
optionLimit: outcome.OptionLimit,
|
|
3341
|
+
declaredOptionCap: outcome.DeclaredOptionCap,
|
|
3342
|
+
narrowed: outcome.NarrowedFrom === undefined ? false : { by: 'semantic search', from: outcome.NarrowedFrom, to: outcome.OptionCount },
|
|
3343
|
+
answer: answer ? {
|
|
3344
|
+
agentId: answer.Agent.ID,
|
|
3345
|
+
agent: answer.Agent.Name,
|
|
3346
|
+
confidence: answer.Confidence,
|
|
3347
|
+
anyApplies: answer.AnyApplies,
|
|
3348
|
+
probabilities: answer.Probabilities,
|
|
3349
|
+
} : undefined,
|
|
3350
|
+
executionTimeMS: result?.executionTimeMS,
|
|
3351
|
+
tokensUsed: result?.tokensUsed,
|
|
3352
|
+
model: result?.DecisionResult?.ResolvedModel ?? result?.modelInfo?.modelName,
|
|
3353
|
+
promptRunId: result?.promptRun?.ID,
|
|
3354
|
+
});
|
|
3355
|
+
}
|
|
3356
|
+
/** Logs that decision discovery failed, so nothing was suggested. */
|
|
3357
|
+
warnDecisionDiscovery(agent, reason) {
|
|
3358
|
+
LogErrorEx({
|
|
3359
|
+
message: `Decision discovery for '${agent.Name}' failed, so no agent is suggested: ${reason}`,
|
|
3360
|
+
severity: 'warning',
|
|
3361
|
+
category: 'DecisionDiscovery',
|
|
3362
|
+
});
|
|
3363
|
+
}
|
|
2964
3364
|
/**
|
|
2965
3365
|
* Converts UI markup (@{...} syntax) in user messages to plain text.
|
|
2966
3366
|
* This prevents agents from being confused by UI-specific JSON syntax and reduces token usage.
|
|
@@ -3437,6 +3837,9 @@ export class BaseAgent {
|
|
|
3437
3837
|
return;
|
|
3438
3838
|
}
|
|
3439
3839
|
// ...and a per-agent-ACTION gate: rows that opt out are removed before the tool set is built.
|
|
3840
|
+
// Catalog narrowing (plan Task 3.7) does not apply here, by design: it shortens the prose
|
|
3841
|
+
// catalog only. A call to an undeclared tool is a Retry, and this mode has no `Actions`
|
|
3842
|
+
// envelope step, so a hidden action or sub-agent left undeclared could not be called at all.
|
|
3440
3843
|
const actions = FilterDeclarableActions(this.getEffectiveActionsForValidation(params.agent.ID), AIEngine.Instance.AgentActions.filter((aa) => UUIDsEqual(aa.AgentID, params.agent.ID)));
|
|
3441
3844
|
const subAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
3442
3845
|
if (actions.length === 0 && subAgents.length === 0) {
|
|
@@ -3464,15 +3867,22 @@ export class BaseAgent {
|
|
|
3464
3867
|
/**
|
|
3465
3868
|
* The `tool_choice` for this turn (§8.3).
|
|
3466
3869
|
*
|
|
3467
|
-
* `'auto'` normally
|
|
3870
|
+
* `'auto'` normally; on the last turn this run will be allowed, a forced `complete_task` call.
|
|
3468
3871
|
*
|
|
3469
3872
|
* **Why the final turn is special.** A tool call is a request to continue: the framework runs
|
|
3470
3873
|
* the action, feeds the result back, and the model decides again. On the last permitted
|
|
3471
3874
|
* iteration there is no "again" — the limit check fires the moment the turn returns, so the
|
|
3472
3875
|
* action is executed, paid for, and its result discarded, and the run ends with no answer for
|
|
3473
3876
|
* the user because the model spent its last turn asking a question instead of answering one.
|
|
3474
|
-
*
|
|
3475
|
-
*
|
|
3877
|
+
* The final turn is forced to what the framework actually needs from it: a terminal answer.
|
|
3878
|
+
*
|
|
3879
|
+
* **Why `complete_task` and not `'none'`.** Under implicit control flow, `'none'` forbids the
|
|
3880
|
+
* model's only way to store its result — `payload_change_request` and `complete_task` are tools
|
|
3881
|
+
* too — so a forced `'none'` turn can end the run but never deliver the payload. Naming
|
|
3882
|
+
* `complete_task` forces the one call that does both. A model that is NOT on implicit control
|
|
3883
|
+
* flow never receives `complete_task` (the runner strips control tools), and the runner then
|
|
3884
|
+
* downgrades the named choice to `'none'`: the envelope turn the hybrid needs. Providers also
|
|
3885
|
+
* enforce a named choice more reliably than `'none'`.
|
|
3476
3886
|
*
|
|
3477
3887
|
* **What this does NOT fix.** Models call a tool on a measurable share of turns whose right
|
|
3478
3888
|
* answer was chat, completion or delegation. Those are not predictable
|
|
@@ -3480,14 +3890,14 @@ export class BaseAgent {
|
|
|
3480
3890
|
* address them. That belongs to the prompt, and is why the native-mode Actions section names
|
|
3481
3891
|
* the cases explicitly.
|
|
3482
3892
|
*
|
|
3483
|
-
* Subclasses may narrow this further; the base contract is that a forced
|
|
3484
|
-
* relaxed to `'auto'` on a turn the framework has already decided is terminal.
|
|
3893
|
+
* Subclasses may narrow this further; the base contract is that a forced terminal choice must
|
|
3894
|
+
* never be relaxed to `'auto'` on a turn the framework has already decided is terminal.
|
|
3485
3895
|
*
|
|
3486
3896
|
* @param params The run parameters, for the per-run iteration override
|
|
3487
|
-
* @returns `'
|
|
3897
|
+
* @returns `{ name: 'complete_task' }` on the final permitted iteration, otherwise `'auto'`
|
|
3488
3898
|
*/
|
|
3489
3899
|
resolveToolChoiceForTurn(params) {
|
|
3490
|
-
return this.isFinalPermittedIteration(params) ?
|
|
3900
|
+
return this.isFinalPermittedIteration(params) ? { name: COMPLETE_TASK_TOOL } : 'auto';
|
|
3491
3901
|
}
|
|
3492
3902
|
/**
|
|
3493
3903
|
* Whether the turn currently being prepared is the last one this run will be allowed.
|
|
@@ -3668,6 +4078,7 @@ export class BaseAgent {
|
|
|
3668
4078
|
if (params.apiKeys && params.apiKeys.length > 0) {
|
|
3669
4079
|
childPromptParams.apiKeys = params.apiKeys;
|
|
3670
4080
|
}
|
|
4081
|
+
childPromptParams.CredentialScope = params.CredentialScope;
|
|
3671
4082
|
// Pass through configurationId to both parent and child prompts if provided
|
|
3672
4083
|
if (params.configurationId) {
|
|
3673
4084
|
promptParams.configurationId = params.configurationId;
|
|
@@ -3703,6 +4114,10 @@ export class BaseAgent {
|
|
|
3703
4114
|
promptParams.apiKeys = params.apiKeys;
|
|
3704
4115
|
this.logStatus(`🔑 Using ${params.apiKeys.length} API key(s) provided at runtime`, true, params);
|
|
3705
4116
|
}
|
|
4117
|
+
promptParams.CredentialScope = params.CredentialScope;
|
|
4118
|
+
if (!CredentialScopeAllows(params.CredentialScope, 'Environment')) {
|
|
4119
|
+
this.logStatus(`🔒 Credential scope is ${params.CredentialScope}: the platform's environment API keys will not be used`, true, params);
|
|
4120
|
+
}
|
|
3706
4121
|
// Thread the per-request provider so prompt run records are saved through the isolated provider
|
|
3707
4122
|
promptParams.provider = params.provider || this._activeProvider;
|
|
3708
4123
|
// Overlay scope-resolved run settings (model / vendor / configuration / effort / sampling
|
|
@@ -4741,23 +5156,96 @@ export class BaseAgent {
|
|
|
4741
5156
|
// Default behavior: let Chat propagate up (no remapping)
|
|
4742
5157
|
return nextStep;
|
|
4743
5158
|
}
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
5159
|
+
async applySelfCheck(params, nextStep, agentRun, currentPayload) {
|
|
5160
|
+
if (params.cancellationToken?.aborted)
|
|
5161
|
+
return null;
|
|
5162
|
+
const provider = params.provider || this._activeProvider;
|
|
5163
|
+
let row = this._selfCheckLinkByAgent.get(agentRun.AgentID);
|
|
5164
|
+
if (row === undefined) {
|
|
5165
|
+
try {
|
|
5166
|
+
const view = RunView.FromMetadataProvider(provider);
|
|
5167
|
+
const found = await view.RunView({
|
|
5168
|
+
EntityName: 'MJ: AI Agent Rubrics',
|
|
5169
|
+
ExtraFilter: `AgentID='${agentRun.AgentID}' AND Purpose='SelfCheck' AND Status='Active'`,
|
|
5170
|
+
OrderBy: 'ID',
|
|
5171
|
+
ResultType: 'simple',
|
|
5172
|
+
}, params.contextUser);
|
|
5173
|
+
if (!found.Success) {
|
|
5174
|
+
LogError(`Self-check lookup failed, so the run continues: ${found.ErrorMessage || 'the view did not succeed'}`);
|
|
5175
|
+
return null;
|
|
5176
|
+
}
|
|
5177
|
+
row = PickSelfCheckLink((found.Results ?? [])) ?? null;
|
|
5178
|
+
this._selfCheckLinkByAgent.set(agentRun.AgentID, row);
|
|
5179
|
+
}
|
|
5180
|
+
catch (error) {
|
|
5181
|
+
LogError(`Self-check lookup failed, so the run continues: ${error instanceof Error ? error.message : String(error)}`);
|
|
5182
|
+
return null;
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
if (!row?.RubricID)
|
|
5186
|
+
return null;
|
|
5187
|
+
const link = {
|
|
5188
|
+
Purpose: String(row.Purpose ?? ''),
|
|
5189
|
+
Status: String(row.Status ?? ''),
|
|
5190
|
+
MaxAttempts: row.MaxSelfCheckAttempts ?? null,
|
|
5191
|
+
rubricId: String(row.RubricID),
|
|
5192
|
+
passThreshold: row.PassThreshold ?? null,
|
|
5193
|
+
evaluatorConfig: row.EvaluatorConfig ?? null,
|
|
5194
|
+
};
|
|
5195
|
+
const attempt = (this._selfCheckAttemptsByRun.get(agentRun.ID) ?? 0) + 1;
|
|
5196
|
+
this._selfCheckAttemptsByRun.set(agentRun.ID, attempt);
|
|
5197
|
+
const record = async (step) => {
|
|
5198
|
+
const saved = await this.createStepEntity({
|
|
5199
|
+
stepType: 'Validation',
|
|
5200
|
+
stepName: 'Rubric self-check',
|
|
5201
|
+
contextUser: params.contextUser,
|
|
5202
|
+
inputData: { rubricEvaluationId: step.EvaluationId, passed: step.Passed },
|
|
5203
|
+
});
|
|
5204
|
+
await this.finalizeStepEntity(saved, step.Passed, step.Message || undefined);
|
|
5205
|
+
};
|
|
5206
|
+
try {
|
|
5207
|
+
const outcome = await ExecuteSelfCheck({
|
|
5208
|
+
engine: ProviderRubricEngine(provider, params.contextUser, this.runPromptExecutionScope(params)),
|
|
5209
|
+
link,
|
|
5210
|
+
runId: agentRun.ID,
|
|
5211
|
+
agentKind: this.AgentTypeInstance?.constructor?.name === 'LoopAgentType' ? 'loop' : 'flow',
|
|
5212
|
+
attempt,
|
|
5213
|
+
candidate: { Message: nextStep.message, Payload: nextStep.newPayload ?? currentPayload },
|
|
5214
|
+
record,
|
|
5215
|
+
});
|
|
5216
|
+
if (outcome.step === 'Success')
|
|
5217
|
+
return null;
|
|
5218
|
+
if (outcome.step === 'Retry')
|
|
5219
|
+
return { ...nextStep, step: 'Retry', message: outcome.decision.Message };
|
|
5220
|
+
return { ...nextStep, step: 'Failed', message: outcome.decision.Message, errorMessage: outcome.decision.Message };
|
|
5221
|
+
}
|
|
5222
|
+
catch (error) {
|
|
5223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5224
|
+
LogError(`Self-check could not be saved, so the run continues: ${message}`);
|
|
5225
|
+
try {
|
|
5226
|
+
await record({ Passed: false, Message: message });
|
|
5227
|
+
}
|
|
5228
|
+
catch (recordError) {
|
|
5229
|
+
LogError(`Self-check validation step could not be recorded: ${recordError instanceof Error ? recordError.message : String(recordError)}`);
|
|
5230
|
+
}
|
|
5231
|
+
return null;
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
/** Runs the self-check when a success exit did not pass through the guardrail hook. */
|
|
5235
|
+
async selfCheckSuccess(params, step, payload) {
|
|
5236
|
+
if (step.step !== 'Success' || !this._agentRun)
|
|
5237
|
+
return step;
|
|
5238
|
+
const checked = await this.applySelfCheck(params, step, this._agentRun, payload);
|
|
5239
|
+
return checked ?? step;
|
|
5240
|
+
}
|
|
4756
5241
|
async checkExecutionGuardrails(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
4757
|
-
|
|
4758
|
-
if (nextStep.step === 'Success' || nextStep.step === 'Failed' || nextStep.step === 'Chat') {
|
|
5242
|
+
if (nextStep.step === 'Failed' || nextStep.step === 'Chat') {
|
|
4759
5243
|
return nextStep;
|
|
4760
5244
|
}
|
|
5245
|
+
if (nextStep.step === 'Success') {
|
|
5246
|
+
const checked = await this.applySelfCheck(params, nextStep, agentRun, currentPayload);
|
|
5247
|
+
return checked ?? nextStep;
|
|
5248
|
+
}
|
|
4761
5249
|
// Check if any guardrails are exceeded
|
|
4762
5250
|
const guardrailResult = await this.hasExceededAgentRunGuardrails(params, agentRun);
|
|
4763
5251
|
if (guardrailResult.exceeded) {
|
|
@@ -5668,14 +6156,24 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5668
6156
|
this.appendNativeAssistantTurn(params, guardrailCheckedStep);
|
|
5669
6157
|
// if we need to retry make sure we add the retry message to the conversation messages
|
|
5670
6158
|
if (guardrailCheckedStep.step === 'Retry' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
5671
|
-
//
|
|
6159
|
+
// A payload-only turn, or a complete_task that failed Success validation, is answered as a
|
|
6160
|
+
// tool result for that call — the feedback is what the model reads next.
|
|
5672
6161
|
params.conversationMessages.push(BuildToolResultTurn([{
|
|
5673
6162
|
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
5674
|
-
toolName:
|
|
6163
|
+
toolName: this.nativeToolNameForCall(guardrailCheckedStep, guardrailCheckedStep.payloadToolCallId),
|
|
5675
6164
|
content: guardrailCheckedStep.retryInstructions || 'Payload change applied.',
|
|
5676
6165
|
isError: false
|
|
5677
6166
|
}], { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
5678
6167
|
}
|
|
6168
|
+
else if (guardrailCheckedStep.step === 'Success' && guardrailCheckedStep.payloadToolCallId && guardrailCheckedStep.nativeTurn?.sendResultsNatively) {
|
|
6169
|
+
// complete_task ended the run: answer the call so the history never ends on a dangling tool_use.
|
|
6170
|
+
params.conversationMessages.push(BuildToolResultTurn([{
|
|
6171
|
+
toolCallId: guardrailCheckedStep.payloadToolCallId,
|
|
6172
|
+
toolName: this.nativeToolNameForCall(guardrailCheckedStep, guardrailCheckedStep.payloadToolCallId),
|
|
6173
|
+
content: 'Task complete.',
|
|
6174
|
+
isError: false
|
|
6175
|
+
}], { turnAdded: this._promptTurnCount, messageType: 'action-result' }));
|
|
6176
|
+
}
|
|
5679
6177
|
else if (guardrailCheckedStep.step === 'Retry' && (guardrailCheckedStep.message || guardrailCheckedStep.errorMessage || guardrailCheckedStep.retryInstructions)) {
|
|
5680
6178
|
params.conversationMessages.push({
|
|
5681
6179
|
role: 'user',
|
|
@@ -5685,6 +6183,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5685
6183
|
// Return the next step directly - execution handling is done in execute NextStep
|
|
5686
6184
|
return guardrailCheckedStep;
|
|
5687
6185
|
}
|
|
6186
|
+
/** The tool name the model used for a call on this turn — `payload_change_request` when it cannot be found. */
|
|
6187
|
+
nativeToolNameForCall(step, toolCallId) {
|
|
6188
|
+
return step.nativeTurn?.toolCalls.find((c) => c.id === toolCallId)?.name ?? 'payload_change_request';
|
|
6189
|
+
}
|
|
5688
6190
|
/**
|
|
5689
6191
|
* Executes a batch of artifact tool calls, recording each as its own
|
|
5690
6192
|
* `Tool` AIAgentRunStep (a sibling of the Prompt step that requested them)
|
|
@@ -5979,10 +6481,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5979
6481
|
throw new Error(`The '${BaseAgent.SummarizeRangePromptName}' system prompt is not present in this environment`);
|
|
5980
6482
|
}
|
|
5981
6483
|
const promptParams = new AIPromptParams();
|
|
6484
|
+
Object.assign(promptParams, this.runPromptExecutionScope(params));
|
|
5982
6485
|
promptParams.prompt = prompt;
|
|
5983
6486
|
// Keys are the summarize-range.template.md contract ({{ lens }}, {{ messages }})
|
|
5984
6487
|
promptParams.data = { lens, messages: rangeText };
|
|
5985
|
-
promptParams.contextUser = params.contextUser;
|
|
5986
6488
|
promptParams.agentId = params.agent.ID;
|
|
5987
6489
|
promptParams.UserID = ResolvePromptRunUserID({
|
|
5988
6490
|
UserID: params.userId,
|
|
@@ -6412,68 +6914,542 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6412
6914
|
};
|
|
6413
6915
|
params.conversationMessages.push(message);
|
|
6414
6916
|
}
|
|
6917
|
+
/** At most this many decision calls run at once for one agent turn. */
|
|
6918
|
+
static { this.DECISION_CONCURRENCY = 8; }
|
|
6415
6919
|
/**
|
|
6416
|
-
*
|
|
6417
|
-
*
|
|
6418
|
-
*
|
|
6419
|
-
* @param {any} result - The result from sub-agent execution
|
|
6420
|
-
* @returns {ChatMessage} A formatted message with sub-agent results
|
|
6421
|
-
* @protected
|
|
6920
|
+
* Runs `fn` over `items` with at most `limit` calls in flight, keeping results in input order.
|
|
6921
|
+
* One call's failure never rejects the others: a call that throws is settled with `onError`'s
|
|
6922
|
+
* result in its place.
|
|
6422
6923
|
*/
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6924
|
+
async runWithConcurrencyLimit(items, limit, fn, onError) {
|
|
6925
|
+
const results = new Array(items.length);
|
|
6926
|
+
let next = 0;
|
|
6927
|
+
const worker = async () => {
|
|
6928
|
+
while (next < items.length) {
|
|
6929
|
+
const index = next++;
|
|
6930
|
+
try {
|
|
6931
|
+
results[index] = await fn(items[index], index);
|
|
6932
|
+
}
|
|
6933
|
+
catch (error) {
|
|
6934
|
+
results[index] = onError(error, items[index], index);
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6427
6937
|
};
|
|
6938
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
6939
|
+
return results;
|
|
6428
6940
|
}
|
|
6429
6941
|
/**
|
|
6430
|
-
*
|
|
6942
|
+
* Answers the decision requests from one agent turn, each logged as an `AIAgentRunStep`
|
|
6943
|
+
* (StepType 'Decision'), with bounded concurrency. A failed request never fails the run: it
|
|
6944
|
+
* becomes that request's `success: false`, which the agent reads on its next turn.
|
|
6431
6945
|
*
|
|
6432
|
-
*
|
|
6433
|
-
*
|
|
6434
|
-
*
|
|
6435
|
-
*
|
|
6946
|
+
* Two limits bound the turn. At most `decisionsMaxRequests` requests run (default
|
|
6947
|
+
* {@link MAX_DECISION_REQUESTS_PER_TURN}); each request over that cap is not run and records no
|
|
6948
|
+
* step, and its result says why. And the requests make at most `decisionsMaxCallsPerTurn`
|
|
6949
|
+
* decision calls in total, counting every `forEachItemIn` item (default
|
|
6950
|
+
* {@link MAX_DECISION_CALLS_PER_TURN}). That budget is handed out in request order before any
|
|
6951
|
+
* call is made, so which calls run never depends on timing. A `forEachItemIn` request it cuts
|
|
6952
|
+
* short asks its first items and reports the rest in `skippedCount`. A request it leaves no calls
|
|
6953
|
+
* for is not run, records no step, and its result says why.
|
|
6436
6954
|
*
|
|
6437
|
-
* @
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6955
|
+
* @since 2.132.0
|
|
6956
|
+
*/
|
|
6957
|
+
async executeDecisionRequestsAsSteps(requests, finalPayload, agentTypePromptParams, params) {
|
|
6958
|
+
const cap = this.decisionRequestCap(agentTypePromptParams);
|
|
6959
|
+
const toRun = requests.slice(0, cap);
|
|
6960
|
+
const overCap = requests.slice(cap);
|
|
6961
|
+
if (overCap.length > 0) {
|
|
6962
|
+
this.logStatus(`[Decisions] ${requests.length} decision requests on one turn — answering the first ${cap}, skipping ${overCap.length} (per-turn cap)`, true, params);
|
|
6963
|
+
}
|
|
6964
|
+
const budget = this.decisionCallBudget(agentTypePromptParams);
|
|
6965
|
+
const allowances = this.allotDecisionCalls(toRun, finalPayload, agentTypePromptParams, budget, params);
|
|
6966
|
+
const results = await this.runWithConcurrencyLimit(toRun, BaseAgent.DECISION_CONCURRENCY, async (req, index) => allowances[index].Wanted > 0 && allowances[index].Allowed === 0
|
|
6967
|
+
? this.overBudgetDecisionResult(req, index, budget)
|
|
6968
|
+
: this.executeSingleDecisionRequestAsStep(req, finalPayload, agentTypePromptParams, params, allowances[index].Allowed), (error, req, index) => ({
|
|
6969
|
+
id: this.decisionRequestId(req, index),
|
|
6970
|
+
success: false,
|
|
6971
|
+
error: `The decision request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6972
|
+
}));
|
|
6973
|
+
const skipped = overCap.map((req, i) => ({
|
|
6974
|
+
id: this.decisionRequestId(req, cap + i),
|
|
6975
|
+
success: false,
|
|
6976
|
+
error: `Not run: at most ${cap} decision requests are answered per turn. Ask again next turn if you still need it.`,
|
|
6977
|
+
}));
|
|
6978
|
+
return [...results, ...skipped];
|
|
6979
|
+
}
|
|
6980
|
+
/** The most decision requests one turn may run: `decisionsMaxRequests`, or {@link MAX_DECISION_REQUESTS_PER_TURN}. */
|
|
6981
|
+
decisionRequestCap(agentTypePromptParams) {
|
|
6982
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxRequests', MAX_DECISION_REQUESTS_PER_TURN);
|
|
6983
|
+
}
|
|
6984
|
+
/**
|
|
6985
|
+
* The most decision calls one turn's requests make in total, `forEachItemIn` items included:
|
|
6986
|
+
* `decisionsMaxCallsPerTurn`, or {@link MAX_DECISION_CALLS_PER_TURN}.
|
|
6987
|
+
*/
|
|
6988
|
+
decisionCallBudget(agentTypePromptParams) {
|
|
6989
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxCallsPerTurn', MAX_DECISION_CALLS_PER_TURN);
|
|
6990
|
+
}
|
|
6991
|
+
/** The most items one `forEachItemIn` request asks about: `decisionsMaxItems`, or its default. */
|
|
6992
|
+
decisionMaxItems(agentTypePromptParams) {
|
|
6993
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxItems', DEFAULT_LOOP_AGENT_PROMPT_PARAMS.decisionsMaxItems);
|
|
6994
|
+
}
|
|
6995
|
+
/** A decision limit from the prompt params when it is a non-negative integer, or `fallback` when it is absent or anything else. */
|
|
6996
|
+
decisionLimit(agentTypePromptParams, name, fallback) {
|
|
6997
|
+
const configured = agentTypePromptParams?.[name];
|
|
6998
|
+
return typeof configured === 'number' && Number.isInteger(configured) && configured >= 0 ? configured : fallback;
|
|
6999
|
+
}
|
|
7000
|
+
/**
|
|
7001
|
+
* Hands out the turn's decision-call budget to its requests in order, before any of them runs.
|
|
7002
|
+
* Each request gets the calls it wants while the budget lasts. The first one the budget cannot
|
|
7003
|
+
* cover gets what is left, and the ones after it get none.
|
|
7004
|
+
*/
|
|
7005
|
+
allotDecisionCalls(requests, finalPayload, agentTypePromptParams, budget, params) {
|
|
7006
|
+
let remaining = budget;
|
|
7007
|
+
const allowances = requests.map((request) => {
|
|
7008
|
+
const wanted = this.decisionCallsWanted(request, finalPayload, agentTypePromptParams);
|
|
7009
|
+
const allowed = Math.min(wanted, remaining);
|
|
7010
|
+
remaining -= allowed;
|
|
7011
|
+
return { Wanted: wanted, Allowed: allowed };
|
|
7012
|
+
});
|
|
7013
|
+
const cut = allowances.reduce((sum, allowance) => sum + allowance.Wanted - allowance.Allowed, 0);
|
|
7014
|
+
if (cut > 0) {
|
|
7015
|
+
this.logStatus(`[Decisions] The turn's decision requests want ${budget + cut} decision calls — making ${budget}, skipping ${cut} (per-turn call budget)`, true, params);
|
|
7016
|
+
}
|
|
7017
|
+
return allowances;
|
|
7018
|
+
}
|
|
7019
|
+
/**
|
|
7020
|
+
* The decision calls a request makes with no budget: one, or one per `forEachItemIn` item up to
|
|
7021
|
+
* `decisionsMaxItems`. A request that cannot make any, because it is not an object or its
|
|
7022
|
+
* `forEachItemIn` is not an array, wants none. A request that fails before it asks, such as one
|
|
7023
|
+
* with no valid question, still holds what its shape wants: the budget can then go unused, but
|
|
7024
|
+
* it is never exceeded.
|
|
7025
|
+
*/
|
|
7026
|
+
decisionCallsWanted(request, finalPayload, agentTypePromptParams) {
|
|
7027
|
+
if (!request || typeof request !== 'object') {
|
|
7028
|
+
return 0;
|
|
7029
|
+
}
|
|
7030
|
+
if (!request.forEachItemIn) {
|
|
7031
|
+
return 1;
|
|
7032
|
+
}
|
|
7033
|
+
const target = typeof request.forEachItemIn === 'string' ? this.resolvePayloadPath(request.forEachItemIn, finalPayload) : undefined;
|
|
7034
|
+
return Array.isArray(target) ? Math.min(target.length, this.decisionMaxItems(agentTypePromptParams)) : 0;
|
|
7035
|
+
}
|
|
7036
|
+
/**
|
|
7037
|
+
* The result of a request the turn's decision-call budget left no calls for. It was not run.
|
|
7038
|
+
* A budget of 0 turns decision calls off for the agent, so then asking again on a later turn
|
|
7039
|
+
* cannot help, and the result does not invite it.
|
|
7040
|
+
*/
|
|
7041
|
+
overBudgetDecisionResult(request, index, budget) {
|
|
7042
|
+
const error = budget === 0
|
|
7043
|
+
? 'Not run: this agent\'s decision-call budget is 0 (decisionsMaxCallsPerTurn), so it makes no decision calls on any turn. Asking again will not help: carry on without the answer.'
|
|
7044
|
+
: `Not run: one turn's decisions make at most ${budget} decision calls in total, counting each forEachItemIn item, and the requests before this one used them all. Ask again next turn if you still need it.`;
|
|
7045
|
+
return { id: this.decisionRequestId(request, index), success: false, error };
|
|
7046
|
+
}
|
|
7047
|
+
/** A request's id for its result, or its position when the model sent no usable id. */
|
|
7048
|
+
decisionRequestId(request, index) {
|
|
7049
|
+
const id = request && typeof request === 'object' ? request.id : undefined;
|
|
7050
|
+
return typeof id === 'string' && id.length > 0 ? id : `request ${index + 1}`;
|
|
7051
|
+
}
|
|
7052
|
+
/**
|
|
7053
|
+
* Runs a `'Decision'` next step: an agent type's decision requests, answered with no LLM turn.
|
|
6441
7054
|
*
|
|
6442
|
-
*
|
|
7055
|
+
* Each request goes through the same path a loop turn's decisions take
|
|
7056
|
+
* ({@link executeSingleDecisionRequestAsStep}), so it gets the same prompt default, state
|
|
7057
|
+
* resolution and error handling, and is logged as a `Decision` run step. Where a loop turn's
|
|
7058
|
+
* results go into the conversation, these come back to the agent type on the returned `'Retry'`
|
|
7059
|
+
* (`decisionResults`), because it routes on them. So each answer keeps its whole distribution
|
|
7060
|
+
* (`SummarizeDecisionAnswers`), which a path condition may read. A failed request is a result
|
|
7061
|
+
* with `success: false`, never a failed run: what a failure means is the agent type's call.
|
|
6443
7062
|
*
|
|
6444
|
-
* @
|
|
7063
|
+
* @since 6.2.0
|
|
7064
|
+
*/
|
|
7065
|
+
async executeDecisionStep(params, decision) {
|
|
7066
|
+
const payload = decision.newPayload ?? decision.previousPayload;
|
|
7067
|
+
const promptParams = decision.decisionPromptName ? { decisionPromptName: decision.decisionPromptName } : undefined;
|
|
7068
|
+
const results = await this.runWithConcurrencyLimit(decision.decisions ?? [], BaseAgent.DECISION_CONCURRENCY, request => this.executeSingleDecisionRequestAsStep(request, payload, promptParams, params, Number.POSITIVE_INFINITY, SummarizeDecisionAnswers), (error, request, index) => ({
|
|
7069
|
+
id: this.decisionRequestId(request, index),
|
|
7070
|
+
success: false,
|
|
7071
|
+
error: `The decision request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7072
|
+
}));
|
|
7073
|
+
return { step: 'Retry', terminate: false, decisionResults: results, previousPayload: payload, newPayload: payload };
|
|
7074
|
+
}
|
|
7075
|
+
/**
|
|
7076
|
+
* Answers one decision request, either once against its `state` or once per item of its
|
|
7077
|
+
* `forEachItemIn` array, and records it as a Decision step. The request is unchecked model
|
|
7078
|
+
* output, so any error finishes the step as failed rather than escaping: this never throws
|
|
7079
|
+
* once the step exists.
|
|
6445
7080
|
*
|
|
6446
|
-
* @
|
|
7081
|
+
* @param callAllowance The most decision calls the request may make, from the turn's
|
|
7082
|
+
* `decisionsMaxCallsPerTurn` budget. It caps a `forEachItemIn` request's items below
|
|
7083
|
+
* `decisionsMaxItems`. A single-state request makes one call, and
|
|
7084
|
+
* {@link executeDecisionRequestsAsSteps} never starts one the budget left without it.
|
|
7085
|
+
* Unbounded by default.
|
|
7086
|
+
* @param summarize how the typed answers are summarised for whoever reads them next; defaults
|
|
7087
|
+
* to the loop turn's model-facing summary
|
|
7088
|
+
* @since 2.132.0
|
|
6447
7089
|
*/
|
|
6448
|
-
async
|
|
7090
|
+
async executeSingleDecisionRequestAsStep(request, finalPayload, agentTypePromptParams, params, callAllowance = Number.POSITIVE_INFINITY, summarize = AgentDecisionService.SummarizeAnswers) {
|
|
7091
|
+
const step = await this.createStepEntity({
|
|
7092
|
+
stepType: 'Decision',
|
|
7093
|
+
stepName: `Decision: ${request.id}`,
|
|
7094
|
+
contextUser: params.contextUser,
|
|
7095
|
+
inputData: { id: request.id, state: request.state, forEachItemIn: request.forEachItemIn, questions: request.questions },
|
|
7096
|
+
});
|
|
6449
7097
|
try {
|
|
6450
|
-
const
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
}
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
7098
|
+
const mapping = request.questions && typeof request.questions === 'object'
|
|
7099
|
+
? AgentDecisionService.ToDecisionQuestions(request.questions)
|
|
7100
|
+
: { Questions: {}, Invalid: [] };
|
|
7101
|
+
if (Object.keys(mapping.Questions).length === 0) {
|
|
7102
|
+
const error = mapping.Invalid.length > 0
|
|
7103
|
+
? `No valid questions: ${mapping.Invalid.join('; ')}`
|
|
7104
|
+
: 'At least one question is required';
|
|
7105
|
+
return await this.finishDecisionStep(step, request, { Error: error });
|
|
7106
|
+
}
|
|
7107
|
+
if (mapping.Invalid.length > 0) {
|
|
7108
|
+
this.logStatus(`[Decisions] "${request.id}": dropped ${mapping.Invalid.length} invalid question(s): ${mapping.Invalid.join('; ')}`, true, params);
|
|
7109
|
+
}
|
|
7110
|
+
const ask = this.decisionAsker(mapping.Questions, agentTypePromptParams, params);
|
|
7111
|
+
const outcome = request.forEachItemIn
|
|
7112
|
+
? await this.askForEachItem(request, finalPayload, agentTypePromptParams, callAllowance, params, step, ask, summarize)
|
|
7113
|
+
: await this.askOnce(request, finalPayload, step, ask, summarize);
|
|
7114
|
+
return await this.finishDecisionStep(step, request, outcome, mapping.Invalid);
|
|
7115
|
+
}
|
|
7116
|
+
catch (error) {
|
|
7117
|
+
const message = `The decision request failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
7118
|
+
return this.finishDecisionStep(step, request, { Error: message });
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
/** Asks the request's questions about one state, with the configured decision prompt. */
|
|
7122
|
+
decisionAsker(questions, agentTypePromptParams, params) {
|
|
7123
|
+
const promptName = typeof agentTypePromptParams?.decisionPromptName === 'string'
|
|
7124
|
+
? agentTypePromptParams.decisionPromptName
|
|
7125
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
7126
|
+
return (state) => this._agentDecisionService.Ask({
|
|
7127
|
+
State: state,
|
|
7128
|
+
Questions: questions,
|
|
7129
|
+
ContextUser: params.contextUser,
|
|
7130
|
+
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
7131
|
+
PromptName: promptName,
|
|
7132
|
+
CancellationToken: params.cancellationToken,
|
|
7133
|
+
ExecutionScope: this.runPromptExecutionScope(params),
|
|
7134
|
+
});
|
|
7135
|
+
}
|
|
7136
|
+
/** Asks the questions once, against the request's state. */
|
|
7137
|
+
async askOnce(request, finalPayload, step, ask, summarize) {
|
|
7138
|
+
const state = this.resolveDecisionState(request.state, finalPayload);
|
|
7139
|
+
if ('Error' in state) {
|
|
7140
|
+
return state;
|
|
7141
|
+
}
|
|
7142
|
+
const result = await ask(state.State);
|
|
7143
|
+
this.attachDecisionPromptRun(step, result);
|
|
7144
|
+
if (!result.success) {
|
|
7145
|
+
return { Error: result.errorMessage || 'Decision evaluation failed' };
|
|
7146
|
+
}
|
|
7147
|
+
return { Answers: summarize(result.Answers), Results: [result] };
|
|
7148
|
+
}
|
|
7149
|
+
/**
|
|
7150
|
+
* Asks the same questions of each item of a payload array, up to `decisionsMaxItems` and the
|
|
7151
|
+
* request's share of the turn's call budget. Each item's call is its own child Decision step of
|
|
7152
|
+
* the request's step, carrying that call's prompt run, as each iteration of a ForEach is a child
|
|
7153
|
+
* step of its loop step.
|
|
7154
|
+
*/
|
|
7155
|
+
async askForEachItem(request, finalPayload, agentTypePromptParams, callAllowance, params, step, ask, summarize) {
|
|
7156
|
+
const target = this.resolvePayloadPath(request.forEachItemIn ?? '', finalPayload);
|
|
7157
|
+
if (!Array.isArray(target)) {
|
|
7158
|
+
return { Error: `forEachItemIn target "${request.forEachItemIn}" is not an array` };
|
|
7159
|
+
}
|
|
7160
|
+
const maxItems = this.decisionMaxItems(agentTypePromptParams);
|
|
7161
|
+
const limit = Math.min(maxItems, callAllowance);
|
|
7162
|
+
const items = target.slice(0, limit);
|
|
7163
|
+
const skippedCount = target.length > limit ? target.length - limit : undefined;
|
|
7164
|
+
if (skippedCount) {
|
|
7165
|
+
const why = limit < maxItems ? "the turn's decision-call budget" : 'decisionsMaxItems';
|
|
7166
|
+
this.logStatus(`[Decisions] "${request.forEachItemIn}" has ${target.length} items; asking the first ${limit} (${skippedCount} skipped: ${why})`, true, params);
|
|
7167
|
+
}
|
|
7168
|
+
const results = await this.runWithConcurrencyLimit(items, BaseAgent.DECISION_CONCURRENCY, (item, index) => this.askItemAsStep(request, item, index, step, ask, params), (error) => ({ success: false, errorMessage: error instanceof Error ? error.message : String(error), Answers: {} }));
|
|
7169
|
+
const failedIndex = results.findIndex(r => !r.success);
|
|
7170
|
+
if (failedIndex >= 0) {
|
|
7171
|
+
return { Error: `Item ${failedIndex}: ${results[failedIndex].errorMessage || 'decision evaluation failed'}` };
|
|
7172
|
+
}
|
|
7173
|
+
return { Answers: results.map(r => summarize(r.Answers)), Results: results, SkippedCount: skippedCount };
|
|
7174
|
+
}
|
|
7175
|
+
/** Asks about one item of a `forEachItemIn` request, recorded as a child step of the request's step. */
|
|
7176
|
+
async askItemAsStep(request, item, index, parentStep, ask, params) {
|
|
7177
|
+
const itemStep = await this.createStepEntity({
|
|
7178
|
+
stepType: 'Decision',
|
|
7179
|
+
stepName: `Decision: ${request.id} (item ${index})`,
|
|
7180
|
+
contextUser: params.contextUser,
|
|
7181
|
+
inputData: { id: request.id, item: index },
|
|
7182
|
+
parentId: parentStep.ID,
|
|
7183
|
+
});
|
|
7184
|
+
let result;
|
|
7185
|
+
try {
|
|
7186
|
+
result = await ask(this.decisionStateText(item));
|
|
7187
|
+
}
|
|
7188
|
+
catch (error) {
|
|
7189
|
+
result = { success: false, errorMessage: `The decision call failed: ${error instanceof Error ? error.message : String(error)}`, Answers: {} };
|
|
7190
|
+
}
|
|
7191
|
+
this.attachDecisionPromptRun(itemStep, result);
|
|
7192
|
+
await this.finalizeStepEntity(itemStep, result.success, result.success ? undefined : result.errorMessage, result.success ? { answers: AgentDecisionService.SummarizeAnswers(result.Answers) } : undefined);
|
|
7193
|
+
return result;
|
|
7194
|
+
}
|
|
7195
|
+
/**
|
|
7196
|
+
* Links a decision call's prompt run to its step, as the Prompt step links its own, so the run's
|
|
7197
|
+
* cost and token totals and its `MaxCostPerRun` / `MaxTokensPerRun` checks count the call.
|
|
7198
|
+
*/
|
|
7199
|
+
attachDecisionPromptRun(step, result) {
|
|
7200
|
+
if (result.promptRun?.ID) {
|
|
7201
|
+
step.TargetLogID = result.promptRun.ID;
|
|
7202
|
+
step.PromptRun = result.promptRun; // transient related object (not a persisted field)
|
|
7203
|
+
}
|
|
7204
|
+
}
|
|
7205
|
+
/**
|
|
7206
|
+
* Links the prompt run of a decision call the run stopped waiting for (a timeout or a cancellation)
|
|
7207
|
+
* to its step, once the call settles. `AIDecisionRunner` writes the prompt run before it calls the
|
|
7208
|
+
* model, so a stopped call still has one. The link keeps that row on the run's trace, and counts its
|
|
7209
|
+
* usage in the run's cost and token totals when it settles before the run ends, as
|
|
7210
|
+
* {@link attachDecisionPromptRun} does on time. The step may already be finalized, so the link is
|
|
7211
|
+
* saved on its own. Never throws, and a call that never settles links nothing.
|
|
7212
|
+
*/
|
|
7213
|
+
linkLateDecisionPromptRun(step, late) {
|
|
7214
|
+
if (!step) {
|
|
7215
|
+
return;
|
|
7216
|
+
}
|
|
7217
|
+
late.then(result => {
|
|
7218
|
+
const promptRunID = result?.promptRun?.ID;
|
|
7219
|
+
if (!result || !promptRunID || step.TargetLogID === promptRunID) {
|
|
7220
|
+
return;
|
|
7221
|
+
}
|
|
7222
|
+
this.attachDecisionPromptRun(step, result);
|
|
7223
|
+
this.queueStepSave(step, (s) => { s.TargetLogID = promptRunID; });
|
|
7224
|
+
}).catch(error => {
|
|
7225
|
+
LogError(`Could not link a stopped decision call's prompt run to its step: ${error instanceof Error ? error.message : String(error)}`);
|
|
7226
|
+
});
|
|
7227
|
+
}
|
|
7228
|
+
/**
|
|
7229
|
+
* A literal state, or a value read from the payload: the whole payload for `payload`, one value
|
|
7230
|
+
* for a `payload.` path.
|
|
7231
|
+
*/
|
|
7232
|
+
resolveDecisionState(state, finalPayload) {
|
|
7233
|
+
if (typeof state !== 'string') {
|
|
7234
|
+
return { Error: 'Either state or forEachItemIn is required' };
|
|
7235
|
+
}
|
|
7236
|
+
if (state.trim() !== 'payload' && !state.startsWith('payload.')) {
|
|
7237
|
+
return { State: state };
|
|
7238
|
+
}
|
|
7239
|
+
const value = this.resolvePayloadPath(state, finalPayload);
|
|
7240
|
+
return value === undefined ? { Error: `Path "${state}" not found in payload` } : { State: this.decisionStateText(value) };
|
|
7241
|
+
}
|
|
7242
|
+
/** Reads a `payload.`-prefixed dotted path (or `payload` itself) from the payload. */
|
|
7243
|
+
resolvePayloadPath(path, finalPayload) {
|
|
7244
|
+
const trimmed = path.trim();
|
|
7245
|
+
if (trimmed === 'payload') {
|
|
7246
|
+
return finalPayload;
|
|
7247
|
+
}
|
|
7248
|
+
return _.get(finalPayload, trimmed.startsWith('payload.') ? trimmed.substring('payload.'.length) : trimmed);
|
|
7249
|
+
}
|
|
7250
|
+
/** Strings pass through; anything else becomes indented JSON. */
|
|
7251
|
+
decisionStateText(value) {
|
|
7252
|
+
return typeof value === 'string' ? value : (value !== null && typeof value === 'object' ? JSON.stringify(value, undefined, 1) : String(value ?? ''));
|
|
7253
|
+
}
|
|
7254
|
+
/** Finalizes the Decision step and builds the result the agent reads next turn. */
|
|
7255
|
+
async finishDecisionStep(step, request, outcome, droppedQuestions = []) {
|
|
7256
|
+
if ('Error' in outcome) {
|
|
7257
|
+
await this.finalizeStepEntity(step, false, outcome.Error, { error: outcome.Error });
|
|
7258
|
+
return { id: request.id, success: false, error: outcome.Error };
|
|
7259
|
+
}
|
|
7260
|
+
const first = outcome.Results[0];
|
|
7261
|
+
await this.finalizeStepEntity(step, true, undefined, {
|
|
7262
|
+
answers: outcome.Answers,
|
|
7263
|
+
skippedCount: outcome.SkippedCount,
|
|
7264
|
+
executionTimeMS: outcome.Results.reduce((sum, r) => sum + (r.executionTimeMS ?? 0), 0),
|
|
7265
|
+
tokensUsed: outcome.Results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0),
|
|
7266
|
+
model: first?.DecisionResult?.ResolvedModel ?? first?.modelInfo?.modelName,
|
|
7267
|
+
promptRunId: first?.promptRun?.ID,
|
|
7268
|
+
driverClass: first?.DriverClass,
|
|
7269
|
+
...(droppedQuestions.length > 0 && { droppedQuestions }),
|
|
7270
|
+
});
|
|
7271
|
+
return { id: request.id, success: true, answers: outcome.Answers, skippedCount: outcome.SkippedCount };
|
|
7272
|
+
}
|
|
7273
|
+
/**
|
|
7274
|
+
* Answers the decision requests a prompt returned, or holds them when the step that carries
|
|
7275
|
+
* them can end the run, because then no turn would read their answers. Held requests run before
|
|
7276
|
+
* the next prompt ({@link runHeldDecisions}), or are skipped if the run ends first
|
|
7277
|
+
* ({@link skipHeldDecisions}). Skips them all, unasked, while the master switch is off
|
|
7278
|
+
* ({@link DecisionsEnabled}) or the agent's `decisions` response field is.
|
|
7279
|
+
*/
|
|
7280
|
+
async processTurnDecisions(decisions, nextStep, finalPayload, agentTypePromptParams, params) {
|
|
7281
|
+
if (!this.DecisionsEnabled(agentTypePromptParams)) {
|
|
7282
|
+
this.logStatus(`[Decisions] LLM requested decisions but decision-model use is off for this agent (decisionsEnabled is not true) — skipped`, true, params);
|
|
7283
|
+
return;
|
|
7284
|
+
}
|
|
7285
|
+
const responseTypeRules = agentTypePromptParams?.includeResponseTypeDefinition;
|
|
7286
|
+
if (responseTypeRules?.decisions === false) {
|
|
7287
|
+
this.logStatus(`[Decisions] LLM requested decisions but feature is disabled (includeResponseTypeDefinition.decisions=false) — skipped`, true, params);
|
|
7288
|
+
return;
|
|
7289
|
+
}
|
|
7290
|
+
if (!Array.isArray(decisions)) {
|
|
7291
|
+
this.injectDecisionResultsMessage(params, [{ id: 'decisions', success: false, error: '"decisions" must be an array of decision requests' }]);
|
|
7292
|
+
return;
|
|
7293
|
+
}
|
|
7294
|
+
const ids = decisions.map((d, i) => this.decisionRequestId(d, i)).join(', ');
|
|
7295
|
+
const holdReason = this.decisionsHoldReason(nextStep);
|
|
7296
|
+
if (holdReason) {
|
|
7297
|
+
this._heldDecisions = { Requests: decisions, Payload: finalPayload, PromptParams: agentTypePromptParams, Reason: holdReason };
|
|
7298
|
+
this.logStatus(`[Decisions] Holding ${decisions.length} decision request(s) (${ids}) until the step has run: ${holdReason}`, true, params);
|
|
7299
|
+
return;
|
|
7300
|
+
}
|
|
7301
|
+
this.logStatus(`[Decisions] LLM requested ${decisions.length} decision request(s): ${ids}`, true, params);
|
|
7302
|
+
const decisionResults = await this.executeDecisionRequestsAsSteps(decisions, finalPayload, agentTypePromptParams, params);
|
|
7303
|
+
this.injectDecisionResultsMessage(params, decisionResults);
|
|
7304
|
+
}
|
|
7305
|
+
/**
|
|
7306
|
+
* Why the decisions sent with this step would be paid for and never read, or `undefined` when
|
|
7307
|
+
* the next turn reads them. Three ways a step's own outcome ends the run: a passing finishIf gate
|
|
7308
|
+
* (only an Actions step or a single sub-agent is gated), a sub-agent's `terminateAfter`, and
|
|
7309
|
+
* client tools sent with `taskComplete`, which end the run once the tools return. Every other
|
|
7310
|
+
* step, including client tools without `taskComplete`, goes on to a prompt that reads them.
|
|
7311
|
+
*/
|
|
7312
|
+
decisionsHoldReason(step) {
|
|
7313
|
+
const isSingleSubAgent = step.step === 'Sub-Agent' && !!step.subAgent && !step.subAgents?.length;
|
|
7314
|
+
if (step.finishIf && (step.step === 'Actions' || isSingleSubAgent) && this.finishIfActs()) {
|
|
7315
|
+
return `the ${step.step} step carries a finishIf gate, and a passing gate ends the run`;
|
|
7316
|
+
}
|
|
7317
|
+
if (step.step === 'Sub-Agent' && this.getRequestedSubAgents(step).some(r => r.terminateAfter === true)) {
|
|
7318
|
+
return 'the Sub-Agent step has terminateAfter, which ends the run after it';
|
|
7319
|
+
}
|
|
7320
|
+
if (step.step === 'ClientTools' && step.terminate === true) {
|
|
7321
|
+
return 'the client tools were sent with taskComplete, which ends the run once they return';
|
|
7322
|
+
}
|
|
7323
|
+
return undefined;
|
|
7324
|
+
}
|
|
7325
|
+
/**
|
|
7326
|
+
* Runs the decision requests a prompt held back, now that the step that carried them has run
|
|
7327
|
+
* without ending the run, so the prompt about to run reads their answers.
|
|
7328
|
+
*/
|
|
7329
|
+
async runHeldDecisions(params) {
|
|
7330
|
+
const held = this._heldDecisions;
|
|
7331
|
+
if (!held) {
|
|
7332
|
+
return;
|
|
7333
|
+
}
|
|
7334
|
+
this._heldDecisions = undefined;
|
|
7335
|
+
this.logStatus(`[Decisions] The run continues, so answering ${held.Requests.length} held decision request(s)`, true, params);
|
|
7336
|
+
const results = await this.executeDecisionRequestsAsSteps(held.Requests, held.Payload, held.PromptParams, params);
|
|
7337
|
+
this.injectDecisionResultsMessage(params, results);
|
|
7338
|
+
}
|
|
7339
|
+
/**
|
|
7340
|
+
* Skips the decision requests a prompt held back, because the run ended before they could be
|
|
7341
|
+
* asked: the step that carried them ended it, or it was cancelled, or an error was thrown out of
|
|
7342
|
+
* the step loop. Called from the loop's terminate branch and from `Execute()`'s `finally`, so
|
|
7343
|
+
* every way out logs them. The held state is cleared before the log line, so the second call on
|
|
7344
|
+
* the terminate path logs nothing and each held request is logged exactly once.
|
|
7345
|
+
*
|
|
7346
|
+
* @param runEnd How the run ended, as a clause for the log line.
|
|
7347
|
+
*/
|
|
7348
|
+
skipHeldDecisions(params, runEnd) {
|
|
7349
|
+
const held = this._heldDecisions;
|
|
7350
|
+
if (!held) {
|
|
7351
|
+
return;
|
|
7352
|
+
}
|
|
7353
|
+
this._heldDecisions = undefined;
|
|
7354
|
+
const ids = held.Requests.map((d, i) => this.decisionRequestId(d, i)).join(', ');
|
|
7355
|
+
this.logStatus(`[Decisions] Skipped ${held.Requests.length} held decision request(s) (${ids}) without asking them: ${runEnd}, so no turn would read the answers (they were held because ${held.Reason})`, false, params);
|
|
7356
|
+
}
|
|
7357
|
+
/** How the run ended, as a clause for held decision requests that a cancellation or an error left unasked. */
|
|
7358
|
+
runEndForHeldDecisions() {
|
|
7359
|
+
switch (this.deriveRunOutcome()) {
|
|
7360
|
+
case 'cancelled':
|
|
7361
|
+
return 'the run was cancelled before they were asked';
|
|
7362
|
+
case 'failure':
|
|
7363
|
+
return 'the run failed before they were asked';
|
|
7364
|
+
default:
|
|
7365
|
+
return 'the run ended before they were asked';
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7368
|
+
/**
|
|
7369
|
+
* Injects decision results into the conversation as one tool-result message for the next turn.
|
|
7370
|
+
*
|
|
7371
|
+
* @since 2.132.0
|
|
7372
|
+
*/
|
|
7373
|
+
injectDecisionResultsMessage(params, results) {
|
|
7374
|
+
if (!results || results.length === 0)
|
|
7375
|
+
return;
|
|
7376
|
+
const message = {
|
|
7377
|
+
role: 'user',
|
|
7378
|
+
content: `Decision results:\n${JSON.stringify(results)}`,
|
|
7379
|
+
metadata: {
|
|
7380
|
+
turnAdded: this._promptTurnCount,
|
|
7381
|
+
messageType: 'tool-result',
|
|
7382
|
+
expirationTurns: 3,
|
|
7383
|
+
expirationMode: 'Compact',
|
|
7384
|
+
compactMode: 'First N Chars',
|
|
7385
|
+
compactLength: 500,
|
|
7386
|
+
compactPromptId: '',
|
|
7387
|
+
},
|
|
7388
|
+
};
|
|
7389
|
+
params.conversationMessages.push(message);
|
|
7390
|
+
}
|
|
7391
|
+
/**
|
|
7392
|
+
* Creates a chat message containing sub-agent execution results.
|
|
7393
|
+
*
|
|
7394
|
+
* @param {AgentSubAgentRequest} subAgent - The sub-agent that was executed
|
|
7395
|
+
* @param {any} result - The result from sub-agent execution
|
|
7396
|
+
* @returns {ChatMessage} A formatted message with sub-agent results
|
|
7397
|
+
* @protected
|
|
7398
|
+
*/
|
|
7399
|
+
createSubAgentResultMessage(subAgent, result) {
|
|
7400
|
+
return {
|
|
7401
|
+
role: 'user',
|
|
7402
|
+
content: this.formatSubAgentResultAsMarkdown(subAgent.name, result)
|
|
7403
|
+
};
|
|
7404
|
+
}
|
|
7405
|
+
/**
|
|
7406
|
+
* Gathers context data about the agent for use in prompt templates.
|
|
7407
|
+
*
|
|
7408
|
+
* This method collects information about the agent's sub-agents and available
|
|
7409
|
+
* actions, formatting them for injection into prompt templates. The data is
|
|
7410
|
+
* structured to provide the LLM with comprehensive context about the agent's
|
|
7411
|
+
* capabilities and hierarchical relationships.
|
|
7412
|
+
*
|
|
7413
|
+
* @param {MJAIAgentEntityExtended} agent - The agent to gather context for
|
|
7414
|
+
* @param {UserInfo} [_contextUser] - Optional user context (reserved for future use)
|
|
7415
|
+
* @param {any} [extraData] - Optional extra data to include in the context, if provided and keys conflict within the agent context data, the extraData will override the agent context data.
|
|
7416
|
+
* @param {ActionChange[]} [actionChanges] - Optional runtime action modifications
|
|
7417
|
+
*
|
|
7418
|
+
* @returns {Promise<AgentContextData>} Structured context data for prompts
|
|
7419
|
+
*
|
|
7420
|
+
* @throws {Error} If there's an error accessing agent data
|
|
7421
|
+
*
|
|
7422
|
+
* @private
|
|
7423
|
+
*/
|
|
7424
|
+
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges, subAgentChanges) {
|
|
7425
|
+
try {
|
|
7426
|
+
const engine = AIEngine.Instance;
|
|
7427
|
+
// Build (or reuse) the agent-invariant base catalog. This is process-wide cached on
|
|
7428
|
+
// AIEngine and wiped on Agent/AgentAction/AgentRelationship/AgentType changes + reloads.
|
|
7429
|
+
// It turns the per-step rebuild (sub-agent + action resolution, markdown, JSON.parse of
|
|
7430
|
+
// agent-type params) into a once-per-agent cost; the common no-override step reuses it wholesale.
|
|
7431
|
+
let catalog = engine.GetAgentBaseCatalog(agent.ID);
|
|
7432
|
+
if (!catalog) {
|
|
7433
|
+
catalog = this.buildAgentBaseCatalog(agent, engine);
|
|
7434
|
+
engine.SetAgentBaseCatalog(agent.ID, catalog);
|
|
7435
|
+
}
|
|
7436
|
+
const isRoot = this._depth === 0;
|
|
7437
|
+
// Sub-agents: reuse cached base unless runtime subAgentChanges apply (then clone + re-format).
|
|
7438
|
+
let uniqueActiveSubAgents = catalog.uniqueActiveSubAgents;
|
|
7439
|
+
let subAgentDetails = catalog.subAgentDetails;
|
|
7440
|
+
let subAgentCount = catalog.subAgentCount;
|
|
7441
|
+
if (subAgentChanges?.length) {
|
|
7442
|
+
uniqueActiveSubAgents = this.applySubAgentChanges(catalog.uniqueActiveSubAgents, subAgentChanges, agent.ID, isRoot, engine);
|
|
7443
|
+
subAgentCount = uniqueActiveSubAgents.length;
|
|
7444
|
+
subAgentDetails = this.formatSubAgentDetails(uniqueActiveSubAgents);
|
|
7445
|
+
}
|
|
7446
|
+
// Actions: reuse cached active set unless runtime actionChanges apply (then clone + re-format).
|
|
7447
|
+
//
|
|
7448
|
+
// FAST-PATH SHARING CONTRACT: on the no-override path, `activeActions` (and therefore
|
|
7449
|
+
// `_effectiveActions`) and `uniqueActiveSubAgents` above are the SAME array references
|
|
7450
|
+
// held by the process-wide AIEngine catalog cache. Downstream consumers MUST treat them
|
|
7451
|
+
// as read-only — they are only ever read (`.find`/`.map`/`.length`/`.some`), never mutated
|
|
7452
|
+
// in place. On the override path a fresh array is built via filter/applyActionChanges, so
|
|
6477
7453
|
// the cached arrays are never the mutated ones. Keeping the references (vs. copying) avoids
|
|
6478
7454
|
// a per-step allocation; if a future consumer needs to mutate, it must `.slice()` first.
|
|
6479
7455
|
let activeActions = catalog.activeActions;
|
|
@@ -6510,10 +7486,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6510
7486
|
// and removes any cache-poisoning foot-gun should a future consumer write to it.
|
|
6511
7487
|
agentTypePromptParams = { ...catalog.baseAgentTypePromptParams };
|
|
6512
7488
|
}
|
|
6513
|
-
//
|
|
6514
|
-
|
|
6515
|
-
// Build app context section if provided in extraData
|
|
6516
|
-
const appContext = this.buildAppContextSection(extraData);
|
|
7489
|
+
// Store for the finishIf gate, which runs after this prompt in executeActionsStep / executeNextStep
|
|
7490
|
+
this._agentTypePromptParams = agentTypePromptParams;
|
|
6517
7491
|
// Skill catalog (name + description only — progressive disclosure). This is the
|
|
6518
7492
|
// SELF-ACTIVATION surface, so it uses the double-gated auto set: empty unless the
|
|
6519
7493
|
// agent's SkillActivationMode is 'Auto', and containing only skills whose own
|
|
@@ -6523,7 +7497,20 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6523
7497
|
// is never even offered a skill the user isn't entitled to — the permission
|
|
6524
7498
|
// boundary is enforced at the catalog, not just at activation.
|
|
6525
7499
|
const availableSkills = await this.availableSkills(engine.GetAutoActivatableSkillsForAgent(agent, _contextUser), 'catalog', agent, _contextUser);
|
|
6526
|
-
|
|
7500
|
+
// Catalog narrowing (plan Task 3.7), off unless decisionsEnabled is true and maxActionsInPrompt /
|
|
7501
|
+
// maxSubAgentsInPrompt is positive and the list is longer. It narrows what the prompt SHOWS, once per run:
|
|
7502
|
+
// _effectiveActions / _effectiveSubAgents above stay whole, so validation forbids nothing.
|
|
7503
|
+
// Skills are narrowed here, after the filterAvailableSkills policy, never inside it: the
|
|
7504
|
+
// decision judges only skills the policy offers, and an override that skips `super` keeps it.
|
|
7505
|
+
// The counts stay whole: they are what the model can call, and a narrowed list says what it hides.
|
|
7506
|
+
await this.ensureCatalogNarrowing(agent, _contextUser, agentTypePromptParams, activeActions, uniqueActiveSubAgents, availableSkills);
|
|
7507
|
+
actionDetails = this.narrowedCatalogSection('action', activeActions, actionDetails, shown => this.formatActionDetails(shown));
|
|
7508
|
+
subAgentDetails = this.narrowedCatalogSection('agent', uniqueActiveSubAgents, subAgentDetails, shown => this.formatSubAgentDetails(shown));
|
|
7509
|
+
const skillsCatalog = this.narrowedCatalogSection('skill', availableSkills, this.formatSkillsCatalog(availableSkills), shown => this.formatSkillsCatalog(shown));
|
|
7510
|
+
// Build client tool details for the prompt (per-run; depends on extraData)
|
|
7511
|
+
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
7512
|
+
// Build app context section if provided in extraData
|
|
7513
|
+
const appContext = this.buildAppContextSection(extraData);
|
|
6527
7514
|
const contextData = {
|
|
6528
7515
|
agentName: agent.Name,
|
|
6529
7516
|
agentDescription: agent.Description,
|
|
@@ -6567,6 +7554,231 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6567
7554
|
throw new Error(`Error gathering context data: ${error.message}`);
|
|
6568
7555
|
}
|
|
6569
7556
|
}
|
|
7557
|
+
/**
|
|
7558
|
+
* Narrows the catalog once per run, on its first prompt (plan Task 3.7), and caches the result for
|
|
7559
|
+
* every later step. With narrowing off, or the master switch ({@link DecisionsEnabled}) off, it
|
|
7560
|
+
* asks nothing and records nothing, so the prompt describes the whole catalog. Never throws: any
|
|
7561
|
+
* failure leaves the full catalog in place. It narrows the prose catalog only:
|
|
7562
|
+
* {@link applyNativeTools} still declares every action and sub-agent.
|
|
7563
|
+
*/
|
|
7564
|
+
async ensureCatalogNarrowing(agent, contextUser, promptParams, actions, subAgents, skills) {
|
|
7565
|
+
if (this._catalogNarrowing) {
|
|
7566
|
+
return;
|
|
7567
|
+
}
|
|
7568
|
+
// Set first, so a failure below is cached as "hide nothing" rather than retried every step.
|
|
7569
|
+
this._catalogNarrowing = NoCatalogNarrowing();
|
|
7570
|
+
if (!this.DecisionsEnabled(promptParams)) {
|
|
7571
|
+
return;
|
|
7572
|
+
}
|
|
7573
|
+
const limits = this.reachableCatalogNarrowingLimits(agent, promptParams, actions);
|
|
7574
|
+
if (limits.Actions === 0 && limits.SubAgents === 0 && limits.Skills === 0) {
|
|
7575
|
+
return;
|
|
7576
|
+
}
|
|
7577
|
+
try {
|
|
7578
|
+
const candidates = this.catalogNarrowingCandidates(agent, actions, subAgents, skills);
|
|
7579
|
+
const asked = CatalogNarrowingCandidatesToAsk(candidates, limits);
|
|
7580
|
+
if (asked.length > 0) {
|
|
7581
|
+
this._catalogNarrowing = await this.narrowCatalog(agent, contextUser, promptParams, candidates, asked, limits);
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
catch (error) {
|
|
7585
|
+
this.warnCatalogNarrowing(agent, error instanceof Error ? error.message : String(error));
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
/**
|
|
7589
|
+
* The run's limits, with actions left whole when the agent lacks Find Candidate Actions: a hidden
|
|
7590
|
+
* action would then be unreachable. That case is logged as a warning when it keeps a long action
|
|
7591
|
+
* list whole, since the agent asked for narrowing and did not get it.
|
|
7592
|
+
*/
|
|
7593
|
+
reachableCatalogNarrowingLimits(agent, promptParams, actions) {
|
|
7594
|
+
const configured = ResolveCatalogNarrowingLimits(promptParams);
|
|
7595
|
+
const limits = ReachableCatalogNarrowingLimits(configured, actions.map(a => a.Name));
|
|
7596
|
+
if (limits.Actions !== configured.Actions && IsCatalogListNarrowed(configured.Actions, actions.length)) {
|
|
7597
|
+
LogErrorEx({
|
|
7598
|
+
message: `Catalog narrowing for '${agent.Name}' shows every action: the agent has no ${CATALOG_NARROWING_FIND_ACTIONS} action, so a hidden action could not be found`,
|
|
7599
|
+
severity: 'warning',
|
|
7600
|
+
category: 'CatalogNarrowing',
|
|
7601
|
+
});
|
|
7602
|
+
}
|
|
7603
|
+
return limits;
|
|
7604
|
+
}
|
|
7605
|
+
/**
|
|
7606
|
+
* Every item of the three lists as a narrowing candidate. An action or sub-agent with
|
|
7607
|
+
* `MinExecutionsPerRun` set, and the Find Candidate tools, are pinned: always shown.
|
|
7608
|
+
*/
|
|
7609
|
+
catalogNarrowingCandidates(agent, actions, subAgents, skills) {
|
|
7610
|
+
const requiredActionIDs = AIEngine.Instance.AgentActions
|
|
7611
|
+
.filter(aa => UUIDsEqual(aa.AgentID, agent.ID) && aa.Status === 'Active' && (aa.MinExecutionsPerRun ?? 0) > 0)
|
|
7612
|
+
.map(aa => aa.ActionID);
|
|
7613
|
+
const candidate = (kind, item, pinned) => ({ Kind: kind, ID: item.ID, Name: item.Name, Description: item.Description ?? '', Pinned: pinned });
|
|
7614
|
+
return [
|
|
7615
|
+
...actions.map(a => candidate('action', a, IsAlwaysShownAction(a.Name) || requiredActionIDs.some(id => UUIDsEqual(id, a.ID)))),
|
|
7616
|
+
...subAgents.filter(s => !!s).map(s => candidate('agent', s, (s.MinExecutionsPerRun ?? 0) > 0)),
|
|
7617
|
+
...skills.map(s => candidate('skill', s, false)),
|
|
7618
|
+
];
|
|
7619
|
+
}
|
|
7620
|
+
/**
|
|
7621
|
+
* Asks the one decision, records it as the `Catalog narrowing` step, and returns what to hide.
|
|
7622
|
+
* Any failure is logged as a warning and hides nothing, so the prompt shows the full catalog.
|
|
7623
|
+
*/
|
|
7624
|
+
async narrowCatalog(agent, contextUser, promptParams, candidates, asked, limits) {
|
|
7625
|
+
if (!this._openingRequest || !contextUser) {
|
|
7626
|
+
this.warnCatalogNarrowing(agent, contextUser
|
|
7627
|
+
? 'the run has no user message to judge the catalog against'
|
|
7628
|
+
: 'the run has no context user');
|
|
7629
|
+
return NoCatalogNarrowing();
|
|
7630
|
+
}
|
|
7631
|
+
const promptName = typeof promptParams.decisionPromptName === 'string'
|
|
7632
|
+
? promptParams.decisionPromptName
|
|
7633
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
7634
|
+
const step = await this.startCatalogNarrowingStep(contextUser, promptName, asked, limits);
|
|
7635
|
+
const result = await this.askCatalogNarrowing(agent, contextUser, BuildCatalogNarrowingQuestions(asked), promptName, step);
|
|
7636
|
+
const outcome = result.success ? SelectCatalogNarrowing(candidates, asked, result.Answers, limits) : undefined;
|
|
7637
|
+
if (!outcome) {
|
|
7638
|
+
const reason = result.success ? 'an answer was missing or was not a probability' : (result.errorMessage || 'the decision call failed');
|
|
7639
|
+
this.warnCatalogNarrowing(agent, reason);
|
|
7640
|
+
await this.finishCatalogNarrowingStep(step, result, undefined, reason);
|
|
7641
|
+
return NoCatalogNarrowing();
|
|
7642
|
+
}
|
|
7643
|
+
await this.finishCatalogNarrowingStep(step, result, outcome);
|
|
7644
|
+
return outcome;
|
|
7645
|
+
}
|
|
7646
|
+
/**
|
|
7647
|
+
* The one decision call, against the opening request. Never throws, and never waits longer than
|
|
7648
|
+
* {@link CATALOG_NARROWING_TIMEOUT_MS} or past the run's cancellation: a throw, a timeout and a
|
|
7649
|
+
* cancelled run each come back as a failed result. Stopping also aborts the call, and links the
|
|
7650
|
+
* prompt run it still returns to `step` once it does ({@link linkLateDecisionPromptRun}).
|
|
7651
|
+
*/
|
|
7652
|
+
async askCatalogNarrowing(agent, contextUser, questions, promptName, step) {
|
|
7653
|
+
const failed = (errorMessage) => ({ success: false, errorMessage, Answers: {} });
|
|
7654
|
+
const controller = new AbortController();
|
|
7655
|
+
let stoppedResult;
|
|
7656
|
+
const stopped = new Promise(resolve => controller.signal.addEventListener('abort', () => {
|
|
7657
|
+
stoppedResult = failed(String(controller.signal.reason));
|
|
7658
|
+
resolve(stoppedResult);
|
|
7659
|
+
}, { once: true }));
|
|
7660
|
+
const runToken = this._executeParams?.cancellationToken;
|
|
7661
|
+
const relayRunAbort = () => controller.abort('the run was cancelled');
|
|
7662
|
+
if (runToken?.aborted) {
|
|
7663
|
+
relayRunAbort();
|
|
7664
|
+
}
|
|
7665
|
+
else {
|
|
7666
|
+
runToken?.addEventListener('abort', relayRunAbort, { once: true });
|
|
7667
|
+
}
|
|
7668
|
+
const timeoutMS = BaseAgent.CATALOG_NARROWING_TIMEOUT_MS;
|
|
7669
|
+
const timer = setTimeout(() => controller.abort(`the decision call timed out after ${timeoutMS}ms`), timeoutMS);
|
|
7670
|
+
try {
|
|
7671
|
+
const ask = this._agentDecisionService.Ask({
|
|
7672
|
+
State: this._openingRequest,
|
|
7673
|
+
Questions: questions,
|
|
7674
|
+
ContextUser: contextUser,
|
|
7675
|
+
AgentID: agent.ID,
|
|
7676
|
+
PromptName: promptName,
|
|
7677
|
+
CancellationToken: controller.signal,
|
|
7678
|
+
ExecutionScope: this._runExecutionScope,
|
|
7679
|
+
});
|
|
7680
|
+
const result = await Promise.race([ask, stopped]);
|
|
7681
|
+
if (result === stoppedResult) {
|
|
7682
|
+
this.linkLateDecisionPromptRun(step, ask);
|
|
7683
|
+
}
|
|
7684
|
+
return result;
|
|
7685
|
+
}
|
|
7686
|
+
catch (error) {
|
|
7687
|
+
return failed(error instanceof Error ? error.message : String(error));
|
|
7688
|
+
}
|
|
7689
|
+
finally {
|
|
7690
|
+
clearTimeout(timer);
|
|
7691
|
+
runToken?.removeEventListener('abort', relayRunAbort);
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
/** Opens the `Catalog narrowing` Decision step with the decision's inputs. Without a run there is no step. */
|
|
7695
|
+
async startCatalogNarrowingStep(contextUser, promptName, asked, limits) {
|
|
7696
|
+
if (!this._agentRun) {
|
|
7697
|
+
return undefined;
|
|
7698
|
+
}
|
|
7699
|
+
const count = (kind) => asked.filter(c => c.Kind === kind).length;
|
|
7700
|
+
try {
|
|
7701
|
+
return await this.createStepEntity({
|
|
7702
|
+
stepType: 'Decision',
|
|
7703
|
+
stepName: 'Catalog narrowing',
|
|
7704
|
+
contextUser,
|
|
7705
|
+
inputData: {
|
|
7706
|
+
request: this._openingRequest,
|
|
7707
|
+
promptName,
|
|
7708
|
+
limits: { actions: limits.Actions, subAgents: limits.SubAgents, skills: limits.Skills },
|
|
7709
|
+
asked: { actions: count('action'), subAgents: count('agent'), skills: count('skill') },
|
|
7710
|
+
},
|
|
7711
|
+
});
|
|
7712
|
+
}
|
|
7713
|
+
catch (error) {
|
|
7714
|
+
LogError(`Could not record the catalog narrowing step: ${error instanceof Error ? error.message : String(error)}`);
|
|
7715
|
+
return undefined;
|
|
7716
|
+
}
|
|
7717
|
+
}
|
|
7718
|
+
/** Finalizes the `Catalog narrowing` step: each narrowed list's counts, kept names and probabilities, or why it failed open. */
|
|
7719
|
+
async finishCatalogNarrowingStep(step, result, outcome, reason) {
|
|
7720
|
+
if (!step) {
|
|
7721
|
+
return;
|
|
7722
|
+
}
|
|
7723
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
7724
|
+
this.attachDecisionPromptRun(step, result);
|
|
7725
|
+
const usage = {
|
|
7726
|
+
executionTimeMS: result.executionTimeMS,
|
|
7727
|
+
tokensUsed: result.tokensUsed,
|
|
7728
|
+
model: result.DecisionResult?.ResolvedModel ?? result.modelInfo?.modelName,
|
|
7729
|
+
promptRunId: result.promptRun?.ID,
|
|
7730
|
+
};
|
|
7731
|
+
if (!outcome) {
|
|
7732
|
+
await this.finalizeStepEntity(step, false, reason, { failedOpen: true, reason, ...usage });
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
const lists = {};
|
|
7736
|
+
const stepKeys = { action: 'actions', agent: 'subAgents', skill: 'skills' };
|
|
7737
|
+
for (const kind of ['action', 'agent', 'skill']) {
|
|
7738
|
+
const list = outcome.Lists[kind];
|
|
7739
|
+
if (list) {
|
|
7740
|
+
lists[stepKeys[kind]] = { total: list.Total, limit: list.Limit, shown: list.Kept.length, kept: list.Kept, pinned: list.Pinned, probabilities: list.Probabilities };
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
7743
|
+
await this.finalizeStepEntity(step, true, undefined, { ...lists, ...usage });
|
|
7744
|
+
}
|
|
7745
|
+
/** Logs that catalog narrowing failed open. */
|
|
7746
|
+
warnCatalogNarrowing(agent, reason) {
|
|
7747
|
+
LogErrorEx({
|
|
7748
|
+
message: `Catalog narrowing for '${agent.Name}' failed, so the full catalog is shown: ${reason}`,
|
|
7749
|
+
severity: 'warning',
|
|
7750
|
+
category: 'CatalogNarrowing',
|
|
7751
|
+
});
|
|
7752
|
+
}
|
|
7753
|
+
/** The items of one list the prompt shows. Returns `items` itself when the run's narrowing hides none of them. */
|
|
7754
|
+
hideNarrowedOut(kind, items) {
|
|
7755
|
+
return ApplyCatalogNarrowing(items, this._catalogNarrowing?.Hidden[kind]);
|
|
7756
|
+
}
|
|
7757
|
+
/**
|
|
7758
|
+
* One catalog section as the prompt shows it: `full` when the run's narrowing hides none of
|
|
7759
|
+
* `items`, otherwise a {@link CatalogNarrowingNote} saying how many are hidden and how to reach
|
|
7760
|
+
* them, then the items still shown. A hidden item can still be called. Fails OPEN, unlike the
|
|
7761
|
+
* skill policy: an error shows `full` and logs a warning.
|
|
7762
|
+
*/
|
|
7763
|
+
narrowedCatalogSection(kind, items, full, format) {
|
|
7764
|
+
try {
|
|
7765
|
+
const shown = this.hideNarrowedOut(kind, items);
|
|
7766
|
+
if (shown === items) {
|
|
7767
|
+
return full;
|
|
7768
|
+
}
|
|
7769
|
+
const shownSet = new Set(shown);
|
|
7770
|
+
const hiddenNames = items.filter(item => !shownSet.has(item)).map(item => item.Name);
|
|
7771
|
+
return `${CatalogNarrowingNote(kind, hiddenNames)}\n\n${format(shown)}`;
|
|
7772
|
+
}
|
|
7773
|
+
catch (error) {
|
|
7774
|
+
LogErrorEx({
|
|
7775
|
+
message: `Catalog narrowing could not narrow the ${kind} list, so all of it is shown: ${error instanceof Error ? error.message : String(error)}`,
|
|
7776
|
+
severity: 'warning',
|
|
7777
|
+
category: 'CatalogNarrowing',
|
|
7778
|
+
});
|
|
7779
|
+
return full;
|
|
7780
|
+
}
|
|
7781
|
+
}
|
|
6570
7782
|
/**
|
|
6571
7783
|
* Builds the agent-invariant {@link AgentBaseCatalog} — the resolved sub-agents + actions and
|
|
6572
7784
|
* their formatted markdown, plus the base agent-type prompt params. Computed once per agent and
|
|
@@ -6702,9 +7914,51 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6702
7914
|
// to distinguish between explicit user settings and schema defaults
|
|
6703
7915
|
const explicitResponseType = runtimeOverrides?.includeResponseTypeDefinition ||
|
|
6704
7916
|
agentParams.includeResponseTypeDefinition;
|
|
7917
|
+
this.warnOnceOnUnknownFinishIfMode(merged.finishIfMode, agent);
|
|
6705
7918
|
this.applyResponseTypeAutoAlignment(merged, explicitResponseType);
|
|
6706
7919
|
return merged;
|
|
6707
7920
|
}
|
|
7921
|
+
/**
|
|
7922
|
+
* The agent and value pairs {@link warnOnceOnUnknownFinishIfMode} has already reported. It is
|
|
7923
|
+
* process-wide because the prompt params are merged again for every run of an agent, and on every
|
|
7924
|
+
* turn that carries runtime overrides. One warning per agent and value is enough. A run request
|
|
7925
|
+
* can set the value, so what this keeps is bounded: see {@link FinishIfModeWarnings}.
|
|
7926
|
+
*/
|
|
7927
|
+
static { this.finishIfModeWarnings = new FinishIfModeWarnings(); }
|
|
7928
|
+
/**
|
|
7929
|
+
* Warns, once per agent and value, when the merged `finishIfMode` is set but is not a
|
|
7930
|
+
* {@link FinishIfMode}. {@link ResolveFinishIfMode} treats such a value as `'off'`, so without
|
|
7931
|
+
* this a typo such as `"On"` or `"true"` would turn the agent's gates off without a word. An
|
|
7932
|
+
* absent value is the default, so it is not reported.
|
|
7933
|
+
*/
|
|
7934
|
+
warnOnceOnUnknownFinishIfMode(value, agent) {
|
|
7935
|
+
const warning = BaseAgent.finishIfModeWarnings.WarningFor(agent.ID, agent.Name, value);
|
|
7936
|
+
if (warning) {
|
|
7937
|
+
this.logError(warning, { agent, category: 'AgentConfiguration', severity: 'warning' });
|
|
7938
|
+
}
|
|
7939
|
+
}
|
|
7940
|
+
/**
|
|
7941
|
+
* The master switch for decision-model use by this agent: `decisionsEnabled` in its merged
|
|
7942
|
+
* agent-type prompt params (type default < agent < the run's `data.__agentTypePromptParams`).
|
|
7943
|
+
* Anything but `true` is off, the default.
|
|
7944
|
+
*
|
|
7945
|
+
* While it is off the agent never asks a decision model on its own. Each automatic use checks it
|
|
7946
|
+
* before it asks, whatever its own setting says: inline `decisions` ({@link processTurnDecisions}),
|
|
7947
|
+
* `finishIf` gates ({@link finishIfSettings}), decision discovery ({@link isDecisionDiscoveryOnFor}),
|
|
7948
|
+
* the payload change check ({@link isPayloadFeedbackCheckOn}), catalog narrowing
|
|
7949
|
+
* ({@link ensureCatalogNarrowing}) and the Memory Manager's note gate. And
|
|
7950
|
+
* {@link applyResponseTypeAutoAlignment} keeps the `decisions` and `finishIf` docs and response
|
|
7951
|
+
* fields out of the prompt. While it is on, each use keeps its own setting, and each is off by default.
|
|
7952
|
+
*
|
|
7953
|
+
* Explicit uses never read it: a Flow agent's Decision step ({@link executeDecisionStep}) runs because
|
|
7954
|
+
* someone placed it, and the task-graph Decision step and the Run Decision action call
|
|
7955
|
+
* {@link AgentDecisionService} directly. That is why the check sits at each automatic use, not in the service.
|
|
7956
|
+
*
|
|
7957
|
+
* @param promptParams - The merged agent-type prompt params.
|
|
7958
|
+
*/
|
|
7959
|
+
DecisionsEnabled(promptParams) {
|
|
7960
|
+
return promptParams?.decisionsEnabled === true;
|
|
7961
|
+
}
|
|
6708
7962
|
/**
|
|
6709
7963
|
* Applies auto-alignment rules to includeResponseTypeDefinition based on other flags.
|
|
6710
7964
|
*
|
|
@@ -6718,6 +7972,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6718
7972
|
* - includeCommandDocs → includeResponseTypeDefinition.commands
|
|
6719
7973
|
* - includeForEachDocs → includeResponseTypeDefinition.forEach
|
|
6720
7974
|
* - includeWhileDocs → includeResponseTypeDefinition.while
|
|
7975
|
+
* - includeDecisionsDocs → includeResponseTypeDefinition.decisions
|
|
7976
|
+
*
|
|
7977
|
+
* While the master switch is off ({@link DecisionsEnabled}), the decisions and finishIf docs and
|
|
7978
|
+
* response fields are off, even when set explicitly.
|
|
6721
7979
|
*
|
|
6722
7980
|
* @param params - The merged params object to modify in place
|
|
6723
7981
|
* @param explicitResponseType - The explicitly set response type config from agent/runtime (not schema defaults)
|
|
@@ -6725,18 +7983,37 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6725
7983
|
* @since 2.132.0
|
|
6726
7984
|
*/
|
|
6727
7985
|
applyResponseTypeAutoAlignment(params, explicitResponseType) {
|
|
7986
|
+
// Align a copy: the object may be the caller's per-run override, which sub-agents inherit with the
|
|
7987
|
+
// run's data. Writing into it would turn a key "explicit" for every later merge of that override.
|
|
7988
|
+
if (IsPlainObject(params.includeResponseTypeDefinition)) {
|
|
7989
|
+
params.includeResponseTypeDefinition = { ...params.includeResponseTypeDefinition };
|
|
7990
|
+
}
|
|
6728
7991
|
// Ensure includeResponseTypeDefinition is an object
|
|
6729
|
-
if (!params.includeResponseTypeDefinition || typeof params.includeResponseTypeDefinition !== 'object') {
|
|
7992
|
+
else if (!params.includeResponseTypeDefinition || typeof params.includeResponseTypeDefinition !== 'object') {
|
|
6730
7993
|
params.includeResponseTypeDefinition = {
|
|
6731
7994
|
payload: true,
|
|
6732
7995
|
responseForms: true,
|
|
6733
7996
|
commands: true,
|
|
6734
7997
|
forEach: true,
|
|
6735
7998
|
while: true,
|
|
6736
|
-
scratchpad: true
|
|
7999
|
+
scratchpad: true,
|
|
8000
|
+
decisions: true,
|
|
8001
|
+
finishIf: true
|
|
6737
8002
|
};
|
|
6738
8003
|
}
|
|
6739
8004
|
const responseType = params.includeResponseTypeDefinition;
|
|
8005
|
+
const decisionsEnabled = this.DecisionsEnabled(params);
|
|
8006
|
+
// finishIf gates are opt-in (`finishIfMode`), and need the master switch. While off, the model is
|
|
8007
|
+
// not taught to write one, whatever includeFinishIfDocs says, so the response field below follows it off too.
|
|
8008
|
+
if (!decisionsEnabled || ResolveFinishIfMode(params.finishIfMode) === 'off') {
|
|
8009
|
+
params.includeFinishIfDocs = false;
|
|
8010
|
+
}
|
|
8011
|
+
// `decisions` is opt-in too (`includeDecisionsDocs: true`), and needs the master switch. Its docs and
|
|
8012
|
+
// types add about 1,200 tokens to every turn, and on the Prompt Eval corpus no model used it
|
|
8013
|
+
// (typed-decision plan, Task 4.7). Unset means off, so the response field below follows it off too.
|
|
8014
|
+
if (!decisionsEnabled || params.includeDecisionsDocs !== true) {
|
|
8015
|
+
params.includeDecisionsDocs = false;
|
|
8016
|
+
}
|
|
6740
8017
|
// Auto-alignment mappings: docs flag → response type property
|
|
6741
8018
|
const alignmentMappings = [
|
|
6742
8019
|
{ docsFlag: 'includePayloadInPrompt', responseTypeKey: 'payload' },
|
|
@@ -6748,7 +8025,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6748
8025
|
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' },
|
|
6749
8026
|
{ docsFlag: 'includeConversationToolsDocs', responseTypeKey: 'conversationToolCalls' },
|
|
6750
8027
|
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' },
|
|
6751
|
-
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' }
|
|
8028
|
+
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' },
|
|
8029
|
+
{ docsFlag: 'includeDecisionsDocs', responseTypeKey: 'decisions' },
|
|
8030
|
+
{ docsFlag: 'includeFinishIfDocs', responseTypeKey: 'finishIf' }
|
|
6752
8031
|
];
|
|
6753
8032
|
for (const { docsFlag, responseTypeKey } of alignmentMappings) {
|
|
6754
8033
|
// Check if the user explicitly set this response type property
|
|
@@ -6766,6 +8045,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6766
8045
|
responseType[responseTypeKey] = true;
|
|
6767
8046
|
}
|
|
6768
8047
|
}
|
|
8048
|
+
// With the master switch off, neither decision field is offered, even when set explicitly: the
|
|
8049
|
+
// model must not be invited to ask for something the run would refuse.
|
|
8050
|
+
if (!decisionsEnabled) {
|
|
8051
|
+
responseType.decisions = false;
|
|
8052
|
+
responseType.finishIf = false;
|
|
8053
|
+
}
|
|
6769
8054
|
// Capability gates align the OTHER way: default OFF, and the section appears only when the
|
|
6770
8055
|
// gate is explicitly on. Running these through the loop above would invert their meaning —
|
|
6771
8056
|
// that loop's `else` branch defaults an unset key to true, which is exactly what a gated
|
|
@@ -6811,12 +8096,346 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6811
8096
|
return {};
|
|
6812
8097
|
}
|
|
6813
8098
|
}
|
|
8099
|
+
/**
|
|
8100
|
+
* The finishIf settings of the current step, from its merged agent-type prompt params. A gate is
|
|
8101
|
+
* evaluated when the master switch is on ({@link DecisionsEnabled}), `finishIfMode` is `shadow` or
|
|
8102
|
+
* `on`, and `includeResponseTypeDefinition.finishIf` is not `false`; it ends the run only in `on`.
|
|
8103
|
+
*/
|
|
8104
|
+
finishIfSettings() {
|
|
8105
|
+
const promptParams = this._agentTypePromptParams;
|
|
8106
|
+
const rules = promptParams?.includeResponseTypeDefinition;
|
|
8107
|
+
const threshold = promptParams?.finishIfThreshold;
|
|
8108
|
+
const mode = ResolveFinishIfMode(promptParams?.finishIfMode);
|
|
8109
|
+
return {
|
|
8110
|
+
Enabled: this.DecisionsEnabled(promptParams) && mode !== 'off' && rules?.finishIf !== false,
|
|
8111
|
+
Mode: mode,
|
|
8112
|
+
Threshold: typeof threshold === 'number' && threshold > 0 && threshold <= 1
|
|
8113
|
+
? threshold
|
|
8114
|
+
: DEFAULT_LOOP_AGENT_PROMPT_PARAMS.finishIfThreshold,
|
|
8115
|
+
PromptName: typeof promptParams?.decisionPromptName === 'string'
|
|
8116
|
+
? promptParams.decisionPromptName
|
|
8117
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME,
|
|
8118
|
+
};
|
|
8119
|
+
}
|
|
8120
|
+
/** Whether a passing finishIf gate ends the run: only in `on` mode. */
|
|
8121
|
+
finishIfActs() {
|
|
8122
|
+
const settings = this.finishIfSettings();
|
|
8123
|
+
return settings.Enabled && settings.Mode === 'on';
|
|
8124
|
+
}
|
|
8125
|
+
/**
|
|
8126
|
+
* Whether an evaluated gate ends the run. In shadow mode a passing gate is only logged: the
|
|
8127
|
+
* outcome is already recorded on its `Finish check` step, and the model takes its next turn.
|
|
8128
|
+
*/
|
|
8129
|
+
gateEndsRun(outcome, params) {
|
|
8130
|
+
if (!outcome.Passed) {
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
if (!this.finishIfActs()) {
|
|
8134
|
+
this.logStatus('[finishIf] Shadow mode: the gate passed, and the run continues as it would have', true, params);
|
|
8135
|
+
return false;
|
|
8136
|
+
}
|
|
8137
|
+
return true;
|
|
8138
|
+
}
|
|
8139
|
+
/**
|
|
8140
|
+
* Runs the finishIf gate after an Actions step, and returns the Success step when it passes.
|
|
8141
|
+
* ForEach and While call executeActionsStep without a conversation message, and are never gated.
|
|
8142
|
+
*/
|
|
8143
|
+
async finishAfterActions(params, decision, actionResults, actionSummaries, payload, parentStepId, addConversationMessage) {
|
|
8144
|
+
const finishIf = decision.finishIf;
|
|
8145
|
+
if (!finishIf || !addConversationMessage || !this.finishIfSettings().Enabled) {
|
|
8146
|
+
return undefined;
|
|
8147
|
+
}
|
|
8148
|
+
// (In shadow mode the gate below is still evaluated and recorded; it just never ends the run.)
|
|
8149
|
+
// `success` means only that the action did not throw; `result.Success` is its outcome.
|
|
8150
|
+
if (!actionResults.every(r => r.success && r.result?.Success === true)) {
|
|
8151
|
+
return undefined;
|
|
8152
|
+
}
|
|
8153
|
+
// AIDirectives are instructions for the model's next turn, which a passing gate would take away.
|
|
8154
|
+
const withDirectives = actionSummaries.filter(a => (a.aiDirectives?.length ?? 0) > 0).map(a => a.actionName);
|
|
8155
|
+
if (withDirectives.length > 0) {
|
|
8156
|
+
this.logStatus(`[finishIf] Gate skipped: ${withDirectives.join(', ')} returned AIDirectives, which the model must see on its next turn`, false, params);
|
|
8157
|
+
return undefined;
|
|
8158
|
+
}
|
|
8159
|
+
const state = CapFinishIfState(actionSummaries.map(FormatActionForFinishIf).join('\n\n'));
|
|
8160
|
+
const outcome = await this.evaluateFinishIf(finishIf, state, params, decision, payload, parentStepId);
|
|
8161
|
+
return this.gateEndsRun(outcome, params)
|
|
8162
|
+
? { ...this.finishIfSuccessStep(finishIf, decision, payload), priorStepResult: actionSummaries }
|
|
8163
|
+
: undefined;
|
|
8164
|
+
}
|
|
8165
|
+
/**
|
|
8166
|
+
* Runs the finishIf gate after a Sub-Agent step that succeeded without terminating, and returns
|
|
8167
|
+
* the step with `terminate: true` when it passes, as `terminateAfter` would.
|
|
8168
|
+
*/
|
|
8169
|
+
async finishAfterSubAgent(params, decision, result) {
|
|
8170
|
+
const finishIf = decision.finishIf;
|
|
8171
|
+
if (!finishIf || result.terminate || result.step !== 'Success' || !this.finishIfSettings().Enabled) {
|
|
8172
|
+
return undefined;
|
|
8173
|
+
}
|
|
8174
|
+
const outcome = await this.evaluateFinishIf(finishIf, FormatSubAgentForFinishIf(result), params, decision, result.newPayload);
|
|
8175
|
+
return this.gateEndsRun(outcome, params) ? { ...result, terminate: true, message: finishIf.message } : undefined;
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* Asks the finishIf questions about the step's results, then checks that the normal Success
|
|
8179
|
+
* validation would allow the run to end. Records the check as a `Finish check` Decision step.
|
|
8180
|
+
* Never throws: any error means the gate did not pass, and the run continues as it would have.
|
|
8181
|
+
*/
|
|
8182
|
+
async evaluateFinishIf(finishIf, state, params, decision, payload, parentStepId) {
|
|
8183
|
+
const settings = this.finishIfSettings();
|
|
8184
|
+
let step;
|
|
8185
|
+
let outcome;
|
|
8186
|
+
try {
|
|
8187
|
+
step = await this.createStepEntity({
|
|
8188
|
+
stepType: 'Decision',
|
|
8189
|
+
stepName: 'Finish check',
|
|
8190
|
+
contextUser: params.contextUser,
|
|
8191
|
+
inputData: { questions: finishIf.questions, state: state.slice(0, FINISH_IF_STATE_EXCERPT) },
|
|
8192
|
+
parentId: parentStepId
|
|
8193
|
+
});
|
|
8194
|
+
outcome = await this.askFinishIf(finishIf, state, settings, params, step);
|
|
8195
|
+
if (outcome.Passed) {
|
|
8196
|
+
const refusal = await this.finishIfValidationRefusal(params, this.finishIfSuccessStep(finishIf, decision, payload), payload, step);
|
|
8197
|
+
if (refusal) {
|
|
8198
|
+
outcome = { ...outcome, Passed: false, Reason: refusal };
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
catch (error) {
|
|
8203
|
+
outcome = { Passed: false, Probabilities: {}, Reason: error instanceof Error ? error.message : String(error) };
|
|
8204
|
+
LogError(`finishIf gate failed, so the run continues: ${outcome.Reason}`);
|
|
8205
|
+
}
|
|
8206
|
+
await this.finalizeFinishCheckStep(step, outcome, settings);
|
|
8207
|
+
return outcome;
|
|
8208
|
+
}
|
|
8209
|
+
/**
|
|
8210
|
+
* Asks each finishIf question as a Likelihood. Every one must reach the threshold. The call's
|
|
8211
|
+
* prompt run is linked to the `Finish check` step, so the run's cost and tokens count it.
|
|
8212
|
+
*/
|
|
8213
|
+
async askFinishIf(finishIf, state, settings, params, step) {
|
|
8214
|
+
const questions = BuildFinishIfQuestions(finishIf);
|
|
8215
|
+
const result = await this._agentDecisionService.Ask({
|
|
8216
|
+
State: state,
|
|
8217
|
+
Questions: questions,
|
|
8218
|
+
ContextUser: params.contextUser,
|
|
8219
|
+
AgentID: params.agent?.ID,
|
|
8220
|
+
PromptName: settings.PromptName,
|
|
8221
|
+
CancellationToken: params.cancellationToken,
|
|
8222
|
+
ExecutionScope: this.runPromptExecutionScope(params)
|
|
8223
|
+
});
|
|
8224
|
+
this.attachDecisionPromptRun(step, result);
|
|
8225
|
+
if (!result.success) {
|
|
8226
|
+
return { Passed: false, Probabilities: {}, Reason: `The decision call failed: ${result.errorMessage ?? 'no error message'}` };
|
|
8227
|
+
}
|
|
8228
|
+
return JudgeFinishIf(result.Answers, questions, settings.Threshold);
|
|
8229
|
+
}
|
|
8230
|
+
/**
|
|
8231
|
+
* Returns why the normal Success validation would refuse to end the run, or `undefined` when it
|
|
8232
|
+
* allows it. The check must not count as a validation retry, so the counters are restored:
|
|
8233
|
+
* otherwise a gate that keeps not passing could exhaust the retries and fail the run.
|
|
8234
|
+
*/
|
|
8235
|
+
async finishIfValidationRefusal(params, successStep, payload, step) {
|
|
8236
|
+
if (!this._agentRun) {
|
|
8237
|
+
return 'There is no agent run to validate against';
|
|
8238
|
+
}
|
|
8239
|
+
const generalRetries = this._generalValidationRetryCount;
|
|
8240
|
+
const payloadRetries = this._validationRetryCount;
|
|
8241
|
+
try {
|
|
8242
|
+
const validated = await this.validateSuccessNextStep(params, successStep, payload, this._agentRun, step);
|
|
8243
|
+
if (validated.step === 'Success') {
|
|
8244
|
+
return undefined;
|
|
8245
|
+
}
|
|
8246
|
+
return validated.errorMessage || validated.retryInstructions || `Success validation returned '${validated.step}'`;
|
|
8247
|
+
}
|
|
8248
|
+
finally {
|
|
8249
|
+
this._generalValidationRetryCount = generalRetries;
|
|
8250
|
+
this._validationRetryCount = payloadRetries;
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
/** The Success step a passing gate returns after actions, in the shape of the client-tools `terminateAfterExecution` branch. */
|
|
8254
|
+
finishIfSuccessStep(finishIf, decision, payload) {
|
|
8255
|
+
return {
|
|
8256
|
+
step: 'Success',
|
|
8257
|
+
terminate: true,
|
|
8258
|
+
message: finishIf.message,
|
|
8259
|
+
payloadChangeRequest: decision.payloadChangeRequest,
|
|
8260
|
+
previousPayload: decision.previousPayload,
|
|
8261
|
+
newPayload: payload,
|
|
8262
|
+
scratchpad: decision.scratchpad,
|
|
8263
|
+
responseForm: decision.responseForm,
|
|
8264
|
+
actionableCommands: decision.actionableCommands,
|
|
8265
|
+
automaticCommands: decision.automaticCommands,
|
|
8266
|
+
artifactDirective: decision.artifactDirective
|
|
8267
|
+
};
|
|
8268
|
+
}
|
|
8269
|
+
/** Finalizes the `Finish check` step with the outcome. A failure to save it never affects the run. */
|
|
8270
|
+
async finalizeFinishCheckStep(step, outcome, settings) {
|
|
8271
|
+
if (!step) {
|
|
8272
|
+
return;
|
|
8273
|
+
}
|
|
8274
|
+
try {
|
|
8275
|
+
await this.finalizeStepEntity(step, true, undefined, {
|
|
8276
|
+
passed: outcome.Passed,
|
|
8277
|
+
mode: settings.Mode,
|
|
8278
|
+
endedRun: outcome.Passed && settings.Mode === 'on',
|
|
8279
|
+
threshold: settings.Threshold,
|
|
8280
|
+
probabilities: outcome.Probabilities,
|
|
8281
|
+
reason: outcome.Reason
|
|
8282
|
+
});
|
|
8283
|
+
}
|
|
8284
|
+
catch (error) {
|
|
8285
|
+
LogError(`Could not save the finishIf step: ${error instanceof Error ? error.message : String(error)}`);
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8288
|
+
/**
|
|
8289
|
+
* Whether the merged agent-type prompt params turn the payload change check on. It is off unless
|
|
8290
|
+
* the master switch ({@link DecisionsEnabled}) and `payloadFeedbackCheck` are both `true`.
|
|
8291
|
+
*/
|
|
8292
|
+
isPayloadFeedbackCheckOn(agentTypePromptParams) {
|
|
8293
|
+
return this.DecisionsEnabled(agentTypePromptParams) && agentTypePromptParams?.payloadFeedbackCheck === true;
|
|
8294
|
+
}
|
|
8295
|
+
/**
|
|
8296
|
+
* Runs the payload change check ({@link checkPayloadChanges}) on a step's applied payload changes
|
|
8297
|
+
* when it applies: the analyzer flagged a change for feedback, the check is on
|
|
8298
|
+
* ({@link isPayloadFeedbackCheckOn}), and a turn will read the result. A step that ends the run
|
|
8299
|
+
* skips it: no call to pay for, and no message left in the conversation. Never throws.
|
|
8300
|
+
*/
|
|
8301
|
+
async checkPayloadChangesWhenOn(changeResult, nextStep, promptResult, agentTypePromptParams, params) {
|
|
8302
|
+
if (!changeResult.requiresFeedback || !this.isPayloadFeedbackCheckOn(agentTypePromptParams)) {
|
|
8303
|
+
return;
|
|
8304
|
+
}
|
|
8305
|
+
if (this.stepEndsRun(nextStep)) {
|
|
8306
|
+
this.logStatus(`[Payload check] Skipped: the ${nextStep.step} step ends the run, so no turn would read the result`, true, params);
|
|
8307
|
+
return;
|
|
8308
|
+
}
|
|
8309
|
+
await this.checkPayloadChanges(changeResult.analysis, nextStep, promptResult, agentTypePromptParams, params);
|
|
8310
|
+
}
|
|
8311
|
+
/**
|
|
8312
|
+
* Whether the step ends the run, so no turn would read the payload change check's message: it
|
|
8313
|
+
* terminates, or it is a `Success` or `Chat` step, which hand the run back to the caller.
|
|
8314
|
+
*/
|
|
8315
|
+
stepEndsRun(step) {
|
|
8316
|
+
return step.terminate === true || step.step === 'Success' || step.step === 'Chat';
|
|
8317
|
+
}
|
|
8318
|
+
/**
|
|
8319
|
+
* The payload change check. Asks whether each change the analyzer flagged was intended, as one
|
|
8320
|
+
* Likelihood per change in one decision call, and records it as a `Payload change check`
|
|
8321
|
+
* Decision step. The changes judged unintended are listed in a message for the agent's next
|
|
8322
|
+
* turn, which asks it to confirm or restore them.
|
|
8323
|
+
*
|
|
8324
|
+
* It never reverts or blocks a change: the agent decides. When the decision cannot answer,
|
|
8325
|
+
* every change is accepted, as it is with the check off. Never throws.
|
|
8326
|
+
*/
|
|
8327
|
+
async checkPayloadChanges(analysis, nextStep, promptResult, agentTypePromptParams, params) {
|
|
8328
|
+
const promptName = agentTypePromptParams?.decisionPromptName;
|
|
8329
|
+
const manager = new PayloadFeedbackManager({ decisionPromptName: typeof promptName === 'string' ? promptName : undefined }, this._agentDecisionService);
|
|
8330
|
+
const questions = manager.GenerateQuestions(analysis?.warnings ?? []);
|
|
8331
|
+
if (questions.length === 0) {
|
|
8332
|
+
return;
|
|
8333
|
+
}
|
|
8334
|
+
let step;
|
|
8335
|
+
try {
|
|
8336
|
+
step = await this.createStepEntity({
|
|
8337
|
+
stepType: 'Decision',
|
|
8338
|
+
stepName: 'Payload change check',
|
|
8339
|
+
contextUser: params.contextUser,
|
|
8340
|
+
inputData: {
|
|
8341
|
+
questions: questions.map(q => ({ id: q.id, path: q.warning.path, type: q.warning.type, change: manager.DescribeChange(q.warning) }))
|
|
8342
|
+
}
|
|
8343
|
+
});
|
|
8344
|
+
const responses = await manager.QueryAgent(questions, this.payloadFeedbackContext(nextStep, promptResult, params), params.contextUser);
|
|
8345
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
8346
|
+
if (manager.LastDecisionResult) {
|
|
8347
|
+
this.attachDecisionPromptRun(step, manager.LastDecisionResult);
|
|
8348
|
+
}
|
|
8349
|
+
await this.finalizePayloadCheckStep(step, questions, responses, manager.IntendedThreshold);
|
|
8350
|
+
const message = manager.BuildUnintendedChangesMessage(questions, responses);
|
|
8351
|
+
if (message) {
|
|
8352
|
+
this.injectPayloadCheckMessage(params, message);
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
catch (error) {
|
|
8356
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
8357
|
+
LogError(`The payload change check failed, so every change stands: ${reason}`);
|
|
8358
|
+
if (step) {
|
|
8359
|
+
await this.finalizeStepEntity(step, false, reason);
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
/**
|
|
8364
|
+
* What the payload change check's decision reads: the agent's own reasoning for the step (from its
|
|
8365
|
+
* response, else from the step), its message, and the reasoning it gave with the change. The check
|
|
8366
|
+
* never runs on a `Chat` or `Success` step, so the message comes from the response alone.
|
|
8367
|
+
*/
|
|
8368
|
+
payloadFeedbackContext(nextStep, promptResult, params) {
|
|
8369
|
+
const response = responseReasoningAndMessage(promptResult?.result);
|
|
8370
|
+
return {
|
|
8371
|
+
Reasoning: response.Reasoning ?? nextStep.reasoning,
|
|
8372
|
+
ChangeReasoning: nextStep.payloadChangeRequest?.reasoning,
|
|
8373
|
+
Message: response.Message,
|
|
8374
|
+
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
8375
|
+
CancellationToken: params.cancellationToken,
|
|
8376
|
+
ExecutionScope: this.runPromptExecutionScope(params)
|
|
8377
|
+
};
|
|
8378
|
+
}
|
|
8379
|
+
/**
|
|
8380
|
+
* Finalizes the `Payload change check` step with each change's probability and outcome. The step
|
|
8381
|
+
* fails when no decision answered, and its error says why. A failure to save it never affects the run.
|
|
8382
|
+
*/
|
|
8383
|
+
async finalizePayloadCheckStep(step, questions, responses, threshold) {
|
|
8384
|
+
const answered = responses.filter(r => typeof r.probability === 'number');
|
|
8385
|
+
const unintended = responses.filter(r => !r.intended).map(r => questions.find(q => q.id === r.questionId)?.warning.path);
|
|
8386
|
+
try {
|
|
8387
|
+
await this.finalizeStepEntity(step, answered.length > 0, answered.length > 0 ? undefined : responses[0]?.explanation, {
|
|
8388
|
+
threshold,
|
|
8389
|
+
probabilities: Object.fromEntries(answered.map(r => [r.questionId, r.probability])),
|
|
8390
|
+
responses,
|
|
8391
|
+
unintendedPaths: unintended
|
|
8392
|
+
});
|
|
8393
|
+
}
|
|
8394
|
+
catch (error) {
|
|
8395
|
+
LogError(`Could not save the payload change check step: ${error instanceof Error ? error.message : String(error)}`);
|
|
8396
|
+
}
|
|
8397
|
+
}
|
|
8398
|
+
/** Adds the payload change check's message to the conversation, for the agent's next turn. */
|
|
8399
|
+
injectPayloadCheckMessage(params, content) {
|
|
8400
|
+
const message = {
|
|
8401
|
+
role: 'user',
|
|
8402
|
+
content,
|
|
8403
|
+
metadata: {
|
|
8404
|
+
turnAdded: this._promptTurnCount,
|
|
8405
|
+
messageType: 'tool-result',
|
|
8406
|
+
expirationTurns: 3,
|
|
8407
|
+
expirationMode: 'Compact',
|
|
8408
|
+
compactMode: 'First N Chars',
|
|
8409
|
+
compactLength: 500,
|
|
8410
|
+
compactPromptId: '',
|
|
8411
|
+
},
|
|
8412
|
+
};
|
|
8413
|
+
params.conversationMessages.push(message);
|
|
8414
|
+
}
|
|
8415
|
+
/**
|
|
8416
|
+
* The {@link AIPromptExecutionScope} of model work this run starts outside the agent's own turn —
|
|
8417
|
+
* summarizing a range, compacting a message or the conversation, decision calls (FinishIf,
|
|
8418
|
+
* discovery, catalog narrowing, decision requests, the payload change check) and self-check
|
|
8419
|
+
* rubrics. Each built its params with only `contextUser`, so it ran on the platform's keys and
|
|
8420
|
+
* default configuration inside a run on a customer's keys, and under a `'RuntimeOnly'` scope
|
|
8421
|
+
* would have bypassed it.
|
|
8422
|
+
*/
|
|
8423
|
+
runPromptExecutionScope(params) {
|
|
8424
|
+
return {
|
|
8425
|
+
contextUser: params.contextUser,
|
|
8426
|
+
provider: params.provider || this._activeProvider,
|
|
8427
|
+
configurationId: params.configurationId,
|
|
8428
|
+
apiKeys: params.apiKeys,
|
|
8429
|
+
CredentialScope: params.CredentialScope,
|
|
8430
|
+
};
|
|
8431
|
+
}
|
|
6814
8432
|
/**
|
|
6815
8433
|
* Whether THIS action may use the run's runtime API key for THIS driver class. The default is
|
|
6816
8434
|
* yes: the run was started on those keys, and an action that calls a vendor on the user's behalf
|
|
6817
8435
|
* (Generate Image) is doing what the prompts do. Override to narrow it — an agent that knows
|
|
6818
8436
|
* which of its actions talk to which vendor can refuse everything else, and a refusal costs the
|
|
6819
8437
|
* action nothing but the customer's key: it falls back to the platform key as if the run had none.
|
|
8438
|
+
* Under a `'RuntimeOnly'` credential scope a refusal leaves the action with no key for that class.
|
|
6820
8439
|
*/
|
|
6821
8440
|
actionMayUseRuntimeAPIKey(action, driverClass, params) {
|
|
6822
8441
|
return true;
|
|
@@ -6829,12 +8448,14 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6829
8448
|
buildRuntimeAPIKeyResolver(params, actionEntity) {
|
|
6830
8449
|
const runKeys = params.apiKeys;
|
|
6831
8450
|
return (driverClass) => {
|
|
8451
|
+
const platformAllowed = CredentialScopeAllows(params.CredentialScope, 'Environment');
|
|
6832
8452
|
if (!this.actionMayUseRuntimeAPIKey(actionEntity, driverClass, params)) {
|
|
6833
|
-
this.logStatus(`🔑 Runtime API key for '${driverClass}' refused to action '${actionEntity.Name}' by policy — platform key applies`, true, params);
|
|
8453
|
+
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);
|
|
6834
8454
|
return undefined;
|
|
6835
8455
|
}
|
|
6836
|
-
const key = GetAIAPIKey(driverClass, runKeys);
|
|
6837
|
-
|
|
8456
|
+
const key = GetAIAPIKey(driverClass, runKeys, false, params.CredentialScope);
|
|
8457
|
+
const source = runKeys?.some((k) => k.driverClass === driverClass) ? 'run' : platformAllowed ? 'platform' : `none — credential scope is ${params.CredentialScope}`;
|
|
8458
|
+
this.logStatus(`🔑 Action '${actionEntity.Name}' resolved an API key for '${driverClass}' (${source})`, true, params);
|
|
6838
8459
|
return key || undefined;
|
|
6839
8460
|
};
|
|
6840
8461
|
}
|
|
@@ -6901,8 +8522,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6901
8522
|
// shared by every action in the run (parallel ones included) and copied into sub-agent
|
|
6902
8523
|
// runs, so anything stamped there would name the wrong action under parallel dispatch
|
|
6903
8524
|
// and travel further than the action it was meant for. Absent when the run has no keys,
|
|
6904
|
-
// so the action uses GetAIAPIKey(driverClass) exactly as before
|
|
8525
|
+
// so the action uses GetAIAPIKey(driverClass) exactly as before — unless the scope below
|
|
8526
|
+
// is RuntimeOnly, which tells the action the resolver's answer (or its absence) is final.
|
|
6905
8527
|
RuntimeAPIKeyResolver: params.apiKeys && params.apiKeys.length > 0 ? this.buildRuntimeAPIKeyResolver(params, actionEntity) : undefined,
|
|
8528
|
+
// AICredentialScope → actions-base's RuntimeCredentialScope mirror: a value added to the
|
|
8529
|
+
// former and not the latter fails to compile here, so the two cannot drift apart.
|
|
8530
|
+
CredentialScope: params.CredentialScope,
|
|
6906
8531
|
});
|
|
6907
8532
|
if (result.Success) {
|
|
6908
8533
|
this.logStatus(` ✅ Action '${action.name}' completed successfully`, true, params);
|
|
@@ -7153,6 +8778,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7153
8778
|
configurationId: params.configurationId, // propagate configuration ID to sub-agent
|
|
7154
8779
|
effortLevel: params.effortLevel, // propagate effort level to sub-agent
|
|
7155
8780
|
apiKeys: params.apiKeys, // propagate API keys to sub-agent
|
|
8781
|
+
CredentialScope: params.CredentialScope, // a sub-agent may not spend keys its parent could not
|
|
7156
8782
|
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)
|
|
7157
8783
|
data: {
|
|
7158
8784
|
...params.data,
|
|
@@ -7845,6 +9471,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7845
9471
|
}
|
|
7846
9472
|
// Reset prompt turn counter for this execution
|
|
7847
9473
|
this._promptTurnCount = 0;
|
|
9474
|
+
this._heldDecisions = undefined;
|
|
7848
9475
|
// Resolve action-result compression config for this run (default on; opt out via
|
|
7849
9476
|
// crushActionResults: false in the agent-type prompt params).
|
|
7850
9477
|
this._actionResultCrush = this.resolveActionResultCrush(params);
|
|
@@ -8510,12 +10137,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8510
10137
|
}
|
|
8511
10138
|
}
|
|
8512
10139
|
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8513
|
-
case 'Sub-Agent':
|
|
10140
|
+
case 'Sub-Agent': {
|
|
8514
10141
|
// A Sub-Agent step carrying a task graph is a FOLDED graph (D9): the agent type
|
|
8515
10142
|
// rewrote a one-node graph into an ordinary in-run call. Record it before running,
|
|
8516
10143
|
// so the run shows what was emitted even if the sub-agent then fails.
|
|
8517
10144
|
await this.recordFoldedTaskGraph(params, previousDecision);
|
|
8518
|
-
|
|
10145
|
+
const subAgentResult = await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
10146
|
+
const afterSubAgent = (await this.finishAfterSubAgent(params, previousDecision, subAgentResult)) ?? subAgentResult;
|
|
10147
|
+
return this.selfCheckSuccess(params, afterSubAgent, afterSubAgent.newPayload ?? subAgentResult.newPayload);
|
|
10148
|
+
}
|
|
8519
10149
|
case 'Actions':
|
|
8520
10150
|
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount, this.actionOptionsForAgentType());
|
|
8521
10151
|
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
@@ -8537,6 +10167,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8537
10167
|
// step union — LoopAgentType.DetermineNextStep() emits it when the LLM chooses client tools.
|
|
8538
10168
|
case 'ClientTools':
|
|
8539
10169
|
return await this.executeClientToolsStep(params, config, previousDecision, stepCount);
|
|
10170
|
+
// Type assertion required because 'Decision' is not part of the BaseAgentNextStep step
|
|
10171
|
+
// union — FlowAgentType emits it for a Decision step, which is one decision call and no
|
|
10172
|
+
// LLM turn. executeDecisionStep returns a 'Retry' carrying the answers.
|
|
10173
|
+
case 'Decision':
|
|
10174
|
+
return await this.executeDecisionStep(params, previousDecision);
|
|
8540
10175
|
case 'Chat':
|
|
8541
10176
|
return await this.executeChatStep(params, previousDecision);
|
|
8542
10177
|
case 'Success':
|
|
@@ -8675,6 +10310,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8675
10310
|
* @private
|
|
8676
10311
|
*/
|
|
8677
10312
|
async executePromptStep(params, config, previousDecision, stepCount = 0) {
|
|
10313
|
+
// Decisions the last prompt held back: the step that carried them did not end the run, so
|
|
10314
|
+
// answer them now, where this prompt will read the answers
|
|
10315
|
+
await this.runHeldDecisions(params);
|
|
8678
10316
|
// Ask the agent type if it has a custom prompt for this step
|
|
8679
10317
|
const promptToUse = await this.AgentTypeInstance.GetPromptForStep(params, config, previousDecision?.newPayload || previousDecision?.previousPayload, this.AgentTypeState, previousDecision);
|
|
8680
10318
|
const promptId = promptToUse?.ID;
|
|
@@ -8858,6 +10496,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8858
10496
|
currentStepPayloadChangeResult = this.buildPayloadChangeResultSummary(changeResult);
|
|
8859
10497
|
// Set the final payload - the changeResult already respects the allowed paths
|
|
8860
10498
|
finalPayload = changeResult.result;
|
|
10499
|
+
// Opt-in payload change check: asks whether the flagged changes were intended. It never
|
|
10500
|
+
// reverts or blocks a change; the agent reads the result on its next turn.
|
|
10501
|
+
const payloadCheckParams = promptParams.data?.__agentTypePromptParams;
|
|
10502
|
+
await this.checkPayloadChangesWhenOn(changeResult, initialNextStep, promptResult, payloadCheckParams, params);
|
|
8861
10503
|
}
|
|
8862
10504
|
// Apply scratchpad changes if provided (zero turn cost — processed inline)
|
|
8863
10505
|
if (initialNextStep.scratchpad) {
|
|
@@ -8916,6 +10558,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8916
10558
|
const pipelineResult = await this.executePipelineAsStep(initialNextStep.pipeline, params);
|
|
8917
10559
|
this.injectPipelineResultMessage(params, pipelineResult);
|
|
8918
10560
|
}
|
|
10561
|
+
// Execute decision requests if provided (zero turn cost — processed inline), unless the
|
|
10562
|
+
// step that carries them can end the run: then they are held until it has run
|
|
10563
|
+
const decisions = initialNextStep.decisions;
|
|
10564
|
+
if (decisions?.length) {
|
|
10565
|
+
const agentTypePromptParams = promptParams.data?.__agentTypePromptParams;
|
|
10566
|
+
await this.processTurnDecisions(decisions, initialNextStep, finalPayload, agentTypePromptParams, params);
|
|
10567
|
+
}
|
|
8919
10568
|
// now that we have processed the payload, we can process the next step which does validation and changes the next step if
|
|
8920
10569
|
// validation fails
|
|
8921
10570
|
const updatedNextStep = await this.processNextStep(initialNextStep, params, config.agentType, promptResult, finalPayload, stepEntity);
|
|
@@ -9567,8 +11216,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9567
11216
|
* by one in-flight sub-agent would race the others' reads.
|
|
9568
11217
|
*
|
|
9569
11218
|
* Uses `structuredClone` (Node 17+) where available; falls back to a JSON
|
|
9570
|
-
* round-trip for environments without it
|
|
9571
|
-
*
|
|
11219
|
+
* round-trip (`ToPlainJSON`) for environments without it AND when
|
|
11220
|
+
* `structuredClone` throws (a Proxy such as a live JSONType `<Field>Object`
|
|
11221
|
+
* view, or functions). Returns the original value — with an error logged —
|
|
11222
|
+
* only when even the JSON clone fails.
|
|
9572
11223
|
*
|
|
9573
11224
|
* **JSON fallback caveats** — the round-trip is *not* shape-preserving:
|
|
9574
11225
|
* - `Date` → ISO string
|
|
@@ -9590,13 +11241,22 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9590
11241
|
return payload;
|
|
9591
11242
|
if (typeof payload !== 'object')
|
|
9592
11243
|
return payload;
|
|
9593
|
-
|
|
9594
|
-
|
|
11244
|
+
if (typeof globalThis.structuredClone === 'function') {
|
|
11245
|
+
try {
|
|
9595
11246
|
return globalThis.structuredClone(payload);
|
|
9596
11247
|
}
|
|
9597
|
-
|
|
11248
|
+
catch {
|
|
11249
|
+
// DataCloneError — typically a Proxy (a live JSONType `<Field>Object` view) or an
|
|
11250
|
+
// object holding functions. Fall through to the JSON clone below; returning the
|
|
11251
|
+
// original here would silently give the sub-agent the caller's LIVE object and lose
|
|
11252
|
+
// payload isolation.
|
|
11253
|
+
}
|
|
9598
11254
|
}
|
|
9599
|
-
|
|
11255
|
+
try {
|
|
11256
|
+
return ToPlainJSON(payload);
|
|
11257
|
+
}
|
|
11258
|
+
catch (error) {
|
|
11259
|
+
LogError(`BaseAgent.cloneSubAgentPayload: payload could not be cloned (${error instanceof Error ? error.message : String(error)}); sub-agent will share the caller's object`);
|
|
9600
11260
|
return payload;
|
|
9601
11261
|
}
|
|
9602
11262
|
}
|
|
@@ -10718,6 +12378,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10718
12378
|
catch (error) {
|
|
10719
12379
|
LogError(`Error in PostProcessActionStep: ${error.message}`);
|
|
10720
12380
|
}
|
|
12381
|
+
const finished = await this.finishAfterActions(params, previousDecision, actionResults, actionSummaries, finalPayload, parentStepId, addConversationMessage);
|
|
12382
|
+
if (finished) {
|
|
12383
|
+
return this.selfCheckSuccess(params, finished, finalPayload);
|
|
12384
|
+
}
|
|
10721
12385
|
// After actions complete, we need to process the results
|
|
10722
12386
|
// The retry step is used to re-execute the prompt with the action results
|
|
10723
12387
|
// This allows the agent to analyze the results and determine what to do next
|
|
@@ -10837,7 +12501,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10837
12501
|
// If the LLM already declared taskComplete=true alongside the client tools,
|
|
10838
12502
|
// honor that intent now that tools have executed — no need for another LLM call.
|
|
10839
12503
|
if (previousDecision.terminateAfterExecution) {
|
|
10840
|
-
return {
|
|
12504
|
+
return this.selfCheckSuccess(params, {
|
|
10841
12505
|
step: 'Success',
|
|
10842
12506
|
terminate: true,
|
|
10843
12507
|
message: previousDecision.message || 'Client tools executed successfully.',
|
|
@@ -10850,7 +12514,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10850
12514
|
// own artifact directive has to come along. Omitting it silently discarded the
|
|
10851
12515
|
// instruction whenever an agent declared client tools and taskComplete together.
|
|
10852
12516
|
artifactDirective: previousDecision.artifactDirective
|
|
10853
|
-
};
|
|
12517
|
+
}, previousDecision.newPayload);
|
|
10854
12518
|
}
|
|
10855
12519
|
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
10856
12520
|
}
|
|
@@ -10939,6 +12603,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10939
12603
|
* them calls this after MJ's gates
|
|
10940
12604
|
* (AcceptsSkills, Status, agent grant, user Run permission, the ActivationMode double gate) and
|
|
10941
12605
|
* before anything activates. The default is the identity: MJ's gates are the whole policy.
|
|
12606
|
+
* Catalog narrowing (plan Task 3.7) is not part of this policy: it runs after it, at the catalog
|
|
12607
|
+
* site, and only hides skills from the prompt. An override never needs to call `super` for it.
|
|
10942
12608
|
*
|
|
10943
12609
|
* Override it to layer a policy MJ has no table for — a tenant licensing model, a per-organization
|
|
10944
12610
|
* entitlement, a feature flag — and it applies everywhere at once. Without this seam a subclass
|
|
@@ -12794,8 +14460,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12794
14460
|
// Iterate through the agent run's steps to sum up tokens
|
|
12795
14461
|
if (this._agentRun?.Steps) {
|
|
12796
14462
|
for (const step of this._agentRun.Steps) {
|
|
12797
|
-
if ((step.StepType === 'Prompt' || step.StepType === 'Compaction') && step.PromptRun) {
|
|
12798
|
-
// Add tokens from prompt runs (rollup fields include any nested child prompt runs)
|
|
14463
|
+
if ((step.StepType === 'Prompt' || step.StepType === 'Compaction' || step.StepType === 'Decision') && step.PromptRun) {
|
|
14464
|
+
// Add tokens from prompt runs (rollup fields include any nested child prompt runs).
|
|
14465
|
+
// A Decision step carries its own decision call's run — the Finish check, a single-state
|
|
14466
|
+
// request, or one item of a forEachItemIn request — so each call is counted once.
|
|
12799
14467
|
totalTokens += step.PromptRun.TokensUsedRollup || 0;
|
|
12800
14468
|
promptTokens += step.PromptRun.TokensPromptRollup || 0;
|
|
12801
14469
|
completionTokens += step.PromptRun.TokensCompletionRollup || 0;
|
|
@@ -13177,6 +14845,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
13177
14845
|
Budget: budget,
|
|
13178
14846
|
ContextUser: params.contextUser,
|
|
13179
14847
|
Provider: this.ProviderToUse,
|
|
14848
|
+
ExecutionScope: this.runPromptExecutionScope(params),
|
|
13180
14849
|
EstimateTokens: (messages) => this.estimateConversationTokens(messages),
|
|
13181
14850
|
Verbose: params.verbose,
|
|
13182
14851
|
// The in-flight agent-response placeholder row: a post-turn pass runs while
|
|
@@ -13401,6 +15070,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
13401
15070
|
const step = await this.createCompactionStep(prompt, message, params);
|
|
13402
15071
|
// Execute summarization prompt
|
|
13403
15072
|
const promptParams = new AIPromptParams();
|
|
15073
|
+
Object.assign(promptParams, this.runPromptExecutionScope(params));
|
|
13404
15074
|
promptParams.prompt = prompt;
|
|
13405
15075
|
promptParams.data = {
|
|
13406
15076
|
originalContent,
|
|
@@ -13409,7 +15079,6 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
13409
15079
|
messageType: message.metadata?.messageType || 'unknown',
|
|
13410
15080
|
turnAdded: message.metadata?.turnAdded || 0
|
|
13411
15081
|
};
|
|
13412
|
-
promptParams.contextUser = params.contextUser;
|
|
13413
15082
|
promptParams.agentId = params.agent.ID;
|
|
13414
15083
|
promptParams.UserID = ResolvePromptRunUserID({
|
|
13415
15084
|
UserID: params.userId,
|