@fillament/agent
v0.2.0
Published
The agent contract layer for Fillament forms: a self-describing form contract agents can read before filling, and typed, machine-parseable validation errors (field path + code) agents can correct against like a 422. Same validation engine, two consumers.
Maintainers
Readme
@fillament/agent
The agent contract layer for Fillament forms. Two primitives that turn a form from something an agent poke at into something an agent can read and reason about:
- A self-describing form contract — the form advertises its own fields, types, constraints, and semantics, so an agent reads a contract instead of inferring one from the DOM.
- Typed, machine-parseable validation errors — when validation fails, agents get a structured error body (field paths + codes), the way they'd correct against a 422 from a real API. Humans get the strings; agents get the codes. Same validation engine, two consumers.
pnpm add @fillament/agentTree-shakeable, side-effect-free. Pure over the form's introspect() + state — it never mutates the form, and it adds zero changes to @fillament/core.
describeForm — the form's self-description
import { describeForm } from "@fillament/agent/describe";
const contract = describeForm(form);{
"name": "signup",
"isValid": false,
"canSubmit": false,
"fields": [
{ "path": "email", "type": "string", "format": "email", "required": true,
"sensitive": false, "visible": true, "value": "ada@", "dirty": true,
"errors": [{ "code": "invalid_email", "message": "Invalid email" }] },
{ "path": "password", "type": "string", "required": true, "constraints": { "minLength": 8 },
"sensitive": true, "visible": true, "value": "[redacted]" },
{ "path": "plan", "type": "string", "enum": ["free", "pro"], "default": "free",
"required": false, "sensitive": false, "visible": true, "value": "free" },
{ "path": "contacts", "type": "array", "constraints": { "minItems": 1 },
"required": false, "sensitive": false, "visible": true },
{ "path": "contacts[].name", "type": "string", "required": true, "sensitive": false, "visible": true }
],
"jsonSchema": { "type": "object", "required": ["email", "password"], "properties": { /* … */ } }
}Each field is addressable: path is a dot-path you can hand straight to form.setValue (or @fillament/webmcp's _fill). Array item shapes are advertised with a [] template segment (contacts[].name); replace [] with a concrete index to address a row.
| Field on each entry | Meaning |
| --- | --- |
| path | Dot-path (address.city, contacts[].email). |
| type | JSON Schema type (string, integer, boolean, array, …). |
| required | Resolved from each level's required[]. |
| label, description | From the schema's title / description (e.g. z.string().describe(...)). |
| enum, format, default | Surfaced from the schema. |
| constraints | minLength, maxLength, pattern, minimum, maximum, minItems, … |
| sensitive | Matched against the redaction list — its value is never emitted. |
| visible | Current conditional-visibility state. |
| value, dirty, errors | Live state (omit with includeState: false). |
Options
describeForm(form, {
includeState: true, // include live value / dirty / errors (default true)
includeHidden: true, // include currently-hidden conditional fields (default true)
redact: ["coupon"], // extra sensitive paths, merged with the built-ins
name: "signup", // contract name (defaults to the form id)
description: "Create your account",
});The contract is built from introspectForm(form) — so the richer your validation schema (Zod / Yup / JSON Schema all implement introspect()), the richer the contract. The raw jsonSchema is always included for agents that prefer to consume JSON Schema directly.
toValidationProblem — errors an agent can act on
import { toValidationProblem } from "@fillament/agent/errors";
await form.validate();
const problem = toValidationProblem(form);{
"valid": false,
"errors": [
{ "path": "email", "code": "invalid_email", "type": "schema", "message": "Invalid email" },
{ "path": "address.city", "code": "required", "type": "required", "message": "Required" }
],
"formErrors": [
{ "path": "", "code": "503", "type": "server", "message": "Service unavailable" }
]
}A flat, typed list keyed by path ("" for form-level errors), carrying code, type, source, and meta straight off each FormError. An agent fixes the reported paths and retries — exactly the loop it runs against a real API's 422.
validationResultToProblem(result) converts a raw ValidationResult (e.g. from running an adapter directly on a server) into the same shape, so client and server can speak one error vocabulary.
Who consumes the contract
@fillament/agent is the substrate the rest of the agent-facing ecosystem builds on — one form description, many consumers:
@fillament/webmcp— its_describetool returnsdescribeForm(form)(the contract an agent reads before filling), and its_get_state,_fill, and_submittools include aproblembody fromtoValidationProblem(form)alongside the human-readable error strings.@fillament/ai— its in-browser autofill builds the model prompt fromdescribeForm(form)and reusesbuildRedactPredicatefor sensitive-value redaction, so the local LLM reads the same contract a remote agent would.@fillament/server— turns server-side validation into the sameValidationProblemshape viavalidationResultToProblem, so a React 19 server action and the browser speak one error vocabulary.
You can use @fillament/agent on its own, too — to render an admin "form contract" view or to power your own agent transport.
Exports
| Entry | Export | Purpose |
| --- | --- | --- |
| @fillament/agent | describeForm, toValidationProblem, validationResultToProblem, defaultIsSensitivePath, buildRedactPredicate | Everything, one import. |
| @fillament/agent/describe | describeForm | The self-describing contract. |
| @fillament/agent/errors | toValidationProblem, validationResultToProblem | Structured validation errors. |
Types: FormContract, FormFieldContract, FieldConstraints, DescribeOptions, ValidationProblem, ValidationProblemItem.
License
MIT © headlessButSmart
