Skip to content

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.

StageWhat existsWhat it does not do
AuthoringTypeScript/TSX expressions and component functionsIt does not contact a provider just because JSX was written
Node constructionImmutable AML JSX descriptors produced by the automatic JSX runtimeIt does not invoke the component function
EvaluationAmlRuntime normalizes values and executes runtime-owned primitivesIt does not render DOM or accept unknown intrinsic elements

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 />.

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.

TypeUse it for
AMLA 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.

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.

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.

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.

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.

Built-in primitive components are marked for special evaluation. They are not ordinary components that merely return a string.

PrimitiveStatusOwnerMeaning
<Agent />StableAgent executorCollects a request plan and runs a provider session.
<System />, <Tool />, <Mcp />, <Skill />, <FollowUp />Stablenearest <Agent />Adds typed data to one <Agent /> plan.
<Include />StableInclude evaluatorReads live UTF-8 prompt content or authors a bounded file reference.
<Sandbox />StableSandbox evaluatorAcquires and narrows an execution lease.
<Script />StableScript evaluatorRuns one literal command or shell source on the host or in the active Sandbox.
<Workspace />StableWorkspace evaluatorMaterializes and publishes durable files.
<File />StableFilesystem evaluatorWrites a local source or resolved text through the nearest active filesystem.
<Block />StableJSX compositionAdds blank-line separation and optional named sections without another scope.
<>…</>StableJSX runtimeGroups 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 Sandbox
publish/release Workspace

The 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.

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.

The normative details live in SPEC.md: evaluation model, Agent sessions, and evaluate().