Skip to content

Add <Answers>: provide elicitation responses as a document construct #267

Description

@taras

Revised after owner interview (2026-08-02): matcher-based <Answer> children replace the original ordered values array. The template engine is borrowed from TestAgent.

Motivation

A component that uses <Elicit> internally asks whoever the host's provider reaches. Sometimes the surrounding document already knows the answers: a workflow exercising a third-party component non-interactively, a demo, a region of a run that should not stop for a person. Today the only way to supply answers is scriptElicitations() in an eval block — TypeScript, sanctioned only for tests. Providing answers should be a document construct (Code Rule 11).

<Answers>
  <Answer template="Approve {?what}?" value={{ decision: "approve" }} />
  <Answer value={{ decision: "reject", note: "unreviewed" }}>
Deploy {?service} to production?
  </Answer>

  <ReviewGate plan={plan} as="verdict" />
</Answers>

Contract

<Answers> is elicitation middleware as a component: it installs a scoped provider around its children's expansion. Its <Answer> children are matchers; every other child is the wrapped body, rendered transparently — the wrapper changes who answers, never what the body produces.

<Answer> matchers

  • template prop for a single-line template, or children for a multiline one — supplying both is a configuration error (the <WhenPrompt> convention).
  • Templates match the whole rendered message of an elicitation: literal text constrains, {?name} matches any text and binds nothing (wildcard only in this slice — capture-into-value is deferred until a use case demands it), {binding} interpolates an existing binding and requires it at that position.
  • An <Answer> with no template matches any message.
  • value (required) — the answer, as an expression or captured JSON text. Parsed at the wrapper, so a malformed value is reported where it was written, not disguised as a provider failure inside a child.
  • <Answer> is meaningful only as a direct child of <Answers>; anywhere else it is a positioned diagnostic, mirroring <Else> outside <If>.
  • An <Answers> with no body — self-closing, or nothing but <Answer> children — is a positioned configuration diagnostic: it can never answer anything, so writing it is a mistake worth catching (owner ruling, 2026-08-02, from PR ✨ Add <Answers>/<Answer>: supply elicitation responses as a document construct #269 review). How the wrapper receives its matchers (slots or expansion machinery) is the implementor's plan question, not contract.

Selection

  • The first declared matching <Answer> answers. Matchers are reusable: one answers every elicitation it matches for the wrapper's lifetime. Consequence, stated deliberately: declaration order is significant, and a broad template above a narrow one shadows it permanently — first-wins plus reusable is the whole selection rule.
  • An <Answer> that never fires is not an error.

Unmatched elicitations

  • delegate (optional boolean on <Answers>, default false):
    • false: an elicitation no matcher answers fails, with a diagnostic naming the actual message and the templates tried (the expected/actual style of PromptMismatchError).
    • true: the middleware calls next — the elicitation walks outward through enclosing <Answers> to the host's provider.
  • Nested <Answers>: installed at { at: "min" }, the nearest region answers first; with delegate the chain composes outward.

What core already guarantees (unchanged)

  • Every provided answer is judged by core against the asking component's schema before it binds — <Answers> performs no validation beyond JSON parsing.
  • Replay restores recorded answers through the ordinary Add provider-neutral Elicit through the core Context API #197 durability path; matchers see nothing on replay.
  • Unmarked, fail-fast under 💥 Fail fast on function-component failures, collect explicitly #251's default. <Answers> and <Answer> are claimed structural names like <Test>/<WhenPrompt> — implemented as an expansion hook receiving raw children, and therefore not overridable by repository files (owner ruling, 2026-08-02, mechanism-plan review: a construct that needs raw children is structural, and a repository function component could not implement matcher semantics).

Borrowed machinery

The template engine is packages/test-agent/src/template.ts — pure, dependency-free parseTemplate/matchPrompt with whole-string anchoring, wildcard holes, binding interpolation, and expected/actual mismatch errors. Hoist it to core and have test-agent import it from there, following the schema-walk/canonicalize consolidation pattern from #264: dependency direction only works that way, and TestAgent's behavior and diagnostics must be provably unchanged (its suites are the evidence). <WhenPrompt>'s staged model — every prompt must match the active stage — is deliberately not borrowed: <Answers> is an unordered matcher set, per the middleware framing.

Acceptance criteria

  • The example above answers a nested component's elicitation with no host provider installed, choosing by template.
  • First-declared-wins proven with two overlapping templates; reusability proven with one matcher answering two elicitations.
  • Templateless <Answer> matches anything; {binding} interpolation constrains; {?name} binds nothing.
  • delegate={false} fails an unmatched elicitation with the templates-tried diagnostic; delegate={true} reaches an observing outer provider; nested regions compose.
  • A provided answer failing the asking schema fails that elicitation with normal validation diagnostics.
  • A misplaced <Answer>, a bodyless <Answers>, a template parse error, both template forms at once, and a non-JSON value are each positioned configuration diagnostics.
  • Replay restores without matching.
  • TestAgent suites pass unchanged against the hoisted engine.
  • The {binding} asymmetry between the template= prop (engine resolves the binding) and the children form (markdown interpolation substitutes it first) is inherited from <WhenPrompt> and documented in the spec for both constructs — net matching constraint identical, diagnostics and absent-binding edges differ (owner ruling, 2026-08-02).
  • Spec section, site documentation, and colocated markdown coverage ship in the same PR.

Sequencing

Next up (owner decision, 2026-08-02) — proceeds in parallel with the #197 chain's PR 2; nothing on #76/#260 depends on it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions