# Result contract

[Documentation index](README.md) · [Authoring guide](authoring-questions.md) · [Architecture](architecture.md)

A completed form produces two related outputs:

1. model-facing text in the tool result (or in the custom message sent by `/rich-questions`), and
2. structured `details` on direct `ask_rich_questions` tool results.

The text is intentionally self-contained so a later model turn does not need the original tool arguments to understand the decision.

## Model-facing text

For each question, `formatRichAnswers` emits:

- normalized question ID and type;
- prompt, body/details, context, recommendation, and caveats when present;
- every predefined option in original order;
- each option's 1-based display position, label, stable value, selected state, recommended state, description, details, presenter note, and user response note when present;
- the “Other / custom answer” entry and custom value when allowed;
- a final selected-answer section;
- text answers, including an explicit `(empty)` marker for an unanswered optional text question.

Form title/context and review-screen additional notes are included when present. Multiline values keep subsequent lines indented under their field.

Example excerpt:

```text
Question 1 (target)
Type: radio
Q: Where should we deploy?
Presented options:
  1. Production
     Value: prod
     Selected: no
     Recommended: no
  2. Staging
     Value: stage
     Selected: yes
     Recommended: yes
     User note: After smoke tests
Selected answer:
  - Label: Staging
    Value: stage
    User note: After smoke tests
```

## Structured `details`

A direct tool call that opens a form returns a `RichFormResult` shaped as follows:

```ts
{
  title?: string;
  description?: string;
  questions: NormalizedQuestion[];
  answers: Array<{
    id: string;
    type: "radio" | "checkbox" | "text";
    value: string | string[];
    wasCustom: boolean;
    selectedOptionIndexes?: number[];
    customValue?: string;
    optionNotes?: Array<{
      optionIndex: number;
      value: string;
      label: string;
      note: string;
    }>;
  }>;
  additionalText?: string;
  cancelled: boolean;
}
```

If the call cannot open a form because Pi is not in TUI mode or no questions were provided, `details` is the smaller error shape `{ cancelled: true }`. Consumers must therefore treat direct-tool details as `RichFormResult | { cancelled: true }`, check `cancelled`, and only read `questions` or `answers` after confirming those fields are present.

On a full `RichFormResult`, `questions` contains normalized labels, options, `allowOther`, and `required` fields plus all original authoring content. A non-empty form produces one answer per normalized question, even when an optional question is left empty.

## Values, indexes, custom answers, and notes

- `value` is a string for radio/text and a string array for checkbox.
- An option's stable value is `option.value`, falling back to its label only when no explicit value was authored.
- `selectedOptionIndexes` contains **zero-based indexes** into that question's normalized predefined `options` array. The model-facing numbered list is one-based for readability.
- Checkbox indexes are sorted in option order. Checkbox `value` contains selected stable values in that order, followed by the custom value when supplied.
- A custom answer sets `wasCustom: true` and `customValue`. For a radio custom answer, `value` is the custom string and no predefined index is selected.
- Text answers currently set `wasCustom: true`; consumers should use `type` rather than interpreting that field as an “Other” marker for text questions.
- `optionNotes` only contains non-empty user-authored response notes on valid predefined options. Each note repeats index, stable value, and label.
- Presenter-authored `options[].note` remains on the normalized question and in model-facing text; it is distinct from `answer.optionNotes`.

## Cancellation

A direct tool call outside TUI mode, a call with no questions, or a user-cancelled form returns `details.cancelled: true`. The first two use the minimal error shape described above; cancellation after a form opens returns the full form result with `cancelled: true`. Callers must check `cancelled` before treating answers as submitted. The slash command does not send partial results when cancelled.

## Compatibility expectations

The project follows Semantic Versioning. Patch releases should fix behavior without intentionally breaking this contract; additive fields are normally backward compatible. A release that removes/renames fields, changes index/value semantics, or materially changes model-facing interpretation should be treated as breaking and documented in [CHANGELOG.md](../CHANGELOG.md).

Consumers should ignore unknown fields, key by stable question IDs and option values, avoid parsing the human-readable text when structured `details` is available, and pin a reviewed package version when exact output is operationally critical.
