<!-- GENERATED by scripts/build-llms.mjs from llms/agent-tools.md — do not edit this file. -->

# `lr-tool-param-form`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tool-param-form.js';` (stable tag alias; registers the tag)
- **Class** `LyraToolParamForm`, also available unregistered from `@aceshooting/lyra-ui/components/agent-tools/tool-param-form/tool-param-form.class.js`
- **Family** `components/agent-tools/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 8 parts, 4 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-tool-param-form`

Renders one form control per top-level property of a JSON Schema object, for ad hoc tool invocation or
approval-editing UIs (e.g. "the agent wants to call `create_event(title, attendees, allDay)` — let the
user tweak the arguments before running it"). First-party invention (no Web Awesome equivalent).
With no host name, the `base` part is the accessible `role="group"`; a native external `<label for>`
can name the form-associated host. A non-empty host `aria-label` remains on the host as the sole
aggregate semantic owner, so `base` omits its duplicate role/name. The individual generated fields
keep their own purpose-specific names in every case.

Choosing Boolean Unset keeps an explicit `undefined` value when a schema default exists; deleting
that key or replacing the value with an absent key restores default materialization. Without a
schema default, Unset removes the key.

**Supported schema subset:** a _flat_ object whose properties use one primitive `type`
(`'string'`, `'number'`, `'integer'`, or `'boolean'`), `required` property presence, string `enum`,
primitive `const`, and the `title`/`description`/`default` annotations. Nested objects, arrays, type
unions, `oneOf`/`anyOf`/`allOf`, `$ref`, string/numeric constraints, and schema-valued
`additionalProperties` are not interpreted. An unsupported property type renders a visible fallback
and makes the form invalid instead of being silently accepted. Schemas are bounded to 100 fields
and 500 enum choices per field; exceeding either ceiling leaves only the bounded prefix mounted and
fails the form closed with a localized form-wide error. A null, array, or other malformed property
definition is a schema-shape error, never misreported as a value-serialization failure.

**Exported types:**

- `ToolParamFormPropertyType = 'string' | 'number' | 'integer' | 'boolean'` — the four leaf property
  types this renderer understands
- `ToolParamFormPrimitive = string | number | boolean` — values accepted by the supported `const`
- `ToolParamFormProperty { readonly type: ToolParamFormPropertyType; readonly enum?: readonly
string[]; readonly description?: string; readonly title?: string; readonly default?: unknown;
readonly const?: ToolParamFormPrimitive; readonly autocomplete?: string; readonly spellcheck?:
boolean; readonly autocapitalize?: string; readonly autoCorrect?: string; readonly inputMode?:
string; readonly enterKeyHint?: string }` — one `schema.properties`
  entry. `enum` is only meaningful when `type` is `'string'` (rendered as a `<lr-select>`); `const`
  enforces one exact primitive value; `title` is the display label; `description` is helper text;
  `default` pre-fills a field whenever `value` doesn't already have that key. For a free-form
  string field, `autocomplete`, `spellcheck`, `autocapitalize`, `autoCorrect`, `inputMode`, and
  `enterKeyHint` forward the corresponding native editing hints to the rendered text input;
  `spellcheck` defaults to `true`, and the other hints are omitted unless supplied.
- `FlatToolParamSchema { readonly type: 'object'; readonly properties:
Readonly<Record<string, ToolParamFormProperty>>; readonly required?: readonly string[] }` — the
  (intentionally flat) schema shape this component can render.
- `ToolParamFormValue = Readonly<Record<string, unknown>>` — the clone-owned argument model.

**Properties:**

- `schema: FlatToolParamSchema = { type: 'object', properties: {} }` (attribute: false) — a
  detached, deeply frozen assignment snapshot, capped at 100 fields and required keys and 500 enum
  choices per field. Exceeding a cap keeps the bounded prefix but fails validation closed. Create
  and reassign a new schema after changes; mutating the caller's prior object has no effect.
- `value: ToolParamFormValue = {}` (attribute: false) — a detached, deeply frozen assignment
  snapshot, capped at 10,000 entries per array/plain record, 50,000 total nodes, and 16 nested
  levels. Unsafe or oversized assignments fail serialization closed. Create and reassign a new
  value after changes; mutating the caller's prior object has no effect. It represents exactly
  what the consumer last assigned.
  A field with no entry in `value` but a schema `default` _displays_ (and is _emitted_, via
  `lr-input`) as that default, but the `value` property itself is left alone until the user actually
  edits that field. JSON Schema ordinarily treats `default` as an annotation; this renderer
  deliberately materializes it before validation/submission, so a valid default can satisfy
  `required`.
- `name: string = ''` — submission key for optional native `<form>` participation
- `disabled: boolean = false` (reflected)
- `customError: string | null = null` (attribute `custom-error`) — reflected consumer validation
  message
- `form: HTMLFormElement | null = null` — browser-resolved owner (and an assignable external owner);
  readonly `labels: NodeList`, `validity: ValidityState`, `validationMessage: string`,
  `willValidate: boolean`, and `effectiveDisabled: boolean` expose the native FACE state

**Getters:**

- `effectiveValue: ToolParamFormValue` — a detached, deeply frozen `value` snapshot with every
  property missing from it filled in
  from `schema`'s own `default`; this is what actually renders and what `lr-input`'s detail carries.
  A key the user has explicitly cleared (a real own property set to `undefined`) stays cleared rather
  than snapping back to its default — only a key genuinely absent from `value` falls back.
- `errors: Readonly<Record<string, string>>` — a frozen effective validation-error snapshot.
  Intrinsic errors use their schema property key; a schema-wide/serialization error or consumer
  custom-validity message uses `base`, the whole-control part. It is independent of which fields
  have been visited.
- `formError: string` — a schema-wide/JSON-serialization error that has no honest field key; empty
  when the current effective value is safe to submit.

**Methods:**

- `getForm(): HTMLFormElement | null` — returns the browser-resolved owning form.
- `checkValidity(): boolean` — synchronously updates `ElementInternals` from the current assigned
  snapshots and returns validity without revealing inline errors.
- `reportValidity(): boolean` — performs the same synchronization, reveals all current field/root
  errors, focuses the first invalid generated field when one exists, and delegates to native
  `ElementInternals.reportValidity()`. If `required` contains an unmet key absent from
  `properties`, there is no generated control to focus, so a localized, programmatically focusable
  root error names that dangling key instead.
- `setCustomValidity(message: string): void` — the standard channel for a rejection the schema
  cannot express ("the tool rejected these arguments"). A non-empty `message` raises `customError`
  and becomes `validationMessage`, so the form fails `checkValidity()`, blocks submission, and
  matches `:state(invalid)`; `''` clears it. Two independent layers: this control already raises
  `customError` intrinsically for a malformed schema or an unsupported field type, and
  `setCustomValidity('')` clears only the consumer's layer — a still-malformed schema stays invalid
  with its own message restored, and clearing never forces a form with an unmet `required` property
  valid. The consumer's error survives every intrinsic recomputation in between (each field edit
  re-runs the validity sync) and a `form.reset()`, matching a native control. The message is
  whole-control state exposed as `errors.base`; it is caller-supplied content and is used verbatim,
  never localized.
