De-hardcode orchestrator and interaction agent names (#748)
This commit was merged in pull request #748.
This commit is contained in:
@@ -11,8 +11,8 @@ import { SessionsController } from './sessions.controller.js';
|
||||
import { AgentConfigsController } from './agent-configs.controller.js';
|
||||
import { InteractionController } from './interaction.controller.js';
|
||||
import { RoutingController } from './routing/routing.controller.js';
|
||||
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
||||
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
||||
import { DurableSessionRepository } from './durable-session.repository.js';
|
||||
import { DurableSessionService } from './durable-session.service.js';
|
||||
import { CoordModule } from '../coord/coord.module.js';
|
||||
import { McpClientModule } from '../mcp-client/mcp-client.module.js';
|
||||
import { SkillsModule } from '../skills/skills.module.js';
|
||||
@@ -44,8 +44,8 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi
|
||||
RoutingService,
|
||||
RoutingEngineService,
|
||||
SkillLoaderService,
|
||||
TessDurableSessionRepository,
|
||||
TessDurableSessionService,
|
||||
DurableSessionRepository,
|
||||
DurableSessionService,
|
||||
{
|
||||
provide: AGENT_RUNTIME_PROVIDER_REGISTRY,
|
||||
useFactory: createGatewayRuntimeProviderRegistry,
|
||||
@@ -76,7 +76,7 @@ export function createGatewayRuntimeProviderRegistry(): AgentRuntimeProviderRegi
|
||||
RoutingService,
|
||||
RoutingEngineService,
|
||||
SkillLoaderService,
|
||||
TessDurableSessionService,
|
||||
DurableSessionService,
|
||||
RuntimeProviderService,
|
||||
AGENT_RUNTIME_PROVIDER_REGISTRY,
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { RuntimeProviderRequestContext } from './runtime-provider-registry.service.js';
|
||||
|
||||
/** Server-side request for a replay-safe provider message. */
|
||||
export interface TessProviderOutboxDto {
|
||||
export interface ProviderOutboxDto {
|
||||
sessionId: string;
|
||||
idempotencyKey: string;
|
||||
correlationId: string;
|
||||
@@ -6,8 +6,8 @@ import { eq, sql, interactionCheckpoints, interactionInbox } from '@mosaicstack/
|
||||
import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { createPgliteDb, runPgliteMigrations, type DbHandle } from '@mosaicstack/db';
|
||||
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
||||
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
||||
import { DurableSessionRepository } from './durable-session.repository.js';
|
||||
import { DurableSessionService } from './durable-session.service.js';
|
||||
|
||||
const IDENTITY: DurableSessionIdentity = {
|
||||
agentName: 'Nova',
|
||||
@@ -18,7 +18,7 @@ const IDENTITY: DurableSessionIdentity = {
|
||||
runtimeSessionId: 'nova',
|
||||
};
|
||||
|
||||
describe('TessDurableSessionRepository', () => {
|
||||
describe('DurableSessionRepository', () => {
|
||||
let dataDir: string | undefined;
|
||||
let handle: DbHandle;
|
||||
let previousAuthSecret: string | undefined;
|
||||
@@ -48,9 +48,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
});
|
||||
|
||||
it('survives a full PGlite close/reopen mid-session without duplicate inbox or outbox side effects', async () => {
|
||||
const beforeRestart = new DurableSessionCoordinator(
|
||||
new TessDurableSessionRepository(handle.db),
|
||||
);
|
||||
const beforeRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await beforeRestart.create(IDENTITY);
|
||||
await beforeRestart.receive({
|
||||
sessionId: IDENTITY.sessionId,
|
||||
@@ -92,7 +90,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
await handle.close();
|
||||
handle = createPgliteDb(dataDir!);
|
||||
|
||||
const afterRestart = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const afterRestart = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
const recovered = await afterRestart.recover(IDENTITY.sessionId);
|
||||
const resumedHandoff = await afterRestart.resumeHandoff('handoff-before-kill');
|
||||
const handled: string[] = [];
|
||||
@@ -120,7 +118,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('redacts sensitive durable payloads before persistence', async () => {
|
||||
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await coordinator.create(IDENTITY);
|
||||
await coordinator.receive({
|
||||
sessionId: IDENTITY.sessionId,
|
||||
@@ -157,7 +155,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('fails closed when the configured idempotency secret is unavailable', async () => {
|
||||
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await coordinator.create(IDENTITY);
|
||||
const secret = process.env['BETTER_AUTH_SECRET'];
|
||||
delete process.env['BETTER_AUTH_SECRET'];
|
||||
@@ -177,7 +175,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('uses keyed pre-redaction digests to reject distinct sensitive checkpoint payloads', async () => {
|
||||
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await coordinator.create(IDENTITY);
|
||||
const input = {
|
||||
sessionId: IDENTITY.sessionId,
|
||||
@@ -227,7 +225,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('rejects distinct sensitive inbox and outbox payloads under reused idempotency keys', async () => {
|
||||
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await coordinator.create(IDENTITY);
|
||||
const inbox = {
|
||||
sessionId: IDENTITY.sessionId,
|
||||
@@ -255,7 +253,7 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('rejects database inbox and outbox idempotency-key conflicts', async () => {
|
||||
const coordinator = new DurableSessionCoordinator(new TessDurableSessionRepository(handle.db));
|
||||
const coordinator = new DurableSessionCoordinator(new DurableSessionRepository(handle.db));
|
||||
await coordinator.create(IDENTITY);
|
||||
await coordinator.receive({
|
||||
sessionId: IDENTITY.sessionId,
|
||||
@@ -293,10 +291,10 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('does not requeue a live outbox claim during a normal scoped dispatch', async () => {
|
||||
const repository = new TessDurableSessionRepository(handle.db);
|
||||
const repository = new DurableSessionRepository(handle.db);
|
||||
const coordinator = new DurableSessionCoordinator(repository);
|
||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||
const service = new TessDurableSessionService(repository, runtimeProviders as never);
|
||||
const service = new DurableSessionService(repository, runtimeProviders as never);
|
||||
const input = {
|
||||
sessionId: IDENTITY.sessionId,
|
||||
idempotencyKey: 'live-effect',
|
||||
@@ -333,10 +331,10 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('rejects an outbox correlation mismatch before claiming the pending effect', async () => {
|
||||
const repository = new TessDurableSessionRepository(handle.db);
|
||||
const repository = new DurableSessionRepository(handle.db);
|
||||
const coordinator = new DurableSessionCoordinator(repository);
|
||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||
const service = new TessDurableSessionService(repository, runtimeProviders as never);
|
||||
const service = new DurableSessionService(repository, runtimeProviders as never);
|
||||
const input = {
|
||||
sessionId: IDENTITY.sessionId,
|
||||
idempotencyKey: 'mismatch-effect',
|
||||
@@ -366,10 +364,10 @@ describe('TessDurableSessionRepository', () => {
|
||||
}, 30_000);
|
||||
|
||||
it('dispatches only the outbox record bound to the supplied correlation and channel', async () => {
|
||||
const repository = new TessDurableSessionRepository(handle.db);
|
||||
const repository = new DurableSessionRepository(handle.db);
|
||||
const coordinator = new DurableSessionCoordinator(repository);
|
||||
const runtimeProviders = { sendMessage: vi.fn().mockResolvedValue(undefined) };
|
||||
const service = new TessDurableSessionService(repository, runtimeProviders as never);
|
||||
const service = new DurableSessionService(repository, runtimeProviders as never);
|
||||
const first = {
|
||||
sessionId: IDENTITY.sessionId,
|
||||
idempotencyKey: 'scoped-effect-one',
|
||||
@@ -33,7 +33,7 @@ import type {
|
||||
import { DB } from '../database/database.module.js';
|
||||
|
||||
@Injectable()
|
||||
export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
export class DurableSessionRepository implements DurableSessionStore {
|
||||
constructor(@Inject(DB) private readonly db: Db) {}
|
||||
|
||||
async create(identity: DurableSessionIdentity): Promise<void> {
|
||||
@@ -51,7 +51,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
|
||||
const existing = await this.session(identity.sessionId);
|
||||
if (!existing || !sameEnrollmentScope(existing, identity)) {
|
||||
throw new Error(`Durable Tess session identity conflict: ${identity.sessionId}`);
|
||||
throw new Error(`Durable session identity conflict: ${identity.sessionId}`);
|
||||
}
|
||||
// A recovered/re-enrolled runtime can receive a new provider session ID;
|
||||
// the conversation handle and owner scope remain immutable.
|
||||
@@ -135,13 +135,13 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing[0]) throw new Error(`Durable Tess inbox enqueue failed: ${input.idempotencyKey}`);
|
||||
if (!existing[0]) throw new Error(`Durable inbox enqueue failed: ${input.idempotencyKey}`);
|
||||
const entry = toInbox(existing[0]);
|
||||
if (
|
||||
!sameInbox(entry, record) ||
|
||||
!matchesContentDigest(existing[0].contentDigest, input.content)
|
||||
) {
|
||||
throw new Error(`Durable Tess inbox idempotency conflict: ${input.idempotencyKey}`);
|
||||
throw new Error(`Durable inbox idempotency conflict: ${input.idempotencyKey}`);
|
||||
}
|
||||
return { accepted: false, status: entry.status };
|
||||
}
|
||||
@@ -224,14 +224,13 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing[0])
|
||||
throw new Error(`Durable Tess outbox enqueue failed: ${input.idempotencyKey}`);
|
||||
if (!existing[0]) throw new Error(`Durable outbox enqueue failed: ${input.idempotencyKey}`);
|
||||
const entry = toOutbox(existing[0]);
|
||||
if (
|
||||
!sameOutbox(entry, record) ||
|
||||
!matchesContentDigest(existing[0].contentDigest, input.content)
|
||||
) {
|
||||
throw new Error(`Durable Tess outbox idempotency conflict: ${input.idempotencyKey}`);
|
||||
throw new Error(`Durable outbox idempotency conflict: ${input.idempotencyKey}`);
|
||||
}
|
||||
return { accepted: false, status: entry.status };
|
||||
}
|
||||
@@ -338,7 +337,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
!sameCheckpoint(toCheckpoint(existing[0]), checkpoint) ||
|
||||
!matchesCheckpointDigest(existing[0].contentDigest, digest)
|
||||
) {
|
||||
throw new Error(`Durable Tess checkpoint identity conflict: ${input.checkpointId}`);
|
||||
throw new Error(`Durable checkpoint identity conflict: ${input.checkpointId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +359,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
async handoff(input: DurableHandoffInput): Promise<void> {
|
||||
const checkpoint = await this.findCheckpoint(input.sessionId, input.checkpointId);
|
||||
if (!checkpoint) {
|
||||
throw new Error(`Durable Tess handoff checkpoint is unavailable: ${input.checkpointId}`);
|
||||
throw new Error(`Durable handoff checkpoint is unavailable: ${input.checkpointId}`);
|
||||
}
|
||||
const inserted = await this.db
|
||||
.insert(interactionHandoffs)
|
||||
@@ -371,7 +370,7 @@ export class TessDurableSessionRepository implements DurableSessionStore {
|
||||
|
||||
const existing = await this.findHandoff(input.handoffId);
|
||||
if (!existing || !sameHandoff(existing, input)) {
|
||||
throw new Error(`Durable Tess handoff identity conflict: ${input.handoffId}`);
|
||||
throw new Error(`Durable handoff identity conflict: ${input.handoffId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
|
||||
import { DurableSessionCoordinator, type DurableSessionIdentity } from '@mosaicstack/agent';
|
||||
import type { TessProviderOutboxDto } from './tess-durable-session.dto.js';
|
||||
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
||||
import type { ProviderOutboxDto } from './durable-session.dto.js';
|
||||
import { DurableSessionRepository } from './durable-session.repository.js';
|
||||
import {
|
||||
RuntimeProviderService,
|
||||
type RuntimeProviderRequestContext,
|
||||
} from './runtime-provider-registry.service.js';
|
||||
|
||||
/**
|
||||
* Scoped gateway boundary for the canonical Tess state machine. It deliberately
|
||||
* Scoped gateway boundary for the canonical durable session state machine. It deliberately
|
||||
* uses composition: raw state methods cannot be injected into channel, CLI, or
|
||||
* MCP adapters without a server-derived actor/tenant/correlation context.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TessDurableSessionService {
|
||||
export class DurableSessionService {
|
||||
private readonly coordinator: DurableSessionCoordinator;
|
||||
|
||||
constructor(
|
||||
@Inject(TessDurableSessionRepository) repository: TessDurableSessionRepository,
|
||||
@Inject(DurableSessionRepository) repository: DurableSessionRepository,
|
||||
@Inject(RuntimeProviderService) private readonly runtimeProviders: RuntimeProviderService,
|
||||
) {
|
||||
this.coordinator = new DurableSessionCoordinator(repository);
|
||||
@@ -32,12 +32,12 @@ export class TessDurableSessionService {
|
||||
identity.ownerId !== context.actorScope.userId ||
|
||||
identity.tenantId !== context.actorScope.tenantId
|
||||
) {
|
||||
throw new ForbiddenException('Durable Tess enrollment scope mismatch');
|
||||
throw new ForbiddenException('Durable session enrollment scope mismatch');
|
||||
}
|
||||
await this.coordinator.create(identity);
|
||||
}
|
||||
|
||||
async queueProviderSend(input: TessProviderOutboxDto): Promise<void> {
|
||||
async queueProviderSend(input: ProviderOutboxDto): Promise<void> {
|
||||
const snapshot = await this.coordinator.snapshot(input.sessionId);
|
||||
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
|
||||
await this.coordinator.enqueueOutbox({
|
||||
@@ -50,9 +50,9 @@ export class TessDurableSessionService {
|
||||
});
|
||||
}
|
||||
|
||||
async dispatchProviderOutbox(sessionId: string, input: TessProviderOutboxDto): Promise<void> {
|
||||
async dispatchProviderOutbox(sessionId: string, input: ProviderOutboxDto): Promise<void> {
|
||||
if (sessionId !== input.sessionId) {
|
||||
throw new ForbiddenException('Durable Tess outbox session mismatch');
|
||||
throw new ForbiddenException('Durable outbox session mismatch');
|
||||
}
|
||||
const snapshot = await this.coordinator.snapshot(sessionId);
|
||||
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
|
||||
@@ -92,7 +92,7 @@ export class TessDurableSessionService {
|
||||
}
|
||||
|
||||
/** Startup/recovery-only path; normal queue/dispatch methods never requeue live work. */
|
||||
async recoverProviderSession(sessionId: string, input: TessProviderOutboxDto): Promise<void> {
|
||||
async recoverProviderSession(sessionId: string, input: ProviderOutboxDto): Promise<void> {
|
||||
const snapshot = await this.coordinator.snapshot(sessionId);
|
||||
this.assertScope(snapshot.identity.ownerId, snapshot.identity.tenantId, input);
|
||||
await this.coordinator.recover(sessionId);
|
||||
@@ -100,24 +100,24 @@ export class TessDurableSessionService {
|
||||
|
||||
private assertOutboxScope(
|
||||
entry: { kind: string; correlationId: string; channelId: string },
|
||||
input: TessProviderOutboxDto,
|
||||
input: ProviderOutboxDto,
|
||||
): void {
|
||||
if (
|
||||
entry.kind !== 'provider.send' ||
|
||||
entry.correlationId !== input.correlationId ||
|
||||
entry.channelId !== input.context.channelId
|
||||
) {
|
||||
throw new ForbiddenException('Durable Tess outbox scope or correlation mismatch');
|
||||
throw new ForbiddenException('Durable outbox scope or correlation mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
private assertScope(ownerId: string, tenantId: string, input: TessProviderOutboxDto): void {
|
||||
private assertScope(ownerId: string, tenantId: string, input: ProviderOutboxDto): void {
|
||||
if (
|
||||
input.context.actorScope.userId !== ownerId ||
|
||||
input.context.actorScope.tenantId !== tenantId ||
|
||||
input.context.correlationId !== input.correlationId
|
||||
) {
|
||||
throw new ForbiddenException('Durable Tess session scope or correlation mismatch');
|
||||
throw new ForbiddenException('Durable session scope or correlation mismatch');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
RUNTIME_PROVIDER_AUDIT_SINK,
|
||||
RuntimeProviderAuditService,
|
||||
} from './runtime-provider-registry.service.js';
|
||||
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
||||
import { TessDurableSessionRepository } from './tess-durable-session.repository.js';
|
||||
import { DurableSessionService } from './durable-session.service.js';
|
||||
import { DurableSessionRepository } from './durable-session.repository.js';
|
||||
import { AgentService } from './agent.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { ProviderCredentialsService } from './provider-credentials.service.js';
|
||||
@@ -88,9 +88,9 @@ describe('Hermes runtime provider reachability', (): void => {
|
||||
.useValue({ record: vi.fn().mockResolvedValue(undefined) })
|
||||
.overrideProvider(RUNTIME_APPROVAL_VERIFIER)
|
||||
.useValue({ consume: vi.fn().mockResolvedValue(false) })
|
||||
.overrideProvider(TessDurableSessionService)
|
||||
.overrideProvider(DurableSessionService)
|
||||
.useValue({})
|
||||
.overrideProvider(TessDurableSessionRepository)
|
||||
.overrideProvider(DurableSessionRepository)
|
||||
.useValue({})
|
||||
.overrideProvider(AgentService)
|
||||
.useValue({})
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Observable } from 'rxjs';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import { scopeFromUser, type AuthenticatedUserLike } from '../auth/session-scope.js';
|
||||
import { TessDurableSessionService } from './tess-durable-session.service.js';
|
||||
import { DurableSessionService } from './durable-session.service.js';
|
||||
import { RuntimeApprovalDeniedFilter } from './runtime-approval-denied.filter.js';
|
||||
import {
|
||||
RuntimeProviderService,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
export class InteractionController {
|
||||
constructor(
|
||||
@Inject(RuntimeProviderService) private readonly runtime: RuntimeProviderService,
|
||||
@Inject(TessDurableSessionService) private readonly durable: TessDurableSessionService,
|
||||
@Inject(DurableSessionService) private readonly durable: DurableSessionService,
|
||||
) {}
|
||||
|
||||
@Get('sessions')
|
||||
|
||||
Reference in New Issue
Block a user