/** * Wire contract for the Home REST endpoints. * * - `GET /v1/home/feed` → `HomeFeedResponse` * - `PATCH /v1/home/feed/:id` → `FeedItem` * - `GET /v1/home/state` → `RelationshipState` * * Holds the canonical feed-item, suggested-prompt, and relationship-state * shapes shared by the daemon route handlers, the on-disk feed-file parser * (`home/feed-types.ts`), and every external client. Defining them here — * rather than inline in the route files — means the daemon, the OpenAPI * generator, and the web/CLI clients all derive from one source and cannot * drift. * * Canonical wire-contract source. Assistant code imports the types * directly from this file via relative paths; external consumers * (web client, gateway, evals) import via `@vellumai/assistant-api`. */ import { z } from "zod"; // --------------------------------------------------------------------------- // Feed item // --------------------------------------------------------------------------- /** High-level kind of feed item — drives which client view renders it. */ export const FeedItemTypeSchema = z.literal("notification"); export type FeedItemType = z.infer; /** User-facing lifecycle of a feed item. */ export const FeedItemStatusSchema = z.enum([ "new", "seen", "acted_on", "dismissed", ]); export type FeedItemStatus = z.infer; /** Visual urgency treatment — controls badge color independently of sort priority. */ export const FeedItemUrgencySchema = z.enum([ "low", "medium", "high", "critical", ]); export type FeedItemUrgency = z.infer; /** Broad category for grouping and filtering feed items. */ export const FeedItemCategorySchema = z.enum([ "security", "scheduling", "background", "email", "system", ]); export type FeedItemCategory = z.infer; /** * Producer of a feed item's source conversation — lets clients filter the * activity feed by what generated each notification: the periodic * heartbeat, a memory-consolidation pass, a recurring schedule, an * auto-analysis run, etc. Derived at read time from the source * conversation's `source` column (see `home/feed-source-enrichment.ts`). * Individual schedules are distinguished by `sourceKey`/`sourceLabel`, not * this coarse type. */ export const FeedItemSourceTypeSchema = z.enum([ "heartbeat", "memory_consolidation", "schedule", "auto_analysis", "user", "other", ]); export type FeedItemSourceType = z.infer; /** * A single action button attached to a feed item. * * `prompt` is the pre-seeded user message the action sends to the * assistant when triggered — the HTTP route creates a new conversation * with this prompt as the first user turn. */ export const FeedActionSchema = z.object({ id: z.string(), label: z.string(), prompt: z.string(), }); export type FeedAction = z.infer; /** Which detail panel the client should open for this feed item. */ export const FeedItemDetailPanelKindSchema = z.enum([ "emailDraft", "documentPreview", "permissionChat", "paymentAuth", "toolPermission", "updatesList", ]); export type FeedItemDetailPanelKind = z.infer< typeof FeedItemDetailPanelKindSchema >; /** Server-driven detail panel descriptor attached to a feed item. */ export const FeedItemDetailPanelSchema = z.object({ kind: FeedItemDetailPanelKindSchema, }); export type FeedItemDetailPanel = z.infer; /** * A single item rendered in the Home feed. * * Notes: * - `priority` must be an integer in [0, 100]; string numerics * (e.g. `"5"`) are rejected — we want deterministic ordering and * silent coercion tends to mask writer bugs. * - `status` defaults to `"new"` so the writer does not need to set it * on every append. * - `createdAt` is the writer-record time (distinct from `timestamp`, * the event time). Used for TTL sweeps and stable ordering. * - `expiresAt` is an absolute ISO-8601 expiry timestamp. * - `title` is optional — clients fall back to `summary` when a row has * no header. */ export const FeedItemSchema = z.object({ id: z.string(), type: FeedItemTypeSchema, priority: z.number().int().min(0).max(100), title: z.string().optional(), summary: z.string(), timestamp: z.string(), status: FeedItemStatusSchema.default("new"), expiresAt: z.string().optional(), actions: z.array(FeedActionSchema).optional(), urgency: FeedItemUrgencySchema.optional(), conversationId: z.string().optional(), detailPanel: FeedItemDetailPanelSchema.optional(), category: FeedItemCategorySchema.optional(), noteworthy: z.boolean().optional(), fromAssistant: z.boolean().optional(), metadata: z.record(z.string(), z.unknown()).optional(), // Source-conversation classification, enriched at read time. `sourceKey` // is the stable filter id — `schedule:` for schedules so each filters // separately, otherwise the `sourceType`. `sourceLabel` is the display // string (a schedule's name, or a static label like "Heartbeat"). sourceType: FeedItemSourceTypeSchema.optional(), sourceKey: z.string().optional(), sourceLabel: z.string().optional(), createdAt: z.string(), }); export type FeedItem = z.infer; // --------------------------------------------------------------------------- // Suggested prompt // --------------------------------------------------------------------------- /** * Origin of a suggested prompt — whether it was deterministically derived * (e.g. from a missing OAuth connection) or generated by the assistant. */ export const SuggestedPromptSourceSchema = z.enum([ "deterministic", "assistant", ]); export type SuggestedPromptSource = z.infer; /** A prompt suggestion shown at the top of the Home page. */ export const SuggestedPromptSchema = z.object({ id: z.string(), label: z.string(), icon: z.string().optional(), prompt: z.string(), source: SuggestedPromptSourceSchema, }); export type SuggestedPrompt = z.infer; // --------------------------------------------------------------------------- // GET /v1/home/feed // --------------------------------------------------------------------------- /** Greeting + relative time-away label + new-item count banner. */ export const ContextBannerSchema = z.object({ greeting: z.string(), timeAwayLabel: z.string(), newCount: z.number().int().min(0), }); export type ContextBanner = z.infer; export const HomeFeedResponseSchema = z.object({ items: z.array(FeedItemSchema), updatedAt: z.string(), contextBanner: ContextBannerSchema, suggestedPrompts: z.array(SuggestedPromptSchema), }); export type HomeFeedResponse = z.infer; // --------------------------------------------------------------------------- // GET /v1/home/state // --------------------------------------------------------------------------- export const FactSchema = z.object({ id: z.string(), category: z.enum(["voice", "world", "priorities"]), text: z.string(), confidence: z.enum(["strong", "uncertain"]), source: z.enum(["onboarding", "inferred"]), }); export type Fact = z.infer; export type FactCategory = Fact["category"]; export type FactConfidence = Fact["confidence"]; export type FactSource = Fact["source"]; export const CapabilitySchema = z.object({ id: z.string(), name: z.string(), description: z.string(), tier: z.enum(["unlocked", "next-up", "earned"]), gate: z.string(), unlockHint: z.string().optional(), ctaLabel: z.string().optional(), }); export type Capability = z.infer; export type CapabilityTier = Capability["tier"]; export const RelationshipStateSchema = z.object({ version: z.literal(1), assistantId: z.string(), tier: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]), progressPercent: z.number(), facts: z.array(FactSchema), capabilities: z.array(CapabilitySchema), conversationCount: z.number(), hatchedDate: z.string(), assistantName: z.string(), userName: z.string().optional(), updatedAt: z.string(), }); export type RelationshipState = z.infer; export type RelationshipTier = RelationshipState["tier"];