feat(frontend): schema-driven elicitation forms in agent chat - #5155
Conversation
…in @agenta/shared
- flat-dialect payload validator (top-level primitives/enums, x-ag-* hints)
with a secret-shaped field denylist routing to the degradation surface
- accept/decline/cancel output envelopes + pinned degradation errorText
prefix so emitters can tell 'user declined' from 'client could not render'
- replayable part-state derivation: pending / submitted / declined /
cancelled / degraded, plus the once-per-turn degradation retry cap
- opt-in {formats} option on buildFormFieldsFromSchema; flag off keeps
gateway-tool execution forms byte-identical
- golden fixtures (elicitation_request/response) cross-asserted by the
Python emitter tests
…agenta/playground
- buildRenderMap(parts): collects sibling {type: 'data-render'} parts into
a toolCallId -> RenderHint map, closing the receive-path gap for the
render.kind wire contract
- agentApprovalResume: isClientTool is now map-aware (any render.kind via
the map, hardcoded tool-name set kept as fallback); new export
isPendingClientToolInteraction
- agentMessageQueue: isHitlPending now counts parked client-tool
interactions, so queued user messages hold until forms settle
Adds a formats prop that renders DatePicker for date/date-time, Input.TextArea for multiline, and typed inputs for email/url. Off by default: gateway-tool execution forms render exactly as before.
- ElicitationWidget: inline form from a flat elicitation payload via
SchemaForm({formats: true}); accept/decline/cancel settle through the
structured output channel; settled states replay as read-only chips;
degraded payloads fall back to the apology surface with a retry cap
- registry: elicitation entry at the render.kind tier; docstring and
dispatch diagram rewritten for the new semantics
- ClientToolPart/meta: consume the render map and the
degraded-earlier-in-turn flag; AgentMessage builds both per message
Adds __ag__request_input mirroring request_connection: a reserved
static-catalog client tool with render {kind: 'elicitation'} and input
{message, requestedSchema}, auto-included in the build kit via
_reserved_static_tool_embeds. The tool description steers the model:
structured params before create-ops, no secrets, respect decline.
Emitted payload and response envelopes are asserted against the golden
fixtures shared with the web validator tests.
Compact in-repo record of the locked decisions behind the elicitation / display / config-card interaction kinds: per-kind contracts, render.kind as a required wire field, replayable part states, secrets-off-wire platform flows, milestone gates, and the model-composed-UI out-of-scope ruling with its re-evaluation trigger.
# Conflicts: # api/oss/src/core/workflows/static_catalog.py # api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py
…e stale find_capabilities op name
…led fallback
Upstream replaced the fabricated 'can't handle' error settle with a
neutral {status: not_handled} output on UnhandledClientTool. Update the
registry/dispatcher docstrings and diagram accordingly, and spell out
that elicitation degradation now settles differently: via the pinned
errorText prefix, which stays reserved for degradation.
…request_input The skill predated the reserved request_input client tool: step 1 said 'clarify the ask' with no mechanism, so agents clarified in prose. Point it at request_input for typed values (which actions, non-secret settings, schedule details), keep the no-secrets rule explicit, and add the tool to the wired-tools list.
Companion to decisions.md: a Mermaid flowchart (GitHub-rendered) plus a step-by-step walkthrough of request_input end to end — build-kit emission, the data-render wire contract, render-map dispatch, the payload-validity and user-action decision branches, settle/resume, and replay states. Includes the live-verification status table and the gated next milestones.
# Conflicts: # api/oss/src/core/workflows/static_catalog.py
A browser-fulfilled client-tool output (e.g. request_input) was keyed only by tool name + canonical args across the whole replayed history, so a NEW identical call in a later turn consumed a PRIOR turn's stored answer and resolved without pausing — asking the same thing twice reused the first answer instead of showing a fresh form. Restrict extractClientToolOutputs to the current turn (tool_results at/after the latest user message). A genuine in-turn resume still resolves from its own output; a later-turn identical call finds nothing and pauses for a fresh answer. Approvals deliberately stay full-history (an idempotent grant may carry across turns). Adds cross-turn (must pause) and in-turn (must resolve) regression tests.
request_input's 'multiline' format is non-standard (unlike date-time), so the tool description never documented it and the agent emitted a plain string for long-text fields — giving an Input instead of a TextArea. Document the supported format vocabulary (incl. multiline) in the request_input tool description, and make the schema builder forgiving of the aliases an LLM naturally emits (textarea, multi-line, long-text -> multiline; datetime -> date-time; url -> uri) via a new normalizeStringFormat in @agenta/shared. Adds normalizeStringFormat + builder-alias tests.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds a new "request_input" (elicitation) client-tool workflow spanning backend static catalog registration, SDK skill guidance, shared parsing/serialization utilities, format-aware schema forms, playground render-hint/resume/queue-gating logic, and a frontend ElicitationWidget with dispatch wiring. It also fixes the runner's client-tool output store to scope reuse to the current turn, and adds design documentation. ChangesElicitation (request_input) feature
Runner turn-scoped client-tool output
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx (1)
28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider importing
ElicitationResultat the top level instead of inline.
toOutputusesimport("@agenta/shared/utils").ElicitationResultinline. Addingtype ElicitationResultto the existing import block would be cleaner and consistent with the other type imports.♻️ Optional refactor
import { + type ElicitationResult, buildAcceptResult, buildCancelResult, buildDeclineResult, buildDegradationErrorText, deriveElicitationPartState, parseElicitationPayload, serializeElicitationContent, } from "`@agenta/shared/utils`"And update
toOutput:-const toOutput = (result: import("`@agenta/shared/utils`").ElicitationResult) => +const toOutput = (result: ElicitationResult) => ({...result}) as Record<string, unknown>web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx (1)
371-376: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize
renderMapto prevent unnecessary re-renders ofClientToolPart.
buildRenderMapcreates a newMapon every render. SinceClientToolPartis wrapped inmemo, the unstablerenderMapreference breaks shallow comparison and forces all client-tool widgets to re-render on every parent re-render — including every streamed token.useMemois already imported and used elsewhere in this component.As per coding guidelines: "Optimize React performance by minimizing re-renders, memoizing expensive computations, stabilizing props/functions in lists."
⚡ Proposed fix
- const renderMap = buildRenderMap(message.parts as {type?: string; data?: unknown}[]) + const renderMap = useMemo( + () => buildRenderMap(message.parts as {type?: string; data?: unknown}[]), + [message.parts], + )Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9bd121dd-5c48-4efb-b3ea-be28ffc3b11b
📒 Files selected for processing (31)
api/oss/src/core/workflows/build_kit.pyapi/oss/src/core/workflows/static_catalog.pyapi/oss/tests/pytest/unit/applications/test_build_kit_overlay.pyapi/oss/tests/pytest/unit/workflows/test_static_catalog.pydocs/design/agent-chat-interaction-kinds/decisions.mddocs/design/agent-chat-interaction-kinds/flow.mdsdks/python/agenta/sdk/agents/adapters/agenta_builtins.pyservices/runner/src/responder.tsservices/runner/tests/unit/responder.test.tsweb/oss/src/components/AgentChatSlice/components/AgentMessage.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/ClientToolPart.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/index.tsweb/oss/src/components/AgentChatSlice/components/clientTools/meta.tsweb/oss/src/components/AgentChatSlice/components/clientTools/registry.tsxweb/oss/src/components/AgentChatSlice/components/clientTools/types.tsweb/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsxweb/packages/agenta-playground/src/index.tsweb/packages/agenta-playground/src/state/execution/agentApprovalResume.tsweb/packages/agenta-playground/src/state/execution/agentMessageQueue.tsweb/packages/agenta-playground/src/state/execution/index.tsweb/packages/agenta-playground/src/state/execution/renderMap.tsweb/packages/agenta-playground/src/state/index.tsweb/packages/agenta-playground/tests/unit/renderMap.test.tsweb/packages/agenta-shared/src/utils/elicitation.tsweb/packages/agenta-shared/src/utils/gatewayToolSchema.tsweb/packages/agenta-shared/src/utils/index.tsweb/packages/agenta-shared/tests/fixtures/elicitation_request.jsonweb/packages/agenta-shared/tests/fixtures/elicitation_response.jsonweb/packages/agenta-shared/tests/unit/elicitation.test.tsweb/packages/agenta-shared/tests/unit/gatewayToolSchema.test.ts
Canonicalize schema format hints once in parseElicitationPayload (aliases like 'datetime' -> 'date-time'; unknown formats dropped), so the renderer and serializeElicitationContent operate on the same canonical values instead of each normalizing independently. Fixes a latent divergence where an aliased date format rendered a picker but serialized the raw value. Also memoize renderMap in AgentMessage so its stable reference stops busting the memoized ClientToolPart on nowTick re-renders, and lift the inline ElicitationResult type import to the top of ElicitationWidget. Adds parse-canonicalization and aliased-date round-trip tests.
|
@coderabbitai review |
✅ Action performedReview finished.
|
Context
Every structured interaction in the playground agent chat costs a hand-built React widget. The client-tool registry shipped exactly one (the connect card); any other client tool rendered a raw JSON card or a "this app can't handle that" apology. This adds a schema-driven interaction layer on the same seam, so most future interactions are a payload instead of a new widget.
What this adds
The
elicitationinteraction kind. A platform op can pause a run and request typed input via a flat JSON schema, which renders as an inline antd form: text, number, boolean, enum as a dropdown, date/date-time as a picker, multiline as a textarea. The user accepts, declines, or dismisses; the run resumes with the structured answer; and every settled state replays correctly after reload.It rides the existing seams rather than a new protocol:
render.kinddispatch, the client-tool round-trip (the proven connect-widget resume path), and AI SDK v6 message parts. The reusable machinery lands once:render.kindis a required wire field. Because AI SDK v6 tool chunks are strict and drop inline fields, it travels as a siblingdata-renderpart and is resolved into a per-message map bybuildRenderMap, consulted by both the registry and the resume predicate.@agenta/shared, pinned by golden fixtures that both vitest and pytest assert, so the wire dialect can't drift across languages.request_input, auto-included in the build kit with zero SDK changes.The result: adding another flat-schema interaction is a payload, not frontend code. Adding a whole new kind is one registry entry plus a contract.
Two fixes found while dogfooding
Identical calls no longer collapse (runner). Asking for the same form twice in one session used to skip the second form and reuse the first answer. The client-tool output store was keyed by tool name plus canonical args across the entire replayed history, so a new identical call in a later turn consumed a prior turn's stored output.
Before: turn 2's
request_input({...})matchedrequest_input#<args-hash>from turn 1 and resolved immediately, no form.After: the store only holds the current turn's outputs (results at or after the latest user message), so turn 2 finds nothing and pauses for a fresh form. Approvals are left full-history on purpose (a grant is idempotent across turns).
Multi-line fields render as a textarea. The
multilineformat is non-standard (unlikedate-time), so the tool description never documented it and the agent emitted a plain string, giving a single-line input. The description now documents the format vocabulary, and the schema builder tolerates the aliases an LLM naturally emits (textarea,multi-line,long-textmap tomultiline;datetimetodate-time;urltouri).Tests
@agenta/shared: validator accept/reject, envelopes, part-state derivation, format normalization, secret denylist, and the cross-language golden fixtures.@agenta/playground:buildRenderMap, the map-aware resume predicate, and queue gating for parked client tools.request_inputpayload against the golden, and build-kit membership.services/runner: added cross-turn (must pause) and in-turn (must resolve) regression tests for the store scoping.What to QA
request_inputfor a couple of typed fields (a name text field, a color dropdown). A form appears (not JSON, not the apology surface). Fill it and Accept; the run resumes and the agent reflects your values.Not in this PR
Automated Playwright coverage needs a deterministic way to emit a client tool in a test, which the current mock provider can't do; it's scoped as a separate test-infra change. The
displayandconfig-cardkinds are the next two milestones, each gated on its own entry criteria.