Skip to content

Correlate application spans and run summaries

Correlate concurrent workflow runs with application requests and collect timing summaries for each completed run.

No credentials needed

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.

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.

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.

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.

The maintained trace summary example contains the complete imports and runnable deterministic flow.