support structured agent questions via ACP forms - #1
Draft
lnittman wants to merge 6 commits into
Draft
Conversation
3 tasks
lnittman
force-pushed
the
feat/acp-form-elicitation
branch
3 times, most recently
from
August 20, 2026 15:29
555b959 to
c2f1205
Compare
…lements The vendored client advertised the whole `Client` surface in its callback type but wired only a few methods, so a consumer that implemented filesystem, terminal, elicitation completion, or a generic extension method saw its handler silently ignored — the request failed as unimplemented with no indication which side dropped it. Registration now follows what the consumer actually provides, including the generic `extMethod` and `extNotification` handlers, so the callback type and the wire behaviour describe the same thing.
An unanswered question can outlive the window that showed it. Keeping the draft in web storage tied it to a renderer that may be replaced, and put agent-supplied content in a store shared with unrelated state. Drafts live in a versioned file under the app data directory, written through a mutex so concurrent windows cannot interleave writes, with a size ceiling so a large form cannot grow the store without bound. Parsing is by version; an unreadable record is discarded rather than trusted.
Berd advertises `elicitation.form` and renders a form request from an agent as a real control rather than prose, returning a typed response on the originating request. The parts that are easy to get wrong, and how they are handled: - A property type this build does not understand is preserved and shown as unsupported rather than rendered as a control that misdescribes it. A required one blocks acceptance instead of being submitted empty, and no value is ever returned for a field the user could not see. - An unsupported mode or scope returns invalid params, so an agent can tell a client limitation from a considered decline. A synthetic user `cancel` would conflate the two. - Untrusted schemas are bounded before rendering — field and option counts, serialized schema bytes, message and description lengths. An oversized form is rejected at the request boundary rather than truncated, since a truncated form can be accepted while misrepresenting what was asked. - Agent-supplied `pattern`s are treated as unevaluable. A native regex cannot be interrupted, so evaluating one is an unbounded cost taken on an attacker's behalf; claiming to enforce it while timing it afterwards would be dishonest about a boundary that does not exist. - Credential-marked fields are unsupported. ACP form mode is specified for non-sensitive data, and a password input here would invite collection this surface should not perform. - Every edit is addressed by question id, so an event from a control that has just been replaced cannot land on its replacement. - Answers to a question whose responder is gone are claimed before they are sent as an ordinary message, so a reattaching responder cannot also be accepted and the backend cannot see both a message and a response. - Drafts are scoped by account, workspace, provider, and connection, and parsed through a versioned schema rather than cast.
A question belongs to one conversation. Rendering it from shared state meant a request raised by a background session could appear over whatever the user was reading, taking the composer of a chat that had asked nothing. Presentation is scoped to the asking session, and a live question always outranks a recovered draft so a draft cannot hide a request an agent is actually blocked on. A security confirmation owns the composer slot alone while it is up; a question waits behind it rather than stacking two interactive surfaces in the same place.
A question asked by a chat the user is not looking at is invisible, and the agent stays blocked until they happen to return to it. The row now carries the waiting state in the attention slot it already owns. A live question and a recovered draft are distinguished rather than merged: only a live one suppresses the running animation, because only a live one means an agent is waiting right now.
Session ids are not unique across accounts, so a draft saved under one account could be matched by a session id belonging to another. Drafts are cleared when the account or workspace changes, before any query cache is invalidated, so a restored answer cannot cross that boundary.
lnittman
force-pushed
the
feat/acp-form-elicitation
branch
from
August 20, 2026 22:02
d9c9abc to
958ec0d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
This is a scoped reference implementation in Luke's fork, not a pull request
against
block/berd. Berd's contribution policy accepts outside participationthrough issues; upstream product/design discussion is
block/berd#114.
End-to-end managed-provider proof depends on the Goose forwarding work proposed
in aaif-goose/goose#11346.
The live captures used an exploratory dirty Goose checkout; this branch does
not vendor or pin those backend changes and is not claiming clean-backend
reproducibility before that prerequisite lands.
Summary
Add ACP form elicitation to Berd's existing chat interaction layer so an agent
can pause for structured human judgment without a prose menu. The implementation
supports single choice, true multi-select schemas, booleans, text/numbers,
typed custom
Otheranswers, direct multi-question navigation, validation,explicit decline/cancel, accessible focus, background attention, and honest
restart recovery.
There is deliberately no Interviews widget or Home node. Rich media, Mermaid,
arbitrary model-authored UI, and Cursor's custom question extension remain out
of scope for this slice.
Architecture
AbortSignalthrough the vendored Goose SDK wrapper.
connection replacement detaches the form so its draft is not destroyed.
answers; a credential-marked field is unsupported rather than rendered, so no
secret value is ever held or serialized.
_askUserQuestionCustomAnswermetadata is preferred, with the currentCodex adapter marker retained as a compatibility input.
Delivery is single-winner: the answer is claimed before it is sent, and a
responder that reattaches after dispatch is settled without being accepted,
so the backend cannot see both a message and a response.
input, typography, status-pill, and sidebar conventions.
Provider truth
custom answers, multi-question navigation, and same-turn continuation.
current ACP adapter serializes question answers through string
oneOf, sotrue multi-select intent is lost before Berd receives the schema.
direct print/JSON provider, which does not expose client tools. Cursor's
cursor/ask_questionextension needs translation at a general ACP adapterboundary; prose is a fallback, not parity.
Interaction and safety details
provider messages as muted context;
inputs, since ACP form mode is specified for non-sensitive data;
as a new message, rather than pretending to replay a dead RPC;
screen; a live question always takes presentation priority over a recovered
draft, so a draft can never hide a request an agent is blocked on;
question waits behind it;
patternis treated as unevaluable rather than executed. Anative regex cannot be interrupted, so running one is an unbounded cost taken
on the sender's behalf; field, option, and byte counts are bounded instead;
unsupported rather than rendered as a control that misdescribes it;
user cancel, so a client limitation is never mistaken for a decision;
asked at once stay distinct;
send or a discard cannot race it;
not unique across accounts;
controls.
Verification
transport, chat, sessions, app shell)
just check, Tauri checks, and warnings-denied Clippy greenA few timing-sensitive suites outside this change — composer mention search and
app-shell focus — flake when a dev backend competes for CPU. They pass in
isolation and fail identically with this branch stashed, so they are noted here
rather than counted. The full Vitest aggregate is ~6,785 tests and was
reproducible with the backend stopped; this head has not been re-measured under
those conditions.
Known limitations
back through several candidates, and an unresolved provider becomes
unknown, so distinct security contexts can collapse into one namespace. Itshould stay memory-only when identity is not verifiable.
whole store rather than just that record, and schema traversal is recursive
with no depth bound.
session's current execution target at send time, so changing provider between
a question and its answer sends the answer to a different provider.
user-entered content is not.
-,0.or1e;email/uri/dateformats are unvalidated; length uses trimmed UTF-16units rather than code points; invalid fields disable the button without a
field-level explanation.
elicitationStore.tsandElicitationField.tsxhave grown past the pointwhere they should be single modules.
Current-work coordination
The final diff is limited to ACP capability/transport wiring, the elicitation
feature, ChatView placement, and the existing sidebar status slot.
The closest active upstream branches remain block#19 (provider/model lifecycle), block#28
(composer quoting), block#67 (memory/chat surfaces), block#113 (shared transcript scroll
container), and block#115 (Changes rail PR context). Rebase/conflict review should
pay special attention to
ChatView.tsx, ACP connection setup, chat i18n, andsession lifecycle files.
Refs block#114
Refs aaif-goose/goose#11346