@memberjunction/ai-agents 5.43.0 → 5.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/DuplicateReasoningAgentProvider.d.ts +35 -0
- package/dist/DuplicateReasoningAgentProvider.d.ts.map +1 -0
- package/dist/DuplicateReasoningAgentProvider.js +95 -0
- package/dist/DuplicateReasoningAgentProvider.js.map +1 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts +7 -0
- package/dist/MJAIAgentRequestEntityServer.d.ts.map +1 -1
- package/dist/MJAIAgentRequestEntityServer.js +28 -1
- package/dist/MJAIAgentRequestEntityServer.js.map +1 -1
- package/dist/SkillImportExportService.d.ts +63 -0
- package/dist/SkillImportExportService.d.ts.map +1 -0
- package/dist/SkillImportExportService.js +148 -0
- package/dist/SkillImportExportService.js.map +1 -0
- package/dist/SkillMarkdownConverter.d.ts +82 -0
- package/dist/SkillMarkdownConverter.d.ts.map +1 -0
- package/dist/SkillMarkdownConverter.js +145 -0
- package/dist/SkillMarkdownConverter.js.map +1 -0
- package/dist/agent-types/loop-agent-response-type.d.ts +17 -2
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +33 -1
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +373 -12
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1007 -78
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +39 -0
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +118 -5
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/operations/AISkillMarkdownOperations.d.ts +11 -0
- package/dist/operations/AISkillMarkdownOperations.d.ts.map +1 -0
- package/dist/operations/AISkillMarkdownOperations.js +57 -0
- package/dist/operations/AISkillMarkdownOperations.js.map +1 -0
- package/dist/prompt-component-resolver.d.ts +73 -0
- package/dist/prompt-component-resolver.d.ts.map +1 -0
- package/dist/prompt-component-resolver.js +166 -0
- package/dist/prompt-component-resolver.js.map +1 -0
- package/dist/realtime/agent-media-library.d.ts +65 -0
- package/dist/realtime/agent-media-library.d.ts.map +1 -0
- package/dist/realtime/agent-media-library.js +160 -0
- package/dist/realtime/agent-media-library.js.map +1 -0
- package/dist/realtime/client-context-channel-server.d.ts +59 -0
- package/dist/realtime/client-context-channel-server.d.ts.map +1 -0
- package/dist/realtime/client-context-channel-server.js +78 -0
- package/dist/realtime/client-context-channel-server.js.map +1 -0
- package/dist/realtime/media-channel-server.d.ts +73 -0
- package/dist/realtime/media-channel-server.d.ts.map +1 -0
- package/dist/realtime/media-channel-server.js +145 -0
- package/dist/realtime/media-channel-server.js.map +1 -0
- package/dist/realtime/realtime-channel-server-data-context.d.ts +42 -0
- package/dist/realtime/realtime-channel-server-data-context.d.ts.map +1 -0
- package/dist/realtime/realtime-channel-server-data-context.js +29 -0
- package/dist/realtime/realtime-channel-server-data-context.js.map +1 -0
- package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -1
- package/dist/realtime/realtime-channel-server-host.js +8 -2
- package/dist/realtime/realtime-channel-server-host.js.map +1 -1
- package/dist/realtime/realtime-client-session-service.d.ts +85 -5
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +234 -25
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +82 -3
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +147 -6
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-recording-capture.d.ts +125 -0
- package/dist/realtime/realtime-recording-capture.d.ts.map +1 -0
- package/dist/realtime/realtime-recording-capture.js +317 -0
- package/dist/realtime/realtime-recording-capture.js.map +1 -0
- package/dist/realtime/realtime-recording-store.d.ts +105 -0
- package/dist/realtime/realtime-recording-store.d.ts.map +1 -0
- package/dist/realtime/realtime-recording-store.js +216 -0
- package/dist/realtime/realtime-recording-store.js.map +1 -0
- package/dist/realtime/realtime-session-runner.d.ts +33 -2
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -1
- package/dist/realtime/realtime-session-runner.js +45 -2
- package/dist/realtime/realtime-session-runner.js.map +1 -1
- package/dist/realtime/realtime-tool-broker.d.ts +41 -3
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -1
- package/dist/realtime/realtime-tool-broker.js +67 -3
- package/dist/realtime/realtime-tool-broker.js.map +1 -1
- package/package.json +18 -17
package/dist/base-agent.js
CHANGED
|
@@ -17,16 +17,23 @@ import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
|
17
17
|
import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
|
|
18
18
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
19
19
|
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
|
|
20
|
+
// token optimization via @memberjunction/context-crush (SmartCrusher/CacheAligner-inspired)
|
|
21
|
+
import { CrushJSON, DescribeCrush, PartitionStablePrefix } from '@memberjunction/context-crush';
|
|
22
|
+
// AST-aware code reduction (CodeCompressor-inspired) — opt-in per agent type
|
|
23
|
+
import { CrushCode } from '@memberjunction/context-crush/code';
|
|
20
24
|
import { RealtimeSessionRunner } from './realtime/realtime-session-runner.js';
|
|
21
25
|
import { ResolveNarrationInstructionsTemplate } from './realtime/realtime-narration.js';
|
|
22
26
|
import { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
|
|
23
27
|
import { RealtimeClientSessionService } from './realtime/realtime-client-session-service.js';
|
|
24
28
|
import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
|
|
29
|
+
import { RealtimeRecordingController } from './realtime/realtime-recording-capture.js';
|
|
30
|
+
import { resolveRecordingStorageAccountID, storeRealtimeRecording } from './realtime/realtime-recording-store.js';
|
|
25
31
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
26
32
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
27
33
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
28
34
|
import { AgentMemoryContextBuilder } from './agent-memory-context-builder.js';
|
|
29
|
-
import {
|
|
35
|
+
import { PromptComponentResolver, InjectScopedPromptParts } from './prompt-component-resolver.js';
|
|
36
|
+
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, ResolveClientTools, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue } from '@memberjunction/ai-core-plus';
|
|
30
37
|
import { AgentRunner } from './AgentRunner.js';
|
|
31
38
|
import { PayloadManager } from './PayloadManager.js';
|
|
32
39
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
@@ -219,6 +226,37 @@ export class BaseAgent {
|
|
|
219
226
|
* @since 2.123.0
|
|
220
227
|
*/
|
|
221
228
|
this._effectiveActions = [];
|
|
229
|
+
/**
|
|
230
|
+
* Effective sub-agents available to this agent after applying subAgentChanges — the sub-agent
|
|
231
|
+
* counterpart of {@link _effectiveActions}. Populated during gatherPromptTemplateData() and used
|
|
232
|
+
* for validation in {@link validateSubAgentNextStep} via {@link getEffectiveSubAgentsForValidation}.
|
|
233
|
+
* Without this, a sub-agent added at runtime (e.g. by Skill activation) would be advertised in the
|
|
234
|
+
* prompt catalog but rejected as "not found" when the agent tried to actually use it.
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
this._effectiveSubAgents = [];
|
|
238
|
+
/**
|
|
239
|
+
* IDs of skills already activated during this run. Prevents re-activation from re-appending
|
|
240
|
+
* the same instructions to context / re-pushing duplicate actionChanges/subAgentChanges entries
|
|
241
|
+
* when the LLM references an already-active skill again.
|
|
242
|
+
* @private
|
|
243
|
+
*/
|
|
244
|
+
this._activatedSkillIDs = [];
|
|
245
|
+
/**
|
|
246
|
+
* Whether Plan Mode is active for this run — resolved once in {@link initializeAgentRun} via
|
|
247
|
+
* {@link resolvePlanModeGate}. True only when `agent.SupportsPlanMode` (capability, default ON)
|
|
248
|
+
* AND `params.planMode` (per-request, default OFF) are both true AND this is a root agent.
|
|
249
|
+
* @private
|
|
250
|
+
*/
|
|
251
|
+
this._planModeActive = false;
|
|
252
|
+
/**
|
|
253
|
+
* Whether Plan Mode's approval gate has already been satisfied for this run — either because
|
|
254
|
+
* Plan Mode isn't active, or because a prior linked run's Plan step was approved. When active
|
|
255
|
+
* and NOT yet approved, `validateNextStep` blocks Actions/Sub-Agent steps until a Plan step
|
|
256
|
+
* has been presented and approved.
|
|
257
|
+
* @private
|
|
258
|
+
*/
|
|
259
|
+
this._planApproved = false;
|
|
222
260
|
/**
|
|
223
261
|
* Counts only prompt (LLM) executions, NOT all agent steps.
|
|
224
262
|
* Used for message expiration age calculations so that `expirationTurns`
|
|
@@ -230,6 +268,14 @@ export class BaseAgent {
|
|
|
230
268
|
* prompt execution occurred after them.
|
|
231
269
|
*/
|
|
232
270
|
this._promptTurnCount = 0;
|
|
271
|
+
/**
|
|
272
|
+
* Per-run config for structurally compressing inline action-result payloads.
|
|
273
|
+
* Resolved once at run start from the agent-type prompt params (default on);
|
|
274
|
+
* read by formatActionResultsAsMarkdown so both the direct and loop callers
|
|
275
|
+
* share the same setting without threading it through every signature.
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
this._actionResultCrush = undefined;
|
|
233
279
|
/**
|
|
234
280
|
* Execution limits for dynamically added actions.
|
|
235
281
|
* Maps action IDs to their MaxExecutionsPerRun limit.
|
|
@@ -256,6 +302,34 @@ export class BaseAgent {
|
|
|
256
302
|
* @private
|
|
257
303
|
*/
|
|
258
304
|
this.MAX_RECOVERY_ATTEMPTS = 1;
|
|
305
|
+
/**
|
|
306
|
+
* Drives a session-driven (Realtime) agent run end-to-end.
|
|
307
|
+
*
|
|
308
|
+
* Resolves the realtime model, assembles the session parameters (system prompt + memory/context),
|
|
309
|
+
* builds the {@link RealtimeSessionRunnerDeps} from this agent's collaborators, runs the
|
|
310
|
+
* {@link RealtimeSessionRunner}, and maps the result onto the finalized `AIAgentRun`.
|
|
311
|
+
*
|
|
312
|
+
* If no realtime model can be resolved (expected today, before the P3 drivers / P4 model
|
|
313
|
+
* metadata land), it finalizes the run as a clean FAILED result with an actionable message
|
|
314
|
+
* rather than throwing — a mis-provisioned environment must not crash the caller.
|
|
315
|
+
*
|
|
316
|
+
* @template R The caller's expected payload type (unused on the realtime path; the session
|
|
317
|
+
* produces transcript/usage rather than a structured payload).
|
|
318
|
+
* @param params The wrapped execution parameters.
|
|
319
|
+
* @param config The loaded agent configuration (provides the system prompt, if any).
|
|
320
|
+
* @returns The finalized {@link ExecuteAgentResult}.
|
|
321
|
+
*/
|
|
322
|
+
// ── Realtime per-session capture state (scoped to one executeRealtimeSession run) ──────────
|
|
323
|
+
/**
|
|
324
|
+
* In-flight realtime turn rows keyed by transcript role (`'user'`/`'assistant'`), driving the
|
|
325
|
+
* create-on-start / update-on-complete persistence lifecycle. Reset at the start of every
|
|
326
|
+
* realtime session so a prior run can never leak an in-flight id into the next.
|
|
327
|
+
*/
|
|
328
|
+
this.realtimeInFlightTurns = new Map();
|
|
329
|
+
/** Active audio recording controller for the current realtime session, or `null` when recording is off. */
|
|
330
|
+
this.realtimeRecording = null;
|
|
331
|
+
/** Storage account id the active recording stores to (RecordingStorageProviderID ?? AttachmentStorageProviderID). */
|
|
332
|
+
this.realtimeRecordingAccountId = null;
|
|
259
333
|
/**
|
|
260
334
|
* Storage for injected notes and examples to include in result
|
|
261
335
|
*/
|
|
@@ -427,6 +501,13 @@ export class BaseAgent {
|
|
|
427
501
|
* @private
|
|
428
502
|
*/
|
|
429
503
|
static { this.LARGE_BINARY_THRESHOLD = 10000; }
|
|
504
|
+
/**
|
|
505
|
+
* Minimum stringified length (chars) of an object/array action-result value before
|
|
506
|
+
* structural JSON compression (CrushJSON) is applied. Small payloads aren't worth a
|
|
507
|
+
* legend, so they pass through verbatim.
|
|
508
|
+
* @private
|
|
509
|
+
*/
|
|
510
|
+
static { this.ACTION_RESULT_CRUSH_THRESHOLD = 600; }
|
|
430
511
|
/**
|
|
431
512
|
* Inspects a set of action output params for any value matching the FileOutputRef shape
|
|
432
513
|
* (an object with `fileName`, `mimeType`, and either `fileData` or `fileId`).
|
|
@@ -1134,6 +1215,9 @@ export class BaseAgent {
|
|
|
1134
1215
|
this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig),
|
|
1135
1216
|
this.InjectPreExecutionRAG(typeof inputText === 'string' ? inputText : '', params.agent, params.contextUser, wrappedParams.conversationMessages, params.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, params.payload)
|
|
1136
1217
|
]);
|
|
1218
|
+
// Inject scope-resolved prompt parts (role-faithful) for this agent's prompt, alongside
|
|
1219
|
+
// memory/RAG. Synchronous — parts are cached on AIEngine. Uses the same run scope.
|
|
1220
|
+
this.InjectScopedPromptParts(params.agent, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes);
|
|
1137
1221
|
if (!config.success) {
|
|
1138
1222
|
this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
|
|
1139
1223
|
agent: params.agent,
|
|
@@ -1238,23 +1322,6 @@ export class BaseAgent {
|
|
|
1238
1322
|
isSessionDrivenAgentType(agentType) {
|
|
1239
1323
|
return agentType.IsSessionDriven === true;
|
|
1240
1324
|
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Drives a session-driven (Realtime) agent run end-to-end.
|
|
1243
|
-
*
|
|
1244
|
-
* Resolves the realtime model, assembles the session parameters (system prompt + memory/context),
|
|
1245
|
-
* builds the {@link RealtimeSessionRunnerDeps} from this agent's collaborators, runs the
|
|
1246
|
-
* {@link RealtimeSessionRunner}, and maps the result onto the finalized `AIAgentRun`.
|
|
1247
|
-
*
|
|
1248
|
-
* If no realtime model can be resolved (expected today, before the P3 drivers / P4 model
|
|
1249
|
-
* metadata land), it finalizes the run as a clean FAILED result with an actionable message
|
|
1250
|
-
* rather than throwing — a mis-provisioned environment must not crash the caller.
|
|
1251
|
-
*
|
|
1252
|
-
* @template R The caller's expected payload type (unused on the realtime path; the session
|
|
1253
|
-
* produces transcript/usage rather than a structured payload).
|
|
1254
|
-
* @param params The wrapped execution parameters.
|
|
1255
|
-
* @param config The loaded agent configuration (provides the system prompt, if any).
|
|
1256
|
-
* @returns The finalized {@link ExecuteAgentResult}.
|
|
1257
|
-
*/
|
|
1258
1325
|
async executeRealtimeSession(params, config) {
|
|
1259
1326
|
// 1) Resolve the realtime model (overridable seam — tests inject a mock).
|
|
1260
1327
|
const modelResolution = await this.resolveRealtimeModel(params);
|
|
@@ -1268,7 +1335,12 @@ export class BaseAgent {
|
|
|
1268
1335
|
}
|
|
1269
1336
|
// 2) Create the single long-lived AIPromptRun that usage is checkpointed onto.
|
|
1270
1337
|
const promptRun = await this.createRealtimePromptRun(params, config, modelResolution);
|
|
1271
|
-
// 3)
|
|
1338
|
+
// 3) Resolve recording (OFF by default; runtime > agent > off; consent + storage gated) and reset
|
|
1339
|
+
// the per-session turn-lifecycle state, then build the injected deps and run the session.
|
|
1340
|
+
this.realtimeInFlightTurns = new Map();
|
|
1341
|
+
const recording = await this.resolveRealtimeRecording(params);
|
|
1342
|
+
this.realtimeRecording = recording?.controller ?? null;
|
|
1343
|
+
this.realtimeRecordingAccountId = recording?.storageAccountId ?? null;
|
|
1272
1344
|
try {
|
|
1273
1345
|
const deps = await this.buildRealtimeSessionDeps(params, config, modelResolution, promptRun);
|
|
1274
1346
|
const runner = new RealtimeSessionRunner(deps);
|
|
@@ -1369,6 +1441,11 @@ export class BaseAgent {
|
|
|
1369
1441
|
UserID: params.contextUser?.ID,
|
|
1370
1442
|
DisableAutoResponse: meetingMode || undefined,
|
|
1371
1443
|
SelfNames: selfNames,
|
|
1444
|
+
// App awareness (Move 1/3/4): the app the session runs in (sources the app cascade layer +
|
|
1445
|
+
// RelevantAgents → allowed-agent union) and the live app-context snapshot injected at mint.
|
|
1446
|
+
// Both ride params.data, the same conduit async agents use for appContext.
|
|
1447
|
+
ApplicationID: params.data?.applicationId?.trim() || undefined,
|
|
1448
|
+
AppContext: params.data?.appContext,
|
|
1372
1449
|
};
|
|
1373
1450
|
}
|
|
1374
1451
|
/**
|
|
@@ -1559,6 +1636,8 @@ export class BaseAgent {
|
|
|
1559
1636
|
DelegateToTarget: (request) => this.delegateRealtimeToTarget(params, config, request),
|
|
1560
1637
|
ExecuteTool: (call) => this.executeRealtimeTool(params, call),
|
|
1561
1638
|
PersistTranscript: (transcript) => this.persistRealtimeTranscript(params, transcript),
|
|
1639
|
+
Recording: this.realtimeRecording ?? undefined,
|
|
1640
|
+
FinalizeRecording: () => this.finalizeRealtimeRecording(params),
|
|
1562
1641
|
CheckpointUsage: (usage) => this.checkpointRealtimeUsage(promptRun, usage),
|
|
1563
1642
|
// DB-driven spoken-progress wording (shared lookup with the client-direct path);
|
|
1564
1643
|
// null → the runner's documented built-in first-person fallback.
|
|
@@ -1800,34 +1879,211 @@ export class BaseAgent {
|
|
|
1800
1879
|
return {};
|
|
1801
1880
|
}
|
|
1802
1881
|
/**
|
|
1803
|
-
* Persists a
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1882
|
+
* Persists a realtime transcript turn as a `ConversationDetail` with a **create-on-start /
|
|
1883
|
+
* update-on-complete** lifecycle, so each turn carries both a start (`__mj_CreatedAt`) and an
|
|
1884
|
+
* immutable end (`TurnEndedAt`):
|
|
1885
|
+
* - **Interim** (`IsFinal=false`): on the FIRST delta for a role, CREATE the row with
|
|
1886
|
+
* `Status='In-Progress'` (so a live UI can show the turn streaming), stamping the recording-relative
|
|
1887
|
+
* `UtteranceStartMs` and the speaker `UserID` (user turns only). Subsequent interim deltas are no-ops.
|
|
1888
|
+
* - **Final** (`IsFinal=true`): UPDATE that in-flight row with the full text, `Status='Complete'`,
|
|
1889
|
+
* `TurnEndedAt`, and `UtteranceEndMs`. If no interim was seen (some providers only emit final), the
|
|
1890
|
+
* row is created and finalized in one step.
|
|
1891
|
+
*
|
|
1892
|
+
* Returns the new row's ID the first time a DISTINCT turn is created, and `null` when an existing
|
|
1893
|
+
* in-flight row is merely updated — the runner uses that to count turns (not events). User turns are
|
|
1894
|
+
* `Role='User'`, assistant turns `Role='AI'`. When recording is active, `MediaType='Audio'` and the
|
|
1895
|
+
* media-relative utterance offsets are stamped from the recording clock.
|
|
1806
1896
|
*
|
|
1807
|
-
* @param params The execution parameters (provides conversation id + context user).
|
|
1808
|
-
* @param transcript The transcript turn emitted by the model.
|
|
1897
|
+
* @param params The execution parameters (provides conversation id + context user + session id).
|
|
1898
|
+
* @param transcript The transcript turn (interim delta or final) emitted by the model.
|
|
1899
|
+
* @returns The created row id on first creation of a turn, else `null`.
|
|
1809
1900
|
*/
|
|
1810
1901
|
async persistRealtimeTranscript(params, transcript) {
|
|
1811
|
-
if (!transcript.
|
|
1812
|
-
return;
|
|
1902
|
+
if (!transcript.Text?.trim()) {
|
|
1903
|
+
return null;
|
|
1813
1904
|
}
|
|
1814
1905
|
const conversationID = params.data?.conversationId;
|
|
1815
1906
|
if (!conversationID) {
|
|
1816
|
-
return; // Without a conversation we have nowhere to durably attach the turn.
|
|
1907
|
+
return null; // Without a conversation we have nowhere to durably attach the turn.
|
|
1817
1908
|
}
|
|
1818
1909
|
const md = params.provider || this._activeProvider;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1910
|
+
const roleKey = transcript.Role; // 'user' | 'assistant'
|
|
1911
|
+
const mjRole = transcript.Role === 'user' ? 'User' : 'AI';
|
|
1912
|
+
// ── INTERIM: create the In-Progress row once per turn (first delta) ───────────────────────
|
|
1913
|
+
if (!transcript.IsFinal) {
|
|
1914
|
+
if (this.realtimeInFlightTurns.has(roleKey)) {
|
|
1915
|
+
return null; // already created for this turn; ignore subsequent deltas
|
|
1916
|
+
}
|
|
1917
|
+
const detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1918
|
+
detail.NewRecord();
|
|
1919
|
+
detail.ConversationID = conversationID;
|
|
1920
|
+
detail.Role = mjRole;
|
|
1921
|
+
detail.Message = transcript.Text;
|
|
1922
|
+
detail.Status = 'In-Progress';
|
|
1923
|
+
this.applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, /*atStart*/ true);
|
|
1924
|
+
if (params.agentSessionID) {
|
|
1925
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1926
|
+
}
|
|
1927
|
+
if (!await detail.Save()) {
|
|
1928
|
+
this.logError(`Failed to create in-progress realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1929
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1930
|
+
});
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
this.realtimeInFlightTurns.set(roleKey, detail.ID);
|
|
1934
|
+
return detail.ID;
|
|
1935
|
+
}
|
|
1936
|
+
// ── FINAL: update the in-flight row (or create+finalize when no interim was seen) ─────────
|
|
1937
|
+
const inFlightId = this.realtimeInFlightTurns.get(roleKey);
|
|
1938
|
+
this.realtimeInFlightTurns.delete(roleKey);
|
|
1939
|
+
let detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1940
|
+
let created = false;
|
|
1941
|
+
if (inFlightId && await detail.Load(inFlightId)) {
|
|
1942
|
+
// updating the existing streaming row → not a new turn
|
|
1943
|
+
}
|
|
1944
|
+
else {
|
|
1945
|
+
detail.NewRecord();
|
|
1946
|
+
detail.ConversationID = conversationID;
|
|
1947
|
+
detail.Role = mjRole;
|
|
1948
|
+
this.applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, /*atStart*/ true);
|
|
1949
|
+
if (params.agentSessionID) {
|
|
1950
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1951
|
+
}
|
|
1952
|
+
created = true;
|
|
1953
|
+
}
|
|
1823
1954
|
detail.Message = transcript.Text;
|
|
1824
|
-
|
|
1825
|
-
|
|
1955
|
+
detail.Status = 'Complete';
|
|
1956
|
+
detail.TurnEndedAt = new Date();
|
|
1957
|
+
if (this.realtimeRecording) {
|
|
1958
|
+
detail.UtteranceEndMs = this.realtimeRecording.NowOffsetMs();
|
|
1826
1959
|
}
|
|
1827
1960
|
if (!await detail.Save()) {
|
|
1828
|
-
this.logError(`Failed to
|
|
1829
|
-
agent: params.agent,
|
|
1830
|
-
|
|
1961
|
+
this.logError(`Failed to finalize realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1962
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return created ? detail.ID : null;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Stamps the speaker identity and recording-relative media fields on a freshly-created turn row.
|
|
1969
|
+
* `UserID` is set only for **user** turns (an AI turn has no human speaker). When recording is
|
|
1970
|
+
* active, `MediaType='Audio'` and `UtteranceStartMs` is captured from the recording clock.
|
|
1971
|
+
*
|
|
1972
|
+
* @param detail The new conversation-detail row.
|
|
1973
|
+
* @param transcript The transcript turn.
|
|
1974
|
+
* @param params The execution parameters.
|
|
1975
|
+
* @param atStart Whether this is the turn's start (stamps `UtteranceStartMs`).
|
|
1976
|
+
*/
|
|
1977
|
+
applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, atStart) {
|
|
1978
|
+
if (transcript.Role === 'user' && params.contextUser?.ID) {
|
|
1979
|
+
detail.UserID = params.contextUser.ID;
|
|
1980
|
+
}
|
|
1981
|
+
if (this.realtimeRecording) {
|
|
1982
|
+
detail.MediaType = 'Audio';
|
|
1983
|
+
if (atStart) {
|
|
1984
|
+
detail.UtteranceStartMs = this.realtimeRecording.NowOffsetMs();
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Resolves whether to record this realtime session, OFF by default, with the precedence
|
|
1990
|
+
* **runtime param > agent (`RecordingDefault`) > off**, hard-gated by consent and a resolvable
|
|
1991
|
+
* storage provider. Returns the recording controller + the resolved storage account, or `null`
|
|
1992
|
+
* to record nothing (fail-closed). Never throws — any resolution problem disables recording.
|
|
1993
|
+
*
|
|
1994
|
+
* Storage resolves to **`AIAgent.RecordingStorageProviderID` ?? `AIAgent.AttachmentStorageProviderID`**
|
|
1995
|
+
* (recordings default to the attachments account), then to that provider's first account. With no
|
|
1996
|
+
* provider configured, or consent not granted, recording is OFF.
|
|
1997
|
+
*
|
|
1998
|
+
* @param params The execution parameters (agent + runtime `data.recording`).
|
|
1999
|
+
* @returns `{ controller, storageAccountId }` when recording is enabled, else `null`.
|
|
2000
|
+
*/
|
|
2001
|
+
async resolveRealtimeRecording(params) {
|
|
2002
|
+
try {
|
|
2003
|
+
const agent = params.agent;
|
|
2004
|
+
const runtime = (params.data?.recording ?? null);
|
|
2005
|
+
// Media: runtime > agent default > off.
|
|
2006
|
+
const rawMedia = runtime?.media ?? agent.RecordingDefault ?? 'None';
|
|
2007
|
+
const media = rawMedia === 'Audio' || rawMedia === 'AudioVideo' ? rawMedia : 'None';
|
|
2008
|
+
if (media === 'None') {
|
|
2009
|
+
return null; // recording off
|
|
2010
|
+
}
|
|
2011
|
+
// Consent is a HARD gate — never record without explicit consent.
|
|
2012
|
+
if (runtime?.consent !== true) {
|
|
2013
|
+
this.logStatus('🔴 Realtime recording requested but consent was not granted — recording disabled.', false, params);
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
// Storage: recording provider, else attachment provider; then that provider's first account.
|
|
2017
|
+
const storageAccountId = params.contextUser
|
|
2018
|
+
? await resolveRecordingStorageAccountID(agent, params.contextUser, params.provider || this._activeProvider)
|
|
2019
|
+
: null;
|
|
2020
|
+
if (!storageAccountId) {
|
|
2021
|
+
this.logStatus('🔴 Realtime recording on but no resolvable storage account (RecordingStorageProviderID/AttachmentStorageProviderID) — recording disabled.', false, params);
|
|
2022
|
+
return null;
|
|
2023
|
+
}
|
|
2024
|
+
const controller = new RealtimeRecordingController({ Media: media });
|
|
2025
|
+
return { controller, storageAccountId };
|
|
2026
|
+
}
|
|
2027
|
+
catch (error) {
|
|
2028
|
+
this.logError(`Failed to resolve realtime recording (recording disabled): ${error instanceof Error ? error.message : String(error)}`, {
|
|
2029
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
2030
|
+
});
|
|
2031
|
+
return null;
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Finalizes the active recording after the session closes: encodes the captured audio to a WAV,
|
|
2036
|
+
* stores it via MJStorage to the resolved account, links it to the `AIAgentSession` (via
|
|
2037
|
+
* `MJ: File Entity Record Links`), and stamps `RecordingFileID` / `RecordingMedia` /
|
|
2038
|
+
* `RecordingStartedAt` on the session. Never throws — a recording failure must not fail the
|
|
2039
|
+
* session run. No-op when recording is off, nothing was captured, or there is no session id.
|
|
2040
|
+
*
|
|
2041
|
+
* @param params The execution parameters (provides the session id + context user + provider).
|
|
2042
|
+
*/
|
|
2043
|
+
async finalizeRealtimeRecording(params) {
|
|
2044
|
+
const controller = this.realtimeRecording;
|
|
2045
|
+
if (!controller) {
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
// One-shot: clear instance state up front so a re-entrant/duplicate Stop can't double-store.
|
|
2049
|
+
this.realtimeRecording = null;
|
|
2050
|
+
const storageAccountId = this.realtimeRecordingAccountId;
|
|
2051
|
+
this.realtimeRecordingAccountId = null;
|
|
2052
|
+
try {
|
|
2053
|
+
controller.Stop();
|
|
2054
|
+
const sessionID = params.agentSessionID;
|
|
2055
|
+
const contextUser = params.contextUser;
|
|
2056
|
+
if (!sessionID || !storageAccountId || !contextUser) {
|
|
2057
|
+
return; // nowhere to attach / store (or no user context to store under)
|
|
2058
|
+
}
|
|
2059
|
+
const encoded = controller.EncodeWav();
|
|
2060
|
+
if (!encoded) {
|
|
2061
|
+
this.logStatus('🔇 Realtime session produced no audio to record.', true, params);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const md = params.provider || this._activeProvider;
|
|
2065
|
+
// Capture-time waveform peaks (max-abs per bucket, normalized 0..1) computed from the
|
|
2066
|
+
// same mixed PCM as the WAV — persisted as a peaks.json sidecar so the player renders the
|
|
2067
|
+
// real waveform without re-decoding the audio. Best-effort: an empty array writes no sidecar.
|
|
2068
|
+
const peaks = controller.GetPeaks();
|
|
2069
|
+
const fileID = await storeRealtimeRecording({
|
|
2070
|
+
Audio: encoded.Buffer,
|
|
2071
|
+
MimeType: 'audio/wav',
|
|
2072
|
+
Media: controller.Media,
|
|
2073
|
+
StartedAt: controller.StartedAt ?? new Date(),
|
|
2074
|
+
StorageAccountID: storageAccountId,
|
|
2075
|
+
SessionID: sessionID,
|
|
2076
|
+
ContextUser: contextUser,
|
|
2077
|
+
Provider: md,
|
|
2078
|
+
Peaks: peaks.length > 0 ? peaks : undefined
|
|
2079
|
+
});
|
|
2080
|
+
if (fileID) {
|
|
2081
|
+
this.logStatus(`🎬 Realtime recording stored (${Math.round(encoded.DurationMs / 1000)}s, file ${fileID}).`, true, params);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
catch (error) {
|
|
2085
|
+
this.logError(`Failed to finalize realtime recording: ${error instanceof Error ? error.message : String(error)}`, {
|
|
2086
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1831
2087
|
});
|
|
1832
2088
|
}
|
|
1833
2089
|
}
|
|
@@ -2093,6 +2349,32 @@ export class BaseAgent {
|
|
|
2093
2349
|
this._injectedMemory = result;
|
|
2094
2350
|
return result;
|
|
2095
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Inject this agent's scoped prompt parts into the conversation, role-faithfully.
|
|
2354
|
+
*
|
|
2355
|
+
* Parallels {@link InjectContextMemory}: resolves `MJ: Scoped Prompt Parts` for the agent's
|
|
2356
|
+
* primary prompt under the run's polymorphic scope (the SAME PrimaryScope/SecondaryScopes the
|
|
2357
|
+
* runtime threads for memory), and unshifts the assembled role-tagged messages onto
|
|
2358
|
+
* `conversationMessages`. In-memory + synchronous (parts are cached on `AIEngine`). No-op when
|
|
2359
|
+
* the agent has no active prompt or no parts resolve for the scope.
|
|
2360
|
+
*/
|
|
2361
|
+
InjectScopedPromptParts(agent, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes) {
|
|
2362
|
+
try {
|
|
2363
|
+
const prompts = AIEngine.Instance.AgentPrompts
|
|
2364
|
+
.filter(ap => UUIDsEqual(ap.AgentID, agent.ID) && ap.Status === 'Active')
|
|
2365
|
+
.sort((a, b) => (a.ExecutionOrder ?? 0) - (b.ExecutionOrder ?? 0));
|
|
2366
|
+
if (prompts.length === 0)
|
|
2367
|
+
return;
|
|
2368
|
+
// Obtain the (possibly downstream-overridden) resolver via the class factory, so any
|
|
2369
|
+
// consumer can plug in custom inclusion/scope logic by subclassing PromptComponentResolver.
|
|
2370
|
+
const resolver = MJGlobal.Instance.ClassFactory.CreateInstance(PromptComponentResolver) ??
|
|
2371
|
+
new PromptComponentResolver();
|
|
2372
|
+
InjectScopedPromptParts(resolver, prompts[0].PromptID, { primaryScopeEntityId, primaryScopeRecordId, secondaryScopes }, conversationMessages);
|
|
2373
|
+
}
|
|
2374
|
+
catch (e) {
|
|
2375
|
+
this.logError(e instanceof Error ? e : new Error(String(e)), { category: 'ScopedPromptParts' });
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2096
2378
|
/**
|
|
2097
2379
|
* Inject pre-execution RAG context for this agent using scoped search.
|
|
2098
2380
|
*
|
|
@@ -2608,6 +2890,20 @@ export class BaseAgent {
|
|
|
2608
2890
|
* @returns
|
|
2609
2891
|
*/
|
|
2610
2892
|
async validateNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
2893
|
+
// Plan Mode enforcement: while active and not yet approved, block Actions/Sub-Agent so the
|
|
2894
|
+
// agent cannot skip straight to execution — it must present a Plan first. Chat, Retry,
|
|
2895
|
+
// Skill activation, ForEach/While, and ClientTools are all still allowed (e.g. asking a
|
|
2896
|
+
// clarifying question, or loading a skill's instructions, before forming the plan).
|
|
2897
|
+
if (this._planModeActive && !this._planApproved && (nextStep.step === 'Actions' || nextStep.step === 'Sub-Agent')) {
|
|
2898
|
+
// nextStep.step is narrowed to 'Actions' | 'Sub-Agent' here, so it can never already be
|
|
2899
|
+
// 'Retry' — always increment (we're demoting it to Retry from a non-retry step).
|
|
2900
|
+
this._generalValidationRetryCount++;
|
|
2901
|
+
return {
|
|
2902
|
+
step: 'Retry',
|
|
2903
|
+
terminate: false,
|
|
2904
|
+
errorMessage: 'Plan mode is active for this request. Present your plan first via a "Plan" next step and wait for approval before executing actions or sub-agents.'
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2611
2907
|
// for next step, let's do a little quick validation here for sub-agent and actions to ensure requests are valid
|
|
2612
2908
|
switch (nextStep.step) {
|
|
2613
2909
|
case 'Sub-Agent':
|
|
@@ -2628,6 +2924,15 @@ export class BaseAgent {
|
|
|
2628
2924
|
case 'While':
|
|
2629
2925
|
// While loops are valid - no additional validation needed
|
|
2630
2926
|
return nextStep;
|
|
2927
|
+
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
2928
|
+
// union (it's non-terminal, like 'ClientTools' — see the type's doc comment).
|
|
2929
|
+
case 'Skill':
|
|
2930
|
+
return this.validateSkillNextStep(params, nextStep, currentPayload, agentRun, currentStep);
|
|
2931
|
+
// Type assertion required because 'Plan' is not part of the BaseAgentNextStep step
|
|
2932
|
+
// union (it's non-terminal — the terminal step it produces is 'Chat', see
|
|
2933
|
+
// executePlanStep's doc comment for why).
|
|
2934
|
+
case 'Plan':
|
|
2935
|
+
return this.validatePlanNextStep(params, nextStep, currentPayload, agentRun, currentStep);
|
|
2631
2936
|
case 'ClientTools':
|
|
2632
2937
|
// Client tools are valid - execution handled by executeClientToolsStep
|
|
2633
2938
|
return nextStep;
|
|
@@ -2670,7 +2975,7 @@ export class BaseAgent {
|
|
|
2670
2975
|
return nextStep.subAgent ? [nextStep.subAgent] : [];
|
|
2671
2976
|
}
|
|
2672
2977
|
async validateSubAgentNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
2673
|
-
const curAgentSubAgents =
|
|
2978
|
+
const curAgentSubAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
2674
2979
|
// Collect requested sub-agents. Prefer plural `subAgents` (parallel fan-out);
|
|
2675
2980
|
// fall back to singular `subAgent` for the classic single-sub-agent next step.
|
|
2676
2981
|
const requested = this.getRequestedSubAgents(nextStep);
|
|
@@ -2849,6 +3154,95 @@ export class BaseAgent {
|
|
|
2849
3154
|
const agentActions = AIEngine.Instance.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, agentId) && aa.Status === 'Active');
|
|
2850
3155
|
return ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)) && a.Status === 'Active');
|
|
2851
3156
|
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Gets the effective sub-agents for validation, using runtime subAgentChanges if available.
|
|
3159
|
+
* Falls back to the database-configured relationship set if _effectiveSubAgents is empty.
|
|
3160
|
+
* Mirrors {@link getEffectiveActionsForValidation}.
|
|
3161
|
+
*
|
|
3162
|
+
* @param agentId - The ID of the agent to get sub-agents for
|
|
3163
|
+
* @returns Array of effective sub-agents available to the agent
|
|
3164
|
+
* @protected
|
|
3165
|
+
*/
|
|
3166
|
+
getEffectiveSubAgentsForValidation(agentId) {
|
|
3167
|
+
if (this._effectiveSubAgents.length > 0) {
|
|
3168
|
+
return this._effectiveSubAgents;
|
|
3169
|
+
}
|
|
3170
|
+
// Fallback: compute from database configuration (ParentID children + AgentRelationships)
|
|
3171
|
+
return AIEngine.Instance.GetSubAgents(agentId, 'Active');
|
|
3172
|
+
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Validates that the requested skill(s) are known and allowed for this agent (resolved via
|
|
3175
|
+
* {@link AIEngine.GetSkillsForAgent}, which enforces the agent's AcceptsSkills gate + the
|
|
3176
|
+
* catalog/grant Status chain). Subclasses can override to implement custom validation logic.
|
|
3177
|
+
*
|
|
3178
|
+
* Mirrors {@link validateActionsNextStep}'s fuzzy-name-matching UX: an exact case-insensitive
|
|
3179
|
+
* match is tried first, falling back to a CONTAINS match when exactly one candidate matches.
|
|
3180
|
+
*
|
|
3181
|
+
* @protected
|
|
3182
|
+
*/
|
|
3183
|
+
async validateSkillNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
3184
|
+
const requested = nextStep.skillActivations ?? [];
|
|
3185
|
+
if (requested.length === 0) {
|
|
3186
|
+
if (nextStep.step !== 'Retry') {
|
|
3187
|
+
this._generalValidationRetryCount++;
|
|
3188
|
+
}
|
|
3189
|
+
return {
|
|
3190
|
+
step: 'Retry',
|
|
3191
|
+
terminate: false,
|
|
3192
|
+
errorMessage: 'When activating a skill, 1 or more skills must be specified'
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
const availableSkills = AIEngine.Instance.GetSkillsForAgent(params.agent, params.contextUser);
|
|
3196
|
+
const missingSkills = requested.filter(req => {
|
|
3197
|
+
const requestedName = req.name.trim().toLowerCase();
|
|
3198
|
+
const exactMatch = availableSkills.find(s => s.Name.trim().toLowerCase() === requestedName);
|
|
3199
|
+
if (exactMatch)
|
|
3200
|
+
return false;
|
|
3201
|
+
const containsMatches = availableSkills.filter(s => s.Name.trim().toLowerCase().includes(requestedName));
|
|
3202
|
+
if (containsMatches.length === 1) {
|
|
3203
|
+
this.logStatus(`Skill name fuzzy matched: '${req.name}' → '${containsMatches[0].Name}'`, true, params);
|
|
3204
|
+
req.name = containsMatches[0].Name;
|
|
3205
|
+
return false;
|
|
3206
|
+
}
|
|
3207
|
+
return true;
|
|
3208
|
+
});
|
|
3209
|
+
if (missingSkills.length > 0) {
|
|
3210
|
+
const missingNames = missingSkills.map(s => s.name).join(', ');
|
|
3211
|
+
const availableNames = availableSkills.map(s => s.Name).join(', ') || '(none)';
|
|
3212
|
+
this.logError(`Skill(s) '${missingNames}' not found or not available for agent '${params.agent.Name}'. Available: ${availableNames}`, {
|
|
3213
|
+
agent: params.agent,
|
|
3214
|
+
category: 'SkillExecution'
|
|
3215
|
+
});
|
|
3216
|
+
if (nextStep.step !== 'Retry') {
|
|
3217
|
+
this._generalValidationRetryCount++;
|
|
3218
|
+
}
|
|
3219
|
+
return {
|
|
3220
|
+
step: 'Retry',
|
|
3221
|
+
terminate: false,
|
|
3222
|
+
errorMessage: `Skill(s) '${missingNames}' not found or not available. Available: ${availableNames}`
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
return nextStep;
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Validates that a 'Plan' next step (Plan Mode) has plan text to present. Subclasses can
|
|
3229
|
+
* override to add additional plan-quality checks (e.g. minimum length, required sections).
|
|
3230
|
+
*
|
|
3231
|
+
* @protected
|
|
3232
|
+
*/
|
|
3233
|
+
async validatePlanNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
3234
|
+
if (!nextStep.planDetails?.plan || nextStep.planDetails.plan.trim().length === 0) {
|
|
3235
|
+
if (nextStep.step !== 'Retry') {
|
|
3236
|
+
this._generalValidationRetryCount++;
|
|
3237
|
+
}
|
|
3238
|
+
return {
|
|
3239
|
+
step: 'Retry',
|
|
3240
|
+
terminate: false,
|
|
3241
|
+
errorMessage: 'Plan text is required when presenting a Plan for approval'
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
return nextStep;
|
|
3245
|
+
}
|
|
2852
3246
|
/**
|
|
2853
3247
|
* Validates that the Success next step is valid and can be executed by the current agent. Subclasses can override
|
|
2854
3248
|
* this method to implement custom validation logic if needed.
|
|
@@ -4367,6 +4761,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4367
4761
|
}
|
|
4368
4762
|
// Store for later validation in executeActionsStep
|
|
4369
4763
|
this._effectiveActions = activeActions;
|
|
4764
|
+
// Store for later validation in validateSubAgentNextStep (see getEffectiveSubAgentsForValidation)
|
|
4765
|
+
this._effectiveSubAgents = uniqueActiveSubAgents;
|
|
4370
4766
|
// Agent type prompt params: reuse cached base merge unless a runtime override is present.
|
|
4371
4767
|
const runtimePromptParamOverrides = extraData?.__agentTypePromptParams;
|
|
4372
4768
|
let agentTypePromptParams;
|
|
@@ -4385,6 +4781,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4385
4781
|
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
4386
4782
|
// Build app context section if provided in extraData
|
|
4387
4783
|
const appContext = this.buildAppContextSection(extraData);
|
|
4784
|
+
// Skill catalog (name + description only — progressive disclosure). Empty for
|
|
4785
|
+
// AcceptsSkills='None' since GetSkillsForAgent already returns [] in that case.
|
|
4786
|
+
// Filtered by the acting user's Run permission (open-by-default) so the agent is
|
|
4787
|
+
// never even offered a skill the user isn't entitled to — the permission boundary
|
|
4788
|
+
// is enforced at the catalog, not just at activation.
|
|
4789
|
+
const availableSkills = engine.GetSkillsForAgent(agent, _contextUser);
|
|
4790
|
+
const skillsCatalog = this.formatSkillsCatalog(availableSkills);
|
|
4388
4791
|
const contextData = {
|
|
4389
4792
|
agentName: agent.Name,
|
|
4390
4793
|
agentDescription: agent.Description,
|
|
@@ -4394,6 +4797,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4394
4797
|
actionCount: activeActions.length,
|
|
4395
4798
|
actionDetails: actionDetails,
|
|
4396
4799
|
clientToolDetails: clientToolDetails,
|
|
4800
|
+
skillCount: availableSkills.length,
|
|
4801
|
+
skillsCatalog: skillsCatalog,
|
|
4802
|
+
planModeActive: this._planModeActive,
|
|
4803
|
+
planApproved: this._planApproved,
|
|
4397
4804
|
appContext: appContext,
|
|
4398
4805
|
};
|
|
4399
4806
|
// Build the final result with __agentTypePromptParams injected
|
|
@@ -4925,6 +5332,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4925
5332
|
return line;
|
|
4926
5333
|
}).join('\n');
|
|
4927
5334
|
}
|
|
5335
|
+
/**
|
|
5336
|
+
* Formats the skill CATALOG as compact markdown — name + description ONLY. This is
|
|
5337
|
+
* progressive disclosure by design: the LLM sees just enough to decide whether to activate a
|
|
5338
|
+
* skill (via a 'Skill' next step), but never sees `Instructions` until it does. Instructions
|
|
5339
|
+
* are appended separately in {@link buildSkillActivationMessage} on activation.
|
|
5340
|
+
*
|
|
5341
|
+
* @private
|
|
5342
|
+
*/
|
|
5343
|
+
formatSkillsCatalog(skills) {
|
|
5344
|
+
return skills.map(s => `- **${s.Name}** — ${s.Description ?? '(no description)'}`).join('\n');
|
|
5345
|
+
}
|
|
4928
5346
|
/**
|
|
4929
5347
|
* Utility method to get agent prompt parameters for a given agent. This gets the
|
|
4930
5348
|
* highest priority prompt for the agent, and then gets the parameters for that
|
|
@@ -5000,44 +5418,41 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5000
5418
|
/**
|
|
5001
5419
|
* Build the client tool prompt section for system prompt injection.
|
|
5002
5420
|
*
|
|
5003
|
-
*
|
|
5004
|
-
*
|
|
5005
|
-
*
|
|
5006
|
-
*
|
|
5421
|
+
* Resolution is delegated to the shared, tier-agnostic {@link ResolveClientTools}
|
|
5422
|
+
* (`@memberjunction/ai-core-plus`) — the single source of truth used by the async
|
|
5423
|
+
* path (here), the realtime co-agent broker, and the conversations runtime. Tiers,
|
|
5424
|
+
* highest precedence first:
|
|
5425
|
+
*
|
|
5426
|
+
* 1. **override** — tools passed directly in the run's `data.clientTools`
|
|
5427
|
+
* 2. **session (dynamic)** — client-SDK enriched tools from {@link ClientToolRequestManager}
|
|
5428
|
+
* 3. **app** — tools the active surface published in the app-context capability manifest
|
|
5429
|
+
* 4. **static** — the agent's metadata tools from the `AI Agent Client Tools` junction
|
|
5430
|
+
*
|
|
5431
|
+
* NOTE (behavior change): the previous inline merge resolved *static-wins* (metadata
|
|
5432
|
+
* was added first and won name collisions). The unified resolver uses the more-correct
|
|
5433
|
+
* *override > session > app > static* — a runtime/dynamic tool now overrides a stale
|
|
5434
|
+
* static metadata tool of the same name. Collisions are rare in practice.
|
|
5007
5435
|
*/
|
|
5008
5436
|
buildClientToolPromptSection(agent, extraData) {
|
|
5009
|
-
|
|
5010
|
-
// 1. Metadata tools from junction table (authoritative source)
|
|
5437
|
+
// Static tier — agent's metadata tools from the AI Agent Client Tools junction.
|
|
5011
5438
|
const engine = AIEngine.Instance;
|
|
5012
|
-
const
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
});
|
|
5022
|
-
}
|
|
5023
|
-
// 2. Session-level enriched tools (client SDK decorated tools)
|
|
5439
|
+
const staticTools = engine.GetClientToolsForAgent(agent.ID).map(tool => ({
|
|
5440
|
+
Name: tool.Name,
|
|
5441
|
+
Description: tool.Description,
|
|
5442
|
+
InputSchema: tool.InputSchemaJSON ? JSON.parse(tool.InputSchemaJSON) : {},
|
|
5443
|
+
OutputSchema: tool.OutputSchemaJSON ? JSON.parse(tool.OutputSchemaJSON) : undefined,
|
|
5444
|
+
Category: tool.Category || undefined,
|
|
5445
|
+
DefaultTimeoutMs: tool.DefaultTimeoutMs || undefined
|
|
5446
|
+
}));
|
|
5447
|
+
// Dynamic (session) tier — client-SDK enriched tools for this session.
|
|
5024
5448
|
const sessionID = extraData?.sessionID;
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
}
|
|
5032
|
-
// 3. Runtime extraData override
|
|
5033
|
-
if (extraData?.clientTools) {
|
|
5034
|
-
for (const tool of extraData.clientTools) {
|
|
5035
|
-
if (!toolMap.has(tool.Name)) {
|
|
5036
|
-
toolMap.set(tool.Name, tool);
|
|
5037
|
-
}
|
|
5038
|
-
}
|
|
5039
|
-
}
|
|
5040
|
-
const tools = Array.from(toolMap.values());
|
|
5449
|
+
const sessionTools = sessionID ? ClientToolRequestManager.Instance.GetSessionTools(sessionID) : [];
|
|
5450
|
+
// App tier — tools the active surface published in the app-context capability manifest.
|
|
5451
|
+
const appContext = extraData?.appContext;
|
|
5452
|
+
const appTools = appContext?.Capabilities?.Tools ?? [];
|
|
5453
|
+
// Override tier — tools passed directly in the run's data.
|
|
5454
|
+
const overrideTools = extraData?.clientTools ?? [];
|
|
5455
|
+
const tools = ResolveClientTools({ agentId: agent.ID, staticTools, sessionTools, appTools, overrideTools });
|
|
5041
5456
|
if (tools.length === 0) {
|
|
5042
5457
|
return ''; // No client tools available
|
|
5043
5458
|
}
|
|
@@ -5234,19 +5649,112 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5234
5649
|
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
5235
5650
|
return `\`${String(value)}\``;
|
|
5236
5651
|
}
|
|
5237
|
-
let stringValue;
|
|
5238
5652
|
if (typeof value === 'string') {
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
//
|
|
5243
|
-
|
|
5653
|
+
// A string param may carry JSON (many actions JSON.stringify their payloads),
|
|
5654
|
+
// SQL/TS code, or plain text. Try structural JSON compression first — it is a
|
|
5655
|
+
// safe no-op on non-JSON (crushParamValue's internal JSON.parse failure is
|
|
5656
|
+
// caught and returns null) — then opt-in AST code reduction (SQL/TS), then pass
|
|
5657
|
+
// through (optionally length-capped).
|
|
5658
|
+
const crushedJson = this.crushParamValue(value);
|
|
5659
|
+
if (crushedJson !== null) {
|
|
5660
|
+
return crushedJson;
|
|
5661
|
+
}
|
|
5662
|
+
const crushedCode = this.crushCodeValue(value);
|
|
5663
|
+
if (crushedCode !== null) {
|
|
5664
|
+
return crushedCode;
|
|
5665
|
+
}
|
|
5666
|
+
return maxLength > 0 && value.length > maxLength ? `${value.substring(0, maxLength)}…` : value;
|
|
5667
|
+
}
|
|
5668
|
+
// Objects/arrays: compact JSON (no pretty-printing), optionally structurally
|
|
5669
|
+
// compressed via context-crush when crushing is enabled and the value is large.
|
|
5670
|
+
const stringValue = JSON.stringify(value);
|
|
5671
|
+
const crushed = this.crushParamValue(stringValue);
|
|
5672
|
+
if (crushed !== null) {
|
|
5673
|
+
return crushed;
|
|
5244
5674
|
}
|
|
5245
5675
|
if (maxLength > 0 && stringValue.length > maxLength) {
|
|
5246
5676
|
return `${stringValue.substring(0, maxLength)}…`;
|
|
5247
5677
|
}
|
|
5248
5678
|
return stringValue;
|
|
5249
5679
|
}
|
|
5680
|
+
/**
|
|
5681
|
+
* Resolve the per-run action-result compression config from the agent-type prompt
|
|
5682
|
+
* params. Crushing is on by default and only disabled when an agent explicitly sets
|
|
5683
|
+
* `crushActionResults: false`, mirroring the `includeXxxDocs` opt-out convention.
|
|
5684
|
+
* @private
|
|
5685
|
+
*/
|
|
5686
|
+
resolveActionResultCrush(params) {
|
|
5687
|
+
const agentTypePromptParams = params.data?.__agentTypePromptParams;
|
|
5688
|
+
if (agentTypePromptParams?.crushActionResults === false) {
|
|
5689
|
+
return undefined;
|
|
5690
|
+
}
|
|
5691
|
+
const requestedLang = agentTypePromptParams?.crushCodeLang;
|
|
5692
|
+
const codeLang = requestedLang === 'sql' || requestedLang === 'typescript' ? requestedLang : undefined;
|
|
5693
|
+
return { threshold: BaseAgent.ACTION_RESULT_CRUSH_THRESHOLD, maxChars: undefined, codeLang };
|
|
5694
|
+
}
|
|
5695
|
+
/**
|
|
5696
|
+
* AST-reduce a large code-string action-result value when the agent opted into a code
|
|
5697
|
+
* language (via `crushCodeLang` or a subclass override) and the value clears the size
|
|
5698
|
+
* threshold. Returns reduced code plus a one-line legend, or null to keep the string
|
|
5699
|
+
* verbatim (crushing disabled, too small, or no net saving).
|
|
5700
|
+
*
|
|
5701
|
+
* token optimization via @memberjunction/context-crush (CodeCompressor-inspired)
|
|
5702
|
+
* @private
|
|
5703
|
+
*/
|
|
5704
|
+
crushCodeValue(stringValue) {
|
|
5705
|
+
const config = this._actionResultCrush;
|
|
5706
|
+
if (!config || !config.codeLang || stringValue.length < config.threshold) {
|
|
5707
|
+
return null;
|
|
5708
|
+
}
|
|
5709
|
+
// Crushing is a best-effort optimization — it must never break an agent turn. Any
|
|
5710
|
+
// failure falls back to the verbatim value.
|
|
5711
|
+
try {
|
|
5712
|
+
const result = CrushCode(stringValue, config.codeLang);
|
|
5713
|
+
if (result.CrushedChars >= result.OriginalChars) {
|
|
5714
|
+
return null;
|
|
5715
|
+
}
|
|
5716
|
+
const legend = DescribeCrush(result);
|
|
5717
|
+
return legend ? `${result.Text}\n ↳ ${legend}` : result.Text;
|
|
5718
|
+
}
|
|
5719
|
+
catch {
|
|
5720
|
+
return null;
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
/**
|
|
5724
|
+
* Structurally compress a JSON action-result value when crushing is enabled for the run
|
|
5725
|
+
* and the value clears the size threshold. Accepts either the `JSON.stringify` of an
|
|
5726
|
+
* object/array param, or a raw string param that itself contains JSON (many actions
|
|
5727
|
+
* stringify their payloads, e.g. `run-adhoc-query`'s `Results`). Returns crushed text
|
|
5728
|
+
* plus a one-line legend, or null when crushing is disabled, the value is too small, the
|
|
5729
|
+
* value isn't valid JSON, or compression wouldn't actually save characters — so callers
|
|
5730
|
+
* fall back to verbatim (and, for strings, to code crushing) behavior.
|
|
5731
|
+
*
|
|
5732
|
+
* token optimization via @memberjunction/context-crush (SmartCrusher-inspired)
|
|
5733
|
+
* @private
|
|
5734
|
+
*/
|
|
5735
|
+
crushParamValue(stringValue) {
|
|
5736
|
+
const config = this._actionResultCrush;
|
|
5737
|
+
if (!config || stringValue.length < config.threshold) {
|
|
5738
|
+
return null;
|
|
5739
|
+
}
|
|
5740
|
+
// Crushing is a best-effort optimization — it must never break an agent turn. Any
|
|
5741
|
+
// failure (non-JSON input, pathologically deep payloads) falls back to verbatim.
|
|
5742
|
+
try {
|
|
5743
|
+
// Parse to a plain JSON value. This is the JSON.stringify of an object/array
|
|
5744
|
+
// param, or a raw string param that contains JSON; non-JSON strings throw here
|
|
5745
|
+
// and are caught below (caller then tries code crushing / verbatim).
|
|
5746
|
+
const json = JSON.parse(stringValue);
|
|
5747
|
+
const result = CrushJSON(json, { MaxChars: config.maxChars });
|
|
5748
|
+
if (result.CrushedChars >= result.OriginalChars) {
|
|
5749
|
+
return null; // no net saving — keep the verbatim JSON
|
|
5750
|
+
}
|
|
5751
|
+
const legend = DescribeCrush(result);
|
|
5752
|
+
return legend ? `${result.Text}\n ↳ ${legend}` : result.Text;
|
|
5753
|
+
}
|
|
5754
|
+
catch {
|
|
5755
|
+
return null;
|
|
5756
|
+
}
|
|
5757
|
+
}
|
|
5250
5758
|
/**
|
|
5251
5759
|
* Formats a parameter value for display in action execution messages.
|
|
5252
5760
|
* Truncates long strings and formats objects/arrays for readability.
|
|
@@ -5458,6 +5966,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5458
5966
|
}
|
|
5459
5967
|
// Reset prompt turn counter for this execution
|
|
5460
5968
|
this._promptTurnCount = 0;
|
|
5969
|
+
// Resolve action-result compression config for this run (default on; opt out via
|
|
5970
|
+
// crushActionResults: false in the agent-type prompt params).
|
|
5971
|
+
this._actionResultCrush = this.resolveActionResultCrush(params);
|
|
5461
5972
|
// Create MJAIAgentRunEntity
|
|
5462
5973
|
this._agentRun = await (params.provider || this._activeProvider).GetEntityObject('MJ: AI Agent Runs', params.contextUser);
|
|
5463
5974
|
this._agentRun.AgentID = params.agent.ID;
|
|
@@ -5581,6 +6092,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5581
6092
|
: [params.agent.Name || 'Unknown Agent'];
|
|
5582
6093
|
this._depth = params.parentDepth !== undefined ? params.parentDepth + 1 : 0;
|
|
5583
6094
|
this._parentStepCounts = params.parentStepCounts || [];
|
|
6095
|
+
// Resolve Plan Mode gate state for this run (must happen before the main loop starts —
|
|
6096
|
+
// gatherPromptTemplateData/validateNextStep both read _planModeActive/_planApproved — and
|
|
6097
|
+
// after _depth is set above, since the gate only applies to root agents).
|
|
6098
|
+
const planModeGate = await this.resolvePlanModeGate(params);
|
|
6099
|
+
this._planModeActive = planModeGate.active;
|
|
6100
|
+
this._planApproved = planModeGate.approved;
|
|
6101
|
+
// Pre-activate any user-requested skills (from a `/skill-name` composer mention). Must run
|
|
6102
|
+
// after _depth is set (root-only) and after the run is persisted (records a Skill step).
|
|
6103
|
+
await this.preActivateRequestedSkills(params);
|
|
5584
6104
|
// Reset execution chain and progress tracking
|
|
5585
6105
|
this._allProgressSteps = [];
|
|
5586
6106
|
// Update params with the modified payload if auto-populated
|
|
@@ -5590,6 +6110,62 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5590
6110
|
params.payload = modifiedParams.payload;
|
|
5591
6111
|
}
|
|
5592
6112
|
}
|
|
6113
|
+
/**
|
|
6114
|
+
* Resolves whether Plan Mode is active for this run, and whether its approval gate is already
|
|
6115
|
+
* satisfied. Called once from {@link initializeAgentRun}, after `_depth` is set.
|
|
6116
|
+
*
|
|
6117
|
+
* - `active`: `agent.SupportsPlanMode` (capability, default ON/opt-out) AND `params.planMode`
|
|
6118
|
+
* (per-request, default OFF) AND this is a root agent (`_depth === 0`). Sub-agents never gate
|
|
6119
|
+
* on Plan Mode — only the top-level agent the user/caller invoked does.
|
|
6120
|
+
* - `approved`: only meaningful when `active`. True when `params.lastRunId` points to a prior
|
|
6121
|
+
* run whose Plan step's `MJ: AI Agent Requests` row resolved to `Approved` or `Responded`
|
|
6122
|
+
* (a `Rejected` plan — or no matching request at all — leaves the gate unsatisfied, sending
|
|
6123
|
+
* the agent back to present a revised plan).
|
|
6124
|
+
*
|
|
6125
|
+
* Override to change Plan Mode eligibility rules (e.g. gate on a specific agent category).
|
|
6126
|
+
*
|
|
6127
|
+
* @protected
|
|
6128
|
+
*/
|
|
6129
|
+
async resolvePlanModeGate(params) {
|
|
6130
|
+
const active = !!(params.agent.SupportsPlanMode && params.planMode === true && this._depth === 0);
|
|
6131
|
+
if (!active) {
|
|
6132
|
+
return { active: false, approved: false };
|
|
6133
|
+
}
|
|
6134
|
+
if (!params.lastRunId) {
|
|
6135
|
+
return { active: true, approved: false };
|
|
6136
|
+
}
|
|
6137
|
+
const rv = new RunView();
|
|
6138
|
+
const requestResult = await rv.RunView({
|
|
6139
|
+
EntityName: 'MJ: AI Agent Requests',
|
|
6140
|
+
ExtraFilter: `OriginatingAgentRunID='${params.lastRunId}'`,
|
|
6141
|
+
Fields: ['Status', 'OriginatingAgentRunStepID'],
|
|
6142
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
6143
|
+
MaxRows: 1,
|
|
6144
|
+
ResultType: 'simple'
|
|
6145
|
+
}, params.contextUser);
|
|
6146
|
+
if (!requestResult.Success || requestResult.Results.length === 0) {
|
|
6147
|
+
return { active: true, approved: false };
|
|
6148
|
+
}
|
|
6149
|
+
const request = requestResult.Results[0];
|
|
6150
|
+
const resolved = request.Status === 'Approved' || request.Status === 'Responded';
|
|
6151
|
+
if (!resolved || !request.OriginatingAgentRunStepID) {
|
|
6152
|
+
return { active: true, approved: false };
|
|
6153
|
+
}
|
|
6154
|
+
// Confirm the request actually originated from a Plan step — a resolved request from an
|
|
6155
|
+
// unrelated Chat clarification (asked before the agent could even form a plan) must NOT
|
|
6156
|
+
// satisfy the Plan Mode gate.
|
|
6157
|
+
const stepResult = await rv.RunView({
|
|
6158
|
+
EntityName: 'MJ: AI Agent Run Steps',
|
|
6159
|
+
ExtraFilter: `ID='${request.OriginatingAgentRunStepID}'`,
|
|
6160
|
+
Fields: ['StepType'],
|
|
6161
|
+
MaxRows: 1,
|
|
6162
|
+
ResultType: 'simple'
|
|
6163
|
+
}, params.contextUser);
|
|
6164
|
+
const approved = stepResult.Success
|
|
6165
|
+
&& stepResult.Results.length > 0
|
|
6166
|
+
&& stepResult.Results[0].StepType === 'Plan';
|
|
6167
|
+
return { active: true, approved };
|
|
6168
|
+
}
|
|
5593
6169
|
/**
|
|
5594
6170
|
* Validates the agent with tracking.
|
|
5595
6171
|
*
|
|
@@ -5666,7 +6242,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5666
6242
|
});
|
|
5667
6243
|
// Fire-and-forget the 'started' INSERT — the agent flow never blocks on a step save. The queue
|
|
5668
6244
|
// tracks the INSERT so every later UPDATE (queueStepSave) chains AFTER it commits.
|
|
5669
|
-
|
|
6245
|
+
// When the step has a parent, chain the INSERT AFTER the parent's INSERT to satisfy the
|
|
6246
|
+
// self-referencing FK_AIAgentRunStep_ParentID constraint — without this, a child INSERT that
|
|
6247
|
+
// races the parent INSERT hits an FK violation (especially under large-payload parent INSERTs).
|
|
6248
|
+
const parentStepEntity = params.parentId && this._agentRun?.Steps
|
|
6249
|
+
? this._agentRun.Steps.find(s => UUIDsEqual(s.ID, params.parentId))
|
|
6250
|
+
: undefined;
|
|
6251
|
+
this._stepSaveQueue.Insert(stepEntity, parentStepEntity);
|
|
5670
6252
|
// Add the step to the agent run's Steps array
|
|
5671
6253
|
if (this._agentRun) {
|
|
5672
6254
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -5941,6 +6523,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5941
6523
|
return await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
5942
6524
|
case 'Actions':
|
|
5943
6525
|
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount);
|
|
6526
|
+
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
6527
|
+
// union (non-terminal, like 'ClientTools') — LoopAgentType.DetermineNextStep() emits it
|
|
6528
|
+
// when the LLM chooses to activate a skill.
|
|
6529
|
+
case 'Skill':
|
|
6530
|
+
return await this.executeSkillStep(params, config, previousDecision, stepCount);
|
|
6531
|
+
// Type assertion required because 'Plan' is not part of the BaseAgentNextStep step
|
|
6532
|
+
// union — LoopAgentType.DetermineNextStep() emits it when the LLM presents a plan
|
|
6533
|
+
// (Plan Mode). executePlanStep's terminal return is 'Chat'-shaped (see its doc comment).
|
|
6534
|
+
case 'Plan':
|
|
6535
|
+
return await this.executePlanStep(params, previousDecision);
|
|
5944
6536
|
// Type assertion required because 'ClientTools' is not part of the BaseAgentNextStep
|
|
5945
6537
|
// step union — LoopAgentType.DetermineNextStep() emits it when the LLM chooses client tools.
|
|
5946
6538
|
case 'ClientTools':
|
|
@@ -8097,6 +8689,315 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8097
8689
|
});
|
|
8098
8690
|
return `${header}\n${lines.join('\n')}`;
|
|
8099
8691
|
}
|
|
8692
|
+
/**
|
|
8693
|
+
* Executes a 'Skill' next step: activates one or more skills the LLM requested by name.
|
|
8694
|
+
* Activating a skill (1) appends its full `Instructions` to the conversation so they take
|
|
8695
|
+
* effect for the remainder of the run, and (2) enables its bundled Actions/sub-agents by
|
|
8696
|
+
* pushing `root`-scoped `add` entries onto `params.actionChanges`/`params.subAgentChanges` —
|
|
8697
|
+
* the same runtime tool-surface-extension mechanism `ExecuteAgentParams` already exposes to
|
|
8698
|
+
* external callers. This is NOT a nested agent run; it never terminates the loop itself.
|
|
8699
|
+
*
|
|
8700
|
+
* Already-activated skills (tracked in `_activatedSkillIDs`) are skipped — re-requesting an
|
|
8701
|
+
* active skill is a harmless no-op rather than re-appending duplicate instructions.
|
|
8702
|
+
*
|
|
8703
|
+
* Decomposed into {@link resolveSkillActivations}, {@link buildSkillActivationMessage},
|
|
8704
|
+
* {@link enableSkillCapabilities}, and {@link recordSkillActivationStep} — override any of
|
|
8705
|
+
* those for fine-grained control (e.g. custom instruction formatting, additional side effects
|
|
8706
|
+
* on activation) without re-implementing the whole step.
|
|
8707
|
+
*
|
|
8708
|
+
* @protected
|
|
8709
|
+
*/
|
|
8710
|
+
async executeSkillStep(params, config, previousDecision, stepCount = 0) {
|
|
8711
|
+
const requested = previousDecision.skillActivations ?? [];
|
|
8712
|
+
if (requested.length === 0) {
|
|
8713
|
+
// Nothing to activate — continue with next prompt
|
|
8714
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8715
|
+
}
|
|
8716
|
+
const resolvedSkills = this.resolveSkillActivations(requested, params.agent, params.contextUser);
|
|
8717
|
+
const newlyActivated = resolvedSkills.filter(skill => !this._activatedSkillIDs.some(id => UUIDsEqual(id, skill.ID)));
|
|
8718
|
+
if (newlyActivated.length === 0) {
|
|
8719
|
+
// All requested skills are already active this run — no-op, just continue
|
|
8720
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8721
|
+
}
|
|
8722
|
+
const currentPayload = previousDecision?.newPayload || previousDecision?.previousPayload || params.payload;
|
|
8723
|
+
for (const skill of newlyActivated) {
|
|
8724
|
+
await this.recordSkillActivationStep(skill, currentPayload, params);
|
|
8725
|
+
this.enableSkillCapabilities(skill, params);
|
|
8726
|
+
this._activatedSkillIDs.push(skill.ID);
|
|
8727
|
+
}
|
|
8728
|
+
const activationMessage = this.buildSkillActivationMessage(newlyActivated);
|
|
8729
|
+
params.conversationMessages.push({
|
|
8730
|
+
role: 'user',
|
|
8731
|
+
content: activationMessage,
|
|
8732
|
+
metadata: {
|
|
8733
|
+
turnAdded: this._promptTurnCount,
|
|
8734
|
+
messageType: 'skill-activation'
|
|
8735
|
+
}
|
|
8736
|
+
});
|
|
8737
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8738
|
+
}
|
|
8739
|
+
/**
|
|
8740
|
+
* Pre-activates skills the caller explicitly requested via {@link ExecuteAgentParams.requestedSkillIDs}
|
|
8741
|
+
* (typically an end user's `/skill-name` composer mentions), at run start — so their Instructions
|
|
8742
|
+
* and bundled Actions/sub-agents take effect from the first turn rather than waiting for the model
|
|
8743
|
+
* to discover and activate them through the catalog.
|
|
8744
|
+
*
|
|
8745
|
+
* **Root-agent only** (skills never cascade to sub-agents), and each requested skill activates
|
|
8746
|
+
* **only if it survives the guard**: it must be in the set {@link AIEngine.GetSkillsForAgent}
|
|
8747
|
+
* allows for this agent (the AcceptsSkills gate) AND the acting user must have Run permission on it
|
|
8748
|
+
* — both enforced by passing `params.contextUser` to `GetSkillsForAgent`. Requested IDs that fail
|
|
8749
|
+
* either check are silently dropped, so a client can never force-activate a skill the user or agent
|
|
8750
|
+
* isn't entitled to. Reuses the same {@link recordSkillActivationStep} / {@link enableSkillCapabilities}
|
|
8751
|
+
* / {@link buildSkillActivationMessage} machinery as the model-initiated `Skill` step, so activation
|
|
8752
|
+
* is recorded and takes effect identically. Plan Mode is unaffected — pre-activation widens the tool
|
|
8753
|
+
* surface, but the plan-approval gate still blocks executing those tools until the plan is approved.
|
|
8754
|
+
*
|
|
8755
|
+
* @protected
|
|
8756
|
+
*/
|
|
8757
|
+
async preActivateRequestedSkills(params) {
|
|
8758
|
+
if (this._depth !== 0) {
|
|
8759
|
+
return; // skills are root-agent only; never pre-activate on sub-agents
|
|
8760
|
+
}
|
|
8761
|
+
const requestedIds = params.requestedSkillIDs;
|
|
8762
|
+
if (!requestedIds || requestedIds.length === 0) {
|
|
8763
|
+
return;
|
|
8764
|
+
}
|
|
8765
|
+
// Guard: intersect the requested IDs with the agent-accepted ∩ user-permitted set.
|
|
8766
|
+
const allowed = AIEngine.Instance.GetSkillsForAgent(params.agent, params.contextUser);
|
|
8767
|
+
const newlyActivated = allowed.filter(s => requestedIds.some(id => UUIDsEqual(id, s.ID)) &&
|
|
8768
|
+
!this._activatedSkillIDs.some(id => UUIDsEqual(id, s.ID)));
|
|
8769
|
+
if (newlyActivated.length === 0) {
|
|
8770
|
+
return;
|
|
8771
|
+
}
|
|
8772
|
+
const currentPayload = params.payload;
|
|
8773
|
+
for (const skill of newlyActivated) {
|
|
8774
|
+
await this.recordSkillActivationStep(skill, currentPayload, params);
|
|
8775
|
+
this.enableSkillCapabilities(skill, params);
|
|
8776
|
+
this._activatedSkillIDs.push(skill.ID);
|
|
8777
|
+
}
|
|
8778
|
+
const activationMessage = this.buildSkillActivationMessage(newlyActivated);
|
|
8779
|
+
if (!params.conversationMessages) {
|
|
8780
|
+
params.conversationMessages = [];
|
|
8781
|
+
}
|
|
8782
|
+
params.conversationMessages.push({
|
|
8783
|
+
role: 'user',
|
|
8784
|
+
content: activationMessage,
|
|
8785
|
+
metadata: {
|
|
8786
|
+
turnAdded: this._promptTurnCount,
|
|
8787
|
+
messageType: 'skill-activation'
|
|
8788
|
+
}
|
|
8789
|
+
});
|
|
8790
|
+
}
|
|
8791
|
+
/**
|
|
8792
|
+
* Resolves the LLM's requested skill names to `MJ: AI Skills` entities, restricted to what
|
|
8793
|
+
* {@link AIEngine.GetSkillsForAgent} allows for this agent (the AcceptsSkills gate + Status
|
|
8794
|
+
* chain). Names that don't resolve are silently dropped here — {@link validateSkillNextStep}
|
|
8795
|
+
* is responsible for rejecting unknown/disallowed names before execution ever reaches this
|
|
8796
|
+
* point, so by the time `executeSkillStep` runs, every requested name is expected to match.
|
|
8797
|
+
*
|
|
8798
|
+
* Override to change resolution semantics (e.g. resolve by ID instead of Name).
|
|
8799
|
+
*
|
|
8800
|
+
* @protected
|
|
8801
|
+
*/
|
|
8802
|
+
resolveSkillActivations(requested, agent, contextUser) {
|
|
8803
|
+
const availableSkills = AIEngine.Instance.GetSkillsForAgent(agent, contextUser);
|
|
8804
|
+
const resolved = [];
|
|
8805
|
+
for (const req of requested) {
|
|
8806
|
+
const requestedName = req.name.trim().toLowerCase();
|
|
8807
|
+
const match = availableSkills.find(s => s.Name.trim().toLowerCase() === requestedName);
|
|
8808
|
+
if (match && !resolved.some(s => UUIDsEqual(s.ID, match.ID))) {
|
|
8809
|
+
resolved.push(match);
|
|
8810
|
+
}
|
|
8811
|
+
}
|
|
8812
|
+
return resolved;
|
|
8813
|
+
}
|
|
8814
|
+
/**
|
|
8815
|
+
* Builds the message appended to `conversationMessages` when skill(s) activate — this is what
|
|
8816
|
+
* actually puts each skill's `Instructions` into effect for the rest of the run. Override to
|
|
8817
|
+
* change formatting (e.g. a more compact representation for a high skill-activation-count agent).
|
|
8818
|
+
*
|
|
8819
|
+
* @protected
|
|
8820
|
+
*/
|
|
8821
|
+
buildSkillActivationMessage(skills) {
|
|
8822
|
+
const sections = skills.map(s => `## Skill Activated: ${s.Name}\n\n${s.Instructions}`);
|
|
8823
|
+
return `The following skill(s) have been activated. Their instructions are now in effect ` +
|
|
8824
|
+
`for the remainder of this run:\n\n${sections.join('\n\n')}`;
|
|
8825
|
+
}
|
|
8826
|
+
/**
|
|
8827
|
+
* Enables a skill's bundled Actions and sub-agents by pushing `specific`-scoped `add` entries
|
|
8828
|
+
* (targeted at exactly the activating agent's ID) onto `params.actionChanges` /
|
|
8829
|
+
* `params.subAgentChanges`. `specific`/`[agent.ID]` is the correct scope for "apply to THIS
|
|
8830
|
+
* agent, at whatever depth it runs, and never leak to its sub-agents":
|
|
8831
|
+
* - {@link doesChangeScopeApply} returns true only when the running agent's ID is in the list,
|
|
8832
|
+
* so it applies to the activating agent regardless of depth (a sub-agent that activates a
|
|
8833
|
+
* skill still gets its tools — which a `root`-scoped change would NOT do, since `root` means
|
|
8834
|
+
* "the depth-0 agent," not "the current agent").
|
|
8835
|
+
* - {@link filterActionChangesForSubAgent} / {@link filterSubAgentChangesForSubAgent} propagate
|
|
8836
|
+
* `specific` as-is, and each downstream agent checks `includes(itsOwnID)` → false, so the
|
|
8837
|
+
* grant never cascades to sub-agents the activating agent later delegates to.
|
|
8838
|
+
* Because `params` is the same object reference used for the rest of this run, every subsequent
|
|
8839
|
+
* turn's `gatherPromptTemplateData()` call picks up the change automatically — no extra plumbing.
|
|
8840
|
+
*
|
|
8841
|
+
* Override to change propagation scope (e.g. a subclass that wants skill-granted capabilities
|
|
8842
|
+
* to cascade to sub-agents could push `scope: 'all-subagents'` instead).
|
|
8843
|
+
*
|
|
8844
|
+
* @protected
|
|
8845
|
+
*/
|
|
8846
|
+
enableSkillCapabilities(skill, params) {
|
|
8847
|
+
const activatingAgentIds = [params.agent.ID];
|
|
8848
|
+
const actionIds = AIEngine.Instance.GetSkillActionIDs(skill.ID);
|
|
8849
|
+
if (actionIds.length > 0) {
|
|
8850
|
+
if (!params.actionChanges) {
|
|
8851
|
+
params.actionChanges = [];
|
|
8852
|
+
}
|
|
8853
|
+
params.actionChanges.push({
|
|
8854
|
+
scope: 'specific',
|
|
8855
|
+
mode: 'add',
|
|
8856
|
+
actionIds,
|
|
8857
|
+
agentIds: activatingAgentIds
|
|
8858
|
+
});
|
|
8859
|
+
}
|
|
8860
|
+
const subAgentIds = AIEngine.Instance.GetSkillSubAgentIDs(skill.ID);
|
|
8861
|
+
if (subAgentIds.length > 0) {
|
|
8862
|
+
if (!params.subAgentChanges) {
|
|
8863
|
+
params.subAgentChanges = [];
|
|
8864
|
+
}
|
|
8865
|
+
params.subAgentChanges.push({
|
|
8866
|
+
scope: 'specific',
|
|
8867
|
+
mode: 'add',
|
|
8868
|
+
subAgentIds,
|
|
8869
|
+
agentIds: activatingAgentIds
|
|
8870
|
+
});
|
|
8871
|
+
}
|
|
8872
|
+
}
|
|
8873
|
+
/**
|
|
8874
|
+
* Creates and immediately finalizes the `AIAgentRunStep` (StepType='Skill') that records this
|
|
8875
|
+
* skill activation for observability/audit. Activation is not itself a failure mode today — it
|
|
8876
|
+
* always finalizes as successful — but subclasses can override to add richer InputData/OutputData
|
|
8877
|
+
* or to make activation conditionally fail (e.g. a licensing check).
|
|
8878
|
+
*
|
|
8879
|
+
* @protected
|
|
8880
|
+
*/
|
|
8881
|
+
async recordSkillActivationStep(skill, currentPayload, params) {
|
|
8882
|
+
const stepEntity = await this.createStepEntity({
|
|
8883
|
+
stepType: 'Skill',
|
|
8884
|
+
stepName: `Skill: ${skill.Name}`,
|
|
8885
|
+
targetId: skill.ID,
|
|
8886
|
+
inputData: { skillName: skill.Name },
|
|
8887
|
+
contextUser: params.contextUser,
|
|
8888
|
+
payloadAtStart: currentPayload,
|
|
8889
|
+
payloadAtEnd: currentPayload
|
|
8890
|
+
});
|
|
8891
|
+
await this.finalizeStepEntity(stepEntity, true, undefined, {
|
|
8892
|
+
skillId: skill.ID,
|
|
8893
|
+
skillName: skill.Name
|
|
8894
|
+
});
|
|
8895
|
+
}
|
|
8896
|
+
/**
|
|
8897
|
+
* Executes a 'Plan' next step (Plan Mode): records a `Plan` run-step, raises the standard
|
|
8898
|
+
* `MJ: AI Agent Requests` HITL request with an editable plan-approval `AgentResponseForm`, and
|
|
8899
|
+
* terminates this run awaiting the human's response — reusing the exact same pause/resume
|
|
8900
|
+
* infrastructure `executeChatStep` uses (`createFeedbackRequest` + the existing
|
|
8901
|
+
* `MJAIAgentRequestEntityServer.Save()` auto-resume-on-status-change hook). A rejected or
|
|
8902
|
+
* edited-and-resubmitted plan resumes as a new linked run via the normal run-chain mechanism;
|
|
8903
|
+
* `resolvePlanModeGate` re-checks approval on that new run so a rejection sends the agent back
|
|
8904
|
+
* to present a revised plan rather than through to execution.
|
|
8905
|
+
*
|
|
8906
|
+
* **Important**: the RETURNED `BaseAgentNextStep.step` is `'Chat'`, not `'Plan'` — 'Plan' is
|
|
8907
|
+
* only ever an intermediate classification (used for the `AIAgentRunStep.StepType` audit
|
|
8908
|
+
* record, which the UI reads to render a plan-approval card instead of a generic chat bubble).
|
|
8909
|
+
* The step returned to the framework must stay within `AIAgentRun.FinalStep`'s DB-CHECK-
|
|
8910
|
+
* constrained, terminal-only vocabulary — `'Plan'` is deliberately not part of it (see the
|
|
8911
|
+
* `BaseAgentNextStep.step` doc comment) — so a plan-approval pause is represented as the same
|
|
8912
|
+
* terminal shape `executeChatStep` already uses.
|
|
8913
|
+
*
|
|
8914
|
+
* @protected
|
|
8915
|
+
*/
|
|
8916
|
+
async executePlanStep(params, previousDecision) {
|
|
8917
|
+
const planText = previousDecision.planDetails?.plan ?? '';
|
|
8918
|
+
const stepEntity = await this.createStepEntity({
|
|
8919
|
+
stepType: 'Plan',
|
|
8920
|
+
stepName: 'Plan Presented for Approval',
|
|
8921
|
+
contextUser: params.contextUser,
|
|
8922
|
+
inputData: { plan: planText }
|
|
8923
|
+
});
|
|
8924
|
+
await this.finalizeStepEntity(stepEntity, true, undefined, { plan: planText });
|
|
8925
|
+
const responseForm = this.buildPlanApprovalForm(planText);
|
|
8926
|
+
const planPresentation = {
|
|
8927
|
+
step: 'Plan',
|
|
8928
|
+
terminate: true,
|
|
8929
|
+
message: previousDecision.message || 'Please review the proposed plan before I proceed.',
|
|
8930
|
+
reasoning: previousDecision.reasoning,
|
|
8931
|
+
confidence: previousDecision.confidence,
|
|
8932
|
+
responseForm
|
|
8933
|
+
};
|
|
8934
|
+
// For root agents, create a persistent AIAgentRequest so the request is tracked in the
|
|
8935
|
+
// dashboard and can be responded to outside a conversation (mirrors executeChatStep).
|
|
8936
|
+
if (this._depth === 0) {
|
|
8937
|
+
await this.createFeedbackRequest(params, stepEntity, planPresentation);
|
|
8938
|
+
}
|
|
8939
|
+
return {
|
|
8940
|
+
step: 'Chat',
|
|
8941
|
+
terminate: true,
|
|
8942
|
+
message: planPresentation.message,
|
|
8943
|
+
reasoning: previousDecision.reasoning,
|
|
8944
|
+
confidence: previousDecision.confidence,
|
|
8945
|
+
previousPayload: previousDecision.previousPayload,
|
|
8946
|
+
newPayload: previousDecision.newPayload || previousDecision.previousPayload,
|
|
8947
|
+
responseForm
|
|
8948
|
+
};
|
|
8949
|
+
}
|
|
8950
|
+
/**
|
|
8951
|
+
* Builds the editable plan-approval `AgentResponseForm`: the Markdown-rendered plan (with an
|
|
8952
|
+
* Edit toggle so the human can amend it before approving), an optional feedback field that
|
|
8953
|
+
* travels back to the agent with the decision (most useful on Reject — it steers the re-plan),
|
|
8954
|
+
* and an Approve/Reject button group. Override to change the card's layout (e.g. split the
|
|
8955
|
+
* plan into per-step checkboxes instead of one field).
|
|
8956
|
+
*
|
|
8957
|
+
* Approval is a HIGHER-ORDER signal, not just a form reply: conversation hosts detect
|
|
8958
|
+
* `decision === 'approve'` on this form and switch the conversation out of Plan Mode
|
|
8959
|
+
* (see ng-conversations' plan-decision handling), so the follow-up run executes the approved
|
|
8960
|
+
* plan instead of planning again. Rejection keeps Plan Mode on — the agent re-plans with the
|
|
8961
|
+
* feedback in context.
|
|
8962
|
+
*
|
|
8963
|
+
* @protected
|
|
8964
|
+
*/
|
|
8965
|
+
buildPlanApprovalForm(planText) {
|
|
8966
|
+
return {
|
|
8967
|
+
title: 'Review Plan',
|
|
8968
|
+
description: 'Review the proposed plan below. Edit it if needed, then approve to proceed — or reject (with a note on what to change) and the agent will re-plan.',
|
|
8969
|
+
submitLabel: 'Submit',
|
|
8970
|
+
questions: [
|
|
8971
|
+
{
|
|
8972
|
+
id: 'plan',
|
|
8973
|
+
label: 'Plan',
|
|
8974
|
+
// markdown: agents author plans in Markdown (see the plan-mode prompt
|
|
8975
|
+
// instructions); the UI renders a formatted preview with an Edit toggle.
|
|
8976
|
+
type: { type: 'textarea', markdown: true },
|
|
8977
|
+
defaultValue: planText,
|
|
8978
|
+
required: true
|
|
8979
|
+
},
|
|
8980
|
+
{
|
|
8981
|
+
id: 'reason',
|
|
8982
|
+
label: 'Feedback',
|
|
8983
|
+
type: { type: 'textarea', placeholder: 'Optional — if rejecting, tell the agent what to change and it will re-plan.' },
|
|
8984
|
+
required: false
|
|
8985
|
+
},
|
|
8986
|
+
{
|
|
8987
|
+
id: 'decision',
|
|
8988
|
+
label: 'Decision',
|
|
8989
|
+
type: {
|
|
8990
|
+
type: 'buttongroup',
|
|
8991
|
+
options: [
|
|
8992
|
+
{ value: 'approve', label: 'Approve' },
|
|
8993
|
+
{ value: 'reject', label: 'Reject' }
|
|
8994
|
+
]
|
|
8995
|
+
},
|
|
8996
|
+
required: true
|
|
8997
|
+
}
|
|
8998
|
+
]
|
|
8999
|
+
};
|
|
9000
|
+
}
|
|
8100
9001
|
async executeChatStep(params, previousDecision) {
|
|
8101
9002
|
const stepEntity = await this.createStepEntity({ stepType: 'Chat', stepName: 'User Interaction', contextUser: params.contextUser });
|
|
8102
9003
|
// Chat steps are successful - they indicate a need for user interaction
|
|
@@ -9302,6 +10203,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9302
10203
|
async pruneAndCompactExpiredMessages(params, currentTurn) {
|
|
9303
10204
|
const messagesToCompact = [];
|
|
9304
10205
|
const messagesToRemove = [];
|
|
10206
|
+
// Cache-aware guard: confine pruning/compaction to the volatile tail so we don't
|
|
10207
|
+
// perturb the provider's KV-cached prompt prefix. The stable prefix is the maximal
|
|
10208
|
+
// contiguous leading run of non-result messages (system/RAG context, injected
|
|
10209
|
+
// memory, the original user request). Expired messages that fall inside that prefix
|
|
10210
|
+
// are deferred — genuine context overflow still reaches them via attemptContextRecovery.
|
|
10211
|
+
// token optimization via @memberjunction/context-crush (CacheAligner-inspired)
|
|
10212
|
+
const { Boundary: stablePrefixBoundary } = PartitionStablePrefix(params.conversationMessages, (msg) => !this.IsVolatileResultMessage(msg));
|
|
9305
10213
|
// Phase 1: Identify expired messages
|
|
9306
10214
|
for (let i = 0; i < params.conversationMessages.length; i++) {
|
|
9307
10215
|
const msg = params.conversationMessages[i];
|
|
@@ -9318,6 +10226,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9318
10226
|
const turnsAlive = currentTurn - turnAdded;
|
|
9319
10227
|
// Check if expired
|
|
9320
10228
|
if (turnsAlive > msg.metadata.expirationTurns) {
|
|
10229
|
+
// Defer expiry of messages inside the cache-stable prefix to preserve the
|
|
10230
|
+
// provider's cached prompt prefix; overflow recovery handles them if needed.
|
|
10231
|
+
if (i < stablePrefixBoundary) {
|
|
10232
|
+
this.logStatus(`[Turn ${currentTurn}] Deferred expiry of cache-stable prefix message at index ${i}`, true, params);
|
|
10233
|
+
continue;
|
|
10234
|
+
}
|
|
9321
10235
|
msg.metadata.isExpired = true;
|
|
9322
10236
|
if (msg.metadata.expirationMode === 'Remove') {
|
|
9323
10237
|
messagesToRemove.push(i);
|
|
@@ -9563,6 +10477,21 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9563
10477
|
|| messageType === 'client-tool-result'
|
|
9564
10478
|
|| messageType === 'tool-result';
|
|
9565
10479
|
}
|
|
10480
|
+
/**
|
|
10481
|
+
* Returns true if the message is a turn-generated result (action, tool, client tool,
|
|
10482
|
+
* sub-agent, or loop). These are the volatile, expirable messages that accumulate over
|
|
10483
|
+
* turns. Everything else — system/RAG context, injected memory, the original user
|
|
10484
|
+
* request — anchors the cache-stable prompt prefix and is protected from routine pruning.
|
|
10485
|
+
* @protected
|
|
10486
|
+
*/
|
|
10487
|
+
IsVolatileResultMessage(msg) {
|
|
10488
|
+
const messageType = msg.metadata?.messageType;
|
|
10489
|
+
return messageType === 'action-result'
|
|
10490
|
+
|| messageType === 'client-tool-result'
|
|
10491
|
+
|| messageType === 'tool-result'
|
|
10492
|
+
|| messageType === 'sub-agent-result'
|
|
10493
|
+
|| messageType === 'loop-result';
|
|
10494
|
+
}
|
|
9566
10495
|
estimateTokens(content, modelName) {
|
|
9567
10496
|
const text = typeof content === 'string'
|
|
9568
10497
|
? content
|