# 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. Version `0.4.0` is validated against Pi `0.84.4` and `0.85.x`; wildcard peers follow Pi's host-bundled package convention and do not mean every future runtime has already been tested. Preferred constrained sampling, sequential scheduling, and optional fullscreen pointer handling do not alter this result shape. 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.
