# Agent span and event attributes

> The identifier rules and Anectico-defined attributes for agent spans, context events, guardrail decisions, and A2A calls.

Canonical page: https://anectico.com/docs/reference/agent-attributes/


Most of what Anectico reads off an agent span comes from a published convention — OpenTelemetry's
GenAI semantic conventions, or OpenInference. This page documents the identifier rules applied to
that metadata and the attributes no convention defines: context assembly, the guardrail decision,
and the A2A call.

## What an absent value means

**An attribute you do not send is recorded as absent, and absent never becomes a value.** This is
the single most important rule on the page, and it runs in both directions:

- If you send no `anectico.policy.decision`, the run reports **no decision** — not `allow`. A
  guardrail that ran and reported nothing about a policy is a different fact from a guardrail that
  permitted the work, and Anectico keeps them apart.
- Conversely, **do not send `allow` to mean "nothing happened."** Send it only when a policy
  considered the work and permitted it.

The same rule applies to every optional field below.

## Bounded values

Each attribute has a value shape, and a value outside it is **refused, not truncated** — the field
is left empty and the rest of the record is kept. Four shapes are used:

| Shape | Meaning |
| --- | --- |
| **Identifier** | `[A-Za-z0-9._:/-]`, 1–128 bytes. The base shape for fields that NAME something. |
| **Model identifier** | `[A-Za-z0-9._:/@-]`, 1–128 bytes. `@` admits vendor model revisions such as `gpt-4o@2024-08-06`. |
| **Version identifier** | `[A-Za-z0-9._:/+-]`, 1–64 bytes. `-` admits prereleases and `+` admits build metadata, such as `1.4.2-rc.1+build.7`. This is a bounded identifier shape, not strict Semantic Versioning validation. |
| **Code** | `[A-Za-z0-9._:/-]`, 1–64 bytes. For fields that CLASSIFY something. |

These fields are grouped and filtered on, so they must stay short and closed. Free text — a
sentence, a matched string, a model's words — belongs in the opt-in content capture path, never
here.

## Canonical span identifiers

The same bounds apply to identifiers read from OpenTelemetry GenAI and OpenInference attributes.
Provider names, agent IDs and names, workflow names, conversation and turn IDs, tool names and call
IDs, response IDs, MCP session IDs, and effective-configuration hashes use the **Identifier** shape.
`gen_ai.request.model`, `gen_ai.response.model`, and OpenInference `llm.model_name` use the **Model
identifier** shape. `gen_ai.agent.version` and `mcp.protocol.version` use the **Version identifier**
shape. Finish reasons use **Code**. `gen_ai.operation.name` is an Identifier and is limited to 128
bytes.

