Dynamic forms and the agent-to-user communication grammar

Overview

Elicitation forms let the agent shape an exchange to fit the moment: instead of a wall of prose, it renders an interactive form whenever a structured turn communicates better than text. A multi-part question becomes one form you answer with a click; a recommendation arrives as weighable cards; a disagreement is shown side-by-side so you can overrule it in one tap. The goal is plainly to improve human/AI communication — make the back-and-forth faster, clearer, and less ambiguous.

Every form is one declarative artifact — a FormSchema of FormQuestions — built from plain data with form_from_dict, validated once with collect_form_response, and rendered by surface-specific renderers. The same artifact renders richly on a widget-capable client and degrades gracefully everywhere else; the answer is validated the same way regardless of surface.

On top of that shared substrate sits a small, growing communication grammar — a family of constructs, each a member of the one form model:

This feature owns the form model, the renderers, the validator, and the MCP tools that expose them. When a construct fires in a conversation is a judgment call governed by the agent's decision routine, not by this subsystem.

Concepts

One substrate, many constructs

A construct is not a parallel system — it is meaning and presentation layered on the single form model. Adding a construct almost never adds a new round-trip or a new validator; it adds a QuestionType and a few optional FormQuestion fields, and reuses the existing answer path.

Construct QuestionType Answer Extra fields
intake single_select / multi_select / … the picked value(s)
decision decision one option rationale, option_notes, recommended
pushback pushback one option user_position (+ reuses recommended, rationale, option_notes)
progress progress one blocked item progress_items (+ reuses recommended, rationale)

decision, pushback, and progress are all presentation-enriched single-selects: the answer is one option, validated by membership exactly like a plain single_select. What differs is the rendering and the framing.

Validation is never skipped (R4)

collect_form_response is the one validator. A missing required field with no default, or a value outside a select's options, raises FormValidationError naming every offending field so the caller can re-ask just those — it never silently accepts malformed input. This holds for every construct, because every construct's answer is a select answer underneath.

Surfaces, and graceful degradation

A form renders three ways, in order of richness:

The terse reply vocabulary (y / go / 1) answers any construct on any surface — a form never blocks a keyboard-only user.

The list render variant (list_style)

A single_select or multi_select can set list_style: "ordered" (numbered) or "unordered" (bulleted) to render its options as a familiar intro-sentence-plus-list shape — each item pickable by mouse or the 1 / 2 / 3 vocabulary — instead of a dropdown or checkboxes. This is presentation only: the answer and its validation are unchanged. It is a render option on the select types, not a separate construct.

Notes & tips