Fix curator subtask chat state

This commit is contained in:
Sai-karthik
2026-06-15 08:36:21 +00:00
parent 4b23dd0905
commit 368410e9d8
2 changed files with 97 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ import { addMessagePg, createConversationPg, ensureCuratorTaskConversationPg, ge
import { generateConversationResponse, getConversationModel } from "../../actors/conversation/agent.js";
import { buildCuratorTasks, todayIsoDate } from "./curator-store.js";
import { emitCuratorEvent } from "./curator-events.js";
import type { CuratorChatResponse } from "./curator-types.js";
import type { CuratorChatResponse, CuratorSubtaskStatusUpdate } from "./curator-types.js";
const chatExtractSchema = z.object({
summary: z.string(),
@@ -13,6 +13,13 @@ const chatExtractSchema = z.object({
shouldPrepareHandoff: z.boolean().default(false),
});
const subtaskStatusUpdateSchema = z.object({
status: z.enum(["needs_more_context", "ready_to_capture", "handoff_ready"]),
summary: z.string().min(1).max(280),
confidence: z.number().min(0).max(1).default(0.5),
nextMissingInfo: z.string().max(180).optional(),
});
function parseJsonObject(text: string) {
const trimmed = text.trim();
try {
@@ -102,10 +109,12 @@ function curatorSystemAddendum(input: {
"Carry state from the conversation history. If the user gives a short answer like a role name, accept it and ask for the next missing slot.",
"Do not ask the same question twice. Do not output checklist items as separate baked chat messages.",
"For target-role tasks, collect target role, current background, constraints, then offer a resume or interview handoff.",
"For service work, use Conversation Actor tools to prepare handoffs. Do not mark work complete unless a service or platform event exists.",
"For service work, use Conversation Actor tools to prepare handoffs only after the focused subtask has enough context.",
"Never say: What should I capture next. Ask a concrete conversational question tied to the task.",
"If a curator subtask is provided, focus on that subtask only. Do not answer as if another subtask was clicked.",
"When the user answers the focused subtask, acknowledge the captured detail and point them to the next missing slot instead of asking the same subtask again.",
"Do not ask about another subtask, another mission, another service, or a later checklist item from this modal.",
"When the user has answered the focused subtask enough, summarize what was captured and stop. Do not ask the next subtask question.",
"If more detail is needed, ask exactly one follow-up question for the focused subtask only.",
`Date: ${input.date}`,
`Curator task id: ${input.taskId ?? "none"}`,
`Focused subtask index: ${Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : "none"}`,
@@ -231,6 +240,55 @@ function firstTurnPrompt(input: {
].join("\n");
}
async function evaluateSubtaskStatus(input: {
task?: Awaited<ReturnType<typeof buildCuratorTasks>>[number];
subtask?: string;
subtaskIndex?: number;
latest: string;
reply: string;
history: CuratorMessage[];
}): Promise<CuratorSubtaskStatusUpdate> {
if (!input.subtask || /^start$/i.test(input.latest.trim())) {
return { status: "needs_more_context", summary: "Subtask opened.", confidence: 0.2 };
}
try {
const result = await generateText({
model: getConversationModel(),
system: [
"You are the GrowQR V1 Curator Actor state evaluator.",
"Return JSON only. Do not wrap it in markdown.",
"Shape: {\"status\": \"needs_more_context\" | \"ready_to_capture\" | \"handoff_ready\", \"summary\": string, \"confidence\": number, \"nextMissingInfo\"?: string}.",
"Evaluate only the focused subtask. Ignore other missions, other subtasks, and later checklist items.",
"Use ready_to_capture only when the latest user answer directly satisfies the focused subtask.",
"Use needs_more_context if the assistant reply asks another question or if the answer is too vague for this exact subtask.",
"Use handoff_ready only when the focused subtask explicitly asks to open or preview a service and the service setup details are present.",
"Never mark ready just because one message exists.",
"Use ASCII punctuation only.",
].join("\n"),
prompt: [
`Task title: ${input.task?.title ?? "none"}`,
`Task service: ${input.task?.serviceName ?? "none"}`,
`Focused subtask index: ${Number.isInteger(input.subtaskIndex) ? input.subtaskIndex : "none"}`,
`Focused subtask: ${input.subtask}`,
`Task context: ${input.task?.contextNarrative ?? "none"}`,
`All task subtasks: ${input.task?.subtasks.join(" | ") ?? "none"}`,
`Latest user answer: ${input.latest}`,
`Assistant reply: ${input.reply}`,
`Visible history: ${input.history.map((message) => `${message.role}: ${message.content}`).join("\n")}`,
].join("\n"),
});
return subtaskStatusUpdateSchema.parse(parseJsonObject(result.text));
} catch (error) {
console.warn("curator status evaluation failed; keeping subtask open", {
taskId: input.task?.id,
subtaskIndex: input.subtaskIndex,
error: error instanceof Error ? error.message : String(error),
});
return { status: "needs_more_context", summary: "The curator needs one more answer before updating this subtask.", confidence: 0.1 };
}
}
async function ensureCuratorConversation(input: { userId: string; taskId?: string; date: string; subtaskIndex?: number; subtask?: string }) {
if (!input.taskId) return createConversationPg(input.userId, "V1 Curator chat");
const task = (await buildCuratorTasks(input.userId, input.date)).find((item) => item.id === input.taskId);
@@ -345,6 +403,33 @@ export async function runCuratorChat(input: {
throw error;
}
const statusUpdate = await evaluateSubtaskStatus({
task,
subtask: input.subtask,
subtaskIndex: input.subtaskIndex,
latest,
reply,
history: conversationHistory,
});
if (statusUpdate.status !== "needs_more_context") {
if (reply.includes("?")) {
reply = sanitize(statusUpdate.summary);
}
await emitCuratorEvent({
userId: input.userId,
type: "curator.subtask.captured",
mission: task ? { missionId: task.missionId, missionInstanceId: task.missionInstanceId, stageId: task.stageId } : undefined,
payload: {
taskId: input.taskId,
subtaskIndex: input.subtaskIndex,
subtask: input.subtask,
statusUpdate,
},
});
}
await addMessagePg(input.userId, {
id: buildId("assistant"),
conversationId: conversation.id,
@@ -358,5 +443,6 @@ export async function runCuratorChat(input: {
taskId: input.taskId,
reply,
messages: visibleCuratorMessages(await listMessagesPg(input.userId, conversation.id)),
statusUpdate,
};
}

View File

@@ -96,11 +96,19 @@ export type CuratorTodayResponse = {
source: "curator-v1";
};
export type CuratorSubtaskStatusUpdate = {
status: "needs_more_context" | "ready_to_capture" | "handoff_ready";
summary: string;
confidence: number;
nextMissingInfo?: string;
};
export type CuratorChatResponse = {
conversationId: string;
taskId?: string;
reply: string;
messages: Array<{ id: string; role: "user" | "assistant"; sender: string; content: string; createdAt: number }>;
statusUpdate?: CuratorSubtaskStatusUpdate;
handoff?: CuratorServiceHandoff;
};