The autopilot's start line counts its steps and folds open to the latest checkpoints, and its result reads as a card. The composer's plus menu starts an autopilot with the draft as the order and a choice of until done, tomorrow morning or a time. Today lists the autopilots that run, wait or ended within a day, and their step runs no longer show one by one. Autopilot runs send no completion or failure push; only a pause notifies. Claude-Session: https://claude.ai/code/session_01UmSCnoCgXqBp2fU3vwz4o1 Co-authored-by: Claude <noreply@anthropic.com>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
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<AutopilotDeps, "prisma">,
|
|
actor: Actor,
|
|
target: ThreadTarget,
|
|
): Promise<Autopilot | null> {
|
|
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<Autopilot> {
|
|
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<AutopilotDeps, "prisma">,
|
|
actor: Actor,
|
|
now = new Date(),
|
|
): Promise<AutopilotRecent[]> {
|
|
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,
|
|
}));
|
|
}
|