Skip to content

docs(spec): add proposals for client-expanded templates and Python fluent builders - #2383

Open
jacobsimionato wants to merge 26 commits into
a2ui-project:mainfrom
jacobsimionato:feat/templates-future-proposals
Open

docs(spec): add proposals for client-expanded templates and Python fluent builders#2383
jacobsimionato wants to merge 26 commits into
a2ui-project:mainfrom
jacobsimionato:feat/templates-future-proposals

Conversation

@jacobsimionato

Copy link
Copy Markdown
Collaborator

Summary

[Part 4 of 4 in Templates Stack - Depends on #2382]

This PR adds two future architecture proposals to the A2UI Templates roadmap under specification/proposals/templates/:

  1. Client-Expanded Templates & Dynamic MCP Federation (client_expanded_templates.md)
  2. Python Fluent Template Builders & Codegen Infrastructure (python_fluent_template_builders.md)

Proposed Architecture 1: Client-Expanded Templates & MCP Federation

This proposal details how A2UI templates can be transported as compact JSON definitions over the wire and expanded directly on the client by the A2UI renderer/framework:

  • Wire Transport: Pre-bundled inside createSurface(templates: [...]) or dynamically streamed via a new updateTemplates protocol message.
  • Component-Level Referencing: Analogous to v1.0's component-level catalogId, components reference remote templates via templateSource: "mcp://github-server/templates".
  • Dynamic On-Demand MCP Resolution: If a client encounters an un-cached template source, it renders an accessible placeholder/skeleton while requesting the template definition from the MCP server endpoint over MCP tools or resources.
  • Client Expansion Engine: Synthesizes internal IDs scoped under the host component ({hostId}_{slot}_{index}_{type}), providing direct two-way reactivity with the client's local DataModel.
  • Declarative Sandboxing: Because templates are pure ASTs validating against template_definition.json, no executable code or arbitrary scripts cross the wire.

Proposed Architecture 2: Python Fluent Template Builders

This proposal details an ergonomic, catalog-driven code generation system for authoring type-safe templates in Python:

  • Ergonomic Codegen Workflow: A CLI tool (a2ui-build-builders) distributed via a2ui-agent-sdk and a2ui-core that ingests any A2UI catalog.json schema and synthesizes strongly typed dataclasses (Card, Column, Row, Text, Button, etc.).
  • Zero-Setup Pre-Bundled Builders: Pre-generates builders for the standard Basic Catalog directly inside a2ui.template.builder, so general developers have zero build steps.
  • Architectural Separation: Strict separation between handcrafted runtime infrastructure (base.py, models.py, serialization protocol) and auto-generated catalog modules (components.py, types.py).
  • Compile-Time Type Safety: Explicit constructor arguments (no untyped **kwargs) eliminate typos like algn="center", while Literal unions enforce valid variant enums in IDE hover popups.

Stack Context

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a server-side UI template expansion engine for the A2UI Python Agent SDK, adding declarative static templates, dynamic server resolvers, and synchronous expansion capabilities. The feedback highlights a few key areas for improvement: ensuring the resolve method in models.py strictly returns a dictionary to prevent downstream type errors, updating the validation guard in processor.py to correctly handle dictionary-based expressions for primitive types, and refactoring nested helper functions in both models.py and processor.py to the module or class level to eliminate redundant recreation overhead during recursive execution.

Comment on lines +794 to +795
return dict(res_val) if isinstance(res_val, dict) else res_val
return dict(res) if isinstance(res, dict) else res

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The resolve method is annotated to return Dict[str, Any], but if the resolver returns None or a non-dictionary type, it returns res_val or res directly. This will cause a TypeError downstream in processor.py when attempting to unpack the resolved data (e.g., {**passed_params, **resolved_data}).

Ensure that resolve always returns a dictionary, falling back to an empty dictionary {} if the resolver output is invalid.

Suggested change
return dict(res_val) if isinstance(res_val, dict) else res_val
return dict(res) if isinstance(res, dict) else res
return dict(res_val) if isinstance(res_val, dict) else {}
return dict(res) if isinstance(res, dict) else {}

Comment on lines +434 to +444
if (
isinstance(p_meta_dict, dict)
and p_type not in ["child", "children", "action"]
and not (
isinstance(params[p_name], str)
and (
params[p_name].startswith("${")
or params[p_name].startswith("__")
)
)
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When a template parameter is a primitive type (like string, number, or boolean), the agent or LLM may pass a dynamic DataBinding (e.g., {"path": "/foo"}), a Concat expression, or a FormatExpr instead of a literal value.

Currently, the validation guard only skips validation if the parameter is a string starting with ${ or __. If a dictionary-based expression is passed, jsonschema.validate will run and fail because a dictionary does not match the primitive type schema (e.g., string).

Update the guard to also skip validation if the parameter value is a dictionary representing an A2UI expression (containing keys like path, concat, format, or param).

                is_expression = isinstance(params[p_name], dict) and any(
                    k in params[p_name] for k in ["path", "concat", "format", "param"]
                )
                if (
                    isinstance(p_meta_dict, dict)
                    and p_type not in ["child", "children", "action"]
                    and not is_expression
                    and not (
                        isinstance(params[p_name], str)
                        and (
                            params[p_name].startswith("${")
                            or params[p_name].startswith("__")
                        )
                    )
                ):

Comment on lines +474 to +507
def map_id(internal_id: str) -> str:
if internal_id == "root":
return instance_id
return f"{instance_id}_{internal_id}"

def resolve_slot(val: Any) -> Tuple[bool, Any]:
if isinstance(val, (ParamRef, TemplateLoop)):
val = val.to_dict()
if (
isinstance(val, dict)
and "param" in val
and "template" not in val
and "item" not in val
):
p_path = val["param"]
found, res = resolve_param_path(p_path, params)
if found:
return True, res
if "default" in val:
return True, val["default"]
elif isinstance(val, str):
if val.startswith("${") and val.endswith("}"):
p_path = val[2:-1]
found, res = resolve_param_path(p_path, params)
if found:
return True, res
elif val.startswith("__PARAM__"):
p_name = val[9:]
found, res = resolve_param_path(p_name, params)
if found:
return True, res
return False, val

def map_child_list(child_list: Any) -> Any:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Avoid defining helper functions like map_id, resolve_slot, and map_child_list inside the _expand_static_layout method. Since this method is called frequently (and recursively) during template expansion, redefining these functions on every invocation introduces unnecessary performance overhead and memory allocation.

Consider refactoring these helpers to the module level or as private methods of the TemplateProcessor class, passing instance_id and params as arguments.

References
  1. Avoid defining helper functions inside loops or nested functions where they are redundantly recreated on every iteration. Define them at the module level to improve performance and maintainability.

Comment on lines +618 to +637
def check_val(val: Any) -> None:
if isinstance(val, dict):
if "param" in val and isinstance(val["param"], str):
check_path(val["param"])
if "concat" in val and isinstance(val["concat"], list):
for elem in val["concat"]:
check_val(elem)
if "format" in val and "args" in val and isinstance(val["args"], dict):
for v in val["args"].values():
check_val(v)
for v in val.values():
check_val(v)
elif isinstance(val, list):
for item in val:
check_val(item)
elif isinstance(val, str):
for m in expr_pattern.finditer(val):
check_path(m.group(1))

def check_path(path: str) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The helper functions check_val and check_path are defined inside the validate_definition method, causing them to be redundantly recreated every time a template is validated.

To improve performance and maintainability, refactor these helpers to the module level or as private methods of the StaticTemplate class (e.g., _check_val and _check_path).

References
  1. Avoid defining helper functions inside loops or nested functions where they are redundantly recreated on every iteration. Define them at the module level to improve performance and maintainability.

@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch from f8c9d7f to 384db67 Compare August 25, 2026 04:05
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch 3 times, most recently from a5a3c99 to 5551454 Compare August 25, 2026 04:34
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch 5 times, most recently from 909da7f to 643cd8b Compare August 25, 2026 05:47
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch from 643cd8b to 452c99a Compare August 26, 2026 01:30
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch 5 times, most recently from 0381f15 to c1c481f Compare August 26, 2026 03:36
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch from c1c481f to 21db203 Compare August 26, 2026 03:53
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch from 21db203 to 358294b Compare August 26, 2026 04:02
@jacobsimionato
jacobsimionato force-pushed the feat/templates-future-proposals branch from 358294b to 9de65d1 Compare August 26, 2026 04:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant