Skip to content

feat(frontend): schema-driven elicitation forms in agent chat - #5155

Merged
ashrafchowdury merged 23 commits into
big-agentsfrom
fe/big-agents-wip
Jul 8, 2026
Merged

feat(frontend): schema-driven elicitation forms in agent chat#5155
ashrafchowdury merged 23 commits into
big-agentsfrom
fe/big-agents-wip

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

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 elicitation interaction 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.kind dispatch, the client-tool round-trip (the proven connect-widget resume path), and AI SDK v6 message parts. The reusable machinery lands once:

  • render.kind is a required wire field. Because AI SDK v6 tool chunks are strict and drop inline fields, it travels as a sibling data-render part and is resolved into a per-message map by buildRenderMap, consulted by both the registry and the resume predicate.
  • All contract logic (payload validator, action envelopes, part-state derivation, format mapping, secret-field denylist) lives in @agenta/shared, pinned by golden fixtures that both vitest and pytest assert, so the wire dialect can't drift across languages.
  • The emitter is a reserved static-catalog client tool, 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({...}) matched request_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 multiline format is non-standard (unlike date-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-text map to multiline; datetime to date-time; url to uri).

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.
  • api pytest: emitted request_input payload 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.
  • Live-verified in the playground: round-trip, settled-state replay after reload, reload-while-pending then accept, and the required-field gate.

What to QA

  • In agent chat, ask the agent to call request_input for 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.
  • Click Accept with a required field empty. You get inline field errors and the run does not resume.
  • Ask for the same form twice in one session. The second call shows a fresh form and pauses, instead of reusing the first answer.
  • Ask for a long-text "notes" field. It renders a textarea, and the value comes back on Accept.
  • Reload after accepting: the settled chip and values replay read-only. Reload while a form is pending: the live form re-renders and still accepts.
  • Regression: an unknown, non-elicitation client tool still shows the neutral "not handled by this client" surface, and existing gateway-tool execution forms (Settings, Tools, run an action) render unchanged.

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 display and config-card kinds are the next two milestones, each gated on its own entry criteria.

ardaerzin added 21 commits July 6, 2026 17:36
…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
…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.
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 8, 2026 11:05am

Request Review

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request feature Frontend labels Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6df93ae0-aee9-4c3c-a814-72a51e101213

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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.

Changes

Elicitation (request_input) feature

Layer / File(s) Summary
Backend static workflow for request_input
api/oss/src/core/workflows/build_kit.py, api/oss/src/core/workflows/static_catalog.py, api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py, api/oss/tests/pytest/unit/workflows/test_static_catalog.py
Adds REQUEST_INPUT_WORKFLOW_SLUG/NAME, extends reserved static embeds to (slug, name) pairs, registers a new client:tool:request_input:v0 catalog entry, and updates overlay/golden fixture tests.
SDK build-an-agent skill guidance
sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py
Instructs agents to gather typed non-secret parameters via request_input, prohibits secrets through it, and adds it to preferred wired tools.
Shared elicitation contract utilities
web/packages/agenta-shared/src/utils/elicitation.ts, .../utils/index.ts, .../tests/fixtures/elicitation_*.json, .../tests/unit/elicitation.test.ts
Adds payload parsing/validation, result builders, degradation/replay-state helpers, and content serialization, with golden fixtures and tests.
Format-aware schema form fields
web/packages/agenta-shared/src/utils/gatewayToolSchema.ts, web/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsx, tests
Adds formats option threaded through schema field building and SchemaForm rendering (DatePicker, TextArea, email/URL validation).
Playground render-map, resume, and queue gating
web/packages/agenta-playground/src/state/execution/renderMap.ts, agentApprovalResume.ts, agentMessageQueue.ts, index re-exports, tests
Adds render-hint map construction/lookup and integrates it into resume and HITL-pending detection for parked client-tool interactions.
Frontend elicitation widget and dispatch
web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx, clientTools/ElicitationWidget.tsx, ClientToolPart.tsx, meta.ts, registry.tsx, types.ts
Adds ElicitationWidget and wires renderMap/degradedEarlierInTurn through message rendering, dispatch, and metadata.
Design documentation
docs/design/agent-chat-interaction-kinds/decisions.md, flow.md
Adds decision record and end-to-end flow documentation for interaction kinds and elicitation.

