AI agents
A form built with this library is self-describing: it hands an AI agent (or any automation) the questions, their allowed values and the precedence rules, so answers can arrive unattended - without the agent ever reading your form's source. The facade exposes three calls for this - agentHelp(), schema() and validate() - and you surface them in your own tool, so an agent can discover the form the moment it meets it.
The answer schema
agentHelp() returns a JSON Schema (draft 2020-12) of the answers - the object an agent supplies, keyed by question id. Each property carries its type and allowed values (a select's enum, a number's minimum/maximum), its title and description, its default, and the env variable that sets it. What it deliberately doesn't name is CLI flags: the flags an agent ultimately calls are yours to define. You retrieve the schema and fold it into your own help - an "AI agents" section of --help, a dedicated flag, a generated README:
use DrevOps\PhpTui\Tui;
// The library hands back the schema; the consumer decides where it goes.
echo (new Tui($form))->agentHelp();
The schema the produce-order form emits:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "Order name",
"env": "PHPTUI_NAME"
},
"fruit": {
"type": "string",
"enum": ["apple", "banana", "cherry"],
"title": "Fruit",
"default": "banana",
"env": "PHPTUI_FRUIT"
},
"quantity": {
"type": "integer",
"minimum": 1,
"maximum": 99,
"title": "Quantity",
"default": 6,
"env": "PHPTUI_QUANTITY"
},
"organic": {
"type": "boolean",
"title": "Organic only?",
"default": false,
"env": "PHPTUI_ORGANIC"
},
"certifier": {
"type": "string",
"title": "Certifier",
"x-asked-when": {"field": "organic", "eq": true},
"x-required-when-asked": true,
"env": "PHPTUI_CERTIFIER"
}
},
"required": ["name"],
"x-precedence": ["provided", "environment", "discovered", "derived", "default"]
}
Each env is the variable that sets its answer - the uppercased id under the active prefix, which defaults to PHPTUI_ and changes with ->envPrefix('MYAPP_') on the form or new Tui($form, env_prefix: 'MYAPP_'). A field that names its own variable advertises that name instead, and any further names it answers to appear beside it in x-env-aliases - see naming the variables. The root x-precedence is the resolution order described below.
Required, and required when asked
The root required array is an assertion a JSON Schema validator acts on, so it lists only the questions every run asks. A question the answers can take off the form - certifier above, which exists only while organic is true - stays out of it, because listing it would refuse a payload that correctly omits a question nobody asked.
What such a question owes travels with the property instead: x-asked-when says when it's asked and x-required-when-asked says it's owed on exactly those runs. Read together they say what required can't, and neither ever rejects a payload the form itself accepts. So an agent resolves requiredness in two steps - satisfy required always, and satisfy x-required-when-asked whenever the matching x-asked-when holds against the answers it has.
if/then composition would express this in standard keywords, and it isn't available honestly: a contains condition reads a list or a substring depending on what the answer turns out to be, and a bare field reference tests for a truthy value, so neither has one JSON Schema form. A partial translation would be worse than none, because an agent would trust it. validate() composes the real rules against a real answer set and stays the authority on whether a payload is complete.
A field's other two guidance texts travel beside its description as extension keywords, so an agent reads all three rather than one merged text:
"crop": {
"type": "string",
"title": "Crop",
"description": "The crop this basket was picked from.",
"x-help": "Type a few letters to filter.",
"x-placeholder": "E.g. Golden Beetroot",
"env": "PHPTUI_CROP"
}
Neither is a standard JSON Schema keyword, and neither is examples: a placeholder illustrates the shape of an answer without being a valid one. A field that declares neither carries neither key.
Dynamic defaults - the fn (Context $c): mixed closures from field behavior - are resolved for both agentHelp() and schema(), so a computed default shows a concrete value rather than null. Pass a context to seed the directory or version those closures read - agentHelp(new Context(version: '2.0')) - while the answers stay empty, since none are collected yet. A default that can only be computed from earlier answers has nothing to resolve from and reads as null unless the field declares a ->schemaDefault(...) stand-in.
Full metadata and validation
schema() is the fuller, raw description: the same questions with every declared attribute - options, bounds, and the internal when, derive and discover rules - under a prompts key. Reach for it when your tooling wants the complete picture rather than the answer contract:
{
"prompts": [
{
"id": "name",
"type": "text",
"label": "Order name",
"description": "",
"help": "",
"placeholder": "",
"options": [],
"options_dynamic": false,
"default": "",
"required": true,
"env": "PHPTUI_NAME",
"env_aliases": [],
"min": null,
"max": null,
"step": null,
"min_selections": null,
"max_selections": null,
"min_date": null,
"max_date": null,
"week_start": null,
"template": null,
"placeholders": [],
"when": null,
"asked_when": null,
"derive": null,
"discover": null,
"depends_on": []
}
]
}
Every prompt carries every key, whether or not the field declares it - so a reader never has to tell a missing key from an unset one. Markup and progress blocks are absent: they collect no answer, so they are not prompts anything drives or validates.
A conditional panel takes everything it holds off the form with it, so a prompt inside one waits on the section's rule as well as its own. when is the prompt's own rule and asked_when is the whole rule that decides whether the question is asked - the section's and its own combined into a single condition, in the same shape, so whatever evaluates one evaluates the other. depends_on names every answer that whole rule reads:
"when": {"field": "certifier", "eq": "Soil Board"},
"asked_when": {
"all": [
{"field": "organic", "eq": true},
{"field": "certifier", "eq": "Soil Board"}
]
},
"depends_on": ["organic", "certifier"]
Here required is the flag as declared - what the question owes when it is asked - so a required prompt carrying an asked_when is read as owing an answer only on the runs that rule allows. The answer schema carries the same rule as x-asked-when, omits it for a question nothing gates, and marks the gated-but-required ones with x-required-when-asked as described above.
validate() checks an answer set against those rules before collection, so an agent can confirm a payload without running the form. Each violation is one message; an empty list means the answers are valid:
$errors = $tui->validate(['name' => 'Weekly Box', 'fruit' => 'grape', 'quantity' => 500]);
// [
// 'Question "fruit": value "grape" is not one of: apple, banana, cherry.',
// 'Question "quantity" must be between 1 and 99.',
// ]
Because it has real answers to resolve the rules against, it's the one place a payload is finally judged complete - and it composes a section's rule with a question's own exactly as asked_when describes:
// The section is not there, so nothing owes an answer for the question inside it.
$tui->validate(['name' => 'Weekly Box', 'organic' => false]);
// []
// The same payload plus the answer that puts the section on the form.
$tui->validate(['name' => 'Weekly Box', 'organic' => true]);
// ['Missing required question "certifier".']
Precedence and environment variables
The schema's x-precedence spells out how every field resolves - the first source that provides a value wins:
- Provided - an explicit value you pass in, however your interface accepts one. Highest precedence.
- Environment - the per-question variable named in
env(e.g.PHPTUI_NAME), which is the uppercased id under the active prefix unless the field named its own, followed by each name inx-env-aliasesin declaration order; the first one that is set wins. - Discovered - a value detected from the target directory (see Discovery).
- Derived - a value computed from other fields.
- Default - the declared default.
Runnable example
playground/08-headless-agent-cli.php folds agentHelp() into a consumer tool's help, and agentHelp(), schema() and validate() each have a script of their own beside it in playground/08-headless-*.
See also Headless collection for driving the same form from CI and self-describing answers for what comes back.