Repository navigation
Description
- I have searched the existing issues to make sure this bug has not already been reported.
Describe the Bug
A CheckRule the agent sends never fails in genui. An agent can say a field is required, the user can leave it empty, and the component reports itself valid. CheckBox, Slider and ChoicePicker all go through the same helper, so it is all of them.
There are two separate reasons, and either one on its own is enough to make validation inert.
The functionCall wrapper is not understood. checksToExpression (widget_helpers.dart:145) wraps the combined condition:
return {
'functionCall': {
'call': 'and',
'args': {'values': checks.map((c) => c['condition']).toList()},
},
};evaluateConditionStream does not read that shape, and what it cannot read it treats as true. Measured against main, with /email holding an empty string:
| expression | result |
|---|---|
{call: required, args: {value: {path: /email}}} |
false |
{call: and, args: {values: [false]}} |
false |
{functionCall: {call: and, args: {values: [false]}}} |
true |
{functionCall: {call: noSuchFunction, args: {}}} |
true |
The last row is the one that worries me: a function that does not exist passes, so nothing fails loudly enough to be noticed.
and sees its arguments unresolved. Dropping the wrapper is not enough. _isTruthy (basic_functions.dart:52) is:
bool _isTruthy(Object? value) {
if (value is bool) return value;
if (value == null) return false;
return true;
}and AndFunction.executeSync reads args['values'] straight out of the map. A {path: ...} or a nested {call: ...} arrives as a Map, which is not a bool and not null, so it counts as true:
| expression | result |
|---|---|
{call: and, args: {values: [false]}} |
false |
{call: and, args: {values: [{path: /no}]}} where /no is false |
true |
{call: and, args: {values: [{call: required, args: {value: {path: /email}}}]}} |
true |
or and not read the same helper.
Steps to Reproduce
final model = InMemoryDataModel();
model.update(DataPath('/email'), '');
final context = DataContext(
model,
DataPath.root,
functions: BasicCatalogItems.asCatalog().functions,
);
final checks = <JsonMap>[
{
'condition': {
'call': 'required',
'args': {'value': {'path': '/email'}},
},
'message': 'Required.',
},
];
// false, as it should be.
print(await context.evaluateConditionStream(checks.first['condition']).first);
// true, with the same rule, through the path the catalog actually uses.
print(await context.evaluateConditionStream(checksToExpression(checks)).first);Expected Behavior
A rule whose condition resolves to false makes the component invalid, and an expression the engine cannot resolve fails loudly rather than passing.
Additional Context
One more thing that falls out of this, and it is a design question rather than a bug: checksToExpression keeps only the conditions and drops every message. CheckRule requires one ("The error message to display if the check fails") and it is the only part of the rule a person ever reads. Combining the rules into a single and means the client knows it is invalid but not what to say about it, so the basic catalog can only colour a field red. Evaluating the rules one at a time costs nothing more and keeps the sentence the agent wrote.
Happy to send a PR for either half if the shape above looks right to you.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
nan-yu commentedon Sep 28, 2026
A2UI Triage: Thanks for reporting. We will investigate the CheckRule validation dispatching in genui. cc @polina-c
jacobsimionato commentedon Sep 29, 2026
We investigated this issue across the A2UI specification and reference implementations in other languages (TypeScript
web_core, SwiftA2UICore, and Pythona2ui_core), and opened a fix in flutter/genui#1045.Here is a summary of the analysis and why the fix was implemented this way:
1. Protocol Specification Analysis (
DynamicBooleanvsAction)common_types.json, aCheckRule'sconditionis typed as aDynamicBoolean, which is a union of a literalboolean,DataBinding({"path": ...}), orFunctionCall({"call": "...", "args": ...}).'functionCall'envelope only exists underActionto distinguish between{"event": ...}and{"functionCall": ...}. It does not belong on dynamic expressions or boolean conditions.{'functionCall': {'call': 'and', ...}}inchecksToExpressionwas invalid protocol syntax, which causedDataContext._evaluateStream(which looks forpathandcall) to treat the expression as an opaque literal object.2. Multi-Language Comparison on List Resolution & Logical Functions
catalog.json, theandandorfunctions specifyargs.valuesas an array ofDynamicBooleans (minItems: 2).renderers/web_core/src/v0_9/rendering/data-context.ts),resolveSignalandresolveDynamicValueexplicitly checkArray.isArray(value), recursively resolving each array item withthis.resolveSignal(item)/this.resolveDynamicValue(item)before combining them.NodeResolver.swift) and Python (generic_binder.py/function_impls.py), arguments and dynamic values inside collections are similarly evaluated recursively so thatandandorreceive evaluated booleans.genui,_evaluateStreamonly handledMapobjects and fell back to treatingLists as literals. Consequently,AndFunction.executeSyncreceived rawMaps ({'path': ...}or{'call': ...}) which_isTruthyincorrectly treated as truthy because they were non-null and not booleans.3. Preserving
CheckRule.message(The Design Question)CheckRule.messageis required in v0.9 ("The error message to display if the check fails").web_core(generic-binder.ts), Swift (ResolvedCheck.swift), and Python (generic_binder.py), each check rule is evaluated individually and active failures are collected intovalidationErrors: string[].genui,TextField,DateTimeInput, andButtonwere already usingValidationHelper.validateStream, which evaluates rules independently and retains the authored message. OnlyCheckBox,Slider, andChoicePickerhad usedchecksToExpressionwith hardcoded fallback strings ('Invalid value','Invalid selection').CheckBox,Slider, andChoicePickertoValidationHelper.validateStreamaligns them with the rest ofgenuiand with the other A2UI renderers, ensuring agent-authored messages are displayed to users.4. Truthiness and Handling Failed / Unknown Expressions
evaluateConditionStreamingenuipreviously didreturn v != null;, which caused any unhandledMapto silently pass validation astrue.isTruthy(Object? value)helper that properly evaluates primitives (bool, non-emptyString, non-zeronum, non-emptyList) and structuredValidationResultobjects (v['valid'] == true), while ensuring unknown functions or unresolvable expression maps evaluate tofalse.The pull request with the fix, unit tests, and an end-to-end integration test reproducing this issue is available at: flutter/genui#1045