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

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