These field-specific additions follow the published value contracts instead of widening every
identifier. The [OpenTelemetry GenAI registry](https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/)
defines model and agent-version values as strings, while its [normative model
source](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/model/gen-ai/spans.yaml)
requires the exact vendor model name. [OpenInference](https://github.com/Arize-ai/openinference/blob/main/spec/semantic_conventions.md)
likewise records `llm.model_name` as the actual model identifier. [Semantic Versioning](https://semver.org/)
uses `-` before prerelease identifiers and `+` before build metadata. The [MCP tool-name
grammar](https://modelcontextprotocol.io/specification/2025-11-25/server/tools) is the narrower
`[A-Za-z0-9_.-]` subset, so it fits the base Identifier shape. The [MCP telemetry
convention](https://github.com/open-telemetry/semantic-conventions-genai/blob/main/docs/gen-ai/mcp.md)
currently defines no server-name attribute; registry-style reverse-DNS names such as
`com.example/invoice-server` also fit the base shape under the [MCP registry server-name
schema](https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/server-json/draft/server.schema.json)
if such a canonical field is added later.

An invalid value is omitted from canonical metadata and does not participate in derived agent or
memory identities. The original source attribute can be retained only when your content-capture
policy permits it; putting prose or personal data in an identifier never bypasses that policy.

For memory item telemetry, an invalid non-empty `gen_ai.data_source.id` also rejects the derived
item identity. It is not treated as though the store were absent, so two invalid store claims
cannot collapse into one item.

Provider attribution distinguishes an inference host from an agent framework. The literals
`autogen` and `strands-agents` identify AutoGen and Strands respectively and are not recorded as
inference providers. When telemetry also declares an actual provider through a supported provider
or system attribute, that value remains the provider.

## Context events

Context evidence is carried as one of three span events on the model-call span. It contains only
identifiers, counts, codes, ordered lists, and SHA-256 digests. Prompt text, document bytes, memory
content, summaries, and tool-result content are never accepted. The SDKs refuse the entire call if
any value is invalid and refuse to attach these events to a run or tool span.

| Event | Attribute | Bound or meaning |
| --- | --- | --- |
| `anectico.context.assembly` | `anectico.context.budget_tokens` | Integer from 0 through 4,294,967,295. |
| | `anectico.context.reserved_output_tokens` | Integer from 0 through 4,294,967,295. |
| | `anectico.context.assembled_tokens` | Integer from 0 through 4,294,967,295. |
| | `anectico.context.overflow` | `none`, `truncated`, or `dropped`. |
| | `anectico.context.assembly_sha256` | Required lower-case SHA-256 over the exact source preimage below. |
| | `anectico.context.sources` | JSON string with at most 64 entries and at most 8 KiB. |
| `anectico.context.compaction` | `anectico.context.compaction.trigger` | `token_threshold`, `turn_threshold`, `manual`, or `cache_expiry`. |
| | `.strategy` | `summarize`, `drop_oldest`, `drop_tool_results`, or `dedup`. |
| | `.threshold_tokens`, `.retained_overlap_tokens`, `.before_tokens`, `.after_tokens` | Integers from 0 through 4,294,967,295; `after_tokens` cannot exceed `before_tokens`. |
| | `.summarizer_model`, `.summarizer_version` | Optional identifiers in `[A-Za-z0-9._:/-]`, at most 128 / 64 bytes. |
| | `.before_sha256`, `.after_sha256` | 64 hexadecimal characters. |
| | `.removed_source_ids` | JSON array of at most 64 identifiers. |
| `anectico.context.cache` | `anectico.context.cache.provider` | `anthropic`, `openai`, `gemini`, `adk`, or `custom`. |
| | `.key_sha256` | Optional; 64 hexadecimal characters when present. Provider wrappers omit it because provider usage responses do not expose a stable cache-key preimage. |
| | `.config_version` | Optional identifier in `[A-Za-z0-9._:/-]`, at most 64 bytes. |
| | `.outcome` | `hit`, `miss`, `write`, `refresh`, or `expire`. |
| | `.ttl_seconds`, `.reused_tokens`, `.latency_saved_ms` | Integers from 0 through 4,294,967,295 when present. A reported zero is distinct from absence. |
| | `.cost_saved_nanos` | Non-negative integer when present. A reported zero is distinct from absence. |

Each object inside `anectico.context.sources` has these bounds:

| Field | Bound |
| --- | --- |
| `kind` | `system`, `developer`, `user`, `history`, `retrieved`, `memory`, `tool_result`, `file`, `skill`, or `summary`. |
| `id` | Identifier in `[A-Za-z0-9._:/-]`, 1–128 bytes. A `memory` source uses its lower-case 64-hex `memory_item_key`. |
| `version`, `owner` | Optional identifiers in `[A-Za-z0-9._:/-]`, at most 64 bytes each. |
| `sha256` | Optional 64-character hexadecimal digest. |
| `sensitivity` | `public`, `internal`, `confidential`, `restricted`, or `unspecified`. |
| `tokens`, `position` | `tokens` is 0 through 4,294,967,295; `position` is 0 through 65,535. |
| `visibility` | `included`, `excluded`, or `truncated`. |
| `reason` | Optional code in `[A-Za-z0-9._:/-]`, at most 64 bytes, such as `budget`, `policy`, `dedup`, or `stale`. |

The assembly digest binds the event to its source coordinates. Sort the sources by `position`
ascending, then encode one UTF-8 line per source as:

```text
position<TAB>id<TAB>visibility<LF>
```

`position` is decimal, `id` is verbatim, and the final source line also ends in `LF`; there is no
other trailing content. SHA-256 that complete byte string and emit the 64-character lower-case hex
digest. The Go, JavaScript, and Python SDKs compute this value from `sources`; an optional
caller-supplied assertion is refused if it differs. The attribute remains required on raw
telemetry: Anectico does not fill a missing digest, and a missing or mismatched digest rejects the
whole assembly event.

There is deliberately no producer-supplied compaction ratio. Anectico derives it as
`after_tokens / before_tokens`; sending a ratio through raw telemetry can cause the whole event to
be rejected if it disagrees. See [Record context assembly](/docs/agents/context) for SDK examples.

## Guardrail decisions

Send these on the span for the work the guardrail evaluated — normally your agent-run span. The
SDKs do it for you: `run.RecordGuardrail(...)` (Go), `run.recordGuardrail({...})` (JavaScript),
`run.record_guardrail(...)` (Python).

| Attribute | Shape | Required | Meaning |
| --- | --- | --- | --- |
| `anectico.guardrail.name` | Identifier | **Yes** | The guardrail that ran. This is what makes the evaluation visible at all — without it the other five are ignored. |
| `anectico.guardrail.result` | Code | No | What the guardrail FOUND, in your own vocabulary: `passed`, `triggered`, `flagged`. |
| `anectico.guardrail.finding_class` | Code | No | The KIND of finding: `pii`, `prompt_injection`, `toxicity`, `secret`. Never the finding itself. |
| `anectico.policy.id` | Identifier | No | The policy in force. |
| `anectico.policy.version` | Code | No | Its version, so a change in behaviour can be attributed to a change in policy. |
| `anectico.policy.decision` | Closed set | No | What the policy DID: `allow`, `deny`, `modify`, `escalate`. |

`result` and `decision` are separate on purpose. A guardrail that triggers under a policy that only
logs is a real and common pair, and one field cannot hold it.

`anectico.policy.decision` accepts exactly four values:

| Value | Meaning |
| --- | --- |
| `allow` | The work was permitted. |
| `deny` | The work was stopped. |
| `modify` | The work continued in altered form — a redaction, a mask, a rewrite. |
| `escalate` | The work was handed to a person. |

Anything else is refused and the field is left empty; it is never mapped to the nearest value.

### Recording a guardrail does not change what the span IS

These attributes are an **annotation**. A span that declares a guardrail name keeps the kind of work
its own `gen_ai.operation.name` says it is — record a guardrail on your agent-run span and the step
is still an agent invocation, not a safety step. Only a span that names no operation at all takes
its classification from the guardrail, which is what a framework's own guardrail span does.

The same rule holds for a handoff: it annotates the work it was declared on and never renames it.

The run's counts still report the guardrail, twice and deliberately:

- **Guardrail checks** counts every step on which a guardrail ran — one your instrumentation named,
  or one a framework reported as safety work without saying anything more. Each step is counted
  once, however it qualified.
- **Guardrail decisions** counts only the steps that declared a guardrail name.

The gap between them is how much of your guardrail surface is instrumented.

## A2A calls

`a2a.*` attributes describe one Agent-to-Agent protocol call. A span carrying `a2a.method.name` is
classified as **A2A** work.

| Attribute | Shape | Required | Meaning |
| --- | --- | --- | --- |
| `a2a.method.name` | Code | **Yes** | The protocol method — `SendMessage`, `GetTask`. This is what makes the call visible as A2A. |
| `a2a.task.id` | Identifier | No | The task the call concerned. A first message that opens a task carries none; the response names it. |
| `a2a.context.id` | Identifier | No | The context grouping related tasks. |

Anectico's own transport span also carries `a2a.role` and `a2a.protocol.version` as diagnostic
attributes. They are not fields in the canonical A2A step, so do not rely on custom values there
being validated or surfaced with the method, task, and context identifiers.

**What this covers today.** Anectico's own A2A surface emits these attributes for every non-streamed
call it serves, so A2A work you drive *against Anectico* appears in your execution graph. Streamed
A2A calls are not yet observed. Your own agent-to-agent traffic appears only if your agents emit
these attributes themselves.
