@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
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 +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, InputType, Field } from 'type-graphql';
|
|
2
2
|
import { randomUUID } from 'crypto';
|
|
3
|
-
import { LogError, LogStatusEx, UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
4
|
-
import {
|
|
3
|
+
import { LogError, LogStatusEx, UserInfo, IMetadataProvider, RunView } from '@memberjunction/core';
|
|
4
|
+
import { EscapeSQLString } from '@memberjunction/global';
|
|
5
|
+
import type { TurnAddressingMode } from '@memberjunction/ai-bridge-base';
|
|
6
|
+
import {
|
|
7
|
+
LiveKitTokenService,
|
|
8
|
+
LiveKitAgentRoomCoordinator,
|
|
9
|
+
LiveKitEgressService,
|
|
10
|
+
LiveKitUserIdentity,
|
|
11
|
+
RoomHandoffEngine,
|
|
12
|
+
} from '@memberjunction/livekit-room-server';
|
|
5
13
|
import { AppContext } from '../types.js';
|
|
6
14
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
7
15
|
import { GetReadWriteProvider } from '../util.js';
|
|
8
|
-
import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision } from '@memberjunction/ai-agents';
|
|
16
|
+
import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision, GetBridgeRealtimeRuntime } from '@memberjunction/ai-agents';
|
|
9
17
|
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
|
|
10
18
|
import { SessionManager } from '../agentSessions/SessionManager.js';
|
|
11
19
|
import { NotificationEngine } from '@memberjunction/notifications';
|
|
12
20
|
import { RegisterMeetingRecordingFile, CorrelateRecordingStart } from './meetingRecordingRegistration.js';
|
|
21
|
+
import { RoomAuthorizationService } from '@memberjunction/livekit-room-server';
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
24
|
* Binds the agent realtime-session factory onto the LiveKit room coordinator's model-session creation seam.
|
|
@@ -21,6 +30,20 @@ import { RegisterMeetingRecordingFile, CorrelateRecordingStart } from './meeting
|
|
|
21
30
|
*/
|
|
22
31
|
LiveKitAgentRoomCoordinator.Instance.SetSessionFactory(CreateBridgeRealtimeSession);
|
|
23
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Binds the turn-taking tool handler seam (same module-load rationale as the factory above). A full-duplex model
|
|
35
|
+
* in a shared room calls `i_am_addressed` / `yield_turn`; the bridge engine owns what those mean, and this installs
|
|
36
|
+
* the engine's handler as the model session runtime's local tool handler (`@memberjunction/ai-agents`).
|
|
37
|
+
*/
|
|
38
|
+
LiveKitAgentRoomCoordinator.Instance.SetTurnToolBinder((session, handler) => {
|
|
39
|
+
const runtime = GetBridgeRealtimeRuntime(session);
|
|
40
|
+
if (!runtime) {
|
|
41
|
+
LogError('[RealtimeBridge] the model session has no bridge runtime; i_am_addressed / yield_turn will not execute for it.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
runtime.SetLocalToolHandler(handler);
|
|
45
|
+
});
|
|
46
|
+
|
|
24
47
|
/**
|
|
25
48
|
* Binds the co-agent run finalizer onto the bridge engine (same module-load rationale as the factory above).
|
|
26
49
|
* Lets the engine finalize a session's dangling co-agent observability run when it reaps a bridge WITHOUT a
|
|
@@ -127,6 +150,35 @@ export class StartLiveKitAgentRoomSessionInput {
|
|
|
127
150
|
|
|
128
151
|
@Field(() => String, { nullable: true })
|
|
129
152
|
TurnMode?: string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Start the agent able to bring someone into this room: a person (who is offered the conversation and may accept), a
|
|
156
|
+
* phone number, or another agent. Opt-in, and honoured only when this server has the handoff collaborators configured
|
|
157
|
+
* (the LiveKit SIP extension); otherwise the agent starts as it always did.
|
|
158
|
+
*/
|
|
159
|
+
@Field(() => Boolean, { nullable: true })
|
|
160
|
+
EnableHandoff?: boolean;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* How this agent decides it was addressed: `Auto` (default — the model's own judgement when it is full-duplex,
|
|
164
|
+
* name matching otherwise), `ModelSide`, or `Regex`.
|
|
165
|
+
*/
|
|
166
|
+
@Field(() => String, { nullable: true })
|
|
167
|
+
TurnAddressing?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The live turn-taking state of a room (floor holder, human speaking, hand-off, loop cap, recent events, seated agents). */
|
|
171
|
+
@ObjectType()
|
|
172
|
+
export class LiveKitRoomTurnStateResult {
|
|
173
|
+
@Field(() => Boolean)
|
|
174
|
+
Success: boolean;
|
|
175
|
+
|
|
176
|
+
@Field(() => String, { nullable: true })
|
|
177
|
+
ErrorMessage?: string;
|
|
178
|
+
|
|
179
|
+
/** JSON of the room's `RoomTurnSnapshot`; `null` when the room holds no agents. Parsed by the typed client. */
|
|
180
|
+
@Field(() => String, { nullable: true })
|
|
181
|
+
StateJSON?: string;
|
|
130
182
|
}
|
|
131
183
|
|
|
132
184
|
@ObjectType()
|
|
@@ -231,6 +283,11 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
231
283
|
if (!user) {
|
|
232
284
|
return failure('Unable to determine current user.');
|
|
233
285
|
}
|
|
286
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
287
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(input.RoomName, user, provider);
|
|
288
|
+
if (!auth.Authorized) {
|
|
289
|
+
return failure(auth.Reason ?? 'Unauthorized room access.');
|
|
290
|
+
}
|
|
234
291
|
const tokenService = new LiveKitTokenService();
|
|
235
292
|
const minted = await tokenService.MintClientToken(input.RoomName, this.participantIdentity(user), input.DisplayName ?? user.Name ?? user.Email);
|
|
236
293
|
return { Success: true, ...minted };
|
|
@@ -264,9 +321,31 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
264
321
|
if (!user) {
|
|
265
322
|
return failure('Unable to determine current user.');
|
|
266
323
|
}
|
|
267
|
-
const provider = GetReadWriteProvider(context.providers) as
|
|
324
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
268
325
|
const roomName = input.RoomName?.trim() || `mj-${randomUUID()}`;
|
|
269
326
|
|
|
327
|
+
if (input.RoomName?.trim()) {
|
|
328
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
329
|
+
if (!auth.Authorized) {
|
|
330
|
+
return failure(auth.Reason ?? 'Unauthorized room access.', roomName);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Opt-in: an agent that can bring a person, a number or another agent into this room (the same room, so a visitor
|
|
335
|
+
// is escalated without leaving it). Falls through to the standard start when the server cannot do handoffs.
|
|
336
|
+
const handoffSessionBridgeID = await this.startWithHandoff(input, roomName, user, provider);
|
|
337
|
+
if (handoffSessionBridgeID) {
|
|
338
|
+
const handoffToken = await new LiveKitTokenService().MintClientToken(roomName, this.participantIdentity(user), user.Name ?? user.Email);
|
|
339
|
+
return {
|
|
340
|
+
Success: true,
|
|
341
|
+
SessionBridgeID: handoffSessionBridgeID,
|
|
342
|
+
RoomName: roomName,
|
|
343
|
+
ServerUrl: handoffToken.ServerUrl,
|
|
344
|
+
ClientToken: handoffToken.Token,
|
|
345
|
+
Identity: handoffToken.Identity,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
270
349
|
// Resolve the AIAgentSession the bridge will reference. The bridge row FK-references
|
|
271
350
|
// AIAgentSession(ID), so we must use an EXISTING session — either one the caller supplied, or a
|
|
272
351
|
// freshly-created one. Previously this minted a bare random UUID with no backing row, so the
|
|
@@ -293,6 +372,7 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
293
372
|
RealtimeModelID: input.RealtimeModelID,
|
|
294
373
|
RealtimeVoice: input.RealtimeVoice,
|
|
295
374
|
TurnMode: this.normalizeTurnMode(input.TurnMode),
|
|
375
|
+
TurnAddressing: this.normalizeTurnAddressing(input.TurnAddressing),
|
|
296
376
|
ContextUser: user,
|
|
297
377
|
MetadataProvider: provider,
|
|
298
378
|
});
|
|
@@ -332,7 +412,15 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
332
412
|
if (!user) {
|
|
333
413
|
return false;
|
|
334
414
|
}
|
|
335
|
-
const provider = GetReadWriteProvider(context.providers) as
|
|
415
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
416
|
+
const roomName = LiveKitAgentRoomCoordinator.Instance.GetRoomForBridge(sessionBridgeID);
|
|
417
|
+
if (roomName) {
|
|
418
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
419
|
+
if (!auth.Authorized) {
|
|
420
|
+
LogError(`StopLiveKitAgentRoomSession unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
336
424
|
return await LiveKitAgentRoomCoordinator.Instance.StopAgentRoomSession(sessionBridgeID, 'Explicit', user, provider);
|
|
337
425
|
} catch (error) {
|
|
338
426
|
LogError(`StopLiveKitAgentRoomSession failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -358,7 +446,12 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
358
446
|
if (!user) {
|
|
359
447
|
return false;
|
|
360
448
|
}
|
|
361
|
-
const provider = GetReadWriteProvider(context.providers) as
|
|
449
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
450
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
451
|
+
if (!auth.Authorized) {
|
|
452
|
+
LogError(`EndLiveKitRoom unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
362
455
|
await LiveKitAgentRoomCoordinator.Instance.StopAllAgentsInRoom(roomName, 'Explicit', user, provider);
|
|
363
456
|
return true;
|
|
364
457
|
} catch (error) {
|
|
@@ -367,6 +460,37 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
367
460
|
}
|
|
368
461
|
}
|
|
369
462
|
|
|
463
|
+
/**
|
|
464
|
+
* A room's live turn-taking state — who holds the floor, whether a person is speaking, any pending hand-off,
|
|
465
|
+
* the agent-to-agent loop-cap progress, the most recent floor/backchannel/yield events, and who is seated. The
|
|
466
|
+
* agent test bed polls it. Read-only. `StateJSON` is `null` when no agent is in the room.
|
|
467
|
+
*
|
|
468
|
+
* @param roomName The LiveKit room.
|
|
469
|
+
*/
|
|
470
|
+
@Query(() => LiveKitRoomTurnStateResult)
|
|
471
|
+
async GetLiveKitRoomTurnState(
|
|
472
|
+
@Arg('roomName', () => String) roomName: string,
|
|
473
|
+
@Ctx() context: AppContext = {} as AppContext,
|
|
474
|
+
): Promise<LiveKitRoomTurnStateResult> {
|
|
475
|
+
try {
|
|
476
|
+
const user = this.GetUserFromPayload(context.userPayload);
|
|
477
|
+
if (!user) {
|
|
478
|
+
return { Success: false, ErrorMessage: 'Unable to determine current user.' };
|
|
479
|
+
}
|
|
480
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
481
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
482
|
+
if (!auth.Authorized) {
|
|
483
|
+
return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.' };
|
|
484
|
+
}
|
|
485
|
+
const snapshot = LiveKitAgentRoomCoordinator.Instance.GetRoomTurnState(roomName);
|
|
486
|
+
return { Success: true, StateJSON: snapshot ? JSON.stringify(snapshot) : undefined };
|
|
487
|
+
} catch (error) {
|
|
488
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
489
|
+
LogError(`GetLiveKitRoomTurnState failed: ${msg}`);
|
|
490
|
+
return { Success: false, ErrorMessage: msg };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
370
494
|
/**
|
|
371
495
|
* Lists active Realtime models with the voices each driver supports — the source for the dev model/voice
|
|
372
496
|
* picker (gated client-side by the `Realtime: Advanced Session Controls` authorization). Read-only; returns
|
|
@@ -381,7 +505,7 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
381
505
|
if (!user) {
|
|
382
506
|
return [];
|
|
383
507
|
}
|
|
384
|
-
const provider = GetReadWriteProvider(context.providers) as
|
|
508
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
385
509
|
return await GetRealtimeModelVoices(user, provider);
|
|
386
510
|
} catch (error) {
|
|
387
511
|
LogError(`GetRealtimeModelVoices failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -411,7 +535,12 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
411
535
|
if (!user) {
|
|
412
536
|
return false;
|
|
413
537
|
}
|
|
414
|
-
const provider = GetReadWriteProvider(context.providers) as
|
|
538
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
539
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
540
|
+
if (!auth.Authorized) {
|
|
541
|
+
LogError(`InviteUsersToLiveKitRoom unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
415
544
|
await NotificationEngine.Instance.Config(false, user, provider);
|
|
416
545
|
|
|
417
546
|
const inviter = user.Name?.trim() || user.Email || 'Someone';
|
|
@@ -453,11 +582,15 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
453
582
|
if (!user) {
|
|
454
583
|
return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: '', Status: '' };
|
|
455
584
|
}
|
|
585
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
586
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(input.RoomName, user, provider);
|
|
587
|
+
if (!auth.Authorized) {
|
|
588
|
+
return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.', EgressID: '', Status: '' };
|
|
589
|
+
}
|
|
456
590
|
const info = await new LiveKitEgressService().StartRoomRecording({ RoomName: input.RoomName, Layout: input.Layout });
|
|
457
591
|
|
|
458
592
|
// Best-effort: correlate the live recording with the room's Meeting-Room Conversation (if it exists
|
|
459
593
|
// yet) by stamping its EgressID. Never fail the start on this — the stop-flow resolves/creates it.
|
|
460
|
-
const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
|
|
461
594
|
void CorrelateRecordingStart(input.RoomName, info.EgressID, user, provider);
|
|
462
595
|
|
|
463
596
|
return { Success: true, EgressID: info.EgressID, Status: info.Status };
|
|
@@ -481,8 +614,43 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
481
614
|
if (!user) {
|
|
482
615
|
return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: egressID, Status: '' };
|
|
483
616
|
}
|
|
484
|
-
const
|
|
485
|
-
|
|
617
|
+
const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
|
|
618
|
+
|
|
619
|
+
// Map egress ID to room name: check LiveKit egress service first, fallback to Conversation if available.
|
|
620
|
+
const egressService = new LiveKitEgressService();
|
|
621
|
+
let roomName = await egressService.GetRoomForEgress(egressID);
|
|
622
|
+
if (!roomName && provider) {
|
|
623
|
+
try {
|
|
624
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
625
|
+
const convResult = await rv.RunView<{ ExternalID: string }>(
|
|
626
|
+
{
|
|
627
|
+
EntityName: 'MJ: Conversations',
|
|
628
|
+
ExtraFilter: `EgressID='${EscapeSQLString(egressID)}' AND (IsArchived IS NULL OR IsArchived=0)`,
|
|
629
|
+
Fields: ['ExternalID'],
|
|
630
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
631
|
+
MaxRows: 1,
|
|
632
|
+
ResultType: 'simple',
|
|
633
|
+
},
|
|
634
|
+
user,
|
|
635
|
+
);
|
|
636
|
+
if (convResult.Success && convResult.Results.length > 0 && convResult.Results[0].ExternalID) {
|
|
637
|
+
roomName = convResult.Results[0].ExternalID;
|
|
638
|
+
}
|
|
639
|
+
} catch (err) {
|
|
640
|
+
LogError(`StopLiveKitRecording: error looking up Conversation for egress ID '${egressID}'`, undefined, err);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (roomName) {
|
|
645
|
+
const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
|
|
646
|
+
if (!auth.Authorized) {
|
|
647
|
+
return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.', EgressID: egressID, Status: '' };
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
return { Success: false, ErrorMessage: `Unable to determine room for egress ID '${egressID}'.`, EgressID: egressID, Status: '' };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const info = await egressService.StopRecording(egressID);
|
|
486
654
|
|
|
487
655
|
// Register the completed egress MP4 as a Files row on the Meeting-Room Conversation. Best-effort:
|
|
488
656
|
// any failure (e.g. storage provider not configured) leaves RecordingFileID unset but still
|
|
@@ -504,9 +672,55 @@ export class RealtimeBridgeResolver extends ResolverBase {
|
|
|
504
672
|
}
|
|
505
673
|
}
|
|
506
674
|
|
|
507
|
-
/**
|
|
675
|
+
/**
|
|
676
|
+
* Starts the agent through the handoff-capable room starter when the caller asked for it and the server can do it.
|
|
677
|
+
* Returns the bridge id, or `undefined` to mean "use the standard start" (not requested, no starter, no agent named).
|
|
678
|
+
*/
|
|
679
|
+
private async startWithHandoff(
|
|
680
|
+
input: StartLiveKitAgentRoomSessionInput,
|
|
681
|
+
roomName: string,
|
|
682
|
+
user: UserInfo,
|
|
683
|
+
provider: IMetadataProvider,
|
|
684
|
+
): Promise<string | undefined> {
|
|
685
|
+
if (!input.EnableHandoff) {
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
688
|
+
const starter = RoomHandoffEngine.Instance.Deps.AgentStarter;
|
|
689
|
+
const agentID = input.TargetAgentID?.trim() || input.AgentID?.trim();
|
|
690
|
+
if (!starter || !agentID) {
|
|
691
|
+
LogError('StartLiveKitAgentRoomSession: handoff was requested but this server has no handoff-capable agent starter; starting the agent without it.');
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
const started = await starter({
|
|
695
|
+
RoomName: roomName,
|
|
696
|
+
AgentID: agentID,
|
|
697
|
+
AgentName: input.AgentName?.trim() ?? '',
|
|
698
|
+
CallerLabel: user.Name?.trim() || user.Email || 'Web visitor',
|
|
699
|
+
ContextUser: user,
|
|
700
|
+
Provider: provider,
|
|
701
|
+
RealtimeModelID: input.RealtimeModelID,
|
|
702
|
+
RealtimeVoice: input.RealtimeVoice,
|
|
703
|
+
});
|
|
704
|
+
return started.SessionBridgeID;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Builds a stable, lowercased participant identity from the authenticated user (shared with the handoff engine, which watches for it). */
|
|
508
708
|
private participantIdentity(user: UserInfo): string {
|
|
509
|
-
return
|
|
709
|
+
return LiveKitUserIdentity(user.ID);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Normalizes an addressing-mode string; unknown/absent leaves the coordinator's default (`Auto`). */
|
|
713
|
+
private normalizeTurnAddressing(mode?: string): TurnAddressingMode | undefined {
|
|
714
|
+
switch ((mode ?? '').trim().toLowerCase()) {
|
|
715
|
+
case 'auto':
|
|
716
|
+
return 'Auto';
|
|
717
|
+
case 'modelside':
|
|
718
|
+
return 'ModelSide';
|
|
719
|
+
case 'regex':
|
|
720
|
+
return 'Regex';
|
|
721
|
+
default:
|
|
722
|
+
return undefined;
|
|
723
|
+
}
|
|
510
724
|
}
|
|
511
725
|
|
|
512
726
|
/** Normalizes a turn-mode string to the bridge's accepted values. */
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
FindIgnoredRealtimeConfigKeys,
|
|
49
49
|
ParseRealtimeTypeConfiguration,
|
|
50
50
|
ResolveEffectiveRealtimeConfig,
|
|
51
|
+
ResolveRealtimeCoAgentID,
|
|
52
|
+
FilterAllowedAgentsByCanRun,
|
|
51
53
|
RealtimeAllowedAgent,
|
|
52
54
|
RealtimeDirectActionsConfig,
|
|
53
55
|
REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
|
|
@@ -56,7 +58,7 @@ import {
|
|
|
56
58
|
writeRealtimeRecordingSegment,
|
|
57
59
|
deleteRealtimeRecordingSegments,
|
|
58
60
|
} from '@memberjunction/ai-agents';
|
|
59
|
-
import { AgentExecutionProgressCallback,
|
|
61
|
+
import { AgentExecutionProgressCallback, AppContextSnapshot } from '@memberjunction/ai-core-plus';
|
|
60
62
|
import { ChatMessage, RealtimeToolDefinition } from '@memberjunction/ai';
|
|
61
63
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
62
64
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
@@ -71,32 +73,6 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
|
|
|
71
73
|
*/
|
|
72
74
|
const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
73
75
|
|
|
74
|
-
/**
|
|
75
|
-
* The seeded name of the internal orchestration agent that fronts a target agent in realtime
|
|
76
|
-
* sessions (voice + interactive channels). This is the GLOBAL DEFAULT co-agent — the final step of
|
|
77
|
-
* the co-agent resolution chain (see {@link RealtimeClientSessionResolver.resolveCoAgentID}).
|
|
78
|
-
* Deployments can override it per agent (`AIAgent.DefaultCoAgentID`), per agent type (an
|
|
79
|
-
* `AIAgentCoAgent` row with `TargetAgentTypeID` + `IsDefault`), or per call (the `coAgentId`
|
|
80
|
-
* mutation argument) without
|
|
81
|
-
* touching this seed.
|
|
82
|
-
*/
|
|
83
|
-
const REALTIME_CO_AGENT_NAME = 'Realtime Co-Agent';
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* DEPRECATED legacy seed name of {@link REALTIME_CO_AGENT_NAME}, from before the agent's rename
|
|
87
|
-
* from "Voice Co-Agent" to "Realtime Co-Agent". Deployments that have not re-synced the agent seed
|
|
88
|
-
* still carry this name, so {@link RealtimeClientSessionResolver.resolveGlobalCoAgentID} falls
|
|
89
|
-
* back to it (with a deprecation log) when no agent named {@link REALTIME_CO_AGENT_NAME} exists.
|
|
90
|
-
*/
|
|
91
|
-
const LEGACY_REALTIME_CO_AGENT_NAME = 'Voice Co-Agent';
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The seeded name of the Realtime agent TYPE. Every co-agent candidate (explicit or metadata
|
|
95
|
-
* default) must be an Active agent of this type — a co-agent drives a streaming full-duplex
|
|
96
|
-
* realtime session, which only `RealtimeAgentType`-driven agents support.
|
|
97
|
-
*/
|
|
98
|
-
const REALTIME_AGENT_TYPE_NAME = 'Realtime';
|
|
99
|
-
|
|
100
76
|
/** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
101
77
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
102
78
|
const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
|
|
@@ -414,7 +390,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
414
390
|
* session is created.
|
|
415
391
|
* 2. Resolve the co-agent id via the metadata-driven resolution chain (runtime `coAgentId` →
|
|
416
392
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
417
|
-
* see {@link
|
|
393
|
+
* see {@link ResolveRealtimeCoAgentID} for the full contract.
|
|
418
394
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
419
395
|
* config server-side — this is the authoritative target for all later relays.
|
|
420
396
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
@@ -463,7 +439,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
463
439
|
): Promise<StartRealtimeClientSessionResult> {
|
|
464
440
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
465
441
|
|
|
466
|
-
const coAgentID = await
|
|
442
|
+
const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
|
|
467
443
|
// PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
|
|
468
444
|
// (with the IsDefault row standing in when no runtime target was supplied); zero rows =
|
|
469
445
|
// universal, today's behavior untouched. Resolves the AUTHORITATIVE target id.
|
|
@@ -578,7 +554,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
578
554
|
TargetAgentID: config.targetAgentID,
|
|
579
555
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
580
556
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
581
|
-
AllowedAgents: await
|
|
557
|
+
AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
582
558
|
DirectActions: config.directActions,
|
|
583
559
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
584
560
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
@@ -1250,39 +1226,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1250
1226
|
}
|
|
1251
1227
|
}
|
|
1252
1228
|
|
|
1253
|
-
/**
|
|
1254
|
-
* Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
|
|
1255
|
-
*
|
|
1256
|
-
* {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
|
|
1257
|
-
* with was never gated at all — a model-named colleague resolves straight to a delegated run.
|
|
1258
|
-
* That was survivable while the run carried the caller's own identity, because base-agent
|
|
1259
|
-
* re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
|
|
1260
|
-
* caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
|
|
1261
|
-
* caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
|
|
1262
|
-
* or not — the authorization identity must never depend on the elevation decision.
|
|
1263
|
-
*
|
|
1264
|
-
* `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
|
|
1265
|
-
* closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
|
|
1266
|
-
* A filtered-out colleague is not an error: the delegation layer reports it as "not available in
|
|
1267
|
-
* this session" and lists what remains, which is the same answer the model gets for a typo.
|
|
1268
|
-
*
|
|
1269
|
-
* @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
|
|
1270
|
-
* @param contextUser The ORIGINAL caller — never the elevated run user.
|
|
1271
|
-
* @returns The subset the caller may run, preserving order.
|
|
1272
|
-
*/
|
|
1273
|
-
private async filterAllowedAgentsByCanRun(
|
|
1274
|
-
allowedAgents: RealtimeAllowedAgent[] | undefined,
|
|
1275
|
-
contextUser: UserInfo,
|
|
1276
|
-
): Promise<RealtimeAllowedAgent[] | undefined> {
|
|
1277
|
-
if (!allowedAgents || allowedAgents.length === 0) {
|
|
1278
|
-
return allowedAgents;
|
|
1279
|
-
}
|
|
1280
|
-
const verdicts = await Promise.all(
|
|
1281
|
-
allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')),
|
|
1282
|
-
);
|
|
1283
|
-
return allowedAgents.filter((_, i) => verdicts[i]);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
1229
|
/**
|
|
1287
1230
|
* Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
|
|
1288
1231
|
* (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
|
|
@@ -1525,195 +1468,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1525
1468
|
}
|
|
1526
1469
|
}
|
|
1527
1470
|
|
|
1528
|
-
/**
|
|
1529
|
-
* Resolves the co-agent (the Realtime-type agent that voices the target agent) for a new
|
|
1530
|
-
* client-direct session via the metadata-driven **CO-AGENT RESOLUTION CHAIN** — first match
|
|
1531
|
-
* wins, evaluated in precedence order:
|
|
1532
|
-
*
|
|
1533
|
-
* 1. **Runtime parameter** — the mutation's explicit `coAgentId`. A per-call override; an
|
|
1534
|
-
* invalid candidate (unknown, not Active, or not of the Realtime agent type) **throws**
|
|
1535
|
-
* (fail loud — the caller asked for something specific, mirroring `preferredModelId`).
|
|
1536
|
-
* 2. **Per-agent persona** — the target agent's `AIAgent.DefaultCoAgentID`. An invalid
|
|
1537
|
-
* reference logs a warning and **falls through** to the next step (stale metadata should
|
|
1538
|
-
* degrade gracefully, never break calls).
|
|
1539
|
-
* 3. **Per-type default** — an Active `AIAgentCoAgent` row of Type `'CoAgent'` whose
|
|
1540
|
-
* `TargetAgentTypeID` is the target agent's type and `IsDefault = 1` (lowest `Sequence`
|
|
1541
|
-
* wins a tie). Same tolerant warn-and-fall-through semantics as step 2.
|
|
1542
|
-
* 4. **Global default** — the seeded {@link REALTIME_CO_AGENT_NAME} agent, looked up by name
|
|
1543
|
-
* (with a deprecated fallback to {@link LEGACY_REALTIME_CO_AGENT_NAME}). Throws when absent
|
|
1544
|
-
* entirely (the realtime feature is unconfigured in this deployment).
|
|
1545
|
-
*
|
|
1546
|
-
* Every candidate from steps 1–3 is validated by {@link findValidCoAgent}: it must exist in
|
|
1547
|
-
* {@link AIEngine}'s cached agents, have Status `Active`, and be of the
|
|
1548
|
-
* {@link REALTIME_AGENT_TYPE_NAME} agent type. Step 4 keeps its original name-lookup contract.
|
|
1549
|
-
*
|
|
1550
|
-
* @param targetAgentId The agent the co-agent will voice on behalf of (drives steps 2–3).
|
|
1551
|
-
* @param explicitCoAgentId The runtime `coAgentId` mutation argument, when supplied (step 1).
|
|
1552
|
-
* @returns The resolved co-agent's id (canonical casing from the metadata cache).
|
|
1553
|
-
*/
|
|
1554
|
-
private async resolveCoAgentID(
|
|
1555
|
-
targetAgentId: string | undefined,
|
|
1556
|
-
explicitCoAgentId: string | undefined,
|
|
1557
|
-
contextUser: UserInfo,
|
|
1558
|
-
provider: IMetadataProvider,
|
|
1559
|
-
): Promise<string> {
|
|
1560
|
-
await AIEngine.Instance.Config(false, contextUser, provider);
|
|
1561
|
-
|
|
1562
|
-
// Step 1 — explicit runtime parameter: fail LOUD on any problem.
|
|
1563
|
-
if (explicitCoAgentId) {
|
|
1564
|
-
const { agent, problem } = this.findValidCoAgent(explicitCoAgentId);
|
|
1565
|
-
if (!agent) {
|
|
1566
|
-
throw new Error(`Invalid coAgentId '${explicitCoAgentId}': ${problem}`);
|
|
1567
|
-
}
|
|
1568
|
-
return agent.ID;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
const targetAgent = targetAgentId
|
|
1572
|
-
? (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, targetAgentId))
|
|
1573
|
-
: undefined;
|
|
1574
|
-
|
|
1575
|
-
// Step 2 — the target agent's own DefaultCoAgentID (per-agent persona): warn + fall through.
|
|
1576
|
-
const fromAgent = this.resolveMetadataDefault(
|
|
1577
|
-
targetAgent?.DefaultCoAgentID,
|
|
1578
|
-
`agent '${targetAgent?.Name}' (DefaultCoAgentID)`,
|
|
1579
|
-
);
|
|
1580
|
-
if (fromAgent) {
|
|
1581
|
-
return fromAgent;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// Step 3 — the type-level AIAgentCoAgent default row (per-type default): warn + fall through.
|
|
1585
|
-
const agentType = targetAgent?.TypeID
|
|
1586
|
-
? (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, targetAgent.TypeID))
|
|
1587
|
-
: undefined;
|
|
1588
|
-
const typeDefaultCoAgentID = agentType
|
|
1589
|
-
? this.findTypeDefaultCoAgentID(agentType.ID, contextUser, provider)
|
|
1590
|
-
: undefined;
|
|
1591
|
-
const fromType = this.resolveMetadataDefault(
|
|
1592
|
-
await typeDefaultCoAgentID,
|
|
1593
|
-
`agent type '${agentType?.Name}' (AIAgentCoAgent type-default row)`,
|
|
1594
|
-
);
|
|
1595
|
-
if (fromType) {
|
|
1596
|
-
return fromType;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
// Step 4 — global default: the seeded Realtime Co-Agent, by name (original behavior).
|
|
1600
|
-
return this.resolveGlobalCoAgentID();
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
/**
|
|
1604
|
-
* Chain step 3 lookup: the TYPE-LEVEL default co-agent for an agent type — the Active
|
|
1605
|
-
* `AIAgentCoAgent` row of Type `'CoAgent'` whose `TargetAgentTypeID` matches, with
|
|
1606
|
-
* `IsDefault = 1` preferred and the lowest `Sequence` breaking ties (so a deployment can
|
|
1607
|
-
* stage multiple type-level candidates deterministically). Reads {@link AIEngineBase}'s
|
|
1608
|
-
* cached rows — no RunView. Tolerant: a failed cache read logs and returns `undefined`
|
|
1609
|
-
* (the chain falls through to the global default).
|
|
1610
|
-
*/
|
|
1611
|
-
private async findTypeDefaultCoAgentID(
|
|
1612
|
-
agentTypeID: string,
|
|
1613
|
-
contextUser: UserInfo,
|
|
1614
|
-
provider: IMetadataProvider,
|
|
1615
|
-
): Promise<string | undefined> {
|
|
1616
|
-
try {
|
|
1617
|
-
const engine = await this.configuredAIEngineBase(contextUser, provider);
|
|
1618
|
-
const candidates = (engine.AgentCoAgents ?? [])
|
|
1619
|
-
.filter((r) =>
|
|
1620
|
-
r.Type === 'CoAgent' &&
|
|
1621
|
-
r.Status === 'Active' &&
|
|
1622
|
-
r.TargetAgentTypeID != null &&
|
|
1623
|
-
UUIDsEqual(r.TargetAgentTypeID, agentTypeID))
|
|
1624
|
-
.sort((a, b) =>
|
|
1625
|
-
(a.IsDefault === b.IsDefault ? 0 : a.IsDefault ? -1 : 1) ||
|
|
1626
|
-
(a.Sequence ?? 0) - (b.Sequence ?? 0));
|
|
1627
|
-
return candidates[0]?.CoAgentID;
|
|
1628
|
-
} catch (error) {
|
|
1629
|
-
LogError(
|
|
1630
|
-
`StartRealtimeClientSession: ${CO_AGENT_ENTITY} cache read failed while resolving the type-level ` +
|
|
1631
|
-
`default co-agent for agent type ${agentTypeID} (${(error as Error).message}) — falling through.`,
|
|
1632
|
-
);
|
|
1633
|
-
return undefined;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
/**
|
|
1638
|
-
* Validates one metadata-level co-agent default (chain steps 2/3). Returns the resolved
|
|
1639
|
-
* co-agent id when the reference is valid; logs a warning and returns `null` (caller falls
|
|
1640
|
-
* through to the next chain step) when the reference is set but invalid — metadata drift must
|
|
1641
|
-
* degrade, not break live calls. A `null`/absent reference returns `null` silently.
|
|
1642
|
-
*/
|
|
1643
|
-
private resolveMetadataDefault(candidateId: string | null | undefined, source: string): string | null {
|
|
1644
|
-
if (!candidateId) {
|
|
1645
|
-
return null;
|
|
1646
|
-
}
|
|
1647
|
-
const { agent, problem } = this.findValidCoAgent(candidateId);
|
|
1648
|
-
if (agent) {
|
|
1649
|
-
return agent.ID;
|
|
1650
|
-
}
|
|
1651
|
-
LogError(
|
|
1652
|
-
`StartRealtimeClientSession: ignoring co-agent default '${candidateId}' from ${source} — ${problem} ` +
|
|
1653
|
-
'Falling through to the next step of the co-agent resolution chain.',
|
|
1654
|
-
);
|
|
1655
|
-
return null;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
/**
|
|
1659
|
-
* Validates a co-agent candidate against {@link AIEngine}'s cached metadata. A valid co-agent
|
|
1660
|
-
* must (a) exist, (b) have Status `Active`, and (c) be of the {@link REALTIME_AGENT_TYPE_NAME}
|
|
1661
|
-
* agent type. Returns the cached agent on success, or a human-readable `problem` on failure —
|
|
1662
|
-
* the CALLER decides whether that's fatal (explicit runtime param) or tolerated (metadata default).
|
|
1663
|
-
*/
|
|
1664
|
-
private findValidCoAgent(candidateId: string): { agent?: MJAIAgentEntityExtended; problem?: string } {
|
|
1665
|
-
const agent = (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, candidateId));
|
|
1666
|
-
if (!agent) {
|
|
1667
|
-
return { problem: 'no agent with that ID exists.' };
|
|
1668
|
-
}
|
|
1669
|
-
if (agent.Status !== 'Active') {
|
|
1670
|
-
return { problem: `agent '${agent.Name}' is not Active (Status: ${agent.Status}).` };
|
|
1671
|
-
}
|
|
1672
|
-
const realtimeType = (AIEngine.Instance.AgentTypes ?? []).find(
|
|
1673
|
-
t => t.Name?.trim().toLowerCase() === REALTIME_AGENT_TYPE_NAME.toLowerCase(),
|
|
1674
|
-
);
|
|
1675
|
-
if (!realtimeType) {
|
|
1676
|
-
return { problem: `the '${REALTIME_AGENT_TYPE_NAME}' agent type is not configured in this deployment.` };
|
|
1677
|
-
}
|
|
1678
|
-
if (!agent.TypeID || !UUIDsEqual(agent.TypeID, realtimeType.ID)) {
|
|
1679
|
-
return { problem: `agent '${agent.Name}' is not of the '${REALTIME_AGENT_TYPE_NAME}' agent type.` };
|
|
1680
|
-
}
|
|
1681
|
-
return { agent };
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* Resolves the seeded Realtime Co-Agent's id from {@link AIEngine}'s cached agents — the GLOBAL
|
|
1686
|
-
* DEFAULT (step 4) of the co-agent resolution chain. The engine is already configured by
|
|
1687
|
-
* {@link resolveCoAgentID}. Looks up the current {@link REALTIME_CO_AGENT_NAME} first, then
|
|
1688
|
-
* falls back to the DEPRECATED {@link LEGACY_REALTIME_CO_AGENT_NAME} (pre-rename seed) with a
|
|
1689
|
-
* deprecation log so un-resynced deployments keep working. Throws a clear error when neither
|
|
1690
|
-
* name is present in metadata.
|
|
1691
|
-
*/
|
|
1692
|
-
private resolveGlobalCoAgentID(): string {
|
|
1693
|
-
const coAgent = this.findAgentByName(REALTIME_CO_AGENT_NAME);
|
|
1694
|
-
if (coAgent) {
|
|
1695
|
-
return coAgent.ID;
|
|
1696
|
-
}
|
|
1697
|
-
const legacy = this.findAgentByName(LEGACY_REALTIME_CO_AGENT_NAME);
|
|
1698
|
-
if (legacy) {
|
|
1699
|
-
LogStatus(
|
|
1700
|
-
`StartRealtimeClientSession: resolved the global co-agent via its DEPRECATED legacy name ` +
|
|
1701
|
-
`'${LEGACY_REALTIME_CO_AGENT_NAME}'. Re-sync the agent seed metadata to rename it to ` +
|
|
1702
|
-
`'${REALTIME_CO_AGENT_NAME}'.`,
|
|
1703
|
-
);
|
|
1704
|
-
return legacy.ID;
|
|
1705
|
-
}
|
|
1706
|
-
throw new Error(
|
|
1707
|
-
`The '${REALTIME_CO_AGENT_NAME}' agent is not configured; cannot start a realtime voice session.`,
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
/** Case/whitespace-insensitive agent lookup by Name in {@link AIEngine}'s cached agents. */
|
|
1712
|
-
private findAgentByName(name: string): MJAIAgentEntityExtended | undefined {
|
|
1713
|
-
const wanted = name.toLowerCase();
|
|
1714
|
-
return (AIEngine.Instance.Agents ?? []).find(a => a.Name?.trim().toLowerCase() === wanted);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
1471
|
/**
|
|
1718
1472
|
* Mints the client session config for a just-created session. On failure, closes the session so
|
|
1719
1473
|
* no half-open record leaks, then throws the service's error message.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The timeout an ad-hoc query gets when the caller names none. */
|
|
2
|
+
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The timeout an ad-hoc query runs with: what the caller asked for (or the default), but never
|
|
6
|
+
* longer than the server's `requestTimeout`, so a caller cannot hold a read-only connection longer
|
|
7
|
+
* than any other request may. `requestTimeoutMs` of 0 or less means the server sets no limit.
|
|
8
|
+
*/
|
|
9
|
+
export function ClampAdhocTimeoutSeconds(requested: number | undefined, requestTimeoutMs: number): number {
|
|
10
|
+
const seconds = requested != null && requested > 0 ? requested : DEFAULT_TIMEOUT_SECONDS;
|
|
11
|
+
return requestTimeoutMs > 0 ? Math.min(seconds, Math.max(1, Math.floor(requestTimeoutMs / 1000))) : seconds;
|
|
12
|
+
}
|