- `click(): void` — forwards a host click to the first generated field's control, so the form
  behaves like a single control under both a `<label>`-driven and a programmatic click; a no-op
  while `disabled`.

**Events:** `lr-input` (deeply frozen `detail: { value: ToolParamFormValue }` — the full detached
current value snapshot, every property with defaults resolved, not just the field that changed),
`lr-validity-change` (deeply frozen
`detail: { valid: boolean; errors: Readonly<Record<string, string>> }` — deduplicated on
effective native validity, including consumer custom errors and own/fieldset validation barring;
fired once up front at connect time and after every effective change; serialization-only failures
publish their root message as `errors.base` and `formError`), and no-detail `focus`/`blur` events for
generated native text/number inputs. The composed
`<lr-select>` controls already bubble their own focus/blur bridges through the host.
Nested control events (`input`, `change`, `lr-change`, select show/hide, and option mutation) are
contained at the form boundary; consumers receive the single form-level `lr-input` contract.
`lr-invalid` (no detail) is the bubbling/composed, cancelable alias emitted when the complete
parameter form fails a native validity check; preventing it also prevents the native `invalid`
event's default validation UI.

**Slots:** none.

**The required marker.** A field whose key is listed in `schema.required` marks its `[part="label"]`
with the library's shared required marker — the same `::after` rule and the same three properties
every labelled control in the library uses (`--lr-form-control-required-content`,
`--lr-form-control-required-color` and `--lr-form-control-required-offset`), so retuning or
suppressing the marker application-wide reaches this form's fields
too (see `llms/shared.md` → "The required-field marker"). Requiredness here is **per field**, not per host:
the host carries no `required` attribute, so the marker keys off a `data-required` attribute the
component sets on each `[part="field"]` wrapper. That attribute is component-owned bookkeeping —
never write it, and note that `::part(field)[data-required]` is invalid CSS (an attribute selector
cannot follow `::part()`), so it is not a selector hook you can use from outside. Enum and boolean
fields render as `<lr-select>` controls with their own labels. The outer schema validator owns
presence, so the nested control stays `.required=false` while its host receives
`aria-required="true"` for a required property.

**CSS parts:** `base` (the aggregate `role="group"`), `field`, `label`, `control`, `description`,
`error`, `unsupported`, `empty`.
`control` is the native `<input>` for a `'string'` (non-enum) or `'number'`/`'integer'` field — one
shared part name across both the text and number inputs, and deliberately _not_ present on the
`'boolean'`/enum (`<lr-select>`), or unsupported-type fallback branches, which are
composed components with their own part surfaces rather than raw natives. It is purely an additive
external theming hook: the internal `.control` class the stylesheet targets is unchanged.

