AST and evaluation
AML has three distinct stages. Keeping them separate prevents a common mistake: treating authored JSX as if it were already a provider request.
| Stage | What exists | What it does not do |
|---|---|---|
| Authoring | TypeScript/TSX expressions and component functions | It does not contact a provider just because JSX was written |
| Node construction | Immutable AML JSX descriptors produced by the automatic JSX runtime | It does not invoke the component function |
| Evaluation | AmlRuntime normalizes values and executes runtime-owned primitives | It does not render DOM or accept unknown intrinsic elements |
Authored JSX is ordinary TypeScript
Section titled “Authored JSX is ordinary TypeScript”Configure TypeScript to use AML’s automatic JSX runtime:
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@aml-jsx/sdk" }}The transform turns this:
const workflow = <Agent model="reviewer">Inspect the change.</Agent>into a call equivalent to jsx(Agent, { model: "reviewer", children: "Inspect the change." }). The returned value is data. Holding it in a variable does not run <Agent />.
Type application components explicitly
Section titled “Type application components explicitly”Import AML with import type from @aml-jsx/sdk. It names both the values AML can evaluate and a namespace of component helpers; it adds no runtime value and requires no React types.
| Type | Use it for |
|---|---|
AML | A workflow value or a helper’s return type: JSX, text, numbers, arrays, promises, or empty values. |
AML.Component<Props> | A reusable function component with explicit props and a synchronous or asynchronous AML result. |
AML.PropsWithChildren<Props> | Readonly props with optional nested AML content. |
AML.PropsWithRequiredChildren<Props> | Readonly props whose children property must be supplied. |
Leaf and async components
Section titled “Leaf and async components”AML.Component without a type argument accepts no custom props or children. Declare a props type for inputs; children are never added implicitly. The same component type supports async functions:
import { AmlRuntime, type AML } from "@aml-jsx/sdk"
const Heading: AML.Component = () => "Release review: "
type ChangeProps = { readonly load: () => Promise<string> }const Change: AML.Component<ChangeProps> = async ({ load }) => await load()
const workflow: AML = [<Heading />, <Change load={async () => "Add CSV export."} />]console.log(await new AmlRuntime().evaluate(workflow))// Release review: Add CSV export.Constructing <Change /> does not call load. AML invokes the component when evaluation reaches its node and awaits the returned value. Components run once per evaluation of their node; they do not rerender.
Optional and required children
Section titled “Optional and required children”Use optional children for components that have a useful self-closing form, and required children for composition boundaries:
import { AmlRuntime, type AML } from "@aml-jsx/sdk"
type NoteProps = AML.PropsWithChildren<{ fallback: string }>const Note: AML.Component<NoteProps> = ({ children, fallback }) => children ?? fallback
type SectionProps = AML.PropsWithRequiredChildren<{ readonly title: string}>
const Section: AML.Component<SectionProps> = ({ children, title }) => [title, ": ", children]
const workflow: AML = ( <Section title="Evidence"> <Note fallback="No findings." /> <Note fallback="No findings.">Inspect the changed files.</Note> </Section>)console.log(await new AmlRuntime().evaluate(workflow))// Evidence: No findings. Inspect the changed files.Both children helpers make the resulting props readonly. Children are authored AML values, so return or compose them directly; they are not necessarily resolved strings. Here, ?? selects the fallback only for null or undefined, not for every empty AML value.
Required children means a required property, not guaranteed text: <Section title="Evidence">{null}</Section> is valid, while <Section title="Evidence" /> is a type error. The optional helper also works without custom props: AML.Component<AML.PropsWithChildren>.
AmlRenderable remains the descriptive equivalent of AML. Existing function declarations are valid too: function Section(props: SectionProps): AML describes the same return contract. Use AML for an ordinary workflow factory’s return type and AML.Component<Props> for components authored through JSX. See the runnable component-types example.
What the JSX runtime constructs
Section titled “What the JSX runtime constructs”Internally, the JSX runtime stores a component reference and frozen props in an AML node descriptor. The descriptor class is deliberately not a root-package value export: application code should treat JSX values as JSX.Element or AmlRenderable and hand them to the runtime. The component function is invoked only while an evaluation owns an active component context.
import { AmlRuntime, Agent, type AmlRenderable } from "@aml-jsx/sdk"
const workflow: AmlRenderable = <Agent>Inspect the change.</Agent>
const result = await new AmlRuntime({ agentProvider }).evaluate(workflow)Do not import implementation paths or call a descriptor’s component reference yourself. That bypasses evaluation domains, lexical scopes, placement checks, limits, tracing, and cleanup.
Normalization rules
Section titled “Normalization rules”The evaluator accepts these renderable values:
- strings and numbers, which become text;
null,undefined, and booleans, which contribute no output;- AML JSX descriptors, which are evaluated by their component type;
- promises, which are awaited;
- arrays, which are flattened in authored order;
- fragments, which group children without creating a runtime resource boundary.
Unknown objects, DOM-like intrinsic elements, cyclic arrays, and cyclic nodes are rejected with EvaluationError. AML intentionally does not coerce arbitrary objects into [object Object] prompt text.
Runtime-owned primitive nodes
Section titled “Runtime-owned primitive nodes”Built-in primitive components are marked for special evaluation. They are not ordinary components that merely return a string.
| Primitive | Status | Owner | Meaning |
|---|---|---|---|
<Agent /> | Stable | Agent executor | Collects a request plan and runs a provider session. |
<System />, <Tool />, <Mcp />, <Skill />, <FollowUp /> | Stable | nearest <Agent /> | Adds typed data to one <Agent /> plan. |
<Include /> | Stable | Include evaluator | Reads live UTF-8 prompt content or authors a bounded file reference. |
<Sandbox /> | Stable | Sandbox evaluator | Acquires and narrows an execution lease. |
<Script /> | Stable | Script evaluator | Runs one literal command or shell source on the host or in the active Sandbox. |
<Workspace /> | Stable | Workspace evaluator | Materializes and publishes durable files. |
<File /> | Stable | Filesystem evaluator | Writes a local source or resolved text through the nearest active filesystem. |
<Block /> | Stable | JSX composition | Adds blank-line separation and optional named sections without another scope. |
<>…</> | Stable | JSX runtime | Groups authored values without adding another boundary. |
Placement is part of the contract. For example, <Tool />, <Mcp />, and <Skill /> are valid only in an Agent plan. <File /> requires an active Workspace or Sandbox, while <Include path> requires the same read scope and <Include src> reads from the application cwd. <Script /> may execute on the host without a Sandbox; an enclosing Sandbox changes its execution location. <Block /> is transparent, so descriptors nested inside it keep their normal owner.
<Parallel> is intentionally different from the runtime-owned nodes above: it is a normal exported function component
built on component-local evaluate(). That existing boundary supplies inherited scopes, limits, cancellation, tracing,
and cleanup joining without adding a second evaluator primitive.
Post-order consumers versus lexical scopes
Section titled “Post-order consumers versus lexical scopes”<Agent /> is a post-order consumer: its child content and descriptors resolve before the session starts. <Workspace /> and <Sandbox /> are lexical scopes: each enters before descendants and releases after them.
Function component bodies run as AML descends into their descriptors. Returning AML then lets that value resolve naturally and flow upward; this is return to compose. If the component body instead needs a nested result before it can decide what to return, it must await evaluate(...); this is evaluate to collect. The component mental model shows both forms and why neither happens at JSX authoring time.
<Workspace id="review" provider={workspaceProvider}> <Sandbox provider={sandboxProvider} access="read-write"> <Agent provider={agentProvider}> <Tool use={readTool} /> Inspect the repository. </Agent> </Sandbox></Workspace>Conceptually, this is:
enter Workspace enter Sandbox resolve Agent plan resolve Tool descriptor resolve prompt text execute Agent session release Sandboxpublish/release WorkspaceThe actual runtime uses an explicit frame stack rather than relying on JavaScript recursion. That implementation detail allows asynchronous components and deep trees to share the same depth accounting and lifecycle checks.
Component-local evaluate()
Section titled “Component-local evaluate()”evaluate() is only valid while an AML function component is active. It is AML’s evaluate to collect boundary: the component pauses, the nested value resolves in the current domain, and the call returns its text or validates one <Agent /> result against a Standard Schema-compatible schema before the component body continues:
import { Agent, evaluate } from "@aml-jsx/sdk"import { z } from "zod"
const Finding = z.object({ severity: z.enum(["low", "high"]), summary: z.string() })
async function Review() { const finding = await evaluate(<Agent>Inspect the authorization change.</Agent>, Finding) return ( <Agent> Explain this {finding.severity} finding: {finding.summary} </Agent> )}Structured evaluation must resolve exactly one <Agent /> result. Schema failure rejects the domain; unvalidated model text is never returned as if it were typed data.
Read the implementation contract
Section titled “Read the implementation contract”The normative details live in SPEC.md: evaluation model, Agent sessions, and evaluate().