Stabilize curator handoff generation

This commit is contained in:
Sai-karthik
2026-06-15 12:50:25 +00:00
parent 2ccc0ea48d
commit 89e1be4b12
3 changed files with 51 additions and 14 deletions

View File

@@ -1,4 +1,4 @@
import { Output, generateText } from "ai";
import { generateText } from "ai";
import { z } from "zod";
import { getConversationModel } from "../actors/conversation/agent.js";
import { config } from "../config.js";
@@ -20,7 +20,7 @@ const feedSchema = z.object({
notifications: z.array(notificationSchema).min(6).max(24),
});
const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 8000);
const HOME_FEED_AGENT_TIMEOUT_MS = Number(process.env.HOME_FEED_AGENT_TIMEOUT_MS ?? 20000);
export type AgentHomeNotification = z.infer<typeof notificationSchema>;
@@ -54,6 +54,18 @@ function stableId(prefix: string, index: number) {
return `${prefix}-${index + 1}`;
}
function parseJsonObject(text: string) {
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
try {
return JSON.parse(cleaned);
} catch {
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) throw new Error("home_feed_agent_invalid_json");
return JSON.parse(cleaned.slice(start, end + 1));
}
}
export async function refineHomeNotificationsWithAgent(input: {
userId: string;
context: Record<string, unknown>;
@@ -64,8 +76,11 @@ export async function refineHomeNotificationsWithAgent(input: {
try {
const result = await generateText({
model: getConversationModel(),
output: Output.object({ schema: feedSchema }),
system: SYSTEM,
system: [
SYSTEM,
"Return JSON only. Shape: {\"notifications\": [...]}. Do not use markdown.",
"Use ASCII punctuation only.",
].join("\n"),
timeout: HOME_FEED_AGENT_TIMEOUT_MS,
prompt: JSON.stringify({
task: "Create coherent GrowQR home dashboard notifications from the provided service context and deterministic candidates.",
@@ -75,8 +90,9 @@ export async function refineHomeNotificationsWithAgent(input: {
}),
});
const parsed = feedSchema.parse(parseJsonObject(result.text));
const now = new Date().toISOString();
return result.output.notifications.map((n, index) => ({
return parsed.notifications.map((n, index) => ({
...n,
href: sanitizeHref(n.href, n.moduleId),
urgency: n.urgency as HomeUrgency,

View File

@@ -1,4 +1,4 @@
import { generateObject, generateText, tool } from "ai";
import { generateText, tool } from "ai";
import { z } from "zod";
import { desc, eq } from "drizzle-orm";
import { createClient, type Client } from "rivetkit/client";
@@ -21,6 +21,18 @@ const signalsSchema = z.object({
})).max(5),
});
function parseJsonObject(text: string) {
const cleaned = text.trim().replace(/^```(?:json)?/i, "").replace(/```$/i, "").trim();
try {
return JSON.parse(cleaned);
} catch {
const start = cleaned.indexOf("{");
const end = cleaned.lastIndexOf("}");
if (start === -1 || end === -1 || end <= start) throw new Error("analytics_actor_invalid_json");
return JSON.parse(cleaned.slice(start, end + 1));
}
}
export const analyticsTools = {
read_platform_events: tool({
description: "Read latest platform events.",
@@ -66,13 +78,17 @@ export const v1AnalyticsActor = {
const events = await db.select().from(growEvents).where(eq(growEvents.userId, input.userId)).orderBy(desc(growEvents.occurredAt)).limit(80);
const messages = await db.select().from(growConversationMessages).where(eq(growConversationMessages.userId, input.userId)).orderBy(desc(growConversationMessages.createdAt)).limit(40);
try {
const result = await generateObject({
const result = await generateText({
model: getConversationModel(),
schema: signalsSchema,
system: "You are the GrowQR V1 Analytics Actor. Generate small overnight improvement signals for the Curator. Use ASCII punctuation.",
system: [
"You are the GrowQR V1 Analytics Actor. Generate small overnight improvement signals for the Curator.",
"Return JSON only. Shape: {\"signals\": [...]}. Do not use markdown.",
"Use ASCII punctuation.",
].join("\n"),
prompt: JSON.stringify({ date: input.date, events, messages }).slice(0, 20000),
});
return result.object.signals.map((signal) => curatorImprovementSignalSchema.parse({ ...signal, userId: input.userId, date: input.date }));
const parsed = signalsSchema.parse(parseJsonObject(result.text));
return parsed.signals.map((signal) => curatorImprovementSignalSchema.parse({ ...signal, userId: input.userId, date: input.date }));
} catch {
return [curatorImprovementSignalSchema.parse({
id: `improvement:${input.userId}:${input.date}:streak`,

View File

@@ -219,7 +219,12 @@ function firstTurnPrompt(input: {
function isExplicitHandoffRequest(text: string) {
const trimmed = text.trim();
if (/^start$/i.test(trimmed)) return false;
return /\b(start|open|launch|begin|set up|setup|create|generate)\b/i.test(trimmed);
return /\b(start|open|launch|begin|set up|setup|create|generate|room|ready|go|give)\b/i.test(trimmed);
}
function shouldPrepareServiceHandoff(status: CuratorSubtaskStatusUpdate, task?: Awaited<ReturnType<typeof buildCuratorTasks>>[number]) {
if (!task?.serviceId) return false;
return status.status === "ready_to_capture" || status.status === "handoff_ready";
}
async function evaluateSubtaskStatus(input: {
@@ -404,7 +409,7 @@ export async function runCuratorChat(input: {
reply,
history: conversationHistory,
});
if (task?.serviceId && isExplicitHandoffRequest(latest)) {
if (task?.serviceId && (isExplicitHandoffRequest(latest) || statusUpdate.status === "ready_to_capture")) {
statusUpdate = {
status: "handoff_ready",
summary: `${task.serviceName} setup is ready. Use the action below to open it.`,
@@ -412,8 +417,8 @@ export async function runCuratorChat(input: {
};
}
const handoff = task?.serviceId && statusUpdate.status === "handoff_ready"
? await prepareHandoffForTask(input.userId, task, task.serviceId)
const handoff = shouldPrepareServiceHandoff(statusUpdate, task)
? await prepareHandoffForTask(input.userId, task!, task!.serviceId)
: undefined;
if (statusUpdate.status !== "needs_more_context") {