**Themeable custom properties:** `--lr-tool-param-form-invalid-border-color` (default
`var(--lr-color-danger)`) — border color of an invalid generated native text or number input.
This component-scoped indirection retints invalid borders without changing error text, required
markers, or other danger-coloured surfaces. Shared tokens remain available for the rest of the
form: `--lr-space-l/-xs/-s`, `--lr-color-border`, `--lr-radius`, `--lr-color-surface`,
`--lr-color-danger`, `--lr-color-text-quiet`, `--lr-focus-ring-width/-color/-offset`,
`--lr-opacity-disabled`.

**Optional peer deps:** none — internally renders `<lr-select>` and `<lr-option>`, both bundled
dependencies of this package imported directly, not optional peers.

```html
<lr-tool-param-form></lr-tool-param-form>
<script type="module">
  const form = document.querySelector("lr-tool-param-form");
  form.schema = {
    type: "object",
    properties: {
      title: { type: "string", title: "Title" },
      attendees: { type: "string", enum: ["team", "everyone"], default: "team" },
      allDay: { type: "boolean", title: "All day" },
    },
    required: ["title"],
  };
  form.value = draftArgs;
  form.addEventListener("lr-input", (e) => (draftArgs = e.detail.value));
  form.addEventListener("lr-validity-change", (e) => (formIsValid = e.detail.valid));
</script>
```

This component owns no Submit/Cancel/Approve chrome — a consumer composes it inside their own dialog
(e.g. `<lr-tool-approval-dialog>`) and reads `.value`/`.errors`/`checkValidity()` (or calls
`reportValidity()` right before acting). Fields render in `Object.keys(schema.properties)` order
(insertion order). A `'string'` property with a non-empty `enum` renders as a `<lr-select>` of
`<lr-option>`s; a plain `'string'` renders a text `<input>`; `'number'`/`'integer'` render a numeric
`<input type="number">` (`step="1"` for integer, `step="any"` for number); `'boolean'` renders a
tri-state `<lr-select>` with localized Unset/True/False choices. This preserves the semantic
difference between an absent optional property and an explicit `false`. Enum and boolean
descriptions/errors flow through `<lr-select>`'s `.hint`/`.errorText` control chrome. The outer
component owns JSON Schema validity: `required` means an own property is present, so `''`, `0`, and
`false` are valid present values. Use `{ type: 'boolean', const: true }` together with `required`
for a must-confirm field; the select still does not impose its own nonempty semantics, while the
outer validator enforces both presence and the exact `true` value.

Visible field and root validation errors remain ordinary descriptive text. When user interaction or
`reportValidity()` makes one or more new errors visible, their distinct messages are coalesced into
one addition to the shared assertive light-DOM announcement sink. Initial and reconnect renders
establish a silent baseline, so pre-existing validation state is not replayed.

Optional native `<form>` participation is implemented via `ElementInternals` attached directly in the
constructor (`static formAssociated = true`) rather than a string-value mixin, since this component's
value is a whole object: the value present on first connection is cloned as the native default, and
`formResetCallback()` restores a fresh clone of that default while clearing touched/interaction state.
Consumer-set custom validity remains in force until explicitly cleared, matching native controls. The
`formDisabledCallback(disabled)` tracks inherited fieldset state separately from the author-owned
`disabled` property. JSON serialization is guarded: circular values, `BigInt`, throwing getters/`toJSON`, and
non-finite numbers cannot escape from an assignment or leave stale form data; the form entry is
temporarily removed and `formError`/custom validity are set until a serializable value replaces it.
This is layered on top of the primary `value` + `lr-input`/`lr-validity-change` contract.
The same safe serialized object is used as session-history/autofill state. Restoration accepts only
a JSON object, falls back to `{}` for malformed/non-object state, and does not emit `lr-input`.

**Known gotchas:**

- a schema property whose `type` isn't `'string'`/`'number'`/`'integer'`/`'boolean'` renders an inline
  "Unsupported field type" message exposed as `[part="unsupported"]` and fails closed with custom
  validity instead of throwing or being silently dropped.
- inline per-field errors only render once a field has been visited (`focusout`) at least once, or
  after an explicit `reportValidity()` call — `checkValidity()` alone never reveals them, matching
  every other form control in this library (`<lr-select>`/`<lr-combobox>`/`<lr-model-select>`
  all avoid flashing red before the user has touched anything).
- a key listed in `schema.required` but absent from `schema.properties` still fails closed. Because
  no field exists for that dangling reference, `reportValidity()` renders and focuses a localized
  root error naming the key rather than leaving an invisible, unreachable validity failure.
- `effectiveValue` distinguishes "key absent from `value`" (falls back to `default`) from "key present
  but `undefined`" (stays cleared and counts as absent for `required`) via `hasOwnProperty`, not an
  `=== undefined` fallback check.
- additional value keys are retained and submitted (matching JSON Schema's default open-object
  behavior), but schema-valued/false `additionalProperties` is outside this renderer's subset.
- `value` and `schema` are detached, deeply frozen assignment-time snapshots. Reassign either
  property after changing caller-owned input; direct in-place mutation cannot alter the component,
  and neither its own `checkValidity()`/`reportValidity()` nor native form validation resnapshots the
  original object.
- `lr-validity-change` fires once immediately at connect time even before any user interaction, so a
  form with an unmet required field announces `valid: false` on mount, not only after the first edit.

---
