Files
pi-ask/docs/contract.md
T

10 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 id and prompt
  • 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, and preview fields must not be blank when provided
  • label falls back to Q1, Q2, ...
  • type defaults to single
  • required defaults to false
  • required is 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 own option

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: true means the user dismissed the flow, UI was unavailable, or the payload was invalid before UI opened
  • invalid payloads return error.kind === "invalid_input" with structured issues and a transcript-friendly Invalid 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", 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
  • continuation.affectedQuestionIds lists the only questions that should be revisited
  • continuation.questionStates marks each question as answered, needs_clarification, or unanswered
  • single-select answers still use arrays
  • indices are 1-based rendered option positions
  • customText stores the free-form answer
  • on single-select questions, saving free-form text clears selected options for that question
  • on multi-select questions, values and labels include both selected options and customText when both are present
  • on multi-select questions, selected options keep their original order and customText is appended last
  • saving or clearing free-form text on a multi-select question does not clear other selected options
  • note stores a question-level note
  • optionNotes includes only notes for selected options
  • question notes may exist without a selected answer
  • elaboration.items includes 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 above remain 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.instruction tells 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
  • elaboration is only present when mode === "elaborate"
  • elaborate content text and transcript rendering describe each note directly using the full question prompt and option label, instead of a generic elaboration banner

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, Submit and Cancel preview notes only for answered questions
  • on the review tab, Elaborate preview expands to all question notes and all option notes, including notes on unselected options
  • transcript-friendly call and result rendering
  • ask settings modal with Keymaps and Behaviour tabs; Behaviour may be empty while unimplemented
  • /ask-settings opens the same ask settings modal as ?
  • 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 the Keymaps tab
  • Tab, Shift+Tab, Left, Right: move between tabs
  • Up, Down: move between options
  • Enter: confirm or submit
  • Esc: cancel the flow
  • Ctrl+C: dismiss the entire flow immediately, even while editing a note or free-form answer
  • 1..9: select or toggle the matching option; on the review tab, 1, 2, and 3 trigger Submit, Elaborate, and Cancel
  • Space: toggle the active option
  • n: edit the active option note
  • Shift+N: edit the current question note

Editing flow:

  • Enter: submit the current editor input and close the editor; in note editors this saves the note only and keeps the ask flow open
  • Esc: save draft and close the editor
  • Ctrl+C: dismiss the entire flow immediately without saving the current editor draft
  • ?: open the ask settings modal on the Keymaps tab when the editor is empty; otherwise enter ? as text
  • when editor has text, arrow keys and Tab stay in the editor so the cursor can move while typing
  • when editor is empty, Up/Down move options and Tab/Shift+Tab/Left/Right move between tabs without requiring Esc first
  • navigation resumes only after closing the editor with Esc, 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 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:

  1. src/types.ts and exported state/result helpers
  2. tests/*.test.ts
  3. this contract