import { StringEnum } from "@earendil-works/pi-ai"; import { type Static, Type } from "typebox"; // --------------------------------------------------------------------------- // Tool / command identity — verbatim string boundaries. // Tool name "todo" is the persistence key for branch replay (filtering // `toolResult.toolName === "todo"`) AND the permissions entry at // `templates/pi-permissions.jsonc:26`. DO NOT rename. // --------------------------------------------------------------------------- export const TOOL_NAME = "todo"; export const TOOL_LABEL = "Todo"; export const COMMAND_NAME = "todos"; // --------------------------------------------------------------------------- // User-facing strings (kept stable for /todos UX parity). // --------------------------------------------------------------------------- export const ERR_REQUIRES_INTERACTIVE = "/todos requires interactive mode"; export const MSG_NO_TODOS = "No todos yet. Ask the agent to add some!"; // --------------------------------------------------------------------------- // Public domain types // --------------------------------------------------------------------------- export type TaskStatus = "pending" | "in_progress" | "completed" | "deleted"; export type TaskAction = "create" | "update" | "list" | "get" | "delete" | "clear" | "batch"; export type TaskBatchOperationAction = "create" | "update" | "delete"; export interface Task { id: number; subject: string; description?: string; status: TaskStatus; blockedBy?: number[]; owner?: string; metadata?: Record; } /** * Persistence + replay snapshot. Every successful `todo` tool call returns this * shape under `details`; `state/replay.ts` reads the latest one from the branch * to reconstruct runtime state. Field order and field names are pinned by * cross-version replay compatibility. */ export interface TaskDetails { /** Missing only on snapshots written by versions before schema versioning. */ schemaVersion?: 1; action: TaskAction; params: Record; tasks: Task[]; nextId: number; error?: string; } /** * Open-shape input bag the reducer accepts. Stays an interface so the index * signature (`[key: string]: unknown`) lets the runtime pass through TypeBox * `Static` without `as` casts. */ export interface TaskBatchOperation extends TaskMutationParams { action: TaskBatchOperationAction; } export interface TaskMutationParams { [key: string]: unknown; subject?: string; description?: string; status?: TaskStatus; blockedBy?: number[]; addBlockedBy?: number[]; removeBlockedBy?: number[]; owner?: string; metadata?: Record; id?: number; includeDeleted?: boolean; operations?: TaskBatchOperation[]; } // --------------------------------------------------------------------------- // TypeBox parameter schema — every `description` doubles as LLM-facing prompt // copy. Keep action-specific constraints explicit because the Google-compatible // flat schema cannot express them with Type.Union/Type.Literal discriminators. // --------------------------------------------------------------------------- const TodoBatchOperationSchema = Type.Object({ action: StringEnum(["create", "update", "delete"] as const), subject: Type.Optional(Type.String({ description: "Task subject line (required for create)" })), description: Type.Optional(Type.String({ description: "Long-form task description" })), status: Type.Optional( StringEnum(["pending", "in_progress", "completed", "deleted"] as const, { description: "Initial create status (pending default) or update target", }), ), blockedBy: Type.Optional(Type.Array(Type.Number())), addBlockedBy: Type.Optional(Type.Array(Type.Number())), removeBlockedBy: Type.Optional(Type.Array(Type.Number())), owner: Type.Optional(Type.String()), metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())), id: Type.Optional(Type.Number()), }); export const TodoParamsSchema = Type.Object({ action: StringEnum(["create", "update", "list", "get", "delete", "clear", "batch"] as const), subject: Type.Optional(Type.String({ description: "Task subject line (required for create)" })), description: Type.Optional(Type.String({ description: "Long-form task description" })), status: Type.Optional( StringEnum(["pending", "in_progress", "completed", "deleted"] as const, { description: "Initial status for create (pending default), target status for update, or list filter", }), ), blockedBy: Type.Optional( Type.Array(Type.Number(), { description: "Initial blockedBy ids (create only)", }), ), addBlockedBy: Type.Optional( Type.Array(Type.Number(), { description: "Task ids to add to blockedBy (update only, additive merge)", }), ), removeBlockedBy: Type.Optional( Type.Array(Type.Number(), { description: "Task ids to remove from blockedBy (update only, additive merge)", }), ), owner: Type.Optional(Type.String({ description: "Agent/owner assigned to this task" })), metadata: Type.Optional( Type.Record(Type.String(), Type.Unknown(), { description: "Arbitrary metadata; pass null value for a key to delete that key on update", }), ), id: Type.Optional( Type.Number({ description: "Task id (required for update, get, delete)", }), ), includeDeleted: Type.Optional( Type.Boolean({ description: "If true, list action returns deleted (tombstoned) tasks as well. Default: false.", }), ), operations: Type.Optional( Type.Array(TodoBatchOperationSchema, { minItems: 1, maxItems: 50, description: "Ordered atomic create/update/delete operations; each sees prior results and all roll back if one fails. Complete or re-queue the active task before starting another.", }), ), }); export type TodoParams = Static;