Correlate application spans and run summaries
Correlate concurrent workflow runs with application requests and collect timing summaries for each completed run.
Measure a custom validation phase with application-owned spans, correlate concurrent application requests to AML runId values, and retrieve one run summary per completed evaluation without a global latest-run value.
This recipe assumes the project setup from Getting started. Its provider is deterministic; no model credentials are needed.
Runtime and request correlation
Section titled “Runtime and request correlation”A shared runtime can execute several roots concurrently. Use application request context to associate the runtime’s public start event with the request that initiated it:
const requestContext = new AsyncLocalStorage<string>()const runIdsByRequest = new Map<string, string>()const summaries = createTraceSummaryCollector()const runtime = new AmlRuntime({ onTraceError: (error, event) => reportTraceFailure(error, event), trace: summaries.trace,})
runtime.on("start", event => { const requestId = requestContext.getStore() if (requestId !== undefined) runIdsByRequest.set(requestId, event.runId)})This mapping remains explicit under overlap. It does not depend on trace arrival order or a mutable latest-run slot.
Time application work
Section titled “Time application work”import { withTraceSpan, type AML } from "@aml-jsx/sdk"
const ReviewPhase: AML.Component<{ readonly candidates: number }> = async ({ candidates }) => { return await withTraceSpan("review.validate", async () => `validated ${candidates}`)}AML measures the callback beneath the active component. It does not add the callback’s arguments, return value, or other application content to the trace.
Retrieve and release the summary
Section titled “Retrieve and release the summary”async function evaluateRequest(requestId: string, candidates: number) { try { const value = await requestContext.run(requestId, async () => runtime.evaluate(<ReviewPhase candidates={candidates} />) ) const runId = runIdsByRequest.get(requestId) if (runId === undefined) throw new Error(`AML run identity was not captured for ${requestId}`)
const summary = summaries.forRun(runId) if (summary === undefined) throw new Error(`AML summary was not completed for ${runId}`) return { summary, value } } finally { const runId = runIdsByRequest.get(requestId) if (runId !== undefined) summaries.deleteRun(runId) runIdsByRequest.delete(requestId) }}
const [first, second] = await Promise.all([evaluateRequest("request-1", 3), evaluateRequest("request-2", 7)])The collector retains completed summaries until deleteRun(runId). To answer whether a supplied capability was actually
invoked, inspect summary.acpToolCalls.byName[providerCapabilityName] without storing raw ACP events. The aggregate keeps
only initial call counts and exact provider-reported names; it does not retain arguments, results, updates, prompts, or
model text. A name is a provider-contract identifier, not a portable backend or MCP-server identity.
summary.tools measures the different declarative AML <Tool> execution-span boundary. A call routed to an AML Tool may
appear in both aggregates and is intentionally not deduplicated.
providerUsage: [] explicitly means no provider usage was reported. Each completed ACP prompt contributes its raw usage exactly once, including repair prompts and completed samples before a later failure or cancellation. Turns without prompt completion usage retain the span-end usage fallback. Entries keep the provider-owned JSON shape without inferred tokens, model calls, cache data, cost, or billing meaning. providerUsage.length can differ from agents.turns.count: authored Agent turns, ACP prompts, and underlying model requests are distinct boundaries.
Complete source
Section titled “Complete source”The maintained trace summary example contains the complete imports and runnable deterministic flow.