@memberjunction/ai-agents 5.43.0 → 5.45.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/AgentRunner.d.ts +20 -0
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +62 -0
- package/dist/AgentRunner.js.map +1 -1
- 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 +23 -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 +34 -1
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +434 -12
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1215 -89
- 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 +114 -6
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +289 -30
- 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,47 @@ 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
|
+
* Full observability records for every skill activated this run — one {@link AgentSkillInvocation}
|
|
247
|
+
* per activation, carrying activation type ('requested' | 'auto'), the provenance-of-authority
|
|
248
|
+
* gate values that admitted the skill, and the agent-stated reason when self-activated.
|
|
249
|
+
* Serialized onto `AIAgentRunStep.Skills`: Skill steps record their own activation(s), Prompt
|
|
250
|
+
* steps record the full set in effect for the turn, and Actions/Sub-Agent steps record the
|
|
251
|
+
* skill(s) that granted the executed tool (see {@link getSkillAttributionForAction} /
|
|
252
|
+
* {@link getSkillAttributionForSubAgent}).
|
|
253
|
+
*/
|
|
254
|
+
this._skillInvocations = [];
|
|
255
|
+
/**
|
|
256
|
+
* Whether Plan Mode is active for this run — resolved once in {@link initializeAgentRun} via
|
|
257
|
+
* {@link resolvePlanModeGate}. True only when `agent.SupportsPlanMode` (capability, default ON)
|
|
258
|
+
* AND `params.planMode` (per-request, default OFF) are both true AND this is a root agent.
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
this._planModeActive = false;
|
|
262
|
+
/**
|
|
263
|
+
* Whether Plan Mode's approval gate has already been satisfied for this run — either because
|
|
264
|
+
* Plan Mode isn't active, or because a prior linked run's Plan step was approved. When active
|
|
265
|
+
* and NOT yet approved, `validateNextStep` blocks Actions/Sub-Agent steps until a Plan step
|
|
266
|
+
* has been presented and approved.
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
this._planApproved = false;
|
|
222
270
|
/**
|
|
223
271
|
* Counts only prompt (LLM) executions, NOT all agent steps.
|
|
224
272
|
* Used for message expiration age calculations so that `expirationTurns`
|
|
@@ -230,6 +278,14 @@ export class BaseAgent {
|
|
|
230
278
|
* prompt execution occurred after them.
|
|
231
279
|
*/
|
|
232
280
|
this._promptTurnCount = 0;
|
|
281
|
+
/**
|
|
282
|
+
* Per-run config for structurally compressing inline action-result payloads.
|
|
283
|
+
* Resolved once at run start from the agent-type prompt params (default on);
|
|
284
|
+
* read by formatActionResultsAsMarkdown so both the direct and loop callers
|
|
285
|
+
* share the same setting without threading it through every signature.
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
this._actionResultCrush = undefined;
|
|
233
289
|
/**
|
|
234
290
|
* Execution limits for dynamically added actions.
|
|
235
291
|
* Maps action IDs to their MaxExecutionsPerRun limit.
|
|
@@ -256,6 +312,34 @@ export class BaseAgent {
|
|
|
256
312
|
* @private
|
|
257
313
|
*/
|
|
258
314
|
this.MAX_RECOVERY_ATTEMPTS = 1;
|
|
315
|
+
/**
|
|
316
|
+
* Drives a session-driven (Realtime) agent run end-to-end.
|
|
317
|
+
*
|
|
318
|
+
* Resolves the realtime model, assembles the session parameters (system prompt + memory/context),
|
|
319
|
+
* builds the {@link RealtimeSessionRunnerDeps} from this agent's collaborators, runs the
|
|
320
|
+
* {@link RealtimeSessionRunner}, and maps the result onto the finalized `AIAgentRun`.
|
|
321
|
+
*
|
|
322
|
+
* If no realtime model can be resolved (expected today, before the P3 drivers / P4 model
|
|
323
|
+
* metadata land), it finalizes the run as a clean FAILED result with an actionable message
|
|
324
|
+
* rather than throwing — a mis-provisioned environment must not crash the caller.
|
|
325
|
+
*
|
|
326
|
+
* @template R The caller's expected payload type (unused on the realtime path; the session
|
|
327
|
+
* produces transcript/usage rather than a structured payload).
|
|
328
|
+
* @param params The wrapped execution parameters.
|
|
329
|
+
* @param config The loaded agent configuration (provides the system prompt, if any).
|
|
330
|
+
* @returns The finalized {@link ExecuteAgentResult}.
|
|
331
|
+
*/
|
|
332
|
+
// ── Realtime per-session capture state (scoped to one executeRealtimeSession run) ──────────
|
|
333
|
+
/**
|
|
334
|
+
* In-flight realtime turn rows keyed by transcript role (`'user'`/`'assistant'`), driving the
|
|
335
|
+
* create-on-start / update-on-complete persistence lifecycle. Reset at the start of every
|
|
336
|
+
* realtime session so a prior run can never leak an in-flight id into the next.
|
|
337
|
+
*/
|
|
338
|
+
this.realtimeInFlightTurns = new Map();
|
|
339
|
+
/** Active audio recording controller for the current realtime session, or `null` when recording is off. */
|
|
340
|
+
this.realtimeRecording = null;
|
|
341
|
+
/** Storage account id the active recording stores to (RecordingStorageProviderID ?? AttachmentStorageProviderID). */
|
|
342
|
+
this.realtimeRecordingAccountId = null;
|
|
259
343
|
/**
|
|
260
344
|
* Storage for injected notes and examples to include in result
|
|
261
345
|
*/
|
|
@@ -427,6 +511,13 @@ export class BaseAgent {
|
|
|
427
511
|
* @private
|
|
428
512
|
*/
|
|
429
513
|
static { this.LARGE_BINARY_THRESHOLD = 10000; }
|
|
514
|
+
/**
|
|
515
|
+
* Minimum stringified length (chars) of an object/array action-result value before
|
|
516
|
+
* structural JSON compression (CrushJSON) is applied. Small payloads aren't worth a
|
|
517
|
+
* legend, so they pass through verbatim.
|
|
518
|
+
* @private
|
|
519
|
+
*/
|
|
520
|
+
static { this.ACTION_RESULT_CRUSH_THRESHOLD = 600; }
|
|
430
521
|
/**
|
|
431
522
|
* Inspects a set of action output params for any value matching the FileOutputRef shape
|
|
432
523
|
* (an object with `fileName`, `mimeType`, and either `fileData` or `fileId`).
|
|
@@ -1134,6 +1225,9 @@ export class BaseAgent {
|
|
|
1134
1225
|
this.InjectContextMemory(typeof inputText === 'string' ? inputText : '', params.agent, userId, companyId, params.contextUser, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, scopeConfig),
|
|
1135
1226
|
this.InjectPreExecutionRAG(typeof inputText === 'string' ? inputText : '', params.agent, params.contextUser, wrappedParams.conversationMessages, params.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, params.payload)
|
|
1136
1227
|
]);
|
|
1228
|
+
// Inject scope-resolved prompt parts (role-faithful) for this agent's prompt, alongside
|
|
1229
|
+
// memory/RAG. Synchronous — parts are cached on AIEngine. Uses the same run scope.
|
|
1230
|
+
this.InjectScopedPromptParts(params.agent, wrappedParams.conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes);
|
|
1137
1231
|
if (!config.success) {
|
|
1138
1232
|
this.logError(`Failed to load agent configuration: ${config.errorMessage}`, {
|
|
1139
1233
|
agent: params.agent,
|
|
@@ -1238,23 +1332,6 @@ export class BaseAgent {
|
|
|
1238
1332
|
isSessionDrivenAgentType(agentType) {
|
|
1239
1333
|
return agentType.IsSessionDriven === true;
|
|
1240
1334
|
}
|
|
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
1335
|
async executeRealtimeSession(params, config) {
|
|
1259
1336
|
// 1) Resolve the realtime model (overridable seam — tests inject a mock).
|
|
1260
1337
|
const modelResolution = await this.resolveRealtimeModel(params);
|
|
@@ -1268,7 +1345,12 @@ export class BaseAgent {
|
|
|
1268
1345
|
}
|
|
1269
1346
|
// 2) Create the single long-lived AIPromptRun that usage is checkpointed onto.
|
|
1270
1347
|
const promptRun = await this.createRealtimePromptRun(params, config, modelResolution);
|
|
1271
|
-
// 3)
|
|
1348
|
+
// 3) Resolve recording (OFF by default; runtime > agent > off; consent + storage gated) and reset
|
|
1349
|
+
// the per-session turn-lifecycle state, then build the injected deps and run the session.
|
|
1350
|
+
this.realtimeInFlightTurns = new Map();
|
|
1351
|
+
const recording = await this.resolveRealtimeRecording(params);
|
|
1352
|
+
this.realtimeRecording = recording?.controller ?? null;
|
|
1353
|
+
this.realtimeRecordingAccountId = recording?.storageAccountId ?? null;
|
|
1272
1354
|
try {
|
|
1273
1355
|
const deps = await this.buildRealtimeSessionDeps(params, config, modelResolution, promptRun);
|
|
1274
1356
|
const runner = new RealtimeSessionRunner(deps);
|
|
@@ -1369,6 +1451,11 @@ export class BaseAgent {
|
|
|
1369
1451
|
UserID: params.contextUser?.ID,
|
|
1370
1452
|
DisableAutoResponse: meetingMode || undefined,
|
|
1371
1453
|
SelfNames: selfNames,
|
|
1454
|
+
// App awareness (Move 1/3/4): the app the session runs in (sources the app cascade layer +
|
|
1455
|
+
// RelevantAgents → allowed-agent union) and the live app-context snapshot injected at mint.
|
|
1456
|
+
// Both ride params.data, the same conduit async agents use for appContext.
|
|
1457
|
+
ApplicationID: params.data?.applicationId?.trim() || undefined,
|
|
1458
|
+
AppContext: params.data?.appContext,
|
|
1372
1459
|
};
|
|
1373
1460
|
}
|
|
1374
1461
|
/**
|
|
@@ -1559,6 +1646,8 @@ export class BaseAgent {
|
|
|
1559
1646
|
DelegateToTarget: (request) => this.delegateRealtimeToTarget(params, config, request),
|
|
1560
1647
|
ExecuteTool: (call) => this.executeRealtimeTool(params, call),
|
|
1561
1648
|
PersistTranscript: (transcript) => this.persistRealtimeTranscript(params, transcript),
|
|
1649
|
+
Recording: this.realtimeRecording ?? undefined,
|
|
1650
|
+
FinalizeRecording: () => this.finalizeRealtimeRecording(params),
|
|
1562
1651
|
CheckpointUsage: (usage) => this.checkpointRealtimeUsage(promptRun, usage),
|
|
1563
1652
|
// DB-driven spoken-progress wording (shared lookup with the client-direct path);
|
|
1564
1653
|
// null → the runner's documented built-in first-person fallback.
|
|
@@ -1800,34 +1889,211 @@ export class BaseAgent {
|
|
|
1800
1889
|
return {};
|
|
1801
1890
|
}
|
|
1802
1891
|
/**
|
|
1803
|
-
* Persists a
|
|
1804
|
-
*
|
|
1805
|
-
*
|
|
1892
|
+
* Persists a realtime transcript turn as a `ConversationDetail` with a **create-on-start /
|
|
1893
|
+
* update-on-complete** lifecycle, so each turn carries both a start (`__mj_CreatedAt`) and an
|
|
1894
|
+
* immutable end (`TurnEndedAt`):
|
|
1895
|
+
* - **Interim** (`IsFinal=false`): on the FIRST delta for a role, CREATE the row with
|
|
1896
|
+
* `Status='In-Progress'` (so a live UI can show the turn streaming), stamping the recording-relative
|
|
1897
|
+
* `UtteranceStartMs` and the speaker `UserID` (user turns only). Subsequent interim deltas are no-ops.
|
|
1898
|
+
* - **Final** (`IsFinal=true`): UPDATE that in-flight row with the full text, `Status='Complete'`,
|
|
1899
|
+
* `TurnEndedAt`, and `UtteranceEndMs`. If no interim was seen (some providers only emit final), the
|
|
1900
|
+
* row is created and finalized in one step.
|
|
1901
|
+
*
|
|
1902
|
+
* Returns the new row's ID the first time a DISTINCT turn is created, and `null` when an existing
|
|
1903
|
+
* in-flight row is merely updated — the runner uses that to count turns (not events). User turns are
|
|
1904
|
+
* `Role='User'`, assistant turns `Role='AI'`. When recording is active, `MediaType='Audio'` and the
|
|
1905
|
+
* media-relative utterance offsets are stamped from the recording clock.
|
|
1806
1906
|
*
|
|
1807
|
-
* @param params The execution parameters (provides conversation id + context user).
|
|
1808
|
-
* @param transcript The transcript turn emitted by the model.
|
|
1907
|
+
* @param params The execution parameters (provides conversation id + context user + session id).
|
|
1908
|
+
* @param transcript The transcript turn (interim delta or final) emitted by the model.
|
|
1909
|
+
* @returns The created row id on first creation of a turn, else `null`.
|
|
1809
1910
|
*/
|
|
1810
1911
|
async persistRealtimeTranscript(params, transcript) {
|
|
1811
|
-
if (!transcript.
|
|
1812
|
-
return;
|
|
1912
|
+
if (!transcript.Text?.trim()) {
|
|
1913
|
+
return null;
|
|
1813
1914
|
}
|
|
1814
1915
|
const conversationID = params.data?.conversationId;
|
|
1815
1916
|
if (!conversationID) {
|
|
1816
|
-
return; // Without a conversation we have nowhere to durably attach the turn.
|
|
1917
|
+
return null; // Without a conversation we have nowhere to durably attach the turn.
|
|
1817
1918
|
}
|
|
1818
1919
|
const md = params.provider || this._activeProvider;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1920
|
+
const roleKey = transcript.Role; // 'user' | 'assistant'
|
|
1921
|
+
const mjRole = transcript.Role === 'user' ? 'User' : 'AI';
|
|
1922
|
+
// ── INTERIM: create the In-Progress row once per turn (first delta) ───────────────────────
|
|
1923
|
+
if (!transcript.IsFinal) {
|
|
1924
|
+
if (this.realtimeInFlightTurns.has(roleKey)) {
|
|
1925
|
+
return null; // already created for this turn; ignore subsequent deltas
|
|
1926
|
+
}
|
|
1927
|
+
const detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1928
|
+
detail.NewRecord();
|
|
1929
|
+
detail.ConversationID = conversationID;
|
|
1930
|
+
detail.Role = mjRole;
|
|
1931
|
+
detail.Message = transcript.Text;
|
|
1932
|
+
detail.Status = 'In-Progress';
|
|
1933
|
+
this.applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, /*atStart*/ true);
|
|
1934
|
+
if (params.agentSessionID) {
|
|
1935
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1936
|
+
}
|
|
1937
|
+
if (!await detail.Save()) {
|
|
1938
|
+
this.logError(`Failed to create in-progress realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1939
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1940
|
+
});
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
this.realtimeInFlightTurns.set(roleKey, detail.ID);
|
|
1944
|
+
return detail.ID;
|
|
1945
|
+
}
|
|
1946
|
+
// ── FINAL: update the in-flight row (or create+finalize when no interim was seen) ─────────
|
|
1947
|
+
const inFlightId = this.realtimeInFlightTurns.get(roleKey);
|
|
1948
|
+
this.realtimeInFlightTurns.delete(roleKey);
|
|
1949
|
+
let detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1950
|
+
let created = false;
|
|
1951
|
+
if (inFlightId && await detail.Load(inFlightId)) {
|
|
1952
|
+
// updating the existing streaming row → not a new turn
|
|
1953
|
+
}
|
|
1954
|
+
else {
|
|
1955
|
+
detail.NewRecord();
|
|
1956
|
+
detail.ConversationID = conversationID;
|
|
1957
|
+
detail.Role = mjRole;
|
|
1958
|
+
this.applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, /*atStart*/ true);
|
|
1959
|
+
if (params.agentSessionID) {
|
|
1960
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1961
|
+
}
|
|
1962
|
+
created = true;
|
|
1963
|
+
}
|
|
1823
1964
|
detail.Message = transcript.Text;
|
|
1824
|
-
|
|
1825
|
-
|
|
1965
|
+
detail.Status = 'Complete';
|
|
1966
|
+
detail.TurnEndedAt = new Date();
|
|
1967
|
+
if (this.realtimeRecording) {
|
|
1968
|
+
detail.UtteranceEndMs = this.realtimeRecording.NowOffsetMs();
|
|
1826
1969
|
}
|
|
1827
1970
|
if (!await detail.Save()) {
|
|
1828
|
-
this.logError(`Failed to
|
|
1829
|
-
agent: params.agent,
|
|
1830
|
-
|
|
1971
|
+
this.logError(`Failed to finalize realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1972
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
return created ? detail.ID : null;
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Stamps the speaker identity and recording-relative media fields on a freshly-created turn row.
|
|
1979
|
+
* `UserID` is set only for **user** turns (an AI turn has no human speaker). When recording is
|
|
1980
|
+
* active, `MediaType='Audio'` and `UtteranceStartMs` is captured from the recording clock.
|
|
1981
|
+
*
|
|
1982
|
+
* @param detail The new conversation-detail row.
|
|
1983
|
+
* @param transcript The transcript turn.
|
|
1984
|
+
* @param params The execution parameters.
|
|
1985
|
+
* @param atStart Whether this is the turn's start (stamps `UtteranceStartMs`).
|
|
1986
|
+
*/
|
|
1987
|
+
applyRealtimeTurnSpeakerAndMedia(detail, transcript, params, atStart) {
|
|
1988
|
+
if (transcript.Role === 'user' && params.contextUser?.ID) {
|
|
1989
|
+
detail.UserID = params.contextUser.ID;
|
|
1990
|
+
}
|
|
1991
|
+
if (this.realtimeRecording) {
|
|
1992
|
+
detail.MediaType = 'Audio';
|
|
1993
|
+
if (atStart) {
|
|
1994
|
+
detail.UtteranceStartMs = this.realtimeRecording.NowOffsetMs();
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Resolves whether to record this realtime session, OFF by default, with the precedence
|
|
2000
|
+
* **runtime param > agent (`RecordingDefault`) > off**, hard-gated by consent and a resolvable
|
|
2001
|
+
* storage provider. Returns the recording controller + the resolved storage account, or `null`
|
|
2002
|
+
* to record nothing (fail-closed). Never throws — any resolution problem disables recording.
|
|
2003
|
+
*
|
|
2004
|
+
* Storage resolves to **`AIAgent.RecordingStorageProviderID` ?? `AIAgent.AttachmentStorageProviderID`**
|
|
2005
|
+
* (recordings default to the attachments account), then to that provider's first account. With no
|
|
2006
|
+
* provider configured, or consent not granted, recording is OFF.
|
|
2007
|
+
*
|
|
2008
|
+
* @param params The execution parameters (agent + runtime `data.recording`).
|
|
2009
|
+
* @returns `{ controller, storageAccountId }` when recording is enabled, else `null`.
|
|
2010
|
+
*/
|
|
2011
|
+
async resolveRealtimeRecording(params) {
|
|
2012
|
+
try {
|
|
2013
|
+
const agent = params.agent;
|
|
2014
|
+
const runtime = (params.data?.recording ?? null);
|
|
2015
|
+
// Media: runtime > agent default > off.
|
|
2016
|
+
const rawMedia = runtime?.media ?? agent.RecordingDefault ?? 'None';
|
|
2017
|
+
const media = rawMedia === 'Audio' || rawMedia === 'AudioVideo' ? rawMedia : 'None';
|
|
2018
|
+
if (media === 'None') {
|
|
2019
|
+
return null; // recording off
|
|
2020
|
+
}
|
|
2021
|
+
// Consent is a HARD gate — never record without explicit consent.
|
|
2022
|
+
if (runtime?.consent !== true) {
|
|
2023
|
+
this.logStatus('🔴 Realtime recording requested but consent was not granted — recording disabled.', false, params);
|
|
2024
|
+
return null;
|
|
2025
|
+
}
|
|
2026
|
+
// Storage: recording provider, else attachment provider; then that provider's first account.
|
|
2027
|
+
const storageAccountId = params.contextUser
|
|
2028
|
+
? await resolveRecordingStorageAccountID(agent, params.contextUser, params.provider || this._activeProvider)
|
|
2029
|
+
: null;
|
|
2030
|
+
if (!storageAccountId) {
|
|
2031
|
+
this.logStatus('🔴 Realtime recording on but no resolvable storage account (RecordingStorageProviderID/AttachmentStorageProviderID) — recording disabled.', false, params);
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
const controller = new RealtimeRecordingController({ Media: media });
|
|
2035
|
+
return { controller, storageAccountId };
|
|
2036
|
+
}
|
|
2037
|
+
catch (error) {
|
|
2038
|
+
this.logError(`Failed to resolve realtime recording (recording disabled): ${error instanceof Error ? error.message : String(error)}`, {
|
|
2039
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
2040
|
+
});
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
/**
|
|
2045
|
+
* Finalizes the active recording after the session closes: encodes the captured audio to a WAV,
|
|
2046
|
+
* stores it via MJStorage to the resolved account, links it to the `AIAgentSession` (via
|
|
2047
|
+
* `MJ: File Entity Record Links`), and stamps `RecordingFileID` / `RecordingMedia` /
|
|
2048
|
+
* `RecordingStartedAt` on the session. Never throws — a recording failure must not fail the
|
|
2049
|
+
* session run. No-op when recording is off, nothing was captured, or there is no session id.
|
|
2050
|
+
*
|
|
2051
|
+
* @param params The execution parameters (provides the session id + context user + provider).
|
|
2052
|
+
*/
|
|
2053
|
+
async finalizeRealtimeRecording(params) {
|
|
2054
|
+
const controller = this.realtimeRecording;
|
|
2055
|
+
if (!controller) {
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
// One-shot: clear instance state up front so a re-entrant/duplicate Stop can't double-store.
|
|
2059
|
+
this.realtimeRecording = null;
|
|
2060
|
+
const storageAccountId = this.realtimeRecordingAccountId;
|
|
2061
|
+
this.realtimeRecordingAccountId = null;
|
|
2062
|
+
try {
|
|
2063
|
+
controller.Stop();
|
|
2064
|
+
const sessionID = params.agentSessionID;
|
|
2065
|
+
const contextUser = params.contextUser;
|
|
2066
|
+
if (!sessionID || !storageAccountId || !contextUser) {
|
|
2067
|
+
return; // nowhere to attach / store (or no user context to store under)
|
|
2068
|
+
}
|
|
2069
|
+
const encoded = controller.EncodeWav();
|
|
2070
|
+
if (!encoded) {
|
|
2071
|
+
this.logStatus('🔇 Realtime session produced no audio to record.', true, params);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const md = params.provider || this._activeProvider;
|
|
2075
|
+
// Capture-time waveform peaks (max-abs per bucket, normalized 0..1) computed from the
|
|
2076
|
+
// same mixed PCM as the WAV — persisted as a peaks.json sidecar so the player renders the
|
|
2077
|
+
// real waveform without re-decoding the audio. Best-effort: an empty array writes no sidecar.
|
|
2078
|
+
const peaks = controller.GetPeaks();
|
|
2079
|
+
const fileID = await storeRealtimeRecording({
|
|
2080
|
+
Audio: encoded.Buffer,
|
|
2081
|
+
MimeType: 'audio/wav',
|
|
2082
|
+
Media: controller.Media,
|
|
2083
|
+
StartedAt: controller.StartedAt ?? new Date(),
|
|
2084
|
+
StorageAccountID: storageAccountId,
|
|
2085
|
+
SessionID: sessionID,
|
|
2086
|
+
ContextUser: contextUser,
|
|
2087
|
+
Provider: md,
|
|
2088
|
+
Peaks: peaks.length > 0 ? peaks : undefined
|
|
2089
|
+
});
|
|
2090
|
+
if (fileID) {
|
|
2091
|
+
this.logStatus(`🎬 Realtime recording stored (${Math.round(encoded.DurationMs / 1000)}s, file ${fileID}).`, true, params);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
catch (error) {
|
|
2095
|
+
this.logError(`Failed to finalize realtime recording: ${error instanceof Error ? error.message : String(error)}`, {
|
|
2096
|
+
agent: params.agent, category: 'RealtimeSession'
|
|
1831
2097
|
});
|
|
1832
2098
|
}
|
|
1833
2099
|
}
|
|
@@ -2093,6 +2359,32 @@ export class BaseAgent {
|
|
|
2093
2359
|
this._injectedMemory = result;
|
|
2094
2360
|
return result;
|
|
2095
2361
|
}
|
|
2362
|
+
/**
|
|
2363
|
+
* Inject this agent's scoped prompt parts into the conversation, role-faithfully.
|
|
2364
|
+
*
|
|
2365
|
+
* Parallels {@link InjectContextMemory}: resolves `MJ: Scoped Prompt Parts` for the agent's
|
|
2366
|
+
* primary prompt under the run's polymorphic scope (the SAME PrimaryScope/SecondaryScopes the
|
|
2367
|
+
* runtime threads for memory), and unshifts the assembled role-tagged messages onto
|
|
2368
|
+
* `conversationMessages`. In-memory + synchronous (parts are cached on `AIEngine`). No-op when
|
|
2369
|
+
* the agent has no active prompt or no parts resolve for the scope.
|
|
2370
|
+
*/
|
|
2371
|
+
InjectScopedPromptParts(agent, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes) {
|
|
2372
|
+
try {
|
|
2373
|
+
const prompts = AIEngine.Instance.AgentPrompts
|
|
2374
|
+
.filter(ap => UUIDsEqual(ap.AgentID, agent.ID) && ap.Status === 'Active')
|
|
2375
|
+
.sort((a, b) => (a.ExecutionOrder ?? 0) - (b.ExecutionOrder ?? 0));
|
|
2376
|
+
if (prompts.length === 0)
|
|
2377
|
+
return;
|
|
2378
|
+
// Obtain the (possibly downstream-overridden) resolver via the class factory, so any
|
|
2379
|
+
// consumer can plug in custom inclusion/scope logic by subclassing PromptComponentResolver.
|
|
2380
|
+
const resolver = MJGlobal.Instance.ClassFactory.CreateInstance(PromptComponentResolver) ??
|
|
2381
|
+
new PromptComponentResolver();
|
|
2382
|
+
InjectScopedPromptParts(resolver, prompts[0].PromptID, { primaryScopeEntityId, primaryScopeRecordId, secondaryScopes }, conversationMessages);
|
|
2383
|
+
}
|
|
2384
|
+
catch (e) {
|
|
2385
|
+
this.logError(e instanceof Error ? e : new Error(String(e)), { category: 'ScopedPromptParts' });
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2096
2388
|
/**
|
|
2097
2389
|
* Inject pre-execution RAG context for this agent using scoped search.
|
|
2098
2390
|
*
|
|
@@ -2608,6 +2900,20 @@ export class BaseAgent {
|
|
|
2608
2900
|
* @returns
|
|
2609
2901
|
*/
|
|
2610
2902
|
async validateNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
2903
|
+
// Plan Mode enforcement: while active and not yet approved, block Actions/Sub-Agent so the
|
|
2904
|
+
// agent cannot skip straight to execution — it must present a Plan first. Chat, Retry,
|
|
2905
|
+
// Skill activation, ForEach/While, and ClientTools are all still allowed (e.g. asking a
|
|
2906
|
+
// clarifying question, or loading a skill's instructions, before forming the plan).
|
|
2907
|
+
if (this._planModeActive && !this._planApproved && (nextStep.step === 'Actions' || nextStep.step === 'Sub-Agent')) {
|
|
2908
|
+
// nextStep.step is narrowed to 'Actions' | 'Sub-Agent' here, so it can never already be
|
|
2909
|
+
// 'Retry' — always increment (we're demoting it to Retry from a non-retry step).
|
|
2910
|
+
this._generalValidationRetryCount++;
|
|
2911
|
+
return {
|
|
2912
|
+
step: 'Retry',
|
|
2913
|
+
terminate: false,
|
|
2914
|
+
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.'
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2611
2917
|
// for next step, let's do a little quick validation here for sub-agent and actions to ensure requests are valid
|
|
2612
2918
|
switch (nextStep.step) {
|
|
2613
2919
|
case 'Sub-Agent':
|
|
@@ -2628,6 +2934,15 @@ export class BaseAgent {
|
|
|
2628
2934
|
case 'While':
|
|
2629
2935
|
// While loops are valid - no additional validation needed
|
|
2630
2936
|
return nextStep;
|
|
2937
|
+
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
2938
|
+
// union (it's non-terminal, like 'ClientTools' — see the type's doc comment).
|
|
2939
|
+
case 'Skill':
|
|
2940
|
+
return this.validateSkillNextStep(params, nextStep, currentPayload, agentRun, currentStep);
|
|
2941
|
+
// Type assertion required because 'Plan' is not part of the BaseAgentNextStep step
|
|
2942
|
+
// union (it's non-terminal — the terminal step it produces is 'Chat', see
|
|
2943
|
+
// executePlanStep's doc comment for why).
|
|
2944
|
+
case 'Plan':
|
|
2945
|
+
return this.validatePlanNextStep(params, nextStep, currentPayload, agentRun, currentStep);
|
|
2631
2946
|
case 'ClientTools':
|
|
2632
2947
|
// Client tools are valid - execution handled by executeClientToolsStep
|
|
2633
2948
|
return nextStep;
|
|
@@ -2670,7 +2985,7 @@ export class BaseAgent {
|
|
|
2670
2985
|
return nextStep.subAgent ? [nextStep.subAgent] : [];
|
|
2671
2986
|
}
|
|
2672
2987
|
async validateSubAgentNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
2673
|
-
const curAgentSubAgents =
|
|
2988
|
+
const curAgentSubAgents = this.getEffectiveSubAgentsForValidation(params.agent.ID);
|
|
2674
2989
|
// Collect requested sub-agents. Prefer plural `subAgents` (parallel fan-out);
|
|
2675
2990
|
// fall back to singular `subAgent` for the classic single-sub-agent next step.
|
|
2676
2991
|
const requested = this.getRequestedSubAgents(nextStep);
|
|
@@ -2849,6 +3164,118 @@ export class BaseAgent {
|
|
|
2849
3164
|
const agentActions = AIEngine.Instance.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, agentId) && aa.Status === 'Active');
|
|
2850
3165
|
return ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)) && a.Status === 'Active');
|
|
2851
3166
|
}
|
|
3167
|
+
/**
|
|
3168
|
+
* Gets the effective sub-agents for validation, using runtime subAgentChanges if available.
|
|
3169
|
+
* Falls back to the database-configured relationship set if _effectiveSubAgents is empty.
|
|
3170
|
+
* Mirrors {@link getEffectiveActionsForValidation}.
|
|
3171
|
+
*
|
|
3172
|
+
* @param agentId - The ID of the agent to get sub-agents for
|
|
3173
|
+
* @returns Array of effective sub-agents available to the agent
|
|
3174
|
+
* @protected
|
|
3175
|
+
*/
|
|
3176
|
+
getEffectiveSubAgentsForValidation(agentId) {
|
|
3177
|
+
if (this._effectiveSubAgents.length > 0) {
|
|
3178
|
+
return this._effectiveSubAgents;
|
|
3179
|
+
}
|
|
3180
|
+
// Fallback: compute from database configuration (ParentID children + AgentRelationships)
|
|
3181
|
+
return AIEngine.Instance.GetSubAgents(agentId, 'Active');
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* Validates that the requested skill(s) are known and allowed for this agent (resolved via
|
|
3185
|
+
* {@link AIEngine.GetSkillsForAgent}, which enforces the agent's AcceptsSkills gate + the
|
|
3186
|
+
* catalog/grant Status chain). Subclasses can override to implement custom validation logic.
|
|
3187
|
+
*
|
|
3188
|
+
* Mirrors {@link validateActionsNextStep}'s fuzzy-name-matching UX: an exact case-insensitive
|
|
3189
|
+
* match is tried first, falling back to a CONTAINS match when exactly one candidate matches.
|
|
3190
|
+
*
|
|
3191
|
+
* @protected
|
|
3192
|
+
*/
|
|
3193
|
+
async validateSkillNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
3194
|
+
const requested = nextStep.skillActivations ?? [];
|
|
3195
|
+
if (requested.length === 0) {
|
|
3196
|
+
if (nextStep.step !== 'Retry') {
|
|
3197
|
+
this._generalValidationRetryCount++;
|
|
3198
|
+
}
|
|
3199
|
+
return {
|
|
3200
|
+
step: 'Retry',
|
|
3201
|
+
terminate: false,
|
|
3202
|
+
errorMessage: 'When activating a skill, 1 or more skills must be specified'
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
// Plan Mode × skills: agent-initiated activations are only legal BEFORE plan approval,
|
|
3206
|
+
// so the plan the human reviews always reflects the widened tool surface. Once the plan
|
|
3207
|
+
// is approved, a new activation would expand capabilities the reviewer never saw — the
|
|
3208
|
+
// agent must present an updated plan instead (the normal re-plan path).
|
|
3209
|
+
if (this._planModeActive && this._planApproved) {
|
|
3210
|
+
if (nextStep.step !== 'Retry') {
|
|
3211
|
+
this._generalValidationRetryCount++;
|
|
3212
|
+
}
|
|
3213
|
+
return {
|
|
3214
|
+
step: 'Retry',
|
|
3215
|
+
terminate: false,
|
|
3216
|
+
errorMessage: 'Skill activations are not allowed after your plan has been approved — ' +
|
|
3217
|
+
'the approved plan did not include these capabilities. Present an updated plan ' +
|
|
3218
|
+
"(nextStep.type='Plan') that includes the skill(s) you need and why, so the user " +
|
|
3219
|
+
'can review the expanded tool surface.'
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
// Agent-initiated (self-)activation is governed by the DOUBLE activation gate: the agent's
|
|
3223
|
+
// SkillActivationMode AND each skill's ActivationMode must both be 'Auto'. RequestedOnly
|
|
3224
|
+
// skills can only enter a run via an explicit user /skill request (requestedSkillIDs) —
|
|
3225
|
+
// never via this step. This is the same set the prompt catalog was built from, so a
|
|
3226
|
+
// well-behaved model can only name skills that pass; the re-check here is the enforcement
|
|
3227
|
+
// boundary against hallucinated or smuggled names.
|
|
3228
|
+
const availableSkills = AIEngine.Instance.GetAutoActivatableSkillsForAgent(params.agent, params.contextUser);
|
|
3229
|
+
const missingSkills = requested.filter(req => {
|
|
3230
|
+
const requestedName = req.name.trim().toLowerCase();
|
|
3231
|
+
const exactMatch = availableSkills.find(s => s.Name.trim().toLowerCase() === requestedName);
|
|
3232
|
+
if (exactMatch)
|
|
3233
|
+
return false;
|
|
3234
|
+
const containsMatches = availableSkills.filter(s => s.Name.trim().toLowerCase().includes(requestedName));
|
|
3235
|
+
if (containsMatches.length === 1) {
|
|
3236
|
+
this.logStatus(`Skill name fuzzy matched: '${req.name}' → '${containsMatches[0].Name}'`, true, params);
|
|
3237
|
+
req.name = containsMatches[0].Name;
|
|
3238
|
+
return false;
|
|
3239
|
+
}
|
|
3240
|
+
return true;
|
|
3241
|
+
});
|
|
3242
|
+
if (missingSkills.length > 0) {
|
|
3243
|
+
const missingNames = missingSkills.map(s => s.name).join(', ');
|
|
3244
|
+
const availableNames = availableSkills.map(s => s.Name).join(', ') || '(none)';
|
|
3245
|
+
this.logError(`Skill(s) '${missingNames}' not found or not available for agent '${params.agent.Name}'. Available: ${availableNames}`, {
|
|
3246
|
+
agent: params.agent,
|
|
3247
|
+
category: 'SkillExecution'
|
|
3248
|
+
});
|
|
3249
|
+
if (nextStep.step !== 'Retry') {
|
|
3250
|
+
this._generalValidationRetryCount++;
|
|
3251
|
+
}
|
|
3252
|
+
return {
|
|
3253
|
+
step: 'Retry',
|
|
3254
|
+
terminate: false,
|
|
3255
|
+
errorMessage: `Skill(s) '${missingNames}' not found or not available. Available: ${availableNames}`
|
|
3256
|
+
};
|
|
3257
|
+
}
|
|
3258
|
+
return nextStep;
|
|
3259
|
+
}
|
|
3260
|
+
/**
|
|
3261
|
+
* Validates that a 'Plan' next step (Plan Mode) has plan text to present. Subclasses can
|
|
3262
|
+
* override to add additional plan-quality checks (e.g. minimum length, required sections).
|
|
3263
|
+
*
|
|
3264
|
+
* @protected
|
|
3265
|
+
*/
|
|
3266
|
+
async validatePlanNextStep(params, nextStep, currentPayload, agentRun, currentStep) {
|
|
3267
|
+
if (!nextStep.planDetails?.plan || nextStep.planDetails.plan.trim().length === 0) {
|
|
3268
|
+
if (nextStep.step !== 'Retry') {
|
|
3269
|
+
this._generalValidationRetryCount++;
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
step: 'Retry',
|
|
3273
|
+
terminate: false,
|
|
3274
|
+
errorMessage: 'Plan text is required when presenting a Plan for approval'
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
return nextStep;
|
|
3278
|
+
}
|
|
2852
3279
|
/**
|
|
2853
3280
|
* Validates that the Success next step is valid and can be executed by the current agent. Subclasses can override
|
|
2854
3281
|
* this method to implement custom validation logic if needed.
|
|
@@ -4367,6 +4794,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4367
4794
|
}
|
|
4368
4795
|
// Store for later validation in executeActionsStep
|
|
4369
4796
|
this._effectiveActions = activeActions;
|
|
4797
|
+
// Store for later validation in validateSubAgentNextStep (see getEffectiveSubAgentsForValidation)
|
|
4798
|
+
this._effectiveSubAgents = uniqueActiveSubAgents;
|
|
4370
4799
|
// Agent type prompt params: reuse cached base merge unless a runtime override is present.
|
|
4371
4800
|
const runtimePromptParamOverrides = extraData?.__agentTypePromptParams;
|
|
4372
4801
|
let agentTypePromptParams;
|
|
@@ -4385,6 +4814,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4385
4814
|
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
4386
4815
|
// Build app context section if provided in extraData
|
|
4387
4816
|
const appContext = this.buildAppContextSection(extraData);
|
|
4817
|
+
// Skill catalog (name + description only — progressive disclosure). This is the
|
|
4818
|
+
// SELF-ACTIVATION surface, so it uses the double-gated auto set: empty unless the
|
|
4819
|
+
// agent's SkillActivationMode is 'Auto', and containing only skills whose own
|
|
4820
|
+
// ActivationMode is 'Auto' (RequestedOnly skills never appear — they can only enter
|
|
4821
|
+
// a run via an explicit user /skill request). Also empty for AcceptsSkills='None',
|
|
4822
|
+
// and filtered by the acting user's Run permission (open-by-default) so the agent
|
|
4823
|
+
// is never even offered a skill the user isn't entitled to — the permission
|
|
4824
|
+
// boundary is enforced at the catalog, not just at activation.
|
|
4825
|
+
const availableSkills = engine.GetAutoActivatableSkillsForAgent(agent, _contextUser);
|
|
4826
|
+
const skillsCatalog = this.formatSkillsCatalog(availableSkills);
|
|
4388
4827
|
const contextData = {
|
|
4389
4828
|
agentName: agent.Name,
|
|
4390
4829
|
agentDescription: agent.Description,
|
|
@@ -4394,6 +4833,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4394
4833
|
actionCount: activeActions.length,
|
|
4395
4834
|
actionDetails: actionDetails,
|
|
4396
4835
|
clientToolDetails: clientToolDetails,
|
|
4836
|
+
skillCount: availableSkills.length,
|
|
4837
|
+
skillsCatalog: skillsCatalog,
|
|
4838
|
+
planModeActive: this._planModeActive,
|
|
4839
|
+
planApproved: this._planApproved,
|
|
4397
4840
|
appContext: appContext,
|
|
4398
4841
|
};
|
|
4399
4842
|
// Build the final result with __agentTypePromptParams injected
|
|
@@ -4925,6 +5368,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4925
5368
|
return line;
|
|
4926
5369
|
}).join('\n');
|
|
4927
5370
|
}
|
|
5371
|
+
/**
|
|
5372
|
+
* Formats the skill CATALOG as compact markdown — name + description ONLY. This is
|
|
5373
|
+
* progressive disclosure by design: the LLM sees just enough to decide whether to activate a
|
|
5374
|
+
* skill (via a 'Skill' next step), but never sees `Instructions` until it does. Instructions
|
|
5375
|
+
* are appended separately in {@link buildSkillActivationMessage} on activation.
|
|
5376
|
+
*
|
|
5377
|
+
* @private
|
|
5378
|
+
*/
|
|
5379
|
+
formatSkillsCatalog(skills) {
|
|
5380
|
+
return skills.map(s => `- **${s.Name}** — ${s.Description ?? '(no description)'}`).join('\n');
|
|
5381
|
+
}
|
|
4928
5382
|
/**
|
|
4929
5383
|
* Utility method to get agent prompt parameters for a given agent. This gets the
|
|
4930
5384
|
* highest priority prompt for the agent, and then gets the parameters for that
|
|
@@ -5000,44 +5454,41 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5000
5454
|
/**
|
|
5001
5455
|
* Build the client tool prompt section for system prompt injection.
|
|
5002
5456
|
*
|
|
5003
|
-
*
|
|
5004
|
-
*
|
|
5005
|
-
*
|
|
5006
|
-
*
|
|
5457
|
+
* Resolution is delegated to the shared, tier-agnostic {@link ResolveClientTools}
|
|
5458
|
+
* (`@memberjunction/ai-core-plus`) — the single source of truth used by the async
|
|
5459
|
+
* path (here), the realtime co-agent broker, and the conversations runtime. Tiers,
|
|
5460
|
+
* highest precedence first:
|
|
5461
|
+
*
|
|
5462
|
+
* 1. **override** — tools passed directly in the run's `data.clientTools`
|
|
5463
|
+
* 2. **session (dynamic)** — client-SDK enriched tools from {@link ClientToolRequestManager}
|
|
5464
|
+
* 3. **app** — tools the active surface published in the app-context capability manifest
|
|
5465
|
+
* 4. **static** — the agent's metadata tools from the `AI Agent Client Tools` junction
|
|
5466
|
+
*
|
|
5467
|
+
* NOTE (behavior change): the previous inline merge resolved *static-wins* (metadata
|
|
5468
|
+
* was added first and won name collisions). The unified resolver uses the more-correct
|
|
5469
|
+
* *override > session > app > static* — a runtime/dynamic tool now overrides a stale
|
|
5470
|
+
* static metadata tool of the same name. Collisions are rare in practice.
|
|
5007
5471
|
*/
|
|
5008
5472
|
buildClientToolPromptSection(agent, extraData) {
|
|
5009
|
-
|
|
5010
|
-
// 1. Metadata tools from junction table (authoritative source)
|
|
5473
|
+
// Static tier — agent's metadata tools from the AI Agent Client Tools junction.
|
|
5011
5474
|
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)
|
|
5475
|
+
const staticTools = engine.GetClientToolsForAgent(agent.ID).map(tool => ({
|
|
5476
|
+
Name: tool.Name,
|
|
5477
|
+
Description: tool.Description,
|
|
5478
|
+
InputSchema: tool.InputSchemaJSON ? JSON.parse(tool.InputSchemaJSON) : {},
|
|
5479
|
+
OutputSchema: tool.OutputSchemaJSON ? JSON.parse(tool.OutputSchemaJSON) : undefined,
|
|
5480
|
+
Category: tool.Category || undefined,
|
|
5481
|
+
DefaultTimeoutMs: tool.DefaultTimeoutMs || undefined
|
|
5482
|
+
}));
|
|
5483
|
+
// Dynamic (session) tier — client-SDK enriched tools for this session.
|
|
5024
5484
|
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());
|
|
5485
|
+
const sessionTools = sessionID ? ClientToolRequestManager.Instance.GetSessionTools(sessionID) : [];
|
|
5486
|
+
// App tier — tools the active surface published in the app-context capability manifest.
|
|
5487
|
+
const appContext = extraData?.appContext;
|
|
5488
|
+
const appTools = appContext?.Capabilities?.Tools ?? [];
|
|
5489
|
+
// Override tier — tools passed directly in the run's data.
|
|
5490
|
+
const overrideTools = extraData?.clientTools ?? [];
|
|
5491
|
+
const tools = ResolveClientTools({ agentId: agent.ID, staticTools, sessionTools, appTools, overrideTools });
|
|
5041
5492
|
if (tools.length === 0) {
|
|
5042
5493
|
return ''; // No client tools available
|
|
5043
5494
|
}
|
|
@@ -5234,19 +5685,112 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5234
5685
|
if (typeof value === 'boolean' || typeof value === 'number') {
|
|
5235
5686
|
return `\`${String(value)}\``;
|
|
5236
5687
|
}
|
|
5237
|
-
let stringValue;
|
|
5238
5688
|
if (typeof value === 'string') {
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
//
|
|
5243
|
-
|
|
5689
|
+
// A string param may carry JSON (many actions JSON.stringify their payloads),
|
|
5690
|
+
// SQL/TS code, or plain text. Try structural JSON compression first — it is a
|
|
5691
|
+
// safe no-op on non-JSON (crushParamValue's internal JSON.parse failure is
|
|
5692
|
+
// caught and returns null) — then opt-in AST code reduction (SQL/TS), then pass
|
|
5693
|
+
// through (optionally length-capped).
|
|
5694
|
+
const crushedJson = this.crushParamValue(value);
|
|
5695
|
+
if (crushedJson !== null) {
|
|
5696
|
+
return crushedJson;
|
|
5697
|
+
}
|
|
5698
|
+
const crushedCode = this.crushCodeValue(value);
|
|
5699
|
+
if (crushedCode !== null) {
|
|
5700
|
+
return crushedCode;
|
|
5701
|
+
}
|
|
5702
|
+
return maxLength > 0 && value.length > maxLength ? `${value.substring(0, maxLength)}…` : value;
|
|
5703
|
+
}
|
|
5704
|
+
// Objects/arrays: compact JSON (no pretty-printing), optionally structurally
|
|
5705
|
+
// compressed via context-crush when crushing is enabled and the value is large.
|
|
5706
|
+
const stringValue = JSON.stringify(value);
|
|
5707
|
+
const crushed = this.crushParamValue(stringValue);
|
|
5708
|
+
if (crushed !== null) {
|
|
5709
|
+
return crushed;
|
|
5244
5710
|
}
|
|
5245
5711
|
if (maxLength > 0 && stringValue.length > maxLength) {
|
|
5246
5712
|
return `${stringValue.substring(0, maxLength)}…`;
|
|
5247
5713
|
}
|
|
5248
5714
|
return stringValue;
|
|
5249
5715
|
}
|
|
5716
|
+
/**
|
|
5717
|
+
* Resolve the per-run action-result compression config from the agent-type prompt
|
|
5718
|
+
* params. Crushing is on by default and only disabled when an agent explicitly sets
|
|
5719
|
+
* `crushActionResults: false`, mirroring the `includeXxxDocs` opt-out convention.
|
|
5720
|
+
* @private
|
|
5721
|
+
*/
|
|
5722
|
+
resolveActionResultCrush(params) {
|
|
5723
|
+
const agentTypePromptParams = params.data?.__agentTypePromptParams;
|
|
5724
|
+
if (agentTypePromptParams?.crushActionResults === false) {
|
|
5725
|
+
return undefined;
|
|
5726
|
+
}
|
|
5727
|
+
const requestedLang = agentTypePromptParams?.crushCodeLang;
|
|
5728
|
+
const codeLang = requestedLang === 'sql' || requestedLang === 'typescript' ? requestedLang : undefined;
|
|
5729
|
+
return { threshold: BaseAgent.ACTION_RESULT_CRUSH_THRESHOLD, maxChars: undefined, codeLang };
|
|
5730
|
+
}
|
|
5731
|
+
/**
|
|
5732
|
+
* AST-reduce a large code-string action-result value when the agent opted into a code
|
|
5733
|
+
* language (via `crushCodeLang` or a subclass override) and the value clears the size
|
|
5734
|
+
* threshold. Returns reduced code plus a one-line legend, or null to keep the string
|
|
5735
|
+
* verbatim (crushing disabled, too small, or no net saving).
|
|
5736
|
+
*
|
|
5737
|
+
* token optimization via @memberjunction/context-crush (CodeCompressor-inspired)
|
|
5738
|
+
* @private
|
|
5739
|
+
*/
|
|
5740
|
+
crushCodeValue(stringValue) {
|
|
5741
|
+
const config = this._actionResultCrush;
|
|
5742
|
+
if (!config || !config.codeLang || stringValue.length < config.threshold) {
|
|
5743
|
+
return null;
|
|
5744
|
+
}
|
|
5745
|
+
// Crushing is a best-effort optimization — it must never break an agent turn. Any
|
|
5746
|
+
// failure falls back to the verbatim value.
|
|
5747
|
+
try {
|
|
5748
|
+
const result = CrushCode(stringValue, config.codeLang);
|
|
5749
|
+
if (result.CrushedChars >= result.OriginalChars) {
|
|
5750
|
+
return null;
|
|
5751
|
+
}
|
|
5752
|
+
const legend = DescribeCrush(result);
|
|
5753
|
+
return legend ? `${result.Text}\n ↳ ${legend}` : result.Text;
|
|
5754
|
+
}
|
|
5755
|
+
catch {
|
|
5756
|
+
return null;
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
/**
|
|
5760
|
+
* Structurally compress a JSON action-result value when crushing is enabled for the run
|
|
5761
|
+
* and the value clears the size threshold. Accepts either the `JSON.stringify` of an
|
|
5762
|
+
* object/array param, or a raw string param that itself contains JSON (many actions
|
|
5763
|
+
* stringify their payloads, e.g. `run-adhoc-query`'s `Results`). Returns crushed text
|
|
5764
|
+
* plus a one-line legend, or null when crushing is disabled, the value is too small, the
|
|
5765
|
+
* value isn't valid JSON, or compression wouldn't actually save characters — so callers
|
|
5766
|
+
* fall back to verbatim (and, for strings, to code crushing) behavior.
|
|
5767
|
+
*
|
|
5768
|
+
* token optimization via @memberjunction/context-crush (SmartCrusher-inspired)
|
|
5769
|
+
* @private
|
|
5770
|
+
*/
|
|
5771
|
+
crushParamValue(stringValue) {
|
|
5772
|
+
const config = this._actionResultCrush;
|
|
5773
|
+
if (!config || stringValue.length < config.threshold) {
|
|
5774
|
+
return null;
|
|
5775
|
+
}
|
|
5776
|
+
// Crushing is a best-effort optimization — it must never break an agent turn. Any
|
|
5777
|
+
// failure (non-JSON input, pathologically deep payloads) falls back to verbatim.
|
|
5778
|
+
try {
|
|
5779
|
+
// Parse to a plain JSON value. This is the JSON.stringify of an object/array
|
|
5780
|
+
// param, or a raw string param that contains JSON; non-JSON strings throw here
|
|
5781
|
+
// and are caught below (caller then tries code crushing / verbatim).
|
|
5782
|
+
const json = JSON.parse(stringValue);
|
|
5783
|
+
const result = CrushJSON(json, { MaxChars: config.maxChars });
|
|
5784
|
+
if (result.CrushedChars >= result.OriginalChars) {
|
|
5785
|
+
return null; // no net saving — keep the verbatim JSON
|
|
5786
|
+
}
|
|
5787
|
+
const legend = DescribeCrush(result);
|
|
5788
|
+
return legend ? `${result.Text}\n ↳ ${legend}` : result.Text;
|
|
5789
|
+
}
|
|
5790
|
+
catch {
|
|
5791
|
+
return null;
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5250
5794
|
/**
|
|
5251
5795
|
* Formats a parameter value for display in action execution messages.
|
|
5252
5796
|
* Truncates long strings and formats objects/arrays for readability.
|
|
@@ -5458,6 +6002,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5458
6002
|
}
|
|
5459
6003
|
// Reset prompt turn counter for this execution
|
|
5460
6004
|
this._promptTurnCount = 0;
|
|
6005
|
+
// Resolve action-result compression config for this run (default on; opt out via
|
|
6006
|
+
// crushActionResults: false in the agent-type prompt params).
|
|
6007
|
+
this._actionResultCrush = this.resolveActionResultCrush(params);
|
|
5461
6008
|
// Create MJAIAgentRunEntity
|
|
5462
6009
|
this._agentRun = await (params.provider || this._activeProvider).GetEntityObject('MJ: AI Agent Runs', params.contextUser);
|
|
5463
6010
|
this._agentRun.AgentID = params.agent.ID;
|
|
@@ -5581,6 +6128,20 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5581
6128
|
: [params.agent.Name || 'Unknown Agent'];
|
|
5582
6129
|
this._depth = params.parentDepth !== undefined ? params.parentDepth + 1 : 0;
|
|
5583
6130
|
this._parentStepCounts = params.parentStepCounts || [];
|
|
6131
|
+
// Resolve Plan Mode gate state for this run (must happen before the main loop starts —
|
|
6132
|
+
// gatherPromptTemplateData/validateNextStep both read _planModeActive/_planApproved — and
|
|
6133
|
+
// after _depth is set above, since the gate only applies to root agents).
|
|
6134
|
+
const planModeGate = await this.resolvePlanModeGate(params);
|
|
6135
|
+
this._planModeActive = planModeGate.active;
|
|
6136
|
+
this._planApproved = planModeGate.approved;
|
|
6137
|
+
// Stamp the run record so the UX (run-header Plan Mode chip) and plan-drift audits can
|
|
6138
|
+
// tell plan-mode runs apart without re-deriving gate state from steps/requests.
|
|
6139
|
+
if (this._agentRun && planModeGate.active) {
|
|
6140
|
+
this._agentRun.PlanMode = true;
|
|
6141
|
+
}
|
|
6142
|
+
// Pre-activate any user-requested skills (from a `/skill-name` composer mention). Must run
|
|
6143
|
+
// after _depth is set (root-only) and after the run is persisted (records a Skill step).
|
|
6144
|
+
await this.preActivateRequestedSkills(params);
|
|
5584
6145
|
// Reset execution chain and progress tracking
|
|
5585
6146
|
this._allProgressSteps = [];
|
|
5586
6147
|
// Update params with the modified payload if auto-populated
|
|
@@ -5590,6 +6151,66 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5590
6151
|
params.payload = modifiedParams.payload;
|
|
5591
6152
|
}
|
|
5592
6153
|
}
|
|
6154
|
+
/**
|
|
6155
|
+
* Resolves whether Plan Mode is active for this run, and whether its approval gate is already
|
|
6156
|
+
* satisfied. Called once from {@link initializeAgentRun}, after `_depth` is set.
|
|
6157
|
+
*
|
|
6158
|
+
* - `active`: this is a root agent (`_depth === 0`) AND either `agent.RequirePlanMode`
|
|
6159
|
+
* (mandatory HITL — forces plan mode on every root run regardless of the per-request flag;
|
|
6160
|
+
* `SupportsPlanMode` is irrelevant when set) OR `agent.SupportsPlanMode` (capability,
|
|
6161
|
+
* default ON/opt-out) AND `params.planMode` (per-request, default OFF). Sub-agents never
|
|
6162
|
+
* gate on Plan Mode — only the top-level agent the user/caller invoked does.
|
|
6163
|
+
* - `approved`: only meaningful when `active`. True when `params.lastRunId` points to a prior
|
|
6164
|
+
* run whose Plan step's `MJ: AI Agent Requests` row resolved to `Approved` or `Responded`
|
|
6165
|
+
* (a `Rejected` plan — or no matching request at all — leaves the gate unsatisfied, sending
|
|
6166
|
+
* the agent back to present a revised plan).
|
|
6167
|
+
*
|
|
6168
|
+
* Override to change Plan Mode eligibility rules (e.g. gate on a specific agent category).
|
|
6169
|
+
*
|
|
6170
|
+
* @protected
|
|
6171
|
+
*/
|
|
6172
|
+
async resolvePlanModeGate(params) {
|
|
6173
|
+
const requiredByAgent = params.agent.RequirePlanMode === true;
|
|
6174
|
+
const requestedByCaller = !!(params.agent.SupportsPlanMode && params.planMode === true);
|
|
6175
|
+
const active = this._depth === 0 && (requiredByAgent || requestedByCaller);
|
|
6176
|
+
if (!active) {
|
|
6177
|
+
return { active: false, approved: false };
|
|
6178
|
+
}
|
|
6179
|
+
if (!params.lastRunId) {
|
|
6180
|
+
return { active: true, approved: false };
|
|
6181
|
+
}
|
|
6182
|
+
const rv = new RunView();
|
|
6183
|
+
const requestResult = await rv.RunView({
|
|
6184
|
+
EntityName: 'MJ: AI Agent Requests',
|
|
6185
|
+
ExtraFilter: `OriginatingAgentRunID='${params.lastRunId}'`,
|
|
6186
|
+
Fields: ['Status', 'OriginatingAgentRunStepID'],
|
|
6187
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
6188
|
+
MaxRows: 1,
|
|
6189
|
+
ResultType: 'simple'
|
|
6190
|
+
}, params.contextUser);
|
|
6191
|
+
if (!requestResult.Success || requestResult.Results.length === 0) {
|
|
6192
|
+
return { active: true, approved: false };
|
|
6193
|
+
}
|
|
6194
|
+
const request = requestResult.Results[0];
|
|
6195
|
+
const resolved = request.Status === 'Approved' || request.Status === 'Responded';
|
|
6196
|
+
if (!resolved || !request.OriginatingAgentRunStepID) {
|
|
6197
|
+
return { active: true, approved: false };
|
|
6198
|
+
}
|
|
6199
|
+
// Confirm the request actually originated from a Plan step — a resolved request from an
|
|
6200
|
+
// unrelated Chat clarification (asked before the agent could even form a plan) must NOT
|
|
6201
|
+
// satisfy the Plan Mode gate.
|
|
6202
|
+
const stepResult = await rv.RunView({
|
|
6203
|
+
EntityName: 'MJ: AI Agent Run Steps',
|
|
6204
|
+
ExtraFilter: `ID='${request.OriginatingAgentRunStepID}'`,
|
|
6205
|
+
Fields: ['StepType'],
|
|
6206
|
+
MaxRows: 1,
|
|
6207
|
+
ResultType: 'simple'
|
|
6208
|
+
}, params.contextUser);
|
|
6209
|
+
const approved = stepResult.Success
|
|
6210
|
+
&& stepResult.Results.length > 0
|
|
6211
|
+
&& stepResult.Results[0].StepType === 'Plan';
|
|
6212
|
+
return { active: true, approved };
|
|
6213
|
+
}
|
|
5593
6214
|
/**
|
|
5594
6215
|
* Validates the agent with tracking.
|
|
5595
6216
|
*
|
|
@@ -5664,9 +6285,25 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5664
6285
|
})
|
|
5665
6286
|
: undefined
|
|
5666
6287
|
});
|
|
6288
|
+
// Skill observability: persist the invocation records for this step. Prompt steps
|
|
6289
|
+
// default to everything currently in effect (so every turn's injection is auditable);
|
|
6290
|
+
// other step types only carry skills when the caller attributes them explicitly.
|
|
6291
|
+
const skillsForStep = params.skills
|
|
6292
|
+
?? (params.stepType === 'Prompt' && this._skillInvocations.length > 0
|
|
6293
|
+
? this._skillInvocations
|
|
6294
|
+
: undefined);
|
|
6295
|
+
if (skillsForStep && skillsForStep.length > 0) {
|
|
6296
|
+
stepEntity.Skills = JSON.stringify(skillsForStep);
|
|
6297
|
+
}
|
|
5667
6298
|
// Fire-and-forget the 'started' INSERT — the agent flow never blocks on a step save. The queue
|
|
5668
6299
|
// tracks the INSERT so every later UPDATE (queueStepSave) chains AFTER it commits.
|
|
5669
|
-
|
|
6300
|
+
// When the step has a parent, chain the INSERT AFTER the parent's INSERT to satisfy the
|
|
6301
|
+
// self-referencing FK_AIAgentRunStep_ParentID constraint — without this, a child INSERT that
|
|
6302
|
+
// races the parent INSERT hits an FK violation (especially under large-payload parent INSERTs).
|
|
6303
|
+
const parentStepEntity = params.parentId && this._agentRun?.Steps
|
|
6304
|
+
? this._agentRun.Steps.find(s => UUIDsEqual(s.ID, params.parentId))
|
|
6305
|
+
: undefined;
|
|
6306
|
+
this._stepSaveQueue.Insert(stepEntity, parentStepEntity);
|
|
5670
6307
|
// Add the step to the agent run's Steps array
|
|
5671
6308
|
if (this._agentRun) {
|
|
5672
6309
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -5941,6 +6578,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5941
6578
|
return await this.processSubAgentStep(params, previousDecision, undefined, undefined, stepCount);
|
|
5942
6579
|
case 'Actions':
|
|
5943
6580
|
return await this.executeActionsStep(params, previousDecision, undefined, true, stepCount);
|
|
6581
|
+
// Type assertion required because 'Skill' is not part of the BaseAgentNextStep step
|
|
6582
|
+
// union (non-terminal, like 'ClientTools') — LoopAgentType.DetermineNextStep() emits it
|
|
6583
|
+
// when the LLM chooses to activate a skill.
|
|
6584
|
+
case 'Skill':
|
|
6585
|
+
return await this.executeSkillStep(params, config, previousDecision, stepCount);
|
|
6586
|
+
// Type assertion required because 'Plan' is not part of the BaseAgentNextStep step
|
|
6587
|
+
// union — LoopAgentType.DetermineNextStep() emits it when the LLM presents a plan
|
|
6588
|
+
// (Plan Mode). executePlanStep's terminal return is 'Chat'-shaped (see its doc comment).
|
|
6589
|
+
case 'Plan':
|
|
6590
|
+
return await this.executePlanStep(params, previousDecision);
|
|
5944
6591
|
// Type assertion required because 'ClientTools' is not part of the BaseAgentNextStep
|
|
5945
6592
|
// step union — LoopAgentType.DetermineNextStep() emits it when the LLM chooses client tools.
|
|
5946
6593
|
case 'ClientTools':
|
|
@@ -6521,7 +7168,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6521
7168
|
* @param parentStepId - Optional ID of parent step (e.g., ForEach/While loop step) for proper UI hierarchy
|
|
6522
7169
|
* @param subAgentPayloadOverride - Optional payload override for sub-agent execution, if provided the normal payload computation is skipped
|
|
6523
7170
|
*/
|
|
6524
|
-
async executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0) {
|
|
7171
|
+
async executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount = 0, resolvedSubAgentEntity) {
|
|
6525
7172
|
const subAgentRequest = previousDecision.subAgent;
|
|
6526
7173
|
// Check for cancellation before starting
|
|
6527
7174
|
if (params.cancellationToken?.aborted) {
|
|
@@ -6557,13 +7204,18 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6557
7204
|
conversationMessages: params.conversationMessages,
|
|
6558
7205
|
parentAgentHierarchy: this._agentHierarchy
|
|
6559
7206
|
};
|
|
6560
|
-
// Get sub-agent entity to access payload paths
|
|
6561
|
-
|
|
6562
|
-
|
|
7207
|
+
// Get sub-agent entity to access payload paths. Prefer the entity the caller already
|
|
7208
|
+
// resolved (resolveSubAgentByName — covers ParentID children AND runtime-granted
|
|
7209
|
+
// sub-agents from skill activations / subAgentChanges); fall back to the ParentID
|
|
7210
|
+
// lookup, then the effective set, for any legacy direct callers of this method.
|
|
7211
|
+
const subAgentEntity = resolvedSubAgentEntity
|
|
7212
|
+
?? AIEngine.Instance.Agents.find(a => a.Name === subAgentRequest.name &&
|
|
7213
|
+
UUIDsEqual(a.ParentID, params.agent.ID))
|
|
7214
|
+
?? this.getEffectiveSubAgentsForValidation(params.agent.ID).find(a => a.Name.trim().toLowerCase() === subAgentRequest.name?.trim().toLowerCase());
|
|
6563
7215
|
if (!subAgentEntity) {
|
|
6564
7216
|
throw new Error(`Sub-agent '${subAgentRequest.name}' not found`);
|
|
6565
7217
|
}
|
|
6566
|
-
const stepEntity = await this.createStepEntity({ stepType: 'Sub-Agent', stepName: `Execute Sub-Agent: ${subAgentRequest.name}`, contextUser: params.contextUser, targetId: subAgentEntity.ID, inputData, payloadAtStart: previousDecision.newPayload, parentId: parentStepId });
|
|
7218
|
+
const stepEntity = await this.createStepEntity({ stepType: 'Sub-Agent', stepName: `Execute Sub-Agent: ${subAgentRequest.name}`, contextUser: params.contextUser, targetId: subAgentEntity.ID, inputData, payloadAtStart: previousDecision.newPayload, parentId: parentStepId, skills: this.getSkillAttributionForSubAgent(subAgentEntity, params.agent) });
|
|
6567
7219
|
// Increment execution count for this sub-agent
|
|
6568
7220
|
this.incrementExecutionCount(subAgentEntity.ID);
|
|
6569
7221
|
try {
|
|
@@ -6826,16 +7478,24 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6826
7478
|
return await this.executeRelatedSubAgentStep(params, previousDecision, resolved.subAgentEntity, resolved.relationship, parentStepId, subAgentPayloadOverride, stepCount);
|
|
6827
7479
|
}
|
|
6828
7480
|
if (resolved) {
|
|
6829
|
-
return await this.executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount);
|
|
6830
|
-
}
|
|
6831
|
-
|
|
7481
|
+
return await this.executeChildSubAgentStep(params, previousDecision, parentStepId, subAgentPayloadOverride, stepCount, resolved.subAgentEntity);
|
|
7482
|
+
}
|
|
7483
|
+
// Execution-time resolution failure. Count it against the shared validation-retry cap
|
|
7484
|
+
// (MAX_VALIDATION_RETRIES) so a model that keeps picking an unresolvable sub-agent fails
|
|
7485
|
+
// the run with a clear guardrail message instead of looping forever, and tell the model
|
|
7486
|
+
// exactly which sub-agents ARE available so it can self-correct on the next turn.
|
|
7487
|
+
const availableNames = this.getEffectiveSubAgentsForValidation(params.agent.ID)
|
|
7488
|
+
.map(a => a.Name).join(', ') || '(none)';
|
|
7489
|
+
this.logError(`Sub-agent '${name}' not found or not active for agent '${params.agent.Name}'. Available sub-agents: ${availableNames}`, {
|
|
6832
7490
|
agent: params.agent,
|
|
6833
7491
|
category: 'SubAgentExecution'
|
|
6834
7492
|
});
|
|
7493
|
+
this._generalValidationRetryCount++;
|
|
6835
7494
|
return {
|
|
6836
7495
|
step: 'Retry',
|
|
6837
7496
|
terminate: false,
|
|
6838
|
-
errorMessage: `Sub-agent '${name}' not found or not active
|
|
7497
|
+
errorMessage: `Sub-agent '${name}' not found or not active. Available sub-agents: ${availableNames}. ` +
|
|
7498
|
+
`Pick one of the available sub-agents, or complete the task another way — do not request '${name}' again.`,
|
|
6839
7499
|
previousPayload: previousDecision.newPayload,
|
|
6840
7500
|
newPayload: previousDecision.newPayload
|
|
6841
7501
|
};
|
|
@@ -6871,6 +7531,16 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6871
7531
|
return { subAgentEntity: relatedAgent, relationship: rel };
|
|
6872
7532
|
}
|
|
6873
7533
|
}
|
|
7534
|
+
// 3) Runtime-granted sub-agents (skill activation / caller subAgentChanges): resolve from
|
|
7535
|
+
// the SAME effective set the prompt offered and validateSubAgentNextStep approved. Without
|
|
7536
|
+
// this branch, a skill-granted sub-agent passes validation but fails execution ("not found
|
|
7537
|
+
// or not active") — and because the catalog keeps offering it, the model re-picks the same
|
|
7538
|
+
// sub-agent forever (observed live: Research Agent looping 36+ turns on the skill-granted
|
|
7539
|
+
// Infographic Agent). No relationship row exists for these, so they dispatch child-style.
|
|
7540
|
+
const effectiveAgent = this.getEffectiveSubAgentsForValidation(params.agent.ID).find(a => a.Status === 'Active' && a.Name.trim().toLowerCase() === normalized);
|
|
7541
|
+
if (effectiveAgent) {
|
|
7542
|
+
return { subAgentEntity: effectiveAgent };
|
|
7543
|
+
}
|
|
6874
7544
|
return undefined;
|
|
6875
7545
|
}
|
|
6876
7546
|
/**
|
|
@@ -7099,6 +7769,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7099
7769
|
stepName: `Execute Parallel Sub-Agent: ${request.name}`,
|
|
7100
7770
|
contextUser: params.contextUser,
|
|
7101
7771
|
targetId: subAgentEntity.ID,
|
|
7772
|
+
skills: this.getSkillAttributionForSubAgent(subAgentEntity, params.agent),
|
|
7102
7773
|
inputData: {
|
|
7103
7774
|
agentName: params.agent.Name,
|
|
7104
7775
|
subAgentName: request.name,
|
|
@@ -7269,7 +7940,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7269
7940
|
targetId: subAgentEntity.ID,
|
|
7270
7941
|
inputData,
|
|
7271
7942
|
payloadAtStart: previousDecision.newPayload,
|
|
7272
|
-
parentId: parentStepId
|
|
7943
|
+
parentId: parentStepId,
|
|
7944
|
+
skills: this.getSkillAttributionForSubAgent(subAgentEntity, params.agent)
|
|
7273
7945
|
});
|
|
7274
7946
|
// Increment execution count for this sub-agent
|
|
7275
7947
|
this.incrementExecutionCount(subAgentEntity.ID);
|
|
@@ -7778,7 +8450,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7778
8450
|
actionName: aa.name,
|
|
7779
8451
|
actionParams: aa.params
|
|
7780
8452
|
};
|
|
7781
|
-
const stepEntity = await this.createStepEntity({ stepType: 'Actions', stepName: `Execute Action: ${aa.name}`, contextUser: params.contextUser, targetId: actionEntity.ID, inputData: actionInputData, payloadAtStart: currentPayload, payloadAtEnd: currentPayload, parentId: parentStepId });
|
|
8453
|
+
const stepEntity = await this.createStepEntity({ stepType: 'Actions', stepName: `Execute Action: ${aa.name}`, contextUser: params.contextUser, targetId: actionEntity.ID, inputData: actionInputData, payloadAtStart: currentPayload, payloadAtEnd: currentPayload, parentId: parentStepId, skills: this.getSkillAttributionForAction(actionEntity.ID, params.agent) });
|
|
7782
8454
|
lastStep = stepEntity;
|
|
7783
8455
|
// Override step number to ensure unique values for parallel actions
|
|
7784
8456
|
stepEntity.StepNumber = baseStepNumber + numActionsProcessed++;
|
|
@@ -8097,6 +8769,432 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8097
8769
|
});
|
|
8098
8770
|
return `${header}\n${lines.join('\n')}`;
|
|
8099
8771
|
}
|
|
8772
|
+
/**
|
|
8773
|
+
* Executes a 'Skill' next step: activates one or more skills the LLM requested by name.
|
|
8774
|
+
* Activating a skill (1) appends its full `Instructions` to the conversation so they take
|
|
8775
|
+
* effect for the remainder of the run, and (2) enables its bundled Actions/sub-agents by
|
|
8776
|
+
* pushing `root`-scoped `add` entries onto `params.actionChanges`/`params.subAgentChanges` —
|
|
8777
|
+
* the same runtime tool-surface-extension mechanism `ExecuteAgentParams` already exposes to
|
|
8778
|
+
* external callers. This is NOT a nested agent run; it never terminates the loop itself.
|
|
8779
|
+
*
|
|
8780
|
+
* Already-activated skills (tracked in `_activatedSkillIDs`) are skipped — re-requesting an
|
|
8781
|
+
* active skill is a harmless no-op rather than re-appending duplicate instructions.
|
|
8782
|
+
*
|
|
8783
|
+
* Decomposed into {@link resolveSkillActivations}, {@link buildSkillActivationMessage},
|
|
8784
|
+
* {@link enableSkillCapabilities}, and {@link recordSkillActivationStep} — override any of
|
|
8785
|
+
* those for fine-grained control (e.g. custom instruction formatting, additional side effects
|
|
8786
|
+
* on activation) without re-implementing the whole step.
|
|
8787
|
+
*
|
|
8788
|
+
* @protected
|
|
8789
|
+
*/
|
|
8790
|
+
async executeSkillStep(params, config, previousDecision, stepCount = 0) {
|
|
8791
|
+
const requested = previousDecision.skillActivations ?? [];
|
|
8792
|
+
if (requested.length === 0) {
|
|
8793
|
+
// Nothing to activate — continue with next prompt
|
|
8794
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8795
|
+
}
|
|
8796
|
+
const resolvedSkills = this.resolveSkillActivations(requested, params.agent, params.contextUser);
|
|
8797
|
+
const newlyActivated = resolvedSkills.filter(skill => !this._activatedSkillIDs.some(id => UUIDsEqual(id, skill.ID)));
|
|
8798
|
+
if (newlyActivated.length === 0) {
|
|
8799
|
+
// All requested skills are already active this run — no-op, just continue
|
|
8800
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8801
|
+
}
|
|
8802
|
+
const currentPayload = previousDecision?.newPayload || previousDecision?.previousPayload || params.payload;
|
|
8803
|
+
for (const skill of newlyActivated) {
|
|
8804
|
+
// Agent self-activation — carry the model's stated rationale (skillActivations[].reason)
|
|
8805
|
+
// into the provenance record. Names were fuzzy-corrected in validateSkillNextStep, so a
|
|
8806
|
+
// case-insensitive exact match against the request list is reliable here.
|
|
8807
|
+
const request = requested.find(r => r.name.trim().toLowerCase() === skill.Name.trim().toLowerCase());
|
|
8808
|
+
const invocation = this.buildSkillInvocation(skill, params.agent, 'auto', request?.reason);
|
|
8809
|
+
await this.recordSkillActivationStep(skill, currentPayload, params, invocation);
|
|
8810
|
+
this.enableSkillCapabilities(skill, params);
|
|
8811
|
+
this._activatedSkillIDs.push(skill.ID);
|
|
8812
|
+
this._skillInvocations.push(invocation);
|
|
8813
|
+
}
|
|
8814
|
+
const activationMessage = this.buildSkillActivationMessage(newlyActivated);
|
|
8815
|
+
params.conversationMessages.push({
|
|
8816
|
+
role: 'user',
|
|
8817
|
+
content: activationMessage,
|
|
8818
|
+
metadata: {
|
|
8819
|
+
turnAdded: this._promptTurnCount,
|
|
8820
|
+
messageType: 'skill-activation'
|
|
8821
|
+
}
|
|
8822
|
+
});
|
|
8823
|
+
return await this.executePromptStep(params, config, previousDecision, stepCount);
|
|
8824
|
+
}
|
|
8825
|
+
/**
|
|
8826
|
+
* Pre-activates skills the caller explicitly requested via {@link ExecuteAgentParams.requestedSkillIDs}
|
|
8827
|
+
* (typically an end user's `/skill-name` composer mentions), at run start — so their Instructions
|
|
8828
|
+
* and bundled Actions/sub-agents take effect from the first turn rather than waiting for the model
|
|
8829
|
+
* to discover and activate them through the catalog.
|
|
8830
|
+
*
|
|
8831
|
+
* **Root-agent only** (skills never cascade to sub-agents), and each requested skill activates
|
|
8832
|
+
* **only if it survives the guard**: it must be in the set {@link AIEngine.GetSkillsForAgent}
|
|
8833
|
+
* allows for this agent (the AcceptsSkills gate) AND the acting user must have Run permission on it
|
|
8834
|
+
* — both enforced by passing `params.contextUser` to `GetSkillsForAgent`. Requested IDs that fail
|
|
8835
|
+
* either check are silently dropped, so a client can never force-activate a skill the user or agent
|
|
8836
|
+
* isn't entitled to. Reuses the same {@link recordSkillActivationStep} / {@link enableSkillCapabilities}
|
|
8837
|
+
* / {@link buildSkillActivationMessage} machinery as the model-initiated `Skill` step, so activation
|
|
8838
|
+
* is recorded and takes effect identically. Plan Mode is unaffected — pre-activation widens the tool
|
|
8839
|
+
* surface, but the plan-approval gate still blocks executing those tools until the plan is approved.
|
|
8840
|
+
*
|
|
8841
|
+
* @protected
|
|
8842
|
+
*/
|
|
8843
|
+
async preActivateRequestedSkills(params) {
|
|
8844
|
+
if (this._depth !== 0) {
|
|
8845
|
+
return; // skills are root-agent only; never pre-activate on sub-agents
|
|
8846
|
+
}
|
|
8847
|
+
const requestedIds = params.requestedSkillIDs;
|
|
8848
|
+
if (!requestedIds || requestedIds.length === 0) {
|
|
8849
|
+
return;
|
|
8850
|
+
}
|
|
8851
|
+
// Guard: intersect the requested IDs with the agent-accepted ∩ user-permitted set.
|
|
8852
|
+
const allowed = AIEngine.Instance.GetSkillsForAgent(params.agent, params.contextUser);
|
|
8853
|
+
const droppedIds = requestedIds.filter(id => !allowed.some(s => UUIDsEqual(id, s.ID)));
|
|
8854
|
+
if (droppedIds.length > 0) {
|
|
8855
|
+
this.notifyDroppedSkillRequests(droppedIds, params);
|
|
8856
|
+
}
|
|
8857
|
+
const newlyActivated = allowed.filter(s => requestedIds.some(id => UUIDsEqual(id, s.ID)) &&
|
|
8858
|
+
!this._activatedSkillIDs.some(id => UUIDsEqual(id, s.ID)));
|
|
8859
|
+
if (newlyActivated.length === 0) {
|
|
8860
|
+
return;
|
|
8861
|
+
}
|
|
8862
|
+
const currentPayload = params.payload;
|
|
8863
|
+
for (const skill of newlyActivated) {
|
|
8864
|
+
const invocation = this.buildSkillInvocation(skill, params.agent, 'requested');
|
|
8865
|
+
await this.recordSkillActivationStep(skill, currentPayload, params, invocation);
|
|
8866
|
+
this.enableSkillCapabilities(skill, params);
|
|
8867
|
+
this._activatedSkillIDs.push(skill.ID);
|
|
8868
|
+
this._skillInvocations.push(invocation);
|
|
8869
|
+
}
|
|
8870
|
+
const activationMessage = this.buildSkillActivationMessage(newlyActivated);
|
|
8871
|
+
if (!params.conversationMessages) {
|
|
8872
|
+
params.conversationMessages = [];
|
|
8873
|
+
}
|
|
8874
|
+
params.conversationMessages.push({
|
|
8875
|
+
role: 'user',
|
|
8876
|
+
content: activationMessage,
|
|
8877
|
+
metadata: {
|
|
8878
|
+
turnAdded: this._promptTurnCount,
|
|
8879
|
+
messageType: 'skill-activation'
|
|
8880
|
+
}
|
|
8881
|
+
});
|
|
8882
|
+
}
|
|
8883
|
+
/**
|
|
8884
|
+
* Handles user-requested skill IDs that failed the activation guard (agent-accepted ∩
|
|
8885
|
+
* user-permitted). A silent drop leaves both the user AND the agent blind to the refusal —
|
|
8886
|
+
* the agent then improvises around the missing capability instead of explaining it. This
|
|
8887
|
+
* emits a server-side warning log and injects a system note into the conversation so the
|
|
8888
|
+
* agent tells the user why the skill isn't available rather than working around it.
|
|
8889
|
+
*
|
|
8890
|
+
* @protected
|
|
8891
|
+
*/
|
|
8892
|
+
notifyDroppedSkillRequests(droppedIds, params) {
|
|
8893
|
+
const names = droppedIds.map(id => AIEngine.Instance.Skills.find(s => UUIDsEqual(s.ID, id))?.Name ?? id);
|
|
8894
|
+
const reason = params.agent.AcceptsSkills === 'None'
|
|
8895
|
+
? `agent '${params.agent.Name}' does not accept skills (AcceptsSkills='None')`
|
|
8896
|
+
: `the skill(s) are not available to agent '${params.agent.Name}' — not Active, not assigned to it (AcceptsSkills='Limited'), or the user lacks Run permission`;
|
|
8897
|
+
LogErrorEx({
|
|
8898
|
+
message: `Requested skill activation dropped for [${names.join(', ')}]: ${reason}`,
|
|
8899
|
+
severity: 'warning',
|
|
8900
|
+
category: 'AgentSkills'
|
|
8901
|
+
});
|
|
8902
|
+
if (!params.conversationMessages) {
|
|
8903
|
+
params.conversationMessages = [];
|
|
8904
|
+
}
|
|
8905
|
+
params.conversationMessages.push({
|
|
8906
|
+
role: 'user',
|
|
8907
|
+
content: `SYSTEM NOTE: The user requested activation of the following skill(s) for this run: ${names.join(', ')}. The request was NOT honored because ${reason}. Briefly inform the user that the requested skill(s) are not available to you and, if appropriate, suggest an agent that accepts skills or that an administrator can grant this capability. Do NOT attempt to build, delegate, or improvise a workaround for the missing capability.`,
|
|
8908
|
+
metadata: {
|
|
8909
|
+
turnAdded: this._promptTurnCount,
|
|
8910
|
+
messageType: 'skill-activation-refused'
|
|
8911
|
+
}
|
|
8912
|
+
});
|
|
8913
|
+
}
|
|
8914
|
+
/**
|
|
8915
|
+
* Resolves the LLM's requested skill names to `MJ: AI Skills` entities, restricted to what
|
|
8916
|
+
* {@link AIEngine.GetSkillsForAgent} allows for this agent (the AcceptsSkills gate + Status
|
|
8917
|
+
* chain). Names that don't resolve are silently dropped here — {@link validateSkillNextStep}
|
|
8918
|
+
* is responsible for rejecting unknown/disallowed names before execution ever reaches this
|
|
8919
|
+
* point, so by the time `executeSkillStep` runs, every requested name is expected to match.
|
|
8920
|
+
*
|
|
8921
|
+
* Override to change resolution semantics (e.g. resolve by ID instead of Name).
|
|
8922
|
+
*
|
|
8923
|
+
* @protected
|
|
8924
|
+
*/
|
|
8925
|
+
resolveSkillActivations(requested, agent, contextUser) {
|
|
8926
|
+
// Agent-initiated activations resolve against the double-gated AUTO set only —
|
|
8927
|
+
// RequestedOnly skills (on either side of the gate) can never be self-activated, even if
|
|
8928
|
+
// a response somehow names one that validateSkillNextStep didn't catch.
|
|
8929
|
+
const availableSkills = AIEngine.Instance.GetAutoActivatableSkillsForAgent(agent, contextUser);
|
|
8930
|
+
const resolved = [];
|
|
8931
|
+
for (const req of requested) {
|
|
8932
|
+
const requestedName = req.name.trim().toLowerCase();
|
|
8933
|
+
const match = availableSkills.find(s => s.Name.trim().toLowerCase() === requestedName);
|
|
8934
|
+
if (match && !resolved.some(s => UUIDsEqual(s.ID, match.ID))) {
|
|
8935
|
+
resolved.push(match);
|
|
8936
|
+
}
|
|
8937
|
+
}
|
|
8938
|
+
return resolved;
|
|
8939
|
+
}
|
|
8940
|
+
/**
|
|
8941
|
+
* Builds the message appended to `conversationMessages` when skill(s) activate — this is what
|
|
8942
|
+
* actually puts each skill's `Instructions` into effect for the rest of the run. Override to
|
|
8943
|
+
* change formatting (e.g. a more compact representation for a high skill-activation-count agent).
|
|
8944
|
+
*
|
|
8945
|
+
* @protected
|
|
8946
|
+
*/
|
|
8947
|
+
buildSkillActivationMessage(skills) {
|
|
8948
|
+
const sections = skills.map(s => `## Skill Activated: ${s.Name}\n\n${s.Instructions}`);
|
|
8949
|
+
return `The following skill(s) have been activated. Their instructions are now in effect ` +
|
|
8950
|
+
`for the remainder of this run:\n\n${sections.join('\n\n')}`;
|
|
8951
|
+
}
|
|
8952
|
+
/**
|
|
8953
|
+
* Enables a skill's bundled Actions and sub-agents by pushing `specific`-scoped `add` entries
|
|
8954
|
+
* (targeted at exactly the activating agent's ID) onto `params.actionChanges` /
|
|
8955
|
+
* `params.subAgentChanges`. `specific`/`[agent.ID]` is the correct scope for "apply to THIS
|
|
8956
|
+
* agent, at whatever depth it runs, and never leak to its sub-agents":
|
|
8957
|
+
* - {@link doesChangeScopeApply} returns true only when the running agent's ID is in the list,
|
|
8958
|
+
* so it applies to the activating agent regardless of depth (a sub-agent that activates a
|
|
8959
|
+
* skill still gets its tools — which a `root`-scoped change would NOT do, since `root` means
|
|
8960
|
+
* "the depth-0 agent," not "the current agent").
|
|
8961
|
+
* - {@link filterActionChangesForSubAgent} / {@link filterSubAgentChangesForSubAgent} propagate
|
|
8962
|
+
* `specific` as-is, and each downstream agent checks `includes(itsOwnID)` → false, so the
|
|
8963
|
+
* grant never cascades to sub-agents the activating agent later delegates to.
|
|
8964
|
+
* Because `params` is the same object reference used for the rest of this run, every subsequent
|
|
8965
|
+
* turn's `gatherPromptTemplateData()` call picks up the change automatically — no extra plumbing.
|
|
8966
|
+
*
|
|
8967
|
+
* Override to change propagation scope (e.g. a subclass that wants skill-granted capabilities
|
|
8968
|
+
* to cascade to sub-agents could push `scope: 'all-subagents'` instead).
|
|
8969
|
+
*
|
|
8970
|
+
* @protected
|
|
8971
|
+
*/
|
|
8972
|
+
enableSkillCapabilities(skill, params) {
|
|
8973
|
+
const activatingAgentIds = [params.agent.ID];
|
|
8974
|
+
const actionIds = AIEngine.Instance.GetSkillActionIDs(skill.ID);
|
|
8975
|
+
if (actionIds.length > 0) {
|
|
8976
|
+
if (!params.actionChanges) {
|
|
8977
|
+
params.actionChanges = [];
|
|
8978
|
+
}
|
|
8979
|
+
params.actionChanges.push({
|
|
8980
|
+
scope: 'specific',
|
|
8981
|
+
mode: 'add',
|
|
8982
|
+
actionIds,
|
|
8983
|
+
agentIds: activatingAgentIds
|
|
8984
|
+
});
|
|
8985
|
+
}
|
|
8986
|
+
const subAgentIds = AIEngine.Instance.GetSkillSubAgentIDs(skill.ID);
|
|
8987
|
+
if (subAgentIds.length > 0) {
|
|
8988
|
+
if (!params.subAgentChanges) {
|
|
8989
|
+
params.subAgentChanges = [];
|
|
8990
|
+
}
|
|
8991
|
+
params.subAgentChanges.push({
|
|
8992
|
+
scope: 'specific',
|
|
8993
|
+
mode: 'add',
|
|
8994
|
+
subAgentIds,
|
|
8995
|
+
agentIds: activatingAgentIds
|
|
8996
|
+
});
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8999
|
+
/**
|
|
9000
|
+
* Builds the {@link AgentSkillInvocation} observability record for a skill activation —
|
|
9001
|
+
* capturing WHO pulled the trigger and the provenance-of-authority gate values in effect at
|
|
9002
|
+
* activation time, so auditors can see exactly which configuration admitted the skill even
|
|
9003
|
+
* if that configuration later changes.
|
|
9004
|
+
*
|
|
9005
|
+
* @protected
|
|
9006
|
+
*/
|
|
9007
|
+
buildSkillInvocation(skill, agent, activationType, reason) {
|
|
9008
|
+
return {
|
|
9009
|
+
SkillID: skill.ID,
|
|
9010
|
+
SkillName: skill.Name,
|
|
9011
|
+
ActivationType: activationType,
|
|
9012
|
+
Provenance: {
|
|
9013
|
+
AgentAcceptsSkills: agent.AcceptsSkills,
|
|
9014
|
+
SkillActivationMode: skill.ActivationMode,
|
|
9015
|
+
AgentSkillActivationMode: agent.SkillActivationMode,
|
|
9016
|
+
RequestedBy: activationType === 'requested' ? 'user-request' : 'agent-decision'
|
|
9017
|
+
},
|
|
9018
|
+
...(reason ? { Reason: reason } : {})
|
|
9019
|
+
};
|
|
9020
|
+
}
|
|
9021
|
+
/**
|
|
9022
|
+
* Resolves which activated skill(s), if any, granted the given action to this agent — the
|
|
9023
|
+
* attribution recorded on the Actions step's `Skills` column. Returns `undefined` (no
|
|
9024
|
+
* linkage) when the action is one of the agent's NATIVE grants (an Active `MJ: AI Agent
|
|
9025
|
+
* Actions` row), even if an activated skill also bundles it: native authority takes
|
|
9026
|
+
* precedence, and `Skills = NULL` is the contract for "the agent had this tool anyway".
|
|
9027
|
+
*
|
|
9028
|
+
* @protected
|
|
9029
|
+
*/
|
|
9030
|
+
getSkillAttributionForAction(actionId, agent) {
|
|
9031
|
+
if (this._skillInvocations.length === 0 || !actionId) {
|
|
9032
|
+
return undefined;
|
|
9033
|
+
}
|
|
9034
|
+
const isNative = AIEngine.Instance.AgentActions.some(aa => UUIDsEqual(aa.AgentID, agent.ID) && UUIDsEqual(aa.ActionID, actionId) && aa.Status === 'Active');
|
|
9035
|
+
if (isNative) {
|
|
9036
|
+
return undefined;
|
|
9037
|
+
}
|
|
9038
|
+
const granting = this._skillInvocations.filter(inv => AIEngine.Instance.GetSkillActionIDs(inv.SkillID).some(id => UUIDsEqual(id, actionId)));
|
|
9039
|
+
return granting.length > 0 ? granting : undefined;
|
|
9040
|
+
}
|
|
9041
|
+
/**
|
|
9042
|
+
* Resolves which activated skill(s), if any, granted the given sub-agent to this agent — the
|
|
9043
|
+
* attribution recorded on the Sub-Agent step's `Skills` column. Returns `undefined` when the
|
|
9044
|
+
* sub-agent is a NATIVE relationship (a `ParentID` child of this agent, or an Active
|
|
9045
|
+
* `MJ: AI Agent Relationships` referenced-sub-agent row), even if an activated skill also
|
|
9046
|
+
* bundles it — same native-precedence contract as {@link getSkillAttributionForAction}.
|
|
9047
|
+
*
|
|
9048
|
+
* @protected
|
|
9049
|
+
*/
|
|
9050
|
+
getSkillAttributionForSubAgent(subAgent, agent) {
|
|
9051
|
+
if (this._skillInvocations.length === 0 || !subAgent) {
|
|
9052
|
+
return undefined;
|
|
9053
|
+
}
|
|
9054
|
+
const isParentChild = subAgent.ParentID != null && UUIDsEqual(subAgent.ParentID, agent.ID);
|
|
9055
|
+
const isReferenced = AIEngine.Instance.AgentRelationships.some(rel => UUIDsEqual(rel.AgentID, agent.ID) && UUIDsEqual(rel.SubAgentID, subAgent.ID) && rel.Status === 'Active');
|
|
9056
|
+
if (isParentChild || isReferenced) {
|
|
9057
|
+
return undefined;
|
|
9058
|
+
}
|
|
9059
|
+
const granting = this._skillInvocations.filter(inv => AIEngine.Instance.GetSkillSubAgentIDs(inv.SkillID).some(id => UUIDsEqual(id, subAgent.ID)));
|
|
9060
|
+
return granting.length > 0 ? granting : undefined;
|
|
9061
|
+
}
|
|
9062
|
+
/**
|
|
9063
|
+
* Creates and immediately finalizes the `AIAgentRunStep` (StepType='Skill') that records this
|
|
9064
|
+
* skill activation for observability/audit. The step's `Skills` column carries the single
|
|
9065
|
+
* {@link AgentSkillInvocation} performed (activation type, provenance of authority, and the
|
|
9066
|
+
* agent-stated reason when self-activated). Activation is not itself a failure mode today — it
|
|
9067
|
+
* always finalizes as successful — but subclasses can override to add richer InputData/OutputData
|
|
9068
|
+
* or to make activation conditionally fail (e.g. a licensing check).
|
|
9069
|
+
*
|
|
9070
|
+
* @protected
|
|
9071
|
+
*/
|
|
9072
|
+
async recordSkillActivationStep(skill, currentPayload, params, invocation) {
|
|
9073
|
+
const stepEntity = await this.createStepEntity({
|
|
9074
|
+
stepType: 'Skill',
|
|
9075
|
+
stepName: `Skill: ${skill.Name}`,
|
|
9076
|
+
targetId: skill.ID,
|
|
9077
|
+
inputData: { skillName: skill.Name },
|
|
9078
|
+
contextUser: params.contextUser,
|
|
9079
|
+
payloadAtStart: currentPayload,
|
|
9080
|
+
payloadAtEnd: currentPayload,
|
|
9081
|
+
...(invocation ? { skills: [invocation] } : {})
|
|
9082
|
+
});
|
|
9083
|
+
await this.finalizeStepEntity(stepEntity, true, undefined, {
|
|
9084
|
+
skillId: skill.ID,
|
|
9085
|
+
skillName: skill.Name,
|
|
9086
|
+
...(invocation ? {
|
|
9087
|
+
activationType: invocation.ActivationType,
|
|
9088
|
+
requestedBy: invocation.Provenance.RequestedBy,
|
|
9089
|
+
...(invocation.Reason ? { reason: invocation.Reason } : {})
|
|
9090
|
+
} : {})
|
|
9091
|
+
});
|
|
9092
|
+
}
|
|
9093
|
+
/**
|
|
9094
|
+
* Executes a 'Plan' next step (Plan Mode): records a `Plan` run-step, raises the standard
|
|
9095
|
+
* `MJ: AI Agent Requests` HITL request with an editable plan-approval `AgentResponseForm`, and
|
|
9096
|
+
* terminates this run awaiting the human's response — reusing the exact same pause/resume
|
|
9097
|
+
* infrastructure `executeChatStep` uses (`createFeedbackRequest` + the existing
|
|
9098
|
+
* `MJAIAgentRequestEntityServer.Save()` auto-resume-on-status-change hook). A rejected or
|
|
9099
|
+
* edited-and-resubmitted plan resumes as a new linked run via the normal run-chain mechanism;
|
|
9100
|
+
* `resolvePlanModeGate` re-checks approval on that new run so a rejection sends the agent back
|
|
9101
|
+
* to present a revised plan rather than through to execution.
|
|
9102
|
+
*
|
|
9103
|
+
* **Important**: the RETURNED `BaseAgentNextStep.step` is `'Chat'`, not `'Plan'` — 'Plan' is
|
|
9104
|
+
* only ever an intermediate classification (used for the `AIAgentRunStep.StepType` audit
|
|
9105
|
+
* record, which the UI reads to render a plan-approval card instead of a generic chat bubble).
|
|
9106
|
+
* The step returned to the framework must stay within `AIAgentRun.FinalStep`'s DB-CHECK-
|
|
9107
|
+
* constrained, terminal-only vocabulary — `'Plan'` is deliberately not part of it (see the
|
|
9108
|
+
* `BaseAgentNextStep.step` doc comment) — so a plan-approval pause is represented as the same
|
|
9109
|
+
* terminal shape `executeChatStep` already uses.
|
|
9110
|
+
*
|
|
9111
|
+
* @protected
|
|
9112
|
+
*/
|
|
9113
|
+
async executePlanStep(params, previousDecision) {
|
|
9114
|
+
const planText = previousDecision.planDetails?.plan ?? '';
|
|
9115
|
+
const stepEntity = await this.createStepEntity({
|
|
9116
|
+
stepType: 'Plan',
|
|
9117
|
+
stepName: 'Plan Presented for Approval',
|
|
9118
|
+
contextUser: params.contextUser,
|
|
9119
|
+
inputData: { plan: planText }
|
|
9120
|
+
});
|
|
9121
|
+
await this.finalizeStepEntity(stepEntity, true, undefined, { plan: planText });
|
|
9122
|
+
const responseForm = this.buildPlanApprovalForm(planText);
|
|
9123
|
+
const planPresentation = {
|
|
9124
|
+
step: 'Plan',
|
|
9125
|
+
terminate: true,
|
|
9126
|
+
message: previousDecision.message || 'Please review the proposed plan before I proceed.',
|
|
9127
|
+
reasoning: previousDecision.reasoning,
|
|
9128
|
+
confidence: previousDecision.confidence,
|
|
9129
|
+
responseForm
|
|
9130
|
+
};
|
|
9131
|
+
// For root agents, create a persistent AIAgentRequest so the request is tracked in the
|
|
9132
|
+
// dashboard and can be responded to outside a conversation (mirrors executeChatStep).
|
|
9133
|
+
if (this._depth === 0) {
|
|
9134
|
+
await this.createFeedbackRequest(params, stepEntity, planPresentation);
|
|
9135
|
+
}
|
|
9136
|
+
return {
|
|
9137
|
+
step: 'Chat',
|
|
9138
|
+
terminate: true,
|
|
9139
|
+
message: planPresentation.message,
|
|
9140
|
+
reasoning: previousDecision.reasoning,
|
|
9141
|
+
confidence: previousDecision.confidence,
|
|
9142
|
+
previousPayload: previousDecision.previousPayload,
|
|
9143
|
+
newPayload: previousDecision.newPayload || previousDecision.previousPayload,
|
|
9144
|
+
responseForm
|
|
9145
|
+
};
|
|
9146
|
+
}
|
|
9147
|
+
/**
|
|
9148
|
+
* Builds the editable plan-approval `AgentResponseForm`: the Markdown-rendered plan (with an
|
|
9149
|
+
* Edit toggle so the human can amend it before approving), an optional feedback field that
|
|
9150
|
+
* travels back to the agent with the decision (most useful on Reject — it steers the re-plan),
|
|
9151
|
+
* and an Approve/Reject button group. Override to change the card's layout (e.g. split the
|
|
9152
|
+
* plan into per-step checkboxes instead of one field).
|
|
9153
|
+
*
|
|
9154
|
+
* Approval is a HIGHER-ORDER signal, not just a form reply: conversation hosts detect
|
|
9155
|
+
* `decision === 'approve'` on this form and switch the conversation out of Plan Mode
|
|
9156
|
+
* (see ng-conversations' plan-decision handling), so the follow-up run executes the approved
|
|
9157
|
+
* plan instead of planning again. Rejection keeps Plan Mode on — the agent re-plans with the
|
|
9158
|
+
* feedback in context.
|
|
9159
|
+
*
|
|
9160
|
+
* @protected
|
|
9161
|
+
*/
|
|
9162
|
+
buildPlanApprovalForm(planText) {
|
|
9163
|
+
return {
|
|
9164
|
+
title: 'Review Plan',
|
|
9165
|
+
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.',
|
|
9166
|
+
submitLabel: 'Submit',
|
|
9167
|
+
questions: [
|
|
9168
|
+
{
|
|
9169
|
+
id: 'plan',
|
|
9170
|
+
label: 'Plan',
|
|
9171
|
+
// markdown: agents author plans in Markdown (see the plan-mode prompt
|
|
9172
|
+
// instructions); the UI renders a formatted preview with an Edit toggle.
|
|
9173
|
+
type: { type: 'textarea', markdown: true },
|
|
9174
|
+
defaultValue: planText,
|
|
9175
|
+
required: true
|
|
9176
|
+
},
|
|
9177
|
+
{
|
|
9178
|
+
id: 'reason',
|
|
9179
|
+
label: 'Feedback',
|
|
9180
|
+
type: { type: 'textarea', placeholder: 'Optional — if rejecting, tell the agent what to change and it will re-plan.' },
|
|
9181
|
+
required: false
|
|
9182
|
+
},
|
|
9183
|
+
{
|
|
9184
|
+
id: 'decision',
|
|
9185
|
+
label: 'Decision',
|
|
9186
|
+
type: {
|
|
9187
|
+
type: 'buttongroup',
|
|
9188
|
+
options: [
|
|
9189
|
+
{ value: 'approve', label: 'Approve' },
|
|
9190
|
+
{ value: 'reject', label: 'Reject' }
|
|
9191
|
+
]
|
|
9192
|
+
},
|
|
9193
|
+
required: true
|
|
9194
|
+
}
|
|
9195
|
+
]
|
|
9196
|
+
};
|
|
9197
|
+
}
|
|
8100
9198
|
async executeChatStep(params, previousDecision) {
|
|
8101
9199
|
const stepEntity = await this.createStepEntity({ stepType: 'Chat', stepName: 'User Interaction', contextUser: params.contextUser });
|
|
8102
9200
|
// Chat steps are successful - they indicate a need for user interaction
|
|
@@ -9302,6 +10400,13 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9302
10400
|
async pruneAndCompactExpiredMessages(params, currentTurn) {
|
|
9303
10401
|
const messagesToCompact = [];
|
|
9304
10402
|
const messagesToRemove = [];
|
|
10403
|
+
// Cache-aware guard: confine pruning/compaction to the volatile tail so we don't
|
|
10404
|
+
// perturb the provider's KV-cached prompt prefix. The stable prefix is the maximal
|
|
10405
|
+
// contiguous leading run of non-result messages (system/RAG context, injected
|
|
10406
|
+
// memory, the original user request). Expired messages that fall inside that prefix
|
|
10407
|
+
// are deferred — genuine context overflow still reaches them via attemptContextRecovery.
|
|
10408
|
+
// token optimization via @memberjunction/context-crush (CacheAligner-inspired)
|
|
10409
|
+
const { Boundary: stablePrefixBoundary } = PartitionStablePrefix(params.conversationMessages, (msg) => !this.IsVolatileResultMessage(msg));
|
|
9305
10410
|
// Phase 1: Identify expired messages
|
|
9306
10411
|
for (let i = 0; i < params.conversationMessages.length; i++) {
|
|
9307
10412
|
const msg = params.conversationMessages[i];
|
|
@@ -9318,6 +10423,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9318
10423
|
const turnsAlive = currentTurn - turnAdded;
|
|
9319
10424
|
// Check if expired
|
|
9320
10425
|
if (turnsAlive > msg.metadata.expirationTurns) {
|
|
10426
|
+
// Defer expiry of messages inside the cache-stable prefix to preserve the
|
|
10427
|
+
// provider's cached prompt prefix; overflow recovery handles them if needed.
|
|
10428
|
+
if (i < stablePrefixBoundary) {
|
|
10429
|
+
this.logStatus(`[Turn ${currentTurn}] Deferred expiry of cache-stable prefix message at index ${i}`, true, params);
|
|
10430
|
+
continue;
|
|
10431
|
+
}
|
|
9321
10432
|
msg.metadata.isExpired = true;
|
|
9322
10433
|
if (msg.metadata.expirationMode === 'Remove') {
|
|
9323
10434
|
messagesToRemove.push(i);
|
|
@@ -9563,6 +10674,21 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9563
10674
|
|| messageType === 'client-tool-result'
|
|
9564
10675
|
|| messageType === 'tool-result';
|
|
9565
10676
|
}
|
|
10677
|
+
/**
|
|
10678
|
+
* Returns true if the message is a turn-generated result (action, tool, client tool,
|
|
10679
|
+
* sub-agent, or loop). These are the volatile, expirable messages that accumulate over
|
|
10680
|
+
* turns. Everything else — system/RAG context, injected memory, the original user
|
|
10681
|
+
* request — anchors the cache-stable prompt prefix and is protected from routine pruning.
|
|
10682
|
+
* @protected
|
|
10683
|
+
*/
|
|
10684
|
+
IsVolatileResultMessage(msg) {
|
|
10685
|
+
const messageType = msg.metadata?.messageType;
|
|
10686
|
+
return messageType === 'action-result'
|
|
10687
|
+
|| messageType === 'client-tool-result'
|
|
10688
|
+
|| messageType === 'tool-result'
|
|
10689
|
+
|| messageType === 'sub-agent-result'
|
|
10690
|
+
|| messageType === 'loop-result';
|
|
10691
|
+
}
|
|
9566
10692
|
estimateTokens(content, modelName) {
|
|
9567
10693
|
const text = typeof content === 'string'
|
|
9568
10694
|
? content
|