@memberjunction/ai-agents 5.40.2 → 5.42.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 +53 -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 +386 -39
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +1121 -261
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +99 -4
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +349 -117
- package/dist/memory-manager-agent.js.map +1 -1
- package/dist/realtime/bridge-realtime-session-factory.d.ts +111 -0
- package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -0
- package/dist/realtime/bridge-realtime-session-factory.js +163 -0
- package/dist/realtime/bridge-realtime-session-factory.js.map +1 -0
- package/dist/realtime/bridge-room-transcript-sink.d.ts +58 -0
- package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -0
- package/dist/realtime/bridge-room-transcript-sink.js +127 -0
- package/dist/realtime/bridge-room-transcript-sink.js.map +1 -0
- 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 +1026 -0
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -0
- package/dist/realtime/realtime-client-session-service.js +1607 -0
- package/dist/realtime/realtime-client-session-service.js.map +1 -0
- package/dist/realtime/realtime-coagent-config.d.ts +258 -0
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -0
- package/dist/realtime/realtime-coagent-config.js +408 -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 +294 -0
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -0
- package/dist/realtime/realtime-tool-broker.js +206 -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
|
@@ -14,19 +14,24 @@ import { FileStorageEngineBase } from '@memberjunction/core-entities';
|
|
|
14
14
|
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase } 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 { BuildRealtimeOverridesJson, BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.js';
|
|
23
|
+
import { RealtimeClientSessionService } from './realtime/realtime-client-session-service.js';
|
|
24
|
+
import { BuildRealtimeAgentFraming } from './realtime/realtime-tool-broker.js';
|
|
19
25
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
20
26
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
21
27
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { RerankerService } from '@memberjunction/ai-reranker';
|
|
25
|
-
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
|
|
28
|
+
import { AgentMemoryContextBuilder } from './agent-memory-context-builder.js';
|
|
29
|
+
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue } from '@memberjunction/ai-core-plus';
|
|
26
30
|
import { AgentRunner } from './AgentRunner.js';
|
|
27
31
|
import { PayloadManager } from './PayloadManager.js';
|
|
28
32
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
29
33
|
import { ArtifactToolManager } from './ArtifactToolManager.js';
|
|
34
|
+
import { MemoryWriteManager } from './MemoryWriteManager.js';
|
|
30
35
|
import { PipelineExecutor, PipelineToolRegistry, ActionInvocable, ArtifactToolInvocable, BuildPipelineToolDocs, formatFinalOutput, summarizePipelineStages, } from './pipeline/index.js';
|
|
31
36
|
import { AgentDataPreloader } from './AgentDataPreloader.js';
|
|
32
37
|
import { ClientToolRequestManager } from './ClientToolRequestManager.js';
|
|
@@ -89,20 +94,13 @@ export class BaseAgent {
|
|
|
89
94
|
*/
|
|
90
95
|
this._promptRunner = new AIPromptRunner();
|
|
91
96
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
97
|
+
* Fire-and-forget save orchestration for this run's observability step records: the create INSERT is
|
|
98
|
+
* fired without blocking the agent flow (the PK is client-generated by `NewRecord()`), each finalize
|
|
99
|
+
* UPDATE chains after its step's INSERT and force-persists (`IgnoreDirtyState`), and all pending saves
|
|
100
|
+
* are flushed (`allSettled`) in {@link finalizeAgentRun}. The pattern lives once in
|
|
101
|
+
* {@link AgentRunStepSaveQueue} (shared with `@memberjunction/computer-use-engine`'s step tracker).
|
|
94
102
|
*/
|
|
95
|
-
this.
|
|
96
|
-
/**
|
|
97
|
-
* Queue map to chain database saves sequentially per step entity.
|
|
98
|
-
* Prevents UPDATE queries running before INSERT queries on quick steps.
|
|
99
|
-
*
|
|
100
|
-
* Keyed by the step ENTITY INSTANCE, not its `ID`: a new step's `ID` is empty at create time and
|
|
101
|
-
* only gets populated during its INSERT `Save()`, so keying by `ID` would file the create and the
|
|
102
|
-
* finalize under different buckets and defeat the chain — letting the UPDATE race ahead of the
|
|
103
|
-
* INSERT on millisecond-fast steps (e.g. pipelines), which left them stuck at `Running`.
|
|
104
|
-
*/
|
|
105
|
-
this._stepSavePromises = new Map();
|
|
103
|
+
this._stepSaveQueue = new AgentRunStepSaveQueue();
|
|
106
104
|
/**
|
|
107
105
|
* Active per-request metadata provider, set at the start of Execute().
|
|
108
106
|
* Defaults to the global Metadata.Provider; overridden when a per-request
|
|
@@ -209,6 +207,11 @@ export class BaseAgent {
|
|
|
209
207
|
* Allows agents to explore input artifacts on demand.
|
|
210
208
|
*/
|
|
211
209
|
this._artifactToolManager = new ArtifactToolManager();
|
|
210
|
+
/**
|
|
211
|
+
* Manages in-flight durable memory writes for the current agent run.
|
|
212
|
+
* Only consulted when the agent has AllowMemoryWrite enabled.
|
|
213
|
+
*/
|
|
214
|
+
this._memoryWriteManager = new MemoryWriteManager();
|
|
212
215
|
/**
|
|
213
216
|
* Effective actions available to this agent after applying actionChanges.
|
|
214
217
|
* Populated during gatherPromptTemplateData() and used for validation in executeActionsStep().
|
|
@@ -253,21 +256,16 @@ export class BaseAgent {
|
|
|
253
256
|
* @private
|
|
254
257
|
*/
|
|
255
258
|
this.MAX_RECOVERY_ATTEMPTS = 1;
|
|
256
|
-
/**
|
|
257
|
-
* Storage for injected memory context to prepend to prompts
|
|
258
|
-
*/
|
|
259
|
-
this._memoryContext = '';
|
|
260
259
|
/**
|
|
261
260
|
* Storage for injected notes and examples to include in result
|
|
262
261
|
*/
|
|
263
262
|
this._injectedMemory = { notes: [], examples: [] };
|
|
264
263
|
/**
|
|
265
264
|
* Storage for injected pre-execution RAG context (Phase 1C of search-scopes-rag-plus).
|
|
266
|
-
* Contains the
|
|
267
|
-
*
|
|
268
|
-
*
|
|
265
|
+
* Contains the structured per-scope / combined result detail for downstream observability
|
|
266
|
+
* and artifact persistence. The formatted `<retrieved_context>` system-message block is
|
|
267
|
+
* unshifted onto `conversationMessages` by the shared {@link AgentMemoryContextBuilder}.
|
|
269
268
|
*/
|
|
270
|
-
this._ragContext = '';
|
|
271
269
|
this._injectedRAG = null;
|
|
272
270
|
/**
|
|
273
271
|
* Determines the request type ID based on the Chat step's context.
|
|
@@ -1006,6 +1004,7 @@ export class BaseAgent {
|
|
|
1006
1004
|
// Reset scratchpad and artifact tools for each new execution (ephemeral per run)
|
|
1007
1005
|
this._scratchpadManager.Clear();
|
|
1008
1006
|
this._artifactToolManager.Clear();
|
|
1007
|
+
this._memoryWriteManager.Clear();
|
|
1009
1008
|
// Initialize artifact tools with any input artifacts attached to the run.
|
|
1010
1009
|
// Artifacts arrive as a typed first-class field on ExecuteAgentParams —
|
|
1011
1010
|
// they are NOT routed through `data` because prompt-template rendering
|
|
@@ -1146,6 +1145,21 @@ export class BaseAgent {
|
|
|
1146
1145
|
// Must wait for config from Phase 2 because it needs the resolved agent type and
|
|
1147
1146
|
// prompt configuration to initialize the type-specific state machine.
|
|
1148
1147
|
await this.initializeAgentType(wrappedParams, config);
|
|
1148
|
+
// =====================================================================================
|
|
1149
|
+
// SESSION-DRIVEN BRANCH (Realtime agent type)
|
|
1150
|
+
//
|
|
1151
|
+
// For session-driven agent types (the Realtime / Realtime Co-Agent type, marked by
|
|
1152
|
+
// `IsSessionDriven === true`), we do NOT enter the iterative reasoning loop. Instead we
|
|
1153
|
+
// hand control to a RealtimeSessionRunner that drives a long-lived duplex model session.
|
|
1154
|
+
//
|
|
1155
|
+
// This is the ONLY entry point into the realtime path. Loop and Flow agent types do not
|
|
1156
|
+
// expose `IsSessionDriven`, so `isSessionDrivenAgentType(...)` returns false for them and
|
|
1157
|
+
// their execution falls through to `executeAgentInternal` below — byte-for-byte unchanged.
|
|
1158
|
+
// =====================================================================================
|
|
1159
|
+
if (this.isSessionDrivenAgentType(this.AgentTypeInstance)) {
|
|
1160
|
+
this.logStatus(`🎙️ Agent '${params.agent.Name}' is session-driven — routing to RealtimeSessionRunner`, true, params);
|
|
1161
|
+
return await this.executeRealtimeSession(wrappedParams, config);
|
|
1162
|
+
}
|
|
1149
1163
|
// Execute the agent's internal logic with wrapped parameters
|
|
1150
1164
|
this.logStatus(`🚀 Executing agent '${params.agent.Name}' internal logic`, true, params);
|
|
1151
1165
|
const executionResult = await this.executeAgentInternal(wrappedParams, config);
|
|
@@ -1201,6 +1215,676 @@ export class BaseAgent {
|
|
|
1201
1215
|
params.cancellationToken = upstreamToken;
|
|
1202
1216
|
}
|
|
1203
1217
|
}
|
|
1218
|
+
// =====================================================================================
|
|
1219
|
+
// REALTIME (SESSION-DRIVEN) AGENT SUPPORT
|
|
1220
|
+
//
|
|
1221
|
+
// The methods below back the session-driven branch taken in Execute() for the Realtime
|
|
1222
|
+
// agent type. They are entered ONLY via that guarded branch; Loop/Flow agents never reach
|
|
1223
|
+
// them. The bulk of the work is building a RealtimeSessionRunnerDeps from BaseAgent's real
|
|
1224
|
+
// collaborators (model resolution, sub-agent delegation, tool execution, transcript
|
|
1225
|
+
// persistence, and usage checkpointing) and then driving RealtimeSessionRunner.Run().
|
|
1226
|
+
// =====================================================================================
|
|
1227
|
+
/**
|
|
1228
|
+
* Type guard for whether the resolved agent-type instance is session-driven.
|
|
1229
|
+
*
|
|
1230
|
+
* Detects the Realtime agent type without importing it (and without `instanceof`, which is
|
|
1231
|
+
* brittle under bundler class-duplication) by duck-typing the `IsSessionDriven` getter that
|
|
1232
|
+
* `RealtimeAgentType` adds. `BaseAgentType` (and Loop/Flow) do not expose this member, so the
|
|
1233
|
+
* guard returns `false` for them and the iterative loop runs unchanged.
|
|
1234
|
+
*
|
|
1235
|
+
* @param agentType The resolved agent-type instance for this run.
|
|
1236
|
+
* @returns `true` only when the type explicitly marks itself session-driven.
|
|
1237
|
+
*/
|
|
1238
|
+
isSessionDrivenAgentType(agentType) {
|
|
1239
|
+
return agentType.IsSessionDriven === true;
|
|
1240
|
+
}
|
|
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
|
+
async executeRealtimeSession(params, config) {
|
|
1259
|
+
// 1) Resolve the realtime model (overridable seam — tests inject a mock).
|
|
1260
|
+
const modelResolution = await this.resolveRealtimeModel(params);
|
|
1261
|
+
if (!modelResolution) {
|
|
1262
|
+
const message = `Agent '${params.agent.Name}' is session-driven (Realtime) but no usable Realtime model could be ` +
|
|
1263
|
+
`resolved. Configure a model of AIModelType 'Realtime' with an active vendor DriverClass and a ` +
|
|
1264
|
+
`valid API key (e.g. AI_VENDOR_API_KEY__<driver>). This is expected until the realtime drivers ` +
|
|
1265
|
+
`and model metadata are provisioned.`;
|
|
1266
|
+
this.logError(message, { agent: params.agent, category: 'RealtimeSession' });
|
|
1267
|
+
return await this.createFailureResult(message, params.contextUser);
|
|
1268
|
+
}
|
|
1269
|
+
// 2) Create the single long-lived AIPromptRun that usage is checkpointed onto.
|
|
1270
|
+
const promptRun = await this.createRealtimePromptRun(params, config, modelResolution);
|
|
1271
|
+
// 3) Build the injected deps and run the session.
|
|
1272
|
+
try {
|
|
1273
|
+
const deps = await this.buildRealtimeSessionDeps(params, config, modelResolution, promptRun);
|
|
1274
|
+
const runner = new RealtimeSessionRunner(deps);
|
|
1275
|
+
const sessionResult = await runner.Run();
|
|
1276
|
+
return await this.finalizeRealtimeRun(params, sessionResult);
|
|
1277
|
+
}
|
|
1278
|
+
catch (error) {
|
|
1279
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1280
|
+
this.logError(`Realtime session failed for agent '${params.agent.Name}': ${msg}`, {
|
|
1281
|
+
agent: params.agent,
|
|
1282
|
+
category: 'RealtimeSession'
|
|
1283
|
+
});
|
|
1284
|
+
return await this.createFailureResult(msg, params.contextUser);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Opens a **raw** {@link IRealtimeSession} for this agent — the duplex model connection a Realtime
|
|
1289
|
+
* Bridge hands to `AIBridgeEngine.StartBridgeSession` so the agent can talk + hear over a media
|
|
1290
|
+
* transport (a LiveKit room, a Zoom/Teams meeting, a phone call). The bridge engine owns turn-taking
|
|
1291
|
+
* and the transport seam, so this deliberately returns the **session itself**, NOT a
|
|
1292
|
+
* {@link RealtimeSessionRunner} (which is the client-direct topology's own orchestration loop).
|
|
1293
|
+
*
|
|
1294
|
+
* It reuses the EXACT same resolution + assembly as {@link executeRealtimeSession} — model selection
|
|
1295
|
+
* ({@link resolveRealtimeModel}), agent configuration ({@link loadAgentConfiguration}), effective-config
|
|
1296
|
+
* persona/voice ({@link resolveRealtimeEffectiveConfig}), and the system-prompt + memory context
|
|
1297
|
+
* ({@link buildRealtimeSessionParams}) — then opens the session via
|
|
1298
|
+
* {@link BaseRealtimeModel.StartSession}. Tools are intentionally NOT pre-populated: the
|
|
1299
|
+
* `invoke-target-agent` + interactive-surface tools are a runner concern; a bridge that needs them
|
|
1300
|
+
* registers them on the returned session itself.
|
|
1301
|
+
*
|
|
1302
|
+
* @param params The execution parameters (agent + context user + the request-scoped provider). A fresh
|
|
1303
|
+
* bridge session typically passes an empty `conversationMessages` array.
|
|
1304
|
+
* @returns The live realtime session.
|
|
1305
|
+
* @throws When the agent configuration fails to load or no usable Realtime model can be resolved.
|
|
1306
|
+
*/
|
|
1307
|
+
async StartBridgeRealtimeSession(params) {
|
|
1308
|
+
// Mirror Execute()'s provider wiring so the realtime helpers operate on the request-scoped provider.
|
|
1309
|
+
this._activeProvider = params.provider ?? Metadata.Provider;
|
|
1310
|
+
const provider = params.provider ?? Metadata.Provider;
|
|
1311
|
+
// A LiveKit / Zoom / Teams bridge is a thin TRANSPORT over the realtime co-agent — it does NOT build
|
|
1312
|
+
// session prep itself. It CONSUMES the one shared producer
|
|
1313
|
+
// ({@link RealtimeClientSessionService.PrepareRealtimeSessionParams}) so the agent's identity (it
|
|
1314
|
+
// speaks first-person AS the target — Sage / Marketing Agent / …), the model + voice precedence
|
|
1315
|
+
// cascade, the tool set (always incl. invoke-target-agent), and memory are byte-for-byte identical to
|
|
1316
|
+
// the native realtime chat. Bridges differ ONLY in opening the session server-side (StartSession) and
|
|
1317
|
+
// their media transport. See plans/realtime/realtime-core-host-convergence.md.
|
|
1318
|
+
// ONE service instance: it produces the prep AND wires the long-lived runtime, so the in-flight
|
|
1319
|
+
// delegation registry (barge-in cancel) is shared between them.
|
|
1320
|
+
const service = new RealtimeClientSessionService();
|
|
1321
|
+
const input = this.buildBridgePrepInput(params);
|
|
1322
|
+
const contextUser = params.contextUser;
|
|
1323
|
+
const prep = await service.PrepareRealtimeSessionParams(input, contextUser, provider);
|
|
1324
|
+
if (!prep.Success || !prep.Resolution || !prep.SessionParams) {
|
|
1325
|
+
throw new Error(prep.ErrorMessage ?? `Failed to prepare a realtime session for agent '${params.agent.Name}'. ` +
|
|
1326
|
+
`Configure an Active AIModelType 'Realtime' model with an active vendor whose DriverClass has a ` +
|
|
1327
|
+
`resolvable API key.`);
|
|
1328
|
+
}
|
|
1329
|
+
const session = await prep.Resolution.Model.StartSession(prep.SessionParams);
|
|
1330
|
+
// Phase 2: wire the SAME core runtime the native chat uses — real `invoke-target-agent` delegation
|
|
1331
|
+
// (target runs via AgentRunner, nested + tracked) + co-agent run/prompt-run observability, finalized
|
|
1332
|
+
// when the bridge calls `session.Close()`. No host-local tool re-implementation. The runtime handle's
|
|
1333
|
+
// side effects live on `session` (OnToolCall + a finalize-wrapped Close), so the bridge just owns the
|
|
1334
|
+
// session. See plans/realtime/realtime-core-host-convergence.md (Phase 2).
|
|
1335
|
+
await service.WireBridgeRealtimeSession(session, input, prep, contextUser, provider);
|
|
1336
|
+
return session;
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Adapts {@link ExecuteAgentParams} → the core {@link PrepareClientSessionInput} for a server-bridged
|
|
1340
|
+
* session. The CO-AGENT is the executed agent; the TARGET agent + the per-session model/voice override
|
|
1341
|
+
* ride `params.data` (the same conduit the native dev picker uses, funneled into the one
|
|
1342
|
+
* `ConfigOverridesJson` cascade slot via {@link BuildRealtimeOverridesJson}). Tools are left empty — a
|
|
1343
|
+
* bridge host injects its OWN UX tools (none for LiveKit audio today); identity/precedence/invoke-target
|
|
1344
|
+
* come from the core. `AgentSessionID` groups this session's observability runs (see
|
|
1345
|
+
* {@link RealtimeClientSessionService.WireBridgeRealtimeSession}).
|
|
1346
|
+
*
|
|
1347
|
+
* @param params The bridge execution parameters.
|
|
1348
|
+
* @returns The core prep input.
|
|
1349
|
+
*/
|
|
1350
|
+
buildBridgePrepInput(params) {
|
|
1351
|
+
const modelID = params.data?.realtimeModelID?.trim() || undefined;
|
|
1352
|
+
const voice = params.data?.realtimeVoice?.trim() || undefined;
|
|
1353
|
+
const targetID = params.data?.targetAgentID?.trim() || '';
|
|
1354
|
+
// Multi-agent meeting signal (set by the room coordinator when the agent joins a room that already
|
|
1355
|
+
// has agents): disable the model's blind auto-response + add meeting discipline to the prompt so it
|
|
1356
|
+
// hears everything but speaks only when addressed. SelfNames feed only the prompt phrasing; the
|
|
1357
|
+
// addressing GATE is the bridge's matcher. See plans/realtime/multi-agent-meeting-turn-taking.md.
|
|
1358
|
+
const meetingMode = params.data?.realtimeMeetingMode === true;
|
|
1359
|
+
const selfNames = Array.isArray(params.data?.realtimeSelfNames)
|
|
1360
|
+
? (params.data?.realtimeSelfNames).filter((n) => typeof n === 'string')
|
|
1361
|
+
: undefined;
|
|
1362
|
+
return {
|
|
1363
|
+
CoAgent: params.agent,
|
|
1364
|
+
TargetAgentID: targetID,
|
|
1365
|
+
AgentSessionID: params.data?.agentSessionId ?? '',
|
|
1366
|
+
PreferredModelID: modelID,
|
|
1367
|
+
ConfigOverridesJson: BuildRealtimeOverridesJson(modelID, voice) ?? undefined,
|
|
1368
|
+
ConversationMessages: params.conversationMessages,
|
|
1369
|
+
UserID: params.contextUser?.ID,
|
|
1370
|
+
DisableAutoResponse: meetingMode || undefined,
|
|
1371
|
+
SelfNames: selfNames,
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Resolves the realtime model + vendor driver + API key for a session-driven run.
|
|
1376
|
+
*
|
|
1377
|
+
* **Overridable seam.** This is the single injection point that test subclasses override to
|
|
1378
|
+
* return a mock {@link BaseRealtimeModel}, so {@link executeRealtimeSession} can be exercised
|
|
1379
|
+
* without provider SDKs or DB metadata.
|
|
1380
|
+
*
|
|
1381
|
+
* Production resolution: pick the highest-power active model of AIModelType `Realtime`; then
|
|
1382
|
+
* pick its highest-priority active vendor whose `DriverClass` has a resolvable API key; then
|
|
1383
|
+
* instantiate the driver via the `ClassFactory`. Returns `null` (never throws) if any step
|
|
1384
|
+
* can't be satisfied — the caller turns that into a clean FAILED result. (Per-agent realtime
|
|
1385
|
+
* model preference can later be wired through the agent's prompt-model config, the same path
|
|
1386
|
+
* loop agents use for `ModelSelectionMode`; the AI Agent entity has no direct model FK.)
|
|
1387
|
+
*
|
|
1388
|
+
* @param params The execution parameters (for the agent + context user).
|
|
1389
|
+
* @returns The resolved model instance plus its model/vendor identifiers, or `null`.
|
|
1390
|
+
*/
|
|
1391
|
+
async resolveRealtimeModel(params, overrideModelID) {
|
|
1392
|
+
// Walk candidates in resolution order (preference first, then highest PowerRank), returning the
|
|
1393
|
+
// FIRST that FULLY resolves (active vendor + resolvable API key + ClassFactory driver). Single-pick
|
|
1394
|
+
// would dead-end whenever the top model lacked a key — e.g. a power-11 model with no env key
|
|
1395
|
+
// (Inworld/AssemblyAI) outranking GPT Realtime — and surface "No usable Realtime model" even though
|
|
1396
|
+
// a usable model exists. This mirrors the same fix in RealtimeClientSessionService.
|
|
1397
|
+
const candidates = this.selectRealtimeModelCandidates(params.agent, overrideModelID);
|
|
1398
|
+
for (const model of candidates) {
|
|
1399
|
+
const vendor = this.selectRealtimeVendor(model.ID);
|
|
1400
|
+
if (!vendor) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const apiKey = GetAIAPIKey(vendor.driverClass);
|
|
1404
|
+
if (!apiKey) {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const instance = MJGlobal.Instance.ClassFactory.CreateInstance(BaseRealtimeModel, vendor.driverClass, apiKey);
|
|
1408
|
+
if (!instance) {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
return { model: instance, modelID: model.ID, vendorID: vendor.vendorID, apiName: vendor.apiName, driverClass: vendor.driverClass };
|
|
1412
|
+
}
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* The active `Realtime`-AIModelType models to try, in resolution order — the candidate list
|
|
1417
|
+
* {@link resolveRealtimeModel} walks until one yields a usable vendor + key + driver. Returns ALL
|
|
1418
|
+
* candidates (not just the top pick) so a keyless / undriveable higher-power model falls through to
|
|
1419
|
+
* the next usable one instead of dead-ending the whole resolution.
|
|
1420
|
+
*
|
|
1421
|
+
* Ordering: an effective-config model preference (`realtime.modelPreference`, an MJ: AI Models Name
|
|
1422
|
+
* or ID) goes FIRST when it resolves, followed by the rest by descending PowerRank (so even a keyless
|
|
1423
|
+
* preferred model degrades gracefully). An unsatisfiable preference logs and is ignored.
|
|
1424
|
+
*
|
|
1425
|
+
* @param agent The agent being executed.
|
|
1426
|
+
* @returns The candidate models in resolution order (empty when none are active).
|
|
1427
|
+
*/
|
|
1428
|
+
selectRealtimeModelCandidates(agent, overrideModelID) {
|
|
1429
|
+
const isRealtime = (m) => typeof m.AIModelType === 'string' && m.AIModelType.trim().toLowerCase() === 'realtime';
|
|
1430
|
+
const realtimeModels = AIEngine.Instance.Models.filter(m => m.IsActive && isRealtime(m));
|
|
1431
|
+
if (realtimeModels.length === 0) {
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1434
|
+
const byPower = [...realtimeModels].sort((a, b) => (b.PowerRank ?? 0) - (a.PowerRank ?? 0));
|
|
1435
|
+
// A per-session override (a dev picking a specific Realtime model for this bridged agent) wins over
|
|
1436
|
+
// the config's modelPreference — same "preferred first, rest by power as fallback" semantics.
|
|
1437
|
+
const preference = (overrideModelID && overrideModelID.trim().length > 0)
|
|
1438
|
+
? overrideModelID.trim()
|
|
1439
|
+
: this.resolveRealtimeEffectiveConfig(agent).realtime?.modelPreference;
|
|
1440
|
+
if (preference) {
|
|
1441
|
+
const wanted = preference.trim().toLowerCase();
|
|
1442
|
+
const preferred = realtimeModels.find(m => UUIDsEqual(m.ID, preference))
|
|
1443
|
+
?? realtimeModels.find(m => m.Name?.trim().toLowerCase() === wanted);
|
|
1444
|
+
if (preferred) {
|
|
1445
|
+
// Preference first, the rest (by power) as fallback so a keyless preferred model still
|
|
1446
|
+
// falls through to a usable one rather than dead-ending.
|
|
1447
|
+
return [preferred, ...byPower.filter(m => !UUIDsEqual(m.ID, preferred.ID))];
|
|
1448
|
+
}
|
|
1449
|
+
this.logError(`Realtime model preference '${preference}' for agent '${agent.Name}' matches no Active Realtime ` +
|
|
1450
|
+
'model — falling through to default (highest-PowerRank) selection.', { agent, category: 'RealtimeSession' });
|
|
1451
|
+
}
|
|
1452
|
+
return byPower;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Resolves the agent's EFFECTIVE realtime configuration — the agent TYPE's
|
|
1456
|
+
* `DefaultConfiguration` (base layer) deep-merged with the agent's `TypeConfiguration`
|
|
1457
|
+
* (per-agent layer; the server-bridged path has no runtime-override layer). Tolerant:
|
|
1458
|
+
* malformed layers contribute nothing and an unloaded type cache yields no type defaults.
|
|
1459
|
+
* See `realtime/realtime-coagent-config.ts` for the merge contract.
|
|
1460
|
+
*
|
|
1461
|
+
* @param agent The session-driven (Realtime) agent.
|
|
1462
|
+
* @returns The normalized effective configuration (possibly empty, never `null`).
|
|
1463
|
+
*/
|
|
1464
|
+
resolveRealtimeEffectiveConfig(agent) {
|
|
1465
|
+
let typeDefault = null;
|
|
1466
|
+
try {
|
|
1467
|
+
if (agent.TypeID) {
|
|
1468
|
+
const type = (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, agent.TypeID));
|
|
1469
|
+
typeDefault = type?.DefaultConfiguration ?? null;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
typeDefault = null;
|
|
1474
|
+
}
|
|
1475
|
+
return ResolveEffectiveRealtimeConfig(typeDefault, agent.TypeConfiguration ?? null, null);
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Selects the highest-priority active vendor for a model whose `DriverClass` has a resolvable
|
|
1479
|
+
* API key. Mirrors the vendor-selection pattern used by prompt execution.
|
|
1480
|
+
*
|
|
1481
|
+
* @param modelID The chosen model's ID.
|
|
1482
|
+
* @returns The vendor driver/api identifiers, or `null` when none has a usable key.
|
|
1483
|
+
*/
|
|
1484
|
+
selectRealtimeVendor(modelID) {
|
|
1485
|
+
const vendors = AIEngine.Instance.ModelVendors
|
|
1486
|
+
.filter(mv => UUIDsEqual(mv.ModelID, modelID) && mv.Status === 'Active' && mv.DriverClass != null)
|
|
1487
|
+
.sort((a, b) => (b.Priority ?? 0) - (a.Priority ?? 0));
|
|
1488
|
+
for (const v of vendors) {
|
|
1489
|
+
if (GetAIAPIKey(v.DriverClass)) {
|
|
1490
|
+
return { vendorID: v.VendorID ?? '', driverClass: v.DriverClass, apiName: v.APIName ?? '' };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Creates the single long-lived `AIPromptRun` that realtime usage is checkpointed onto.
|
|
1497
|
+
*
|
|
1498
|
+
* One run is created per session (not per turn) so {@link RealtimeSessionRunnerDeps.CheckpointUsage}
|
|
1499
|
+
* can incrementally update the same record — crash-safe by design. Returns `null` on failure;
|
|
1500
|
+
* the session still runs (usage checkpoints simply become no-ops).
|
|
1501
|
+
*
|
|
1502
|
+
* @param params The execution parameters.
|
|
1503
|
+
* @param config The agent configuration (provides the system prompt id, if any).
|
|
1504
|
+
* @param modelResolution The resolved model/vendor identifiers.
|
|
1505
|
+
* @returns The persisted prompt run, or `null` if it could not be created.
|
|
1506
|
+
*/
|
|
1507
|
+
async createRealtimePromptRun(params, config, modelResolution) {
|
|
1508
|
+
try {
|
|
1509
|
+
const md = params.provider || this._activeProvider;
|
|
1510
|
+
const promptRun = await md.GetEntityObject('MJ: AI Prompt Runs', params.contextUser);
|
|
1511
|
+
promptRun.NewRecord();
|
|
1512
|
+
if (config.systemPrompt) {
|
|
1513
|
+
promptRun.PromptID = config.systemPrompt.ID;
|
|
1514
|
+
}
|
|
1515
|
+
promptRun.ModelID = modelResolution.modelID;
|
|
1516
|
+
promptRun.VendorID = modelResolution.vendorID || null;
|
|
1517
|
+
promptRun.AgentID = params.agent.ID;
|
|
1518
|
+
promptRun.AgentRunID = this._agentRun?.ID ?? null;
|
|
1519
|
+
promptRun.Status = 'Running';
|
|
1520
|
+
promptRun.RunAt = new Date();
|
|
1521
|
+
promptRun.StreamingEnabled = true;
|
|
1522
|
+
promptRun.Cancelled = false;
|
|
1523
|
+
promptRun.CacheHit = false;
|
|
1524
|
+
if (!await promptRun.Save()) {
|
|
1525
|
+
this.logError(`Failed to create realtime AIPromptRun: ${promptRun.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1526
|
+
agent: params.agent,
|
|
1527
|
+
category: 'RealtimeSession'
|
|
1528
|
+
});
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
return promptRun;
|
|
1532
|
+
}
|
|
1533
|
+
catch (error) {
|
|
1534
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1535
|
+
this.logError(`Error creating realtime AIPromptRun: ${msg}`, { agent: params.agent, category: 'RealtimeSession' });
|
|
1536
|
+
return null;
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Builds the fully-populated {@link RealtimeSessionRunnerDeps} from this agent's collaborators.
|
|
1541
|
+
*
|
|
1542
|
+
* Each dependency is a thin closure over BaseAgent state so the runner stays decoupled from
|
|
1543
|
+
* metadata/DB. The closures cover: target delegation (via {@link ExecuteSubAgent}), non-target
|
|
1544
|
+
* tool execution, transcript persistence (as `ConversationDetail`), and usage checkpointing
|
|
1545
|
+
* (onto the long-lived prompt run).
|
|
1546
|
+
*
|
|
1547
|
+
* @param params The execution parameters.
|
|
1548
|
+
* @param config The agent configuration.
|
|
1549
|
+
* @param modelResolution The resolved realtime model + identifiers.
|
|
1550
|
+
* @param promptRun The long-lived prompt run for usage checkpoints (may be `null`).
|
|
1551
|
+
* @returns The assembled deps object.
|
|
1552
|
+
*/
|
|
1553
|
+
async buildRealtimeSessionDeps(params, config, modelResolution, promptRun) {
|
|
1554
|
+
const effectiveConfig = this.resolveRealtimeEffectiveConfig(params.agent);
|
|
1555
|
+
const sessionParams = await this.buildRealtimeSessionParams(params, config, modelResolution.apiName, effectiveConfig, modelResolution.driverClass);
|
|
1556
|
+
return {
|
|
1557
|
+
Model: modelResolution.model,
|
|
1558
|
+
SessionParams: sessionParams,
|
|
1559
|
+
DelegateToTarget: (request) => this.delegateRealtimeToTarget(params, config, request),
|
|
1560
|
+
ExecuteTool: (call) => this.executeRealtimeTool(params, call),
|
|
1561
|
+
PersistTranscript: (transcript) => this.persistRealtimeTranscript(params, transcript),
|
|
1562
|
+
CheckpointUsage: (usage) => this.checkpointRealtimeUsage(promptRun, usage),
|
|
1563
|
+
// DB-driven spoken-progress wording (shared lookup with the client-direct path);
|
|
1564
|
+
// null → the runner's documented built-in first-person fallback.
|
|
1565
|
+
NarrationInstructionsTemplate: ResolveNarrationInstructionsTemplate(),
|
|
1566
|
+
// Effective-config narration pacing (realtime.narration.paceMs); null → runner default.
|
|
1567
|
+
NarrationPaceMs: GetNarrationPaceMs(effectiveConfig),
|
|
1568
|
+
LogStatus: (message, verboseOnly) => this.logStatus(message, verboseOnly ?? false, params),
|
|
1569
|
+
LogError: (error) => this.logError(error, { agent: params.agent, category: 'RealtimeSession' })
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Assembles the {@link RealtimeSessionParams} for the session.
|
|
1574
|
+
*
|
|
1575
|
+
* The system prompt is framed as a companion "voice for the target agent". The base system
|
|
1576
|
+
* prompt text (when an agent-level system prompt exists) plus the same memory/context a loop
|
|
1577
|
+
* agent would assemble (via {@link AgentMemoryContextBuilder}) are concatenated. The
|
|
1578
|
+
* always-present `invoke-target-agent` tool is added by the runner itself, so it is NOT
|
|
1579
|
+
* populated here.
|
|
1580
|
+
*
|
|
1581
|
+
* @param params The execution parameters.
|
|
1582
|
+
* @param config The agent configuration.
|
|
1583
|
+
* @param modelApiName The vendor API name of the resolved realtime model.
|
|
1584
|
+
* @returns The session parameters.
|
|
1585
|
+
*/
|
|
1586
|
+
async buildRealtimeSessionParams(params, config, modelApiName, effectiveConfig, driverClass) {
|
|
1587
|
+
// Identity framing comes from the ONE shared producer so the agent speaks first-person AS the
|
|
1588
|
+
// TARGET (Sage / Marketing Agent / …), identical to every other realtime host — not as the co-agent.
|
|
1589
|
+
// See BuildRealtimeAgentFraming + plans/realtime/realtime-core-host-convergence.md.
|
|
1590
|
+
const targetAgent = this.resolveRealtimeTargetAgent(params);
|
|
1591
|
+
const framing = BuildRealtimeAgentFraming(targetAgent?.Name ?? 'the configured target agent');
|
|
1592
|
+
const basePrompt = config.systemPrompt?.TemplateText ? config.systemPrompt.TemplateText : '';
|
|
1593
|
+
// Effective-config voice persona (realtime.voice.default) → short "Voice & manner" section.
|
|
1594
|
+
const voiceManner = BuildVoiceMannerSection(effectiveConfig);
|
|
1595
|
+
const memoryContext = await this.assembleRealtimeContext(params);
|
|
1596
|
+
const systemPrompt = [framing, basePrompt, voiceManner, memoryContext]
|
|
1597
|
+
.filter(part => part && part.trim().length > 0)
|
|
1598
|
+
.join('\n\n');
|
|
1599
|
+
// Provider-matched voice settings (realtime.voice.providers.<provider>) flow into the
|
|
1600
|
+
// driver's open Config bag — the same pact every other config entry rides.
|
|
1601
|
+
const providerVoice = GetProviderVoiceSettings(effectiveConfig, driverClass ?? null);
|
|
1602
|
+
return {
|
|
1603
|
+
Model: modelApiName,
|
|
1604
|
+
SystemPrompt: systemPrompt,
|
|
1605
|
+
InitialContext: memoryContext || undefined,
|
|
1606
|
+
// JSONObjectLike -> JSONObject: safe — the settings object came from JSON.parse.
|
|
1607
|
+
Config: providerVoice ? providerVoice : undefined
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Assembles the same memory/context block a loop agent injects, reusing
|
|
1612
|
+
* {@link AgentMemoryContextBuilder} so there is no duplicated retrieval logic. The builder
|
|
1613
|
+
* unshifts a system message onto a throwaway array, which we pull back out as plain text to
|
|
1614
|
+
* feed the realtime model's session context.
|
|
1615
|
+
*
|
|
1616
|
+
* @param params The execution parameters.
|
|
1617
|
+
* @returns The concatenated context text (empty string when nothing was injected).
|
|
1618
|
+
*/
|
|
1619
|
+
async assembleRealtimeContext(params) {
|
|
1620
|
+
const lastUserMessage = params.conversationMessages.filter(m => m.role === 'user').pop();
|
|
1621
|
+
const inputText = typeof lastUserMessage?.content === 'string' ? lastUserMessage.content : '';
|
|
1622
|
+
const scratch = [];
|
|
1623
|
+
const builder = new AgentMemoryContextBuilder();
|
|
1624
|
+
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));
|
|
1625
|
+
return scratch
|
|
1626
|
+
.map(m => (typeof m.content === 'string' ? m.content : ''))
|
|
1627
|
+
.filter(c => c.length > 0)
|
|
1628
|
+
.join('\n\n');
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Delegates an `invoke-target-agent` tool call to the top-level target agent.
|
|
1632
|
+
*
|
|
1633
|
+
* Threads the runner-owned {@link DelegateToTargetRequest.AbortSignal} into the child run's
|
|
1634
|
+
* `cancellationToken` (so barge-in cancels the delegated work), and links the child run to this
|
|
1635
|
+
* run via `parentRun` (→ `ParentRunID`) while propagating `agentSessionID` so both runs group
|
|
1636
|
+
* under the same session.
|
|
1637
|
+
*
|
|
1638
|
+
* **Target source.** The target agent id comes from `params.data.targetAgentID` when present
|
|
1639
|
+
* (the Realtime Co-Agent receives its target as a runtime parameter), falling back to the agent's
|
|
1640
|
+
* own `DefaultModelID`-style config is NOT applicable here; absent a target the delegation
|
|
1641
|
+
* returns a failed {@link DelegatedResult} the model can narrate.
|
|
1642
|
+
*
|
|
1643
|
+
* @param params The (parent) execution parameters.
|
|
1644
|
+
* @param config The agent configuration (unused today; reserved for target-from-config wiring).
|
|
1645
|
+
* @param request The delegation request derived from the tool call.
|
|
1646
|
+
* @returns The delegated result for the model's tool_response.
|
|
1647
|
+
*/
|
|
1648
|
+
async delegateRealtimeToTarget(params, config, request) {
|
|
1649
|
+
const targetAgent = this.resolveRealtimeTargetAgent(params);
|
|
1650
|
+
if (!targetAgent) {
|
|
1651
|
+
return {
|
|
1652
|
+
CallID: request.CallID,
|
|
1653
|
+
Success: false,
|
|
1654
|
+
Output: 'No target agent is configured for this voice session, so the request could not be performed.'
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
try {
|
|
1658
|
+
const requestText = this.parseDelegateRequestText(request.Arguments);
|
|
1659
|
+
const runner = new AgentRunner(params.provider || this._activeProvider);
|
|
1660
|
+
const result = await runner.RunAgent({
|
|
1661
|
+
agent: targetAgent,
|
|
1662
|
+
conversationMessages: [{ role: 'user', content: requestText }],
|
|
1663
|
+
contextUser: params.contextUser,
|
|
1664
|
+
cancellationToken: request.AbortSignal,
|
|
1665
|
+
parentRun: this._agentRun ?? undefined,
|
|
1666
|
+
agentSessionID: params.agentSessionID,
|
|
1667
|
+
parentAgentHierarchy: this._agentHierarchy,
|
|
1668
|
+
parentDepth: this._depth,
|
|
1669
|
+
configurationId: params.configurationId,
|
|
1670
|
+
apiKeys: params.apiKeys,
|
|
1671
|
+
data: params.data,
|
|
1672
|
+
verbose: params.verbose,
|
|
1673
|
+
// Progress streams BOTH to the runner's narration consumer (request.OnProgress —
|
|
1674
|
+
// it paces SendContextNote/RequestSpokenUpdate over the live socket) AND to any
|
|
1675
|
+
// host-level onProgress the parent execution carries.
|
|
1676
|
+
onProgress: this.combineProgressCallbacks(request.OnProgress, params.onProgress)
|
|
1677
|
+
});
|
|
1678
|
+
return {
|
|
1679
|
+
CallID: request.CallID,
|
|
1680
|
+
Success: result.success,
|
|
1681
|
+
Output: result.success
|
|
1682
|
+
? (result.agentRun?.Message || 'The target agent completed the request.')
|
|
1683
|
+
: (result.agentRun?.ErrorMessage || 'The target agent failed to complete the request.')
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
catch (error) {
|
|
1687
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1688
|
+
return { CallID: request.CallID, Success: false, Output: `Delegation failed: ${msg}` };
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Combines the runner-supplied delegation progress callback with the host-level one so a
|
|
1693
|
+
* single `onProgress` fans out to both. Returns the lone callback when only one exists, and
|
|
1694
|
+
* `undefined` when neither does. A throw from one consumer never starves the other.
|
|
1695
|
+
*/
|
|
1696
|
+
combineProgressCallbacks(first, second) {
|
|
1697
|
+
if (!first) {
|
|
1698
|
+
return second;
|
|
1699
|
+
}
|
|
1700
|
+
if (!second) {
|
|
1701
|
+
return first;
|
|
1702
|
+
}
|
|
1703
|
+
return (progress) => {
|
|
1704
|
+
try {
|
|
1705
|
+
first(progress);
|
|
1706
|
+
}
|
|
1707
|
+
catch {
|
|
1708
|
+
/* one consumer failing must not starve the other */
|
|
1709
|
+
}
|
|
1710
|
+
second(progress);
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Resolves the top-level target agent for the voice session.
|
|
1715
|
+
*
|
|
1716
|
+
* The target is supplied as a runtime parameter on `params.data.targetAgentID` (the Voice
|
|
1717
|
+
* Co-Agent voices on behalf of a target chosen at session start). Returns `null` when no
|
|
1718
|
+
* resolvable target is configured.
|
|
1719
|
+
*
|
|
1720
|
+
* @param params The execution parameters.
|
|
1721
|
+
* @returns The target agent entity, or `null`.
|
|
1722
|
+
*/
|
|
1723
|
+
resolveRealtimeTargetAgent(params) {
|
|
1724
|
+
const targetID = params.data?.targetAgentID;
|
|
1725
|
+
if (!targetID) {
|
|
1726
|
+
return null;
|
|
1727
|
+
}
|
|
1728
|
+
return AIEngine.Instance.Agents.find(a => UUIDsEqual(a.ID, targetID)) ?? null;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Parses the natural-language request text out of an `invoke-target-agent` call's arguments.
|
|
1732
|
+
* Falls back to the raw argument string when it is not the expected `{ request: string }` JSON.
|
|
1733
|
+
*
|
|
1734
|
+
* @param argumentsJson The raw arguments string emitted by the model.
|
|
1735
|
+
* @returns The request text to hand to the target agent.
|
|
1736
|
+
*/
|
|
1737
|
+
parseDelegateRequestText(argumentsJson) {
|
|
1738
|
+
try {
|
|
1739
|
+
const parsed = JSON.parse(argumentsJson);
|
|
1740
|
+
if (typeof parsed.request === 'string') {
|
|
1741
|
+
return parsed.request;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
/* not JSON — fall through to raw */
|
|
1746
|
+
}
|
|
1747
|
+
return argumentsJson;
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Executes a non-target realtime tool call by routing it through the agent's existing action
|
|
1751
|
+
* execution under the session context user.
|
|
1752
|
+
*
|
|
1753
|
+
* Today this maps the realtime call onto the agent's configured actions by name; unknown tools
|
|
1754
|
+
* return a failed {@link ToolExecutionResult} the model can narrate. (The richer client/UI tool
|
|
1755
|
+
* routing is wired in a later phase; this keeps server actions usable now.)
|
|
1756
|
+
*
|
|
1757
|
+
* @param params The execution parameters.
|
|
1758
|
+
* @param call The non-target tool call.
|
|
1759
|
+
* @returns The tool execution result for the model's tool_response.
|
|
1760
|
+
*/
|
|
1761
|
+
async executeRealtimeTool(params, call) {
|
|
1762
|
+
const action = this.getEffectiveActionsForValidation(params.agent.ID).find(a => a.Name === call.ToolName);
|
|
1763
|
+
if (!action) {
|
|
1764
|
+
return {
|
|
1765
|
+
CallID: call.CallID,
|
|
1766
|
+
Success: false,
|
|
1767
|
+
Output: `Tool '${call.ToolName}' is not available to this agent.`
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
try {
|
|
1771
|
+
const agentAction = { name: action.Name, params: this.parseRealtimeToolParams(call.Arguments) };
|
|
1772
|
+
const result = await this.ExecuteSingleAction(params, agentAction, action, params.contextUser);
|
|
1773
|
+
return {
|
|
1774
|
+
CallID: call.CallID,
|
|
1775
|
+
Success: result.Success,
|
|
1776
|
+
Output: result.Message || (result.Success ? 'Tool completed.' : 'Tool failed.')
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1781
|
+
return { CallID: call.CallID, Success: false, Output: `Tool execution failed: ${msg}` };
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Parses a realtime tool call's JSON arguments into an action parameter map.
|
|
1786
|
+
*
|
|
1787
|
+
* @param argumentsJson The raw arguments string.
|
|
1788
|
+
* @returns A record of parameter name → value (empty when not parseable).
|
|
1789
|
+
*/
|
|
1790
|
+
parseRealtimeToolParams(argumentsJson) {
|
|
1791
|
+
try {
|
|
1792
|
+
const parsed = JSON.parse(argumentsJson);
|
|
1793
|
+
if (parsed && typeof parsed === 'object') {
|
|
1794
|
+
return parsed;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
catch {
|
|
1798
|
+
/* ignore — return empty params */
|
|
1799
|
+
}
|
|
1800
|
+
return {};
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Persists a single realtime transcript turn as a `ConversationDetail` stamped with the
|
|
1804
|
+
* session id. User turns are written as `Role='User'`, assistant turns as `Role='AI'`. Only
|
|
1805
|
+
* final transcripts are persisted (interim/partial updates are skipped to avoid churn).
|
|
1806
|
+
*
|
|
1807
|
+
* @param params The execution parameters (provides conversation id + context user).
|
|
1808
|
+
* @param transcript The transcript turn emitted by the model.
|
|
1809
|
+
*/
|
|
1810
|
+
async persistRealtimeTranscript(params, transcript) {
|
|
1811
|
+
if (!transcript.IsFinal || !transcript.Text?.trim()) {
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const conversationID = params.data?.conversationId;
|
|
1815
|
+
if (!conversationID) {
|
|
1816
|
+
return; // Without a conversation we have nowhere to durably attach the turn.
|
|
1817
|
+
}
|
|
1818
|
+
const md = params.provider || this._activeProvider;
|
|
1819
|
+
const detail = await md.GetEntityObject('MJ: Conversation Details', params.contextUser);
|
|
1820
|
+
detail.NewRecord();
|
|
1821
|
+
detail.ConversationID = conversationID;
|
|
1822
|
+
detail.Role = transcript.Role === 'user' ? 'User' : 'AI';
|
|
1823
|
+
detail.Message = transcript.Text;
|
|
1824
|
+
if (params.agentSessionID) {
|
|
1825
|
+
detail.AgentSessionID = params.agentSessionID;
|
|
1826
|
+
}
|
|
1827
|
+
if (!await detail.Save()) {
|
|
1828
|
+
this.logError(`Failed to persist realtime transcript turn: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1829
|
+
agent: params.agent,
|
|
1830
|
+
category: 'RealtimeSession'
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Checkpoints accumulated realtime usage onto the single long-lived prompt run. This is the
|
|
1836
|
+
* incremental, crash-safe write the runner invokes on a debounced cadence and at close.
|
|
1837
|
+
*
|
|
1838
|
+
* @param promptRun The long-lived prompt run (no-op when `null`).
|
|
1839
|
+
* @param usage The cumulative usage snapshot to persist.
|
|
1840
|
+
*/
|
|
1841
|
+
async checkpointRealtimeUsage(promptRun, usage) {
|
|
1842
|
+
if (!promptRun) {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
promptRun.TokensPrompt = usage.InputTokens;
|
|
1846
|
+
promptRun.TokensCompletion = usage.OutputTokens;
|
|
1847
|
+
promptRun.TokensUsed = usage.InputTokens + usage.OutputTokens;
|
|
1848
|
+
if (!await promptRun.Save()) {
|
|
1849
|
+
this.logError(`Failed to checkpoint realtime usage: ${promptRun.LatestResult?.CompleteMessage ?? 'unknown error'}`, {
|
|
1850
|
+
category: 'RealtimeSession'
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Maps a completed {@link RealtimeSessionResult} onto the finalized `AIAgentRun` and returns
|
|
1856
|
+
* the {@link ExecuteAgentResult}. A clean close finalizes as success; a session error finalizes
|
|
1857
|
+
* as failure with the error message.
|
|
1858
|
+
*
|
|
1859
|
+
* @template R The caller's payload type (unused on the realtime path).
|
|
1860
|
+
* @param params The execution parameters.
|
|
1861
|
+
* @param sessionResult The result returned by {@link RealtimeSessionRunner.Run}.
|
|
1862
|
+
* @returns The finalized agent result.
|
|
1863
|
+
*/
|
|
1864
|
+
async finalizeRealtimeRun(params, sessionResult) {
|
|
1865
|
+
if (sessionResult.Success) {
|
|
1866
|
+
this.logStatus(`🎙️ Realtime session for '${params.agent.Name}' completed: ${sessionResult.TranscriptTurnCount} turn(s), ` +
|
|
1867
|
+
`${sessionResult.FinalUsage.InputTokens + sessionResult.FinalUsage.OutputTokens} token(s).`, true, params);
|
|
1868
|
+
const successStep = this.createSessionSuccessStep();
|
|
1869
|
+
return await this.finalizeAgentRun(successStep, undefined, params.contextUser);
|
|
1870
|
+
}
|
|
1871
|
+
const message = sessionResult.ErrorMessage || 'Realtime session ended with an error.';
|
|
1872
|
+
return await this.createFailureResult(message, params.contextUser);
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Builds a terminal `Success` step describing the completion of a realtime session, used to
|
|
1876
|
+
* finalize the run through the shared {@link finalizeAgentRun} path.
|
|
1877
|
+
*
|
|
1878
|
+
* @template R The caller's payload type.
|
|
1879
|
+
* @returns A terminal success step.
|
|
1880
|
+
*/
|
|
1881
|
+
createSessionSuccessStep() {
|
|
1882
|
+
return {
|
|
1883
|
+
step: 'Success',
|
|
1884
|
+
terminate: true,
|
|
1885
|
+
message: 'Realtime session completed.'
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1204
1888
|
/**
|
|
1205
1889
|
* Sub-classes can override this method to perform any specialized initialization
|
|
1206
1890
|
* @param params
|
|
@@ -1338,8 +2022,13 @@ export class BaseAgent {
|
|
|
1338
2022
|
* @protected
|
|
1339
2023
|
*/
|
|
1340
2024
|
async initializeEngines(contextUser) {
|
|
1341
|
-
|
|
2025
|
+
// Load the Action engine BEFORE the AI engine. AIEngine.RefreshActions()
|
|
2026
|
+
// (invoked by AIEngine.Config) reuses already-cached 'MJ: Actions'
|
|
2027
|
+
// metadata via BaseEngineRegistry, so priming ActionEngineServer first
|
|
2028
|
+
// lets AIEngine skip loading a second copy into ActionEngineBase —
|
|
2029
|
+
// eliminating the duplicate-RunView telemetry warning at agent startup.
|
|
1342
2030
|
await ActionEngineServer.Instance.Config(false, contextUser);
|
|
2031
|
+
await AIEngine.Instance.Config(false, contextUser);
|
|
1343
2032
|
}
|
|
1344
2033
|
/**
|
|
1345
2034
|
* Determine the scope label for a note based on its scope fields.
|
|
@@ -1393,72 +2082,16 @@ export class BaseAgent {
|
|
|
1393
2082
|
* @returns Object containing injected notes and examples
|
|
1394
2083
|
*/
|
|
1395
2084
|
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 };
|
|
2085
|
+
// Delegate the orchestration to the shared, reusable builder so both BaseAgent and the
|
|
2086
|
+
// Realtime agent type inject memory identically. The observability context and verbose
|
|
2087
|
+
// status logging are derived from this instance and passed through.
|
|
2088
|
+
const observability = this._agentRun
|
|
2089
|
+
? { agentRunID: this._agentRun.ID, stepNumber: (this._agentRun.Steps?.length || 0) + 1 }
|
|
2090
|
+
: undefined;
|
|
2091
|
+
const result = await new AgentMemoryContextBuilder().InjectContextMemory(input, agent, userId, companyId, contextUser, conversationMessages, primaryScopeEntityId, primaryScopeRecordId, secondaryScopes, secondaryScopeConfig, observability, (message, verboseOnly) => this.logStatus(message, verboseOnly));
|
|
2092
|
+
// Store for inclusion in result (externally observable behavior preserved)
|
|
2093
|
+
this._injectedMemory = result;
|
|
2094
|
+
return result;
|
|
1462
2095
|
}
|
|
1463
2096
|
/**
|
|
1464
2097
|
* Inject pre-execution RAG context for this agent using scoped search.
|
|
@@ -1486,40 +2119,12 @@ export class BaseAgent {
|
|
|
1486
2119
|
* @returns The structured RAG result, or `null` if no scopes produced results.
|
|
1487
2120
|
*/
|
|
1488
2121
|
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
|
-
}
|
|
2122
|
+
// Delegate to the shared builder so the Realtime agent type injects pre-execution RAG
|
|
2123
|
+
// identically. Verbose status + non-fatal error logging are threaded through from this instance.
|
|
2124
|
+
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));
|
|
2125
|
+
// Store for inclusion in result (externally observable behavior preserved)
|
|
2126
|
+
this._injectedRAG = result;
|
|
2127
|
+
return result;
|
|
1523
2128
|
}
|
|
1524
2129
|
/**
|
|
1525
2130
|
* Converts UI markup (@{...} syntax) in user messages to plain text.
|
|
@@ -1793,7 +2398,7 @@ export class BaseAgent {
|
|
|
1793
2398
|
const systemPrompt = config.systemPrompt;
|
|
1794
2399
|
const childPrompt = config.childPrompt;
|
|
1795
2400
|
// Gather context data (including runtime action changes)
|
|
1796
|
-
const promptTemplateData = await this.gatherPromptTemplateData(params.agent, params.contextUser, params.data, params.actionChanges);
|
|
2401
|
+
const promptTemplateData = await this.gatherPromptTemplateData(params.agent, params.contextUser, params.data, params.actionChanges, params.subAgentChanges);
|
|
1797
2402
|
// Set up the hierarchical prompt execution
|
|
1798
2403
|
const promptParams = new AIPromptParams();
|
|
1799
2404
|
// Handle case where systemPrompt is optional (e.g., Flow Agent Type)
|
|
@@ -1868,6 +2473,13 @@ export class BaseAgent {
|
|
|
1868
2473
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
1869
2474
|
this.logStatus(`[ArtifactTools] Artifacts present but tools disabled by agent config (includeArtifactToolsDocs=false)`, true, params);
|
|
1870
2475
|
}
|
|
2476
|
+
// Enable the memory-writes response field + docs only for agents that opted in
|
|
2477
|
+
// via AllowMemoryWrite. Disabled agents never see the docs, so a well-behaved
|
|
2478
|
+
// LLM never emits the field (the turn loop still guards against drift).
|
|
2479
|
+
const memoryWritesDocsEnabled = agentTypePromptParams?.includeMemoryWritesDocs !== false;
|
|
2480
|
+
if (memoryWritesDocsEnabled && params.agent.AllowMemoryWrite === true) {
|
|
2481
|
+
promptParams.data['_MEMORY_WRITES_ENABLED'] = true;
|
|
2482
|
+
}
|
|
1871
2483
|
// Inject pipeline tool docs when pipelines are enabled and at least one source exists.
|
|
1872
2484
|
// A pipeline's first step must be a source (Action or artifact tool); with none
|
|
1873
2485
|
// available pipelines are impossible, so BuildPipelineToolDocs returns '' and the
|
|
@@ -3433,6 +4045,129 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3433
4045
|
`Instead: page it with get_rows(start, count), or run a pipeline that filters/aggregates it ` +
|
|
3434
4046
|
`server-side (where / select / groupBy → only the small final result returns to you).]`);
|
|
3435
4047
|
}
|
|
4048
|
+
/**
|
|
4049
|
+
* Executes a batch of in-flight memory writes, recording each as its own
|
|
4050
|
+
* `Tool` AIAgentRunStep (a sibling of the Prompt step that requested them)
|
|
4051
|
+
* with full inputs/outcomes captured in InputData/OutputData.
|
|
4052
|
+
*
|
|
4053
|
+
* Writes run SEQUENTIALLY (not Promise.all like artifact tools) by design:
|
|
4054
|
+
* each persisted note is embedded and synced into the in-memory vector
|
|
4055
|
+
* service on Save, so write N must be visible to write N+1's near-duplicate
|
|
4056
|
+
* check (this is also what makes same-run supersede-own work). The per-run
|
|
4057
|
+
* cap bounds the cost of the serialization.
|
|
4058
|
+
*
|
|
4059
|
+
* Step naming convention: `Memory Write` for log/UI clarity.
|
|
4060
|
+
*
|
|
4061
|
+
* @protected
|
|
4062
|
+
*/
|
|
4063
|
+
async executeMemoryWritesAsSteps(writes, params) {
|
|
4064
|
+
const results = [];
|
|
4065
|
+
for (const write of writes) {
|
|
4066
|
+
const writeStep = await this.createStepEntity({
|
|
4067
|
+
stepType: 'Tool',
|
|
4068
|
+
stepName: 'Memory Write',
|
|
4069
|
+
contextUser: params.contextUser,
|
|
4070
|
+
inputData: {
|
|
4071
|
+
note: write.note,
|
|
4072
|
+
type: write.type,
|
|
4073
|
+
scopeHint: write.scopeHint,
|
|
4074
|
+
},
|
|
4075
|
+
});
|
|
4076
|
+
const result = await this._memoryWriteManager.ExecuteWrite(write, {
|
|
4077
|
+
agentId: params.agent.ID,
|
|
4078
|
+
contextUser: params.contextUser,
|
|
4079
|
+
agentRunId: this._agentRun?.ID,
|
|
4080
|
+
conversationId: this._agentRun?.ConversationID || undefined,
|
|
4081
|
+
conversationDetailId: params.conversationDetailId,
|
|
4082
|
+
userId: params.userId || params.contextUser?.ID,
|
|
4083
|
+
companyId: params.companyId,
|
|
4084
|
+
verbose: params.verbose,
|
|
4085
|
+
provider: this.ProviderToUse,
|
|
4086
|
+
});
|
|
4087
|
+
const failed = result.disposition === 'error' || result.disposition === 'rejected-type';
|
|
4088
|
+
await this.finalizeStepEntity(writeStep, !failed, failed ? result.reason : undefined, {
|
|
4089
|
+
disposition: result.disposition,
|
|
4090
|
+
noteId: result.noteId,
|
|
4091
|
+
finalScope: result.finalScope,
|
|
4092
|
+
reason: result.reason,
|
|
4093
|
+
durationMs: result.durationMs,
|
|
4094
|
+
});
|
|
4095
|
+
results.push(result);
|
|
4096
|
+
}
|
|
4097
|
+
return results;
|
|
4098
|
+
}
|
|
4099
|
+
/**
|
|
4100
|
+
* Turn-loop entry point for in-flight memory writes, gated on the agent's
|
|
4101
|
+
* AllowMemoryWrite flag. When disabled but the LLM emitted writes anyway
|
|
4102
|
+
* (prompt drift / injection attempt), records ONE summary skip step —
|
|
4103
|
+
* observable without per-write noise — and tells the agent the memories
|
|
4104
|
+
* were NOT saved so it stops re-emitting. When enabled, executes the
|
|
4105
|
+
* writes as run steps and injects the results message.
|
|
4106
|
+
*
|
|
4107
|
+
* @protected
|
|
4108
|
+
*/
|
|
4109
|
+
async processMemoryWritesForTurn(memoryWrites, params) {
|
|
4110
|
+
if (params.agent.AllowMemoryWrite !== true) {
|
|
4111
|
+
this.logStatus(`[MemoryWrites] LLM emitted ${memoryWrites.length} memory write(s) but AllowMemoryWrite=false — skipping`, true, params);
|
|
4112
|
+
const skipStep = await this.createStepEntity({
|
|
4113
|
+
stepType: 'Tool',
|
|
4114
|
+
stepName: 'Memory Writes: skipped (AllowMemoryWrite=false)',
|
|
4115
|
+
contextUser: params.contextUser,
|
|
4116
|
+
inputData: { requestedWriteCount: memoryWrites.length },
|
|
4117
|
+
});
|
|
4118
|
+
await this.finalizeStepEntity(skipStep, true, undefined, { skipped: true, reason: 'AllowMemoryWrite=false' });
|
|
4119
|
+
params.conversationMessages.push({
|
|
4120
|
+
role: 'user',
|
|
4121
|
+
content: 'Memory write result: this agent does not have durable memory writes enabled — the requested memories were NOT saved. Do not emit memoryWrites again.',
|
|
4122
|
+
metadata: {
|
|
4123
|
+
turnAdded: this._promptTurnCount,
|
|
4124
|
+
messageType: 'tool-result',
|
|
4125
|
+
expirationTurns: 3,
|
|
4126
|
+
expirationMode: 'Compact',
|
|
4127
|
+
compactMode: 'First N Chars',
|
|
4128
|
+
compactLength: 200,
|
|
4129
|
+
compactPromptId: '',
|
|
4130
|
+
},
|
|
4131
|
+
});
|
|
4132
|
+
return;
|
|
4133
|
+
}
|
|
4134
|
+
this.logStatus(`[MemoryWrites] LLM requested ${memoryWrites.length} memory write(s)`, true, params);
|
|
4135
|
+
const writeResults = await this.executeMemoryWritesAsSteps(memoryWrites, params);
|
|
4136
|
+
this.injectMemoryWriteResultsMessage(params, writeResults);
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Pushes a single user-role message containing memory-write outcomes into
|
|
4140
|
+
* the conversation, mirroring `injectArtifactToolResultsMessage`'s
|
|
4141
|
+
* inject-once-then-expire pattern. Closing the loop here is what stops the
|
|
4142
|
+
* LLM from re-emitting the same memory on subsequent turns.
|
|
4143
|
+
*
|
|
4144
|
+
* @protected
|
|
4145
|
+
*/
|
|
4146
|
+
injectMemoryWriteResultsMessage(params, results) {
|
|
4147
|
+
if (results.length === 0)
|
|
4148
|
+
return;
|
|
4149
|
+
const header = results.length === 1
|
|
4150
|
+
? 'Memory write result:'
|
|
4151
|
+
: `Memory write results (${results.length} writes):`;
|
|
4152
|
+
const body = results.map((r, i) => {
|
|
4153
|
+
const note = r.request.note.length > 120 ? `${r.request.note.slice(0, 120)}…` : r.request.note;
|
|
4154
|
+
return `${i + 1}. "${note}" — **${r.disposition}**${r.reason ? `: ${r.reason}` : ''}`;
|
|
4155
|
+
}).join('\n');
|
|
4156
|
+
const message = {
|
|
4157
|
+
role: 'user',
|
|
4158
|
+
content: `${header}\n${body}`,
|
|
4159
|
+
metadata: {
|
|
4160
|
+
turnAdded: this._promptTurnCount,
|
|
4161
|
+
messageType: 'tool-result',
|
|
4162
|
+
expirationTurns: 3,
|
|
4163
|
+
expirationMode: 'Compact',
|
|
4164
|
+
compactMode: 'First N Chars',
|
|
4165
|
+
compactLength: 300,
|
|
4166
|
+
compactPromptId: '',
|
|
4167
|
+
},
|
|
4168
|
+
};
|
|
4169
|
+
params.conversationMessages.push(message);
|
|
4170
|
+
}
|
|
3436
4171
|
/**
|
|
3437
4172
|
* Builds a per-run {@link PipelineToolRegistry} that unifies the three pipeline-able
|
|
3438
4173
|
* substrates behind one namespace: built-in transforms, the agent's effective Actions, and
|
|
@@ -3583,37 +4318,70 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3583
4318
|
*
|
|
3584
4319
|
* @private
|
|
3585
4320
|
*/
|
|
3586
|
-
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges) {
|
|
4321
|
+
async gatherPromptTemplateData(agent, _contextUser, extraData, actionChanges, subAgentChanges) {
|
|
3587
4322
|
try {
|
|
3588
4323
|
const engine = AIEngine.Instance;
|
|
3589
|
-
//
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
const
|
|
3599
|
-
//
|
|
3600
|
-
|
|
3601
|
-
let
|
|
3602
|
-
|
|
4324
|
+
// Build (or reuse) the agent-invariant base catalog. This is process-wide cached on
|
|
4325
|
+
// AIEngine and wiped on Agent/AgentAction/AgentRelationship/AgentType changes + reloads.
|
|
4326
|
+
// It turns the per-step rebuild (sub-agent + action resolution, markdown, JSON.parse of
|
|
4327
|
+
// agent-type params) into a once-per-agent cost; the common no-override step reuses it wholesale.
|
|
4328
|
+
let catalog = engine.GetAgentBaseCatalog(agent.ID);
|
|
4329
|
+
if (!catalog) {
|
|
4330
|
+
catalog = this.buildAgentBaseCatalog(agent, engine);
|
|
4331
|
+
engine.SetAgentBaseCatalog(agent.ID, catalog);
|
|
4332
|
+
}
|
|
4333
|
+
const isRoot = this._depth === 0;
|
|
4334
|
+
// Sub-agents: reuse cached base unless runtime subAgentChanges apply (then clone + re-format).
|
|
4335
|
+
let uniqueActiveSubAgents = catalog.uniqueActiveSubAgents;
|
|
4336
|
+
let subAgentDetails = catalog.subAgentDetails;
|
|
4337
|
+
let subAgentCount = catalog.subAgentCount;
|
|
4338
|
+
if (subAgentChanges?.length) {
|
|
4339
|
+
uniqueActiveSubAgents = this.applySubAgentChanges(catalog.uniqueActiveSubAgents, subAgentChanges, agent.ID, isRoot, engine);
|
|
4340
|
+
subAgentCount = uniqueActiveSubAgents.length;
|
|
4341
|
+
subAgentDetails = this.formatSubAgentDetails(uniqueActiveSubAgents);
|
|
4342
|
+
}
|
|
4343
|
+
// Actions: reuse cached active set unless runtime actionChanges apply (then clone + re-format).
|
|
4344
|
+
//
|
|
4345
|
+
// FAST-PATH SHARING CONTRACT: on the no-override path, `activeActions` (and therefore
|
|
4346
|
+
// `_effectiveActions`) and `uniqueActiveSubAgents` above are the SAME array references
|
|
4347
|
+
// held by the process-wide AIEngine catalog cache. Downstream consumers MUST treat them
|
|
4348
|
+
// as read-only — they are only ever read (`.find`/`.map`/`.length`/`.some`), never mutated
|
|
4349
|
+
// in place. On the override path a fresh array is built via filter/applyActionChanges, so
|
|
4350
|
+
// the cached arrays are never the mutated ones. Keeping the references (vs. copying) avoids
|
|
4351
|
+
// a per-step allocation; if a future consumer needs to mutate, it must `.slice()` first.
|
|
4352
|
+
let activeActions = catalog.activeActions;
|
|
4353
|
+
let actionDetails = catalog.actionDetails;
|
|
3603
4354
|
if (actionChanges?.length) {
|
|
3604
|
-
const
|
|
3605
|
-
|
|
3606
|
-
actions = result.actions;
|
|
4355
|
+
const result = this.applyActionChanges([...catalog.baseActionsRaw], actionChanges, agent.ID, isRoot);
|
|
4356
|
+
activeActions = result.actions.filter(a => a.Status === 'Active');
|
|
3607
4357
|
this._dynamicActionLimits = result.dynamicLimits;
|
|
4358
|
+
actionDetails = this.formatActionDetails(activeActions);
|
|
3608
4359
|
}
|
|
3609
|
-
|
|
3610
|
-
|
|
4360
|
+
else {
|
|
4361
|
+
// No actionChanges this step → no dynamically-added actions, hence no dynamic limits.
|
|
4362
|
+
// gatherPromptTemplateData runs once per step, and _dynamicActionLimits is keyed to the
|
|
4363
|
+
// actionChanges of the CURRENT step (read at validation time in checkActionExecutionLimits).
|
|
4364
|
+
// Resetting to {} is correct and required: it prevents a prior step's actionChanges limits
|
|
4365
|
+
// from leaking into a step that has none. It is NOT relied upon to persist across steps.
|
|
4366
|
+
this._dynamicActionLimits = {};
|
|
4367
|
+
}
|
|
4368
|
+
// Store for later validation in executeActionsStep
|
|
3611
4369
|
this._effectiveActions = activeActions;
|
|
3612
|
-
//
|
|
3613
|
-
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4370
|
+
// Agent type prompt params: reuse cached base merge unless a runtime override is present.
|
|
3614
4371
|
const runtimePromptParamOverrides = extraData?.__agentTypePromptParams;
|
|
3615
|
-
|
|
3616
|
-
|
|
4372
|
+
let agentTypePromptParams;
|
|
4373
|
+
if (runtimePromptParamOverrides) {
|
|
4374
|
+
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4375
|
+
agentTypePromptParams = this.buildAgentTypePromptParams(agentType, agent, runtimePromptParamOverrides);
|
|
4376
|
+
}
|
|
4377
|
+
else {
|
|
4378
|
+
// Fast path: shallow-clone the cached base params before handing them out. The cached
|
|
4379
|
+
// object lives in the process-wide AIEngine catalog and is shared across every run of
|
|
4380
|
+
// this agent; the audit shows it is read-only downstream today, but the clone is cheap
|
|
4381
|
+
// and removes any cache-poisoning foot-gun should a future consumer write to it.
|
|
4382
|
+
agentTypePromptParams = { ...catalog.baseAgentTypePromptParams };
|
|
4383
|
+
}
|
|
4384
|
+
// Build client tool details for the prompt (per-run; depends on extraData)
|
|
3617
4385
|
const clientToolDetails = this.buildClientToolPromptSection(agent, extraData);
|
|
3618
4386
|
// Build app context section if provided in extraData
|
|
3619
4387
|
const appContext = this.buildAppContextSection(extraData);
|
|
@@ -3621,10 +4389,10 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3621
4389
|
agentName: agent.Name,
|
|
3622
4390
|
agentDescription: agent.Description,
|
|
3623
4391
|
parentAgentName: agent.Parent ? agent.Parent.trim() : "",
|
|
3624
|
-
subAgentCount:
|
|
3625
|
-
subAgentDetails:
|
|
4392
|
+
subAgentCount: subAgentCount,
|
|
4393
|
+
subAgentDetails: subAgentDetails,
|
|
3626
4394
|
actionCount: activeActions.length,
|
|
3627
|
-
actionDetails:
|
|
4395
|
+
actionDetails: actionDetails,
|
|
3628
4396
|
clientToolDetails: clientToolDetails,
|
|
3629
4397
|
appContext: appContext,
|
|
3630
4398
|
};
|
|
@@ -3656,6 +4424,98 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3656
4424
|
throw new Error(`Error gathering context data: ${error.message}`);
|
|
3657
4425
|
}
|
|
3658
4426
|
}
|
|
4427
|
+
/**
|
|
4428
|
+
* Builds the agent-invariant {@link AgentBaseCatalog} — the resolved sub-agents + actions and
|
|
4429
|
+
* their formatted markdown, plus the base agent-type prompt params. Computed once per agent and
|
|
4430
|
+
* cached on AIEngine (see gatherPromptTemplateData); does NOT apply any runtime overrides.
|
|
4431
|
+
*
|
|
4432
|
+
* @protected
|
|
4433
|
+
*/
|
|
4434
|
+
buildAgentBaseCatalog(agent, engine) {
|
|
4435
|
+
// Resolve sub-agents: direct ParentID children + active relationships, de-duped, ordered.
|
|
4436
|
+
const activeSubAgents = engine.Agents.filter(a => UUIDsEqual(a.ParentID, agent.ID) && a.Status === 'Active')
|
|
4437
|
+
.sort((a, b) => a.ExecutionOrder - b.ExecutionOrder);
|
|
4438
|
+
const activeAgentRelationships = engine.AgentRelationships.filter(ar => UUIDsEqual(ar.AgentID, agent.ID) && ar.Status === 'Active');
|
|
4439
|
+
const uniqueActiveSubAgentIDs = new Set();
|
|
4440
|
+
activeSubAgents.forEach(a => uniqueActiveSubAgentIDs.add(a.ID));
|
|
4441
|
+
activeAgentRelationships.forEach(ar => uniqueActiveSubAgentIDs.add(ar.SubAgentID));
|
|
4442
|
+
const uniqueActiveSubAgents = Array.from(uniqueActiveSubAgentIDs).map(id => engine.Agents.find(a => UUIDsEqual(a.ID, id)));
|
|
4443
|
+
// Resolve actions from the agent's active AIAgentAction junctions.
|
|
4444
|
+
const agentActions = engine.AgentActions.filter(aa => UUIDsEqual(aa.AgentID, agent.ID) && aa.Status === 'Active');
|
|
4445
|
+
const baseActionsRaw = ActionEngineServer.Instance.Actions.filter(a => agentActions.some(aa => UUIDsEqual(aa.ActionID, a.ID)));
|
|
4446
|
+
const activeActions = baseActionsRaw.filter(a => a.Status === 'Active');
|
|
4447
|
+
// Base agent-type prompt params (schema defaults + agent config; NO runtime overrides).
|
|
4448
|
+
const agentType = engine.AgentTypes.find(at => UUIDsEqual(at.ID, agent.TypeID));
|
|
4449
|
+
const baseAgentTypePromptParams = this.buildAgentTypePromptParams(agentType, agent, undefined);
|
|
4450
|
+
return {
|
|
4451
|
+
uniqueActiveSubAgents,
|
|
4452
|
+
subAgentCount: uniqueActiveSubAgents.length,
|
|
4453
|
+
subAgentDetails: this.formatSubAgentDetails(uniqueActiveSubAgents),
|
|
4454
|
+
baseActionsRaw,
|
|
4455
|
+
activeActions,
|
|
4456
|
+
actionDetails: this.formatActionDetails(activeActions),
|
|
4457
|
+
baseAgentTypePromptParams,
|
|
4458
|
+
};
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* Applies runtime {@link SubAgentChange}s to a base sub-agent set — the sub-agent counterpart of
|
|
4462
|
+
* {@link applyActionChanges}. Returns a NEW array (never mutates the cached base set).
|
|
4463
|
+
*
|
|
4464
|
+
* @protected
|
|
4465
|
+
*/
|
|
4466
|
+
applySubAgentChanges(baseSubAgents, subAgentChanges, agentId, isRoot, engine) {
|
|
4467
|
+
let subAgents = [...baseSubAgents];
|
|
4468
|
+
for (const change of subAgentChanges) {
|
|
4469
|
+
if (!this.doesChangeScopeApply(change.scope, agentId, isRoot, change.agentIds)) {
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
4472
|
+
if (change.mode === 'add') {
|
|
4473
|
+
for (const subAgentId of change.subAgentIds) {
|
|
4474
|
+
if (!subAgents.some(a => UUIDsEqual(a.ID, subAgentId))) {
|
|
4475
|
+
const toAdd = engine.Agents.find(a => UUIDsEqual(a.ID, subAgentId));
|
|
4476
|
+
if (toAdd) {
|
|
4477
|
+
subAgents.push(toAdd);
|
|
4478
|
+
}
|
|
4479
|
+
else {
|
|
4480
|
+
LogStatus(`Sub-agent with ID '${subAgentId}' not found in AIEngine - skipping add`);
|
|
4481
|
+
}
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
else if (change.mode === 'remove') {
|
|
4486
|
+
subAgents = subAgents.filter(a => !change.subAgentIds.some(id => UUIDsEqual(id, a.ID)));
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
return subAgents;
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Filters/transforms sub-agent changes for propagation to a sub-agent — the sub-agent counterpart
|
|
4493
|
+
* of {@link filterActionChangesForSubAgent} (same propagation rules).
|
|
4494
|
+
*
|
|
4495
|
+
* @protected
|
|
4496
|
+
*/
|
|
4497
|
+
filterSubAgentChangesForSubAgent(subAgentChanges) {
|
|
4498
|
+
if (!subAgentChanges?.length) {
|
|
4499
|
+
return undefined;
|
|
4500
|
+
}
|
|
4501
|
+
const filtered = [];
|
|
4502
|
+
for (const change of subAgentChanges) {
|
|
4503
|
+
switch (change.scope) {
|
|
4504
|
+
case 'root':
|
|
4505
|
+
continue; // only applies to root — don't propagate
|
|
4506
|
+
case 'global':
|
|
4507
|
+
filtered.push(change);
|
|
4508
|
+
break;
|
|
4509
|
+
case 'all-subagents':
|
|
4510
|
+
filtered.push({ ...change, scope: 'global' });
|
|
4511
|
+
break;
|
|
4512
|
+
case 'specific':
|
|
4513
|
+
filtered.push(change);
|
|
4514
|
+
break;
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
return filtered.length > 0 ? filtered : undefined;
|
|
4518
|
+
}
|
|
3659
4519
|
/**
|
|
3660
4520
|
* Builds merged agent type prompt params from schema defaults,
|
|
3661
4521
|
* agent config, and runtime overrides.
|
|
@@ -3743,7 +4603,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3743
4603
|
{ docsFlag: 'includeWhileDocs', responseTypeKey: 'while' },
|
|
3744
4604
|
{ docsFlag: 'includeScratchpadDocs', responseTypeKey: 'scratchpad' },
|
|
3745
4605
|
{ docsFlag: 'includeArtifactToolsDocs', responseTypeKey: 'artifactToolCalls' },
|
|
3746
|
-
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' }
|
|
4606
|
+
{ docsFlag: 'includePipelineDocs', responseTypeKey: 'pipeline' },
|
|
4607
|
+
{ docsFlag: 'includeMemoryWritesDocs', responseTypeKey: 'memoryWrites' }
|
|
3747
4608
|
];
|
|
3748
4609
|
for (const { docsFlag, responseTypeKey } of alignmentMappings) {
|
|
3749
4610
|
// Check if the user explicitly set this response type property
|
|
@@ -3985,8 +4846,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
3985
4846
|
this.logStatus(`🎯 Propagating effort level ${params.effortLevel} to sub-agent '${subAgentRequest.name}'`, true, params);
|
|
3986
4847
|
}
|
|
3987
4848
|
const parentStepCountsToPass = [...this._parentStepCounts, stepCount + 1];
|
|
3988
|
-
// Filter action changes for sub-agent propagation
|
|
4849
|
+
// Filter action / sub-agent changes for sub-agent propagation
|
|
3989
4850
|
const subAgentActionChanges = this.filterActionChangesForSubAgent(params.actionChanges);
|
|
4851
|
+
const subAgentSubAgentChanges = this.filterSubAgentChangesForSubAgent(params.subAgentChanges);
|
|
3990
4852
|
// Execute the sub-agent with cancellation and streaming support
|
|
3991
4853
|
// Use subAgentRequest.context if provided, otherwise fall back to params.context
|
|
3992
4854
|
// This allows Flow agents and Loop agents to propagate context through sub-agent requests
|
|
@@ -4014,12 +4876,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4014
4876
|
context: subAgentContext, // use subAgentRequest.context if provided, otherwise params.context
|
|
4015
4877
|
verbose: params.verbose, // pass verbose flag to sub-agent
|
|
4016
4878
|
actionChanges: subAgentActionChanges, // propagate filtered action changes to sub-agent
|
|
4879
|
+
subAgentChanges: subAgentSubAgentChanges, // propagate filtered sub-agent changes to sub-agent
|
|
4017
4880
|
PrimaryScopeEntityName: params.PrimaryScopeEntityName, // propagate scope to sub-agent
|
|
4018
4881
|
PrimaryScopeRecordID: params.PrimaryScopeRecordID,
|
|
4019
4882
|
SecondaryScopes: params.SecondaryScopes,
|
|
4020
4883
|
onAgentRunCreated: async (agentRunId) => {
|
|
4021
4884
|
stepEntity.TargetLogID = agentRunId;
|
|
4022
|
-
this
|
|
4885
|
+
// Re-apply post-INSERT: this callback can fire while the step's INSERT is still in flight,
|
|
4886
|
+
// and the INSERT's reload would otherwise revert TargetLogID back to null.
|
|
4887
|
+
this.queueStepSave(stepEntity, (s) => { s.TargetLogID = agentRunId; });
|
|
4023
4888
|
}
|
|
4024
4889
|
});
|
|
4025
4890
|
// Check if execution was successful
|
|
@@ -4604,6 +5469,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4604
5469
|
if (params.data?.conversationId) {
|
|
4605
5470
|
this._agentRun.ConversationID = params.data.conversationId;
|
|
4606
5471
|
}
|
|
5472
|
+
// Stamp the realtime/long-lived session id (if any) so every run — including delegated
|
|
5473
|
+
// child runs that inherit this value — is groupable under the same MJ: AI Agent Session.
|
|
5474
|
+
if (params.agentSessionID) {
|
|
5475
|
+
this._agentRun.AgentSessionID = params.agentSessionID;
|
|
5476
|
+
}
|
|
4607
5477
|
this._agentRun.Status = 'Running';
|
|
4608
5478
|
this._agentRun.StartedAt = new Date();
|
|
4609
5479
|
this._agentRun.UserID = params.userId || params.contextUser?.ID || null;
|
|
@@ -4761,38 +5631,42 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4761
5631
|
*/
|
|
4762
5632
|
async createStepEntity(params) {
|
|
4763
5633
|
const stepEntity = await this._activeProvider.GetEntityObject('MJ: AI Agent Run Steps', params.contextUser);
|
|
4764
|
-
|
|
5634
|
+
// Client-generate the PK so the step ID is valid IMMEDIATELY (before the INSERT lands) — child
|
|
5635
|
+
// steps link via ParentID and the post-create UPDATE-phase mutations reference this row, and the
|
|
5636
|
+
// create INSERT is fire-and-forget (the agent flow must not block on it).
|
|
5637
|
+
stepEntity.NewRecord();
|
|
4765
5638
|
// Step number is based on current count of steps + 1
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
// Include hierarchy breadcrumb in StepName for better logging
|
|
4769
|
-
stepEntity.StepName = this.formatHierarchicalMessage(params.stepName);
|
|
4770
|
-
// check to see if targetId is a valid UUID
|
|
5639
|
+
const stepNumber = (this._agentRun.Steps?.length || 0) + 1;
|
|
5640
|
+
// Warn on a non-UUID targetId before delegating (initAgentRunStep silently ignores invalid ids).
|
|
4771
5641
|
if (params.targetId && !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(params.targetId)) {
|
|
4772
|
-
// If not valid, we can just ignore it, but console.warn
|
|
4773
5642
|
console.warn(`Invalid target ID format: ${params.targetId}`);
|
|
4774
5643
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
5644
|
+
// Populate the started fields via the shared single-source-of-truth helper. Instance-specific
|
|
5645
|
+
// concerns (hierarchy breadcrumb, InputData context, payload serialization) are computed here.
|
|
5646
|
+
initAgentRunStep(stepEntity, {
|
|
5647
|
+
AgentRunID: this._agentRun.ID,
|
|
5648
|
+
StepNumber: stepNumber,
|
|
5649
|
+
StepType: params.stepType,
|
|
5650
|
+
StepName: this.formatHierarchicalMessage(params.stepName), // include hierarchy breadcrumb
|
|
5651
|
+
TargetID: params.targetId,
|
|
5652
|
+
TargetLogID: params.targetLogId,
|
|
5653
|
+
ParentID: params.parentId, // Link to parent step (e.g., loop step)
|
|
5654
|
+
PayloadAtStart: this.serializePayloadAtStart(params.payloadAtStart),
|
|
5655
|
+
PayloadAtEnd: this.serializePayloadAtEnd(params.payloadAtEnd),
|
|
5656
|
+
InputData: params.inputData
|
|
5657
|
+
? JSON.stringify({
|
|
5658
|
+
...params.inputData,
|
|
5659
|
+
context: {
|
|
5660
|
+
agentHierarchy: this._agentHierarchy,
|
|
5661
|
+
depth: this._depth,
|
|
5662
|
+
stepNumber
|
|
5663
|
+
}
|
|
5664
|
+
})
|
|
5665
|
+
: undefined
|
|
5666
|
+
});
|
|
5667
|
+
// Fire-and-forget the 'started' INSERT — the agent flow never blocks on a step save. The queue
|
|
5668
|
+
// tracks the INSERT so every later UPDATE (queueStepSave) chains AFTER it commits.
|
|
5669
|
+
this._stepSaveQueue.Insert(stepEntity);
|
|
4796
5670
|
// Add the step to the agent run's Steps array
|
|
4797
5671
|
if (this._agentRun) {
|
|
4798
5672
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -4859,57 +5733,42 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4859
5733
|
*/
|
|
4860
5734
|
async finalizeStepEntity(stepEntity, success, errorMessage, outputData) {
|
|
4861
5735
|
try {
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
5736
|
+
// Capture the completion timestamp NOW so the duration is accurate regardless of when the
|
|
5737
|
+
// mutation is actually applied/persisted.
|
|
5738
|
+
const finalizeOpts = {
|
|
5739
|
+
success,
|
|
5740
|
+
errorMessage,
|
|
5741
|
+
outputData: outputData ? CopyScalarsAndArrays(outputData, true) : undefined,
|
|
5742
|
+
completedAt: new Date(),
|
|
5743
|
+
// Capture any TargetLogID already stamped on the entity (e.g. a prompt-run / sub-agent-run id
|
|
5744
|
+
// set before finalize) so the post-INSERT re-apply restores it too — otherwise the INSERT's
|
|
5745
|
+
// reload could leave it null on a fast step.
|
|
5746
|
+
targetLogID: stepEntity.TargetLogID ?? undefined
|
|
5747
|
+
};
|
|
5748
|
+
// Apply to the in-memory entity NOW so the run's Steps array / UI see the terminal state
|
|
5749
|
+
// immediately. This in-memory copy can be reverted by the INSERT's post-save reload if the step
|
|
5750
|
+
// finished while its INSERT was still in flight, which is why we ALSO re-apply it inside the
|
|
5751
|
+
// post-INSERT continuation below (idempotent — same completedAt).
|
|
5752
|
+
finalizeAgentRunStep(stepEntity, finalizeOpts);
|
|
5753
|
+
// Fire-and-forget the UPDATE, but re-assert the finalize state AFTER the INSERT (and its reload)
|
|
5754
|
+
// lands so the force-persisted UPDATE never writes stale pre-finalize values. The agent flow
|
|
5755
|
+
// never blocks on this UPDATE.
|
|
5756
|
+
this.queueStepSave(stepEntity, (s) => finalizeAgentRunStep(s, finalizeOpts));
|
|
4878
5757
|
}
|
|
4879
5758
|
catch (e) {
|
|
4880
5759
|
LogError(`Failed to update agent run step record: ${e?.message ?? e}`, undefined, e);
|
|
4881
5760
|
}
|
|
4882
5761
|
}
|
|
4883
5762
|
/**
|
|
4884
|
-
* Queues a
|
|
4885
|
-
*
|
|
4886
|
-
*
|
|
4887
|
-
*
|
|
4888
|
-
* - Saves on different step records run concurrently.
|
|
4889
|
-
* - Failures are not thrown — they're logged via `LogError` (with the entity's
|
|
4890
|
-
* `LatestResult.CompleteMessage` per the BaseEntity convention) so the
|
|
4891
|
-
* agent loop isn't blocked by observability writes — but they ARE surfaced
|
|
4892
|
-
* in `finalizeAgentRun` so callers see step-record drift.
|
|
4893
|
-
*
|
|
4894
|
-
* Exposed as `protected` so driver sub-classes (e.g. Skip) that author
|
|
4895
|
-
* custom `AIAgentRunStep` records can fire-and-forget saves through the
|
|
4896
|
-
* same chained/non-blocking machinery instead of awaiting `entity.Save()`
|
|
4897
|
-
* inline and blocking the agent loop.
|
|
5763
|
+
* Queues a fire-and-forget UPDATE of a step entity whose fields the caller has ALREADY mutated.
|
|
5764
|
+
* Delegates to {@link AgentRunStepSaveQueue.QueueUpdate} — the agent flow never awaits this; the UPDATE
|
|
5765
|
+
* chains after the step's INSERT and force-persists (`IgnoreDirtyState`). Kept `protected` so driver
|
|
5766
|
+
* subclasses that finalize their own steps get the same non-blocking behavior.
|
|
4898
5767
|
*
|
|
4899
5768
|
* @protected
|
|
4900
5769
|
*/
|
|
4901
|
-
queueStepSave(stepEntity) {
|
|
4902
|
-
|
|
4903
|
-
// Save() assigns it, so an ID-keyed chain breaks for fast create→finalize sequences.
|
|
4904
|
-
const previousSave = this._stepSavePromises.get(stepEntity) ?? Promise.resolve();
|
|
4905
|
-
const currentSave = previousSave.then(() => stepEntity.Save()).then((ok) => {
|
|
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
|
-
});
|
|
4911
|
-
this._stepSavePromises.set(stepEntity, currentSave);
|
|
4912
|
-
this._pendingSaves.push(currentSave);
|
|
5770
|
+
queueStepSave(stepEntity, applyMutation) {
|
|
5771
|
+
this._stepSaveQueue.QueueUpdate(stepEntity, applyMutation);
|
|
4913
5772
|
}
|
|
4914
5773
|
/**
|
|
4915
5774
|
* Maps an array through an async worker with bounded concurrency.
|
|
@@ -5242,10 +6101,8 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5242
6101
|
},
|
|
5243
6102
|
displayMode: 'live' // Only show in live mode
|
|
5244
6103
|
});
|
|
5245
|
-
//
|
|
5246
|
-
|
|
5247
|
-
stepEntity.PayloadAtStart = this.serializePayloadAtStart(payload);
|
|
5248
|
-
}
|
|
6104
|
+
// PayloadAtStart was already serialized from this same `payload` by createStepEntity
|
|
6105
|
+
// above (payloadAtStart: payload) — no need to re-serialize the (potentially large) payload here.
|
|
5249
6106
|
let downstreamPayload = payload; // Start with current payload
|
|
5250
6107
|
if (params.agent.PayloadSelfReadPaths) {
|
|
5251
6108
|
const downstreamPaths = JSON.parse(params.agent.PayloadSelfReadPaths);
|
|
@@ -5282,7 +6139,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5282
6139
|
} : undefined;
|
|
5283
6140
|
promptParams.onPromptRunCreated = async (promptRunId) => {
|
|
5284
6141
|
stepEntity.TargetLogID = promptRunId;
|
|
5285
|
-
|
|
6142
|
+
// Re-apply post-INSERT: onPromptRunCreated can fire before the step's INSERT lands, and the
|
|
6143
|
+
// INSERT's reload would otherwise revert TargetLogID back to null.
|
|
6144
|
+
this.queueStepSave(stepEntity, (s) => { s.TargetLogID = promptRunId; });
|
|
5286
6145
|
};
|
|
5287
6146
|
// Execute the prompt
|
|
5288
6147
|
const promptResult = await this.executePrompt(promptParams);
|
|
@@ -5294,7 +6153,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5294
6153
|
// Update step entity with AIPromptRun ID if available
|
|
5295
6154
|
if (promptResult.promptRun?.ID) {
|
|
5296
6155
|
stepEntity.TargetLogID = promptResult.promptRun.ID;
|
|
5297
|
-
stepEntity.PromptRun = promptResult.promptRun; //
|
|
6156
|
+
stepEntity.PromptRun = promptResult.promptRun; // transient related object (not a persisted field)
|
|
5298
6157
|
// don't save here, we save when we call finalizeStepEntity()
|
|
5299
6158
|
}
|
|
5300
6159
|
// Check if prompt execution failed
|
|
@@ -5411,6 +6270,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5411
6270
|
else if (this._artifactToolManager.HasArtifacts()) {
|
|
5412
6271
|
this.logStatus(`[ArtifactTools] LLM did not use artifact tools this turn (artifacts available but not accessed)`, true, params);
|
|
5413
6272
|
}
|
|
6273
|
+
// Execute in-flight memory writes if provided (zero turn cost — processed inline)
|
|
6274
|
+
const memoryWrites = initialNextStep.memoryWrites;
|
|
6275
|
+
if (memoryWrites?.length) {
|
|
6276
|
+
await this.processMemoryWritesForTurn(memoryWrites, params);
|
|
6277
|
+
}
|
|
5414
6278
|
// Execute a tool pipeline if provided (zero turn cost — processed inline). Each step's
|
|
5415
6279
|
// output is threaded into the next server-side; only the final step's output returns to
|
|
5416
6280
|
// the LLM, so intermediate payloads never enter the context window.
|
|
@@ -5797,7 +6661,7 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5797
6661
|
// Update step entity with AIAgentRun ID if available
|
|
5798
6662
|
if (subAgentResult.agentRun?.ID) {
|
|
5799
6663
|
stepEntity.TargetLogID = subAgentResult.agentRun.ID;
|
|
5800
|
-
// Set the SubAgentRun property for hierarchical tracking
|
|
6664
|
+
// Set the SubAgentRun property for hierarchical tracking (transient related object)
|
|
5801
6665
|
stepEntity.SubAgentRun = subAgentResult.agentRun;
|
|
5802
6666
|
stepEntity.PayloadAtEnd = this.serializePayloadAtEnd(mergedPayload);
|
|
5803
6667
|
// saving happens later by calling finalizeStepEntity()
|
|
@@ -6926,8 +7790,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6926
7790
|
actionResult = await this.ExecuteSingleAction(params, aa, actionEntity, params.contextUser);
|
|
6927
7791
|
// Update step entity with ActionExecutionLog ID if available
|
|
6928
7792
|
if (actionResult.LogEntry?.ID) {
|
|
6929
|
-
|
|
6930
|
-
|
|
7793
|
+
const logId = actionResult.LogEntry.ID;
|
|
7794
|
+
stepEntity.TargetLogID = logId;
|
|
7795
|
+
// Re-apply post-INSERT: a fast action can finish before the step's INSERT lands, and
|
|
7796
|
+
// the INSERT's reload would otherwise revert TargetLogID back to null.
|
|
7797
|
+
this.queueStepSave(stepEntity, (s) => { s.TargetLogID = logId; });
|
|
6931
7798
|
}
|
|
6932
7799
|
// Prepare output data with action result
|
|
6933
7800
|
const outputData = {
|
|
@@ -8203,27 +9070,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8203
9070
|
* @private
|
|
8204
9071
|
*/
|
|
8205
9072
|
async finalizeAgentRun(finalStep, payload, contextUser) {
|
|
8206
|
-
//
|
|
8207
|
-
//
|
|
8208
|
-
//
|
|
8209
|
-
|
|
8210
|
-
|
|
8211
|
-
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
|
|
8215
|
-
const rejections = settled.filter(s => s.status === 'rejected');
|
|
8216
|
-
const falses = settled.filter(s => s.status === 'fulfilled' && s.value === false).length;
|
|
8217
|
-
for (const r of rejections) {
|
|
8218
|
-
LogError(`Pending step save rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
8219
|
-
}
|
|
8220
|
-
const totalFailures = rejections.length + falses;
|
|
8221
|
-
if (totalFailures > 0 && this._agentRun) {
|
|
8222
|
-
const note = `${totalFailures} step record save(s) failed during this run; see logs for details.`;
|
|
8223
|
-
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
8224
|
-
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
8225
|
-
: note;
|
|
8226
|
-
}
|
|
9073
|
+
// Flush every pending step save (success OR failure) via the shared queue, which allSettles so a
|
|
9074
|
+
// single failure doesn't shadow the rest and drains itself so a reused instance doesn't leak
|
|
9075
|
+
// settled promises. Surface the failure count on the run for visibility.
|
|
9076
|
+
const { failures } = await this._stepSaveQueue.Flush();
|
|
9077
|
+
if (failures > 0 && this._agentRun) {
|
|
9078
|
+
const note = `${failures} step record save(s) failed during this run; see logs for details.`;
|
|
9079
|
+
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
9080
|
+
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
9081
|
+
: note;
|
|
8227
9082
|
}
|
|
8228
9083
|
// Only resolve media placeholders for ROOT agents (depth === 0)
|
|
8229
9084
|
// Sub-agents keep placeholders intact so parent agents don't get huge base64 in their context
|
|
@@ -8259,12 +9114,17 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8259
9114
|
else {
|
|
8260
9115
|
this._agentRun.Status = 'Completed';
|
|
8261
9116
|
}
|
|
8262
|
-
|
|
9117
|
+
// Serialize the (largest-it-ever-gets) final payload ONCE and reuse for both
|
|
9118
|
+
// Result and FinalPayload instead of stringifying the same object three times.
|
|
9119
|
+
const finalPayloadJson = resolvedPayload ? JSON.stringify(resolvedPayload) : null;
|
|
9120
|
+
this._agentRun.Result = finalPayloadJson;
|
|
8263
9121
|
this._agentRun.FinalStep = finalStep.step;
|
|
8264
9122
|
this._agentRun.Message = finalStep.message;
|
|
8265
|
-
// Set the FinalPayloadObject
|
|
9123
|
+
// Set the FinalPayloadObject (populates the object cache; its setter also writes
|
|
9124
|
+
// FinalPayload when the value changes). We then assign FinalPayload from the
|
|
9125
|
+
// already-computed JSON to guarantee it's set regardless of the setter's change guard.
|
|
8266
9126
|
this._agentRun.FinalPayloadObject = resolvedPayload;
|
|
8267
|
-
this._agentRun.FinalPayload =
|
|
9127
|
+
this._agentRun.FinalPayload = finalPayloadJson;
|
|
8268
9128
|
// Calculate total tokens from all prompts and sub-agents
|
|
8269
9129
|
const tokenStats = this.calculateTokenStats();
|
|
8270
9130
|
this._agentRun.TotalTokensUsed = tokenStats.totalTokens;
|