pi-critique-model 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  **Critique questions the last work step with a separate model and can challenge user instructions before the model starts.** It is not limited to code: it can review implementation work, writing, plans, research, data analysis, ops, or any other project work. Optionally, Critique also runs a fast pre-flight check on sufficiently rich user prompts and shows a short `Critique` widget when the instruction deserves pushback. The feedback is **non-mandatory**: the user and the working model remain the final judges.
10
10
 
11
+ Additionally, the **Questions** feature detects ambiguous user input and offers clarifying suggestions before the model acts on it.
12
+
11
13
  ---
12
14
 
13
15
  ## Features
@@ -27,6 +29,8 @@
27
29
  - **Prompt-critique model source** — use either the active working model or the configured critique model
28
30
  - **Interactive Critique widget** — ultra-short one-sentence advice in the user's interaction language with `Accept`, `Discard`, or `Reply`; auto-discards after 30 seconds
29
31
  - **Persistent config** — `~/.pi/agent/critique.json` stores model choice, auto-inject, and automatic prompt-critique settings across all projects
32
+ - **Questions feature** — optional clarifying widget that detects ambiguous user input and offers three options: two suggested interpretations and a free-text answer; auto-discards after 30 seconds
33
+ - **Questions frequency** — three sensitivity levels: `Essential only` (minimal), `Normal` (moderate), or `Many questions` (high sensitivity)
30
34
 
31
35
  ## Install
32
36
 
@@ -87,7 +91,7 @@ To focus the review:
87
91
  | `/critique view` | Show the review only, without injecting it |
88
92
  | `/critique view <focus>` | View-only, with a focus note |
89
93
  | `/critique view N` | View-only, last `N` steps |
90
- | `/critique config` | Open the editable settings menu for model, auto-inject, and automatic prompt critique |
94
+ | `/critique config` | Open the editable settings menu for model, auto-inject, prompt critique, and questions |
91
95
 
92
96
  **Argument parsing:**
93
97
 
@@ -166,6 +170,29 @@ If critique is useful, pi shows an ultra-short `Critique` widget in the user's i
166
170
 
167
171
  Automatic prompt critique can use either the active working model or the configured critique model. It does not require a separate model.
168
172
 
173
+ ### 5. Optional Questions feature
174
+
175
+ When enabled in `/critique config`, Critique analyzes user input for ambiguity — unclear pronouns, vague directives, or scope-unclear requests — and shows a `Questions` widget before the model receives the prompt.
176
+
177
+ The widget presents three options in the user's interaction language:
178
+
179
+ - **A** — a suggested interpretation (e.g., "Focus on the primary task")
180
+ - **B** — an alternative interpretation (e.g., "Address all aspects comprehensively")
181
+ - **C** — the user types their own answer in a free-text editor
182
+ - **Dismiss** — send the original prompt unchanged (auto-dismisses after 30 seconds)
183
+
184
+ The selected answer is prepended to the user's prompt as a clarification, so the working model receives a more precise instruction.
185
+
186
+ #### Questions Frequency
187
+
188
+ The sensitivity of the Questions feature is controlled by the **Questions frequency** setting:
189
+
190
+ | Level | Description |
191
+ |-------|-------------|
192
+ | **Essential only** | Only ask when the input is clearly ambiguous (long, multi-sentence, or contains unclear pronouns/references) |
193
+ | **Normal** | Moderate sensitivity; ask for most inputs that could benefit from clarification |
194
+ | **Many questions** | High sensitivity; ask frequently, even for shorter or mildly ambiguous inputs |
195
+
169
196
  ## Model Selection
170
197
 
171
198
  `/critique config` opens an editable settings menu. Select a setting to change only that value, toggle booleans directly, or choose `Done`/`Esc` to close. Changes are persisted as soon as each setting is edited.
@@ -188,7 +215,9 @@ The config is persisted as JSON at `~/.pi/agent/critique.json`:
188
215
  "autoInject": true,
189
216
  "autoPromptCritique": false,
190
217
  "autoPromptCritiqueLevel": "inconsistencies",
