187 lines
5.2 KiB
TypeScript
187 lines
5.2 KiB
TypeScript
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
import type { AgentMessage, AgentSession, IAgentProvider, InjectResult } from "@mosaic/shared";
|
|
import type { Prisma } from "@prisma/client";
|
|
import { PrismaService } from "../../prisma/prisma.service";
|
|
import { AgentProviderRegistry } from "../agents/agent-provider.registry";
|
|
|
|
type MissionControlAction = "inject" | "pause" | "resume" | "kill";
|
|
|
|
const DEFAULT_OPERATOR_ID = "mission-control";
|
|
|
|
export interface AuditLogEntry {
|
|
id: string;
|
|
userId: string;
|
|
sessionId: string;
|
|
provider: string;
|
|
action: string;
|
|
content: string | null;
|
|
metadata: Prisma.JsonValue;
|
|
createdAt: Date;
|
|
}
|
|
|
|
export interface MissionControlAuditLogPage {
|
|
items: AuditLogEntry[];
|
|
total: number;
|
|
page: number;
|
|
pages: number;
|
|
}
|
|
|
|
@Injectable()
|
|
export class MissionControlService {
|
|
constructor(
|
|
private readonly registry: AgentProviderRegistry,
|
|
private readonly prisma: PrismaService
|
|
) {}
|
|
|
|
listSessions(): Promise<AgentSession[]> {
|
|
return this.registry.listAllSessions();
|
|
}
|
|
|
|
async getSession(sessionId: string): Promise<AgentSession> {
|
|
const resolved = await this.registry.getProviderForSession(sessionId);
|
|
if (!resolved) {
|
|
throw new NotFoundException(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
return resolved.session;
|
|
}
|
|
|
|
async getMessages(sessionId: string, limit?: number, before?: string): Promise<AgentMessage[]> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
return provider.getMessages(sessionId, limit, before);
|
|
}
|
|
|
|
async getAuditLog(
|
|
sessionId: string | undefined,
|
|
page: number,
|
|
limit: number
|
|
): Promise<MissionControlAuditLogPage> {
|
|
const normalizedSessionId = sessionId?.trim();
|
|
const where: Prisma.OperatorAuditLogWhereInput =
|
|
normalizedSessionId && normalizedSessionId.length > 0
|
|
? { sessionId: normalizedSessionId }
|
|
: {};
|
|
|
|
const [total, items] = await this.prisma.$transaction([
|
|
this.prisma.operatorAuditLog.count({ where }),
|
|
this.prisma.operatorAuditLog.findMany({
|
|
where,
|
|
orderBy: { createdAt: "desc" },
|
|
skip: (page - 1) * limit,
|
|
take: limit,
|
|
}),
|
|
]);
|
|
|
|
return {
|
|
items,
|
|
total,
|
|
page,
|
|
pages: total === 0 ? 0 : Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
async injectMessage(
|
|
sessionId: string,
|
|
message: string,
|
|
operatorId = DEFAULT_OPERATOR_ID
|
|
): Promise<InjectResult> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
const result = await provider.injectMessage(sessionId, message);
|
|
|
|
await this.writeOperatorAuditLog({
|
|
sessionId,
|
|
providerId: provider.providerId,
|
|
operatorId,
|
|
action: "inject",
|
|
content: message,
|
|
payload: { message },
|
|
});
|
|
|
|
return result;
|
|
}
|
|
|
|
async pauseSession(sessionId: string, operatorId = DEFAULT_OPERATOR_ID): Promise<void> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
await provider.pauseSession(sessionId);
|
|
|
|
await this.writeOperatorAuditLog({
|
|
sessionId,
|
|
providerId: provider.providerId,
|
|
operatorId,
|
|
action: "pause",
|
|
payload: {},
|
|
});
|
|
}
|
|
|
|
async resumeSession(sessionId: string, operatorId = DEFAULT_OPERATOR_ID): Promise<void> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
await provider.resumeSession(sessionId);
|
|
|
|
await this.writeOperatorAuditLog({
|
|
sessionId,
|
|
providerId: provider.providerId,
|
|
operatorId,
|
|
action: "resume",
|
|
payload: {},
|
|
});
|
|
}
|
|
|
|
async killSession(
|
|
sessionId: string,
|
|
force = true,
|
|
operatorId = DEFAULT_OPERATOR_ID
|
|
): Promise<void> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
await provider.killSession(sessionId, force);
|
|
|
|
await this.writeOperatorAuditLog({
|
|
sessionId,
|
|
providerId: provider.providerId,
|
|
operatorId,
|
|
action: "kill",
|
|
payload: { force },
|
|
});
|
|
}
|
|
|
|
async streamMessages(sessionId: string): Promise<AsyncIterable<AgentMessage>> {
|
|
const { provider } = await this.getProviderForSessionOrThrow(sessionId);
|
|
return provider.streamMessages(sessionId);
|
|
}
|
|
|
|
private async getProviderForSessionOrThrow(
|
|
sessionId: string
|
|
): Promise<{ provider: IAgentProvider; session: AgentSession }> {
|
|
const resolved = await this.registry.getProviderForSession(sessionId);
|
|
|
|
if (!resolved) {
|
|
throw new NotFoundException(`Session ${sessionId} not found`);
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
private toJsonValue(value: Record<string, unknown>): Prisma.InputJsonValue {
|
|
return value as Prisma.InputJsonValue;
|
|
}
|
|
|
|
private async writeOperatorAuditLog(params: {
|
|
sessionId: string;
|
|
providerId: string;
|
|
operatorId: string;
|
|
action: MissionControlAction;
|
|
content?: string;
|
|
payload: Record<string, unknown>;
|
|
}): Promise<void> {
|
|
await this.prisma.operatorAuditLog.create({
|
|
data: {
|
|
sessionId: params.sessionId,
|
|
userId: params.operatorId,
|
|
provider: params.providerId,
|
|
action: params.action,
|
|
...(params.content !== undefined ? { content: params.content } : {}),
|
|
metadata: this.toJsonValue({ payload: params.payload }),
|
|
},
|
|
});
|
|
}
|
|
}
|