# Correlate application spans and run summaries

Time application phases and retrieve content-free summaries for overlapping AML evaluations.
Canonical: https://agent-markup-language.com/docs/cookbook/application-observability/
Documentation index: https://agent-markup-language.com/docs/
Complete documentation: https://agent-markup-language.com/docs/llms.txt

**Deterministic**

## Goal

Measure a custom validation phase, correlate concurrent application requests to AML `runId` values, and retrieve one
portable summary per completed evaluation without a global latest-run value.

## 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:

```tsx
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

```tsx
async function ReviewPhase({ candidates }: { readonly candidates: number }) {
  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

```tsx
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. If ACP reports usage, the entry keeps that provider-owned JSON shape without inferred tokens, model calls,
cache data, cost, or billing meaning.

**Note — Telemetry failures are not workflow failures**

Handle sink failures with the runtime's existing `onTraceError` option; sink failures do not change the workflow
result. The collector reports runtime-emitted Agent cleanup outcomes in `cleanup` without rewriting evaluation status.

## Complete source

The maintained [trace summary example](https://github.com/we-are-singular/aml/blob/main/examples/src/operations/trace-summaries.tsx)
contains the complete imports and runnable deterministic flow.
