Skip to content

feat(acp): implement the ACP v1 elicitation family (form mode) - #159

Open
YoungSx wants to merge 3 commits into
developfrom
feat-acp-elicitation-146
Open

feat(acp): implement the ACP v1 elicitation family (form mode)#159
YoungSx wants to merge 3 commits into
developfrom
feat-acp-elicitation-146

Conversation

@YoungSx

@YoungSx YoungSx commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Closes #146. Follow-up for URL mode: #154.

What was missing

The stable ACP v1 elicitation family had zero implementation, so any
standards-compliant agent asking for structured input got -32601 Method not found. The private _interaction.ask_user extension covered the same
scenario, but it is invisible to third-party agents — functional, not
interoperable.

SDK

  • elicitation/create (request) and elicitation/complete (notification).
  • clientCapabilities.elicitation, carried in both the v1 and v2 initialize
    wire forms (elicitation is a root capability in v2, unlike
    fs/terminal/session).
  • Form schema: string / integer / number / boolean / multi-select, including
    titled and untitled enums, formats, and range/length/pattern/count
    constraints.
  • accept / decline / cancel responses.
  • Unknown mode, unknown property schema type, and unknown multi-select item
    types keep their raw payload and round-trip byte for byte instead of being
    rendered as a known variant. ACP requires mode explicitly, so an omitted
    mode is not promoted to form the way MCP would.

Capability gating (fail-closed, two levels)

Situation Response
elicitation not advertised -32601 Method not found
advertised, requested mode not advertised -32602 Invalid params
advertised, mode unknown to this client -32602 Invalid params

URL mode is deliberately not advertised

URL mode obliges the client to open the target in a context the agent's model
cannot inspect, and that platform capability does not exist here yet.
Advertising it would invite OAuth flows this client cannot honour, and the spec
forbids agents from falling back to form mode for those. Tracked in #154.

Chat surface

Form requests join the existing authoritative per-conversation
pending-interaction chain that already owns ask_user — no second state owner.
Session switch, conversation removal, and remote reset clear them alongside
ask_user; the composer is disabled while either is pending.

Submitted content preserves each field's JSON type (integer stays a number,
multi-select stays an array) because the agent re-validates content against its
own schema. A field type the client does not model is skipped rather than
rendered as some other control.

Public API compatibility

The new IAcpClient members ship as default interface implementations and
ClientCapabilities' published constructor signature is unchanged, so the
package validates against the 1.0.0 baseline with no suppressions.

Verification

Check Result
scripts/gates/run-acp-sdk-gates.sh (format, analyzers, tests, pack + ApiCompat vs 1.0.0) pass
SalmonEgg.Acp.Tests 375 pass
SalmonEgg.Application.Tests 120 pass
SalmonEgg.Presentation.Core.Tests 3286 pass
SalmonEgg.Domain.Tests / Infrastructure.Tests / Cli.Tests 125 / 732 / 95 pass
Desktop app build incl. XAML (net10.0-desktop) pass
First commit verified to build and test on its own pass

Both fail-closed gates were confirmed with a temporary throw probe to prove the
tests actually reach the gating branch, not just assert a green result; the
probes were removed and the suites re-run.

Not verified locally: WASM target (the wasm-tools workload is unavailable in
this environment) and Windows/MSIX-specific gates — both are left to CI.

🤖 Generated with Claude Code

Add the stable `elicitation/create` request and `elicitation/complete`
notification, which the client previously answered with -32601 because the
whole family was unimplemented. The private `_interaction.ask_user` extension
covered the same scenario but is invisible to third-party agents, so a
standards-compliant agent had no way to ask for structured input.

Model the request as a tagged union on `mode` with form, url, and passthrough
variants, mirroring the McpServer pattern: an unrecognized mode keeps its raw
payload and round-trips byte for byte instead of being rendered as a known
mode. The same applies to unknown property schema types and unknown
multi-select item types. ACP requires `mode` explicitly and, unlike MCP, does
not default an omitted mode to form, so an absent mode lands in the
passthrough variant rather than being promoted.

Advertise `elicitation.form` by default and leave `url` unadvertised: URL mode
obliges the client to open the target in a context the agent's model cannot
inspect, and that platform capability does not exist yet. Advertising it would
invite OAuth flows this client cannot honour, and the spec forbids agents from
falling back to form mode for those. Tracked separately for follow-up.

Gate the family fail-closed at two levels. An omitted capability object means
the method genuinely does not exist here, so it keeps answering -32601,
symmetric with the fs and terminal gates. An advertised family with an
unadvertised mode answers -32602, which the elicitation spec names explicitly
because only the mode is unavailable. Carry the capability through the v2
initialize wire form too, where `elicitation` is a root capability unlike
fs/terminal/session, so it is not silently dropped.

Refs #146
Project inbound `elicitation/create` form requests into the same authoritative
per-conversation pending-interaction chain that already owns ask_user, rather
than adding a second state owner: the panel state coordinator stores them per
conversation, conversation sync projects them, and session switch, conversation
removal and remote reset clear them alongside ask_user. The composer is
disabled while either interaction is pending.

Build the form from the requested schema with one field view model per
primitive type and validate locally before answering, so range, length,
pattern and selection-count constraints surface next to the field the agent
declared them on. Submitted content keeps each field's JSON type — an integer
stays a number and a multi-select stays an array — because the agent
re-validates the content against its own schema and a stringly-typed payload
would fail that check.

Offer decline and cancel next to submit: the spec requires clear decline and
cancel controls, and an agent must not assume an elicitation succeeds. A field
whose schema type this client does not model is skipped rather than rendered as
some other control, matching the SDK's passthrough contract.

Refs #146
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
salmonegg Ready Ready Preview Sep 2, 2026 1:48am UTC

The formatting gate only inspects files a PR touches, so this pre-existing
violation stayed latent until this branch modified ChatService.cs: `dotnet
format` wants the two trailing comments indented into the preceding case body,
which would misattribute a note about the deliberately unprojected variants to
the `SessionInfoUpdate` arm.

Put them on an explicit `default:` arm instead, which is where they belong
semantically and is what the formatter already accepts.
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