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`
|
||||
- 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
|
||||
- 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`
|
||||
- `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
|
||||
|
||||
@@ -13,6 +13,7 @@ export function formatResultLines(
|
||||
for (const question of result.questions) {
|
||||
const answer = result.answers[question.id];
|
||||
if (!answer) {
|
||||
lines.push(formatUnansweredLine(question.label, options.mode));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -44,6 +45,15 @@ export function formatResultLines(
|
||||
return lines;
|
||||
}
|
||||
|
||||
function formatUnansweredLine(
|
||||
questionLabel: string,
|
||||
mode: "summary" | "render"
|
||||
): string {
|
||||
return mode === "summary"
|
||||
? `${questionLabel}: (no answer)`
|
||||
: `? ${questionLabel}: (no answer)`;
|
||||
}
|
||||
|
||||
function formatAnswerLine(
|
||||
questionLabel: 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 {
|
||||
applyNumberShortcut,
|
||||
confirmCurrentSelection,
|
||||
enterOptionNoteMode,
|
||||
enterQuestionNoteMode,
|
||||
moveOption,
|
||||
@@ -36,6 +37,71 @@ test("summarizeResult formats selected answers", () => {
|
||||
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", () => {
|
||||
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");
|
||||
});
|
||||
|
||||
test("cancelled result renders as cancelled", () => {
|
||||
test("cancelled result hides unanswered questions", () => {
|
||||
const result = {
|
||||
title: "Interview",
|
||||
cancelled: true,
|
||||
mode: "submit" as const,
|
||||
questions: [],
|
||||
questions: [
|
||||
{
|
||||
id: "scope",
|
||||
label: "Scope",
|
||||
prompt: "What scope?",
|
||||
type: "single" as const,
|
||||
},
|
||||
],
|
||||
answers: {},
|
||||
};
|
||||
|
||||
assert.equal(summarizeResult(result), "User cancelled the ask flow");
|
||||
assert.equal(renderResultText(result), "Cancelled");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user