@memberjunction/ai-agents 5.40.1 → 5.41.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 +45 -0
- package/dist/AgentRunner.d.ts +5 -2
- package/dist/AgentRunner.d.ts.map +1 -1
- package/dist/AgentRunner.js +14 -4
- package/dist/AgentRunner.js.map +1 -1
- package/dist/MemoryWriteManager.d.ts +188 -0
- package/dist/MemoryWriteManager.d.ts.map +1 -0
- package/dist/MemoryWriteManager.js +299 -0
- package/dist/MemoryWriteManager.js.map +1 -0
- package/dist/agent-context-injector.d.ts +29 -0
- package/dist/agent-context-injector.d.ts.map +1 -1
- package/dist/agent-context-injector.js +90 -32
- package/dist/agent-context-injector.js.map +1 -1
- package/dist/agent-memory-context-builder.d.ts +100 -0
- package/dist/agent-memory-context-builder.d.ts.map +1 -0
- package/dist/agent-memory-context-builder.js +172 -0
- package/dist/agent-memory-context-builder.js.map +1 -0
- package/dist/agent-types/index.d.ts +1 -0
- package/dist/agent-types/index.d.ts.map +1 -1
- package/dist/agent-types/index.js +1 -0
- package/dist/agent-types/index.js.map +1 -1
- package/dist/agent-types/loop-agent-response-type.d.ts +12 -1
- package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-response-type.js.map +1 -1
- package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
- package/dist/agent-types/loop-agent-type.js +4 -0
- package/dist/agent-types/loop-agent-type.js.map +1 -1
- package/dist/agent-types/realtime-agent-type.d.ts +146 -0
- package/dist/agent-types/realtime-agent-type.d.ts.map +1 -0
- package/dist/agent-types/realtime-agent-type.js +176 -0
- package/dist/agent-types/realtime-agent-type.js.map +1 -0
- package/dist/base-agent.d.ts +365 -24
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +995 -175
- 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 +15 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +55 -2
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +261 -62
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/realtime/meeting-controls-channel-server.d.ts +198 -0
- package/dist/realtime/meeting-controls-channel-server.d.ts.map +1 -0
- package/dist/realtime/meeting-controls-channel-server.js +319 -0
- package/dist/realtime/meeting-controls-channel-server.js.map +1 -0
- package/dist/realtime/meeting-controls-state.d.ts +191 -0
- package/dist/realtime/meeting-controls-state.d.ts.map +1 -0
- package/dist/realtime/meeting-controls-state.js +219 -0
- package/dist/realtime/meeting-controls-state.js.map +1 -0
- package/dist/realtime/realtime-channel-server-host.d.ts +166 -0
- package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -0
- package/dist/realtime/realtime-channel-server-host.js +378 -0
- package/dist/realtime/realtime-channel-server-host.js.map +1 -0
- package/dist/realtime/realtime-client-session-service.d.ts +884 -0
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -0
- package/dist/realtime/realtime-client-session-service.js +1401 -0
- package/dist/realtime/realtime-client-session-service.js.map +1 -0
- package/dist/realtime/realtime-coagent-config.d.ts +202 -0
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -0
- package/dist/realtime/realtime-coagent-config.js +334 -0
- package/dist/realtime/realtime-coagent-config.js.map +1 -0
- package/dist/realtime/realtime-narration.d.ts +67 -0
- package/dist/realtime/realtime-narration.d.ts.map +1 -0
- package/dist/realtime/realtime-narration.js +127 -0
- package/dist/realtime/realtime-narration.js.map +1 -0
- package/dist/realtime/realtime-session-runner.d.ts +383 -0
- package/dist/realtime/realtime-session-runner.d.ts.map +1 -0
- package/dist/realtime/realtime-session-runner.js +532 -0
- package/dist/realtime/realtime-session-runner.js.map +1 -0
- package/dist/realtime/realtime-tool-broker.d.ts +279 -0
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -0
- package/dist/realtime/realtime-tool-broker.js +184 -0
- package/dist/realtime/realtime-tool-broker.js.map +1 -0
- package/dist/realtime/whiteboard-channel-server.d.ts +50 -0
- package/dist/realtime/whiteboard-channel-server.d.ts.map +1 -0
- package/dist/realtime/whiteboard-channel-server.js +85 -0
- package/dist/realtime/whiteboard-channel-server.js.map +1 -0
- package/package.json +17 -17
package/dist/base-agent.js
CHANGED
|
@@ -11,22 +11,25 @@
|
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase } from '@memberjunction/core-entities';
|
|
14
|
-
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } from '@memberjunction/core';
|
|
14
|
+
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase, EntitySaveOptions } from '@memberjunction/core';
|
|
15
15
|
import { AgentRunWatchdog } from './agent-run-watchdog.js';
|
|
16
16
|
import { AIPromptRunner } from '@memberjunction/ai-prompts';
|
|
17
|
+
import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
|
|
17
18
|
import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
18
|
-
import { CopyScalarsAndArrays, JSONValidator, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
|
|
19
|
+
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
|
|
20
|
+
import { RealtimeSessionRunner } from './realtime/realtime-session-runner.js';
|
|
21
|
+
import { ResolveNarrationInstructionsTemplate } from './realtime/realtime-narration.js';
|
|
22
|
+
import { BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
|
|
19
23
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
20
24
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
21
25
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
22
|
-
import {
|
|
23
|
-
import { AgentPreExecutionRAG } from './agent-pre-execution-rag.js';
|
|
24
|
-
import { RerankerService } from '@memberjunction/ai-reranker';
|
|
26
|
+
import { AgentMemoryContextBuilder } from './agent-memory-context-builder.js';
|
|
25
27
|
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
|
|
26
28
|
import { AgentRunner } from './AgentRunner.js';
|
|
27
29
|
import { PayloadManager } from './PayloadManager.js';
|
|
28
30
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
29
31
|
import { ArtifactToolManager } from './ArtifactToolManager.js';
|
|
32
|
+
import { MemoryWriteManager } from './MemoryWriteManager.js';
|
|
30
33
|
import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
|
|
31
34
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
32
35
|
import { ClientToolRequestManager } from './ClientToolRequestManager.js';
|
|
@@ -103,6 +106,16 @@ export class BaseAgent {
|
|
|
103
106
|
* INSERT on millisecond-fast steps (e.g. pipelines), which left them stuck at `Running`.
|
|
104
107
|
*/
|
|
105
108
|
this._stepSavePromises = new Map();
|
|
109
|
+
/**
|
|
110
|
+
* Per-step 'started'-INSERT promises, keyed by the step entity instance. The create INSERT is
|
|
111
|
+
* fire-and-forget (the agent flow never blocks on it — the PK is client-generated by NewRecord() so
|
|
112
|
+
* the ID is valid immediately). Every UPDATE-phase save ({@link queueStepSave}) chains after this
|
|
113
|
+
* INSERT promise so the UPDATE never races ahead of the create, and force-persists with
|
|
114
|
+
* `IgnoreDirtyState` so a mutation absorbed by the INSERT's post-save dirty-reset (which silently
|
|
115
|
+
* no-op'd UPDATEs and left fast create→finalize steps stuck at Status='Running') is still written.
|
|
116
|
+
* A WeakMap so entries are GC'd with the entity.
|
|
117
|
+
*/
|
|
118
|
+
this._stepInsertPromises = new WeakMap();
|
|
106
119
|
/**
|
|
107
120
|
* Active per-request metadata provider, set at the start of Execute().
|
|
108
121
|
* Defaults to the global Metadata.Provider; overridden when a per-request
|
|
@@ -209,6 +222,11 @@ export class BaseAgent {
|
|
|
209
222
|
* Allows agents to explore input artifacts on demand.
|
|
210
223
|
*/
|
|
211
224
|
this._artifactToolManager = new ArtifactToolManager();
|
|
225
|
+
/**
|
|
226
|
+
* Manages in-flight durable memory writes for the current agent run.
|
|
227
|
+
* Only consulted when the agent has AllowMemoryWrite enabled.
|
|
228
|
+
*/
|
|
229
|
+
this._memoryWriteManager = new MemoryWriteManager();
|
|
212
230
|
/**
|
|
213
231
|
* Effective actions available to this agent after applying actionChanges.
|
|
214
232
|
* Populated during gatherPromptTemplateData() and used for validation in executeActionsStep().
|
|
@@ -253,21 +271,16 @@ export class BaseAgent {
|
|
|
253
271
|
* @private
|
|
254
272
|
*/
|
|
255
273
|
this.MAX_RECOVERY_ATTEMPTS = 1;
|
|
256
|
-
/**
|
|
257
|
-
* Storage for injected memory context to prepend to prompts
|
|
258
|
-
*/
|
|
259
|
-
this._memoryContext = '';
|
|
260
274
|
/**
|
|
261
275
|
* Storage for injected notes and examples to include in result
|
|
262
276
|
*/
|
|
263
277
|
this._injectedMemory = { notes: [], examples: [] };
|
|
264
278
|
/**
|
|
265
279
|
* Storage for injected pre-execution RAG context (Phase 1C of search-scopes-rag-plus).
|
|
266
|
-
* Contains the
|
|
267
|
-
*
|
|
268
|
-
*
|
|
280
|
+
* Contains the structured per-scope / combined result detail for downstream observability
|
|
281
|
+
* and artifact persistence. The formatted `<retrieved_context>` system-message block is
|
|
282
|
+
* unshifted onto `conversationMessages` by the shared {@link AgentMemoryContextBuilder}.
|
|
269
283
|
*/
|
|
270
|
-
this._ragContext = '';
|
|
271
284
|
this._injectedRAG = null;
|
|
272
285
|
/**
|
|
273
286
|
* Determines the request type ID based on the Chat step's context.
|
|
@@ -1006,6 +1019,7 @@ export class BaseAgent {
|
|
|
1006
1019
|
// Reset scratchpad and artifact tools for each new execution (ephemeral per run)
|
|
1007
1020
|
this._scratchpadManager.Clear();
|
|
1008
1021
|
this._artifactToolManager.Clear();
|
|
1022
|
+
this._memoryWriteManager.Clear();
|
|
1009
1023
|
// Initialize artifact tools with any input artifacts attached to the run.
|
|
1010
1024
|
// Artifacts arrive as a typed first-class field on ExecuteAgentParams —
|
|
1011
1025
|
// they are NOT routed through `data` because prompt-template rendering
|
|
@@ -1146,6 +1160,21 @@ export class BaseAgent {
|
|
|
1146
1160
|
// Must wait for config from Phase 2 because it needs the resolved agent type and
|
|
1147
1161
|
// prompt configuration to initialize the type-specific state machine.
|
|
1148
1162
|
await this.initializeAgentType(wrappedParams, config);
|
|
1163
|
+
// =====================================================================================
|
|
1164
|
+
// SESSION-DRIVEN BRANCH (Realtime agent type)
|
|
1165
|
+
//
|
|
1166
|
+
// For session-driven agent types (the Realtime / Realtime Co-Agent type, marked by
|
|
1167
|
+
// `IsSessionDriven === true`), we do NOT enter the iterative reasoning loop. Instead we
|
|
1168
|
+
// hand control to a RealtimeSessionRunner that drives a long-lived duplex model session.
|
|
1169
|
+
//
|
|
1170
|
+
// This is the ONLY entry point into the realtime path. Loop and Flow agent types do not
|
|
1171
|
+
// expose `IsSessionDriven`, so `isSessionDrivenAgentType(...)` returns false for them and
|
|
1172
|
+
// their execution falls through to `executeAgentInternal` below — byte-for-byte unchanged.
|
|
1173
|
+
// =====================================================================================
|
|
1174
|
+
if (this.isSessionDrivenAgentType(this.AgentTypeInstance)) {
|
|
1175
|
+
this.logStatus(`🎙️ Agent '${params.agent.Name}' is session-driven — routing to RealtimeSessionRunner`, true, params);
|
|
1176
|
+
return await this.executeRealtimeSession(wrappedParams, config);
|
|
1177
|
+
}
|
|
1149
1178
|
// Execute the agent's internal logic with wrapped parameters
|
|
1150
1179
|
this.logStatus(`🚀 Executing agent '${params.agent.Name}' internal logic`, true, params);
|
|
1151
1180
|
const executionResult = await this.executeAgentInternal(wrappedParams, config);
|
|
@@ -1201,6 +1230,580 @@ export class BaseAgent {
|
|
|
1201
1230
|
params.cancellationToken = upstreamToken;
|
|
1202
1231
|
}
|
|
1203
1232
|
}
|
|
1233
|
+
// =====================================================================================
|
|
1234
|
+
// REALTIME (SESSION-DRIVEN) AGENT SUPPORT
|
|
1235
|
+
//
|
|
1236
|
+
// The methods below back the session-driven branch taken in Execute() for the Realtime
|
|
1237
|
+
// agent type. They are entered ONLY via that guarded branch; Loop/Flow agents never reach
|
|
1238
|
+
// them. The bulk of the work is building a RealtimeSessionRunnerDeps from BaseAgent's real
|
|
1239
|
+
// collaborators (model resolution, sub-agent delegation, tool execution, transcript
|
|
1240
|
+
// persistence, and usage checkpointing) and then driving RealtimeSessionRunner.Run().
|
|
1241
|
+
// =====================================================================================
|
|
1242
|
+
/**
|
|
1243
|
+
* Type guard for whether the resolved agent-type instance is session-driven.
|
|
1244
|
+
*
|
|
1245
|
+
* Detects the Realtime agent type without importing it (and without `instanceof`, which is
|
|
1246
|
+
* brittle under bundler class-duplication) by duck-typing the `IsSessionDriven` getter that
|
|
1247
|
+
* `RealtimeAgentType` adds. `BaseAgentType` (and Loop/Flow) do not expose this member, so the
|
|
1248
|
+
* guard returns `false` for them and the iterative loop runs unchanged.
|
|
1249
|
+
*
|
|
1250
|
+
* @param agentType The resolved agent-type instance for this run.
|
|
1251
|
+
* @returns `true` only when the type explicitly marks itself session-driven.
|
|
1252
|
+
*/
|
|
1253
|
+
isSessionDrivenAgentType(agentType) {
|
|
1254
|
+
return agentType.IsSessionDriven === true;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Drives a session-driven (Realtime) agent run end-to-end.
|
|
1258
|
+
*
|
|
1259
|
+
* Resolves the realtime model, assembles the session parameters (system prompt + memory/context),
|
|
1260
|
+
* builds the {@link RealtimeSessionRunnerDeps} from this agent's collaborators, runs the
|
|
1261
|
+
* {@link RealtimeSessionRunner}, and maps the result onto the finalized `AIAgentRun`.
|
|
1262
|
+
*
|
|
1263
|
+
* If no realtime model can be resolved (expected today, before the P3 drivers / P4 model
|
|
1264
|
+
* metadata land), it finalizes the run as a clean FAILED result with an actionable message
|
|
1265
|
+
* rather than throwing — a mis-provisioned environment must not crash the caller.
|
|
1266
|
+
*
|
|
1267
|
+
* @template R The caller's expected payload type (unused on the realtime path; the session
|
|
1268
|
+
* produces transcript/usage rather than a structured payload).
|
|
1269
|
+
* @param params The wrapped execution parameters.
|
|
1270
|
+
* @param config The loaded agent configuration (provides the system prompt, if any).
|
|
1271
|
+
* @returns The finalized {@link ExecuteAgentResult}.
|
|
1272
|
+
*/
|
|
1273
|
+
async executeRealtimeSession(params, config) {
|
|
1274
|
+
// 1) Resolve the realtime model (overridable seam — tests inject a mock).
|
|
1275
|
+
const modelResolution = await this.resolveRealtimeModel(params);
|
|
1276
|
+
if (!modelResolution) {
|
|
1277
|
+
const message = `Agent '${params.agent.Name}' is session-driven (Realtime) but no usable Realtime model could be ` +
|
|
1278
|
+
`resolved. Configure a model of AIModelType 'Realtime' with an active vendor DriverClass and a ` +
|
|
1279
|
+
`valid API key (e.g. AI_VENDOR_API_KEY__<driver>). This is expected until the realtime drivers ` +
|
|
1280
|
+
`and model metadata are provisioned.`;
|
|
1281
|
+
this.logError(message, { agent: params.agent, category: 'RealtimeSession' });
|
|
1282
|
+
return await this.createFailureResult(message, params.contextUser);
|
|
1283
|
+
}
|
|
1284
|
+
// 2) Create the single long-lived AIPromptRun that usage is checkpointed onto.
|
|
1285
|
+
const promptRun = await this.createRealtimePromptRun(params, config, modelResolution);
|
|
1286
|
+
// 3) Build the injected deps and run the session.
|
|
1287
|
+
try {
|
|
1288
|
+
const deps = await this.buildRealtimeSessionDeps(params, config, modelResolution, promptRun);
|
|
1289
|
+
const runner = new RealtimeSessionRunner(deps);
|
|
1290
|
+
const sessionResult = await runner.Run();
|
|
1291
|
+
return await this.finalizeRealtimeRun(params, sessionResult);
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1295
|
+
this.logError(`Realtime session failed for agent '${params.agent.Name}': ${msg}`, {
|
|
1296
|
+
agent: params.agent,
|
|
1297
|
+
category: 'RealtimeSession'
|
|
1298
|
+
});
|
|
1299
|
+
return await this.createFailureResult(msg, params.contextUser);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Resolves the realtime model + vendor driver + API key for a session-driven run.
|
|
1304
|
+
*
|
|
1305
|
+
* **Overridable seam.** This is the single injection point that test subclasses override to
|
|
1306
|
+
* return a mock {@link BaseRealtimeModel}, so {@link executeRealtimeSession} can be exercised
|
|
1307
|
+
* without provider SDKs or DB metadata.
|
|
1308
|
+
*
|
|
1309
|
+
* Production resolution: pick the highest-power active model of AIModelType `Realtime`; then
|
|
1310
|
+
* pick its highest-priority active vendor whose `DriverClass` has a resolvable API key; then
|
|
1311
|
+
* instantiate the driver via the `ClassFactory`. Returns `null` (never throws) if any step
|
|
1312
|
+
* can't be satisfied — the caller turns that into a clean FAILED result. (Per-agent realtime
|
|
1313
|
+
* model preference can later be wired through the agent's prompt-model config, the same path
|
|
1314
|
+
* loop agents use for `ModelSelectionMode`; the AI Agent entity has no direct model FK.)
|
|
1315
|
+
*
|
|
1316
|
+
* @param params The execution parameters (for the agent + context user).
|
|
1317
|
+
* @returns The resolved model instance plus its model/vendor identifiers, or `null`.
|
|
1318
|
+
*/
|
|
1319
|
+
async resolveRealtimeModel(params) {
|
|
1320
|
+
const model = this.selectRealtimeModelEntity(params.agent);
|
|
1321
|
+
if (!model) {
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const vendor = this.selectRealtimeVendor(model.ID);
|
|
1325
|
+
if (!vendor) {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
const apiKey = GetAIAPIKey(vendor.driverClass);
|
|
1329
|
+
if (!apiKey) {
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeModel, vendor.driverClass, apiKey);
|
|
1333
|
+
if (!instance) {
|
|
1334
|
+
return null;
|
|
1335
|
+
}
|
|
1336
|
+
return { model: instance, modelID: model.ID, vendorID: vendor.vendorID, apiName: vendor.apiName, driverClass: vendor.driverClass };
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Selects the `MJ: AI Models` row to use for a realtime session: the highest-power active
|
|
1340
|
+
* model of AIModelType `Realtime`. Returns `null` when no `Realtime` model exists in metadata
|
|
1341
|
+
* (expected before P4).
|
|
1342
|
+
*
|
|
1343
|
+
* @param agent The agent being executed (reserved for future per-agent model preference).
|
|
1344
|
+
* @returns The chosen model entity, or `null`.
|
|
1345
|
+
*/
|
|
1346
|
+
selectRealtimeModelEntity(agent) {
|
|
1347
|
+
const isRealtime = (m) => typeof m.AIModelType === 'string' && m.AIModelType.trim().toLowerCase() === 'realtime';
|
|
1348
|
+
const realtimeModels = AIEngine.Instance.Models.filter(m => m.IsActive && isRealtime(m));
|
|
1349
|
+
if (realtimeModels.length === 0) {
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
// Effective-config model preference (realtime.modelPreference, an MJ: AI Models Name or
|
|
1353
|
+
// ID) participates first. METADATA preferences degrade gracefully: an unsatisfiable
|
|
1354
|
+
// preference logs and falls through to the default highest-PowerRank selection.
|
|
1355
|
+
const preference = this.resolveRealtimeEffectiveConfig(agent).realtime?.modelPreference;
|
|
1356
|
+
if (preference) {
|
|
1357
|
+
const wanted = preference.trim().toLowerCase();
|
|
1358
|
+
const preferred = realtimeModels.find(m => UUIDsEqual(m.ID, preference))
|
|
1359
|
+
?? realtimeModels.find(m => m.Name?.trim().toLowerCase() === wanted);
|
|
1360
|
+
if (preferred) {
|
|
1361
|
+
return preferred;
|
|
1362
|
+
}
|
|
1363
|
+
this.logError(`Realtime model preference '${preference}' for agent '${agent.Name}' matches no Active Realtime ` +
|
|
1364
|
+
'model — falling through to default (highest-PowerRank) selection.', { agent, category: 'RealtimeSession' });
|
|
1365
|
+
}
|
|
1366
|
+
return realtimeModels.sort((a, b) => (b.PowerRank ?? 0) - (a.PowerRank ?? 0))[0];
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Resolves the agent's EFFECTIVE realtime configuration — the agent TYPE's
|
|
1370
|
+
* `DefaultConfiguration` (base layer) deep-merged with the agent's `TypeConfiguration`
|
|
1371
|
+
* (per-agent layer; the server-bridged path has no runtime-override layer). Tolerant:
|
|
1372
|
+
* malformed layers contribute nothing and an unloaded type cache yields no type defaults.
|
|
1373
|
+
* See `realtime/realtime-coagent-config.ts` for the merge contract.
|
|
1374
|
+
*
|
|
1375
|
+
* @param agent The session-driven (Realtime) agent.
|
|
1376
|
+
* @returns The normalized effective configuration (possibly empty, never `null`).
|
|
1377
|
+
*/
|
|
1378
|
+
resolveRealtimeEffectiveConfig(agent) {
|
|
1379
|
+
let typeDefault = null;
|
|
1380
|
+
try {
|
|
1381
|
+
if (agent.TypeID) {
|
|
1382
|
+
const type = (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, agent.TypeID));
|
|
1383
|
+
typeDefault = type?.DefaultConfiguration ?? null;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
catch {
|
|
1387
|
+
typeDefault = null;
|
|
1388
|
+
}
|
|
1389
|
+
return ResolveEffectiveRealtimeConfig(typeDefault, agent.TypeConfiguration ?? null, null);
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Selects the highest-priority active vendor for a model whose `DriverClass` has a resolvable
|
|
1393
|
+
* API key. Mirrors the vendor-selection pattern used by prompt execution.
|
|
1394
|
+
*
|
|
1395
|
+
* @param modelID The chosen model's ID.
|
|
1396
|
+
* @returns The vendor driver/api identifiers, or `null` when none has a usable key.
|
|
1397
|
+
*/
|
|
1398
|
+
selectRealtimeVendor(modelID) {
|
|
1399
|
+
const vendors = AIEngine.Instance.ModelVendors
|
|
1400
|
+
.filter(mv => UUIDsEqual(mv.ModelID, modelID) && mv.Status === 'Active' && mv.DriverClass != null)
|
|
1401
|
+
.sort((a, b) => (b.Priority ?? 0) - (a.Priority ?? 0));
|
|
1402
|
+
for (const v of vendors) {
|
|
1403
|
+
if (GetAIAPIKey(v.DriverClass)) {
|
|
1404
|
+
return { vendorID: v.VendorID ?? '', driverClass: v.DriverClass, apiName: v.APIName ?? '' };
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* Creates the single long-lived `AIPromptRun` that realtime usage is checkpointed onto.
|
|
1411
|
+
*
|
|
1412
|
+
* One run is created per session (not per turn) so {@link RealtimeSessionRunnerDeps.CheckpointUsage}
|
|
1413
|
+
* can incrementally update the same record — crash-safe by design. Returns `null` on failure;
|
|
1414
|
+
* the session still runs (usage checkpoints simply become no-ops).
|
|
1415
|
+
*
|
|
1416
|
+
* @param params The execution parameters.
|
|
1417
|
+
* @param config The agent configuration (provides the system prompt id, if any).
|
|
1418
|
+
* @param modelResolution The resolved model/vendor identifiers.
|
|
1419
|
+
* @returns The persisted prompt run, or `null` if it could not be created.
|
|
1420
|
+
*/
|
|
1421
|
+
async createRealtimePromptRun(params, config, modelResolution) {
|
|
1422
|
+
try {
|
|
1423
|
+
const md = params.provider || this._activeProvider;
|
|
1424
|
+
const promptRun = await md.GetEntityObject('MJ: AI Prompt Runs', params.contextUser);
|
|
1425
|
+
promptRun.NewRecord();
|
|
1426
|
+
if (config.systemPrompt) {
|
|
1427
|
+
promptRun.PromptID = config.systemPrompt.ID;
|
|
1428
|
+
}
|
|
1429
|
+
promptRun.ModelID = modelResolution.modelID;
|
|
1430
|
+
promptRun.VendorID = modelResolution.vendorID || null;
|
|
1431
|
+
promptRun.AgentID = params.agent.ID;
|
|
1432
|
+
promptRun.AgentRunID = this._agentRun?.ID ?? null;
|
|
1433
|
+
promptRun.Status = 'Running';
|
|
1434
|
+
promptRun.RunAt = new Date();
|
|
1435
|
+
promptRun.StreamingEnabled = true;
|
|
1436
|
+
promptRun.Cancelled = false;
|
|
1437
|
+
promptRun.CacheHit = false;
|
|
1438
|
+
if (!await promptRun.Save()) {
|
|
1439
|
+
this.logError(`Failed to create realtime AIPromptRun: ${promptRun.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1440
|
+
agent: params.agent,
|
|
1441
|
+
category: 'RealtimeSession'
|
|
1442
|
+
});
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
return promptRun;
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1449
|
+
this.logError(`Error creating realtime AIPromptRun: ${msg}`, { agent: params.agent, category: 'RealtimeSession' });
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Builds the fully-populated {@link RealtimeSessionRunnerDeps} from this agent's collaborators.
|
|
1455
|
+
*
|
|
1456
|
+
* Each dependency is a thin closure over BaseAgent state so the runner stays decoupled from
|
|
1457
|
+
* metadata/DB. The closures cover: target delegation (via {@link ExecuteSubAgent}), non-target
|
|
1458
|
+
* tool execution, transcript persistence (as `ConversationDetail`), and usage checkpointing
|
|
1459
|
+
* (onto the long-lived prompt run).
|
|
1460
|
+
*
|
|
1461
|
+
* @param params The execution parameters.
|
|
1462
|
+
* @param config The agent configuration.
|
|
1463
|
+
* @param modelResolution The resolved realtime model + identifiers.
|
|
1464
|
+
* @param promptRun The long-lived prompt run for usage checkpoints (may be `null`).
|
|
1465
|
+
* @returns The assembled deps object.
|
|
1466
|
+
*/
|
|
1467
|
+
async buildRealtimeSessionDeps(params, config, modelResolution, promptRun) {
|
|
1468
|
+
const effectiveConfig = this.resolveRealtimeEffectiveConfig(params.agent);
|
|
1469
|
+
const sessionParams = await this.buildRealtimeSessionParams(params, config, modelResolution.apiName, effectiveConfig, modelResolution.driverClass);
|
|
1470
|
+
return {
|
|
1471
|
+
Model: modelResolution.model,
|
|
1472
|
+
SessionParams: sessionParams,
|
|
1473
|
+
DelegateToTarget: (request) => this.delegateRealtimeToTarget(params, config, request),
|
|
1474
|
+
ExecuteTool: (call) => this.executeRealtimeTool(params, call),
|
|
1475
|
+
PersistTranscript: (transcript) => this.persistRealtimeTranscript(params, transcript),
|
|
1476
|
+
CheckpointUsage: (usage) => this.checkpointRealtimeUsage(promptRun, usage),
|
|
1477
|
+
// DB-driven spoken-progress wording (shared lookup with the client-direct path);
|
|
1478
|
+
// null → the runner's documented built-in first-person fallback.
|
|
1479
|
+
NarrationInstructionsTemplate: ResolveNarrationInstructionsTemplate(),
|
|
1480
|
+
// Effective-config narration pacing (realtime.narration.paceMs); null → runner default.
|
|
1481
|
+
NarrationPaceMs: GetNarrationPaceMs(effectiveConfig),
|
|
1482
|
+
LogStatus: (message, verboseOnly) => this.logStatus(message, verboseOnly ?? false, params),
|
|
1483
|
+
LogError: (error) => this.logError(error, { agent: params.agent, category: 'RealtimeSession' })
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Assembles the {@link RealtimeSessionParams} for the session.
|
|
1488
|
+
*
|
|
1489
|
+
* The system prompt is framed as a companion "voice for the target agent". The base system
|
|
1490
|
+
* prompt text (when an agent-level system prompt exists) plus the same memory/context a loop
|
|
1491
|
+
* agent would assemble (via {@link AgentMemoryContextBuilder}) are concatenated. The
|
|
1492
|
+
* always-present `invoke-target-agent` tool is added by the runner itself, so it is NOT
|
|
1493
|
+
* populated here.
|
|
1494
|
+
*
|
|
1495
|
+
* @param params The execution parameters.
|
|
1496
|
+
* @param config The agent configuration.
|
|
1497
|
+
* @param modelApiName The vendor API name of the resolved realtime model.
|
|
1498
|
+
* @returns The session parameters.
|
|
1499
|
+
*/
|
|
1500
|
+
async buildRealtimeSessionParams(params, config, modelApiName, effectiveConfig, driverClass) {
|
|
1501
|
+
const framing = `You are the real-time voice for the agent "${params.agent.Name}". Hold a natural, ` +
|
|
1502
|
+
`low-latency conversation with the user. When actual work is required, call the ` +
|
|
1503
|
+
`'invoke-target-agent' tool and narrate progress while it runs — do not attempt to do ` +
|
|
1504
|
+
`the work yourself. ONE EXCEPTION: besides 'invoke-target-agent' you may have been given ` +
|
|
1505
|
+
`interactive-surface tools (for example 'browser_*' to drive a LIVE web browser the user ` +
|
|
1506
|
+
`can watch, or 'Whiteboard_*' to draw on a shared board). Those surfaces are operated by ` +
|
|
1507
|
+
`YOU, directly — when the user asks to use one (e.g. "open/show a browser", "go to a ` +
|
|
1508
|
+
`site", "add to the whiteboard"), call the matching tool yourself immediately and narrate ` +
|
|
1509
|
+
`what you're doing. NEVER route an interactive-surface request through 'invoke-target-agent', ` +
|
|
1510
|
+
`and never claim you lack a session — calling the tool is all that's needed.`;
|
|
1511
|
+
const basePrompt = config.systemPrompt?.TemplateText ? config.systemPrompt.TemplateText : '';
|
|
1512
|
+
// Effective-config voice persona (realtime.voice.default) → short "Voice & manner" section.
|
|
1513
|
+
const voiceManner = BuildVoiceMannerSection(effectiveConfig);
|
|
1514
|
+
const memoryContext = await this.assembleRealtimeContext(params);
|
|
1515
|
+
const systemPrompt = [framing, basePrompt, voiceManner, memoryContext]
|
|
1516
|
+
.filter(part => part && part.trim().length > 0)
|
|
1517
|
+
.join('\n\n');
|
|
1518
|
+
// Provider-matched voice settings (realtime.voice.providers.<provider>) flow into the
|
|
1519
|
+
// driver's open Config bag — the same pact every other config entry rides.
|
|
1520
|
+
const providerVoice = GetProviderVoiceSettings(effectiveConfig, driverClass ?? null);
|
|
1521
|
+
return {
|
|
1522
|
+
Model: modelApiName,
|
|
1523
|
+
SystemPrompt: systemPrompt,
|
|
1524
|
+
InitialContext: memoryContext || undefined,
|
|
1525
|
+
// JSONObjectLike -> JSONObject: safe — the settings object came from JSON.parse.
|
|
1526
|
+
Config: providerVoice ? providerVoice : undefined
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Assembles the same memory/context block a loop agent injects, reusing
|
|
1531
|
+
* {@link AgentMemoryContextBuilder} so there is no duplicated retrieval logic. The builder
|
|
1532
|
+
* unshifts a system message onto a throwaway array, which we pull back out as plain text to
|
|
1533
|
+
* feed the realtime model's session context.
|
|
1534
|
+
*
|
|
1535
|
+
* @param params The execution parameters.
|
|
1536
|
+
* @returns The concatenated context text (empty string when nothing was injected).
|
|
1537
|
+
*/
|
|
1538
|
+
async assembleRealtimeContext(params) {
|
|
1539
|
+
const lastUserMessage = params.conversationMessages.filter(m => m.role === 'user').pop();
|
|
1540
|
+
const inputText = typeof lastUserMessage?.content === 'string' ? lastUserMessage.content : '';
|
|
1541
|
+
const scratch = [];
|
|
1542
|
+
const builder = new AgentMemoryContextBuilder();
|
|
1543
|
+
await builder.InjectContextMemory(inputText, params.agent, params.userId || params.contextUser?.ID, params.companyId, params.contextUser, scratch, undefined, undefined, undefined, null, undefined, (message, verboseOnly) => this.logStatus(message, verboseOnly ?? false, params));
|
|
1544
|
+
return scratch
|
|
1545
|
+
.map(m => (typeof m.content === 'string' ? m.content : ''))
|
|
1546
|
+
.filter(c => c.length > 0)
|
|
1547
|
+
.join('\n\n');
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Delegates an `invoke-target-agent` tool call to the top-level target agent.
|
|
1551
|
+
*
|
|
1552
|
+
* Threads the runner-owned {@link DelegateToTargetRequest.AbortSignal} into the child run's
|
|
1553
|
+
* `cancellationToken` (so barge-in cancels the delegated work), and links the child run to this
|
|
1554
|
+
* run via `parentRun` (→ `ParentRunID`) while propagating `agentSessionID` so both runs group
|
|
1555
|
+
* under the same session.
|
|
1556
|
+
*
|
|
1557
|
+
* **Target source.** The target agent id comes from `params.data.targetAgentID` when present
|
|
1558
|
+
* (the Realtime Co-Agent receives its target as a runtime parameter), falling back to the agent's
|
|
1559
|
+
* own `DefaultModelID`-style config is NOT applicable here; absent a target the delegation
|
|
1560
|
+
* returns a failed {@link DelegatedResult} the model can narrate.
|
|
1561
|
+
*
|
|
1562
|
+
* @param params The (parent) execution parameters.
|
|
1563
|
+
* @param config The agent configuration (unused today; reserved for target-from-config wiring).
|
|
1564
|
+
* @param request The delegation request derived from the tool call.
|
|
1565
|
+
* @returns The delegated result for the model's tool_response.
|
|
1566
|
+
*/
|
|
1567
|
+
async delegateRealtimeToTarget(params, config, request) {
|
|
1568
|
+
const targetAgent = this.resolveRealtimeTargetAgent(params);
|
|
1569
|
+
if (!targetAgent) {
|
|
1570
|
+
return {
|
|
1571
|
+
CallID: request.CallID,
|
|
1572
|
+
Success: false,
|
|
1573
|
+
Output: 'No target agent is configured for this voice session, so the request could not be performed.'
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
try {
|
|
1577
|
+
const requestText = this.parseDelegateRequestText(request.Arguments);
|
|
1578
|
+
const runner = new AgentRunner(params.provider || this._activeProvider);
|
|
1579
|
+
const result = await runner.RunAgent({
|
|
1580
|
+
agent: targetAgent,
|
|
1581
|
+
conversationMessages: [{ role: 'user', content: requestText }],
|
|
1582
|
+
contextUser: params.contextUser,
|
|
1583
|
+
cancellationToken: request.AbortSignal,
|
|
1584
|
+
parentRun: this._agentRun ?? undefined,
|
|
1585
|
+
agentSessionID: params.agentSessionID,
|
|
1586
|
+
parentAgentHierarchy: this._agentHierarchy,
|
|
1587
|
+
parentDepth: this._depth,
|
|
1588
|
+
configurationId: params.configurationId,
|
|
1589
|
+
apiKeys: params.apiKeys,
|
|
1590
|
+
data: params.data,
|
|
1591
|
+
verbose: params.verbose,
|
|
1592
|
+
// Progress streams BOTH to the runner's narration consumer (request.OnProgress —
|
|
1593
|
+
// it paces SendContextNote/RequestSpokenUpdate over the live socket) AND to any
|
|
1594
|
+
// host-level onProgress the parent execution carries.
|
|
1595
|
+
onProgress: this.combineProgressCallbacks(request.OnProgress, params.onProgress)
|
|
1596
|
+
});
|
|
1597
|
+
return {
|
|
1598
|
+
CallID: request.CallID,
|
|
1599
|
+
Success: result.success,
|
|
1600
|
+
Output: result.success
|
|
1601
|
+
? (result.agentRun?.Message || 'The target agent completed the request.')
|
|
1602
|
+
: (result.agentRun?.ErrorMessage || 'The target agent failed to complete the request.')
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
catch (error) {
|
|
1606
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1607
|
+
return { CallID: request.CallID, Success: false, Output: `Delegation failed: ${msg}` };
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Combines the runner-supplied delegation progress callback with the host-level one so a
|
|
1612
|
+
* single `onProgress` fans out to both. Returns the lone callback when only one exists, and
|
|
1613
|
+
* `undefined` when neither does. A throw from one consumer never starves the other.
|
|
1614
|
+
*/
|
|
1615
|
+
combineProgressCallbacks(first, second) {
|
|
1616
|
+
if (!first) {
|
|
1617
|
+
return second;
|
|
1618
|
+
}
|
|
1619
|
+
if (!second) {
|
|
1620
|
+
return first;
|
|
1621
|
+
}
|
|
1622
|
+
return (progress) => {
|
|
1623
|
+
try {
|
|
1624
|
+
first(progress);
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
/* one consumer failing must not starve the other */
|
|
1628
|
+
}
|
|
1629
|
+
second(progress);
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Resolves the top-level target agent for the voice session.
|
|
1634
|
+
*
|
|
1635
|
+
* The target is supplied as a runtime parameter on `params.data.targetAgentID` (the Voice
|
|
1636
|
+
* Co-Agent voices on behalf of a target chosen at session start). Returns `null` when no
|
|
1637
|
+
* resolvable target is configured.
|
|
1638
|
+
*
|
|
1639
|
+
* @param params The execution parameters.
|
|
1640
|
+
* @returns The target agent entity, or `null`.
|
|
1641
|
+
*/
|
|
1642
|
+
resolveRealtimeTargetAgent(params) {
|
|
1643
|
+
const targetID = params.data?.targetAgentID;
|
|
1644
|
+
if (!targetID) {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
return AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, targetID)) ?? null;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Parses the natural-language request text out of an `invoke-target-agent` call's arguments.
|
|
1651
|
+
* Falls back to the raw argument string when it is not the expected `{ request: string }` JSON.
|
|
1652
|
+
*
|
|
1653
|
+
* @param argumentsJson The raw arguments string emitted by the model.
|
|
1654
|
+
* @returns The request text to hand to the target agent.
|
|
1655
|
+
*/
|
|
1656
|
+
parseDelegateRequestText(argumentsJson) {
|
|
1657
|
+
try {
|
|
1658
|
+
const parsed = JSON.parse(argumentsJson);
|
|
1659
|
+
if (typeof parsed.request === 'string') {
|
|
1660
|
+
return parsed.request;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
catch {
|
|
1664
|
+
/* not JSON — fall through to raw */
|
|
1665
|
+
}
|
|
1666
|
+
return argumentsJson;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Executes a non-target realtime tool call by routing it through the agent's existing action
|
|
1670
|
+
* execution under the session context user.
|
|
1671
|
+
*
|
|
1672
|
+
* Today this maps the realtime call onto the agent's configured actions by name; unknown tools
|
|
1673
|
+
* return a failed {@link ToolExecutionResult} the model can narrate. (The richer client/UI tool
|
|
1674
|
+
* routing is wired in a later phase; this keeps server actions usable now.)
|
|
1675
|
+
*
|
|
1676
|
+
* @param params The execution parameters.
|
|
1677
|
+
* @param call The non-target tool call.
|
|
1678
|
+
* @returns The tool execution result for the model's tool_response.
|
|
1679
|
+
*/
|
|
1680
|
+
async executeRealtimeTool(params, call) {
|
|
1681
|
+
const action = this.getEffectiveActionsForValidation(params.agent.ID).find(a => a.Name === call.ToolName);
|
|
1682
|
+
if (!action) {
|
|
1683
|
+
return {
|
|
1684
|
+
CallID: call.CallID,
|
|
1685
|
+
Success: false,
|
|
1686
|
+
Output: `Tool '${call.ToolName}' is not available to this agent.`
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
try {
|
|
1690
|
+
const agentAction = { name: action.Name, params: this.parseRealtimeToolParams(call.Arguments) };
|
|
1691
|
+
const result = await this.ExecuteSingleAction(params, agentAction, action, params.contextUser);
|
|
1692
|
+
return {
|
|
1693
|
+
CallID: call.CallID,
|
|
1694
|
+
Success: result.Success,
|
|
1695
|
+
Output: result.Message || (result.Success ? 'Tool completed.' : 'Tool failed.')
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
catch (error) {
|
|
1699
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1700
|
+
return { CallID: call.CallID, Success: false, Output: `Tool execution failed: ${msg}` };
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Parses a realtime tool call's JSON arguments into an action parameter map.
|
|
1705
|
+
*
|
|
1706
|
+
* @param argumentsJson The raw arguments string.
|
|
1707
|
+
* @returns A record of parameter name → value (empty when not parseable).
|
|
1708
|
+
*/
|
|
1709
|
+
parseRealtimeToolParams(argumentsJson) {
|
|
1710
|
+
try {
|
|
1711
|
+
const parsed = JSON.parse(argumentsJson);
|
|
1712
|
+
if (parsed && typeof parsed === 'object') {
|
|
1713
|
+
return parsed;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
catch {
|
|
1717
|
+
/* ignore — return empty params */
|
|
1718
|
+
}
|
|
1719
|
+
return {};
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Persists a single realtime transcript turn as a `ConversationDetail` stamped with the
|
|
1723
|
+
* session id. User turns are written as `Role='User'`, assistant turns as `Role='AI'`. Only
|
|
1724
|
+
* final transcripts are persisted (interim/partial updates are skipped to avoid churn).
|
|
1725
|
+
*
|
|
1726
|
+
* @param params The execution parameters (provides conversation id + context user).
|
|
1727
|
+
* @param transcript The transcript turn emitted by the model.
|
|
1728
|
+
*/
|
|
1729
|
+
async persistRealtimeTranscript(params, transcript) {
|
|
1730
|
+
if (!transcript.IsFinal || !transcript.Text?.trim()) {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const conversationID = params.data?.conversationId;
|
|
1734
|
+
if (!conversationID) {
|
|
1735
|
+
return; // Without a conversation we have nowhere to durably attach the turn.
|
|
1736
|
+
}
|
|
1737
|
+
const md = params.provider || this._activeProvider;
|
|
1738
|
+
const detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1739
|
+
detail.NewRecord();
|
|
1740
|
+
detail.ConversationID = conversationID;
|
|
1741
|
+
detail.Role = transcript.Role === 'user' ? 'User' : 'AI';
|
|
1742
|
+
detail.Message = transcript.Text;
|
|
1743
|
+
if (params.agentSessionID) {
|
|
1744
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1745
|
+
}
|
|
1746
|
+
if (!await detail.Save()) {
|
|
1747
|
+
this.logError(`Failed to persist realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1748
|
+
agent: params.agent,
|
|
1749
|
+
category: 'RealtimeSession'
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Checkpoints accumulated realtime usage onto the single long-lived prompt run. This is the
|
|
1755
|
+
* incremental, crash-safe write the runner invokes on a debounced cadence and at close.
|
|
1756
|
+
*
|
|
1757
|
+
* @param promptRun The long-lived prompt run (no-op when `null`).
|
|
1758
|
+
* @param usage The cumulative usage snapshot to persist.
|
|
1759
|
+
*/
|
|
1760
|
+
async checkpointRealtimeUsage(promptRun, usage) {
|
|
1761
|
+
if (!promptRun) {
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
promptRun.TokensPrompt = usage.InputTokens;
|
|
1765
|
+
promptRun.TokensCompletion = usage.OutputTokens;
|
|
1766
|
+
promptRun.TokensUsed = usage.InputTokens + usage.OutputTokens;
|
|
1767
|
+
if (!await promptRun.Save()) {
|
|
1768
|
+
this.logError(`Failed to checkpoint realtime usage: ${promptRun.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1769
|
+
category: 'RealtimeSession'
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
/**
|
|
1774
|
+
* Maps a completed {@link RealtimeSessionResult} onto the finalized `AIAgentRun` and returns
|
|
1775
|
+
* the {@link ExecuteAgentResult}. A clean close finalizes as success; a session error finalizes
|
|
1776
|
+
* as failure with the error message.
|
|
1777
|
+
*
|
|
1778
|
+
* @template R The caller's payload type (unused on the realtime path).
|
|
1779
|
+
* @param params The execution parameters.
|
|
1780
|
+
* @param sessionResult The result returned by {@link RealtimeSessionRunner.Run}.
|
|
1781
|
+
* @returns The finalized agent result.
|
|
1782
|
+
*/
|
|
1783
|
+
async finalizeRealtimeRun(params, sessionResult) {
|
|
1784
|
+
if (sessionResult.Success) {
|
|
1785
|
+
this.logStatus(`🎙️ Realtime session for '${params.agent.Name}' completed: ${sessionResult.TranscriptTurnCount} turn(s), ` +
|
|
1786
|
+
`${sessionResult.FinalUsage.InputTokens + sessionResult.FinalUsage.OutputTokens} token(s).`, true, params);
|
|
1787
|
+
const successStep = this.createSessionSuccessStep();
|
|
1788
|
+
return await this.finalizeAgentRun(successStep, undefined, params.contextUser);
|
|
1789
|
+
}
|
|
1790
|
+
const message = sessionResult.ErrorMessage || 'Realtime session ended with an error.';
|
|
1791
|
+
return await this.createFailureResult(message, params.contextUser);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Builds a terminal `Success` step describing the completion of a realtime session, used to
|
|
1795
|
+
* finalize the run through the shared {@link finalizeAgentRun} path.
|
|
1796
|
+
*
|
|
1797
|
+
* @template R The caller's payload type.
|
|
1798
|
+
* @returns A terminal success step.
|
|
1799
|
+
*/
|
|
1800
|
+
createSessionSuccessStep() {
|
|
1801
|
+
return {
|
|
1802
|
+
step: 'Success',
|
|
1803
|
+
terminate: true,
|
|
1804
|
+
message: 'Realtime session completed.'
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1204
1807
|
/**
|
|
1205
1808
|
* Sub-classes can override this method to perform any specialized initialization
|
|
1206
1809
|
* @param params
|
|
@@ -1393,72 +1996,16 @@ export class BaseAgent {
|
|
|
1393
1996
|
* @returns Object containing injected notes and examples
|
|
1394
1997
|
*/
|
|
1395
1998
|
async InjectContextMemory(input, agent, userId, companyId, contextUser, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, secondaryScopeConfig) {
|
|
1396
|
-
//
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
? await injector.GetNotesForContext({
|
|
1407
|
-
agentId: agent.ID,
|
|
1408
|
-
userId,
|
|
1409
|
-
companyId,
|
|
1410
|
-
currentInput: input,
|
|
1411
|
-
strategy: agent.NoteInjectionStrategy,
|
|
1412
|
-
maxNotes: agent.MaxNotesToInject || 5,
|
|
1413
|
-
contextUser: contextUser,
|
|
1414
|
-
rerankerConfig,
|
|
1415
|
-
primaryScopeEntityId,
|
|
1416
|
-
primaryScopeRecordId,
|
|
1417
|
-
secondaryScopes,
|
|
1418
|
-
secondaryScopeConfig,
|
|
1419
|
-
// Pass observability context for run step tracking
|
|
1420
|
-
observability: this._agentRun ? {
|
|
1421
|
-
agentRunID: this._agentRun.ID,
|
|
1422
|
-
stepNumber: (this._agentRun.Steps?.length || 0) + 1
|
|
1423
|
-
} : undefined
|
|
1424
|
-
})
|
|
1425
|
-
: [];
|
|
1426
|
-
this.logStatus(`BaseAgent: Got ${notes.length} notes from injector`, true);
|
|
1427
|
-
// Get examples if injection enabled
|
|
1428
|
-
const examples = agent.InjectExamples
|
|
1429
|
-
? await injector.GetExamplesForContext({
|
|
1430
|
-
agentId: agent.ID,
|
|
1431
|
-
userId,
|
|
1432
|
-
companyId,
|
|
1433
|
-
currentInput: input,
|
|
1434
|
-
strategy: agent.ExampleInjectionStrategy,
|
|
1435
|
-
maxExamples: agent.MaxExamplesToInject || 3,
|
|
1436
|
-
contextUser: contextUser,
|
|
1437
|
-
primaryScopeEntityId,
|
|
1438
|
-
primaryScopeRecordId,
|
|
1439
|
-
secondaryScopes,
|
|
1440
|
-
secondaryScopeConfig
|
|
1441
|
-
})
|
|
1442
|
-
: [];
|
|
1443
|
-
// Format and inject memory context into conversation messages
|
|
1444
|
-
if ((notes.length > 0 || examples.length > 0) && conversationMessages) {
|
|
1445
|
-
const notesText = injector.FormatNotesForInjection(notes);
|
|
1446
|
-
const examplesText = injector.FormatExamplesForInjection(examples);
|
|
1447
|
-
this._memoryContext = '';
|
|
1448
|
-
if (notesText)
|
|
1449
|
-
this._memoryContext += notesText + '\n\n';
|
|
1450
|
-
if (examplesText)
|
|
1451
|
-
this._memoryContext += examplesText + '\n\n';
|
|
1452
|
-
// Inject as system message at the start
|
|
1453
|
-
conversationMessages.unshift({
|
|
1454
|
-
role: 'system',
|
|
1455
|
-
content: this._memoryContext
|
|
1456
|
-
});
|
|
1457
|
-
this.logStatus(`💾 Injected ${notes.length} notes and ${examples.length} examples into conversation context`, true);
|
|
1458
|
-
}
|
|
1459
|
-
// Store for inclusion in result
|
|
1460
|
-
this._injectedMemory = { notes, examples };
|
|
1461
|
-
return { notes, examples };
|
|
1999
|
+
// Delegate the orchestration to the shared, reusable builder so both BaseAgent and the
|
|
2000
|
+
// Realtime agent type inject memory identically. The observability context and verbose
|
|
2001
|
+
// status logging are derived from this instance and passed through.
|
|
2002
|
+
const observability = this._agentRun
|
|
2003
|
+
? { agentRunID: this._agentRun.ID, stepNumber: (this._agentRun.Steps?.length || 0) + 1 }
|
|
2004
|
+
: undefined;
|
|
2005
|
+
const result = await new AgentMemoryContextBuilder().InjectContextMemory(input, agent, userId, companyId, contextUser, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, secondaryScopeConfig, observability, (message, verboseOnly) => this.logStatus(message, verboseOnly));
|
|
2006
|
+
// Store for inclusion in result (externally observable behavior preserved)
|
|
2007
|
+
this._injectedMemory = result;
|
|
2008
|
+
return result;
|
|
1462
2009
|
}
|
|
1463
2010
|
/**
|
|
1464
2011
|
* Inject pre-execution RAG context for this agent using scoped search.
|
|
@@ -1486,40 +2033,12 @@ export class BaseAgent {
|
|
|
1486
2033
|
* @returns The structured RAG result, or `null` if no scopes produced results.
|
|
1487
2034
|
*/
|
|
1488
2035
|
async InjectPreExecutionRAG(lastUserMessage, agent, contextUser, conversationMessages, originalMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, payload) {
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const result = await rag.Execute({
|
|
1496
|
-
agent,
|
|
1497
|
-
lastUserMessage,
|
|
1498
|
-
recentMessages: originalMessages ? originalMessages.slice(-5) : undefined,
|
|
1499
|
-
payload,
|
|
1500
|
-
primaryScopeRecordId,
|
|
1501
|
-
primaryScopeEntityId,
|
|
1502
|
-
secondaryScopes,
|
|
1503
|
-
contextUser
|
|
1504
|
-
});
|
|
1505
|
-
if (!result)
|
|
1506
|
-
return null;
|
|
1507
|
-
if (conversationMessages && result.formattedSystemMessage) {
|
|
1508
|
-
this._ragContext = result.formattedSystemMessage;
|
|
1509
|
-
conversationMessages.unshift({ role: 'system', content: this._ragContext });
|
|
1510
|
-
this.logStatus(`🔎 Injected pre-execution RAG context: ${result.combinedResults.length} result(s) from ${result.queriedScopeIDs.length} scope(s)`, true);
|
|
1511
|
-
}
|
|
1512
|
-
this._injectedRAG = result;
|
|
1513
|
-
return result;
|
|
1514
|
-
}
|
|
1515
|
-
catch (error) {
|
|
1516
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
1517
|
-
this.logError(`InjectPreExecutionRAG failed — continuing without RAG context: ${msg}`, {
|
|
1518
|
-
agent,
|
|
1519
|
-
category: 'AgentPreExecutionRAG'
|
|
1520
|
-
});
|
|
1521
|
-
return null;
|
|
1522
|
-
}
|
|
2036
|
+
// Delegate to the shared builder so the Realtime agent type injects pre-execution RAG
|
|
2037
|
+
// identically. Verbose status + non-fatal error logging are threaded through from this instance.
|
|
2038
|
+
const result = await new AgentMemoryContextBuilder().InjectPreExecutionRAG(lastUserMessage, agent, contextUser, conversationMessages, originalMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, payload, (message, verboseOnly) => this.logStatus(message, verboseOnly), (error, options) => this.logError(error, options));
|
|
2039
|
+
// Store for inclusion in result (externally observable behavior preserved)
|
|
2040
|
+
this._injectedRAG = result;
|
|
2041
|
+
return result;
|
|
1523
2042
|
}
|
|
1524
2043
|
/**
|
|
1525
2044
|
* Converts UI markup (@{...} syntax) in user messages to plain text.
|
|
@@ -1793,7 +2312,7 @@ export class BaseAgent {
|
|
|
1793
2312
|
const systemPrompt = config.systemPrompt;
|
|
1794
2313
|
const childPrompt = config.childPrompt;
|
|
1795
2314
|
// Gather context data (including runtime action changes)
|
|
1796
|
-
const promptTemplateData = await this.gatherPromptTemplateData(params.agent, params.contextUser, params.data, params.actionChanges);
|
|
2315
|
+
const promptTemplateData = await this.gatherPromptTemplateData(params.agent, params.contextUser, params.data, params.actionChanges, params.subAgentChanges);
|
|
1797
2316
|
// Set up the hierarchical prompt execution
|
|
1798
2317
|
const promptParams = new AIPromptParams();
|
|
1799
2318
|
// Handle case where systemPrompt is optional (e.g., Flow Agent Type)
|
|
@@ -1868,6 +2387,13 @@ export class BaseAgent {
|
|
|
1868
2387
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
1869
2388
|
this.logStatus(`[ArtifactTools] Artifacts present but tools disabled by agent config (includeArtifactToolsDocs=false)`, true, params);
|
|
1870
2389
|
}
|
|
2390
|
+
// Enable the memory-writes response field + docs only for agents that opted in
|
|
2391
|
+
// via AllowMemoryWrite. Disabled agents never see the docs, so a well-behaved
|
|
2392
|
+
// LLM never emits the field (the turn loop still guards against drift).
|
|
2393
|
+
const memoryWritesDocsEnabled = agentTypePromptParams?.includeMemoryWritesDocs !== false;
|
|
2394
|
+
if (memoryWritesDocsEnabled && params.agent.AllowMemoryWrite === true) {
|
|
2395
|
+
promptParams.data['_MEMORY_WRITES_ENABLED'] = true;
|
|
2396
|
+
}
|
|
1871
2397
|
// Inject pipeline tool docs when pipelines are enabled and at least one source exists.
|
|
1872
2398
|
// A pipeline's first step must be a source (Action or artifact tool); with none
|
|
1873
2399
|
// available pipelines are impossible, so BuildPipelineToolDocs returns '' and the
|
|
@@ -3433,6 +3959,129 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3433
3959
|
`Instead: page it with get_rows(start, count), or run a pipeline that filters/aggregates it ` +
|
|
3434
3960
|
`server-side (where / select / groupBy → only the small final result returns to you).]`);
|
|
3435
3961
|
}
|
|
3962
|
+
/**
|
|
3963
|
+
* Executes a batch of in-flight memory writes, recording each as its own
|
|
3964
|
+
* `Tool` AIAgentRunStep (a sibling of the Prompt step that requested them)
|
|
3965
|
+
* with full inputs/outcomes captured in InputData/OutputData.
|
|
3966
|
+
*
|
|
3967
|
+
* Writes run SEQUENTIALLY (not Promise.all like artifact tools) by design:
|
|
3968
|
+
* each persisted note is embedded and synced into the in-memory vector
|
|
3969
|
+
* service on Save, so write N must be visible to write N+1's near-duplicate
|
|
3970
|
+
* check (this is also what makes same-run supersede-own work). The per-run
|
|
3971
|
+
* cap bounds the cost of the serialization.
|
|
3972
|
+
*
|
|
3973
|
+
* Step naming convention: `Memory Write` for log/UI clarity.
|
|
3974
|
+
*
|
|
3975
|
+
* @protected
|
|
3976
|
+
*/
|
|
3977
|
+
async executeMemoryWritesAsSteps(writes, params) {
|
|
3978
|
+
const results = [];
|
|
3979
|
+
for (const write of writes) {
|
|
3980
|
+
const writeStep = await this.createStepEntity({
|
|
3981
|
+
stepType: 'Tool',
|
|
3982
|
+
stepName: 'Memory Write',
|
|
3983
|
+
contextUser: params.contextUser,
|
|
3984
|
+
inputData: {
|
|
3985
|
+
note: write.note,
|
|
3986
|
+
type: write.type,
|
|
3987
|
+
scopeHint: write.scopeHint,
|
|
3988
|
+
},
|
|
3989
|
+
});
|
|
3990
|
+
const result = await this._memoryWriteManager.ExecuteWrite(write, {
|
|
3991
|
+
agentId: params.agent.ID,
|
|
3992
|
+
contextUser: params.contextUser,
|
|
3993
|
+
agentRunId: this._agentRun?.ID,
|
|
3994
|
+
conversationId: this._agentRun?.ConversationID || undefined,
|
|
3995
|
+
conversationDetailId: params.conversationDetailId,
|
|
3996
|
+
userId: params.userId || params.contextUser?.ID,
|
|
3997
|
+
companyId: params.companyId,
|
|
3998
|
+
verbose: params.verbose,
|
|
3999
|
+
provider: this.ProviderToUse,
|
|
4000
|
+
});
|
|
4001
|
+
const failed = result.disposition === 'error' || result.disposition === 'rejected-type';
|
|
4002
|
+
await this.finalizeStepEntity(writeStep, !failed, failed ? result.reason : undefined, {
|
|
4003
|
+
disposition: result.disposition,
|
|
4004
|
+
noteId: result.noteId,
|
|
4005
|
+
finalScope: result.finalScope,
|
|
4006
|
+
reason: result.reason,
|
|
4007
|
+
durationMs: result.durationMs,
|
|
4008
|
+
});
|
|
4009
|
+
results.push(result);
|
|
4010
|
+
}
|
|
4011
|
+
return results;
|
|
4012
|
+
}
|
|
4013
|
+
/**
|
|
4014
|
+
* Turn-loop entry point for in-flight memory writes, gated on the agent's
|
|
4015
|
+
* AllowMemoryWrite flag. When disabled but the LLM emitted writes anyway
|
|
4016
|
+
* (prompt drift / injection attempt), records ONE summary skip step —
|
|
4017
|
+
* observable without per-write noise — and tells the agent the memories
|
|
4018
|
+
* were NOT saved so it stops re-emitting. When enabled, executes the
|
|
4019
|
+
* writes as run steps and injects the results message.
|
|
4020
|
+
*
|
|
4021
|
+
* @protected
|
|
4022
|
+
*/
|
|
4023
|
+
async processMemoryWritesForTurn(memoryWrites, params) {
|
|
4024
|
+
if (params.agent.AllowMemoryWrite !== true) {
|
|
4025
|
+
this.logStatus(`[MemoryWrites] LLM emitted ${memoryWrites.length} memory write(s) but AllowMemoryWrite=false — skipping`, true, params);
|
|
4026
|
+
const skipStep = await this.createStepEntity({
|
|
4027
|
+
stepType: 'Tool',
|
|
4028
|
+
stepName: 'Memory Writes: skipped (AllowMemoryWrite=false)',
|
|
4029
|
+
contextUser: params.contextUser,
|
|
4030
|
+
inputData: { requestedWriteCount: memoryWrites.length },
|
|
4031
|
+
});
|
|
4032
|
+
await this.finalizeStepEntity(skipStep, true, undefined, { skipped: true, reason: 'AllowMemoryWrite=false' });
|
|
4033
|
+
params.conversationMessages.push({
|
|
4034
|
+
role: 'user',
|
|
4035
|
+
content: 'Memory write result: this agent does not have durable memory writes enabled — the requested memories were NOT saved. Do not emit memoryWrites again.',
|
|
4036
|
+
metadata: {
|
|
4037
|
+
turnAdded: this._promptTurnCount,
|
|
4038
|
+
messageType: 'tool-result',
|
|
4039
|
+
expirationTurns: 3,
|
|
4040
|
+
expirationMode: 'Compact',
|
|
4041
|
+
compactMode: 'First N Chars',
|
|
4042
|
+
compactLength: 200,
|
|
4043
|
+
compactPromptId: '',
|
|
4044
|
+
},
|
|
4045
|
+
});
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
this.logStatus(`[MemoryWrites] LLM requested ${memoryWrites.length} memory write(s)`, true, params);
|
|
4049
|
+
const writeResults = await this.executeMemoryWritesAsSteps(memoryWrites, params);
|
|
4050
|
+
this.injectMemoryWriteResultsMessage(params, writeResults);
|
|
4051
|
+
}
|
|
4052
|
+
/**
|
|
4053
|
+
* Pushes a single user-role message containing memory-write outcomes into
|
|
4054
|
+
* the conversation, mirroring `injectArtifactToolResultsMessage`'s
|
|
4055
|
+
* inject-once-then-expire pattern. Closing the loop here is what stops the
|
|
4056
|
+
* LLM from re-emitting the same memory on subsequent turns.
|
|
4057
|
+
*
|
|
4058
|
+
* @protected
|
|
4059
|
+
*/
|
|
4060
|
+
injectMemoryWriteResultsMessage(params, results) {
|
|
4061
|
+
if (results.length === 0)
|
|
4062
|
+
return;
|
|
4063
|
+
const header = results.length === 1
|
|
4064
|
+
? 'Memory write result:'
|
|
4065
|
+
: `Memory write results (${results.length} writes):`;
|
|
4066
|
+
const body = results.map((r, i) => {
|
|
4067
|
+
const note = r.request.note.length > 120 ? `${r.request.note.slice(0, 120)}…` : r.request.note;
|
|
4068
|
+
return `${i + 1}. "${note}" — **${r.disposition}**${r.reason ? `: ${r.reason}` : ''}`;
|
|
4069
|
+
}).join('\n');
|
|
4070
|
+
const message = {
|
|
4071
|
+
role: 'user',
|
|
4072
|
+
content: `${header}\n${body}`,
|
|
4073
|
+
metadata: {
|
|
4074
|
+
turnAdded: this._promptTurnCount,
|
|
4075
|
+
messageType: 'tool-result',
|
|
4076
|
+
expirationTurns: 3,
|
|
4077
|
+
expirationMode: 'Compact',
|
|
4078
|
+
compactMode: 'First N Chars',
|
|
4079
|
+
compactLength: 300,
|
|
4080
|
+
compactPromptId: '',
|
|
4081
|
+
},
|
|
4082
|
+
};
|
|
4083
|
+
params.conversationMessages.push(message);
|
|
4084
|
+
}
|
|
3436
4085
|
/**
|
|
3437
4086
|
* Builds a per-run {@link PipelineToolRegistry} that unifies the three pipeline-able
|
|
3438
4087
|
* substrates behind one namespace: built-in transforms, the agent's effective Actions, and
|
|
@@ -3583,37 +4232,70 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3583
4232
|
*
|
|
3584
4233
|
* @private
|
|
3585
4234
|
*/
|
|
3586
|
-
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges) {
|
|
4235
|
+
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges, subAgentChanges) {
|
|
3587
4236
|
try {
|
|
3588
4237
|
const engine = AIEngine.Instance;
|
|
3589
|
-
//
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const
|
|
3599
|
-
//
|
|
3600
|
-
|
|
3601
|
-
let
|
|
3602
|
-
|
|
4238
|
+
// Build (or reuse) the agent-invariant base catalog. This is process-wide cached on
|
|
4239
|
+
// AIEngine and wiped on Agent/AgentAction/AgentRelationship/AgentType changes + reloads.
|
|
4240
|
+
// It turns the per-step rebuild (sub-agent + action resolution, markdown, JSON.parse of
|
|
4241
|
+
// agent-type params) into a once-per-agent cost; the common no-override step reuses it wholesale.
|
|
4242
|
+
let catalog = engine.GetAgentBaseCatalog(agent.ID);
|
|
4243
|
+
if (!catalog) {
|
|
4244
|
+
catalog = this.buildAgentBaseCatalog(agent, engine);
|
|
4245
|
+
engine.SetAgentBaseCatalog(agent.ID, catalog);
|
|
4246
|
+
}
|
|
4247
|
+
const isRoot = this._depth === 0;
|
|
4248
|
+
// Sub-agents: reuse cached base unless runtime subAgentChanges apply (then clone + re-format).
|
|
4249
|
+
let uniqueActiveSubAgents = catalog.uniqueActiveSubAgents;
|
|
4250
|
+
let subAgentDetails = catalog.subAgentDetails;
|
|
4251
|
+
let subAgentCount = catalog.subAgentCount;
|
|
4252
|
+
if (subAgentChanges?.length) {
|
|
4253
|
+
uniqueActiveSubAgents = this.applySubAgentChanges(catalog.uniqueActiveSubAgents, subAgentChanges, agent.ID, isRoot, engine);
|
|
4254
|
+
subAgentCount = uniqueActiveSubAgents.length;
|
|
4255
|
+
subAgentDetails = this.formatSubAgentDetails(uniqueActiveSubAgents);
|
|
4256
|
+
}
|
|
4257
|
+
// Actions: reuse cached active set unless runtime actionChanges apply (then clone + re-format).
|
|
4258
|
+
//
|
|
4259
|
+
// FAST-PATH SHARING CONTRACT: on the no-override path, `activeActions` (and therefore
|
|
4260
|
+
// `_effectiveActions`) and `uniqueActiveSubAgents` above are the SAME array references
|
|
4261
|
+
// held by the process-wide AIEngine catalog cache. Downstream consumers MUST treat them
|
|
4262
|
+
// as read-only — they are only ever read (`.find`/`.map`/`.length`/`.some`), never mutated
|
|
4263
|
+
// in place. On the override path a fresh array is built via filter/applyActionChanges, so
|
|
4264
|
+
// the cached arrays are never the mutated ones. Keeping the references (vs. copying) avoids
|
|
4265
|
+
// a per-step allocation; if a future consumer needs to mutate, it must `.slice()` first.
|
|
4266
|
+
let activeActions = catalog.activeActions;
|
|
4267
|
+
let actionDetails = catalog.actionDetails;
|
|
3603
4268
|
if (actionChanges?.length) {
|
|
3604
|
-
const
|
|
3605
|
-
|
|
3606
|
-
actions = result.actions;
|
|
4269
|
+
const result = this.applyActionChanges([...catalog.baseActionsRaw], actionChanges, agent.ID, isRoot);
|
|
4270
|
+
activeActions = result.actions.filter(a => a.Status === 'Active');
|
|
3607
4271
|
this._dynamicActionLimits = result.dynamicLimits;
|
|
4272
|
+
actionDetails = this.formatActionDetails(activeActions);
|
|
3608
4273
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
4274
|
+
else {
|
|
4275
|
+
// No actionChanges this step → no dynamically-added actions, hence no dynamic limits.
|
|
4276
|
+
// gatherPromptTemplateData runs once per step, and _dynamicActionLimits is keyed to the
|
|
4277
|
+
// actionChanges of the CURRENT step (read at validation time in checkActionExecutionLimits).
|
|
4278
|
+
// Resetting to {} is correct and required: it prevents a prior step's actionChanges limits
|
|
4279
|
+
// from leaking into a step that has none. It is NOT relied upon to persist across steps.
|
|
4280
|
+
this._dynamicActionLimits = {};
|
|
4281
|
+
}
|
|
4282
|
+
// Store for later validation in executeActionsStep
|
|
3611
4283
|
this._effectiveActions = activeActions;
|
|
3612
|
-
//
|
|
3613
|
-
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4284
|
+
// Agent type prompt params: reuse cached base merge unless a runtime override is present.
|
|
3614
4285
|
const runtimePromptParamOverrides = extraData?.__agentTypePromptParams;
|
|
3615
|
-
|
|
3616
|
-
|
|
4286
|
+
let agentTypePromptParams;
|
|
4287
|
+
if (runtimePromptParamOverrides) {
|
|
4288
|
+
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4289
|
+
agentTypePromptParams = this.buildAgentTypePromptParams(agentType, agent, runtimePromptParamOverrides);
|
|
4290
|
+
}
|
|
4291
|
+
else {
|
|
4292
|
+
// Fast path: shallow-clone the cached base params before handing them out. The cached
|
|
4293
|
+
// object lives in the process-wide AIEngine catalog and is shared across every run of
|
|
4294
|
+
// this agent; the audit shows it is read-only downstream today, but the clone is cheap
|
|
4295
|
+
// and removes any cache-poisoning foot-gun should a future consumer write to it.
|
|
4296
|
+
agentTypePromptParams = { ...catalog.baseAgentTypePromptParams };
|
|
4297
|
+
}
|
|
4298
|
+
// Build client tool details for the prompt (per-run; depends on extraData)
|
|
3617
4299
|
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
3618
4300
|
// Build app context section if provided in extraData
|
|
3619
4301
|
const appContext = this.buildAppContextSection(extraData);
|
|
@@ -3621,10 +4303,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3621
4303
|
agentName: agent.Name,
|
|
3622
4304
|
agentDescription: agent.Description,
|
|
3623
4305
|
parentAgentName: agent.Parent ? agent.Parent.trim() : "",
|
|
3624
|
-
subAgentCount:
|
|
3625
|
-
subAgentDetails:
|
|
4306
|
+
subAgentCount: subAgentCount,
|
|
4307
|
+
subAgentDetails: subAgentDetails,
|
|
3626
4308
|
actionCount: activeActions.length,
|
|
3627
|
-
actionDetails:
|
|
4309
|
+
actionDetails: actionDetails,
|
|
3628
4310
|
clientToolDetails: clientToolDetails,
|
|
3629
4311
|
appContext: appContext,
|
|
3630
4312
|
};
|
|
@@ -3656,6 +4338,98 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3656
4338
|
throw new Error(`Error gathering context data: ${error.message}`);
|
|
3657
4339
|
}
|
|
3658
4340
|
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Builds the agent-invariant {@link AgentBaseCatalog} — the resolved sub-agents + actions and
|
|
4343
|
+
* their formatted markdown, plus the base agent-type prompt params. Computed once per agent and
|
|
4344
|
+
* cached on AIEngine (see gatherPromptTemplateData); does NOT apply any runtime overrides.
|
|
4345
|
+
*
|
|
4346
|
+
* @protected
|
|
4347
|
+
*/
|
|
4348
|
+
buildAgentBaseCatalog(agent, engine) {
|
|
4349
|
+
// Resolve sub-agents: direct ParentID children + active relationships, de-duped, ordered.
|
|
4350
|
+
const activeSubAgents = engine.Agents.filter(a => UUIDsEqual(a.ParentID, agent.ID) && a.Status === 'Active')
|
|
4351
|
+
.sort((a, b) => a.ExecutionOrder - b.ExecutionOrder);
|
|
4352
|
+
const activeAgentRelationships = engine.AgentRelationships.filter(ar => UUIDsEqual(ar.AgentID, agent.ID) && ar.Status === 'Active');
|
|
4353
|
+
const uniqueActiveSubAgentIDs = new Set();
|
|
4354
|
+
activeSubAgents.forEach(a => uniqueActiveSubAgentIDs.add(a.ID));
|
|
4355
|
+
activeAgentRelationships.forEach(ar => uniqueActiveSubAgentIDs.add(ar.SubAgentID));
|
|
4356
|
+
const uniqueActiveSubAgents = Array.from(uniqueActiveSubAgentIDs).map(id => engine.Agents.find(a => UUIDsEqual(a.ID, id)));
|
|
4357
|
+
// Resolve actions from the agent's active AIAgentAction junctions.
|
|
4358
|
+
const agentActions = engine.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, agent.ID) && aa.Status === 'Active');
|
|
4359
|
+
const baseActionsRaw = ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)));
|
|
4360
|
+
const activeActions = baseActionsRaw.filter(a => a.Status === 'Active');
|
|
4361
|
+
// Base agent-type prompt params (schema defaults + agent config; NO runtime overrides).
|
|
4362
|
+
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4363
|
+
const baseAgentTypePromptParams = this.buildAgentTypePromptParams(agentType, agent, undefined);
|
|
4364
|
+
return {
|
|
4365
|
+
uniqueActiveSubAgents,
|
|
4366
|
+
subAgentCount: uniqueActiveSubAgents.length,
|
|
4367
|
+
subAgentDetails: this.formatSubAgentDetails(uniqueActiveSubAgents),
|
|
4368
|
+
baseActionsRaw,
|
|
4369
|
+
activeActions,
|
|
4370
|
+
actionDetails: this.formatActionDetails(activeActions),
|
|
4371
|
+
baseAgentTypePromptParams,
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Applies runtime {@link SubAgentChange}s to a base sub-agent set — the sub-agent counterpart of
|
|
4376
|
+
* {@link applyActionChanges}. Returns a NEW array (never mutates the cached base set).
|
|
4377
|
+
*
|
|
4378
|
+
* @protected
|
|
4379
|
+
*/
|
|
4380
|
+
applySubAgentChanges(baseSubAgents, subAgentChanges, agentId, isRoot, engine) {
|
|
4381
|
+
let subAgents = [...baseSubAgents];
|
|
4382
|
+
for (const change of subAgentChanges) {
|
|
4383
|
+
if (!this.doesChangeScopeApply(change.scope, agentId, isRoot, change.agentIds)) {
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
if (change.mode === 'add') {
|
|
4387
|
+
for (const subAgentId of change.subAgentIds) {
|
|
4388
|
+
if (!subAgents.some(a => UUIDsEqual(a.ID, subAgentId))) {
|
|
4389
|
+
const toAdd = engine.Agents.find(a => UUIDsEqual(a.ID, subAgentId));
|
|
4390
|
+
if (toAdd) {
|
|
4391
|
+
subAgents.push(toAdd);
|
|
4392
|
+
}
|
|
4393
|
+
else {
|
|
4394
|
+
LogStatus(`Sub-agent with ID '${subAgentId}' not found in AIEngine - skipping add`);
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
else if (change.mode === 'remove') {
|
|
4400
|
+
subAgents = subAgents.filter(a => !change.subAgentIds.some(id => UUIDsEqual(id, a.ID)));
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return subAgents;
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4406
|
+
* Filters/transforms sub-agent changes for propagation to a sub-agent — the sub-agent counterpart
|
|
4407
|
+
* of {@link filterActionChangesForSubAgent} (same propagation rules).
|
|
4408
|
+
*
|
|
4409
|
+
* @protected
|
|
4410
|
+
*/
|
|
4411
|
+
filterSubAgentChangesForSubAgent(subAgentChanges) {
|
|
4412
|
+
if (!subAgentChanges?.length) {
|
|
4413
|
+
return undefined;
|
|
4414
|
+
}
|
|
4415
|
+
const filtered = [];
|
|
4416
|
+
for (const change of subAgentChanges) {
|
|
4417
|
+
switch (change.scope) {
|
|
4418
|
+
case 'root':
|
|
4419
|
+
continue; // only applies to root — don't propagate
|
|
4420
|
+
case 'global':
|
|
4421
|
+
filtered.push(change);
|
|
4422
|
+
break;
|
|
4423
|
+
case 'all-subagents':
|
|
4424
|
+
filtered.push({ ...change, scope: 'global' });
|
|
4425
|
+
break;
|
|
4426
|
+
case 'specific':
|
|
4427
|
+
filtered.push(change);
|
|
4428
|
+
break;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
4432
|
+
}
|
|
3659
4433
|
/**
|
|
3660
4434
|
* Builds merged agent type prompt params from schema defaults,
|
|
3661
4435
|
* agent config, and runtime overrides.
|
|
@@ -3743,7 +4517,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3743
4517
|
{ docsFlag: 'includeWhileDocs', responseTypeKey: 'while' },
|
|
3744
4518
|
{ docsFlag: 'includeScratchpadDocs', responseTypeKey: 'scratchpad' },
|
|
3745
4519
|
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' },
|
|
3746
|
-
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' }
|
|
4520
|
+
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' },
|
|
4521
|
+
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' }
|
|
3747
4522
|
];
|
|
3748
4523
|
for (const { docsFlag, responseTypeKey } of alignmentMappings) {
|
|
3749
4524
|
// Check if the user explicitly set this response type property
|
|
@@ -3985,8 +4760,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3985
4760
|
this.logStatus(`🎯 Propagating effort level ${params.effortLevel} to sub-agent '${subAgentRequest.name}'`, true, params);
|
|
3986
4761
|
}
|
|
3987
4762
|
const parentStepCountsToPass = [...this._parentStepCounts, stepCount + 1];
|
|
3988
|
-
// Filter action changes for sub-agent propagation
|
|
4763
|
+
// Filter action / sub-agent changes for sub-agent propagation
|
|
3989
4764
|
const subAgentActionChanges = this.filterActionChangesForSubAgent(params.actionChanges);
|
|
4765
|
+
const subAgentSubAgentChanges = this.filterSubAgentChangesForSubAgent(params.subAgentChanges);
|
|
3990
4766
|
// Execute the sub-agent with cancellation and streaming support
|
|
3991
4767
|
// Use subAgentRequest.context if provided, otherwise fall back to params.context
|
|
3992
4768
|
// This allows Flow agents and Loop agents to propagate context through sub-agent requests
|
|
@@ -4014,6 +4790,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4014
4790
|
context: subAgentContext, // use subAgentRequest.context if provided, otherwise params.context
|
|
4015
4791
|
verbose: params.verbose, // pass verbose flag to sub-agent
|
|
4016
4792
|
actionChanges: subAgentActionChanges, // propagate filtered action changes to sub-agent
|
|
4793
|
+
subAgentChanges: subAgentSubAgentChanges, // propagate filtered sub-agent changes to sub-agent
|
|
4017
4794
|
PrimaryScopeEntityName: params.PrimaryScopeEntityName, // propagate scope to sub-agent
|
|
4018
4795
|
PrimaryScopeRecordID: params.PrimaryScopeRecordID,
|
|
4019
4796
|
SecondaryScopes: params.SecondaryScopes,
|
|
@@ -4604,6 +5381,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4604
5381
|
if (params.data?.conversationId) {
|
|
4605
5382
|
this._agentRun.ConversationID = params.data.conversationId;
|
|
4606
5383
|
}
|
|
5384
|
+
// Stamp the realtime/long-lived session id (if any) so every run — including delegated
|
|
5385
|
+
// child runs that inherit this value — is groupable under the same MJ: AI Agent Session.
|
|
5386
|
+
if (params.agentSessionID) {
|
|
5387
|
+
this._agentRun.AgentSessionID = params.agentSessionID;
|
|
5388
|
+
}
|
|
4607
5389
|
this._agentRun.Status = 'Running';
|
|
4608
5390
|
this._agentRun.StartedAt = new Date();
|
|
4609
5391
|
this._agentRun.UserID = params.userId || params.contextUser?.ID || null;
|
|
@@ -4761,6 +5543,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4761
5543
|
*/
|
|
4762
5544
|
async createStepEntity(params) {
|
|
4763
5545
|
const stepEntity = await this._activeProvider.GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
|
|
5546
|
+
// Client-generate the PK so the step ID is valid IMMEDIATELY (before the INSERT lands) — child
|
|
5547
|
+
// steps link via ParentID and the post-create UPDATE-phase mutations reference this row, and the
|
|
5548
|
+
// create INSERT is fire-and-forget (the agent flow must not block on it).
|
|
5549
|
+
stepEntity.NewRecord();
|
|
4764
5550
|
stepEntity.AgentRunID = this._agentRun.ID;
|
|
4765
5551
|
// Step number is based on current count of steps + 1
|
|
4766
5552
|
stepEntity.StepNumber = (this._agentRun.Steps?.length || 0) + 1;
|
|
@@ -4792,7 +5578,12 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4792
5578
|
}
|
|
4793
5579
|
});
|
|
4794
5580
|
}
|
|
4795
|
-
|
|
5581
|
+
// Fire-and-forget the 'started' INSERT — the agent flow never blocks on a step save. Store the
|
|
5582
|
+
// promise per-step so every later UPDATE (queueStepSave) runs only AFTER this INSERT commits
|
|
5583
|
+
// (see _stepInsertPromises).
|
|
5584
|
+
const insertPromise = this.saveStepRecord(stepEntity, 'insert');
|
|
5585
|
+
this._stepInsertPromises.set(stepEntity, insertPromise);
|
|
5586
|
+
this._pendingSaves.push(insertPromise);
|
|
4796
5587
|
// Add the step to the agent run's Steps array
|
|
4797
5588
|
if (this._agentRun) {
|
|
4798
5589
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -4859,6 +5650,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4859
5650
|
*/
|
|
4860
5651
|
async finalizeStepEntity(stepEntity, success, errorMessage, outputData) {
|
|
4861
5652
|
try {
|
|
5653
|
+
// Apply the completion state to the in-memory entity NOW (so the run's Steps array / UI see
|
|
5654
|
+
// Completed immediately), then fire-and-forget the UPDATE via queueStepSave — which chains after
|
|
5655
|
+
// the INSERT and force-persists (IgnoreDirtyState). The agent flow never blocks on this UPDATE.
|
|
4862
5656
|
stepEntity.Status = success ? 'Completed' : 'Failed';
|
|
4863
5657
|
stepEntity.CompletedAt = new Date();
|
|
4864
5658
|
stepEntity.Success = success;
|
|
@@ -4881,33 +5675,51 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4881
5675
|
}
|
|
4882
5676
|
}
|
|
4883
5677
|
/**
|
|
4884
|
-
*
|
|
4885
|
-
*
|
|
4886
|
-
*
|
|
4887
|
-
*
|
|
4888
|
-
* -
|
|
4889
|
-
*
|
|
4890
|
-
*
|
|
4891
|
-
|
|
4892
|
-
|
|
5678
|
+
* Saves one step record and ALWAYS logs (never swallows, never verbose-gated) a failure via `LogError`
|
|
5679
|
+
* with `LatestResult.CompleteMessage` — the log is observability, so a failure must surface but must
|
|
5680
|
+
* not throw into the agent loop. The `update` phase force-saves with `IgnoreDirtyState` because a
|
|
5681
|
+
* finalize/TargetLogID mutation applied while the INSERT was still in flight gets absorbed by the
|
|
5682
|
+
* INSERT's post-save dirty-reset, leaving the new values only in memory (the "step stuck at Running"
|
|
5683
|
+
* bug); forcing the UPDATE re-persists them. `insert` saves normally.
|
|
5684
|
+
* @returns whether the row persisted.
|
|
5685
|
+
*/
|
|
5686
|
+
async saveStepRecord(stepEntity, phase) {
|
|
5687
|
+
try {
|
|
5688
|
+
let options;
|
|
5689
|
+
if (phase === 'update') {
|
|
5690
|
+
options = new EntitySaveOptions();
|
|
5691
|
+
options.IgnoreDirtyState = true;
|
|
5692
|
+
}
|
|
5693
|
+
const ok = await stepEntity.Save(options);
|
|
5694
|
+
if (!ok) {
|
|
5695
|
+
LogError(`Failed to ${phase} agent run step record ${stepEntity.ID || '(unsaved)'}: ${stepEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
5696
|
+
}
|
|
5697
|
+
return ok;
|
|
5698
|
+
}
|
|
5699
|
+
catch (e) {
|
|
5700
|
+
LogError(`Error on ${phase} of agent run step record ${stepEntity.ID || '(unsaved)'}: ${e?.message ?? e}`);
|
|
5701
|
+
return false;
|
|
5702
|
+
}
|
|
5703
|
+
}
|
|
5704
|
+
/**
|
|
5705
|
+
* Queues a fire-and-forget UPDATE of a step entity whose fields the caller has ALREADY mutated.
|
|
4893
5706
|
*
|
|
4894
|
-
*
|
|
4895
|
-
*
|
|
4896
|
-
*
|
|
4897
|
-
*
|
|
5707
|
+
* - The agent flow never awaits this (logging is fire-and-forget).
|
|
5708
|
+
* - Chains after the step's 'started' INSERT and any prior queued save, so the UPDATE never races
|
|
5709
|
+
* ahead of the INSERT; updates to DIFFERENT steps run concurrently.
|
|
5710
|
+
* - Force-persists (IgnoreDirtyState) because a mutation applied while the INSERT was in flight can be
|
|
5711
|
+
* absorbed by the INSERT's post-save dirty-reset, leaving the entity "clean" with the new values
|
|
5712
|
+
* only in memory — without the force, the UPDATE would silently no-op and the row would stay stuck
|
|
5713
|
+
* at Status='Running' / null TargetLogID.
|
|
5714
|
+
* - Failures are logged (never thrown) and surfaced via `_pendingSaves` at run finalize.
|
|
4898
5715
|
*
|
|
4899
5716
|
* @protected
|
|
4900
5717
|
*/
|
|
4901
5718
|
queueStepSave(stepEntity) {
|
|
4902
|
-
// Chain on the entity INSTANCE (stable), NOT stepEntity.ID
|
|
4903
|
-
//
|
|
4904
|
-
const previousSave = this._stepSavePromises.get(stepEntity) ?? Promise.resolve();
|
|
4905
|
-
const currentSave = previousSave.then(() =>
|
|
4906
|
-
if (!ok) {
|
|
4907
|
-
LogError(`Failed to save agent run step record ${stepEntity.ID || '(unsaved)'}: ${stepEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
4908
|
-
}
|
|
4909
|
-
return ok;
|
|
4910
|
-
});
|
|
5719
|
+
// Chain on the entity INSTANCE (stable), NOT stepEntity.ID. Fall back to the INSERT promise so an
|
|
5720
|
+
// UPDATE queued before the create lands still runs after it.
|
|
5721
|
+
const previousSave = this._stepSavePromises.get(stepEntity) ?? this._stepInsertPromises.get(stepEntity) ?? Promise.resolve();
|
|
5722
|
+
const currentSave = previousSave.then(() => this.saveStepRecord(stepEntity, 'update'));
|
|
4911
5723
|
this._stepSavePromises.set(stepEntity, currentSave);
|
|
4912
5724
|
this._pendingSaves.push(currentSave);
|
|
4913
5725
|
}
|
|
@@ -5242,10 +6054,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5242
6054
|
},
|
|
5243
6055
|
displayMode: 'live' // Only show in live mode
|
|
5244
6056
|
});
|
|
5245
|
-
//
|
|
5246
|
-
|
|
5247
|
-
stepEntity.PayloadAtStart = this.serializePayloadAtStart(payload);
|
|
5248
|
-
}
|
|
6057
|
+
// PayloadAtStart was already serialized from this same `payload` by createStepEntity
|
|
6058
|
+
// above (payloadAtStart: payload) — no need to re-serialize the (potentially large) payload here.
|
|
5249
6059
|
let downstreamPayload = payload; // Start with current payload
|
|
5250
6060
|
if (params.agent.PayloadSelfReadPaths) {
|
|
5251
6061
|
const downstreamPaths = JSON.parse(params.agent.PayloadSelfReadPaths);
|
|
@@ -5294,7 +6104,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5294
6104
|
// Update step entity with AIPromptRun ID if available
|
|
5295
6105
|
if (promptResult.promptRun?.ID) {
|
|
5296
6106
|
stepEntity.TargetLogID = promptResult.promptRun.ID;
|
|
5297
|
-
stepEntity.PromptRun = promptResult.promptRun; //
|
|
6107
|
+
stepEntity.PromptRun = promptResult.promptRun; // transient related object (not a persisted field)
|
|
5298
6108
|
// don't save here, we save when we call finalizeStepEntity()
|
|
5299
6109
|
}
|
|
5300
6110
|
// Check if prompt execution failed
|
|
@@ -5411,6 +6221,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5411
6221
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
5412
6222
|
this.logStatus(`[ArtifactTools] LLM did not use artifact tools this turn (artifacts available but not accessed)`, true, params);
|
|
5413
6223
|
}
|
|
6224
|
+
// Execute in-flight memory writes if provided (zero turn cost — processed inline)
|
|
6225
|
+
const memoryWrites = initialNextStep.memoryWrites;
|
|
6226
|
+
if (memoryWrites?.length) {
|
|
6227
|
+
await this.processMemoryWritesForTurn(memoryWrites, params);
|
|
6228
|
+
}
|
|
5414
6229
|
// Execute a tool pipeline if provided (zero turn cost — processed inline). Each step's
|
|
5415
6230
|
// output is threaded into the next server-side; only the final step's output returns to
|
|
5416
6231
|
// the LLM, so intermediate payloads never enter the context window.
|
|
@@ -5797,7 +6612,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5797
6612
|
// Update step entity with AIAgentRun ID if available
|
|
5798
6613
|
if (subAgentResult.agentRun?.ID) {
|
|
5799
6614
|
stepEntity.TargetLogID = subAgentResult.agentRun.ID;
|
|
5800
|
-
// Set the SubAgentRun property for hierarchical tracking
|
|
6615
|
+
// Set the SubAgentRun property for hierarchical tracking (transient related object)
|
|
5801
6616
|
stepEntity.SubAgentRun = subAgentResult.agentRun;
|
|
5802
6617
|
stepEntity.PayloadAtEnd = this.serializePayloadAtEnd(mergedPayload);
|
|
5803
6618
|
// saving happens later by calling finalizeStepEntity()
|
|
@@ -8259,12 +9074,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8259
9074
|
else {
|
|
8260
9075
|
this._agentRun.Status = 'Completed';
|
|
8261
9076
|
}
|
|
8262
|
-
|
|
9077
|
+
// Serialize the (largest-it-ever-gets) final payload ONCE and reuse for both
|
|
9078
|
+
// Result and FinalPayload instead of stringifying the same object three times.
|
|
9079
|
+
const finalPayloadJson = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
9080
|
+
this._agentRun.Result = finalPayloadJson;
|
|
8263
9081
|
this._agentRun.FinalStep = finalStep.step;
|
|
8264
9082
|
this._agentRun.Message = finalStep.message;
|
|
8265
|
-
// Set the FinalPayloadObject
|
|
9083
|
+
// Set the FinalPayloadObject (populates the object cache; its setter also writes
|
|
9084
|
+
// FinalPayload when the value changes). We then assign FinalPayload from the
|
|
9085
|
+
// already-computed JSON to guarantee it's set regardless of the setter's change guard.
|
|
8266
9086
|
this._agentRun.FinalPayloadObject = resolvedPayload;
|
|
8267
|
-
this._agentRun.FinalPayload =
|
|
9087
|
+
this._agentRun.FinalPayload = finalPayloadJson;
|
|
8268
9088
|
// Calculate total tokens from all prompts and sub-agents
|
|
8269
9089
|
const tokenStats = this.calculateTokenStats();
|
|
8270
9090
|
this._agentRun.TotalTokensUsed = tokenStats.totalTokens;
|