import { Injectable } from "@nestjs/common"; import type { AgentMessage, AgentMessageRole, AgentSession, AgentSessionList, AgentSessionStatus, IAgentProvider, InjectResult, } from "@mosaic/shared"; import type { AgentConversationMessage, AgentSessionTree } from "@prisma/client"; import { AgentControlService } from "./agent-control.service"; import { AgentMessagesService } from "./agent-messages.service"; import { AgentTreeService } from "./agent-tree.service"; const DEFAULT_SESSION_LIMIT = 50; const DEFAULT_MESSAGE_LIMIT = 50; const MAX_MESSAGE_LIMIT = 200; const STREAM_POLL_INTERVAL_MS = 1000; const INTERNAL_OPERATOR_ID = "internal-provider"; @Injectable() export class InternalAgentProvider implements IAgentProvider { readonly providerId = "internal"; readonly providerType = "internal"; readonly displayName = "Internal Orchestrator"; constructor( private readonly messagesService: AgentMessagesService, private readonly controlService: AgentControlService, private readonly treeService: AgentTreeService ) {} async listSessions(cursor?: string, limit = DEFAULT_SESSION_LIMIT): Promise { const { sessions, total, cursor: nextCursor, } = await this.treeService.listSessions(cursor, limit); return { sessions: sessions.map((session) => this.toAgentSession(session)), total, ...(nextCursor !== undefined ? { cursor: nextCursor } : {}), }; } async getSession(sessionId: string): Promise { const session = await this.treeService.getSession(sessionId); return session ? this.toAgentSession(session) : null; } async getMessages( sessionId: string, limit = DEFAULT_MESSAGE_LIMIT, before?: string ): Promise { const safeLimit = this.normalizeMessageLimit(limit); const skip = this.parseSkip(before); const result = await this.messagesService.getMessages(sessionId, safeLimit, skip); return result.messages.map((message) => this.toAgentMessage(message)); } async injectMessage(sessionId: string, content: string): Promise { await this.controlService.injectMessage(sessionId, INTERNAL_OPERATOR_ID, content); return { accepted: true, }; } async pauseSession(sessionId: string): Promise { await this.controlService.pauseAgent(sessionId, INTERNAL_OPERATOR_ID); } async resumeSession(sessionId: string): Promise { await this.controlService.resumeAgent(sessionId, INTERNAL_OPERATOR_ID); } async killSession(sessionId: string, force = true): Promise { await this.controlService.killAgent(sessionId, INTERNAL_OPERATOR_ID, force); } async *streamMessages(sessionId: string): AsyncIterable { const replayMessages = await this.messagesService.getReplayMessages( sessionId, DEFAULT_MESSAGE_LIMIT ); let lastSeenTimestamp = new Date(); let lastSeenMessageId: string | null = null; for (const message of replayMessages) { yield this.toAgentMessage(message); lastSeenTimestamp = message.timestamp; lastSeenMessageId = message.id; } for (;;) { const newMessages = await this.messagesService.getMessagesAfter( sessionId, lastSeenTimestamp, lastSeenMessageId ); for (const message of newMessages) { yield this.toAgentMessage(message); lastSeenTimestamp = message.timestamp; lastSeenMessageId = message.id; } await this.delay(STREAM_POLL_INTERVAL_MS); } } isAvailable(): Promise { return Promise.resolve(true); } private toAgentSession(session: AgentSessionTree): AgentSession { const metadata = this.toMetadata(session.metadata); return { id: session.sessionId, providerId: this.providerId, providerType: this.providerType, ...(session.taskId !== null ? { label: session.taskId } : {}), status: this.toSessionStatus(session.status), ...(session.parentSessionId !== null ? { parentSessionId: session.parentSessionId } : {}), createdAt: session.spawnedAt, updatedAt: session.completedAt ?? session.spawnedAt, ...(metadata !== undefined ? { metadata } : {}), }; } private toAgentMessage(message: AgentConversationMessage): AgentMessage { const metadata = this.toMetadata(message.metadata); return { id: message.id, sessionId: message.sessionId, role: this.toMessageRole(message.role), content: message.content, timestamp: message.timestamp, ...(metadata !== undefined ? { metadata } : {}), }; } private toSessionStatus(status: string): AgentSessionStatus { switch (status) { case "running": return "active"; case "paused": return "paused"; case "completed": return "completed"; case "failed": case "killed": return "failed"; case "spawning": default: return "idle"; } } private toMessageRole(role: string): AgentMessageRole { switch (role) { case "agent": case "assistant": return "assistant"; case "system": return "system"; case "tool": return "tool"; case "operator": case "user": default: return "user"; } } private normalizeMessageLimit(limit: number): number { const normalized = Number.isFinite(limit) ? Math.trunc(limit) : DEFAULT_MESSAGE_LIMIT; if (normalized < 1) { return 1; } return Math.min(normalized, MAX_MESSAGE_LIMIT); } private parseSkip(before?: string): number { if (!before) { return 0; } const parsed = Number.parseInt(before, 10); if (Number.isNaN(parsed) || parsed < 0) { return 0; } return parsed; } private toMetadata(value: unknown): Record | undefined { if (value !== null && typeof value === "object" && !Array.isArray(value)) { return value as Record; } return undefined; } private async delay(ms: number): Promise { await new Promise((resolve) => { setTimeout(resolve, ms); }); } }