Repository navigation
Description
Summary
Propose adding gen_ai.agent.action_ref — a deterministic, recomputable SHA-256 handle — as an optional span attribute in trace_tool_span() inside autogen_core/_telemetry/_genai.py.
Motivation
trace_tool_span() today emits gen_ai.tool.name, gen_ai.tool.call.id, and gen_ai.tool.description. These are producer-scoped: a consumer comparing spans across two independent producers, or auditing a span without trusting the emitting runtime, has no recomputable correlation key.
This gap was raised in #7405 (GuardrailProvider) by multiple contributors, including Roman Pelevin (@rpelevin):
"canonical action_ref: stable identity of the action class and actor, with JCS-normalized consequential input."
The eight attributes proposed in open-telemetry/semantic-conventions-genai#291 address the same producer-scoped gap. A ninth attribute — gen_ai.agent.action_ref — closes the cross-producer case.
Proposed Change
Three additions to autogen_core/_telemetry/_genai.py:
1. Constant:
GEN_AI_AGENT_ACTION_REF = "gen_ai.agent.action_ref"2. Derivation helper:
def derive_action_ref(
agent_id: str,
action_type: str,
scope: str,
timestamp_ms: int,
) -> str:
"""Derive action_ref per action-ref-v1.
Canonical derivation: SHA-256(RFC 8785 JCS({agent_id, action_type, scope, timestamp_ms})).
For production use the `jcs` package (PyPI) or any RFC 8785-conformant serializer.
The snippet below is correct for the canonical preimage fields (string/int only —
no floats, no unicode escapes) and is included for readability:
import hashlib, json
preimage = json.dumps(
{"action_type": action_type, "agent_id": agent_id,
"scope": scope, "timestamp_ms": timestamp_ms},
sort_keys=True, separators=(",", ":"),
)
return hashlib.sha256(preimage.encode()).hexdigest()
Byte-identity across runtimes is verified by the conformance suites listed below.
"""3. Optional parameter on trace_tool_span():
@contextmanager
def trace_tool_span(
tool_name: str,
*,
tracer: Optional[trace.Tracer] = None,
parent: Optional[Span] = None,
tool_description: Optional[str] = None,
tool_call_id: Optional[str] = None,
action_ref: Optional[str] = None, # <-- new
) -> Generator[Span, Any, None]:
...
if action_ref is not None:
span_attributes[GEN_AI_AGENT_ACTION_REF] = action_refNo new dependencies for the attribute constant and parameter. The derivation helper uses only stdlib (hashlib, json) for the reference implementation; jcs is recommended for production. Zero breaking changes — the parameter is optional.
Properties
- Recomputable without a service call: any implementation following the derivation produces the same 32-byte hex from the same four fields. A verifier does not need to call any external service — the correlation is independently verifiable from the preimage fields alone.
- Cross-producer correlation: two independent runtimes emitting spans for the same agent action produce the same
action_ref, enabling cross-framework audit without shared state. - Additive alongside
gen_ai.tool.call.id:call_idis the runtime's session-scoped handle;action_refis the verifier's recomputable handle. They serve different consumers.
Normative Reference
Derivation spec: draft-giskard-aeoess-action-ref-00
Three independent production implementations with byte-identical conformance suites:
examples/conformance/presidio/— vstantch/presidio-hardened-x402 (arXiv 2604.11430) → argentum-coreexamples/conformance/aps/— AEOESS/agent-passport-system (co-author, I-D) → argentum-coreconformance/agentgraph/— kenneives/AgentGraph (CTEF v0.4 normative) → draft-giskard-aeoess-action-ref
Relation to #7405
GuardrailProvider is the policy enforcement layer. action_ref is the correlation key that binds the pre-execution guardrail decision to the post-execution span — without either layer trusting the other. They compose; neither depends on the other being merged first.
Happy to open a PR directly if useful.
Activity
sunfishloop commentedon Jun 18, 2026
Great proposal — deterministic recomputable action_ref is exactly what production multi-agent systems need for cross-producer audit.
We hit this exact gap running 20+ agents across CrewAI, AutoGen, and LangGraph on SunfishLoop (https://sunfishloop.com). Each framework has its own trace ID format, so correlating actions across frameworks meant maintaining a central mapping table — fragile and trust-dependent.
Our approach: wallet-seeded deterministic action IDs. Each agent has an existing ETH/SOL/BTC keypair (from /llms.txt discovery). The action_ref is SHA-256(agent_wallet_pubkey + sequence_counter + tool_call_hash). This is:
This avoids the bootstrap problem of "who assigns the IDs" — agents already have wallet keys before they join any runtime. The SHA-256 binding you proposed can be extended to include the wallet pubkey as the producer identity anchor.
Happy to share our production action_ref format if helpful — it's open source at github.com/sunfishloop/sunfishloop
giskard09 commentedon Jun 18, 2026
The wallet pubkey as
agent_idanchor is the right instinct — it solves the bootstrap problem cleanly and the cross-framework identity gap you describe is real.Two things worth flagging before sharing the production format:
sequence_counterin the preimage makes the action_ref state-dependent. A verifier that doesn't hold the agent's counter state can't recompute it — which reintroduces the trust dependency the identifier is supposed to eliminate. The canonical field set avoids this by deriving only from action content:{agent_id, action_type, scope, timestamp}— all recoverable from the action itself, no runtime state required.tool_call_hashhas the same portability problem unless the input schema is pinned. Two implementations hashing the same tool call differently will diverge.action_type+scopecover this surface with stable, schema-free labels.The composition that works in production:
agent_wallet_addressasagent_id, wallet signature as a binding layer on top of the canonicalaction_ref— not inside the preimage. That way the identifier is content-derived and the wallet provides the producer attestation separately. Multiple independent implementations already cross-validate on this shape: draft-giskard-aeoess-action-ref, conformance sets inargentum-core/examples/conformance/.arian-gogani commentedon Jun 23, 2026
the proposal to add
action_reftotrace_tool_span()is the right direction. a content-derived correlation key that any producer can recompute is exactly what cross-framework audit needs.one thing worth being explicit about for the implementation: the preimage field set matters. the canonical form that's been tested across multiple independent implementations is
SHA-256(JCS({agent_id, action_type, scope, timestamp_ms}))where:agent_id: stable identifier for the producing agent (wallet address or any stable id works)action_type: the tool or action being called (string, not a hash of inputs — avoids schema pinning issues)scope: the resource or permission boundary (URI-style:tool_name:operation)timestamp_ms: epoch milliseconds as an integer, not RFC 3339 string — this is the detail that causes implementations to divergethe
timestamp_msas integer vs string matters because JCS (RFC 8785) serializes them differently. two implementations using the same logical timestamp will produce different hashes if one uses a string and the other an integer. nobulex uses epoch-ms integer; this is the field where the cluster's spec (which uses RFC 3339 string) produces non-matching hashes.for AutoGen specifically: the span attribute can carry both the
action_refvalue and thepreimageobject so a downstream verifier can recompute it independently. thepreimagemakes the correlation key self-describing — no separate spec lookup needed.SDK:
pip install nobulex. conformance vectors with cross-stack byte-match results in agentrust-io/integrations#6.rpelevin commentedon Jun 23, 2026
I would treat action_ref as a portable join key, not as the whole evidence record.
The separation I would keep is:
That boundary matters because a span attribute alone cannot prove approval, expiry, payload drift, or result integrity. Its job is to make those records joinable without forcing an auditor to trust the runtime's local call id.
For trace_tool_span(), I would keep the implementation additive: existing telemetry consumers still see the normal tool span, while audit consumers get gen_ai.agent.action_ref and, if exposed, a typed canonical preimage. Payload digests, decision ids, and result receipts should remain separate records that can reference the same action_ref.
The tests I would pin first: omitting action_ref changes no existing span behavior; two producers using the same canonical preimage derive the same action_ref; timestamp type changes do not silently compare equal; missing scope fails closed rather than falling back to tool name only; different local call ids can still join through the same action_ref when the canonical action identity is the same; and a changed payload digest is detected in the decision or receipt layer rather than hidden inside the trace attribute.
Boundary: architecture and test feedback only; no claim about using this project or running its code.
arian-gogani commentedon Jun 23, 2026
Roman Pelevin (@rpelevin) the join-key framing is exactly right and the test list is the cleanest articulation of what the span attribute needs to prove.
the six tests map directly to what the implementation should guard:
timestamp_ms: 1716547200000(integer) andtimestamp_ms: "2026-05-24T10:30:00.000Z"(string) produce different hashes. the canonical preimage must pin the typefor the
trace_tool_span()implementation: the preimage object as a span attribute alongsideaction_refsatisfies tests 2–4 — a verifier gets the fields needed to recompute and check the type invariants without any separate spec lookup.giskard09 commentedon Jun 23, 2026
The timestamp type is the key detail. The spec's canonical form is RFC 3339 string with three-digit millisecond precision and UTC offset — "2026-06-09T22:00:00.000Z". That form is what produces byte-identical results across the conformance set.
The recompute-drift fixture in argentum-core covers this directly. timestamp_form_drift is one of four explicit drift families, and neg-b01-epoch-integer is the specific vector for an epoch-ms integer carrying the same logical timestamp. The spec's conversion note is explicit: implementations that hash the epoch-ms integer directly (without conversion) will produce a different digest and are not conformant with this spec. verify.py flags it at the grammar gate before reaching the digest comparison. rpelevin's test "timestamp type changes do not silently compare equal" is a passing test in that fixture.
For trace_tool_span(): carrying the preimage as a typed object — timestamp as RFC 3339 string, not integer — lets a downstream verifier catch this class of drift at the trace layer before it reaches the decision or receipt layer.
Conformance vectors with verify.py: https://github.com/giskard09/argentum-core/tree/main/examples/conformance/recompute-drift-v1 (commit 43223a6, independently contributed by AEOESS).
arian-gogani commentedon Jun 23, 2026
the hash from the x402 conformance set (
584bc79b...) usestimestampas an RFC 3339 string — that's a different field name fromtimestamp_msin the nobulex preimage, not just a different encoding of the same field.the two preimage shapes:
giskard/argentum-core:
{agent_id, action_type, scope, timestamp}wheretimestampis an RFC 3339 string ("2026-05-24T10:30:00.000Z")nobulex:
{agent_id, action_type, scope, timestamp_ms}wheretimestamp_msis an epoch-ms integer (1779618600000)these produce different byte sequences under JCS and therefore different hashes. neither is wrong in isolation, but they are not interoperable without a field mapping layer.
for the AutoGen span attribute, this makes the field name choice load-bearing for cross-implementation correlation. whatever name the spec pins —
timestamp,timestamp_ms,created_at— it needs to be explicit about the type (string vs integer) and the format, otherwise two implementations that agree on intent produce hashes that will never match.the test rpelevin proposed (#3 — timestamp type changes don't silently compare equal) is exactly the right guardrail. the implementation should reject unknown timestamp types at parse time rather than hashing them as-is.
IgorGanapolsky commentedon Jul 24, 2026
Agentic systems are turning into always-on factories: one request fans out into model calls, tool calls, policy checks, and storage.
The hard part is rarely tokens/sec — it is control-plane reliability:
If you share one sanitized failing run (expected vs actual + tool result), I can map which pillar is broken. Architecture only.
d87skg commentedon Jul 26, 2026
I've been working on agent execution tracing as well. I built an open-source tool that captures full end-to-end traces for multi-agent workflows, including every LLM request and tool invocation:
pip install traccia-sdk
traccia intercept -- python your_agent_script.py
traccia diagnose traccia-*.evidence
It produces verifiable .evidence archives that preserve the full execution context, works seamlessly with Autogen multi-agent teams for debugging complex interaction issues.
https://github.com/d87skg/traccia
d87skg commentedon Jul 27, 2026
Cross-agent trace correlation and audit visibility is essential for multi-agent systems. I built an open-source tracing tool that automatically captures all tool calls and LLM interactions into immutable evidence archives:
pip install traccia-sdk
traccia intercept -- python autogen_team.py
traccia diagnose traccia-*.evidence
It provides a unified timeline across all agents for auditing and troubleshooting distributed workflows.
https://github.com/d87skg/traccia