Imagine composing AI assistants the way you create reusable React components.
One agent reviews correctness. Another plans the tests. Their outputs flow into a third agent that produces one release recommendation.
Correctness ──┐
├──> Synthesis ──> Release recommendation
Test plan ────┘
That is agent composition: small specialists, explicit dataflow, one final result.
The Codex SDK gives TypeScript applications a clean thread-and-turn API. Composing several threads is still your application's job:
import { Codex } from "@openai/codex-sdk"
const codex = new Codex()
async function run(prompt: string) {
const thread = codex.startThread()
return (await thread.run(prompt)).finalResponse
}
const correctness = await run("Review the current diff for correctness defects.")
const tests = await run("Write the smallest useful test plan for the current diff.")
const coordinator = codex.startThread()
const result = await coordinator.run(`
Write one release recommendation from these reports.
Do not invent findings.
CORRECTNESS
${correctness}
TEST PLAN
${tests}
`)
console.log(result.finalResponse)
This is perfectly reasonable TypeScript. It also makes the application responsible for threads, execution order, result extraction, and prompt assembly. As the workflow grows, the glue becomes the thing you spend time reading.
Agent Markup Language puts that composition into TypeScript and JSX:
function ReleaseReview() {
return (
<Agent provider={Codex} system="Synthesize evidence without inventing findings.">
## Correctness
<Agent provider={Codex}>Review the current diff for correctness defects.</Agent>
## Test plan
<Agent provider={Codex}>Write the smallest useful test plan.</Agent>
Write one release recommendation.
</Agent>
)
}
AML runs the two child agents from left to right. Their outputs land exactly where they appear in the parent prompt. The synthesis agent starts after both are ready.
AML is not React, but the component model is deliberately familiar. Functions package behaviour. Props carry inputs. JSX shows which agent receives each result. Ordinary TypeScript handles conditions.
You can keep the workflow compact, or split a larger one into named specialists:
import { Agent, AmlRuntime, type AmlRenderable, codexAgent, FollowUp } from "@aml-jsx/sdk"
const Codex = codexAgent({})
function CorrectnessReview() {
return <Agent provider={Codex}>Find concrete correctness defects in the current diff.</Agent>
}
function SecurityReview({ paths }: { paths: readonly string[] }) {
return <Agent provider={Codex}>Find security risks in: {paths.join(", ")}.</Agent>
}
function TestPlan({ children, runner }: { children?: AmlRenderable; runner: string }) {
return (
<Agent provider={Codex}>
Write the smallest useful test plan for {children}. Use {runner}.
</Agent>
)
}
function DocsImpact({ publicChange }: { publicChange: boolean }) {
return publicChange ? (
<Agent provider={Codex}>Check whether the current diff changes documented behaviour.</Agent>
) : (
"No public documentation review requested."
)
}
function ReleaseCouncil({
changedFiles,
publicChange,
target,
}: {
changedFiles: readonly string[]
publicChange: boolean
target: string
}) {
return (
<Agent provider={Codex} system="Use only the supplied evidence. Resolve conflicts explicitly.">
## Correctness
<CorrectnessReview />
## Security
<SecurityReview paths={changedFiles} />
## Tests
<TestPlan runner="npm test">regressions around {target}</TestPlan>
## Documentation
<DocsImpact publicChange={publicChange} />
<FollowUp>Decide whether this is ready to release and explain why or why not.</FollowUp>
</Agent>
)
}
const runtime = new AmlRuntime()
await runtime.evaluate(
<ReleaseCouncil
changedFiles={["src/auth.ts", "src/session.ts"]}
publicChange
target="the current diff"
/>,
)
The components now use JSX in different ways: no props, an array prop, nested children, and a boolean that conditionally returns an Agent or plain text. They run in authored order. The parent opens after their outputs have been assembled into its prompt, then FollowUp
asks for the release decision in the same session.
Swap a specialist, reuse it elsewhere, or give it a different provider without redesigning the workflow.
The developer still authors the control flow. Agent output stays data; AML never executes model-generated JSX as a new workflow. Tools and MCP servers also stay scoped to the agent that receives them.
We are building AML so complex agent systems can be read as composed parts instead of a pile of orchestration glue.
AML is open source, MIT licensed, and under active development. Read the code, run the examples, and star the repository on GitHub.