From 13e82e0a52b4d49aff5476a9924bf4bd4f7a33ca Mon Sep 17 00:00:00 2001 From: sai karthik Date: Wed, 24 Jun 2026 16:53:09 +0530 Subject: [PATCH] Register curator actor --- src/actors/registry.ts | 2 + src/v1/analytics/analytics-actor.ts | 6 +- src/v1/curator/curator-actor.ts | 92 ++++++++++++++++++++++++++++- src/v1/curator/curator-routes.ts | 37 ++++++++---- 4 files changed, 120 insertions(+), 17 deletions(-) diff --git a/src/actors/registry.ts b/src/actors/registry.ts index 961e351..2599d43 100644 --- a/src/actors/registry.ts +++ b/src/actors/registry.ts @@ -7,6 +7,7 @@ import { memoryActor } from "./memory/index.js"; import { growActor } from "./grow/index.js"; import { userEventActor } from "./events/index.js"; import { analyticsActor } from "./analytics/index.js"; +import { curatorActor } from "../v1/curator/curator-actor.js"; import { careerTransitionMissionActor, interviewToOfferMissionActor, @@ -20,6 +21,7 @@ export const registry = setup({ growActor, userEventActor, analyticsActor, + curatorActor, conversationActor, memoryActor, interviewToOfferMissionActor, diff --git a/src/v1/analytics/analytics-actor.ts b/src/v1/analytics/analytics-actor.ts index 27a965b..9bc2504 100644 --- a/src/v1/analytics/analytics-actor.ts +++ b/src/v1/analytics/analytics-actor.ts @@ -7,7 +7,7 @@ import type { Registry } from "../../actors/registry.js"; import { getConversationModel } from "../../actors/conversation/agent.js"; import { db } from "../../db/client.js"; import { growConversationMessages, growEvents } from "../../db/schema.js"; -import { curatorActor } from "../curator/curator-actor.js"; +import { curatorService } from "../curator/curator-actor.js"; import { curatorImprovementSignalSchema } from "../curator/curator-types.js"; let _client: Client | null = null; @@ -57,7 +57,7 @@ export const analyticsTools = { apply_improvement_to_curator: tool({ description: "Apply generated improvement signals to the curator.", inputSchema: z.object({ userId: z.string(), date: z.string(), signals: z.array(curatorImprovementSignalSchema) }), - execute: async ({ userId, date, signals }) => curatorActor.applyImprovementSignals({ userId, date, signals }), + execute: async ({ userId, date, signals }) => curatorService.applyImprovementSignals({ userId, date, signals }), }), }; @@ -103,7 +103,7 @@ export const v1AnalyticsActor = { }, async applyImprovementSignals(input: { userId: string; date: string; signals: z.infer[] }) { - return curatorActor.applyImprovementSignals(input); + return curatorService.applyImprovementSignals(input); }, async runNightly(input: { date: string; userId?: string }) { diff --git a/src/v1/curator/curator-actor.ts b/src/v1/curator/curator-actor.ts index bf6627a..2b76781 100644 --- a/src/v1/curator/curator-actor.ts +++ b/src/v1/curator/curator-actor.ts @@ -1,3 +1,4 @@ +import { actor } from "rivetkit"; import { buildCuratorPlan, buildCuratorSprint, buildCuratorStreak, buildCuratorTasks, buildServiceCurationPreview, todayIsoDate } from "./curator-store.js"; import { curatorPlanSchema, curatorSprintResponseSchema, type CuratorImprovementSignal } from "./curator-types.js"; import { emitCuratorEvent } from "./curator-events.js"; @@ -6,7 +7,22 @@ import { prepareHandoffForTask } from "./curator-tools.js"; import type { CuratorIcpId } from "./curator-icp-playbooks.js"; import { runCuratorOnboardingLoop } from "./curator-onboarding-loop.js"; -export const curatorActor = { +type CuratorActorState = { + userId: string; + planGenerations: number; + sprintReads: number; + taskCompletions: number; + lastActionAt?: string; + lastEventId?: string; +}; + +function touch(c: { state: CuratorActorState }, input: { userId: string }) { + if (c.state.userId && c.state.userId !== input.userId) throw new Error("curatorActor initialized for a different user"); + c.state.userId = input.userId; + c.state.lastActionAt = new Date().toISOString(); +} + +export const curatorService = { async generatePlanRange(input: { userId: string; startDate?: string; endDate?: string; goals?: string[]; forceRegenerate?: boolean }) { const startDate = input.startDate ?? todayIsoDate(); const endDate = input.endDate ?? startDate; @@ -129,3 +145,77 @@ export const curatorActor = { return { tasks: sprint.todayTasks, streak: sprint.streak, sprint }; }, }; + +export const curatorActor = actor({ + options: { name: "Curator Actor", icon: "sparkles", noSleep: true, actionTimeout: 300_000 }, + state: { + userId: "", + planGenerations: 0, + sprintReads: 0, + taskCompletions: 0, + } as CuratorActorState, + actions: { + generatePlanRange: async (c, input: Parameters[0]) => { + touch(c, input); + c.state.planGenerations += 1; + return curatorService.generatePlanRange(input); + }, + getPlan: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.getPlan(input); + }, + previewCuration: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.previewCuration(input); + }, + runOnboardingLoop: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.runOnboardingLoop(input); + }, + getToday: async (c, input: Parameters[0]) => { + touch(c, input); + c.state.sprintReads += 1; + return curatorService.getToday(input); + }, + getSprint: async (c, input: Parameters[0]) => { + touch(c, input); + c.state.sprintReads += 1; + return curatorService.getSprint(input); + }, + chat: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.chat(input); + }, + startTask: async (c, input: Parameters[0]) => { + touch(c, input); + const result = await curatorService.startTask(input); + c.state.lastEventId = result.eventId; + return result; + }, + prepareTaskHandoff: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.prepareTaskHandoff(input); + }, + completeTask: async (c, input: Parameters[0]) => { + touch(c, input); + const result = await curatorService.completeTask(input); + c.state.taskCompletions += 1; + c.state.lastEventId = result.eventId; + return result; + }, + recordServiceImpact: async (c, input: Parameters[0]) => { + touch(c, input); + c.state.lastEventId = input.eventId; + return curatorService.recordServiceImpact(input); + }, + applyImprovementSignals: async (c, input: Parameters[0]) => { + touch(c, input); + return curatorService.applyImprovementSignals(input); + }, + getState: async (c, input: Parameters[0]) => { + touch(c, input); + const state = await curatorService.getState(input); + return { ...state, actorState: c.state }; + }, + }, +}); diff --git a/src/v1/curator/curator-routes.ts b/src/v1/curator/curator-routes.ts index 773813f..5f67de6 100644 --- a/src/v1/curator/curator-routes.ts +++ b/src/v1/curator/curator-routes.ts @@ -1,7 +1,18 @@ import { Hono } from "hono"; import { z } from "zod"; +import { createClient, type Client } from "rivetkit/client"; import { requireUser, type AuthContext } from "../../auth/clerk.js"; -import { curatorActor } from "./curator-actor.js"; +import { config } from "../../config.js"; +import type { Registry } from "../../actors/registry.js"; + +let _client: Client | null = null; +function getClient(): Client { + return (_client ??= createClient(config.rivetClientEndpoint)); +} + +function getCuratorActor(userId: string) { + return getClient().curatorActor.getOrCreate(["user", userId]); +} const chatSchema = z.object({ conversationId: z.string().optional(), @@ -31,12 +42,12 @@ export function v1CuratorRoutes() { goals: z.array(z.string()).optional(), forceRegenerate: z.boolean().optional(), }).parse(await c.req.json().catch(() => ({}))); - return c.json(await curatorActor.generatePlanRange({ userId, ...body })); + return c.json(await getCuratorActor(userId).generatePlanRange({ userId, ...body })); }); app.get("/plan", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.getPlan({ + return c.json(await getCuratorActor(userId).getPlan({ userId, startDate: c.req.query("startDate"), endDate: c.req.query("endDate"), @@ -45,18 +56,18 @@ export function v1CuratorRoutes() { app.get("/today", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.getToday({ userId, date: c.req.query("date") })); + return c.json(await getCuratorActor(userId).getToday({ userId, date: c.req.query("date") })); }); app.get("/sprint", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.getSprint({ userId, date: c.req.query("date") })); + return c.json(await getCuratorActor(userId).getSprint({ userId, date: c.req.query("date") })); }); app.post("/curation/preview", async (c) => { const userId = c.get("userId"); const body = curationPreviewSchema.parse(await c.req.json().catch(() => ({}))); - return c.json(await curatorActor.previewCuration({ userId, ...body })); + return c.json(await getCuratorActor(userId).previewCuration({ userId, ...body })); }); app.post("/onboarding/run", async (c) => { @@ -64,40 +75,40 @@ export function v1CuratorRoutes() { const body = z.object({ completedAt: z.string().optional(), }).parse(await c.req.json().catch(() => ({}))); - return c.json(await curatorActor.runOnboardingLoop({ userId, ...body })); + return c.json(await getCuratorActor(userId).runOnboardingLoop({ userId, ...body })); }); app.post("/chat", async (c) => { const userId = c.get("userId"); const body = chatSchema.parse(await c.req.json()); - return c.json(await curatorActor.chat({ userId, ...body })); + return c.json(await getCuratorActor(userId).chat({ userId, ...body })); }); app.post("/tasks/:taskId/start", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.startTask({ userId, taskId: c.req.param("taskId"), date: c.req.query("date") })); + return c.json(await getCuratorActor(userId).startTask({ userId, taskId: c.req.param("taskId"), date: c.req.query("date") })); }); app.post("/tasks/:taskId/handoff", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.prepareTaskHandoff({ userId, taskId: c.req.param("taskId"), date: c.req.query("date") })); + return c.json(await getCuratorActor(userId).prepareTaskHandoff({ userId, taskId: c.req.param("taskId"), date: c.req.query("date") })); }); app.post("/tasks/:taskId/complete", async (c) => { const userId = c.get("userId"); const body = z.object({ reason: z.string().optional() }).parse(await c.req.json().catch(() => ({}))); - return c.json(await curatorActor.completeTask({ userId, taskId: c.req.param("taskId"), date: c.req.query("date"), reason: body.reason })); + return c.json(await getCuratorActor(userId).completeTask({ userId, taskId: c.req.param("taskId"), date: c.req.query("date"), reason: body.reason })); }); app.post("/events/service-impact", async (c) => { const userId = c.get("userId"); const body = z.object({ eventId: z.string() }).parse(await c.req.json()); - return c.json(await curatorActor.recordServiceImpact({ userId, eventId: body.eventId })); + return c.json(await getCuratorActor(userId).recordServiceImpact({ userId, eventId: body.eventId })); }); app.get("/state", async (c) => { const userId = c.get("userId"); - return c.json(await curatorActor.getState({ userId })); + return c.json(await getCuratorActor(userId).getState({ userId })); }); return app;