191
- "autoPromptCritiqueModel": "working"
218
+ "autoPromptCritiqueModel": "working",
219
+ "questions": false,
220
+ "questionsFrequency": "normal"
192
221
  }
193
222
  ```
194
223
 
@@ -197,6 +226,8 @@ The config is persisted as JSON at `~/.pi/agent/critique.json`:
197
226
  - **`autoPromptCritique`** — when `true`, sufficiently rich user instructions are challenged before the model starts.
198
227
  - **`autoPromptCritiqueLevel`** — `inconsistencies` only flags real misunderstanding risks; `critical` is moderate; `corrosive` is highly sensitive and skips only clearly logical/complete prompts.
199
228
  - **`autoPromptCritiqueModel`** — `working` uses the active model; `critique` uses the configured critique model.
229
+ - **`questions`** — when `true`, ambiguous user input triggers a clarifying Questions widget before the model receives the prompt.
230
+ - **`questionsFrequency`** — sensitivity of the Questions feature: `essential` (minimal), `normal` (moderate), or `verbose` (high; "many questions").
200
231
 
201
232
  The settings menu offers any model with configured auth that's available in pi's registry; the config persists per-machine (in `getAgentDir()`), shared across all projects.
202
233
 
@@ -213,7 +244,7 @@ critique/
213
244
  └── src/
214
245
  ├── index.ts # /critique command surface, config UI, review UI, input hook
215
246
  ├── config.ts # persistence + model resolution: pinned, auto, fallback
216
- ├── prompt-critique.ts # automatic user-prompt critique prompt, gate, model call
247
+ ├── prompt-critique.ts # automatic user-prompt critique prompt, gate, model call, questions detection
217
248
  ├── work-step.ts # episode splitting + token-budgeted serialization
218
249
  └── review.ts # reviewer prompt + model call + injected-message builder
219
250
  ```
