fix: surface unanswered ask_user questions
This commit is contained in:
+1
-1
@@ -165,7 +165,7 @@ This document defines the stable external behavior. It does not explain internal
|
|||||||
- semantically invalid payloads that reach tool execution return `error.kind === "invalid_input"` with structured `issues` and a transcript-friendly `Invalid ask_user payload:` message; their rendered status is `Invalid tool payload`
|
- semantically invalid payloads that reach tool execution return `error.kind === "invalid_input"` with structured `issues` and a transcript-friendly `Invalid ask_user payload:` message; their rendered status is `Invalid tool payload`
|
||||||
- payloads missing schema-required fields fail Pi's schema validation before tool execution and use Pi's standard tool-error result without structured `details`
|
- payloads missing schema-required fields fail Pi's schema validation before tool execution and use Pi's standard tool-error result without structured `details`
|
||||||
- `mode: "submit"` is normal completion; `mode: "elaborate"` means the user asked the agent to continue with follow-up clarification based on notes
|
- `mode: "submit"` is normal completion; `mode: "elaborate"` means the user asked the agent to continue with follow-up clarification based on notes
|
||||||
- unanswered questions are omitted from `answers`
|
- unanswered questions are omitted from `answers`, but non-cancelled submitted result text includes `<label>: (no answer)` in summary mode and `? <label>: (no answer)` in transcript rendering
|
||||||
- in `mode: "elaborate"`, `answers` contains only committed answers; note-only entries move to `elaboration.items`
|
- in `mode: "elaborate"`, `answers` contains only committed answers; note-only entries move to `elaboration.items`
|
||||||
- `continuation.strategy === "refine_only"` means the next ask should refine the current flow rather than restart it
|
- `continuation.strategy === "refine_only"` means the next ask should refine the current flow rather than restart it
|
||||||
- `continuation.preservedAnswers` contains previously committed answers that should be kept as context and not re-asked
|
- `continuation.preservedAnswers` contains previously committed answers that should be kept as context and not re-asked
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function formatResultLines(
|
|||||||
for (const question of result.questions) {
|
for (const question of result.questions) {
|
||||||
const answer = result.answers[question.id];
|
const answer = result.answers[question.id];
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
|
lines.push(formatUnansweredLine(question.label, options.mode));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +45,15 @@ export function formatResultLines(
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatUnansweredLine(
|
||||||
|
questionLabel: string,
|
||||||
|
mode: "summary" | "render"
|
||||||
|
): string {
|
||||||
|
return mode === "summary"
|
||||||
|
? `${questionLabel}: (no answer)`
|
||||||
|
: `? ${questionLabel}: (no answer)`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatAnswerLine(
|
function formatAnswerLine(
|
||||||
questionLabel: string,
|
questionLabel: string,
|
||||||
answer: AskResult["answers"][string],
|
answer: AskResult["answers"][string],
|
||||||
|
|||||||
+76
-2
@@ -5,6 +5,7 @@ import { createInitialState } from "../src/state/create.ts";
|
|||||||
import { summarizeResult, toAskResult } from "../src/state/result.ts";
|
import { summarizeResult, toAskResult } from "../src/state/result.ts";
|
||||||
import {
|
import {
|
||||||
applyNumberShortcut,
|
applyNumberShortcut,
|
||||||
|
confirmCurrentSelection,
|
||||||
enterOptionNoteMode,
|
enterOptionNoteMode,
|
||||||
enterQuestionNoteMode,
|
enterQuestionNoteMode,
|
||||||
moveOption,
|
moveOption,
|
||||||
@@ -36,6 +37,71 @@ test("summarizeResult formats selected answers", () => {
|
|||||||
assert.equal(renderResultText(result), "✓ Style: Rich");
|
assert.equal(renderResultText(result), "✓ Style: Rich");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("submitted results surface unanswered questions without serializing answers", () => {
|
||||||
|
let state = createInitialState({
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "architecture",
|
||||||
|
label: "Architecture",
|
||||||
|
prompt: "How should we wire secrets?",
|
||||||
|
options: [{ value: "encrypted", label: "Encrypted" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sources",
|
||||||
|
label: "Secret sources",
|
||||||
|
prompt: "Where do secrets come from?",
|
||||||
|
type: "multi",
|
||||||
|
options: [{ value: "env", label: ".env file" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
state = applyNumberShortcut(state, 1);
|
||||||
|
state = confirmCurrentSelection(state);
|
||||||
|
state = confirmCurrentSelection(state);
|
||||||
|
const result = toAskResult(state);
|
||||||
|
|
||||||
|
assert.equal(result.answers.sources, undefined);
|
||||||
|
assert.equal(
|
||||||
|
summarizeResult(result),
|
||||||
|
"Architecture: Encrypted\nSecret sources: (no answer)"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderResultText(result),
|
||||||
|
"✓ Architecture: Encrypted\n? Secret sources: (no answer)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("submitted results surface every question when all are unanswered", () => {
|
||||||
|
const result = {
|
||||||
|
cancelled: false,
|
||||||
|
mode: "submit" as const,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "scope",
|
||||||
|
label: "Scope",
|
||||||
|
prompt: "What scope?",
|
||||||
|
type: "single" as const,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tests",
|
||||||
|
label: "Tests",
|
||||||
|
prompt: "Which tests?",
|
||||||
|
type: "multi" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
answers: {},
|
||||||
|
};
|
||||||
|
assert.equal(
|
||||||
|
summarizeResult(result),
|
||||||
|
"Scope: (no answer)\nTests: (no answer)"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
renderResultText(result),
|
||||||
|
"? Scope: (no answer)\n? Tests: (no answer)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("single questions presented as multi preserve requested type in results", () => {
|
test("single questions presented as multi preserve requested type in results", () => {
|
||||||
let state = createInitialState(
|
let state = createInitialState(
|
||||||
{
|
{
|
||||||
@@ -584,14 +650,22 @@ test("invalid tool payload result is attributed to the tool call", () => {
|
|||||||
assert.equal(renderResultText(result), "Invalid tool payload");
|
assert.equal(renderResultText(result), "Invalid tool payload");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("cancelled result renders as cancelled", () => {
|
test("cancelled result hides unanswered questions", () => {
|
||||||
const result = {
|
const result = {
|
||||||
title: "Interview",
|
title: "Interview",
|
||||||
cancelled: true,
|
cancelled: true,
|
||||||
mode: "submit" as const,
|
mode: "submit" as const,
|
||||||
questions: [],
|
questions: [
|
||||||
|
{
|
||||||
|
id: "scope",
|
||||||
|
label: "Scope",
|
||||||
|
prompt: "What scope?",
|
||||||
|
type: "single" as const,
|
||||||
|
},
|
||||||
|
],
|
||||||
answers: {},
|
answers: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert.equal(summarizeResult(result), "User cancelled the ask flow");
|
||||||
assert.equal(renderResultText(result), "Cancelled");
|
assert.equal(renderResultText(result), "Cancelled");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user