import { ORPCError } from "@orpc/server"; import type { JobPublisher, SandboxProvider } from "@rakazo/adapter-kit"; import { autopilotView, endAutopilot, findOpenAutopilot, kickAutopilot, resumeAutopilot, startAutopilot, } from "@rakazo/adapters"; import type { Actor, Autopilot, AutopilotRecent } from "@rakazo/contracts"; import { AUTOPILOT_RECENT_MS } from "@rakazo/contracts"; import type { PrismaClient, ThreadEvents } from "@rakazo/db"; import { IsolationError } from "@rakazo/db"; import type { ThreadTarget } from "./thread-target.js"; import { groupDefaultBotId, stopThreadRuns } from "./thread-target.js"; type AutopilotDeps = { prisma: PrismaClient; events: ThreadEvents; jobs: JobPublisher; sandbox: SandboxProvider; }; function owner(actor: Actor, target: ThreadTarget) { const botId = target.kind === "bot" ? target.botId : groupDefaultBotId(target); if (!botId) throw new IsolationError(); return { spaceId: actor.spaceId, userId: actor.userId, botId, threadId: target.threadId }; } export async function getThreadAutopilot( deps: Pick, actor: Actor, target: ThreadTarget, ): Promise { const open = await findOpenAutopilot(deps.prisma, owner(actor, target)); return open ? autopilotView(open) : null; } /** Start from the composer: the first run begins at once. */ export async function startThreadAutopilot( deps: AutopilotDeps, actor: Actor, target: ThreadTarget, input: { goal: string; focus?: string; until?: string }, ): Promise { const started = await startAutopilot(deps, owner(actor, target), { goal: input.goal, focus: input.focus, until: input.until ? new Date(input.until) : undefined, }); if ("error" in started) throw new ORPCError("CONFLICT", { message: started.error }); await kickAutopilot(deps, started.autopilot.id); return autopilotView(started.autopilot); } /** Stop at once: end the autopilot first so no run starts another, then stop what runs. */ export async function stopThreadAutopilot( deps: AutopilotDeps, actor: Actor, target: ThreadTarget, ): Promise<{ ok: true }> { const open = await findOpenAutopilot(deps.prisma, owner(actor, target)); if (open && (await endAutopilot(deps, open.id, { status: "stopped" }))) { await stopThreadRuns(deps, actor, target); } return { ok: true }; } export async function resumeThreadAutopilot( deps: AutopilotDeps, actor: Actor, target: ThreadTarget, ): Promise<{ ok: true }> { const open = await findOpenAutopilot(deps.prisma, owner(actor, target)); if (open?.status !== "paused" || !(await resumeAutopilot(deps, open.id))) { throw new ORPCError("CONFLICT", { message: "This autopilot is not paused." }); } return { ok: true }; } /** Today's view: the user's open autopilots in this space and those that ended within a day. */ export async function listRecentAutopilots( deps: Pick, actor: Actor, now = new Date(), ): Promise { const rows = await deps.prisma.autopilot.findMany({ where: { spaceId: actor.spaceId, userId: actor.userId, OR: [ { status: { in: ["active", "paused"] } }, { endedAt: { gte: new Date(now.getTime() - AUTOPILOT_RECENT_MS) } }, ], }, include: { thread: { select: { groupId: true } } }, orderBy: { startedAt: "desc" }, take: 10, }); return rows.map((row) => ({ ...autopilotView(row), spaceId: row.spaceId, groupId: row.thread.groupId, })); }