@@ -225,12 +256,13 @@ Five-file extension with zero external dependencies (only pi's bundled `@earendi
225
256
  - **Reviewer call** — tool-free `ctx.modelRegistry.complete()` with a domain-general structured prompt
226
257
  - **Advisory formatter** — wraps the review in a "non-mandatory" envelope before injecting as a follow-up user message
227
258
  - **Prompt critique** — optional input hook with local trivial-prompt gate, three challenge levels, and ultra-short JSON model output
228
- - **UI** — lazy-loaded pickers/viewers/loaders + 30-second Critique widget
259
+ - **Questions detection** — heuristic ambiguity detector with three frequency levels; shows a clarifying widget with A/B/C options
260
+ - **UI** — lazy-loaded pickers/viewers/loaders + 30-second Critique widget + 30-second Questions widget
229
261
 
230
262
  ## Notes
231
263
 
232
264
  - The critique model runs with **no tools** and never touches the filesystem. It judges purely from the serialized work step, whatever the domain.
233
- - In TUI mode the review runs behind a cancelable loader (Esc aborts) and `/critique view` opens a scrollable Markdown viewer. Automatic prompt critique appears as a compact `Critique` widget with a 30-second auto-discard timeout. In RPC mode reviews are surfaced through notifications/dialogs; print mode logs manual reviews to stdout and skips automatic prompt critique.
265
+ - In TUI mode the review runs behind a cancelable loader (Esc aborts) and `/critique view` opens a scrollable Markdown viewer. Automatic prompt critique appears as a compact `Critique` widget with a 30-second auto-discard timeout. The Questions feature appears as a `Questions` widget with the same timeout. In RPC mode reviews are surfaced through notifications/dialogs; print mode logs manual reviews to stdout and skips automatic prompt critique and questions.
234
266
  - Provider errors (bad keys, insufficient balance, rate limit) are surfaced as errors instead of silently producing empty reviews.
235
267
  - Reviews are capped at 16,000 chars to keep the injected follow-up reasonable; longer reviews are truncated with `… [review truncated]`.
236
268
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-critique-model",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "pi critique: work review + prompt challenge.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/config.ts CHANGED
@@ -16,6 +16,8 @@ import type {
16
16
  AutoPromptCritiqueModelSource,
17
17
  } from "./prompt-critique.ts";
18
18
 
19
+ export type QuestionsFrequency = "essential" | "normal" | "verbose";
20
+
19
21
  export interface CritiqueConfig {
20
22
  /** Canonical "provider/modelId" of the critique model. Empty string = auto. */
21
23
  model: string;
@@ -27,6 +29,10 @@ export interface CritiqueConfig {
27
29
  autoPromptCritiqueLevel: AutoPromptCritiqueLevel;
28
30
  /** Which model is used for automatic prompt critique. */
29
31
  autoPromptCritiqueModel: AutoPromptCritiqueModelSource;
32
+ /** Ask clarifying questions when user input is ambiguous. */
33
+ questions: boolean;
34
+ /** Sensitivity level for the questions feature. */
35
+ questionsFrequency: QuestionsFrequency;
30
36
  }
31
37
 
32
38
  export const DEFAULT_CONFIG: CritiqueConfig = {
@@ -35,6 +41,8 @@ export const DEFAULT_CONFIG: CritiqueConfig = {
35
41
  autoPromptCritique: false,
36
42
  autoPromptCritiqueLevel: "inconsistencies",
37
43
  autoPromptCritiqueModel: "working",
44
+ questions: false,
45
+ questionsFrequency: "normal",
38
46
  };
39
47
 
40
48
  export function configFilePath(): string {
@@ -46,6 +54,7 @@ export function loadConfig(): CritiqueConfig {
46
54
  const raw = JSON.parse(readFileSync(configFilePath(), "utf8")) as Partial<CritiqueConfig>;
47
55
  const level = raw.autoPromptCritiqueLevel;
48
56
  const modelSource = raw.autoPromptCritiqueModel;
57
+ const frequency = raw.questionsFrequency;
49
58
  return {
50
59
  model: typeof raw.model === "string" ? raw.model : DEFAULT_CONFIG.model,
51
60
  autoInject:
@@ -62,6 +71,12 @@ export function loadConfig(): CritiqueConfig {
62
71
  modelSource === "working" || modelSource === "critique"
63
72
  ? modelSource
64
73
  : DEFAULT_CONFIG.autoPromptCritiqueModel,
74
+ questions:
75
+ typeof raw.questions === "boolean" ? raw.questions : DEFAULT_CONFIG.questions,
76
+ questionsFrequency:
77
+ frequency === "essential" || frequency === "normal" || frequency === "verbose"
78
+ ? frequency
79
+ : DEFAULT_CONFIG.questionsFrequency,
65
80
  };
66
81
  } catch {
67
82
  return { ...DEFAULT_CONFIG };
package/src/index.ts CHANGED
@@ -22,6 +22,19 @@ import {
22
22
  saveConfig,
23
23
  } from "./config.ts";
24
24
 
25
+ import {
26
+ detectAmbiguity,
27
+ isPromptCritiqueCandidate,
28
+ runAutoPromptCritique,
29
+ buildAcceptedPromptCritiqueMessage,
30
+ buildPromptCritiqueReplyMessage,
31
+ buildQuestionsMessage,
32
+ questionsFrequencyLabel,
33
+ type QuestionsFrequency,
34
+ type AutoPromptCritiqueLevel,
35
+ type AutoPromptCritiqueModelSource,
36
+ } from "./prompt-critique.ts";
37
+
25
38
  type Mode = "config" | "review" | "view";
26
39
 
27
40
  interface ParsedArgs {
@@ -82,9 +95,11 @@ async function runWithLoader(
82
95
  type AutoPromptCritiqueLevel = ReturnType<typeof loadConfig>["autoPromptCritiqueLevel"];
83
96
  type AutoPromptCritiqueModelSource = ReturnType<typeof loadConfig>["autoPromptCritiqueModel"];
84
97
  type PromptCritiqueAction = "accept" | "discard" | "reply";
98
+ type QuestionAction = "a" | "b" | "c" | "dismiss";
85
99
 
86
100
  const MAX_VISIBLE_MODELS = 10;
87
101
  const PROMPT_CRITIQUE_TIMEOUT_MS = 30_000;
102
+ const QUESTIONS_TIMEOUT_MS = 30_000;
88
103
 
89
104
  function autoPromptCritiqueLevelLabel(level: AutoPromptCritiqueLevel): string {
90
105
  return level === "inconsistencies" ? "Inconsistencies" : level === "critical" ? "Critical" : "Corrosive";
@@ -99,7 +114,7 @@ function updateCritiqueStatus(ctx: ExtensionContext): void {
99
114
  const config = loadConfig();
100
115
  ctx.ui.setStatus(
101
116
  "critique",
102
- config.autoPromptCritique ? `critique:${autoPromptCritiqueLevelLabel(config.autoPromptCritiqueLevel)}` : undefined,
117
+ config.autoPromptCritique ? "🧠 critique:on" : undefined,
103
118
  );
104
119
  }
105
120
 
@@ -294,6 +309,89 @@ async function showPromptCritiqueWidget(
294
309
  }, { overlay: true, overlayOptions: { anchor: "bottom-center", width: "90%", minWidth: 20 } });
295
310
  }
296
311
 
312
+ async function showQuestionsWidget(
313
+ ctx: ExtensionContext,
314
+ interpretationA: string,
315
+ interpretationB: string,
316
+ ): Promise<QuestionAction> {
317
+ if (ctx.mode !== "tui") {
318
+ const choice = await ctx.ui.select(
319
+ "What did you mean?",
320
+ [
321
+ `A: ${interpretationA}`,
322
+ `B: ${interpretationB}`,
323
+ "C: My own answer",
324
+ "Dismiss (send as-is)",
325
+ ],
326
+ { timeout: QUESTIONS_TIMEOUT_MS },
327
+ );
328
+ if (choice === "A") return "a";
329
+ if (choice === "B") return "b";
330
+ if (choice === "C") return "c";
331
+ return "dismiss";
332
+ }
333
+
334
+ const { matchesKey } = await import("@earendil-works/pi-tui");
335
+
336
+ return ctx.ui.custom<QuestionAction>((tui, theme, _kb, done) => {
337
+ const started = Date.now();
338
+ let remaining = QUESTIONS_TIMEOUT_MS;
339
+ let closed = false;
340
+
341
+ const finish = (action: QuestionAction) => {
342
+ if (closed) return;
343
+ closed = true;
344
+ clearInterval(interval);
345
+ clearTimeout(timeout);
346
+ done(action);
347
+ };
348
+
349
+ const timeout = setTimeout(() => finish("dismiss"), QUESTIONS_TIMEOUT_MS);
350
+ const interval = setInterval(() => {
351
+ remaining = QUESTIONS_TIMEOUT_MS - (Date.now() - started);
352
+ tui.requestRender();
353
+ }, 250);
354
+
355
+ return {
356
+ render: (width: number) => {
357
+ const actualWidth = Math.max(20, Math.min(width, 100));
358
+ const color = (s: string) => theme.fg("accent", s);
359
+ const lines: string[] = [];
360
+ lines.push(frameBorder(actualWidth, "╭", "╮", ` Questions (${formatRemaining(remaining)}) `, color));
361
+ lines.push(frameLine("", actualWidth, color));
362
+ lines.push(frameLine("What did you mean?", actualWidth, color));
363
+ lines.push(frameLine("", actualWidth, color));
364
+ lines.push(frameLine(`A) ${interpretationA}`, actualWidth, color));
365
+ lines.push(frameLine(`B) ${interpretationB}`, actualWidth, color));
366
+ lines.push(frameLine(`C) My own answer`, actualWidth, color));
367
+ lines.push(frameLine("", actualWidth, color));
368
+ lines.push(
369
+ frameLine(
370
+ "A/B/C choice · Esc dismiss",
371
+ actualWidth,
372
+ color,
373
+ ),
374
+ );
375
+ lines.push(frameBorder(actualWidth, "╰", "╯", "", color));
376
+ return lines;
377
+ },
378
+ invalidate: () => {},
379
+ handleInput: (data: string) => {
380
+ if (data === "a" || data === "A") finish("a");
381
+ else if (data === "b" || data === "B") finish("b");
382
+ else if (data === "c" || data === "C") finish("c"); else if (data === "d" || data === "D" || matchesKey(data, "escape")) {
383
+ finish("dismiss");
384
+ }
385
+ tui.requestRender();
386
+ },
387
+ dispose: () => {
388
+ clearInterval(interval);
389
+ clearTimeout(timeout);
390
+ },
391
+ };
392
+ }, { overlay: true, overlayOptions: { anchor: "bottom-center", width: "90%", minWidth: 20 } });
393
+ }
394
+
297
395
  async function handleAutomaticPromptCritique(
298
396
  ctx: ExtensionContext,
299
397
  text: string,
@@ -303,13 +401,6 @@ async function handleAutomaticPromptCritique(
303
401
  if (!config.autoPromptCritique) return null;
304
402
  if (!ctx.hasUI) return null;
305
403
 
306
- const {
307
- buildAcceptedPromptCritiqueMessage,
308
- buildPromptCritiqueReplyMessage,
309
- isPromptCritiqueCandidate,
310
- runAutoPromptCritique,
311
- } = await import("./prompt-critique.ts");
312
-
313
404
  if (!isPromptCritiqueCandidate(text, (images?.length ?? 0) > 0, config.autoPromptCritiqueLevel)) return null;
314
405
 
315
406
  const model = resolveAutoPromptCritiqueModel(ctx, config);
@@ -348,6 +439,30 @@ async function handleAutomaticPromptCritique(
348
439
  return null;
349
440
  }
350
441
 
442
+ async function handleQuestions(
443
+ ctx: ExtensionContext,
444
+ text: string,
445
+ ): Promise<string | null> {
446
+ const config = loadConfig();
447
+ if (!config.questions) return null;
448
+ if (!ctx.hasUI) return null;
449
+
450
+ const ambiguity = detectAmbiguity(text, false, config.questionsFrequency);
451
+ if (!ambiguity) return null;
452
+
453
+ const action = await showQuestionsWidget(ctx, ambiguity.interpretationA, ambiguity.interpretationB);
454
+ if (action === "dismiss") return null;
455
+
456
+ let customAnswer = "";
457
+ if (action === "c") {
458
+ const reply = await ctx.ui.editor("Your interpretation", "");
459
+ if (!reply?.trim()) return null;
460
+ customAnswer = reply.trim();
461
+ }
462
+
463
+ return buildQuestionsMessage(text, ambiguity.interpretationA, ambiguity.interpretationB, action, customAnswer);
464
+ }
465
+
351
466
  async function runReview(pi: ExtensionAPI, ctx: ExtensionCommandContext, parsed: ParsedArgs): Promise<void> {
352
467
  const config = loadConfig();
353
468
 
@@ -432,7 +547,10 @@ function configSummary(config: ReturnType<typeof loadConfig>): string {
432
547
  const promptCritique = config.autoPromptCritique
433
548
  ? `${autoPromptCritiqueLevelLabel(config.autoPromptCritiqueLevel)} via ${autoPromptCritiqueModelSourceLabel(config.autoPromptCritiqueModel)}`
434
549
  : "off";
435
- return `model:${config.model || "auto"} | inject:${config.autoInject ? "on" : "off"} | prompt:${promptCritique}`;
550
+ const questions = config.questions
551
+ ? `on (${questionsFrequencyLabel(config.questionsFrequency)})`
552
+ : "off";
553
+ return `model:${config.model || "auto"} | inject:${config.autoInject ? "on" : "off"} | prompt:${promptCritique} | questions:${questions}`;
436
554
  }
437
555
 
438
556
  function configMenuItems(config: ReturnType<typeof loadConfig>): SelectItem[] {
@@ -462,6 +580,16 @@ function configMenuItems(config: ReturnType<typeof loadConfig>): SelectItem[] {
462
580
  label: "Prompt model",
463
581
  description: autoPromptCritiqueModelSourceLabel(config.autoPromptCritiqueModel),
464
582
  },
583
+ {
584
+ value: "questions",
585
+ label: "Questions",
586
+ description: config.questions ? "on" : "off",
587
+ },
588
+ {
589
+ value: "questionsFrequency",
590
+ label: "Questions frequency",
591
+ description: questionsFrequencyLabel(config.questionsFrequency),
592
+ },
465
593
  {
466
594
  value: "done",
467
595
  label: "Done",
@@ -592,6 +720,13 @@ async function handleConfig(ctx: ExtensionCommandContext): Promise<void> {
592
720
  case "autoPromptCritiqueModel":
593
721
  changed = await editPromptCritiqueModel(ctx, config);
594
722
  break;
723
+ case "questions":
724
+ config.questions = !config.questions;
725
+ changed = true;
726
+ break;
727
+ case "questionsFrequency":
728
+ changed = await editQuestionsFrequency(ctx, config);
729
+ break;
595
730
  }
596
731
 
597
732
  if (changed) {
@@ -606,6 +741,38 @@ async function handleConfig(ctx: ExtensionCommandContext): Promise<void> {
606
741
  ctx.ui.notify(`Critique config closed — ${configSummary(config)}`, "info");
607
742
  }
608
743
 
744
+ async function editQuestionsFrequency(
745
+ ctx: ExtensionCommandContext,
746
+ config: ReturnType<typeof loadConfig>,
747
+ ): Promise<boolean> {
748
+ const freqItems: SelectItem[] = [
749
+ {
750
+ value: "essential",
751
+ label: "Essential only",
752
+ description: "only ask when truly necessary",
753
+ },
754
+ {
755
+ value: "normal",
756
+ label: "Normal",
757
+ description: "moderate sensitivity",
758
+ },
759
+ {
760
+ value: "verbose",
761
+ label: "Many questions",
762
+ description: "high sensitivity; ask often",
763
+ },
764
+ ];
765
+ const choice = await chooseConfigItem(
766
+ ctx,
767
+ "Questions frequency",
768
+ freqItems,
769
+ config.questionsFrequency,
770
+ );
771
+ if (choice === undefined) return false;
772
+ config.questionsFrequency = choice as QuestionsFrequency;
773
+ return true;
774
+ }
775
+
609
776
  export default function (pi: ExtensionAPI) {
610
777
  pi.on("session_start", async (_event, ctx) => {
611
778
  updateCritiqueStatus(ctx);
@@ -613,6 +780,14 @@ export default function (pi: ExtensionAPI) {
613
780
 
614
781
  pi.on("input", async (event, ctx) => {
615
782
  if (event.source === "extension") return { action: "continue" as const };
783
+
784
+ // First, check if the questions feature wants to ask a clarifying question.
785
+ const questionsTransformed = await handleQuestions(ctx, event.text);
786
+ if (questionsTransformed) {
787
+ return { action: "transform" as const, text: questionsTransformed, images: event.images };
788
+ }
789
+
790
+ // Then, check if automatic prompt critique wants to challenge the prompt.
616
791
  const transformed = await handleAutomaticPromptCritique(ctx, event.text, event.images);
617
792
  if (!transformed) return { action: "continue" as const };
618
793
  return { action: "transform" as const, text: transformed, images: event.images };
@@ -1,9 +1,12 @@
1
1
  /**
2
- * Critique — automatic user-prompt critique.
2
+ * Critique — automatic user-prompt critique and questions.
3
3
  *
4
4
  * This module judges user instructions before the working model receives them.
5
5
  * It is intentionally lightweight: a cheap heuristic avoids trivial prompts,
6
6
  * then a tool-free model call decides whether there is anything worth showing.
7
+ *
8
+ * It also provides the "questions" feature: when user input is ambiguous,
9
+ * a clarifying widget offers three options (two suggestions + free text).
7
10
  */
8
11
 
9
12
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -11,6 +14,7 @@ import { type Message, type Model, uuidv7 } from "@earendil-works/pi-ai";
11
14
 
12
15
  export type AutoPromptCritiqueLevel = "inconsistencies" | "critical" | "corrosive";
13
16
  export type AutoPromptCritiqueModelSource = "working" | "critique";
17
+ export type QuestionsFrequency = "essential" | "normal" | "verbose";
14
18
 
15
19
  export const AUTO_PROMPT_CRITIQUE_LEVELS: AutoPromptCritiqueLevel[] = [
16
20
  "inconsistencies",
@@ -34,6 +38,19 @@ export interface AutoPromptCritiqueAdvice {
34
38
  solution: string;
35
39
  }
36
40
 
41
+ export interface QuestionOption {
42
+ label: string;
43
+ description: string;
44
+ value: string;
45
+ }
46
+
47
+ export interface QuestionResult {
48
+ /** "a", "b", or "c" */
49
+ choice: string;
50
+ /** Free-text answer when choice is "c". */
51
+ customAnswer: string;
52
+ }
53
+
37
54
  const LEVEL_GUIDANCE: Record<AutoPromptCritiqueLevel, string> = {
38
55
  inconsistencies:
39
56
  "Low sensitivity. True contradiction/gap/ambiguity likely to make model misunderstand. Otherwise skip.",
@@ -43,6 +60,12 @@ const LEVEL_GUIDANCE: Record<AutoPromptCritiqueLevel, string> = {
43
60
  "High sensitivity. Hunt weak logic/inconsistency/vagueness/overreach. Skip only clearly logical+complete prompts.",
44
61
  };
45
62
 
63
+ const QUESTIONS_FREQUENCY_LABELS: Record<QuestionsFrequency, string> = {
64
+ essential: "Essential only",
65
+ normal: "Normal",
66
+ verbose: "Verbose",
67
+ };
68
+
46
69
  export function autoPromptCritiqueLevelLabel(level: AutoPromptCritiqueLevel): string {
47
70
  switch (level) {
48
71
  case "inconsistencies":
@@ -58,6 +81,111 @@ export function autoPromptCritiqueModelSourceLabel(source: AutoPromptCritiqueMod
58
81
  return source === "working" ? "Working model" : "Critique model";
59
82
  }
60
83
 
84
+ export function questionsFrequencyLabel(freq: QuestionsFrequency): string {
85
+ return QUESTIONS_FREQUENCY_LABELS[freq];
86
+ }
87
+
88
+ /**
89
+ * Detect whether the user's input is ambiguous enough to warrant a
90
+ * clarifying question. Returns a suggested interpretation pair when one is
91
+ * found, or null when the input is clear.
92
+ */
93
+ export function detectAmbiguity(
94
+ text: string,
95
+ hasImages: boolean,
96
+ level: QuestionsFrequency,
97
+ ): { interpretationA: string; interpretationB: string } | null {
98
+ const trimmed = text.trim();
99
+ if (!trimmed) return null;
100
+
101
+ const normalized = trimmed
102
+ .toLowerCase()
103
+ .replace(/[.!?¡¿,;:()\[\]{}"'`´]/g, "")
104
+ .replace(/\s+/g, " ")
105
+ .trim();
106
+
107
+ // Skip trivial/acknowledgement inputs.
108
+ const bareReplies = new Set([
109
+ "ok",
110
+ "okay",
111
+ "yes",
112
+ "no",
113
+ "y",
114
+ "n",
115
+ "thanks",
116
+ "thank you",
117
+ "gracias",
118
+ "vale",
119
+ "sí",
120
+ "si",
121
+ "sigue",
122
+ "continua",
123
+ "continúa",
124
+ "continue",
125
+ "stop",
126
+ "para",
127
+ "no hagas eso",
128
+ ]);
129
+ if (bareReplies.has(normalized)) return null;
130
+
131
+ const words = normalized.split(/\s+/).filter(Boolean);
132
+ const wordCount = words.length;
133
+ const charCount = trimmed.length;
134
+
135
+ // Thresholds vary by frequency level.
136
+ const thresholds = {
137
+ essential: { minChars: 120, minWords: 14, hasNewlineBonus: true },
138
+ normal: { minChars: 70, minWords: 9, hasNewlineBonus: true },
139
+ verbose: { minChars: 40, minWords: 6, hasNewlineBonus: false },
140
+ };
141
+ const t = thresholds[level];
142
+
143
+ // Basic length/word check.
144
+ if (charCount < t.minChars && wordCount < t.minWords) return null;
145
+
146
+ // Newlines increase the chance of ambiguity (multi-sentence or structured input).
147
+ const hasNewline = trimmed.includes("\n");
148
+ if (hasNewline && t.hasNewlineBonus) {
149
+ // Already passed thresholds; proceed to ambiguity detection.
150
+ } else if (!hasNewline && charCount < t.minChars) {
151
+ return null;
152
+ }
153
+
154
+ // Heuristic ambiguity patterns:
155
+ // 1. Pronouns without clear referent ("it", "this", "that", "them")
156
+ // 2. Vague directives ("fix it", "improve this", "make it better")
157
+ // 3. Short ambiguous requests with unclear scope
158
+
159
+ const ambiguousPatterns = [
160
+ // Pronoun-heavy patterns
161
+ { pattern: /\b(it|this|that|them|those)\b/, interpretationA: "The most recently mentioned item/topic", interpretationB: "The overall task or goal" },
162
+ // Vague improvement requests
163
+ { pattern: /\b(fix|improve|change|adjust|modify|refactor)\b.*\b(it|this|that|them)\b/, interpretationA: "Fix the code/logic errors", interpretationB: "Improve the overall quality/style" },
164
+ // Scope-ambiguous requests
165
+ { pattern: /\b(make|do|handle|deal with|address)\b.*\b(it|this|that|them|the\b)/, interpretationA: "Focus on the primary/most obvious aspect", interpretationB: "Cover all aspects comprehensively" },
166
+ // Unclear referent with "the"
167
+ { pattern: /\b(the\s+\w+\s+\w+)\b.*\b(needs|requires|should|must)\b/, interpretationA: "The specific item mentioned", interpretationB: "The broader system or context" },
168
+ // General ambiguity with "something"
169
+ { pattern: /\b(something|anything|somewhere|someone)\b/, interpretationA: "The most relevant/obvious option", interpretationB: "Explore all possible options" },
170
+ ];
171
+
172
+ for (const ap of ambiguousPatterns) {
173
+ if (ap.pattern.test(normalized)) {
174
+ return { interpretationA: ap.interpretationA, interpretationB: ap.interpretationB };
175
+ }
176
+ }
177
+
178
+ // If we got here and the input is long enough, it's ambiguous by default.
179
+ if (charCount >= t.minChars * 1.5 && wordCount >= t.minWords * 1.5) {
180
+ return {
181
+ interpretationA: "Focus on the primary task or request",
182
+ interpretationB: "Address all aspects and edge cases",
183
+ };
184
+ }
185
+
186
+ return null;
187
+ }
188
+
61
189
  /** Fast local gate: avoid paying a model call for acknowledgements and tiny commands. */
62
190
  export function isPromptCritiqueCandidate(
63
191
  text: string,
@@ -70,7 +198,8 @@ export function isPromptCritiqueCandidate(
70
198
  if (
71
199
  trimmed.includes("[Critique accepted by the user]") ||
72
200
  trimmed.includes("[Critique solution accepted by the user]") ||
73
- trimmed.includes("[User reply to Critique]")
201
+ trimmed.includes("[User reply to Critique]") ||
202
+ trimmed.includes("[Questions answer]")
74
203
  ) {
75
204
  return false;
76
205
  }
@@ -271,3 +400,28 @@ export function buildPromptCritiqueReplyMessage(
271
400
  reply.trim(),
272
401
  ].join("\n");
273
402
  }
403
+
404
+ export function buildQuestionsMessage(
405
+ originalPrompt: string,
406
+ interpretationA: string,
407
+ interpretationB: string,
408
+ choice: string,
409
+ customAnswer: string,
410
+ ): string {
411
+ const base = [
412
+ originalPrompt,
413
+ "",
414
+ "[Questions answer]",
415
+ `What the user meant:`,
416
+ ];
417
+
418
+ if (choice === "a") {
419
+ base.push(`Interpretation A: ${interpretationA}`);
420
+ } else if (choice === "b") {
421
+ base.push(`Interpretation B: ${interpretationB}`);
422
+ } else if (choice === "c") {
423
+ base.push(`User's own interpretation: ${customAnswer.trim()}`);
424
+ }
425
+
426
+ return base.join("\n");
427
+ }