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

81 lines
3.0 KiB
TypeScript

import * as z from "zod";
import { Id } from "./ids.js";
/** Continuations without a checkpoint, or failed runs in a row, before the autopilot pauses. */
export const AUTOPILOT_IDLE_LIMIT = 3;
export const AUTOPILOT_FAIL_LIMIT = 3;
export const AUTOPILOT_LIMITS = { goal: 500, focus: 200, checkpoint: 300, result: 2000 } as const;
/** Checkpoints kept on the start line; older ones only count toward the step total. */
export const AUTOPILOT_CHECKPOINT_LOG = 50;
export const AutopilotStatusSchema = z.enum(["active", "paused", "finished", "stopped"]);
export type AutopilotStatus = z.infer<typeof AutopilotStatusSchema>;
/**
* A quiet timeline line for an autopilot: when it started (with its goal, end time, focus and
* the approvals granted only for it), paused, stopped or finished. `ended` is set on the start
* line once the autopilot is over, so its Stop control goes away.
*/
export const AutopilotBlockSchema = z.object({
kind: z.literal("autopilot"),
event: z.enum([
"autopilot.started",
"autopilot.paused",
"autopilot.stopped",
"autopilot.finished",
]),
autopilotId: Id,
goal: z.string(),
focus: z.string().optional(),
until: z.string().optional(),
/** Tools or apps the bot may use without asking while this autopilot runs. */
allowed: z.array(z.string()).optional(),
steps: z.number().int().nonnegative().optional(),
/** The latest checkpoints, newest last, on the start line. */
checkpoints: z
.array(z.object({ text: z.string(), at: z.string() }))
.max(AUTOPILOT_CHECKPOINT_LOG)
.optional(),
durationMs: z.number().int().nonnegative().optional(),
/** Why it paused, or what it achieved. */
text: z.string().optional(),
ended: z.boolean().optional(),
});
export type AutopilotBlock = z.infer<typeof AutopilotBlockSchema>;
export const AutopilotSchema = z.object({
id: Id,
botId: Id,
threadId: Id,
goal: z.string(),
focus: z.string().nullable(),
until: z.string().nullable(),
status: AutopilotStatusSchema,
pauseReason: z.string().nullable(),
lastCheckpoint: z.string().nullable(),
nextStep: z.string().nullable(),
result: z.string().nullable(),
stepCount: z.number().int(),
startedAt: z.string(),
endedAt: z.string().nullable(),
});
export type Autopilot = z.infer<typeof AutopilotSchema>;
/** How long a finished or stopped autopilot stays in Today, so the night's work is there at breakfast. */
export const AUTOPILOT_RECENT_MS = 24 * 60 * 60 * 1000;
/** An autopilot in Today: open ones, and those that ended within AUTOPILOT_RECENT_MS. */
export const AutopilotRecentSchema = AutopilotSchema.extend({
spaceId: Id,
groupId: Id.nullable(),
});
export type AutopilotRecent = z.infer<typeof AutopilotRecentSchema>;
export const AutopilotStartInputSchema = z.object({
goal: z.string().trim().min(1).max(AUTOPILOT_LIMITS.goal),
focus: z.string().trim().max(AUTOPILOT_LIMITS.focus).optional(),
/** ISO time to stop at; without it the autopilot runs until the goal is done. */
until: z.string().datetime({ offset: true }).optional(),
});