Runner turn-scoped client-tool output

Layer / File(s) Summary
Turn-scoped tool result extraction
services/runner/src/responder.ts, services/runner/tests/unit/responder.test.ts
Restricts client-tool output extraction to the current turn via new helpers, preventing reuse of prior-turn tool results, with new tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • Agenta-AI/agenta#4859: Both PRs modify agent lane HITL resume logic in agentApprovalResume.ts.
  • Agenta-AI/agenta#4903: Both PRs modify queued-message release gating in agentMessageQueue.ts.
  • Agenta-AI/agenta#4934: Both PRs extend the client-tool dispatch pipeline in ClientToolPart/meta.ts for routing new render kinds.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: schema-driven elicitation forms in agent chat.
Description check ✅ Passed The description is detailed and directly matches the implemented elicitation form flow and supporting fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fe/big-agents-wip

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx (1)

28-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider importing ElicitationResult at the top level instead of inline.

toOutput uses import("@agenta/shared/utils").ElicitationResult inline. Adding type ElicitationResult to 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 win

Memoize renderMap to prevent unnecessary re-renders of ClientToolPart.

buildRenderMap creates a new Map on every render. Since ClientToolPart is wrapped in memo, the unstable renderMap reference breaks shallow comparison and forces all client-tool widgets to re-render on every parent re-render — including every streamed token. useMemo is 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

📥 Commits

Reviewing files that changed from the base of the PR and between a8d2fb8 and 6a62e6a.

📒 Files selected for processing (31)
  • api/oss/src/core/workflows/build_kit.py
  • api/oss/src/core/workflows/static_catalog.py
  • api/oss/tests/pytest/unit/applications/test_build_kit_overlay.py
  • api/oss/tests/pytest/unit/workflows/test_static_catalog.py
  • docs/design/agent-chat-interaction-kinds/decisions.md
  • docs/design/agent-chat-interaction-kinds/flow.md
  • sdks/python/agenta/sdk/agents/adapters/agenta_builtins.py
  • services/runner/src/responder.ts
  • services/runner/tests/unit/responder.test.ts
  • web/oss/src/components/AgentChatSlice/components/AgentMessage.tsx
  • web/oss/src/components/AgentChatSlice/components/clientTools/ClientToolPart.tsx
  • web/oss/src/components/AgentChatSlice/components/clientTools/ElicitationWidget.tsx
  • web/oss/src/components/AgentChatSlice/components/clientTools/index.ts
  • web/oss/src/components/AgentChatSlice/components/clientTools/meta.ts
  • web/oss/src/components/AgentChatSlice/components/clientTools/registry.tsx
  • web/oss/src/components/AgentChatSlice/components/clientTools/types.ts
  • web/packages/agenta-entity-ui/src/gatewayTool/components/SchemaForm.tsx
  • web/packages/agenta-playground/src/index.ts
  • web/packages/agenta-playground/src/state/execution/agentApprovalResume.ts
  • web/packages/agenta-playground/src/state/execution/agentMessageQueue.ts
  • web/packages/agenta-playground/src/state/execution/index.ts
  • web/packages/agenta-playground/src/state/execution/renderMap.ts
  • web/packages/agenta-playground/src/state/index.ts
  • web/packages/agenta-playground/tests/unit/renderMap.test.ts
  • web/packages/agenta-shared/src/utils/elicitation.ts
  • web/packages/agenta-shared/src/utils/gatewayToolSchema.ts
  • web/packages/agenta-shared/src/utils/index.ts
  • web/packages/agenta-shared/tests/fixtures/elicitation_request.json
  • web/packages/agenta-shared/tests/fixtures/elicitation_response.json
  • web/packages/agenta-shared/tests/unit/elicitation.test.ts
  • web/packages/agenta-shared/tests/unit/gatewayToolSchema.test.ts

Comment thread web/packages/agenta-shared/src/utils/elicitation.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.
@ardaerzin

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request feature Frontend lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants