Fix curator subtask chat state
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user