cynarAI 54658ee689
feat: autopilot progress, composer start and morning summary (#146)
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>
2026-10-05 04:45:49 +02:00

105 lines
3.6 KiB
TypeScript

import { type Actor, MessageBlock, type RunActivityRow } from "@rakazo/contracts";
import {
ACTIVE_RUN_STATUSES,
botMessageContext,
DELEGATED_TASK_TRIGGER,
taskBlockOf,
} from "@rakazo/core";
import type { PrismaClient } from "@rakazo/db";
const RECENT_LIMIT = 20;
const TERMINAL_STATUSES = ["completed", "failed", "cancelled"] as const;
export function promptSnippet(prompt: string, max = 120): string {
const oneLine = prompt.replace(/\s+/g, " ").trim();
if (oneLine.length <= max) return oneLine;
return `${oneLine.slice(0, max - 1)}…`;
}
export function activityPromptSnippet(
input: { trigger: string; prompt: string; sourceBlocks?: unknown },
max = 120,
): string {
if (input.trigger === DELEGATED_TASK_TRIGGER) {
// Both the specialist's work and the main bot's wake carry the task card; its title says it.
const parsed = MessageBlock.array().safeParse(input.sourceBlocks);
const task = parsed.success ? taskBlockOf(parsed.data) : undefined;
return promptSnippet(task?.title ?? input.prompt, max);
}
if (input.trigger === "autopilot") {
// The continuation prompt is for the model; people know the run by its order.
return promptSnippet(/^Order: (.*)$/m.exec(input.prompt)?.[1] ?? input.prompt, max);
}
if (input.trigger !== "bot_message") return promptSnippet(input.prompt, max);
const parsed = MessageBlock.array().safeParse(input.sourceBlocks);
const message = parsed.success ? botMessageContext(parsed.data) : undefined;
if (!message) return "Message from another agent";
const name = message.fromBotName.trim() || "Another agent";
const label =
message.intent === "result" || message.intent === "status" || message.intent === "fyi"
? `Update from ${name}`
: `${name} asked`;
return promptSnippet(message.text.trim() ? `${label}: ${message.text}` : label, max);
}
export function activityNotificationsEnabled(
groupId: string | null,
notifyOnFinish: boolean,
): boolean {
return groupId !== null || notifyOnFinish;
}
export async function listSpaceRuns(
prisma: PrismaClient,
actor: Actor,
filter: "active" | "recent",
): Promise<RunActivityRow[]> {
const rows = await prisma.run.findMany({
where: {
spaceId: actor.spaceId,
userId: actor.userId,
bot: { archivedAt: null },
...(filter === "active"
? { status: { in: [...ACTIVE_RUN_STATUSES] } }
: { status: { in: [...TERMINAL_STATUSES] } }),
},
include: {
bot: { select: { name: true, archivedAt: true, notifyOnFinish: true } },
task: { select: { prompt: true } },
sourceMessage: { select: { blocks: true } },
thread: {
select: {
groupId: true,
group: { select: { name: true } },
},
},
},
orderBy:
filter === "active"
? [{ updatedAt: "desc" }, { id: "desc" }]
: [{ completedAt: "desc" }, { updatedAt: "desc" }, { id: "desc" }],
take: filter === "recent" ? RECENT_LIMIT : undefined,
});
return rows.map((row) => ({
runId: row.id,
botId: row.botId,
botName: row.bot.name,
groupId: row.thread.groupId,
groupName: row.thread.group?.name ?? null,
threadId: row.threadId,
status: row.status as RunActivityRow["status"],
trigger: row.trigger as RunActivityRow["trigger"],
notificationsEnabled: activityNotificationsEnabled(row.thread.groupId, row.bot.notifyOnFinish),
promptSnippet: activityPromptSnippet({
trigger: row.trigger,
prompt: row.task.prompt,
sourceBlocks: row.sourceMessage?.blocks,
}),
updatedAt: (filter === "recent" && row.completedAt
? row.completedAt
: row.updatedAt
).toISOString(),
}));
}