@memberjunction/ai-agents 5.41.0 → 5.43.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 +10 -0
- package/dist/agent-types/flow-agent-type.d.ts.map +1 -1
- package/dist/agent-types/flow-agent-type.js +10 -2
- package/dist/agent-types/flow-agent-type.js.map +1 -1
- package/dist/base-agent.d.ts +57 -51
- package/dist/base-agent.d.ts.map +1 -1
- package/dist/base-agent.js +239 -187
- package/dist/base-agent.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/memory-manager-agent.d.ts +44 -2
- package/dist/memory-manager-agent.d.ts.map +1 -1
- package/dist/memory-manager-agent.js +133 -100
- 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 +63 -0
- package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -0
- package/dist/realtime/bridge-room-transcript-sink.js +151 -0
- package/dist/realtime/bridge-room-transcript-sink.js.map +1 -0
- package/dist/realtime/realtime-client-session-service.d.ts +148 -6
- package/dist/realtime/realtime-client-session-service.d.ts.map +1 -1
- package/dist/realtime/realtime-client-session-service.js +241 -35
- package/dist/realtime/realtime-client-session-service.js.map +1 -1
- package/dist/realtime/realtime-coagent-config.d.ts +142 -7
- package/dist/realtime/realtime-coagent-config.d.ts.map +1 -1
- package/dist/realtime/realtime-coagent-config.js +176 -8
- package/dist/realtime/realtime-coagent-config.js.map +1 -1
- package/dist/realtime/realtime-tool-broker.d.ts +15 -0
- package/dist/realtime/realtime-tool-broker.d.ts.map +1 -1
- package/dist/realtime/realtime-tool-broker.js +22 -0
- package/dist/realtime/realtime-tool-broker.js.map +1 -1
- package/dist/realtime/realtime-turn-moderator.d.ts +103 -0
- package/dist/realtime/realtime-turn-moderator.d.ts.map +1 -0
- package/dist/realtime/realtime-turn-moderator.js +266 -0
- package/dist/realtime/realtime-turn-moderator.js.map +1 -0
- package/package.json +17 -17
package/dist/base-agent.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @since 2.49.0
|
|
12
12
|
*/
|
|
13
13
|
import { FileStorageEngineBase } from '@memberjunction/core-entities';
|
|
14
|
-
import { Metadata, RunView, LogStatus, LogStatusEx, LogError, LogErrorEx, IsVerboseLoggingEnabled, DatabaseProviderBase
|
|
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
17
|
import { BaseRealtimeModel, GetAIAPIKey } from '@memberjunction/ai';
|
|
@@ -19,12 +19,14 @@ import { BaseAgentType } from './agent-types/base-agent-type.js';
|
|
|
19
19
|
import { CopyScalarsAndArrays, JSONValidator, MJGlobal, SafeExpressionEvaluator, UUIDsEqual } from '@memberjunction/global';
|
|
20
20
|
import { RealtimeSessionRunner } from './realtime/realtime-session-runner.js';
|
|
21
21
|
import { ResolveNarrationInstructionsTemplate } from './realtime/realtime-narration.js';
|
|
22
|
-
import { BuildVoiceMannerSection, GetNarrationPaceMs, GetProviderVoiceSettings, ResolveEffectiveRealtimeConfig } from './realtime/realtime-coagent-config.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';
|
|
23
25
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
24
26
|
import { ActionEngineServer } from '@memberjunction/actions';
|
|
25
27
|
import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
|
|
26
28
|
import { AgentMemoryContextBuilder } from './agent-memory-context-builder.js';
|
|
27
|
-
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy } from '@memberjunction/ai-core-plus';
|
|
29
|
+
import { AIPromptParams, ChildPromptParam, ConversationUtility, ParseFileOutputRef, parseAssignmentStrategy, initAgentRunStep, finalizeAgentRunStep, AgentRunStepSaveQueue } from '@memberjunction/ai-core-plus';
|
|
28
30
|
import { AgentRunner } from './AgentRunner.js';
|
|
29
31
|
import { PayloadManager } from './PayloadManager.js';
|
|
30
32
|
import { ScratchpadManager } from './ScratchpadManager.js';
|
|
@@ -92,30 +94,13 @@ export class BaseAgent {
|
|
|
92
94
|
*/
|
|
93
95
|
this._promptRunner = new AIPromptRunner();
|
|
94
96
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
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).
|
|
97
102
|
*/
|
|
98
|
-
this.
|
|
99
|
-
/**
|
|
100
|
-
* Queue map to chain database saves sequentially per step entity.
|
|
101
|
-
* Prevents UPDATE queries running before INSERT queries on quick steps.
|
|
102
|
-
*
|
|
103
|
-
* Keyed by the step ENTITY INSTANCE, not its `ID`: a new step's `ID` is empty at create time and
|
|
104
|
-
* only gets populated during its INSERT `Save()`, so keying by `ID` would file the create and the
|
|
105
|
-
* finalize under different buckets and defeat the chain — letting the UPDATE race ahead of the
|
|
106
|
-
* INSERT on millisecond-fast steps (e.g. pipelines), which left them stuck at `Running`.
|
|
107
|
-
*/
|
|
108
|
-
this._stepSavePromises = new Map();
|
|
109
|
-
/**
|
|
110
|
-
* Per-step 'started'-INSERT promises, keyed by the step entity instance. The create INSERT is
|
|
111
|
-
* fire-and-forget (the agent flow never blocks on it — the PK is client-generated by NewRecord() so
|
|
112
|
-
* the ID is valid immediately). Every UPDATE-phase save ({@link queueStepSave}) chains after this
|
|
113
|
-
* INSERT promise so the UPDATE never races ahead of the create, and force-persists with
|
|
114
|
-
* `IgnoreDirtyState` so a mutation absorbed by the INSERT's post-save dirty-reset (which silently
|
|
115
|
-
* no-op'd UPDATEs and left fast create→finalize steps stuck at Status='Running') is still written.
|
|
116
|
-
* A WeakMap so entries are GC'd with the entity.
|
|
117
|
-
*/
|
|
118
|
-
this._stepInsertPromises = new WeakMap();
|
|
103
|
+
this._stepSaveQueue = new AgentRunStepSaveQueue();
|
|
119
104
|
/**
|
|
120
105
|
* Active per-request metadata provider, set at the start of Execute().
|
|
121
106
|
* Defaults to the global Metadata.Provider; overridden when a per-request
|
|
@@ -1299,6 +1284,93 @@ export class BaseAgent {
|
|
|
1299
1284
|
return await this.createFailureResult(msg, params.contextUser);
|
|
1300
1285
|
}
|
|
1301
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
|
+
}
|
|
1302
1374
|
/**
|
|
1303
1375
|
* Resolves the realtime model + vendor driver + API key for a session-driven run.
|
|
1304
1376
|
*
|
|
@@ -1316,54 +1388,68 @@ export class BaseAgent {
|
|
|
1316
1388
|
* @param params The execution parameters (for the agent + context user).
|
|
1317
1389
|
* @returns The resolved model instance plus its model/vendor identifiers, or `null`.
|
|
1318
1390
|
*/
|
|
1319
|
-
async resolveRealtimeModel(params) {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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 };
|
|
1335
1412
|
}
|
|
1336
|
-
return
|
|
1413
|
+
return null;
|
|
1337
1414
|
}
|
|
1338
1415
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
* (
|
|
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.
|
|
1342
1424
|
*
|
|
1343
|
-
* @param agent The agent being executed
|
|
1344
|
-
* @returns The
|
|
1425
|
+
* @param agent The agent being executed.
|
|
1426
|
+
* @returns The candidate models in resolution order (empty when none are active).
|
|
1345
1427
|
*/
|
|
1346
|
-
|
|
1428
|
+
selectRealtimeModelCandidates(agent, overrideModelID) {
|
|
1347
1429
|
const isRealtime = (m) => typeof m.AIModelType === 'string' && m.AIModelType.trim().toLowerCase() === 'realtime';
|
|
1348
1430
|
const realtimeModels = AIEngine.Instance.Models.filter(m => m.IsActive && isRealtime(m));
|
|
1349
1431
|
if (realtimeModels.length === 0) {
|
|
1350
|
-
return
|
|
1432
|
+
return [];
|
|
1351
1433
|
}
|
|
1352
|
-
|
|
1353
|
-
//
|
|
1354
|
-
//
|
|
1355
|
-
const preference =
|
|
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;
|
|
1356
1440
|
if (preference) {
|
|
1357
1441
|
const wanted = preference.trim().toLowerCase();
|
|
1358
1442
|
const preferred = realtimeModels.find(m => UUIDsEqual(m.ID, preference))
|
|
1359
1443
|
?? realtimeModels.find(m => m.Name?.trim().toLowerCase() === wanted);
|
|
1360
1444
|
if (preferred) {
|
|
1361
|
-
|
|
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))];
|
|
1362
1448
|
}
|
|
1363
1449
|
this.logError(`Realtime model preference '${preference}' for agent '${agent.Name}' matches no Active Realtime ` +
|
|
1364
1450
|
'model — falling through to default (highest-PowerRank) selection.', { agent, category: 'RealtimeSession' });
|
|
1365
1451
|
}
|
|
1366
|
-
return
|
|
1452
|
+
return byPower;
|
|
1367
1453
|
}
|
|
1368
1454
|
/**
|
|
1369
1455
|
* Resolves the agent's EFFECTIVE realtime configuration — the agent TYPE's
|
|
@@ -1498,16 +1584,11 @@ export class BaseAgent {
|
|
|
1498
1584
|
* @returns The session parameters.
|
|
1499
1585
|
*/
|
|
1500
1586
|
async buildRealtimeSessionParams(params, config, modelApiName, effectiveConfig, driverClass) {
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
`can watch, or 'Whiteboard_*' to draw on a shared board). Those surfaces are operated by ` +
|
|
1507
|
-
`YOU, directly — when the user asks to use one (e.g. "open/show a browser", "go to a ` +
|
|
1508
|
-
`site", "add to the whiteboard"), call the matching tool yourself immediately and narrate ` +
|
|
1509
|
-
`what you're doing. NEVER route an interactive-surface request through 'invoke-target-agent', ` +
|
|
1510
|
-
`and never claim you lack a session — calling the tool is all that's needed.`;
|
|
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');
|
|
1511
1592
|
const basePrompt = config.systemPrompt?.TemplateText ? config.systemPrompt.TemplateText : '';
|
|
1512
1593
|
// Effective-config voice persona (realtime.voice.default) → short "Voice & manner" section.
|
|
1513
1594
|
const voiceManner = BuildVoiceMannerSection(effectiveConfig);
|
|
@@ -1941,8 +2022,13 @@ export class BaseAgent {
|
|
|
1941
2022
|
* @protected
|
|
1942
2023
|
*/
|
|
1943
2024
|
async initializeEngines(contextUser) {
|
|
1944
|
-
|
|
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.
|
|
1945
2030
|
await ActionEngineServer.Instance.Config(false, contextUser);
|
|
2031
|
+
await AIEngine.Instance.Config(false, contextUser);
|
|
1946
2032
|
}
|
|
1947
2033
|
/**
|
|
1948
2034
|
* Determine the scope label for a note based on its scope fields.
|
|
@@ -4796,7 +4882,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
4796
4882
|
SecondaryScopes: params.SecondaryScopes,
|
|
4797
4883
|
onAgentRunCreated: async (agentRunId) => {
|
|
4798
4884
|
stepEntity.TargetLogID = agentRunId;
|
|
4799
|
-
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; });
|
|
4800
4888
|
}
|
|
4801
4889
|
});
|
|
4802
4890
|
// Check if execution was successful
|
|
@@ -5547,43 +5635,38 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5547
5635
|
// steps link via ParentID and the post-create UPDATE-phase mutations reference this row, and the
|
|
5548
5636
|
// create INSERT is fire-and-forget (the agent flow must not block on it).
|
|
5549
5637
|
stepEntity.NewRecord();
|
|
5550
|
-
stepEntity.AgentRunID = this._agentRun.ID;
|
|
5551
5638
|
// Step number is based on current count of steps + 1
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
// Include hierarchy breadcrumb in StepName for better logging
|
|
5555
|
-
stepEntity.StepName = this.formatHierarchicalMessage(params.stepName);
|
|
5556
|
-
// 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).
|
|
5557
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)) {
|
|
5558
|
-
// If not valid, we can just ignore it, but console.warn
|
|
5559
5642
|
console.warn(`Invalid target ID format: ${params.targetId}`);
|
|
5560
5643
|
}
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
this.
|
|
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);
|
|
5587
5670
|
// Add the step to the agent run's Steps array
|
|
5588
5671
|
if (this._agentRun) {
|
|
5589
5672
|
this._agentRun.Steps.push(stepEntity);
|
|
@@ -5650,78 +5733,42 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
5650
5733
|
*/
|
|
5651
5734
|
async finalizeStepEntity(stepEntity, success, errorMessage, outputData) {
|
|
5652
5735
|
try {
|
|
5653
|
-
//
|
|
5654
|
-
//
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
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));
|
|
5672
5757
|
}
|
|
5673
5758
|
catch (e) {
|
|
5674
5759
|
LogError(`Failed to update agent run step record: ${e?.message ?? e}`, undefined, e);
|
|
5675
5760
|
}
|
|
5676
5761
|
}
|
|
5677
|
-
/**
|
|
5678
|
-
* Saves one step record and ALWAYS logs (never swallows, never verbose-gated) a failure via `LogError`
|
|
5679
|
-
* with `LatestResult.CompleteMessage` — the log is observability, so a failure must surface but must
|
|
5680
|
-
* not throw into the agent loop. The `update` phase force-saves with `IgnoreDirtyState` because a
|
|
5681
|
-
* finalize/TargetLogID mutation applied while the INSERT was still in flight gets absorbed by the
|
|
5682
|
-
* INSERT's post-save dirty-reset, leaving the new values only in memory (the "step stuck at Running"
|
|
5683
|
-
* bug); forcing the UPDATE re-persists them. `insert` saves normally.
|
|
5684
|
-
* @returns whether the row persisted.
|
|
5685
|
-
*/
|
|
5686
|
-
async saveStepRecord(stepEntity, phase) {
|
|
5687
|
-
try {
|
|
5688
|
-
let options;
|
|
5689
|
-
if (phase === 'update') {
|
|
5690
|
-
options = new EntitySaveOptions();
|
|
5691
|
-
options.IgnoreDirtyState = true;
|
|
5692
|
-
}
|
|
5693
|
-
const ok = await stepEntity.Save(options);
|
|
5694
|
-
if (!ok) {
|
|
5695
|
-
LogError(`Failed to ${phase} agent run step record ${stepEntity.ID || '(unsaved)'}: ${stepEntity.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
5696
|
-
}
|
|
5697
|
-
return ok;
|
|
5698
|
-
}
|
|
5699
|
-
catch (e) {
|
|
5700
|
-
LogError(`Error on ${phase} of agent run step record ${stepEntity.ID || '(unsaved)'}: ${e?.message ?? e}`);
|
|
5701
|
-
return false;
|
|
5702
|
-
}
|
|
5703
|
-
}
|
|
5704
5762
|
/**
|
|
5705
5763
|
* Queues a fire-and-forget UPDATE of a step entity whose fields the caller has ALREADY mutated.
|
|
5706
|
-
*
|
|
5707
|
-
*
|
|
5708
|
-
*
|
|
5709
|
-
* ahead of the INSERT; updates to DIFFERENT steps run concurrently.
|
|
5710
|
-
* - Force-persists (IgnoreDirtyState) because a mutation applied while the INSERT was in flight can be
|
|
5711
|
-
* absorbed by the INSERT's post-save dirty-reset, leaving the entity "clean" with the new values
|
|
5712
|
-
* only in memory — without the force, the UPDATE would silently no-op and the row would stay stuck
|
|
5713
|
-
* at Status='Running' / null TargetLogID.
|
|
5714
|
-
* - Failures are logged (never thrown) and surfaced via `_pendingSaves` at run finalize.
|
|
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.
|
|
5715
5767
|
*
|
|
5716
5768
|
* @protected
|
|
5717
5769
|
*/
|
|
5718
|
-
queueStepSave(stepEntity) {
|
|
5719
|
-
|
|
5720
|
-
// UPDATE queued before the create lands still runs after it.
|
|
5721
|
-
const previousSave = this._stepSavePromises.get(stepEntity) ?? this._stepInsertPromises.get(stepEntity) ?? Promise.resolve();
|
|
5722
|
-
const currentSave = previousSave.then(() => this.saveStepRecord(stepEntity, 'update'));
|
|
5723
|
-
this._stepSavePromises.set(stepEntity, currentSave);
|
|
5724
|
-
this._pendingSaves.push(currentSave);
|
|
5770
|
+
queueStepSave(stepEntity, applyMutation) {
|
|
5771
|
+
this._stepSaveQueue.QueueUpdate(stepEntity, applyMutation);
|
|
5725
5772
|
}
|
|
5726
5773
|
/**
|
|
5727
5774
|
* Maps an array through an async worker with bounded concurrency.
|
|
@@ -6092,7 +6139,9 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
6092
6139
|
} : undefined;
|
|
6093
6140
|
promptParams.onPromptRunCreated = async (promptRunId) => {
|
|
6094
6141
|
stepEntity.TargetLogID = promptRunId;
|
|
6095
|
-
|
|
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; });
|
|
6096
6145
|
};
|
|
6097
6146
|
// Execute the prompt
|
|
6098
6147
|
const promptResult = await this.executePrompt(promptParams);
|
|
@@ -7741,8 +7790,11 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
7741
7790
|
actionResult = await this.ExecuteSingleAction(params, aa, actionEntity, params.contextUser);
|
|
7742
7791
|
// Update step entity with ActionExecutionLog ID if available
|
|
7743
7792
|
if (actionResult.LogEntry?.ID) {
|
|
7744
|
-
|
|
7745
|
-
|
|
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; });
|
|
7746
7798
|
}
|
|
7747
7799
|
// Prepare output data with action result
|
|
7748
7800
|
const outputData = {
|
|
@@ -8468,6 +8520,18 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
8468
8520
|
* Strips "payload." prefix if present (for LLM convenience)
|
|
8469
8521
|
*/
|
|
8470
8522
|
getCollectionFromPayload(payload, path) {
|
|
8523
|
+
// Support a literal static collection: "static:[1,2,3,4,5]". This lets a ForEach iterate a
|
|
8524
|
+
// fixed list/range without a prior step having to build the array in the payload first.
|
|
8525
|
+
const trimmed = path.trim();
|
|
8526
|
+
if (trimmed.toLowerCase().startsWith('static:')) {
|
|
8527
|
+
try {
|
|
8528
|
+
const parsed = JSON.parse(trimmed.substring(trimmed.indexOf(':') + 1).trim());
|
|
8529
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
8530
|
+
}
|
|
8531
|
+
catch {
|
|
8532
|
+
return null;
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8471
8535
|
// Remove "payload." prefix if present
|
|
8472
8536
|
const cleanPath = path.toLowerCase().startsWith('payload.')
|
|
8473
8537
|
? path.substring(8)
|
|
@@ -9018,27 +9082,15 @@ The context is now within limits. Please retry your request with the recovered c
|
|
|
9018
9082
|
* @private
|
|
9019
9083
|
*/
|
|
9020
9084
|
async finalizeAgentRun(finalStep, payload, contextUser) {
|
|
9021
|
-
//
|
|
9022
|
-
//
|
|
9023
|
-
//
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
const rejections = settled.filter(s => s.status === 'rejected');
|
|
9031
|
-
const falses = settled.filter(s => s.status === 'fulfilled' && s.value === false).length;
|
|
9032
|
-
for (const r of rejections) {
|
|
9033
|
-
LogError(`Pending step save rejected: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`);
|
|
9034
|
-
}
|
|
9035
|
-
const totalFailures = rejections.length + falses;
|
|
9036
|
-
if (totalFailures > 0 && this._agentRun) {
|
|
9037
|
-
const note = `${totalFailures} step record save(s) failed during this run; see logs for details.`;
|
|
9038
|
-
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
9039
|
-
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
9040
|
-
: note;
|
|
9041
|
-
}
|
|
9085
|
+
// Flush every pending step save (success OR failure) via the shared queue, which allSettles so a
|
|
9086
|
+
// single failure doesn't shadow the rest and drains itself so a reused instance doesn't leak
|
|
9087
|
+
// settled promises. Surface the failure count on the run for visibility.
|
|
9088
|
+
const { failures } = await this._stepSaveQueue.Flush();
|
|
9089
|
+
if (failures > 0 && this._agentRun) {
|
|
9090
|
+
const note = `${failures} step record save(s) failed during this run; see logs for details.`;
|
|
9091
|
+
this._agentRun.ErrorMessage = this._agentRun.ErrorMessage
|
|
9092
|
+
? `${this._agentRun.ErrorMessage}\n${note}`
|
|
9093
|
+
: note;
|
|
9042
9094
|
}
|
|
9043
9095
|
// Only resolve media placeholders for ROOT agents (depth === 0)
|
|
9044
9096
|
// Sub-agents keep placeholders intact so parent agents don't get huge base64 in their context
|