@memberjunction/ai-agents 6.2.0-edge.1 → 6.2.0-edge.2
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 +83 -0
- package/dist/AgentDecisionService.d.ts.map +1 -0
- package/dist/AgentDecisionService.js +155 -0
- package/dist/AgentDecisionService.js.map +1 -0
- package/dist/AgentRunner.d.ts +7 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +16 -5
- package/dist/AgentRunner.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 +113 -14
- package/dist/PayloadFeedbackManager.d.ts.map +1 -1
- package/dist/PayloadFeedbackManager.js +228 -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 +6 -0
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +42 -3
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +492 -2
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1686 -95
- 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 +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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/realtime/bridge-realtime-session-factory.d.ts +5 -2
- package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -1
- package/dist/realtime/bridge-realtime-session-factory.js +7 -4
- 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/realtime-client-session-service.d.ts +54 -12
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +76 -25
- package/dist/realtime/realtime-client-session-service.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 +38 -0
- package/dist/rubric-evaluation-agent-runner.js.map +1 -0
- package/dist/self-check.d.ts +98 -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
|
@@ -19,9 +19,11 @@ import { ResolvePromptRunUserID } from "@memberjunction/ai-core-plus";
|
|
|
19
19
|
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } 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, 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
|
|
@@ -44,7 +46,7 @@ import { FormatToolResultSection, FormatToolErrorSection, RenderToolResultData,
|
|
|
44
46
|
import { PriorTurnToolResultCache } from './prior-turn-tool-result-cache.js';
|
|
45
47
|
import { PromptComponentResolver, InjectScopedPromptParts } from './prompt-component-resolver.js';
|
|
46
48
|
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';
|
|
49
|
+
import { StringifyForPersistence, AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue, ExtractPromptResultText, GetTaskGraphSubmitter, SummarizeDecisionAnswers, SystemPlaceholderManager } from '@memberjunction/ai-core-plus';
|
|
48
50
|
import { ActionResult, RunActionParams } from '@memberjunction/actions-base';
|
|
49
51
|
import { TemplateEngineServer } from '@memberjunction/templates';
|
|
50
52
|
import { RuntimeStateFragmentBuilder, EscapeRuntimeStateTagsInMessage } from './runtime-state-fragment.js';
|
|
@@ -55,6 +57,13 @@ import { PayloadManager } from './PayloadManager.js';
|
|
|
55
57
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
56
58
|
import { ArtifactToolManager } from './ArtifactToolManager.js';
|
|
57
59
|
import { MemoryWriteManager } from './MemoryWriteManager.js';
|
|
60
|
+
import { AgentDecisionService } from './AgentDecisionService.js';
|
|
61
|
+
import { PayloadFeedbackManager } from './PayloadFeedbackManager.js';
|
|
62
|
+
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';
|
|
63
|
+
import { FinishIfModeWarnings } from './finish-if-mode-warnings.js';
|
|
64
|
+
import { ApplyCatalogNarrowing, BuildCatalogNarrowingQuestions, CatalogNarrowingCandidatesToAsk, CatalogNarrowingNote, CATALOG_NARROWING_FIND_ACTIONS, IsAlwaysShownAction, IsCatalogListNarrowed, NoCatalogNarrowing, OpeningRequestText, ReachableCatalogNarrowingLimits, ResolveCatalogNarrowingLimits, SelectCatalogNarrowing, } from './catalog-narrowing.js';
|
|
65
|
+
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';
|
|
66
|
+
import { BuildFinishIfQuestions, CapFinishIfState, FINISH_IF_STATE_EXCERPT, FormatActionForFinishIf, FormatSubAgentForFinishIf, JudgeFinishIf } from './finish-if-state.js';
|
|
58
67
|
import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
|
|
59
68
|
// The ONE payload-mapping dialect. Loop agents and the task-graph dispatcher read the same authored
|
|
60
69
|
// mapping strings, so these must be the same functions rather than two copies that agree today.
|
|
@@ -127,6 +136,19 @@ export class CircuitBreakerActionResult extends ActionResult {
|
|
|
127
136
|
this.Reason = Reason;
|
|
128
137
|
}
|
|
129
138
|
}
|
|
139
|
+
/** The top-level `reasoning` and `message` of a prompt's JSON response, when it has them. */
|
|
140
|
+
function responseReasoningAndMessage(result) {
|
|
141
|
+
const parsed = typeof result === 'string' ? CleanAndParseJSON(result) : result;
|
|
142
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
const reasoning = 'reasoning' in parsed ? parsed.reasoning : undefined;
|
|
146
|
+
const message = 'message' in parsed ? parsed.message : undefined;
|
|
147
|
+
return {
|
|
148
|
+
Reasoning: typeof reasoning === 'string' ? reasoning : undefined,
|
|
149
|
+
Message: typeof message === 'string' ? message : undefined
|
|
150
|
+
};
|
|
151
|
+
}
|
|
130
152
|
export class BaseAgent {
|
|
131
153
|
constructor() {
|
|
132
154
|
/**
|
|
@@ -285,6 +307,10 @@ export class BaseAgent {
|
|
|
285
307
|
* Allows agents to explore input artifacts on demand.
|
|
286
308
|
*/
|
|
287
309
|
this._artifactToolManager = new ArtifactToolManager();
|
|
310
|
+
/**
|
|
311
|
+
* Service for evaluating agent decisions inline via fast decision models.
|
|
312
|
+
*/
|
|
313
|
+
this._agentDecisionService = new AgentDecisionService();
|
|
288
314
|
/**
|
|
289
315
|
* Manages conversation-history retrieval tools for the current agent run.
|
|
290
316
|
* Armed only when the run has a conversationId (the cross-turn context gate).
|
|
@@ -311,6 +337,18 @@ export class BaseAgent {
|
|
|
311
337
|
* @private
|
|
312
338
|
*/
|
|
313
339
|
this._effectiveSubAgents = [];
|
|
340
|
+
/**
|
|
341
|
+
* The request that opened the run: the last user message when it started, before the framework
|
|
342
|
+
* added anything. Catalog narrowing judges the catalog against it.
|
|
343
|
+
* @private
|
|
344
|
+
*/
|
|
345
|
+
this._openingRequest = '';
|
|
346
|
+
/**
|
|
347
|
+
* Whether the run answers its conversation's opening request ({@link IsOpeningTurn}), read from
|
|
348
|
+
* the messages it started with. Decision discovery asks about no other turn.
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
this._isOpeningTurn = false;
|
|
314
352
|
/**
|
|
315
353
|
* IDs of skills already activated during this run. Prevents re-activation from re-appending
|
|
316
354
|
* the same instructions to context / re-pushing duplicate actionChanges/subAgentChanges entries
|
|
@@ -460,6 +498,20 @@ export class BaseAgent {
|
|
|
460
498
|
* unshifted onto `conversationMessages` by the shared {@link AgentMemoryContextBuilder}.
|
|
461
499
|
*/
|
|
462
500
|
this._injectedRAG = null;
|
|
501
|
+
/**
|
|
502
|
+
* Checks execution guardrails and modifies next step if limits are exceeded.
|
|
503
|
+
* This method is called after validation but before execution of non-terminal steps.
|
|
504
|
+
*
|
|
505
|
+
* @param params - Execution parameters
|
|
506
|
+
* @param nextStep - The validated next step
|
|
507
|
+
* @param currentPayload - Current payload
|
|
508
|
+
* @param agentRun - Current agent run
|
|
509
|
+
* @param currentStep - Current execution step
|
|
510
|
+
* @returns Modified next step if guardrails exceeded, or original next step
|
|
511
|
+
* @protected
|
|
512
|
+
*/
|
|
513
|
+
this._selfCheckAttemptsByRun = new Map();
|
|
514
|
+
this._selfCheckLinkByAgent = new Map();
|
|
463
515
|
/**
|
|
464
516
|
* Determines the request type ID based on the Chat step's context.
|
|
465
517
|
* - If the responseForm has only approve/reject-style buttons, uses "Approval"
|
|
@@ -1109,6 +1161,8 @@ export class BaseAgent {
|
|
|
1109
1161
|
* @protected
|
|
1110
1162
|
*/
|
|
1111
1163
|
get FileOutputs() { return this._fileOutputs; }
|
|
1164
|
+
/** The longest the catalog narrowing decision may take before the run shows the full catalog. */
|
|
1165
|
+
static { this.CATALOG_NARROWING_TIMEOUT_MS = 30000; }
|
|
1112
1166
|
/** The skills activated so far in this run (requested or self-activated), in activation order. */
|
|
1113
1167
|
get ActivatedSkillIDs() {
|
|
1114
1168
|
return this._activatedSkillIDs;
|
|
@@ -1563,6 +1617,9 @@ export class BaseAgent {
|
|
|
1563
1617
|
this._dynamicActionLimits = {};
|
|
1564
1618
|
this._mediaOutputs = [];
|
|
1565
1619
|
this._messageLifecycleCallback = params.onMessageLifecycle;
|
|
1620
|
+
this._catalogNarrowing = undefined;
|
|
1621
|
+
this._openingRequest = OpeningRequestText(params.conversationMessages);
|
|
1622
|
+
this._isOpeningTurn = IsOpeningTurn(params.conversationMessages);
|
|
1566
1623
|
// Resolve storage account for file artifacts
|
|
1567
1624
|
this._resolvedStorageAccountId = await this.getStorageAccountID(wrappedParams);
|
|
1568
1625
|
// Check for cancellation after Phase 1
|
|
@@ -1624,7 +1681,10 @@ export class BaseAgent {
|
|
|
1624
1681
|
// Carry the previous turn's tool results forward (no-op without a
|
|
1625
1682
|
// conversationId). Runs here so the results are in the messages before
|
|
1626
1683
|
// the pre-turn compaction check and the first prompt.
|
|
1627
|
-
this.injectPriorTurnToolResults(wrappedParams)
|
|
1684
|
+
this.injectPriorTurnToolResults(wrappedParams),
|
|
1685
|
+
// Decision discovery (plan Task 3.1), off unless the decisionsEnabled and decisionDiscovery prompt
|
|
1686
|
+
// params are true: may add a <suggested_agent> system message, within DECISION_DISCOVERY_TIMEOUT_MS.
|
|
1687
|
+
this.InjectDecisionDiscovery(params.agent, params.contextUser, wrappedParams.conversationMessages, params.data)
|
|
1628
1688
|
]);
|
|
1629
1689
|
// Inject scope-resolved prompt parts (role-faithful) for this agent's prompt, alongside
|
|
1630
1690
|
// memory/RAG. Synchronous — parts are cached on AIEngine. Uses the same run scope.
|
|
@@ -1713,6 +1773,9 @@ export class BaseAgent {
|
|
|
1713
1773
|
// consumers that re-read `params` after the call see what they
|
|
1714
1774
|
// passed in, not our chained signal.
|
|
1715
1775
|
params.cancellationToken = upstreamToken;
|
|
1776
|
+
// A cancellation, or an error thrown out of the step loop, leaves any held decision
|
|
1777
|
+
// requests unasked. The terminate path has already logged and cleared its own.
|
|
1778
|
+
this.skipHeldDecisions(params, this.runEndForHeldDecisions());
|
|
1716
1779
|
this.releasePerRunDataCache();
|
|
1717
1780
|
await this.finalizeRun(this.deriveRunOutcome());
|
|
1718
1781
|
}
|
|
@@ -1891,6 +1954,11 @@ export class BaseAgent {
|
|
|
1891
1954
|
return {
|
|
1892
1955
|
CoAgent: params.agent,
|
|
1893
1956
|
TargetAgentID: targetID,
|
|
1957
|
+
// The run's runtime API keys, so the bridged voice session mints on the caller's key when
|
|
1958
|
+
// the run carries one for the realtime driver (the same run-key → platform order as
|
|
1959
|
+
// GetAIAPIKey; realtime does not consult MJ Credentials). Absent ⇒ platform keys, as before.
|
|
1960
|
+
// CreateBridgeRealtimeSession (the LiveKit / telephony factory) passes no apiKeys today.
|
|
1961
|
+
APIKeys: params.apiKeys,
|
|
1894
1962
|
AgentSessionID: params.data?.agentSessionId ?? '',
|
|
1895
1963
|
PreferredModelID: modelID,
|
|
1896
1964
|
ConfigOverridesJson: BuildRealtimeOverridesJson(modelID, voice) ?? undefined,
|
|
@@ -1928,13 +1996,22 @@ export class BaseAgent {
|
|
|
1928
1996
|
// would dead-end whenever the top model lacked a key — e.g. a power-11 model with no env key
|
|
1929
1997
|
// (Inworld/AssemblyAI) outranking GPT Realtime — and surface "No usable Realtime model" even though
|
|
1930
1998
|
// a usable model exists. This mirrors the same fix in RealtimeClientSessionService.
|
|
1999
|
+
// The run's runtime API keys, in GetAIAPIKey's order: the run's key for a driver class, else
|
|
2000
|
+
// the platform's. That is the legacy tier of prompt key resolution — a prompt consults MJ
|
|
2001
|
+
// Credentials / AICredentialBindings first, and realtime does not consult them at all.
|
|
2002
|
+
//
|
|
2003
|
+
// This also affects WHICH vendor runs the session, deliberately. Vendor selection walks
|
|
2004
|
+
// candidates by priority and takes the first whose key resolves, so an organization that
|
|
2005
|
+
// brings its own credential for a vendor we hold no platform key for now reaches that
|
|
2006
|
+
// vendor — which is the point of bringing your own key, not a side effect of it.
|
|
2007
|
+
const resolveAPIKey = MakeAIAPIKeyResolver(params.apiKeys);
|
|
1931
2008
|
const candidates = this.selectRealtimeModelCandidates(params.agent, overrideModelID);
|
|
1932
2009
|
for (const model of candidates) {
|
|
1933
|
-
const vendor = SelectRealtimeVendorForModel(model.ID);
|
|
2010
|
+
const vendor = SelectRealtimeVendorForModel(model.ID, resolveAPIKey);
|
|
1934
2011
|
if (!vendor) {
|
|
1935
2012
|
continue;
|
|
1936
2013
|
}
|
|
1937
|
-
const apiKey =
|
|
2014
|
+
const apiKey = resolveAPIKey(vendor.DriverClass);
|
|
1938
2015
|
if (!apiKey) {
|
|
1939
2016
|
continue;
|
|
1940
2017
|
}
|
|
@@ -2799,6 +2876,7 @@ export class BaseAgent {
|
|
|
2799
2876
|
// Check if we should continue or terminate
|
|
2800
2877
|
if (nextStep.terminate) {
|
|
2801
2878
|
continueExecution = false;
|
|
2879
|
+
this.skipHeldDecisions(params, 'the run ended after the step that carried them');
|
|
2802
2880
|
this.logStatus(`🏁 Agent '${params.agent.Name}' terminating after ${stepCount} steps with result: ${nextStep.step}`, true, params);
|
|
2803
2881
|
}
|
|
2804
2882
|
else {
|
|
@@ -2873,6 +2951,25 @@ export class BaseAgent {
|
|
|
2873
2951
|
}
|
|
2874
2952
|
return 'combined';
|
|
2875
2953
|
}
|
|
2954
|
+
/**
|
|
2955
|
+
* The observability context memory injection runs under for an agent run: the run's ID, the next
|
|
2956
|
+
* step number, and a callback that adds each rerank step to the run's steps. A rerank step carries
|
|
2957
|
+
* the rerank's prompt run, whose cost and token rollups include a prompt-backed reranker's own
|
|
2958
|
+
* prompt runs, so the run's totals and its `MaxCostPerRun` / `MaxTokensPerRun` guardrails count
|
|
2959
|
+
* them. Undefined when there is no run.
|
|
2960
|
+
*
|
|
2961
|
+
* @param run - The current agent run, or null before one exists
|
|
2962
|
+
*/
|
|
2963
|
+
MemoryObservability(run) {
|
|
2964
|
+
if (!run) {
|
|
2965
|
+
return undefined;
|
|
2966
|
+
}
|
|
2967
|
+
return {
|
|
2968
|
+
agentRunID: run.ID,
|
|
2969
|
+
stepNumber: (run.Steps?.length || 0) + 1,
|
|
2970
|
+
OnStepCreated: step => run.Steps.push(step),
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2876
2973
|
/**
|
|
2877
2974
|
* Inject notes and examples into agent context memory.
|
|
2878
2975
|
* Called automatically before agent execution if injection is enabled on the agent.
|
|
@@ -2894,9 +2991,7 @@ export class BaseAgent {
|
|
|
2894
2991
|
// Delegate the orchestration to the shared, reusable builder so both BaseAgent and the
|
|
2895
2992
|
// Realtime agent type inject memory identically. The observability context and verbose
|
|
2896
2993
|
// 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;
|
|
2994
|
+
const observability = this.MemoryObservability(this._agentRun);
|
|
2900
2995
|
const result = await new AgentMemoryContextBuilder().InjectContextMemory(input, agent, userId, companyId, contextUser, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, secondaryScopeConfig, observability, (message, verboseOnly) => this.logStatus(message, verboseOnly));
|
|
2901
2996
|
// Store for inclusion in result (externally observable behavior preserved)
|
|
2902
2997
|
this._injectedMemory = result;
|
|
@@ -2961,6 +3056,301 @@ export class BaseAgent {
|
|
|
2961
3056
|
this._injectedRAG = result;
|
|
2962
3057
|
return result;
|
|
2963
3058
|
}
|
|
3059
|
+
/**
|
|
3060
|
+
* Decision discovery (plan Task 3.1): suggests the agent to delegate to before the first prompt.
|
|
3061
|
+
*
|
|
3062
|
+
* Runs in parallel with the rest of Phase 2 of `Execute()`, and only when the merged agent-type
|
|
3063
|
+
* prompt params set `decisionsEnabled: true` (the master switch, {@link DecisionsEnabled}) and
|
|
3064
|
+
* `decisionDiscovery: true`, the run answers its conversation's opening request
|
|
3065
|
+
* ({@link IsOpeningTurn}), and that request @mentions no agent. A follow-up turn is never asked
|
|
3066
|
+
* about: it may be continuing work with an agent already engaged, and a suggestion must not pull
|
|
3067
|
+
* the agent away from it. One decision asks which of the agents the user may run (and the host
|
|
3068
|
+
* allows) should handle the request, and whether a specialist should. When both answers are
|
|
3069
|
+
* confident, a `<suggested_agent>` system message is unshifted onto `conversationMessages`, the
|
|
3070
|
+
* way pre-execution RAG adds `<retrieved_context>`. Each discovery is recorded as one
|
|
3071
|
+
* `Agent discovery` Decision step.
|
|
3072
|
+
*
|
|
3073
|
+
* Fails safe: an error, a timeout, a cancelled run, an unusable answer or an unsure one adds
|
|
3074
|
+
* nothing, so the agent behaves as it would without discovery. So do fewer than
|
|
3075
|
+
* {@link DECISION_DISCOVERY_MIN_OPTIONS} options. Never delays the first prompt by more than
|
|
3076
|
+
* {@link DECISION_DISCOVERY_TIMEOUT_MS}, and never throws.
|
|
3077
|
+
*
|
|
3078
|
+
* The any-applies question does not name the options, so the decision cannot say that none of
|
|
3079
|
+
* them fits: see {@link DECISION_DISCOVERY_APPLIES_INSTRUCTIONS} for why it stays that way for now.
|
|
3080
|
+
*
|
|
3081
|
+
* @param agent - The agent being executed.
|
|
3082
|
+
* @param contextUser - The user whose run permission decides the options.
|
|
3083
|
+
* @param conversationMessages - The mutated message array that flows to the LLM (the suggestion is unshifted here).
|
|
3084
|
+
* @param data - The run's `ExecuteAgentParams.data`. Its `__agentTypePromptParams` overrides the agent's prompt
|
|
3085
|
+
* params, and its `ALL_AVAILABLE_AGENTS`, when an array, is the host's allow-list: no other agent is suggested.
|
|
3086
|
+
*/
|
|
3087
|
+
async InjectDecisionDiscovery(agent, contextUser, conversationMessages, data) {
|
|
3088
|
+
try {
|
|
3089
|
+
const override = data?.__agentTypePromptParams;
|
|
3090
|
+
const overrides = IsPlainObject(override) ? override : undefined;
|
|
3091
|
+
if (!this.isDecisionDiscoveryOnFor(agent, overrides) || !conversationMessages) {
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
if (!this._openingRequest || !contextUser) {
|
|
3095
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': the run has no ${contextUser ? 'user message' : 'context user'}`, true);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
if (!this._isOpeningTurn) {
|
|
3099
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': a follow-up turn, which may continue with an agent already engaged`, true);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
if (MentionsAgent(this._openingRequest, AIEngine.Instance.Agents, agent.ID)) {
|
|
3103
|
+
this.logStatus(`Decision discovery skipped for '${agent.Name}': the request @mentions an agent`, true);
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const promptParams = this.decisionDiscoveryPromptParams(agent, overrides);
|
|
3107
|
+
const outcome = await this.runDecisionDiscovery(agent, contextUser, promptParams, HostAllowedAgentIDs(data));
|
|
3108
|
+
if (outcome.Injected && outcome.Message) {
|
|
3109
|
+
conversationMessages.unshift({ role: 'system', content: outcome.Message });
|
|
3110
|
+
this.logStatus(`Decision discovery suggested '${outcome.Answer?.Agent.Name}' to '${agent.Name}'`, true);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
catch (error) {
|
|
3114
|
+
this.warnDecisionDiscovery(agent, error instanceof Error ? error.message : String(error));
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Whether discovery is on for this run: the master switch ({@link DecisionsEnabled}) and
|
|
3119
|
+
* `decisionDiscovery` are both on. Read without rebuilding the merged prompt params, since it is
|
|
3120
|
+
* asked on every run of every agent: each setting from the per-run override when it sets one,
|
|
3121
|
+
* otherwise from the agent's base params, cached on AIEngine with its base catalog. Only a cold
|
|
3122
|
+
* cache builds them.
|
|
3123
|
+
*/
|
|
3124
|
+
isDecisionDiscoveryOnFor(agent, overrides) {
|
|
3125
|
+
let base;
|
|
3126
|
+
const paramsSetting = (key) => {
|
|
3127
|
+
if (overrides && key in overrides) {
|
|
3128
|
+
return overrides;
|
|
3129
|
+
}
|
|
3130
|
+
base ??= this.cachedBasePromptParams(agent) ?? this.buildDecisionDiscoveryPromptParams(agent, undefined);
|
|
3131
|
+
return base;
|
|
3132
|
+
};
|
|
3133
|
+
return this.DecisionsEnabled(paramsSetting('decisionsEnabled')) && IsDecisionDiscoveryOn(paramsSetting('decisionDiscovery'));
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* The run's merged agent-type prompt params (schema defaults < agent < per-run overrides), once
|
|
3137
|
+
* discovery is on: the cached base params when there is no override, otherwise built.
|
|
3138
|
+
*/
|
|
3139
|
+
decisionDiscoveryPromptParams(agent, overrides) {
|
|
3140
|
+
const cached = overrides ? undefined : this.cachedBasePromptParams(agent);
|
|
3141
|
+
return cached ?? this.buildDecisionDiscoveryPromptParams(agent, overrides);
|
|
3142
|
+
}
|
|
3143
|
+
/** The agent's base prompt params (no per-run overrides), from the base catalog AIEngine caches, when it holds one. */
|
|
3144
|
+
cachedBasePromptParams(agent) {
|
|
3145
|
+
return AIEngine.Instance.GetAgentBaseCatalog(agent.ID)?.baseAgentTypePromptParams;
|
|
3146
|
+
}
|
|
3147
|
+
/** Builds the merged prompt params, as `gatherPromptTemplateData` does. */
|
|
3148
|
+
buildDecisionDiscoveryPromptParams(agent, overrides) {
|
|
3149
|
+
const agentType = AIEngine.Instance.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
3150
|
+
return this.buildAgentTypePromptParams(agentType, agent, overrides);
|
|
3151
|
+
}
|
|
3152
|
+
/**
|
|
3153
|
+
* Records one `Agent discovery` step around a bounded discovery, and returns what it found. A
|
|
3154
|
+
* failed discovery is logged as a warning; an unsure one only in verbose logs, except that an
|
|
3155
|
+
* answer from a model with no discovery calibration is warned about once per model, because
|
|
3156
|
+
* then discovery can never suggest anything.
|
|
3157
|
+
*/
|
|
3158
|
+
async runDecisionDiscovery(agent, contextUser, promptParams, hostAllowedIDs) {
|
|
3159
|
+
const promptName = typeof promptParams.decisionPromptName === 'string'
|
|
3160
|
+
? promptParams.decisionPromptName
|
|
3161
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
3162
|
+
const step = await this.startDecisionDiscoveryStep(contextUser, promptName);
|
|
3163
|
+
const outcome = await this.boundDecisionDiscovery(signal => this.discoverAgent(agent, contextUser, promptName, hostAllowedIDs, signal), step);
|
|
3164
|
+
await this.finishDecisionDiscoveryStep(step, outcome);
|
|
3165
|
+
if (!outcome.Succeeded) {
|
|
3166
|
+
this.warnDecisionDiscovery(agent, outcome.Reason ?? 'unknown error');
|
|
3167
|
+
}
|
|
3168
|
+
else if (!outcome.Injected) {
|
|
3169
|
+
if (outcome.UncalibratedModel) {
|
|
3170
|
+
BaseAgent.warnUncalibratedDiscoveryModel(outcome.UncalibratedModel);
|
|
3171
|
+
}
|
|
3172
|
+
this.logStatus(`Decision discovery for '${agent.Name}' suggested no agent: ${outcome.Reason}`, true);
|
|
3173
|
+
}
|
|
3174
|
+
return outcome;
|
|
3175
|
+
}
|
|
3176
|
+
/** The answering models already warned about as having no discovery calibration, so each is warned about once per process. */
|
|
3177
|
+
static { this._uncalibratedDiscoveryModelsWarned = new Set(); }
|
|
3178
|
+
/**
|
|
3179
|
+
* Warns, once per model, that discovery answered by a model with no discovery calibration
|
|
3180
|
+
* suggests nothing. That happens when a decision model is added or renamed, when a vendor model
|
|
3181
|
+
* moves to another version (Jev's `APIName`), or when LLM Decision answers through a chat model
|
|
3182
|
+
* other than the one its calibration was fitted on; without the warning the only trace is each
|
|
3183
|
+
* `Agent discovery` step's reason.
|
|
3184
|
+
*/
|
|
3185
|
+
static warnUncalibratedDiscoveryModel(model) {
|
|
3186
|
+
if (BaseAgent._uncalibratedDiscoveryModelsWarned.has(model)) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
BaseAgent._uncalibratedDiscoveryModelsWarned.add(model);
|
|
3190
|
+
LogErrorEx({
|
|
3191
|
+
message: `Decision discovery answered by ${model} suggests nothing: the model has no discovery calibration `
|
|
3192
|
+
+ '(DECISION_DISCOVERY_CALIBRATION), so its answers are treated as unsure. Fit one with the agent-discovery '
|
|
3193
|
+
+ 'Decision Eval, or check that the model was not renamed or moved to another version.',
|
|
3194
|
+
severity: 'warning',
|
|
3195
|
+
category: 'DecisionDiscovery',
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* Runs `discover` until it finishes, {@link DECISION_DISCOVERY_TIMEOUT_MS} passes, or the run is
|
|
3200
|
+
* cancelled. A timeout or a cancellation aborts the signal `discover` was given, which aborts the
|
|
3201
|
+
* decision call, and returns a failed discovery at once. What `discover` returns after that
|
|
3202
|
+
* suggests nothing, but the prompt run of its decision call is still linked to `step` once it
|
|
3203
|
+
* settles ({@link linkLateDecisionPromptRun}), so the run counts the call's cost.
|
|
3204
|
+
*/
|
|
3205
|
+
async boundDecisionDiscovery(discover, step) {
|
|
3206
|
+
const controller = new AbortController();
|
|
3207
|
+
let stoppedOutcome;
|
|
3208
|
+
const stopped = new Promise(resolve => controller.signal.addEventListener('abort', () => {
|
|
3209
|
+
stoppedOutcome = FailedDecisionDiscovery(String(controller.signal.reason));
|
|
3210
|
+
resolve(stoppedOutcome);
|
|
3211
|
+
}, { once: true }));
|
|
3212
|
+
const runToken = this._executeParams?.cancellationToken;
|
|
3213
|
+
const relayRunAbort = () => controller.abort('the run was cancelled');
|
|
3214
|
+
if (runToken?.aborted) {
|
|
3215
|
+
relayRunAbort();
|
|
3216
|
+
}
|
|
3217
|
+
else {
|
|
3218
|
+
runToken?.addEventListener('abort', relayRunAbort, { once: true });
|
|
3219
|
+
}
|
|
3220
|
+
const timeoutMS = DECISION_DISCOVERY_TIMEOUT_MS;
|
|
3221
|
+
const timer = setTimeout(() => controller.abort(`the decision timed out after ${timeoutMS}ms`), timeoutMS);
|
|
3222
|
+
try {
|
|
3223
|
+
const discovering = discover(controller.signal);
|
|
3224
|
+
const outcome = await Promise.race([discovering, stopped]);
|
|
3225
|
+
if (outcome === stoppedOutcome) {
|
|
3226
|
+
this.linkLateDecisionPromptRun(step, discovering.then(late => late.Result));
|
|
3227
|
+
}
|
|
3228
|
+
return outcome;
|
|
3229
|
+
}
|
|
3230
|
+
catch (error) {
|
|
3231
|
+
return FailedDecisionDiscovery(error instanceof Error ? error.message : String(error));
|
|
3232
|
+
}
|
|
3233
|
+
finally {
|
|
3234
|
+
clearTimeout(timer);
|
|
3235
|
+
runToken?.removeEventListener('abort', relayRunAbort);
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
/**
|
|
3239
|
+
* The discovery itself: the options, rebuilt from the permitted catalog, then one decision about
|
|
3240
|
+
* the opening request, and what its answer means. Never throws.
|
|
3241
|
+
*/
|
|
3242
|
+
async discoverAgent(agent, contextUser, promptName, hostAllowedIDs, signal) {
|
|
3243
|
+
try {
|
|
3244
|
+
const { Options: options, Error: optionsError, ...rest } = await this.decisionDiscoveryOptionSet(agent, contextUser, promptName, hostAllowedIDs);
|
|
3245
|
+
const sizes = { ...rest, OptionCount: options.length };
|
|
3246
|
+
if (optionsError) {
|
|
3247
|
+
return { ...FailedDecisionDiscovery(optionsError), ...sizes };
|
|
3248
|
+
}
|
|
3249
|
+
if (options.length < DECISION_DISCOVERY_MIN_OPTIONS) {
|
|
3250
|
+
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 };
|
|
3251
|
+
}
|
|
3252
|
+
if (signal.aborted) {
|
|
3253
|
+
return { ...FailedDecisionDiscovery(String(signal.reason)), ...sizes };
|
|
3254
|
+
}
|
|
3255
|
+
const result = await this._agentDecisionService.Ask({
|
|
3256
|
+
State: this._openingRequest,
|
|
3257
|
+
Questions: BuildDecisionDiscoveryQuestions(options),
|
|
3258
|
+
ContextUser: contextUser,
|
|
3259
|
+
AgentID: agent.ID,
|
|
3260
|
+
PromptName: promptName,
|
|
3261
|
+
CancellationToken: signal,
|
|
3262
|
+
});
|
|
3263
|
+
return { ...DecisionDiscoveryFromResult(result, options, DECISION_DISCOVERY_MIN_CONFIDENCE), ...sizes };
|
|
3264
|
+
}
|
|
3265
|
+
catch (error) {
|
|
3266
|
+
return FailedDecisionDiscovery(error instanceof Error ? error.message : String(error));
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
/**
|
|
3270
|
+
* The Choice's options, rebuilt on every call and never cached ({@link BuildDecisionDiscoveryOptionSet}):
|
|
3271
|
+
* from AIEngine's current catalog, the agents the user may run, the decision prompt's option cap,
|
|
3272
|
+
* and the semantic search over the opening request when the provider can run it.
|
|
3273
|
+
*/
|
|
3274
|
+
async decisionDiscoveryOptionSet(agent, contextUser, promptName, hostAllowedIDs) {
|
|
3275
|
+
const provider = this.ProviderToUse;
|
|
3276
|
+
return BuildDecisionDiscoveryOptionSet({
|
|
3277
|
+
Agents: await DecisionDiscoveryRunnableAgents(AIEngine.Instance.Agents, contextUser),
|
|
3278
|
+
RunningAgentID: agent.ID,
|
|
3279
|
+
HostAllowedIDs: hostAllowedIDs,
|
|
3280
|
+
DeclaredCap: DecisionPromptOptionCap(AIEngine.Instance, promptName),
|
|
3281
|
+
Search: CanSearchEntities(provider) ? DecisionDiscoveryAgentSearch(provider, this._openingRequest, contextUser) : undefined,
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
/** Opens the `Agent discovery` Decision step. Without a run there is no step. */
|
|
3285
|
+
async startDecisionDiscoveryStep(contextUser, promptName) {
|
|
3286
|
+
if (!this._agentRun) {
|
|
3287
|
+
return undefined;
|
|
3288
|
+
}
|
|
3289
|
+
try {
|
|
3290
|
+
return await this.createStepEntity({
|
|
3291
|
+
stepType: 'Decision',
|
|
3292
|
+
stepName: 'Agent discovery',
|
|
3293
|
+
contextUser,
|
|
3294
|
+
inputData: {
|
|
3295
|
+
request: this._openingRequest,
|
|
3296
|
+
promptName,
|
|
3297
|
+
minConfidence: DECISION_DISCOVERY_MIN_CONFIDENCE,
|
|
3298
|
+
timeoutMS: DECISION_DISCOVERY_TIMEOUT_MS,
|
|
3299
|
+
},
|
|
3300
|
+
});
|
|
3301
|
+
}
|
|
3302
|
+
catch (error) {
|
|
3303
|
+
LogError(`Could not record the agent discovery step: ${error instanceof Error ? error.message : String(error)}`);
|
|
3304
|
+
return undefined;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Finalizes the `Agent discovery` step: the answer, whether it was injected, the host allow-list,
|
|
3309
|
+
* the agents left out for having no description (at most {@link DECISION_DISCOVERY_MAX_RECORDED_IDS}
|
|
3310
|
+
* IDs), any narrowing, and the call's usage.
|
|
3311
|
+
*/
|
|
3312
|
+
async finishDecisionDiscoveryStep(step, outcome) {
|
|
3313
|
+
if (!step) {
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
const result = outcome.Result;
|
|
3317
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
3318
|
+
if (result) {
|
|
3319
|
+
this.attachDecisionPromptRun(step, result);
|
|
3320
|
+
}
|
|
3321
|
+
const answer = outcome.Answer;
|
|
3322
|
+
const withoutDescription = outcome.WithoutDescription ?? [];
|
|
3323
|
+
await this.finalizeStepEntity(step, outcome.Succeeded, outcome.Succeeded ? undefined : outcome.Reason, {
|
|
3324
|
+
injected: outcome.Injected,
|
|
3325
|
+
reason: outcome.Reason,
|
|
3326
|
+
catalogSize: outcome.CatalogSize,
|
|
3327
|
+
hostAllowList: outcome.HostAllowListSize === undefined ? false : { size: outcome.HostAllowListSize },
|
|
3328
|
+
withoutDescription: { count: withoutDescription.length, agentIds: withoutDescription.slice(0, DECISION_DISCOVERY_MAX_RECORDED_IDS) },
|
|
3329
|
+
options: outcome.OptionCount,
|
|
3330
|
+
optionLimit: outcome.OptionLimit,
|
|
3331
|
+
declaredOptionCap: outcome.DeclaredOptionCap,
|
|
3332
|
+
narrowed: outcome.NarrowedFrom === undefined ? false : { by: 'semantic search', from: outcome.NarrowedFrom, to: outcome.OptionCount },
|
|
3333
|
+
answer: answer ? {
|
|
3334
|
+
agentId: answer.Agent.ID,
|
|
3335
|
+
agent: answer.Agent.Name,
|
|
3336
|
+
confidence: answer.Confidence,
|
|
3337
|
+
anyApplies: answer.AnyApplies,
|
|
3338
|
+
probabilities: answer.Probabilities,
|
|
3339
|
+
} : undefined,
|
|
3340
|
+
executionTimeMS: result?.executionTimeMS,
|
|
3341
|
+
tokensUsed: result?.tokensUsed,
|
|
3342
|
+
model: result?.DecisionResult?.ResolvedModel ?? result?.modelInfo?.modelName,
|
|
3343
|
+
promptRunId: result?.promptRun?.ID,
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
/** Logs that decision discovery failed, so nothing was suggested. */
|
|
3347
|
+
warnDecisionDiscovery(agent, reason) {
|
|
3348
|
+
LogErrorEx({
|
|
3349
|
+
message: `Decision discovery for '${agent.Name}' failed, so no agent is suggested: ${reason}`,
|
|
3350
|
+
severity: 'warning',
|
|
3351
|
+
category: 'DecisionDiscovery',
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
2964
3354
|
/**
|
|
2965
3355
|
* Converts UI markup (@{...} syntax) in user messages to plain text.
|
|
2966
3356
|
* This prevents agents from being confused by UI-specific JSON syntax and reduces token usage.
|
|
@@ -3437,6 +3827,9 @@ export class BaseAgent {
|
|
|
3437
3827
|
return;
|
|
3438
3828
|
}
|
|
3439
3829
|
// ...and a per-agent-ACTION gate: rows that opt out are removed before the tool set is built.
|
|
3830
|
+
// Catalog narrowing (plan Task 3.7) does not apply here, by design: it shortens the prose
|
|
3831
|
+
// catalog only. A call to an undeclared tool is a Retry, and this mode has no `Actions`
|
|
3832
|
+
// envelope step, so a hidden action or sub-agent left undeclared could not be called at all.
|
|
3440
3833
|
const actions = FilterDeclarableActions(this.getEffectiveActionsForValidation(params.agent.ID), AIEngine.Instance.AgentActions.filter((aa) => UUIDsEqual(aa.AgentID, params.agent.ID)));
|
|
3441
3834
|
const subAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
3442
3835
|
if (actions.length === 0 && subAgents.length === 0) {
|
|
@@ -4741,23 +5134,95 @@ export class BaseAgent {
|
|
|
4741
5134
|
// Default behavior: let Chat propagate up (no remapping)
|
|
4742
5135
|
return nextStep;
|
|
4743
5136
|
}
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
5137
|
+
async applySelfCheck(params, nextStep, agentRun, currentPayload) {
|
|
5138
|
+
if (params.cancellationToken?.aborted)
|
|
5139
|
+
return null;
|
|
5140
|
+
const provider = params.provider || this._activeProvider;
|
|
5141
|
+
let row = this._selfCheckLinkByAgent.get(agentRun.AgentID);
|
|
5142
|
+
if (row === undefined) {
|
|
5143
|
+
try {
|
|
5144
|
+
const view = RunView.FromMetadataProvider(provider);
|
|
5145
|
+
const found = await view.RunView({
|
|
5146
|
+
EntityName: 'MJ: AI Agent Rubrics',
|
|
5147
|
+
ExtraFilter: `AgentID='${agentRun.AgentID}' AND Purpose='SelfCheck' AND Status='Active'`,
|
|
5148
|
+
OrderBy: 'ID',
|
|
5149
|
+
ResultType: 'simple',
|
|
5150
|
+
}, params.contextUser);
|
|
5151
|
+
if (!found.Success) {
|
|
5152
|
+
LogError(`Self-check lookup failed, so the run continues: ${found.ErrorMessage || 'the view did not succeed'}`);
|
|
5153
|
+
return null;
|
|
5154
|
+
}
|
|
5155
|
+
row = PickSelfCheckLink((found.Results ?? [])) ?? null;
|
|
5156
|
+
this._selfCheckLinkByAgent.set(agentRun.AgentID, row);
|
|
5157
|
+
}
|
|
5158
|
+
catch (error) {
|
|
5159
|
+
LogError(`Self-check lookup failed, so the run continues: ${error instanceof Error ? error.message : String(error)}`);
|
|
5160
|
+
return null;
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
if (!row?.RubricID)
|
|
5164
|
+
return null;
|
|
5165
|
+
const link = {
|
|
5166
|
+
Purpose: String(row.Purpose ?? ''),
|
|
5167
|
+
Status: String(row.Status ?? ''),
|
|
5168
|
+
MaxAttempts: row.MaxSelfCheckAttempts ?? null,
|
|
5169
|
+
rubricId: String(row.RubricID),
|
|
5170
|
+
passThreshold: row.PassThreshold ?? null,
|
|
5171
|
+
};
|
|
5172
|
+
const attempt = (this._selfCheckAttemptsByRun.get(agentRun.ID) ?? 0) + 1;
|
|
5173
|
+
this._selfCheckAttemptsByRun.set(agentRun.ID, attempt);
|
|
5174
|
+
const record = async (step) => {
|
|
5175
|
+
const saved = await this.createStepEntity({
|
|
5176
|
+
stepType: 'Validation',
|
|
5177
|
+
stepName: 'Rubric self-check',
|
|
5178
|
+
contextUser: params.contextUser,
|
|
5179
|
+
inputData: { rubricEvaluationId: step.EvaluationId, passed: step.Passed },
|
|
5180
|
+
});
|
|
5181
|
+
await this.finalizeStepEntity(saved, step.Passed, step.Message || undefined);
|
|
5182
|
+
};
|
|
5183
|
+
try {
|
|
5184
|
+
const outcome = await ExecuteSelfCheck({
|
|
5185
|
+
engine: ProviderRubricEngine(provider, params.contextUser),
|
|
5186
|
+
link,
|
|
5187
|
+
runId: agentRun.ID,
|
|
5188
|
+
agentKind: this.AgentTypeInstance?.constructor?.name === 'LoopAgentType' ? 'loop' : 'flow',
|
|
5189
|
+
attempt,
|
|
5190
|
+
candidate: { Message: nextStep.message, Payload: nextStep.newPayload ?? currentPayload },
|
|
5191
|
+
record,
|
|
5192
|
+
});
|
|
5193
|
+
if (outcome.step === 'Success')
|
|
5194
|
+
return null;
|
|
5195
|
+
if (outcome.step === 'Retry')
|
|
5196
|
+
return { ...nextStep, step: 'Retry', message: outcome.decision.Message };
|
|
5197
|
+
return { ...nextStep, step: 'Failed', message: outcome.decision.Message, errorMessage: outcome.decision.Message };
|
|
5198
|
+
}
|
|
5199
|
+
catch (error) {
|
|
5200
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5201
|
+
LogError(`Self-check could not be saved, so the run continues: ${message}`);
|
|
5202
|
+
try {
|
|
5203
|
+
await record({ Passed: false, Message: message });
|
|
5204
|
+
}
|
|
5205
|
+
catch (recordError) {
|
|
5206
|
+
LogError(`Self-check validation step could not be recorded: ${recordError instanceof Error ? recordError.message : String(recordError)}`);
|
|
5207
|
+
}
|
|
5208
|
+
return null;
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
/** Runs the self-check when a success exit did not pass through the guardrail hook. */
|
|
5212
|
+
async selfCheckSuccess(params, step, payload) {
|
|
5213
|
+
if (step.step !== 'Success' || !this._agentRun)
|
|
5214
|
+
return step;
|
|
5215
|
+
const checked = await this.applySelfCheck(params, step, this._agentRun, payload);
|
|
5216
|
+
return checked ?? step;
|
|
5217
|
+
}
|
|
4756
5218
|
async checkExecutionGuardrails(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
4757
|
-
|
|
4758
|
-
if (nextStep.step === 'Success' || nextStep.step === 'Failed' || nextStep.step === 'Chat') {
|
|
5219
|
+
if (nextStep.step === 'Failed' || nextStep.step === 'Chat') {
|
|
4759
5220
|
return nextStep;
|
|
4760
5221
|
}
|
|
5222
|
+
if (nextStep.step === 'Success') {
|
|
5223
|
+
const checked = await this.applySelfCheck(params, nextStep, agentRun, currentPayload);
|
|
5224
|
+
return checked ?? nextStep;
|
|
5225
|
+
}
|
|
4761
5226
|
// Check if any guardrails are exceeded
|
|
4762
5227
|
const guardrailResult = await this.hasExceededAgentRunGuardrails(params, agentRun);
|
|
4763
5228
|
if (guardrailResult.exceeded) {
|
|
@@ -6412,76 +6877,549 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6412
6877
|
};
|
|
6413
6878
|
params.conversationMessages.push(message);
|
|
6414
6879
|
}
|
|
6880
|
+
/** At most this many decision calls run at once for one agent turn. */
|
|
6881
|
+
static { this.DECISION_CONCURRENCY = 8; }
|
|
6415
6882
|
/**
|
|
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
|
|
6883
|
+
* Runs `fn` over `items` with at most `limit` calls in flight, keeping results in input order.
|
|
6884
|
+
* One call's failure never rejects the others: a call that throws is settled with `onError`'s
|
|
6885
|
+
* result in its place.
|
|
6422
6886
|
*/
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6887
|
+
async runWithConcurrencyLimit(items, limit, fn, onError) {
|
|
6888
|
+
const results = new Array(items.length);
|
|
6889
|
+
let next = 0;
|
|
6890
|
+
const worker = async () => {
|
|
6891
|
+
while (next < items.length) {
|
|
6892
|
+
const index = next++;
|
|
6893
|
+
try {
|
|
6894
|
+
results[index] = await fn(items[index], index);
|
|
6895
|
+
}
|
|
6896
|
+
catch (error) {
|
|
6897
|
+
results[index] = onError(error, items[index], index);
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6427
6900
|
};
|
|
6901
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
6902
|
+
return results;
|
|
6428
6903
|
}
|
|
6429
6904
|
/**
|
|
6430
|
-
*
|
|
6905
|
+
* Answers the decision requests from one agent turn, each logged as an `AIAgentRunStep`
|
|
6906
|
+
* (StepType 'Decision'), with bounded concurrency. A failed request never fails the run: it
|
|
6907
|
+
* becomes that request's `success: false`, which the agent reads on its next turn.
|
|
6431
6908
|
*
|
|
6432
|
-
*
|
|
6433
|
-
*
|
|
6434
|
-
*
|
|
6435
|
-
*
|
|
6909
|
+
* Two limits bound the turn. At most `decisionsMaxRequests` requests run (default
|
|
6910
|
+
* {@link MAX_DECISION_REQUESTS_PER_TURN}); each request over that cap is not run and records no
|
|
6911
|
+
* step, and its result says why. And the requests make at most `decisionsMaxCallsPerTurn`
|
|
6912
|
+
* decision calls in total, counting every `forEachItemIn` item (default
|
|
6913
|
+
* {@link MAX_DECISION_CALLS_PER_TURN}). That budget is handed out in request order before any
|
|
6914
|
+
* call is made, so which calls run never depends on timing. A `forEachItemIn` request it cuts
|
|
6915
|
+
* short asks its first items and reports the rest in `skippedCount`. A request it leaves no calls
|
|
6916
|
+
* for is not run, records no step, and its result says why.
|
|
6436
6917
|
*
|
|
6437
|
-
* @
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6918
|
+
* @since 2.132.0
|
|
6919
|
+
*/
|
|
6920
|
+
async executeDecisionRequestsAsSteps(requests, finalPayload, agentTypePromptParams, params) {
|
|
6921
|
+
const cap = this.decisionRequestCap(agentTypePromptParams);
|
|
6922
|
+
const toRun = requests.slice(0, cap);
|
|
6923
|
+
const overCap = requests.slice(cap);
|
|
6924
|
+
if (overCap.length > 0) {
|
|
6925
|
+
this.logStatus(`[Decisions] ${requests.length} decision requests on one turn — answering the first ${cap}, skipping ${overCap.length} (per-turn cap)`, true, params);
|
|
6926
|
+
}
|
|
6927
|
+
const budget = this.decisionCallBudget(agentTypePromptParams);
|
|
6928
|
+
const allowances = this.allotDecisionCalls(toRun, finalPayload, agentTypePromptParams, budget, params);
|
|
6929
|
+
const results = await this.runWithConcurrencyLimit(toRun, BaseAgent.DECISION_CONCURRENCY, async (req, index) => allowances[index].Wanted > 0 && allowances[index].Allowed === 0
|
|
6930
|
+
? this.overBudgetDecisionResult(req, index, budget)
|
|
6931
|
+
: this.executeSingleDecisionRequestAsStep(req, finalPayload, agentTypePromptParams, params, allowances[index].Allowed), (error, req, index) => ({
|
|
6932
|
+
id: this.decisionRequestId(req, index),
|
|
6933
|
+
success: false,
|
|
6934
|
+
error: `The decision request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
6935
|
+
}));
|
|
6936
|
+
const skipped = overCap.map((req, i) => ({
|
|
6937
|
+
id: this.decisionRequestId(req, cap + i),
|
|
6938
|
+
success: false,
|
|
6939
|
+
error: `Not run: at most ${cap} decision requests are answered per turn. Ask again next turn if you still need it.`,
|
|
6940
|
+
}));
|
|
6941
|
+
return [...results, ...skipped];
|
|
6942
|
+
}
|
|
6943
|
+
/** The most decision requests one turn may run: `decisionsMaxRequests`, or {@link MAX_DECISION_REQUESTS_PER_TURN}. */
|
|
6944
|
+
decisionRequestCap(agentTypePromptParams) {
|
|
6945
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxRequests', MAX_DECISION_REQUESTS_PER_TURN);
|
|
6946
|
+
}
|
|
6947
|
+
/**
|
|
6948
|
+
* The most decision calls one turn's requests make in total, `forEachItemIn` items included:
|
|
6949
|
+
* `decisionsMaxCallsPerTurn`, or {@link MAX_DECISION_CALLS_PER_TURN}.
|
|
6950
|
+
*/
|
|
6951
|
+
decisionCallBudget(agentTypePromptParams) {
|
|
6952
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxCallsPerTurn', MAX_DECISION_CALLS_PER_TURN);
|
|
6953
|
+
}
|
|
6954
|
+
/** The most items one `forEachItemIn` request asks about: `decisionsMaxItems`, or its default. */
|
|
6955
|
+
decisionMaxItems(agentTypePromptParams) {
|
|
6956
|
+
return this.decisionLimit(agentTypePromptParams, 'decisionsMaxItems', DEFAULT_LOOP_AGENT_PROMPT_PARAMS.decisionsMaxItems);
|
|
6957
|
+
}
|
|
6958
|
+
/** A decision limit from the prompt params when it is a non-negative integer, or `fallback` when it is absent or anything else. */
|
|
6959
|
+
decisionLimit(agentTypePromptParams, name, fallback) {
|
|
6960
|
+
const configured = agentTypePromptParams?.[name];
|
|
6961
|
+
return typeof configured === 'number' && Number.isInteger(configured) && configured >= 0 ? configured : fallback;
|
|
6962
|
+
}
|
|
6963
|
+
/**
|
|
6964
|
+
* Hands out the turn's decision-call budget to its requests in order, before any of them runs.
|
|
6965
|
+
* Each request gets the calls it wants while the budget lasts. The first one the budget cannot
|
|
6966
|
+
* cover gets what is left, and the ones after it get none.
|
|
6967
|
+
*/
|
|
6968
|
+
allotDecisionCalls(requests, finalPayload, agentTypePromptParams, budget, params) {
|
|
6969
|
+
let remaining = budget;
|
|
6970
|
+
const allowances = requests.map((request) => {
|
|
6971
|
+
const wanted = this.decisionCallsWanted(request, finalPayload, agentTypePromptParams);
|
|
6972
|
+
const allowed = Math.min(wanted, remaining);
|
|
6973
|
+
remaining -= allowed;
|
|
6974
|
+
return { Wanted: wanted, Allowed: allowed };
|
|
6975
|
+
});
|
|
6976
|
+
const cut = allowances.reduce((sum, allowance) => sum + allowance.Wanted - allowance.Allowed, 0);
|
|
6977
|
+
if (cut > 0) {
|
|
6978
|
+
this.logStatus(`[Decisions] The turn's decision requests want ${budget + cut} decision calls — making ${budget}, skipping ${cut} (per-turn call budget)`, true, params);
|
|
6979
|
+
}
|
|
6980
|
+
return allowances;
|
|
6981
|
+
}
|
|
6982
|
+
/**
|
|
6983
|
+
* The decision calls a request makes with no budget: one, or one per `forEachItemIn` item up to
|
|
6984
|
+
* `decisionsMaxItems`. A request that cannot make any, because it is not an object or its
|
|
6985
|
+
* `forEachItemIn` is not an array, wants none. A request that fails before it asks, such as one
|
|
6986
|
+
* with no valid question, still holds what its shape wants: the budget can then go unused, but
|
|
6987
|
+
* it is never exceeded.
|
|
6988
|
+
*/
|
|
6989
|
+
decisionCallsWanted(request, finalPayload, agentTypePromptParams) {
|
|
6990
|
+
if (!request || typeof request !== 'object') {
|
|
6991
|
+
return 0;
|
|
6992
|
+
}
|
|
6993
|
+
if (!request.forEachItemIn) {
|
|
6994
|
+
return 1;
|
|
6995
|
+
}
|
|
6996
|
+
const target = typeof request.forEachItemIn === 'string' ? this.resolvePayloadPath(request.forEachItemIn, finalPayload) : undefined;
|
|
6997
|
+
return Array.isArray(target) ? Math.min(target.length, this.decisionMaxItems(agentTypePromptParams)) : 0;
|
|
6998
|
+
}
|
|
6999
|
+
/**
|
|
7000
|
+
* The result of a request the turn's decision-call budget left no calls for. It was not run.
|
|
7001
|
+
* A budget of 0 turns decision calls off for the agent, so then asking again on a later turn
|
|
7002
|
+
* cannot help, and the result does not invite it.
|
|
7003
|
+
*/
|
|
7004
|
+
overBudgetDecisionResult(request, index, budget) {
|
|
7005
|
+
const error = budget === 0
|
|
7006
|
+
? '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.'
|
|
7007
|
+
: `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.`;
|
|
7008
|
+
return { id: this.decisionRequestId(request, index), success: false, error };
|
|
7009
|
+
}
|
|
7010
|
+
/** A request's id for its result, or its position when the model sent no usable id. */
|
|
7011
|
+
decisionRequestId(request, index) {
|
|
7012
|
+
const id = request && typeof request === 'object' ? request.id : undefined;
|
|
7013
|
+
return typeof id === 'string' && id.length > 0 ? id : `request ${index + 1}`;
|
|
7014
|
+
}
|
|
7015
|
+
/**
|
|
7016
|
+
* Runs a `'Decision'` next step: an agent type's decision requests, answered with no LLM turn.
|
|
6441
7017
|
*
|
|
6442
|
-
*
|
|
7018
|
+
* Each request goes through the same path a loop turn's decisions take
|
|
7019
|
+
* ({@link executeSingleDecisionRequestAsStep}), so it gets the same prompt default, state
|
|
7020
|
+
* resolution and error handling, and is logged as a `Decision` run step. Where a loop turn's
|
|
7021
|
+
* results go into the conversation, these come back to the agent type on the returned `'Retry'`
|
|
7022
|
+
* (`decisionResults`), because it routes on them. So each answer keeps its whole distribution
|
|
7023
|
+
* (`SummarizeDecisionAnswers`), which a path condition may read. A failed request is a result
|
|
7024
|
+
* with `success: false`, never a failed run: what a failure means is the agent type's call.
|
|
6443
7025
|
*
|
|
6444
|
-
* @
|
|
7026
|
+
* @since 6.2.0
|
|
7027
|
+
*/
|
|
7028
|
+
async executeDecisionStep(params, decision) {
|
|
7029
|
+
const payload = decision.newPayload ?? decision.previousPayload;
|
|
7030
|
+
const promptParams = decision.decisionPromptName ? { decisionPromptName: decision.decisionPromptName } : undefined;
|
|
7031
|
+
const results = await this.runWithConcurrencyLimit(decision.decisions ?? [], BaseAgent.DECISION_CONCURRENCY, request => this.executeSingleDecisionRequestAsStep(request, payload, promptParams, params, Number.POSITIVE_INFINITY, SummarizeDecisionAnswers), (error, request, index) => ({
|
|
7032
|
+
id: this.decisionRequestId(request, index),
|
|
7033
|
+
success: false,
|
|
7034
|
+
error: `The decision request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
7035
|
+
}));
|
|
7036
|
+
return { step: 'Retry', terminate: false, decisionResults: results, previousPayload: payload, newPayload: payload };
|
|
7037
|
+
}
|
|
7038
|
+
/**
|
|
7039
|
+
* Answers one decision request, either once against its `state` or once per item of its
|
|
7040
|
+
* `forEachItemIn` array, and records it as a Decision step. The request is unchecked model
|
|
7041
|
+
* output, so any error finishes the step as failed rather than escaping: this never throws
|
|
7042
|
+
* once the step exists.
|
|
6445
7043
|
*
|
|
6446
|
-
* @
|
|
7044
|
+
* @param callAllowance The most decision calls the request may make, from the turn's
|
|
7045
|
+
* `decisionsMaxCallsPerTurn` budget. It caps a `forEachItemIn` request's items below
|
|
7046
|
+
* `decisionsMaxItems`. A single-state request makes one call, and
|
|
7047
|
+
* {@link executeDecisionRequestsAsSteps} never starts one the budget left without it.
|
|
7048
|
+
* Unbounded by default.
|
|
7049
|
+
* @param summarize how the typed answers are summarised for whoever reads them next; defaults
|
|
7050
|
+
* to the loop turn's model-facing summary
|
|
7051
|
+
* @since 2.132.0
|
|
6447
7052
|
*/
|
|
6448
|
-
async
|
|
7053
|
+
async executeSingleDecisionRequestAsStep(request, finalPayload, agentTypePromptParams, params, callAllowance = Number.POSITIVE_INFINITY, summarize = AgentDecisionService.SummarizeAnswers) {
|
|
7054
|
+
const step = await this.createStepEntity({
|
|
7055
|
+
stepType: 'Decision',
|
|
7056
|
+
stepName: `Decision: ${request.id}`,
|
|
7057
|
+
contextUser: params.contextUser,
|
|
7058
|
+
inputData: { id: request.id, state: request.state, forEachItemIn: request.forEachItemIn, questions: request.questions },
|
|
7059
|
+
});
|
|
6449
7060
|
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
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
7061
|
+
const mapping = request.questions && typeof request.questions === 'object'
|
|
7062
|
+
? AgentDecisionService.ToDecisionQuestions(request.questions)
|
|
7063
|
+
: { Questions: {}, Invalid: [] };
|
|
7064
|
+
if (Object.keys(mapping.Questions).length === 0) {
|
|
7065
|
+
const error = mapping.Invalid.length > 0
|
|
7066
|
+
? `No valid questions: ${mapping.Invalid.join('; ')}`
|
|
7067
|
+
: 'At least one question is required';
|
|
7068
|
+
return await this.finishDecisionStep(step, request, { Error: error });
|
|
7069
|
+
}
|
|
7070
|
+
if (mapping.Invalid.length > 0) {
|
|
7071
|
+
this.logStatus(`[Decisions] "${request.id}": dropped ${mapping.Invalid.length} invalid question(s): ${mapping.Invalid.join('; ')}`, true, params);
|
|
7072
|
+
}
|
|
7073
|
+
const ask = this.decisionAsker(mapping.Questions, agentTypePromptParams, params);
|
|
7074
|
+
const outcome = request.forEachItemIn
|
|
7075
|
+
? await this.askForEachItem(request, finalPayload, agentTypePromptParams, callAllowance, params, step, ask, summarize)
|
|
7076
|
+
: await this.askOnce(request, finalPayload, step, ask, summarize);
|
|
7077
|
+
return await this.finishDecisionStep(step, request, outcome, mapping.Invalid);
|
|
7078
|
+
}
|
|
7079
|
+
catch (error) {
|
|
7080
|
+
const message = `The decision request failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
7081
|
+
return this.finishDecisionStep(step, request, { Error: message });
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
/** Asks the request's questions about one state, with the configured decision prompt. */
|
|
7085
|
+
decisionAsker(questions, agentTypePromptParams, params) {
|
|
7086
|
+
const promptName = typeof agentTypePromptParams?.decisionPromptName === 'string'
|
|
7087
|
+
? agentTypePromptParams.decisionPromptName
|
|
7088
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
7089
|
+
return (state) => this._agentDecisionService.Ask({
|
|
7090
|
+
State: state,
|
|
7091
|
+
Questions: questions,
|
|
7092
|
+
ContextUser: params.contextUser,
|
|
7093
|
+
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
7094
|
+
PromptName: promptName,
|
|
7095
|
+
CancellationToken: params.cancellationToken,
|
|
7096
|
+
});
|
|
7097
|
+
}
|
|
7098
|
+
/** Asks the questions once, against the request's state. */
|
|
7099
|
+
async askOnce(request, finalPayload, step, ask, summarize) {
|
|
7100
|
+
const state = this.resolveDecisionState(request.state, finalPayload);
|
|
7101
|
+
if ('Error' in state) {
|
|
7102
|
+
return state;
|
|
7103
|
+
}
|
|
7104
|
+
const result = await ask(state.State);
|
|
7105
|
+
this.attachDecisionPromptRun(step, result);
|
|
7106
|
+
if (!result.success) {
|
|
7107
|
+
return { Error: result.errorMessage || 'Decision evaluation failed' };
|
|
7108
|
+
}
|
|
7109
|
+
return { Answers: summarize(result.Answers), Results: [result] };
|
|
7110
|
+
}
|
|
7111
|
+
/**
|
|
7112
|
+
* Asks the same questions of each item of a payload array, up to `decisionsMaxItems` and the
|
|
7113
|
+
* request's share of the turn's call budget. Each item's call is its own child Decision step of
|
|
7114
|
+
* the request's step, carrying that call's prompt run, as each iteration of a ForEach is a child
|
|
7115
|
+
* step of its loop step.
|
|
7116
|
+
*/
|
|
7117
|
+
async askForEachItem(request, finalPayload, agentTypePromptParams, callAllowance, params, step, ask, summarize) {
|
|
7118
|
+
const target = this.resolvePayloadPath(request.forEachItemIn ?? '', finalPayload);
|
|
7119
|
+
if (!Array.isArray(target)) {
|
|
7120
|
+
return { Error: `forEachItemIn target "${request.forEachItemIn}" is not an array` };
|
|
7121
|
+
}
|
|
7122
|
+
const maxItems = this.decisionMaxItems(agentTypePromptParams);
|
|
7123
|
+
const limit = Math.min(maxItems, callAllowance);
|
|
7124
|
+
const items = target.slice(0, limit);
|
|
7125
|
+
const skippedCount = target.length > limit ? target.length - limit : undefined;
|
|
7126
|
+
if (skippedCount) {
|
|
7127
|
+
const why = limit < maxItems ? "the turn's decision-call budget" : 'decisionsMaxItems';
|
|
7128
|
+
this.logStatus(`[Decisions] "${request.forEachItemIn}" has ${target.length} items; asking the first ${limit} (${skippedCount} skipped: ${why})`, true, params);
|
|
7129
|
+
}
|
|
7130
|
+
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: {} }));
|
|
7131
|
+
const failedIndex = results.findIndex(r => !r.success);
|
|
7132
|
+
if (failedIndex >= 0) {
|
|
7133
|
+
return { Error: `Item ${failedIndex}: ${results[failedIndex].errorMessage || 'decision evaluation failed'}` };
|
|
7134
|
+
}
|
|
7135
|
+
return { Answers: results.map(r => summarize(r.Answers)), Results: results, SkippedCount: skippedCount };
|
|
7136
|
+
}
|
|
7137
|
+
/** Asks about one item of a `forEachItemIn` request, recorded as a child step of the request's step. */
|
|
7138
|
+
async askItemAsStep(request, item, index, parentStep, ask, params) {
|
|
7139
|
+
const itemStep = await this.createStepEntity({
|
|
7140
|
+
stepType: 'Decision',
|
|
7141
|
+
stepName: `Decision: ${request.id} (item ${index})`,
|
|
7142
|
+
contextUser: params.contextUser,
|
|
7143
|
+
inputData: { id: request.id, item: index },
|
|
7144
|
+
parentId: parentStep.ID,
|
|
7145
|
+
});
|
|
7146
|
+
let result;
|
|
7147
|
+
try {
|
|
7148
|
+
result = await ask(this.decisionStateText(item));
|
|
7149
|
+
}
|
|
7150
|
+
catch (error) {
|
|
7151
|
+
result = { success: false, errorMessage: `The decision call failed: ${error instanceof Error ? error.message : String(error)}`, Answers: {} };
|
|
7152
|
+
}
|
|
7153
|
+
this.attachDecisionPromptRun(itemStep, result);
|
|
7154
|
+
await this.finalizeStepEntity(itemStep, result.success, result.success ? undefined : result.errorMessage, result.success ? { answers: AgentDecisionService.SummarizeAnswers(result.Answers) } : undefined);
|
|
7155
|
+
return result;
|
|
7156
|
+
}
|
|
7157
|
+
/**
|
|
7158
|
+
* Links a decision call's prompt run to its step, as the Prompt step links its own, so the run's
|
|
7159
|
+
* cost and token totals and its `MaxCostPerRun` / `MaxTokensPerRun` checks count the call.
|
|
7160
|
+
*/
|
|
7161
|
+
attachDecisionPromptRun(step, result) {
|
|
7162
|
+
if (result.promptRun?.ID) {
|
|
7163
|
+
step.TargetLogID = result.promptRun.ID;
|
|
7164
|
+
step.PromptRun = result.promptRun; // transient related object (not a persisted field)
|
|
7165
|
+
}
|
|
7166
|
+
}
|
|
7167
|
+
/**
|
|
7168
|
+
* Links the prompt run of a decision call the run stopped waiting for (a timeout or a cancellation)
|
|
7169
|
+
* to its step, once the call settles. `AIDecisionRunner` writes the prompt run before it calls the
|
|
7170
|
+
* model, so a stopped call still has one. The link keeps that row on the run's trace, and counts its
|
|
7171
|
+
* usage in the run's cost and token totals when it settles before the run ends, as
|
|
7172
|
+
* {@link attachDecisionPromptRun} does on time. The step may already be finalized, so the link is
|
|
7173
|
+
* saved on its own. Never throws, and a call that never settles links nothing.
|
|
7174
|
+
*/
|
|
7175
|
+
linkLateDecisionPromptRun(step, late) {
|
|
7176
|
+
if (!step) {
|
|
7177
|
+
return;
|
|
7178
|
+
}
|
|
7179
|
+
late.then(result => {
|
|
7180
|
+
const promptRunID = result?.promptRun?.ID;
|
|
7181
|
+
if (!result || !promptRunID || step.TargetLogID === promptRunID) {
|
|
7182
|
+
return;
|
|
7183
|
+
}
|
|
7184
|
+
this.attachDecisionPromptRun(step, result);
|
|
7185
|
+
this.queueStepSave(step, (s) => { s.TargetLogID = promptRunID; });
|
|
7186
|
+
}).catch(error => {
|
|
7187
|
+
LogError(`Could not link a stopped decision call's prompt run to its step: ${error instanceof Error ? error.message : String(error)}`);
|
|
7188
|
+
});
|
|
7189
|
+
}
|
|
7190
|
+
/**
|
|
7191
|
+
* A literal state, or a value read from the payload: the whole payload for `payload`, one value
|
|
7192
|
+
* for a `payload.` path.
|
|
7193
|
+
*/
|
|
7194
|
+
resolveDecisionState(state, finalPayload) {
|
|
7195
|
+
if (typeof state !== 'string') {
|
|
7196
|
+
return { Error: 'Either state or forEachItemIn is required' };
|
|
7197
|
+
}
|
|
7198
|
+
if (state.trim() !== 'payload' && !state.startsWith('payload.')) {
|
|
7199
|
+
return { State: state };
|
|
7200
|
+
}
|
|
7201
|
+
const value = this.resolvePayloadPath(state, finalPayload);
|
|
7202
|
+
return value === undefined ? { Error: `Path "${state}" not found in payload` } : { State: this.decisionStateText(value) };
|
|
7203
|
+
}
|
|
7204
|
+
/** Reads a `payload.`-prefixed dotted path (or `payload` itself) from the payload. */
|
|
7205
|
+
resolvePayloadPath(path, finalPayload) {
|
|
7206
|
+
const trimmed = path.trim();
|
|
7207
|
+
if (trimmed === 'payload') {
|
|
7208
|
+
return finalPayload;
|
|
7209
|
+
}
|
|
7210
|
+
return _.get(finalPayload, trimmed.startsWith('payload.') ? trimmed.substring('payload.'.length) : trimmed);
|
|
7211
|
+
}
|
|
7212
|
+
/** Strings pass through; anything else becomes indented JSON. */
|
|
7213
|
+
decisionStateText(value) {
|
|
7214
|
+
return typeof value === 'string' ? value : (value !== null && typeof value === 'object' ? JSON.stringify(value, undefined, 1) : String(value ?? ''));
|
|
7215
|
+
}
|
|
7216
|
+
/** Finalizes the Decision step and builds the result the agent reads next turn. */
|
|
7217
|
+
async finishDecisionStep(step, request, outcome, droppedQuestions = []) {
|
|
7218
|
+
if ('Error' in outcome) {
|
|
7219
|
+
await this.finalizeStepEntity(step, false, outcome.Error, { error: outcome.Error });
|
|
7220
|
+
return { id: request.id, success: false, error: outcome.Error };
|
|
7221
|
+
}
|
|
7222
|
+
const first = outcome.Results[0];
|
|
7223
|
+
await this.finalizeStepEntity(step, true, undefined, {
|
|
7224
|
+
answers: outcome.Answers,
|
|
7225
|
+
skippedCount: outcome.SkippedCount,
|
|
7226
|
+
executionTimeMS: outcome.Results.reduce((sum, r) => sum + (r.executionTimeMS ?? 0), 0),
|
|
7227
|
+
tokensUsed: outcome.Results.reduce((sum, r) => sum + (r.tokensUsed ?? 0), 0),
|
|
7228
|
+
model: first?.DecisionResult?.ResolvedModel ?? first?.modelInfo?.modelName,
|
|
7229
|
+
promptRunId: first?.promptRun?.ID,
|
|
7230
|
+
driverClass: first?.DriverClass,
|
|
7231
|
+
...(droppedQuestions.length > 0 && { droppedQuestions }),
|
|
7232
|
+
});
|
|
7233
|
+
return { id: request.id, success: true, answers: outcome.Answers, skippedCount: outcome.SkippedCount };
|
|
7234
|
+
}
|
|
7235
|
+
/**
|
|
7236
|
+
* Answers the decision requests a prompt returned, or holds them when the step that carries
|
|
7237
|
+
* them can end the run, because then no turn would read their answers. Held requests run before
|
|
7238
|
+
* the next prompt ({@link runHeldDecisions}), or are skipped if the run ends first
|
|
7239
|
+
* ({@link skipHeldDecisions}). Skips them all, unasked, while the master switch is off
|
|
7240
|
+
* ({@link DecisionsEnabled}) or the agent's `decisions` response field is.
|
|
7241
|
+
*/
|
|
7242
|
+
async processTurnDecisions(decisions, nextStep, finalPayload, agentTypePromptParams, params) {
|
|
7243
|
+
if (!this.DecisionsEnabled(agentTypePromptParams)) {
|
|
7244
|
+
this.logStatus(`[Decisions] LLM requested decisions but decision-model use is off for this agent (decisionsEnabled is not true) — skipped`, true, params);
|
|
7245
|
+
return;
|
|
7246
|
+
}
|
|
7247
|
+
const responseTypeRules = agentTypePromptParams?.includeResponseTypeDefinition;
|
|
7248
|
+
if (responseTypeRules?.decisions === false) {
|
|
7249
|
+
this.logStatus(`[Decisions] LLM requested decisions but feature is disabled (includeResponseTypeDefinition.decisions=false) — skipped`, true, params);
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
if (!Array.isArray(decisions)) {
|
|
7253
|
+
this.injectDecisionResultsMessage(params, [{ id: 'decisions', success: false, error: '"decisions" must be an array of decision requests' }]);
|
|
7254
|
+
return;
|
|
7255
|
+
}
|
|
7256
|
+
const ids = decisions.map((d, i) => this.decisionRequestId(d, i)).join(', ');
|
|
7257
|
+
const holdReason = this.decisionsHoldReason(nextStep);
|
|
7258
|
+
if (holdReason) {
|
|
7259
|
+
this._heldDecisions = { Requests: decisions, Payload: finalPayload, PromptParams: agentTypePromptParams, Reason: holdReason };
|
|
7260
|
+
this.logStatus(`[Decisions] Holding ${decisions.length} decision request(s) (${ids}) until the step has run: ${holdReason}`, true, params);
|
|
7261
|
+
return;
|
|
7262
|
+
}
|
|
7263
|
+
this.logStatus(`[Decisions] LLM requested ${decisions.length} decision request(s): ${ids}`, true, params);
|
|
7264
|
+
const decisionResults = await this.executeDecisionRequestsAsSteps(decisions, finalPayload, agentTypePromptParams, params);
|
|
7265
|
+
this.injectDecisionResultsMessage(params, decisionResults);
|
|
7266
|
+
}
|
|
7267
|
+
/**
|
|
7268
|
+
* Why the decisions sent with this step would be paid for and never read, or `undefined` when
|
|
7269
|
+
* the next turn reads them. Three ways a step's own outcome ends the run: a passing finishIf gate
|
|
7270
|
+
* (only an Actions step or a single sub-agent is gated), a sub-agent's `terminateAfter`, and
|
|
7271
|
+
* client tools sent with `taskComplete`, which end the run once the tools return. Every other
|
|
7272
|
+
* step, including client tools without `taskComplete`, goes on to a prompt that reads them.
|
|
7273
|
+
*/
|
|
7274
|
+
decisionsHoldReason(step) {
|
|
7275
|
+
const isSingleSubAgent = step.step === 'Sub-Agent' && !!step.subAgent && !step.subAgents?.length;
|
|
7276
|
+
if (step.finishIf && (step.step === 'Actions' || isSingleSubAgent) && this.finishIfActs()) {
|
|
7277
|
+
return `the ${step.step} step carries a finishIf gate, and a passing gate ends the run`;
|
|
7278
|
+
}
|
|
7279
|
+
if (step.step === 'Sub-Agent' && this.getRequestedSubAgents(step).some(r => r.terminateAfter === true)) {
|
|
7280
|
+
return 'the Sub-Agent step has terminateAfter, which ends the run after it';
|
|
7281
|
+
}
|
|
7282
|
+
if (step.step === 'ClientTools' && step.terminate === true) {
|
|
7283
|
+
return 'the client tools were sent with taskComplete, which ends the run once they return';
|
|
7284
|
+
}
|
|
7285
|
+
return undefined;
|
|
7286
|
+
}
|
|
7287
|
+
/**
|
|
7288
|
+
* Runs the decision requests a prompt held back, now that the step that carried them has run
|
|
7289
|
+
* without ending the run, so the prompt about to run reads their answers.
|
|
7290
|
+
*/
|
|
7291
|
+
async runHeldDecisions(params) {
|
|
7292
|
+
const held = this._heldDecisions;
|
|
7293
|
+
if (!held) {
|
|
7294
|
+
return;
|
|
7295
|
+
}
|
|
7296
|
+
this._heldDecisions = undefined;
|
|
7297
|
+
this.logStatus(`[Decisions] The run continues, so answering ${held.Requests.length} held decision request(s)`, true, params);
|
|
7298
|
+
const results = await this.executeDecisionRequestsAsSteps(held.Requests, held.Payload, held.PromptParams, params);
|
|
7299
|
+
this.injectDecisionResultsMessage(params, results);
|
|
7300
|
+
}
|
|
7301
|
+
/**
|
|
7302
|
+
* Skips the decision requests a prompt held back, because the run ended before they could be
|
|
7303
|
+
* asked: the step that carried them ended it, or it was cancelled, or an error was thrown out of
|
|
7304
|
+
* the step loop. Called from the loop's terminate branch and from `Execute()`'s `finally`, so
|
|
7305
|
+
* every way out logs them. The held state is cleared before the log line, so the second call on
|
|
7306
|
+
* the terminate path logs nothing and each held request is logged exactly once.
|
|
7307
|
+
*
|
|
7308
|
+
* @param runEnd How the run ended, as a clause for the log line.
|
|
7309
|
+
*/
|
|
7310
|
+
skipHeldDecisions(params, runEnd) {
|
|
7311
|
+
const held = this._heldDecisions;
|
|
7312
|
+
if (!held) {
|
|
7313
|
+
return;
|
|
7314
|
+
}
|
|
7315
|
+
this._heldDecisions = undefined;
|
|
7316
|
+
const ids = held.Requests.map((d, i) => this.decisionRequestId(d, i)).join(', ');
|
|
7317
|
+
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);
|
|
7318
|
+
}
|
|
7319
|
+
/** How the run ended, as a clause for held decision requests that a cancellation or an error left unasked. */
|
|
7320
|
+
runEndForHeldDecisions() {
|
|
7321
|
+
switch (this.deriveRunOutcome()) {
|
|
7322
|
+
case 'cancelled':
|
|
7323
|
+
return 'the run was cancelled before they were asked';
|
|
7324
|
+
case 'failure':
|
|
7325
|
+
return 'the run failed before they were asked';
|
|
7326
|
+
default:
|
|
7327
|
+
return 'the run ended before they were asked';
|
|
7328
|
+
}
|
|
7329
|
+
}
|
|
7330
|
+
/**
|
|
7331
|
+
* Injects decision results into the conversation as one tool-result message for the next turn.
|
|
7332
|
+
*
|
|
7333
|
+
* @since 2.132.0
|
|
7334
|
+
*/
|
|
7335
|
+
injectDecisionResultsMessage(params, results) {
|
|
7336
|
+
if (!results || results.length === 0)
|
|
7337
|
+
return;
|
|
7338
|
+
const message = {
|
|
7339
|
+
role: 'user',
|
|
7340
|
+
content: `Decision results:\n${JSON.stringify(results)}`,
|
|
7341
|
+
metadata: {
|
|
7342
|
+
turnAdded: this._promptTurnCount,
|
|
7343
|
+
messageType: 'tool-result',
|
|
7344
|
+
expirationTurns: 3,
|
|
7345
|
+
expirationMode: 'Compact',
|
|
7346
|
+
compactMode: 'First N Chars',
|
|
7347
|
+
compactLength: 500,
|
|
7348
|
+
compactPromptId: '',
|
|
7349
|
+
},
|
|
7350
|
+
};
|
|
7351
|
+
params.conversationMessages.push(message);
|
|
7352
|
+
}
|
|
7353
|
+
/**
|
|
7354
|
+
* Creates a chat message containing sub-agent execution results.
|
|
7355
|
+
*
|
|
7356
|
+
* @param {AgentSubAgentRequest} subAgent - The sub-agent that was executed
|
|
7357
|
+
* @param {any} result - The result from sub-agent execution
|
|
7358
|
+
* @returns {ChatMessage} A formatted message with sub-agent results
|
|
7359
|
+
* @protected
|
|
7360
|
+
*/
|
|
7361
|
+
createSubAgentResultMessage(subAgent, result) {
|
|
7362
|
+
return {
|
|
7363
|
+
role: 'user',
|
|
7364
|
+
content: this.formatSubAgentResultAsMarkdown(subAgent.name, result)
|
|
7365
|
+
};
|
|
7366
|
+
}
|
|
7367
|
+
/**
|
|
7368
|
+
* Gathers context data about the agent for use in prompt templates.
|
|
7369
|
+
*
|
|
7370
|
+
* This method collects information about the agent's sub-agents and available
|
|
7371
|
+
* actions, formatting them for injection into prompt templates. The data is
|
|
7372
|
+
* structured to provide the LLM with comprehensive context about the agent's
|
|
7373
|
+
* capabilities and hierarchical relationships.
|
|
7374
|
+
*
|
|
7375
|
+
* @param {MJAIAgentEntityExtended} agent - The agent to gather context for
|
|
7376
|
+
* @param {UserInfo} [_contextUser] - Optional user context (reserved for future use)
|
|
7377
|
+
* @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.
|
|
7378
|
+
* @param {ActionChange[]} [actionChanges] - Optional runtime action modifications
|
|
7379
|
+
*
|
|
7380
|
+
* @returns {Promise<AgentContextData>} Structured context data for prompts
|
|
7381
|
+
*
|
|
7382
|
+
* @throws {Error} If there's an error accessing agent data
|
|
7383
|
+
*
|
|
7384
|
+
* @private
|
|
7385
|
+
*/
|
|
7386
|
+
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges, subAgentChanges) {
|
|
7387
|
+
try {
|
|
7388
|
+
const engine = AIEngine.Instance;
|
|
7389
|
+
// Build (or reuse) the agent-invariant base catalog. This is process-wide cached on
|
|
7390
|
+
// AIEngine and wiped on Agent/AgentAction/AgentRelationship/AgentType changes + reloads.
|
|
7391
|
+
// It turns the per-step rebuild (sub-agent + action resolution, markdown, JSON.parse of
|
|
7392
|
+
// agent-type params) into a once-per-agent cost; the common no-override step reuses it wholesale.
|
|
7393
|
+
let catalog = engine.GetAgentBaseCatalog(agent.ID);
|
|
7394
|
+
if (!catalog) {
|
|
7395
|
+
catalog = this.buildAgentBaseCatalog(agent, engine);
|
|
7396
|
+
engine.SetAgentBaseCatalog(agent.ID, catalog);
|
|
7397
|
+
}
|
|
7398
|
+
const isRoot = this._depth === 0;
|
|
7399
|
+
// Sub-agents: reuse cached base unless runtime subAgentChanges apply (then clone + re-format).
|
|
7400
|
+
let uniqueActiveSubAgents = catalog.uniqueActiveSubAgents;
|
|
7401
|
+
let subAgentDetails = catalog.subAgentDetails;
|
|
7402
|
+
let subAgentCount = catalog.subAgentCount;
|
|
7403
|
+
if (subAgentChanges?.length) {
|
|
7404
|
+
uniqueActiveSubAgents = this.applySubAgentChanges(catalog.uniqueActiveSubAgents, subAgentChanges, agent.ID, isRoot, engine);
|
|
7405
|
+
subAgentCount = uniqueActiveSubAgents.length;
|
|
7406
|
+
subAgentDetails = this.formatSubAgentDetails(uniqueActiveSubAgents);
|
|
7407
|
+
}
|
|
7408
|
+
// Actions: reuse cached active set unless runtime actionChanges apply (then clone + re-format).
|
|
7409
|
+
//
|
|
7410
|
+
// FAST-PATH SHARING CONTRACT: on the no-override path, `activeActions` (and therefore
|
|
7411
|
+
// `_effectiveActions`) and `uniqueActiveSubAgents` above are the SAME array references
|
|
7412
|
+
// held by the process-wide AIEngine catalog cache. Downstream consumers MUST treat them
|
|
7413
|
+
// as read-only — they are only ever read (`.find`/`.map`/`.length`/`.some`), never mutated
|
|
7414
|
+
// in place. On the override path a fresh array is built via filter/applyActionChanges, so
|
|
7415
|
+
// the cached arrays are never the mutated ones. Keeping the references (vs. copying) avoids
|
|
7416
|
+
// a per-step allocation; if a future consumer needs to mutate, it must `.slice()` first.
|
|
7417
|
+
let activeActions = catalog.activeActions;
|
|
7418
|
+
let actionDetails = catalog.actionDetails;
|
|
7419
|
+
if (actionChanges?.length) {
|
|
7420
|
+
const result = this.applyActionChanges([...catalog.baseActionsRaw], actionChanges, agent.ID, isRoot);
|
|
7421
|
+
activeActions = result.actions.filter(a => a.Status === 'Active');
|
|
7422
|
+
this._dynamicActionLimits = result.dynamicLimits;
|
|
6485
7423
|
actionDetails = this.formatActionDetails(activeActions);
|
|
6486
7424
|
}
|
|
6487
7425
|
else {
|
|
@@ -6510,10 +7448,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6510
7448
|
// and removes any cache-poisoning foot-gun should a future consumer write to it.
|
|
6511
7449
|
agentTypePromptParams = { ...catalog.baseAgentTypePromptParams };
|
|
6512
7450
|
}
|
|
6513
|
-
//
|
|
6514
|
-
|
|
6515
|
-
// Build app context section if provided in extraData
|
|
6516
|
-
const appContext = this.buildAppContextSection(extraData);
|
|
7451
|
+
// Store for the finishIf gate, which runs after this prompt in executeActionsStep / executeNextStep
|
|
7452
|
+
this._agentTypePromptParams = agentTypePromptParams;
|
|
6517
7453
|
// Skill catalog (name + description only — progressive disclosure). This is the
|
|
6518
7454
|
// SELF-ACTIVATION surface, so it uses the double-gated auto set: empty unless the
|
|
6519
7455
|
// agent's SkillActivationMode is 'Auto', and containing only skills whose own
|
|
@@ -6523,7 +7459,20 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6523
7459
|
// is never even offered a skill the user isn't entitled to — the permission
|
|
6524
7460
|
// boundary is enforced at the catalog, not just at activation.
|
|
6525
7461
|
const availableSkills = await this.availableSkills(engine.GetAutoActivatableSkillsForAgent(agent, _contextUser), 'catalog', agent, _contextUser);
|
|
6526
|
-
|
|
7462
|
+
// Catalog narrowing (plan Task 3.7), off unless decisionsEnabled is true and maxActionsInPrompt /
|
|
7463
|
+
// maxSubAgentsInPrompt is positive and the list is longer. It narrows what the prompt SHOWS, once per run:
|
|
7464
|
+
// _effectiveActions / _effectiveSubAgents above stay whole, so validation forbids nothing.
|
|
7465
|
+
// Skills are narrowed here, after the filterAvailableSkills policy, never inside it: the
|
|
7466
|
+
// decision judges only skills the policy offers, and an override that skips `super` keeps it.
|
|
7467
|
+
// The counts stay whole: they are what the model can call, and a narrowed list says what it hides.
|
|
7468
|
+
await this.ensureCatalogNarrowing(agent, _contextUser, agentTypePromptParams, activeActions, uniqueActiveSubAgents, availableSkills);
|
|
7469
|
+
actionDetails = this.narrowedCatalogSection('action', activeActions, actionDetails, shown => this.formatActionDetails(shown));
|
|
7470
|
+
subAgentDetails = this.narrowedCatalogSection('agent', uniqueActiveSubAgents, subAgentDetails, shown => this.formatSubAgentDetails(shown));
|
|
7471
|
+
const skillsCatalog = this.narrowedCatalogSection('skill', availableSkills, this.formatSkillsCatalog(availableSkills), shown => this.formatSkillsCatalog(shown));
|
|
7472
|
+
// Build client tool details for the prompt (per-run; depends on extraData)
|
|
7473
|
+
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
7474
|
+
// Build app context section if provided in extraData
|
|
7475
|
+
const appContext = this.buildAppContextSection(extraData);
|
|
6527
7476
|
const contextData = {
|
|
6528
7477
|
agentName: agent.Name,
|
|
6529
7478
|
agentDescription: agent.Description,
|
|
@@ -6567,6 +7516,230 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6567
7516
|
throw new Error(`Error gathering context data: ${error.message}`);
|
|
6568
7517
|
}
|
|
6569
7518
|
}
|
|
7519
|
+
/**
|
|
7520
|
+
* Narrows the catalog once per run, on its first prompt (plan Task 3.7), and caches the result for
|
|
7521
|
+
* every later step. With narrowing off, or the master switch ({@link DecisionsEnabled}) off, it
|
|
7522
|
+
* asks nothing and records nothing, so the prompt describes the whole catalog. Never throws: any
|
|
7523
|
+
* failure leaves the full catalog in place. It narrows the prose catalog only:
|
|
7524
|
+
* {@link applyNativeTools} still declares every action and sub-agent.
|
|
7525
|
+
*/
|
|
7526
|
+
async ensureCatalogNarrowing(agent, contextUser, promptParams, actions, subAgents, skills) {
|
|
7527
|
+
if (this._catalogNarrowing) {
|
|
7528
|
+
return;
|
|
7529
|
+
}
|
|
7530
|
+
// Set first, so a failure below is cached as "hide nothing" rather than retried every step.
|
|
7531
|
+
this._catalogNarrowing = NoCatalogNarrowing();
|
|
7532
|
+
if (!this.DecisionsEnabled(promptParams)) {
|
|
7533
|
+
return;
|
|
7534
|
+
}
|
|
7535
|
+
const limits = this.reachableCatalogNarrowingLimits(agent, promptParams, actions);
|
|
7536
|
+
if (limits.Actions === 0 && limits.SubAgents === 0 && limits.Skills === 0) {
|
|
7537
|
+
return;
|
|
7538
|
+
}
|
|
7539
|
+
try {
|
|
7540
|
+
const candidates = this.catalogNarrowingCandidates(agent, actions, subAgents, skills);
|
|
7541
|
+
const asked = CatalogNarrowingCandidatesToAsk(candidates, limits);
|
|
7542
|
+
if (asked.length > 0) {
|
|
7543
|
+
this._catalogNarrowing = await this.narrowCatalog(agent, contextUser, promptParams, candidates, asked, limits);
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
catch (error) {
|
|
7547
|
+
this.warnCatalogNarrowing(agent, error instanceof Error ? error.message : String(error));
|
|
7548
|
+
}
|
|
7549
|
+
}
|
|
7550
|
+
/**
|
|
7551
|
+
* The run's limits, with actions left whole when the agent lacks Find Candidate Actions: a hidden
|
|
7552
|
+
* action would then be unreachable. That case is logged as a warning when it keeps a long action
|
|
7553
|
+
* list whole, since the agent asked for narrowing and did not get it.
|
|
7554
|
+
*/
|
|
7555
|
+
reachableCatalogNarrowingLimits(agent, promptParams, actions) {
|
|
7556
|
+
const configured = ResolveCatalogNarrowingLimits(promptParams);
|
|
7557
|
+
const limits = ReachableCatalogNarrowingLimits(configured, actions.map(a => a.Name));
|
|
7558
|
+
if (limits.Actions !== configured.Actions && IsCatalogListNarrowed(configured.Actions, actions.length)) {
|
|
7559
|
+
LogErrorEx({
|
|
7560
|
+
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`,
|
|
7561
|
+
severity: 'warning',
|
|
7562
|
+
category: 'CatalogNarrowing',
|
|
7563
|
+
});
|
|
7564
|
+
}
|
|
7565
|
+
return limits;
|
|
7566
|
+
}
|
|
7567
|
+
/**
|
|
7568
|
+
* Every item of the three lists as a narrowing candidate. An action or sub-agent with
|
|
7569
|
+
* `MinExecutionsPerRun` set, and the Find Candidate tools, are pinned: always shown.
|
|
7570
|
+
*/
|
|
7571
|
+
catalogNarrowingCandidates(agent, actions, subAgents, skills) {
|
|
7572
|
+
const requiredActionIDs = AIEngine.Instance.AgentActions
|
|
7573
|
+
.filter(aa => UUIDsEqual(aa.AgentID, agent.ID) && aa.Status === 'Active' && (aa.MinExecutionsPerRun ?? 0) > 0)
|
|
7574
|
+
.map(aa => aa.ActionID);
|
|
7575
|
+
const candidate = (kind, item, pinned) => ({ Kind: kind, ID: item.ID, Name: item.Name, Description: item.Description ?? '', Pinned: pinned });
|
|
7576
|
+
return [
|
|
7577
|
+
...actions.map(a => candidate('action', a, IsAlwaysShownAction(a.Name) || requiredActionIDs.some(id => UUIDsEqual(id, a.ID)))),
|
|
7578
|
+
...subAgents.filter(s => !!s).map(s => candidate('agent', s, (s.MinExecutionsPerRun ?? 0) > 0)),
|
|
7579
|
+
...skills.map(s => candidate('skill', s, false)),
|
|
7580
|
+
];
|
|
7581
|
+
}
|
|
7582
|
+
/**
|
|
7583
|
+
* Asks the one decision, records it as the `Catalog narrowing` step, and returns what to hide.
|
|
7584
|
+
* Any failure is logged as a warning and hides nothing, so the prompt shows the full catalog.
|
|
7585
|
+
*/
|
|
7586
|
+
async narrowCatalog(agent, contextUser, promptParams, candidates, asked, limits) {
|
|
7587
|
+
if (!this._openingRequest || !contextUser) {
|
|
7588
|
+
this.warnCatalogNarrowing(agent, contextUser
|
|
7589
|
+
? 'the run has no user message to judge the catalog against'
|
|
7590
|
+
: 'the run has no context user');
|
|
7591
|
+
return NoCatalogNarrowing();
|
|
7592
|
+
}
|
|
7593
|
+
const promptName = typeof promptParams.decisionPromptName === 'string'
|
|
7594
|
+
? promptParams.decisionPromptName
|
|
7595
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME;
|
|
7596
|
+
const step = await this.startCatalogNarrowingStep(contextUser, promptName, asked, limits);
|
|
7597
|
+
const result = await this.askCatalogNarrowing(agent, contextUser, BuildCatalogNarrowingQuestions(asked), promptName, step);
|
|
7598
|
+
const outcome = result.success ? SelectCatalogNarrowing(candidates, asked, result.Answers, limits) : undefined;
|
|
7599
|
+
if (!outcome) {
|
|
7600
|
+
const reason = result.success ? 'an answer was missing or was not a probability' : (result.errorMessage || 'the decision call failed');
|
|
7601
|
+
this.warnCatalogNarrowing(agent, reason);
|
|
7602
|
+
await this.finishCatalogNarrowingStep(step, result, undefined, reason);
|
|
7603
|
+
return NoCatalogNarrowing();
|
|
7604
|
+
}
|
|
7605
|
+
await this.finishCatalogNarrowingStep(step, result, outcome);
|
|
7606
|
+
return outcome;
|
|
7607
|
+
}
|
|
7608
|
+
/**
|
|
7609
|
+
* The one decision call, against the opening request. Never throws, and never waits longer than
|
|
7610
|
+
* {@link CATALOG_NARROWING_TIMEOUT_MS} or past the run's cancellation: a throw, a timeout and a
|
|
7611
|
+
* cancelled run each come back as a failed result. Stopping also aborts the call, and links the
|
|
7612
|
+
* prompt run it still returns to `step` once it does ({@link linkLateDecisionPromptRun}).
|
|
7613
|
+
*/
|
|
7614
|
+
async askCatalogNarrowing(agent, contextUser, questions, promptName, step) {
|
|
7615
|
+
const failed = (errorMessage) => ({ success: false, errorMessage, Answers: {} });
|
|
7616
|
+
const controller = new AbortController();
|
|
7617
|
+
let stoppedResult;
|
|
7618
|
+
const stopped = new Promise(resolve => controller.signal.addEventListener('abort', () => {
|
|
7619
|
+
stoppedResult = failed(String(controller.signal.reason));
|
|
7620
|
+
resolve(stoppedResult);
|
|
7621
|
+
}, { once: true }));
|
|
7622
|
+
const runToken = this._executeParams?.cancellationToken;
|
|
7623
|
+
const relayRunAbort = () => controller.abort('the run was cancelled');
|
|
7624
|
+
if (runToken?.aborted) {
|
|
7625
|
+
relayRunAbort();
|
|
7626
|
+
}
|
|
7627
|
+
else {
|
|
7628
|
+
runToken?.addEventListener('abort', relayRunAbort, { once: true });
|
|
7629
|
+
}
|
|
7630
|
+
const timeoutMS = BaseAgent.CATALOG_NARROWING_TIMEOUT_MS;
|
|
7631
|
+
const timer = setTimeout(() => controller.abort(`the decision call timed out after ${timeoutMS}ms`), timeoutMS);
|
|
7632
|
+
try {
|
|
7633
|
+
const ask = this._agentDecisionService.Ask({
|
|
7634
|
+
State: this._openingRequest,
|
|
7635
|
+
Questions: questions,
|
|
7636
|
+
ContextUser: contextUser,
|
|
7637
|
+
AgentID: agent.ID,
|
|
7638
|
+
PromptName: promptName,
|
|
7639
|
+
CancellationToken: controller.signal,
|
|
7640
|
+
});
|
|
7641
|
+
const result = await Promise.race([ask, stopped]);
|
|
7642
|
+
if (result === stoppedResult) {
|
|
7643
|
+
this.linkLateDecisionPromptRun(step, ask);
|
|
7644
|
+
}
|
|
7645
|
+
return result;
|
|
7646
|
+
}
|
|
7647
|
+
catch (error) {
|
|
7648
|
+
return failed(error instanceof Error ? error.message : String(error));
|
|
7649
|
+
}
|
|
7650
|
+
finally {
|
|
7651
|
+
clearTimeout(timer);
|
|
7652
|
+
runToken?.removeEventListener('abort', relayRunAbort);
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
/** Opens the `Catalog narrowing` Decision step with the decision's inputs. Without a run there is no step. */
|
|
7656
|
+
async startCatalogNarrowingStep(contextUser, promptName, asked, limits) {
|
|
7657
|
+
if (!this._agentRun) {
|
|
7658
|
+
return undefined;
|
|
7659
|
+
}
|
|
7660
|
+
const count = (kind) => asked.filter(c => c.Kind === kind).length;
|
|
7661
|
+
try {
|
|
7662
|
+
return await this.createStepEntity({
|
|
7663
|
+
stepType: 'Decision',
|
|
7664
|
+
stepName: 'Catalog narrowing',
|
|
7665
|
+
contextUser,
|
|
7666
|
+
inputData: {
|
|
7667
|
+
request: this._openingRequest,
|
|
7668
|
+
promptName,
|
|
7669
|
+
limits: { actions: limits.Actions, subAgents: limits.SubAgents, skills: limits.Skills },
|
|
7670
|
+
asked: { actions: count('action'), subAgents: count('agent'), skills: count('skill') },
|
|
7671
|
+
},
|
|
7672
|
+
});
|
|
7673
|
+
}
|
|
7674
|
+
catch (error) {
|
|
7675
|
+
LogError(`Could not record the catalog narrowing step: ${error instanceof Error ? error.message : String(error)}`);
|
|
7676
|
+
return undefined;
|
|
7677
|
+
}
|
|
7678
|
+
}
|
|
7679
|
+
/** Finalizes the `Catalog narrowing` step: each narrowed list's counts, kept names and probabilities, or why it failed open. */
|
|
7680
|
+
async finishCatalogNarrowingStep(step, result, outcome, reason) {
|
|
7681
|
+
if (!step) {
|
|
7682
|
+
return;
|
|
7683
|
+
}
|
|
7684
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
7685
|
+
this.attachDecisionPromptRun(step, result);
|
|
7686
|
+
const usage = {
|
|
7687
|
+
executionTimeMS: result.executionTimeMS,
|
|
7688
|
+
tokensUsed: result.tokensUsed,
|
|
7689
|
+
model: result.DecisionResult?.ResolvedModel ?? result.modelInfo?.modelName,
|
|
7690
|
+
promptRunId: result.promptRun?.ID,
|
|
7691
|
+
};
|
|
7692
|
+
if (!outcome) {
|
|
7693
|
+
await this.finalizeStepEntity(step, false, reason, { failedOpen: true, reason, ...usage });
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
const lists = {};
|
|
7697
|
+
const stepKeys = { action: 'actions', agent: 'subAgents', skill: 'skills' };
|
|
7698
|
+
for (const kind of ['action', 'agent', 'skill']) {
|
|
7699
|
+
const list = outcome.Lists[kind];
|
|
7700
|
+
if (list) {
|
|
7701
|
+
lists[stepKeys[kind]] = { total: list.Total, limit: list.Limit, shown: list.Kept.length, kept: list.Kept, pinned: list.Pinned, probabilities: list.Probabilities };
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
await this.finalizeStepEntity(step, true, undefined, { ...lists, ...usage });
|
|
7705
|
+
}
|
|
7706
|
+
/** Logs that catalog narrowing failed open. */
|
|
7707
|
+
warnCatalogNarrowing(agent, reason) {
|
|
7708
|
+
LogErrorEx({
|
|
7709
|
+
message: `Catalog narrowing for '${agent.Name}' failed, so the full catalog is shown: ${reason}`,
|
|
7710
|
+
severity: 'warning',
|
|
7711
|
+
category: 'CatalogNarrowing',
|
|
7712
|
+
});
|
|
7713
|
+
}
|
|
7714
|
+
/** The items of one list the prompt shows. Returns `items` itself when the run's narrowing hides none of them. */
|
|
7715
|
+
hideNarrowedOut(kind, items) {
|
|
7716
|
+
return ApplyCatalogNarrowing(items, this._catalogNarrowing?.Hidden[kind]);
|
|
7717
|
+
}
|
|
7718
|
+
/**
|
|
7719
|
+
* One catalog section as the prompt shows it: `full` when the run's narrowing hides none of
|
|
7720
|
+
* `items`, otherwise a {@link CatalogNarrowingNote} saying how many are hidden and how to reach
|
|
7721
|
+
* them, then the items still shown. A hidden item can still be called. Fails OPEN, unlike the
|
|
7722
|
+
* skill policy: an error shows `full` and logs a warning.
|
|
7723
|
+
*/
|
|
7724
|
+
narrowedCatalogSection(kind, items, full, format) {
|
|
7725
|
+
try {
|
|
7726
|
+
const shown = this.hideNarrowedOut(kind, items);
|
|
7727
|
+
if (shown === items) {
|
|
7728
|
+
return full;
|
|
7729
|
+
}
|
|
7730
|
+
const shownSet = new Set(shown);
|
|
7731
|
+
const hiddenNames = items.filter(item => !shownSet.has(item)).map(item => item.Name);
|
|
7732
|
+
return `${CatalogNarrowingNote(kind, hiddenNames)}\n\n${format(shown)}`;
|
|
7733
|
+
}
|
|
7734
|
+
catch (error) {
|
|
7735
|
+
LogErrorEx({
|
|
7736
|
+
message: `Catalog narrowing could not narrow the ${kind} list, so all of it is shown: ${error instanceof Error ? error.message : String(error)}`,
|
|
7737
|
+
severity: 'warning',
|
|
7738
|
+
category: 'CatalogNarrowing',
|
|
7739
|
+
});
|
|
7740
|
+
return full;
|
|
7741
|
+
}
|
|
7742
|
+
}
|
|
6570
7743
|
/**
|
|
6571
7744
|
* Builds the agent-invariant {@link AgentBaseCatalog} — the resolved sub-agents + actions and
|
|
6572
7745
|
* their formatted markdown, plus the base agent-type prompt params. Computed once per agent and
|
|
@@ -6702,9 +7875,51 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6702
7875
|
// to distinguish between explicit user settings and schema defaults
|
|
6703
7876
|
const explicitResponseType = runtimeOverrides?.includeResponseTypeDefinition ||
|
|
6704
7877
|
agentParams.includeResponseTypeDefinition;
|
|
7878
|
+
this.warnOnceOnUnknownFinishIfMode(merged.finishIfMode, agent);
|
|
6705
7879
|
this.applyResponseTypeAutoAlignment(merged, explicitResponseType);
|
|
6706
7880
|
return merged;
|
|
6707
7881
|
}
|
|
7882
|
+
/**
|
|
7883
|
+
* The agent and value pairs {@link warnOnceOnUnknownFinishIfMode} has already reported. It is
|
|
7884
|
+
* process-wide because the prompt params are merged again for every run of an agent, and on every
|
|
7885
|
+
* turn that carries runtime overrides. One warning per agent and value is enough. A run request
|
|
7886
|
+
* can set the value, so what this keeps is bounded: see {@link FinishIfModeWarnings}.
|
|
7887
|
+
*/
|
|
7888
|
+
static { this.finishIfModeWarnings = new FinishIfModeWarnings(); }
|
|
7889
|
+
/**
|
|
7890
|
+
* Warns, once per agent and value, when the merged `finishIfMode` is set but is not a
|
|
7891
|
+
* {@link FinishIfMode}. {@link ResolveFinishIfMode} treats such a value as `'off'`, so without
|
|
7892
|
+
* this a typo such as `"On"` or `"true"` would turn the agent's gates off without a word. An
|
|
7893
|
+
* absent value is the default, so it is not reported.
|
|
7894
|
+
*/
|
|
7895
|
+
warnOnceOnUnknownFinishIfMode(value, agent) {
|
|
7896
|
+
const warning = BaseAgent.finishIfModeWarnings.WarningFor(agent.ID, agent.Name, value);
|
|
7897
|
+
if (warning) {
|
|
7898
|
+
this.logError(warning, { agent, category: 'AgentConfiguration', severity: 'warning' });
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
/**
|
|
7902
|
+
* The master switch for decision-model use by this agent: `decisionsEnabled` in its merged
|
|
7903
|
+
* agent-type prompt params (type default < agent < the run's `data.__agentTypePromptParams`).
|
|
7904
|
+
* Anything but `true` is off, the default.
|
|
7905
|
+
*
|
|
7906
|
+
* While it is off the agent never asks a decision model on its own. Each automatic use checks it
|
|
7907
|
+
* before it asks, whatever its own setting says: inline `decisions` ({@link processTurnDecisions}),
|
|
7908
|
+
* `finishIf` gates ({@link finishIfSettings}), decision discovery ({@link isDecisionDiscoveryOnFor}),
|
|
7909
|
+
* the payload change check ({@link isPayloadFeedbackCheckOn}), catalog narrowing
|
|
7910
|
+
* ({@link ensureCatalogNarrowing}) and the Memory Manager's note gate. And
|
|
7911
|
+
* {@link applyResponseTypeAutoAlignment} keeps the `decisions` and `finishIf` docs and response
|
|
7912
|
+
* fields out of the prompt. While it is on, each use keeps its own setting, and each is off by default.
|
|
7913
|
+
*
|
|
7914
|
+
* Explicit uses never read it: a Flow agent's Decision step ({@link executeDecisionStep}) runs because
|
|
7915
|
+
* someone placed it, and the task-graph Decision step and the Run Decision action call
|
|
7916
|
+
* {@link AgentDecisionService} directly. That is why the check sits at each automatic use, not in the service.
|
|
7917
|
+
*
|
|
7918
|
+
* @param promptParams - The merged agent-type prompt params.
|
|
7919
|
+
*/
|
|
7920
|
+
DecisionsEnabled(promptParams) {
|
|
7921
|
+
return promptParams?.decisionsEnabled === true;
|
|
7922
|
+
}
|
|
6708
7923
|
/**
|
|
6709
7924
|
* Applies auto-alignment rules to includeResponseTypeDefinition based on other flags.
|
|
6710
7925
|
*
|
|
@@ -6718,6 +7933,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6718
7933
|
* - includeCommandDocs → includeResponseTypeDefinition.commands
|
|
6719
7934
|
* - includeForEachDocs → includeResponseTypeDefinition.forEach
|
|
6720
7935
|
* - includeWhileDocs → includeResponseTypeDefinition.while
|
|
7936
|
+
* - includeDecisionsDocs → includeResponseTypeDefinition.decisions
|
|
7937
|
+
*
|
|
7938
|
+
* While the master switch is off ({@link DecisionsEnabled}), the decisions and finishIf docs and
|
|
7939
|
+
* response fields are off, even when set explicitly.
|
|
6721
7940
|
*
|
|
6722
7941
|
* @param params - The merged params object to modify in place
|
|
6723
7942
|
* @param explicitResponseType - The explicitly set response type config from agent/runtime (not schema defaults)
|
|
@@ -6725,18 +7944,37 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6725
7944
|
* @since 2.132.0
|
|
6726
7945
|
*/
|
|
6727
7946
|
applyResponseTypeAutoAlignment(params, explicitResponseType) {
|
|
7947
|
+
// Align a copy: the object may be the caller's per-run override, which sub-agents inherit with the
|
|
7948
|
+
// run's data. Writing into it would turn a key "explicit" for every later merge of that override.
|
|
7949
|
+
if (IsPlainObject(params.includeResponseTypeDefinition)) {
|
|
7950
|
+
params.includeResponseTypeDefinition = { ...params.includeResponseTypeDefinition };
|
|
7951
|
+
}
|
|
6728
7952
|
// Ensure includeResponseTypeDefinition is an object
|
|
6729
|
-
if (!params.includeResponseTypeDefinition || typeof params.includeResponseTypeDefinition !== 'object') {
|
|
7953
|
+
else if (!params.includeResponseTypeDefinition || typeof params.includeResponseTypeDefinition !== 'object') {
|
|
6730
7954
|
params.includeResponseTypeDefinition = {
|
|
6731
7955
|
payload: true,
|
|
6732
7956
|
responseForms: true,
|
|
6733
7957
|
commands: true,
|
|
6734
7958
|
forEach: true,
|
|
6735
7959
|
while: true,
|
|
6736
|
-
scratchpad: true
|
|
7960
|
+
scratchpad: true,
|
|
7961
|
+
decisions: true,
|
|
7962
|
+
finishIf: true
|
|
6737
7963
|
};
|
|
6738
7964
|
}
|
|
6739
7965
|
const responseType = params.includeResponseTypeDefinition;
|
|
7966
|
+
const decisionsEnabled = this.DecisionsEnabled(params);
|
|
7967
|
+
// finishIf gates are opt-in (`finishIfMode`), and need the master switch. While off, the model is
|
|
7968
|
+
// not taught to write one, whatever includeFinishIfDocs says, so the response field below follows it off too.
|
|
7969
|
+
if (!decisionsEnabled || ResolveFinishIfMode(params.finishIfMode) === 'off') {
|
|
7970
|
+
params.includeFinishIfDocs = false;
|
|
7971
|
+
}
|
|
7972
|
+
// `decisions` is opt-in too (`includeDecisionsDocs: true`), and needs the master switch. Its docs and
|
|
7973
|
+
// types add about 1,200 tokens to every turn, and on the Prompt Eval corpus no model used it
|
|
7974
|
+
// (typed-decision plan, Task 4.7). Unset means off, so the response field below follows it off too.
|
|
7975
|
+
if (!decisionsEnabled || params.includeDecisionsDocs !== true) {
|
|
7976
|
+
params.includeDecisionsDocs = false;
|
|
7977
|
+
}
|
|
6740
7978
|
// Auto-alignment mappings: docs flag → response type property
|
|
6741
7979
|
const alignmentMappings = [
|
|
6742
7980
|
{ docsFlag: 'includePayloadInPrompt', responseTypeKey: 'payload' },
|
|
@@ -6748,7 +7986,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6748
7986
|
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' },
|
|
6749
7987
|
{ docsFlag: 'includeConversationToolsDocs', responseTypeKey: 'conversationToolCalls' },
|
|
6750
7988
|
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' },
|
|
6751
|
-
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' }
|
|
7989
|
+
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' },
|
|
7990
|
+
{ docsFlag: 'includeDecisionsDocs', responseTypeKey: 'decisions' },
|
|
7991
|
+
{ docsFlag: 'includeFinishIfDocs', responseTypeKey: 'finishIf' }
|
|
6752
7992
|
];
|
|
6753
7993
|
for (const { docsFlag, responseTypeKey } of alignmentMappings) {
|
|
6754
7994
|
// Check if the user explicitly set this response type property
|
|
@@ -6766,6 +8006,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6766
8006
|
responseType[responseTypeKey] = true;
|
|
6767
8007
|
}
|
|
6768
8008
|
}
|
|
8009
|
+
// With the master switch off, neither decision field is offered, even when set explicitly: the
|
|
8010
|
+
// model must not be invited to ask for something the run would refuse.
|
|
8011
|
+
if (!decisionsEnabled) {
|
|
8012
|
+
responseType.decisions = false;
|
|
8013
|
+
responseType.finishIf = false;
|
|
8014
|
+
}
|
|
6769
8015
|
// Capability gates align the OTHER way: default OFF, and the section appears only when the
|
|
6770
8016
|
// gate is explicitly on. Running these through the loop above would invert their meaning —
|
|
6771
8017
|
// that loop's `else` branch defaults an unset key to true, which is exactly what a gated
|
|
@@ -6811,6 +8057,320 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6811
8057
|
return {};
|
|
6812
8058
|
}
|
|
6813
8059
|
}
|
|
8060
|
+
/**
|
|
8061
|
+
* The finishIf settings of the current step, from its merged agent-type prompt params. A gate is
|
|
8062
|
+
* evaluated when the master switch is on ({@link DecisionsEnabled}), `finishIfMode` is `shadow` or
|
|
8063
|
+
* `on`, and `includeResponseTypeDefinition.finishIf` is not `false`; it ends the run only in `on`.
|
|
8064
|
+
*/
|
|
8065
|
+
finishIfSettings() {
|
|
8066
|
+
const promptParams = this._agentTypePromptParams;
|
|
8067
|
+
const rules = promptParams?.includeResponseTypeDefinition;
|
|
8068
|
+
const threshold = promptParams?.finishIfThreshold;
|
|
8069
|
+
const mode = ResolveFinishIfMode(promptParams?.finishIfMode);
|
|
8070
|
+
return {
|
|
8071
|
+
Enabled: this.DecisionsEnabled(promptParams) && mode !== 'off' && rules?.finishIf !== false,
|
|
8072
|
+
Mode: mode,
|
|
8073
|
+
Threshold: typeof threshold === 'number' && threshold > 0 && threshold <= 1
|
|
8074
|
+
? threshold
|
|
8075
|
+
: DEFAULT_LOOP_AGENT_PROMPT_PARAMS.finishIfThreshold,
|
|
8076
|
+
PromptName: typeof promptParams?.decisionPromptName === 'string'
|
|
8077
|
+
? promptParams.decisionPromptName
|
|
8078
|
+
: AgentDecisionService.DEFAULT_PROMPT_NAME,
|
|
8079
|
+
};
|
|
8080
|
+
}
|
|
8081
|
+
/** Whether a passing finishIf gate ends the run: only in `on` mode. */
|
|
8082
|
+
finishIfActs() {
|
|
8083
|
+
const settings = this.finishIfSettings();
|
|
8084
|
+
return settings.Enabled && settings.Mode === 'on';
|
|
8085
|
+
}
|
|
8086
|
+
/**
|
|
8087
|
+
* Whether an evaluated gate ends the run. In shadow mode a passing gate is only logged: the
|
|
8088
|
+
* outcome is already recorded on its `Finish check` step, and the model takes its next turn.
|
|
8089
|
+
*/
|
|
8090
|
+
gateEndsRun(outcome, params) {
|
|
8091
|
+
if (!outcome.Passed) {
|
|
8092
|
+
return false;
|
|
8093
|
+
}
|
|
8094
|
+
if (!this.finishIfActs()) {
|
|
8095
|
+
this.logStatus('[finishIf] Shadow mode: the gate passed, and the run continues as it would have', true, params);
|
|
8096
|
+
return false;
|
|
8097
|
+
}
|
|
8098
|
+
return true;
|
|
8099
|
+
}
|
|
8100
|
+
/**
|
|
8101
|
+
* Runs the finishIf gate after an Actions step, and returns the Success step when it passes.
|
|
8102
|
+
* ForEach and While call executeActionsStep without a conversation message, and are never gated.
|
|
8103
|
+
*/
|
|
8104
|
+
async finishAfterActions(params, decision, actionResults, actionSummaries, payload, parentStepId, addConversationMessage) {
|
|
8105
|
+
const finishIf = decision.finishIf;
|
|
8106
|
+
if (!finishIf || !addConversationMessage || !this.finishIfSettings().Enabled) {
|
|
8107
|
+
return undefined;
|
|
8108
|
+
}
|
|
8109
|
+
// (In shadow mode the gate below is still evaluated and recorded; it just never ends the run.)
|
|
8110
|
+
// `success` means only that the action did not throw; `result.Success` is its outcome.
|
|
8111
|
+
if (!actionResults.every(r => r.success && r.result?.Success === true)) {
|
|
8112
|
+
return undefined;
|
|
8113
|
+
}
|
|
8114
|
+
// AIDirectives are instructions for the model's next turn, which a passing gate would take away.
|
|
8115
|
+
const withDirectives = actionSummaries.filter(a => (a.aiDirectives?.length ?? 0) > 0).map(a => a.actionName);
|
|
8116
|
+
if (withDirectives.length > 0) {
|
|
8117
|
+
this.logStatus(`[finishIf] Gate skipped: ${withDirectives.join(', ')} returned AIDirectives, which the model must see on its next turn`, false, params);
|
|
8118
|
+
return undefined;
|
|
8119
|
+
}
|
|
8120
|
+
const state = CapFinishIfState(actionSummaries.map(FormatActionForFinishIf).join('\n\n'));
|
|
8121
|
+
const outcome = await this.evaluateFinishIf(finishIf, state, params, decision, payload, parentStepId);
|
|
8122
|
+
return this.gateEndsRun(outcome, params)
|
|
8123
|
+
? { ...this.finishIfSuccessStep(finishIf, decision, payload), priorStepResult: actionSummaries }
|
|
8124
|
+
: undefined;
|
|
8125
|
+
}
|
|
8126
|
+
/**
|
|
8127
|
+
* Runs the finishIf gate after a Sub-Agent step that succeeded without terminating, and returns
|
|
8128
|
+
* the step with `terminate: true` when it passes, as `terminateAfter` would.
|
|
8129
|
+
*/
|
|
8130
|
+
async finishAfterSubAgent(params, decision, result) {
|
|
8131
|
+
const finishIf = decision.finishIf;
|
|
8132
|
+
if (!finishIf || result.terminate || result.step !== 'Success' || !this.finishIfSettings().Enabled) {
|
|
8133
|
+
return undefined;
|
|
8134
|
+
}
|
|
8135
|
+
const outcome = await this.evaluateFinishIf(finishIf, FormatSubAgentForFinishIf(result), params, decision, result.newPayload);
|
|
8136
|
+
return this.gateEndsRun(outcome, params) ? { ...result, terminate: true, message: finishIf.message } : undefined;
|
|
8137
|
+
}
|
|
8138
|
+
/**
|
|
8139
|
+
* Asks the finishIf questions about the step's results, then checks that the normal Success
|
|
8140
|
+
* validation would allow the run to end. Records the check as a `Finish check` Decision step.
|
|
8141
|
+
* Never throws: any error means the gate did not pass, and the run continues as it would have.
|
|
8142
|
+
*/
|
|
8143
|
+
async evaluateFinishIf(finishIf, state, params, decision, payload, parentStepId) {
|
|
8144
|
+
const settings = this.finishIfSettings();
|
|
8145
|
+
let step;
|
|
8146
|
+
let outcome;
|
|
8147
|
+
try {
|
|
8148
|
+
step = await this.createStepEntity({
|
|
8149
|
+
stepType: 'Decision',
|
|
8150
|
+
stepName: 'Finish check',
|
|
8151
|
+
contextUser: params.contextUser,
|
|
8152
|
+
inputData: { questions: finishIf.questions, state: state.slice(0, FINISH_IF_STATE_EXCERPT) },
|
|
8153
|
+
parentId: parentStepId
|
|
8154
|
+
});
|
|
8155
|
+
outcome = await this.askFinishIf(finishIf, state, settings, params, step);
|
|
8156
|
+
if (outcome.Passed) {
|
|
8157
|
+
const refusal = await this.finishIfValidationRefusal(params, this.finishIfSuccessStep(finishIf, decision, payload), payload, step);
|
|
8158
|
+
if (refusal) {
|
|
8159
|
+
outcome = { ...outcome, Passed: false, Reason: refusal };
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
catch (error) {
|
|
8164
|
+
outcome = { Passed: false, Probabilities: {}, Reason: error instanceof Error ? error.message : String(error) };
|
|
8165
|
+
LogError(`finishIf gate failed, so the run continues: ${outcome.Reason}`);
|
|
8166
|
+
}
|
|
8167
|
+
await this.finalizeFinishCheckStep(step, outcome, settings);
|
|
8168
|
+
return outcome;
|
|
8169
|
+
}
|
|
8170
|
+
/**
|
|
8171
|
+
* Asks each finishIf question as a Likelihood. Every one must reach the threshold. The call's
|
|
8172
|
+
* prompt run is linked to the `Finish check` step, so the run's cost and tokens count it.
|
|
8173
|
+
*/
|
|
8174
|
+
async askFinishIf(finishIf, state, settings, params, step) {
|
|
8175
|
+
const questions = BuildFinishIfQuestions(finishIf);
|
|
8176
|
+
const result = await this._agentDecisionService.Ask({
|
|
8177
|
+
State: state,
|
|
8178
|
+
Questions: questions,
|
|
8179
|
+
ContextUser: params.contextUser,
|
|
8180
|
+
AgentID: params.agent?.ID,
|
|
8181
|
+
PromptName: settings.PromptName,
|
|
8182
|
+
CancellationToken: params.cancellationToken
|
|
8183
|
+
});
|
|
8184
|
+
this.attachDecisionPromptRun(step, result);
|
|
8185
|
+
if (!result.success) {
|
|
8186
|
+
return { Passed: false, Probabilities: {}, Reason: `The decision call failed: ${result.errorMessage ?? 'no error message'}` };
|
|
8187
|
+
}
|
|
8188
|
+
return JudgeFinishIf(result.Answers, questions, settings.Threshold);
|
|
8189
|
+
}
|
|
8190
|
+
/**
|
|
8191
|
+
* Returns why the normal Success validation would refuse to end the run, or `undefined` when it
|
|
8192
|
+
* allows it. The check must not count as a validation retry, so the counters are restored:
|
|
8193
|
+
* otherwise a gate that keeps not passing could exhaust the retries and fail the run.
|
|
8194
|
+
*/
|
|
8195
|
+
async finishIfValidationRefusal(params, successStep, payload, step) {
|
|
8196
|
+
if (!this._agentRun) {
|
|
8197
|
+
return 'There is no agent run to validate against';
|
|
8198
|
+
}
|
|
8199
|
+
const generalRetries = this._generalValidationRetryCount;
|
|
8200
|
+
const payloadRetries = this._validationRetryCount;
|
|
8201
|
+
try {
|
|
8202
|
+
const validated = await this.validateSuccessNextStep(params, successStep, payload, this._agentRun, step);
|
|
8203
|
+
if (validated.step === 'Success') {
|
|
8204
|
+
return undefined;
|
|
8205
|
+
}
|
|
8206
|
+
return validated.errorMessage || validated.retryInstructions || `Success validation returned '${validated.step}'`;
|
|
8207
|
+
}
|
|
8208
|
+
finally {
|
|
8209
|
+
this._generalValidationRetryCount = generalRetries;
|
|
8210
|
+
this._validationRetryCount = payloadRetries;
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
/** The Success step a passing gate returns after actions, in the shape of the client-tools `terminateAfterExecution` branch. */
|
|
8214
|
+
finishIfSuccessStep(finishIf, decision, payload) {
|
|
8215
|
+
return {
|
|
8216
|
+
step: 'Success',
|
|
8217
|
+
terminate: true,
|
|
8218
|
+
message: finishIf.message,
|
|
8219
|
+
payloadChangeRequest: decision.payloadChangeRequest,
|
|
8220
|
+
previousPayload: decision.previousPayload,
|
|
8221
|
+
newPayload: payload,
|
|
8222
|
+
scratchpad: decision.scratchpad,
|
|
8223
|
+
responseForm: decision.responseForm,
|
|
8224
|
+
actionableCommands: decision.actionableCommands,
|
|
8225
|
+
automaticCommands: decision.automaticCommands,
|
|
8226
|
+
artifactDirective: decision.artifactDirective
|
|
8227
|
+
};
|
|
8228
|
+
}
|
|
8229
|
+
/** Finalizes the `Finish check` step with the outcome. A failure to save it never affects the run. */
|
|
8230
|
+
async finalizeFinishCheckStep(step, outcome, settings) {
|
|
8231
|
+
if (!step) {
|
|
8232
|
+
return;
|
|
8233
|
+
}
|
|
8234
|
+
try {
|
|
8235
|
+
await this.finalizeStepEntity(step, true, undefined, {
|
|
8236
|
+
passed: outcome.Passed,
|
|
8237
|
+
mode: settings.Mode,
|
|
8238
|
+
endedRun: outcome.Passed && settings.Mode === 'on',
|
|
8239
|
+
threshold: settings.Threshold,
|
|
8240
|
+
probabilities: outcome.Probabilities,
|
|
8241
|
+
reason: outcome.Reason
|
|
8242
|
+
});
|
|
8243
|
+
}
|
|
8244
|
+
catch (error) {
|
|
8245
|
+
LogError(`Could not save the finishIf step: ${error instanceof Error ? error.message : String(error)}`);
|
|
8246
|
+
}
|
|
8247
|
+
}
|
|
8248
|
+
/**
|
|
8249
|
+
* Whether the merged agent-type prompt params turn the payload change check on. It is off unless
|
|
8250
|
+
* the master switch ({@link DecisionsEnabled}) and `payloadFeedbackCheck` are both `true`.
|
|
8251
|
+
*/
|
|
8252
|
+
isPayloadFeedbackCheckOn(agentTypePromptParams) {
|
|
8253
|
+
return this.DecisionsEnabled(agentTypePromptParams) && agentTypePromptParams?.payloadFeedbackCheck === true;
|
|
8254
|
+
}
|
|
8255
|
+
/**
|
|
8256
|
+
* Runs the payload change check ({@link checkPayloadChanges}) on a step's applied payload changes
|
|
8257
|
+
* when it applies: the analyzer flagged a change for feedback, the check is on
|
|
8258
|
+
* ({@link isPayloadFeedbackCheckOn}), and a turn will read the result. A step that ends the run
|
|
8259
|
+
* skips it: no call to pay for, and no message left in the conversation. Never throws.
|
|
8260
|
+
*/
|
|
8261
|
+
async checkPayloadChangesWhenOn(changeResult, nextStep, promptResult, agentTypePromptParams, params) {
|
|
8262
|
+
if (!changeResult.requiresFeedback || !this.isPayloadFeedbackCheckOn(agentTypePromptParams)) {
|
|
8263
|
+
return;
|
|
8264
|
+
}
|
|
8265
|
+
if (this.stepEndsRun(nextStep)) {
|
|
8266
|
+
this.logStatus(`[Payload check] Skipped: the ${nextStep.step} step ends the run, so no turn would read the result`, true, params);
|
|
8267
|
+
return;
|
|
8268
|
+
}
|
|
8269
|
+
await this.checkPayloadChanges(changeResult.analysis, nextStep, promptResult, agentTypePromptParams, params);
|
|
8270
|
+
}
|
|
8271
|
+
/**
|
|
8272
|
+
* Whether the step ends the run, so no turn would read the payload change check's message: it
|
|
8273
|
+
* terminates, or it is a `Success` or `Chat` step, which hand the run back to the caller.
|
|
8274
|
+
*/
|
|
8275
|
+
stepEndsRun(step) {
|
|
8276
|
+
return step.terminate === true || step.step === 'Success' || step.step === 'Chat';
|
|
8277
|
+
}
|
|
8278
|
+
/**
|
|
8279
|
+
* The payload change check. Asks whether each change the analyzer flagged was intended, as one
|
|
8280
|
+
* Likelihood per change in one decision call, and records it as a `Payload change check`
|
|
8281
|
+
* Decision step. The changes judged unintended are listed in a message for the agent's next
|
|
8282
|
+
* turn, which asks it to confirm or restore them.
|
|
8283
|
+
*
|
|
8284
|
+
* It never reverts or blocks a change: the agent decides. When the decision cannot answer,
|
|
8285
|
+
* every change is accepted, as it is with the check off. Never throws.
|
|
8286
|
+
*/
|
|
8287
|
+
async checkPayloadChanges(analysis, nextStep, promptResult, agentTypePromptParams, params) {
|
|
8288
|
+
const promptName = agentTypePromptParams?.decisionPromptName;
|
|
8289
|
+
const manager = new PayloadFeedbackManager({ decisionPromptName: typeof promptName === 'string' ? promptName : undefined }, this._agentDecisionService);
|
|
8290
|
+
const questions = manager.GenerateQuestions(analysis?.warnings ?? []);
|
|
8291
|
+
if (questions.length === 0) {
|
|
8292
|
+
return;
|
|
8293
|
+
}
|
|
8294
|
+
let step;
|
|
8295
|
+
try {
|
|
8296
|
+
step = await this.createStepEntity({
|
|
8297
|
+
stepType: 'Decision',
|
|
8298
|
+
stepName: 'Payload change check',
|
|
8299
|
+
contextUser: params.contextUser,
|
|
8300
|
+
inputData: {
|
|
8301
|
+
questions: questions.map(q => ({ id: q.id, path: q.warning.path, type: q.warning.type, change: manager.DescribeChange(q.warning) }))
|
|
8302
|
+
}
|
|
8303
|
+
});
|
|
8304
|
+
const responses = await manager.QueryAgent(questions, this.payloadFeedbackContext(nextStep, promptResult, params), params.contextUser);
|
|
8305
|
+
// Like every decision call, it counts toward the run's cost and tokens through its step
|
|
8306
|
+
if (manager.LastDecisionResult) {
|
|
8307
|
+
this.attachDecisionPromptRun(step, manager.LastDecisionResult);
|
|
8308
|
+
}
|
|
8309
|
+
await this.finalizePayloadCheckStep(step, questions, responses, manager.IntendedThreshold);
|
|
8310
|
+
const message = manager.BuildUnintendedChangesMessage(questions, responses);
|
|
8311
|
+
if (message) {
|
|
8312
|
+
this.injectPayloadCheckMessage(params, message);
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
catch (error) {
|
|
8316
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
8317
|
+
LogError(`The payload change check failed, so every change stands: ${reason}`);
|
|
8318
|
+
if (step) {
|
|
8319
|
+
await this.finalizeStepEntity(step, false, reason);
|
|
8320
|
+
}
|
|
8321
|
+
}
|
|
8322
|
+
}
|
|
8323
|
+
/**
|
|
8324
|
+
* What the payload change check's decision reads: the agent's own reasoning for the step (from its
|
|
8325
|
+
* response, else from the step), its message, and the reasoning it gave with the change. The check
|
|
8326
|
+
* never runs on a `Chat` or `Success` step, so the message comes from the response alone.
|
|
8327
|
+
*/
|
|
8328
|
+
payloadFeedbackContext(nextStep, promptResult, params) {
|
|
8329
|
+
const response = responseReasoningAndMessage(promptResult?.result);
|
|
8330
|
+
return {
|
|
8331
|
+
Reasoning: response.Reasoning ?? nextStep.reasoning,
|
|
8332
|
+
ChangeReasoning: nextStep.payloadChangeRequest?.reasoning,
|
|
8333
|
+
Message: response.Message,
|
|
8334
|
+
AgentID: params.agent?.ID ?? this._agentRun?.AgentID,
|
|
8335
|
+
CancellationToken: params.cancellationToken
|
|
8336
|
+
};
|
|
8337
|
+
}
|
|
8338
|
+
/**
|
|
8339
|
+
* Finalizes the `Payload change check` step with each change's probability and outcome. The step
|
|
8340
|
+
* fails when no decision answered, and its error says why. A failure to save it never affects the run.
|
|
8341
|
+
*/
|
|
8342
|
+
async finalizePayloadCheckStep(step, questions, responses, threshold) {
|
|
8343
|
+
const answered = responses.filter(r => typeof r.probability === 'number');
|
|
8344
|
+
const unintended = responses.filter(r => !r.intended).map(r => questions.find(q => q.id === r.questionId)?.warning.path);
|
|
8345
|
+
try {
|
|
8346
|
+
await this.finalizeStepEntity(step, answered.length > 0, answered.length > 0 ? undefined : responses[0]?.explanation, {
|
|
8347
|
+
threshold,
|
|
8348
|
+
probabilities: Object.fromEntries(answered.map(r => [r.questionId, r.probability])),
|
|
8349
|
+
responses,
|
|
8350
|
+
unintendedPaths: unintended
|
|
8351
|
+
});
|
|
8352
|
+
}
|
|
8353
|
+
catch (error) {
|
|
8354
|
+
LogError(`Could not save the payload change check step: ${error instanceof Error ? error.message : String(error)}`);
|
|
8355
|
+
}
|
|
8356
|
+
}
|
|
8357
|
+
/** Adds the payload change check's message to the conversation, for the agent's next turn. */
|
|
8358
|
+
injectPayloadCheckMessage(params, content) {
|
|
8359
|
+
const message = {
|
|
8360
|
+
role: 'user',
|
|
8361
|
+
content,
|
|
8362
|
+
metadata: {
|
|
8363
|
+
turnAdded: this._promptTurnCount,
|
|
8364
|
+
messageType: 'tool-result',
|
|
8365
|
+
expirationTurns: 3,
|
|
8366
|
+
expirationMode: 'Compact',
|
|
8367
|
+
compactMode: 'First N Chars',
|
|
8368
|
+
compactLength: 500,
|
|
8369
|
+
compactPromptId: '',
|
|
8370
|
+
},
|
|
8371
|
+
};
|
|
8372
|
+
params.conversationMessages.push(message);
|
|
8373
|
+
}
|
|
6814
8374
|
/**
|
|
6815
8375
|
* Whether THIS action may use the run's runtime API key for THIS driver class. The default is
|
|
6816
8376
|
* yes: the run was started on those keys, and an action that calls a vendor on the user's behalf
|
|
@@ -7845,6 +9405,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7845
9405
|
}
|
|
7846
9406
|
// Reset prompt turn counter for this execution
|
|
7847
9407
|
this._promptTurnCount = 0;
|
|
9408
|
+
this._heldDecisions = undefined;
|
|
7848
9409
|
// Resolve action-result compression config for this run (default on; opt out via
|
|
7849
9410
|
// crushActionResults: false in the agent-type prompt params).
|
|
7850
9411
|
this._actionResultCrush = this.resolveActionResultCrush(params);
|
|
@@ -8510,12 +10071,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8510
10071
|
}
|
|
8511
10072
|
}
|
|
8512
10073
|
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8513
|
-
case 'Sub-Agent':
|
|
10074
|
+
case 'Sub-Agent': {
|
|
8514
10075
|
// A Sub-Agent step carrying a task graph is a FOLDED graph (D9): the agent type
|
|
8515
10076
|
// rewrote a one-node graph into an ordinary in-run call. Record it before running,
|
|
8516
10077
|
// so the run shows what was emitted even if the sub-agent then fails.
|
|
8517
10078
|
await this.recordFoldedTaskGraph(params, previousDecision);
|
|
8518
|
-
|
|
10079
|
+
const subAgentResult = await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
10080
|
+
const afterSubAgent = (await this.finishAfterSubAgent(params, previousDecision, subAgentResult)) ?? subAgentResult;
|
|
10081
|
+
return this.selfCheckSuccess(params, afterSubAgent, afterSubAgent.newPayload ?? subAgentResult.newPayload);
|
|
10082
|
+
}
|
|
8519
10083
|
case 'Actions':
|
|
8520
10084
|
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount, this.actionOptionsForAgentType());
|
|
8521
10085
|
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
@@ -8537,6 +10101,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8537
10101
|
// step union — LoopAgentType.DetermineNextStep() emits it when the LLM chooses client tools.
|
|
8538
10102
|
case 'ClientTools':
|
|
8539
10103
|
return await this.executeClientToolsStep(params, config, previousDecision, stepCount);
|
|
10104
|
+
// Type assertion required because 'Decision' is not part of the BaseAgentNextStep step
|
|
10105
|
+
// union — FlowAgentType emits it for a Decision step, which is one decision call and no
|
|
10106
|
+
// LLM turn. executeDecisionStep returns a 'Retry' carrying the answers.
|
|
10107
|
+
case 'Decision':
|
|
10108
|
+
return await this.executeDecisionStep(params, previousDecision);
|
|
8540
10109
|
case 'Chat':
|
|
8541
10110
|
return await this.executeChatStep(params, previousDecision);
|
|
8542
10111
|
case 'Success':
|
|
@@ -8675,6 +10244,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8675
10244
|
* @private
|
|
8676
10245
|
*/
|
|
8677
10246
|
async executePromptStep(params, config, previousDecision, stepCount = 0) {
|
|
10247
|
+
// Decisions the last prompt held back: the step that carried them did not end the run, so
|
|
10248
|
+
// answer them now, where this prompt will read the answers
|
|
10249
|
+
await this.runHeldDecisions(params);
|
|
8678
10250
|
// Ask the agent type if it has a custom prompt for this step
|
|
8679
10251
|
const promptToUse = await this.AgentTypeInstance.GetPromptForStep(params, config, previousDecision?.newPayload || previousDecision?.previousPayload, this.AgentTypeState, previousDecision);
|
|
8680
10252
|
const promptId = promptToUse?.ID;
|
|
@@ -8858,6 +10430,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8858
10430
|
currentStepPayloadChangeResult = this.buildPayloadChangeResultSummary(changeResult);
|
|
8859
10431
|
// Set the final payload - the changeResult already respects the allowed paths
|
|
8860
10432
|
finalPayload = changeResult.result;
|
|
10433
|
+
// Opt-in payload change check: asks whether the flagged changes were intended. It never
|
|
10434
|
+
// reverts or blocks a change; the agent reads the result on its next turn.
|
|
10435
|
+
const payloadCheckParams = promptParams.data?.__agentTypePromptParams;
|
|
10436
|
+
await this.checkPayloadChangesWhenOn(changeResult, initialNextStep, promptResult, payloadCheckParams, params);
|
|
8861
10437
|
}
|
|
8862
10438
|
// Apply scratchpad changes if provided (zero turn cost — processed inline)
|
|
8863
10439
|
if (initialNextStep.scratchpad) {
|
|
@@ -8916,6 +10492,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8916
10492
|
const pipelineResult = await this.executePipelineAsStep(initialNextStep.pipeline, params);
|
|
8917
10493
|
this.injectPipelineResultMessage(params, pipelineResult);
|
|
8918
10494
|
}
|
|
10495
|
+
// Execute decision requests if provided (zero turn cost — processed inline), unless the
|
|
10496
|
+
// step that carries them can end the run: then they are held until it has run
|
|
10497
|
+
const decisions = initialNextStep.decisions;
|
|
10498
|
+
if (decisions?.length) {
|
|
10499
|
+
const agentTypePromptParams = promptParams.data?.__agentTypePromptParams;
|
|
10500
|
+
await this.processTurnDecisions(decisions, initialNextStep, finalPayload, agentTypePromptParams, params);
|
|
10501
|
+
}
|
|
8919
10502
|
// now that we have processed the payload, we can process the next step which does validation and changes the next step if
|
|
8920
10503
|
// validation fails
|
|
8921
10504
|
const updatedNextStep = await this.processNextStep(initialNextStep, params, config.agentType, promptResult, finalPayload, stepEntity);
|
|
@@ -10718,6 +12301,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10718
12301
|
catch (error) {
|
|
10719
12302
|
LogError(`Error in PostProcessActionStep: ${error.message}`);
|
|
10720
12303
|
}
|
|
12304
|
+
const finished = await this.finishAfterActions(params, previousDecision, actionResults, actionSummaries, finalPayload, parentStepId, addConversationMessage);
|
|
12305
|
+
if (finished) {
|
|
12306
|
+
return this.selfCheckSuccess(params, finished, finalPayload);
|
|
12307
|
+
}
|
|
10721
12308
|
// After actions complete, we need to process the results
|
|
10722
12309
|
// The retry step is used to re-execute the prompt with the action results
|
|
10723
12310
|
// This allows the agent to analyze the results and determine what to do next
|
|
@@ -10837,7 +12424,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10837
12424
|
// If the LLM already declared taskComplete=true alongside the client tools,
|
|
10838
12425
|
// honor that intent now that tools have executed — no need for another LLM call.
|
|
10839
12426
|
if (previousDecision.terminateAfterExecution) {
|
|
10840
|
-
return {
|
|
12427
|
+
return this.selfCheckSuccess(params, {
|
|
10841
12428
|
step: 'Success',
|
|
10842
12429
|
terminate: true,
|
|
10843
12430
|
message: previousDecision.message || 'Client tools executed successfully.',
|
|
@@ -10850,7 +12437,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10850
12437
|
// own artifact directive has to come along. Omitting it silently discarded the
|
|
10851
12438
|
// instruction whenever an agent declared client tools and taskComplete together.
|
|
10852
12439
|
artifactDirective: previousDecision.artifactDirective
|
|
10853
|
-
};
|
|
12440
|
+
}, previousDecision.newPayload);
|
|
10854
12441
|
}
|
|
10855
12442
|
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
10856
12443
|
}
|
|
@@ -10939,6 +12526,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
10939
12526
|
* them calls this after MJ's gates
|
|
10940
12527
|
* (AcceptsSkills, Status, agent grant, user Run permission, the ActivationMode double gate) and
|
|
10941
12528
|
* before anything activates. The default is the identity: MJ's gates are the whole policy.
|
|
12529
|
+
* Catalog narrowing (plan Task 3.7) is not part of this policy: it runs after it, at the catalog
|
|
12530
|
+
* site, and only hides skills from the prompt. An override never needs to call `super` for it.
|
|
10942
12531
|
*
|
|
10943
12532
|
* Override it to layer a policy MJ has no table for — a tenant licensing model, a per-organization
|
|
10944
12533
|
* entitlement, a feature flag — and it applies everywhere at once. Without this seam a subclass
|
|
@@ -12794,8 +14383,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
12794
14383
|
// Iterate through the agent run's steps to sum up tokens
|
|
12795
14384
|
if (this._agentRun?.Steps) {
|
|
12796
14385
|
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)
|
|
14386
|
+
if ((step.StepType === 'Prompt' || step.StepType === 'Compaction' || step.StepType === 'Decision') && step.PromptRun) {
|
|
14387
|
+
// Add tokens from prompt runs (rollup fields include any nested child prompt runs).
|
|
14388
|
+
// A Decision step carries its own decision call's run — the Finish check, a single-state
|
|
14389
|
+
// request, or one item of a forEachItemIn request — so each call is counted once.
|
|
12799
14390
|
totalTokens += step.PromptRun.TokensUsedRollup || 0;
|
|
12800
14391
|
promptTokens += step.PromptRun.TokensPromptRollup || 0;
|
|
12801
14392
|
completionTokens += step.PromptRun.TokensCompletionRollup || 0;
|