12 KiB
Ask tool contract
ask_user is a pi-native clarification tool for cases where implementation depends on user preference or missing requirements.
This document defines the stable external behavior. It does not explain internal helper-by-helper implementation.
Input
{
title?: string;
questions: Array<{
id: string;
label?: string;
prompt: string;
type?: "single" | "multi" | "preview";
required?: boolean;
options: Array<{
value: string;
label: string;
description?: string;
preview?: string;
}>;
}>;
}
Input rules
- at least one question is required
- every question must have non-empty trimmed
idandprompt - every question must have at least one option
- question ids must be unique within one tool call
- option
values must be unique within a question - optional
label,description, andpreviewfields must not be blank when provided labelfalls back toQ1,Q2, ...typedefaults tosinglerequireddefaults tofalserequiredis metadata only; it never blocks submission- preview questions require preview text for every declared option; option descriptions do not satisfy this requirement, and invalid preview payloads report a fix hint to add preview text or switch to
type: "single" - all questions get an internal
Type your ownoption
Output
{
content: [{ type: "text"; text: string }];
details: {
title?: string;
cancelled: boolean;
error?: {
kind: "invalid_input";
issues: Array<{
path: string;
message: string;
}>;
};
mode: "submit" | "elaborate";
questions: Array<{
id: string;
label: string;
prompt: string;
type: "single" | "multi" | "preview";
}>;
answers: Record<
string,
{
values: string[];
labels: string[];
indices: number[];
customText?: string;
note?: string;
optionNotes?: Record<string, string>;
}
>;
continuation?: {
strategy: "refine_only" | "resume";
affectedQuestionIds: string[];
preservedAnswers: Record<string, {
values: string[];
labels: string[];
indices: number[];
customText?: string;
note?: string;
optionNotes?: Record<string, string>;
}>;
questionStates: Record<string, {
status: "answered" | "needs_clarification" | "unanswered";
}>;
};
elaboration?: {
instruction: string;
nextAction: "clarify" | "clarify_then_reask";
items: Array<
| {
target: { kind: "question" };
question: {
id: string;
label: string;
prompt: string;
type: "single" | "multi" | "preview";
options: Array<{
value: string;
label: string;
description?: string;
preview?: string;
}>;
};
answered: boolean;
answer?: {
values: string[];
labels: string[];
indices: number[];
customText?: string;
note?: string;
optionNotes?: Record<string, string>;
};
note: string;
}
| {
target: { kind: "option"; optionValue: string };
question: {
id: string;
label: string;
prompt: string;
type: "single" | "multi" | "preview";
options: Array<{
value: string;
label: string;
description?: string;
preview?: string;
}>;
};
option: {
value: string;
label: string;
description?: string;
preview?: string;
};
selected: boolean;
answered: boolean;
answer?: {
values: string[];
labels: string[];
indices: number[];
customText?: string;
note?: string;
optionNotes?: Record<string, string>;
};
note: string;
}
>;
};
};
}
Output rules
cancelled: truemeans the user dismissed the flow, UI was unavailable, or the payload was invalid before UI opened- invalid payloads return
error.kind === "invalid_input"with structuredissuesand a transcript-friendlyInvalid ask_user payload:message 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 - in
mode: "elaborate",answerscontains only committed answers; note-only entries move toelaboration.items continuation.strategy === "refine_only"means the next ask should refine the current flow rather than restart itcontinuation.preservedAnswerscontains previously committed answers that should be kept as context and not re-askedcontinuation.affectedQuestionIdslists the only questions that should be revisitedcontinuation.questionStatesmarks each question asanswered,needs_clarification, orunanswered- single-select answers still use arrays
indicesare 1-based rendered option positionscustomTextstores the free-form answer- on single-select questions, saving free-form text clears selected options for that question
- on multi-select questions,
valuesandlabelsinclude both selected options andcustomTextwhen both are present - on multi-select questions, selected options keep their original order and
customTextis appended last - saving or clearing free-form text on a multi-select question does not clear other selected options
notestores a question-level noteoptionNotesincludes only notes for selected options- question notes may exist without a selected answer
elaboration.itemsincludes all question notes and all option notes, even for unselected options- every elaboration item includes the full normalized question and option list for that question so referential notes like
aboveremain understandable to the agent - option-targeted elaboration items include the specific noted option plus whether it is currently selected
- question-targeted elaboration items include whether the question already has a committed answer
elaboration.instructiontells the agent to answer the clarification directly first, then re-ask only the affected questions if a choice is still needed- after clarification, agents should prefer another structured follow-up over plain-text multiple choice when a decision is still unresolved
- once prior answers narrow the branch, agents should bundle the next 2-3 related unresolved questions into one follow-up ask when possible, instead of using a long sequence of single-question asks
elaborationis only present whenmode === "elaborate"- elaborate
contenttext and transcript rendering describe each note directly using the full question prompt and option label, and include the current committed answer text when available, instead of a generic elaboration banner - when the user selects
Elaboratewithout adding notes, elaboratecontenttext and transcript rendering still include the committed answer text so the agent can elaborate on that answer directly
Supported UX
- tabbed multi-question flow
- single-select, multi-select, and preview questions
- inline free-form answers for all question types
- native pi-style
@file path autocomplete inside free-form answer and note editors - question notes via
Shift+N - option notes via
n - number-key quick selection
- submit/elaborate/cancel review tab
- on the review tab,
SubmitandCancelpreview notes only for answered questions - on the review tab,
Elaboratepreview expands to all question notes and all option notes, including notes on unselected options - transcript-friendly call and result rendering
- ask settings modal with
KeymapsandBehaviourtabs ?in the ask flow and/ask-settingsin pi open the same ask settings modalBehaviourexposesAuto-submit when answered without notesas a persisted user settingKeymapsis a persisted config section forcancel,dismiss,toggle,confirm,optionNote, andquestionNote- the
Keymapstab is read-only and shows the active bindings plus the config file path - if the flow is already on the review tab, all questions are answered, and no notes exist, enabling auto-submit can complete the current ask flow immediately
- elaborate results are phrased as direct follow-up instructions, for example:
User asked to elaborate on question "Which option would you like to select?" option "Option A" with note "why this one?"
Keyboard behavior
Main flow:
?: open the ask settings modal on theKeymapstab- inside the ask settings modal,
Esc,Ctrl+C, and?close it; dirty drafts require a second close action within a short window to discard unsaved changes Tab,Shift+Tab,Left,Right: move between tabsUp,Down: move between options1..9: select or toggle the matching option; on the review tab,1,2, and3triggerSubmit,Elaborate, andCancel- the following actions are configurable via persisted
keymaps:Enterby default confirms or submits,Esccancels,Ctrl+Cdismisses,Spacetoggles,nedits the active option note, andShift+Nedits the current question note
Editing flow:
- the configurable
confirmbinding submits the current editor input and closes the editor; in note editors this saves the note only and keeps the ask flow open - the configurable
cancelbinding saves draft and closes the editor - the configurable
dismissbinding dismisses the entire flow immediately without saving the current editor draft ?: open the ask settings modal on theKeymapstab when the editor is empty; otherwise enter?as text- inside the ask settings modal,
Ctrl+Ssaves the current draft config without closing the modal - when editor has text, arrow keys and
Tabstay in the editor so the cursor can move while typing - when editor is empty,
Up/Downmove options andTab/Shift+Tab/Left/Rightmove between tabs without requiring the configurable cancel key first - navigation resumes only after closing the editor with the configurable
cancelbinding, unless the editor is empty and the navigation keys above are used
Non-interactive mode
If ctx.hasUI === false, the tool returns a Needs user input message in content and a cancelled result in details.
Validation is handled inside the tool so malformed calls produce the same structured error shape as other invalid payloads instead of relying on pre-execution schema failures.
The ask flow subscribes to runtime settings updates while open. In practice, this means changing Auto-submit when answered without notes or reloading config-backed keymaps can affect the in-progress ask flow immediately instead of only future asks.
The fallback message includes normalized pending questions and options so the caller can re-ask them manually. details.questions still contains normalized question metadata, while details.answers stays empty until a user responds.
Skill alignment (advisory)
The auto-bundled skill profile at skills/ask-user/SKILL.md defines agent-side decision-gate guidance for when to call ask_user. It is enabled by default when the package is installed, but can be disabled via pi config.
It is advisory only. If there is any conflict, contract + tests win.
Source of truth
Behavior should be verified against:
src/types.tsand exported state/result helperstests/*.test.ts- this contract