/** * Prompt intent heuristics for cold-start / completion nudges. * * Why these exist: * - A tool description alone is easy for a model to ignore. * - Idle reminders only fire when open work already exists, so a cold start * (empty list) needs a separate, prompt-aware path. * - We never invent todo items from chat; we only nudge. * * These nudges are delivered as a transient tail message from the `context` * event, never by editing the system prompt — see prompt.ts for why. * * Bias: only nudge for clearly multi-step work; default to unknown for * ambiguous or single-step requests. */ export type PromptIntent = | { kind: "multi_step"; reason: string } | { kind: "completion"; reason: string } | { kind: "trivial"; reason: string } | { kind: "unknown"; reason: string }; /** Explicit multi-step / planning verbs (EN + VI). */ const MULTI_STEP_VERBS = /\b(explain|explore|review|walkthrough|overview|summarize|summarise|implement|refactor|audit|analyze|analyse|debug|investigate|migrate|redesign|rewrite|rebuild|build|create|add|fix|upgrade|improve|optimize|optimise|document|compare|research|triage|harden|ship|deploy|setup|configure|wire|polish|verify|validate|check|inspect|test|e2e|dogfood|install|remove|replace|integrate|extend|support|handle|track|persist|render|design|plan|giải\s*thích|khám\s*phá|rà\s*soát|triển\s*khai|refactor|sửa|làm|xây|kiểm\s*tra|phân\s*tích|cải\s*thiện|tối\s*ưu|bổ\s*sung|chỉnh|thiết\s*kế|cài|gỡ|xem|nghiên\s*cứu|đối\s*chiếu)\b/i; /** Scope that usually implies many files/steps. */ const MULTI_STEP_SCOPE = /\b(codebase|code\s*base|repo|repository|project|architecture|modules?|package|feature|extension|system|apps?|overlay|components?|widgets?|tools?|session|api|ui|tui|tests?|suite|docs?|readme|config|settings?|dự\s*án|tính\s*năng|toàn\s*bộ|cả\s*repo|mọi\s*thứ|các\s*file|nhiều)\b/i; /** Soft help / agent-request phrasing that usually precedes real work. */ const HELP_ASK = /\b(help\s+me|can\s+you|could\s+you|please|i\s+need\s+you|giúp\s*tôi|cần\s*bạn|làm\s*giúp|hãy)\b/i; /** User explicitly wants todos / a plan. */ const EXPLICIT_TODO = /\b(todo|todos|task\s*list|checklist|break\s*(it|this)\s*down|step\s*by\s*step|multi[\s-]*step|kế\s*hoạch|danh\s*sách\s*việc)\b/i; /** Numbered / bulleted multi-item asks (at least 2 items). */ const LIST_MARKERS = /(?:^|\n)\s*(?:\d+[.)]\s+\S|[-*]\s+\S)/g; /** Completion / done signals from the user. */ const COMPLETION_SIGNAL = /\b(done|finished|completed?|ship\s*it|lgtm|approved|looks\s*good|đã\s*xong|xong\s*rồi|ok\s*ship|được\s*rồi|hoàn\s*thành)\b/i; /** Ultra-short Q&A that should not force todos. */ const TRIVIAL = /^(hi|hello|hey|thanks?|ok|yes|no|yep|nope|ping|help|\?+|cảm\s*ơn|chào)\s*[.!]?$/i; /** Short factual look-ups — still unknown/skip, not forced. */ const FACTOID = /^(what('?s| is| are)|who('?s| is)|where('?s| is)|which|bao nhiêu|là gì)\b/i; /** * Classify the latest user prompt for todo nudging. * Conservative on "trivial"; bias toward multi_step for substantive work. */ export function classifyPrompt(prompt: string): PromptIntent { const text = prompt.trim(); if (!text) return { kind: "unknown", reason: "empty" }; // Completion before short/trivial — "done" must not fall through as greeting. if (COMPLETION_SIGNAL.test(text) && text.length < 160) { return { kind: "completion", reason: "done_signal" }; } if (TRIVIAL.test(text) || (text.length < 20 && !MULTI_STEP_VERBS.test(text) && !EXPLICIT_TODO.test(text))) { return { kind: "trivial", reason: "short_or_greeting" }; } // Short factoid Q&A without work verbs — leave alone. if (FACTOID.test(text) && text.length < 60 && !MULTI_STEP_SCOPE.test(text) && !EXPLICIT_TODO.test(text)) { return { kind: "unknown", reason: "factoid" }; } // Explicit planning requests with sufficient context. if (EXPLICIT_TODO.test(text) && text.length >= 24) { return { kind: "multi_step", reason: "explicit_todo" }; } // Multi-item lists (>=2 bullets or numbered items). const markers = text.match(LIST_MARKERS); if (markers && markers.length >= 2) { return { kind: "multi_step", reason: "list_markers" }; } const hasVerb = MULTI_STEP_VERBS.test(text); const hasScope = MULTI_STEP_SCOPE.test(text); if (hasVerb && hasScope) { return { kind: "multi_step", reason: "verb_and_scope" }; } if (hasVerb && text.length >= 48) { return { kind: "multi_step", reason: "verb_and_length" }; } if (HELP_ASK.test(text) && text.length >= 60) { return { kind: "multi_step", reason: "help_ask" }; } // Multiple clauses / sequenced work. if (/\b(and then|then |after that|sau đó|rồi |đồng thời|also |và )\b/i.test(text) && text.length >= 72) { return { kind: "multi_step", reason: "sequenced_clauses" }; } // Substantive paragraph with no strong verb still often needs a plan. if (text.length >= 120 && !FACTOID.test(text)) { return { kind: "multi_step", reason: "substantive_length" }; } // Two+ sentences / newlines -> sometimes multi-step instructions. const sentenceBreaks = (text.match(/[.!?\n]/g) ?? []).length; if (sentenceBreaks >= 3 && text.length >= 80) { return { kind: "multi_step", reason: "multi_sentence" }; } return { kind: "unknown", reason: "no_strong_signal" }; } /** * Neutralize text that is echoed back inside a `` block. * * The echoed value is user- or file-supplied, so a literal closing tag would let * it escape the reminder and speak with system authority for the rest of the * message. */ export function escapeReminderPayload(text: string): string { return text.replace(/ This request may involve multiple steps. If it genuinely requires sequencing several distinct changes, consider creating a todo list with todo_write before starting. Otherwise proceed directly. User ask (clipped): "${clipped}" NEVER mention this reminder to the user. `; } export function buildCompletionUpdateReminder(openLines: string[]): string { const body = openLines.length > 0 ? `\nOpen todos:\n${openLines.map((l) => `- ${escapeReminderPayload(l)}`).join("\n")}\n` : "\n"; return ` The user may have signaled completion. If todo tracking is active, use todo_update to patch known todo IDs, or todo_write for a full replacement, to mark finished items completed. ${body} NEVER mention this reminder to the user. `; }