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 />.
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}> <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().