Skip to content

interactions: add the choices kind (AskUserQuestion) — Rust reference + shared schema - #475

Merged
brentrager merged 1 commit into
mainfrom
choices-interaction-rust
Aug 18, 2026
Merged

interactions: add the choices kind (AskUserQuestion) — Rust reference + shared schema#475
brentrager merged 1 commit into
mainfrom
choices-interaction-rust

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Problem

Agents constantly need a structured multiple-choice answer mid-turn ("which plan?", "what can we help with?") but today they free-form the menu in prose, with no server-side validation and no channel-normalized card. Rich Interactions is the framework for exactly this; identity_intake was the first (reference) kind. The design doc named "choice chips / menus" as a candidate — this ships it.

Solution

choices — the second Rich Interaction kind, modeled on Claude Code's AskUserQuestion. Wave 1: the Rust reference kind + the shared JSON-Schema contract the other four servers and the web SDK mirror.

  • Raise tool request_choices { questions, reason } — 1–4 questions, each { question, header (short ≤12-char label), options: [{ label, description }] (2–4), multiSelect? }.
  • Capability choice_chips — a session that declares it gets a parked chips/menu card (interaction_required { kind: "choices" }); text/voice channels inherit the enumerated conversational fallback ("choices = enumerated ask").
  • Values { answers: [{ header, options: [labels], other? }] } — every question keyed by header, with an always-available free-text other escape hatch (mirrors AskUserQuestion's ever-present "Other").
  • Validator validate_choices (shared by the card path's WS handler and the fallback path's submit_interaction tool): every question answered, each selected label offered, single-select takes exactly one pick (label XOR other), multi-select one or more. Invalid → retryable per-question interaction_invalid (never a terminal error). No host effect (choices collects, it doesn't mutate the session).
  • Registered in the default InteractionRegistry; the generic envelope is unchanged (no per-kind enums).

Verification

  • cargo test -p smooai-smooth-operator -p smooai-smooth-operator-server — green (574 passed).
  • Validator unit tests (valid single/multi/other; unknown label, single-select-over-pick, unanswered → per-field errors) + parse_questions contract tests.
  • Park/resume WS integration test (tests/choices_interaction.rs): rich card path (park → interaction_required → invalid submit stays parked → valid submit resumes with canonical payload) and the capability-less conversational fallback.
  • spec/interactions/choices.schema.json (Spec/Values/Payload) + conformance fixtures; the fixture conformance test passes. clippy + rustfmt clean.
  • Docs: Rich Interactions + Protocol Reference updated; choices moved from candidate to implemented (Rust).

The other-language servers (TS / Python / Go / .NET) + web SDK follow as parity work against this reference + schema.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

… + shared schema

Adds `choices`, the second Rich Interaction kind, modeled on Claude Code's
AskUserQuestion: the agent raises 1–4 structured multiple-choice questions and
the turn parks until the visitor picks.

- Rust kind (`rust/smooth-operator/src/choices.rs`): `ChoicesKind` implementing
  the `InteractionKind` seam — `request_choices { questions, reason }` raise
  tool, `validate_choices` server-side validator (every question answered, each
  label offered, single vs multiSelect, always-available free-text `other`
  escape hatch), and the enumerated conversational fallback directive. Capability
  id `choice_chips`. Registered in the default `InteractionRegistry`.
- Shared contract (`spec/interactions/choices.schema.json`): Spec / Values /
  Payload $defs the other four servers + web SDK mirror, plus conformance
  fixtures. Envelope stays generic (no per-kind enums).
- Tests: validator unit tests (single/multi/other/invalid) + a park/resume WS
  integration test (rich card path + capability-less conversational fallback).
- Docs: Rich Interactions + Protocol Reference updated; `choices` moved from
  candidate to implemented (Rust). Changeset added.

Wave 1 of the polyglot effort — this is the reference the ports follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a751647

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@smooai/smooth-operator Minor
@smooai/smooth-operator-web-chat-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager
brentrager merged commit 0e8f36e into main Aug 18, 2026
9 checks passed
brentrager added a commit that referenced this pull request Aug 18, 2026
…ion) (#479)

* python-server: Rich Interactions runtime + choices kind (AskUserQuestion)

Port the Rich Interactions framework runtime and the `choices` kind to the
Python server (Wave 2 of the polyglot effort), mirroring the Rust reference
(PR #475).

Framework (interaction.py): kind-agnostic InteractionKind protocol,
InteractionRegistry catalog (default: choices), and a session-keyed
PendingInteractions park/resume registry generalizing the write-confirmation
ConfirmationRegistry. Per turn, the runner registers per-kind request_<kind>
raise tools (interaction_tools.py): rich channels park on an asyncio future
(emit interaction_required, await submit_interaction, resume with the canonical
payload); text-only channels degrade to the kind's conversational directive and
submit through the generic submit_interaction tool. A submit_interaction
dispatcher action routes values to the kind validator — invalid -> retryable
interaction_invalid (turn stays parked), valid -> resume. Capability gating from
the session's declared `supports` (choice_chips), captured connection-local.

The choices kind (choices.py) mirrors choices.rs: request_choices { questions
(1-4), reason } with 2-4 options + optional multiSelect; validate_choices (all
answered, labels in options, single=one pick XOR other, multi>=1, blank other
dropped, one-pass errors); enumerated fallback directive; capability choice_chips.

Tests: validator unit tests + the park/resume WS integration test (raise ->
interaction_required -> submit_interaction -> resume; invalid-then-resubmit;
fallback without capability), validating the shared choices conformance
fixtures. Full python/server suite green (364 passed); ruff + compileall clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

* python-server: ruff format the new interaction modules + tests

CI runs `ruff format --check`; apply it (line-length reflow only, no logic
change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 18, 2026
…#483)

Port the kind-agnostic Rich Interactions framework and the `choices` kind
to the TypeScript server, mirroring the merged Rust reference (PR #475).

Framework (interaction.ts): the InteractionKind seam, InteractionRegistry
of hosted kinds, a session-keyed InteractionParkRegistry (the interaction
analog of the write-confirmation registry), and the per-kind request_<kind>
raise tool + generic submit_interaction fallback tool. A raise on a session
that declared the kind's render capability parks the turn (the raise tool
awaits inside execute), the server emits interaction_required, and a
submit_interaction action validates + resumes (invalid -> retryable
interaction_invalid, never terminal). A text-only channel degrades to the
kind's conversational fallback directive.

choices kind (choices.ts): request_choices (1-4 questions, header <=12
unique, 2-4 options, optional multiSelect), validate_choices rules mirrored
one-for-one, capability choice_chips. Session `supports` is now persisted
(in-memory + Postgres) and gates rich-vs-fallback per kind; the server hosts
choices by default.

Tests: validator/parse/kind unit tests + a WS park/resume integration test
(raise -> interaction_required -> submit_interaction -> resume; invalid ->
interaction_invalid -> resubmit; mismatch; no-pending; text-only fallback),
all cross-checked against the shared spec/conformance choices fixtures.


Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 18, 2026
…ion) (#484)

Wave 2 of the polyglot Rich Interactions effort — mirror the merged Rust
reference (#475) on the .NET (C#) server, Brent's tsx-dev-agent host.

Framework (generalizes the write-confirmation park/resume kind-agnostically):
- IInteractionKind / InteractionCatalog — the kind catalog (default = choices).
- InteractionParkRegistry — session-keyed park store (peek vs resolve, so an
  invalid submit re-prompts without consuming the park), the interaction analog
  of ConfirmationRegistry.
- RequestInteractionTool / SubmitInteractionTool — the per-kind request_<kind>
  raise tool (rich = park + interaction_required; fallback = conversational
  directive) and the generic submit_interaction fallback tool.
- ProtocolEvents.InteractionRequired / InteractionInvalid — the double-nested
  data.data envelopes matching spec/events/*.schema.json.

The choices kind (mirrors choices.rs exactly): request_choices (1–4 questions,
header ≤12 unique, 2–4 options, optional multiSelect), validate_choices
(all-answered; labels ∈ options; single = one label XOR other, multi = ≥1; blank
other dropped; one-pass errors), fallback directive, capability id choice_chips.

Server wiring: submit_interaction dispatch (validate → resume, invalid →
retryable interaction_invalid, decline path, INTERACTION_MISMATCH /
NO_PENDING_INTERACTION guards, ownership-scoped); per-connection `supports`
capture at create_conversation_session drives the rich-vs-fallback branch;
teardown/cancel unpark. The catalog is a DI-provided capability
(AddSmoothOperatorServer registers InteractionCatalog.Default), so direct
FrameDispatcher construction is unchanged.

Tests: xUnit validator unit tests (ported from choices.rs, + the shared
choices_* conformance fixtures through the C# validator) and a WS park/resume
integration suite (raise → interaction_required → submit → resume; invalid →
stays parked → resubmit; decline; text-only fallback; no-pending error).


Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
brentrager added a commit that referenced this pull request Aug 18, 2026
…#486)

Port the Rich Interactions framework runtime + the `choices` kind to the Go
LocalServer, mirroring the Rust reference (PR #475). Wave 2 of the polyglot
rollout.

- interaction.go: kind-agnostic framework — InteractionKind interface, the
  InteractionKinds catalog, and a per-connection park/resume InteractionRegistry
  (the analog of the write-confirmation ConfirmationRegistry).
- choices.go: the `choices` kind (request_choices raise tool, validateChoices,
  fallback directive; capability choice_chips), mirroring choices.rs.
- turn_runner.go: registers one raise tool per hosted kind — a kind whose
  declared capability parks the turn (emit interaction_required, block awaiting
  submit_interaction), the rest degrade to the conversational fallback. The
  raise tool's toolCall chunk is deferred + emitted before the park (as the
  confirmation path does) so ordering is deterministic.
- dispatcher.go: captures `supports` at create_conversation_session and adds the
  submit_interaction action — validate via the kind, invalid -> retryable
  interaction_invalid (turn stays parked), valid -> canonicalize + resume.
- protocol.go: interaction_required / interaction_invalid event builders.
- server.go: teardown rejects parked interactions (fail-open to continue).

Tests: validator unit tests + shared choices-fixture validation, and a WS
park/resume integration suite (rich resume, invalid-stays-parked, and the
no-capability conversational fallback).


Claude-Session: https://claude.ai/code/session_01YbN45JeWDbcjvFqGJvmVD3

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant