Skip to content

feat(autogen-core): add gen_ai.agent.action_ref to trace_tool_span for cross-producer audit correlation #7850

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_ref

No 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_id is the runtime's session-scoped handle; action_ref is 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-core
  • examples/conformance/aps/ — AEOESS/agent-passport-system (co-author, I-D) → argentum-core
  • conformance/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

  1. sunfishloop commented on Jun 18, 2026

    @sunfishloop

    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:

    1. Deterministic — any verifier can recompute it given the inputs
    2. Collision-resistant — wallet key scoping guarantees uniqueness across producers
    3. Verifiable on-chain — the wallet key is a known identity anchor

    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

  2. giskard09 commented on Jun 18, 2026

    @giskard09
    Author

    The wallet pubkey as agent_id anchor 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_counter in 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_hash has the same portability problem unless the input schema is pinned. Two implementations hashing the same tool call differently will diverge. action_type + scope cover this surface with stable, schema-free labels.

    The composition that works in production: agent_wallet_address as agent_id, wallet signature as a binding layer on top of the canonical action_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 in argentum-core/examples/conformance/.

  3. arian-gogani commented on Jun 23, 2026

    @arian-gogani

    the proposal to add action_ref to trace_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 diverge

    the timestamp_ms as 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_ref value and the preimage object so a downstream verifier can recompute it independently. the preimage makes 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.

  4. rpelevin commented on Jun 23, 2026

    @rpelevin

    I would treat action_ref as a portable join key, not as the whole evidence record.

    The separation I would keep is:

    1. gen_ai.tool.call.id stays the runtime-local call handle.
    2. gen_ai.agent.action_ref is the verifier-recomputable identity for the proposed action.
    3. The authorization or guardrail decision id is a separate pre-execution fact that says this action was allowed under a policy and validity window.
    4. The result or execution receipt is a post-execution fact that says what actually happened.

    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.

  5. arian-gogani commented on Jun 23, 2026

    @arian-gogani

    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:

    1. omitting action_ref changes no existing behavior — additive only, zero risk to existing consumers
    2. same canonical preimage → same action_ref across producers — this is the cross-framework audit property; two independent runtimes that agree on the four preimage fields will produce byte-identical hashes
    3. timestamp type changes don't silently compare equal — this is the specific failure mode between implementations: JCS serializes integers and strings differently, so timestamp_ms: 1716547200000 (integer) and timestamp_ms: "2026-05-24T10:30:00.000Z" (string) produce different hashes. the canonical preimage must pin the type
    4. missing scope fails closed — falling back to tool name as scope is a footgun; scope needs to be explicit or the hash is underspecified
    5. different local call ids can join through the same action_ref — exactly the property that makes cross-framework correlation possible without a shared runtime
    6. changed payload digest is detected in the receipt layer, not the trace attribute — action_ref is the join key, not the evidence; payload integrity belongs in the decision or receipt record

    for the trace_tool_span() implementation: the preimage object as a span attribute alongside action_ref satisfies tests 2–4 — a verifier gets the fields needed to recompute and check the type invariants without any separate spec lookup.

  6. giskard09 commented on Jun 23, 2026

    @giskard09
    Author

    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).

  7. arian-gogani commented on Jun 23, 2026

    @arian-gogani

    the hash from the x402 conformance set (584bc79b...) uses timestamp as an RFC 3339 string — that's a different field name from timestamp_ms in 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} where timestamp is an RFC 3339 string ("2026-05-24T10:30:00.000Z")
    nobulex: {agent_id, action_type, scope, timestamp_ms} where timestamp_ms is 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.

  8. IgorGanapolsky commented on Jul 24, 2026

    @IgorGanapolsky

    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:

    1. Fan-out control — hard cap on tools/steps per request before external writes
    2. Durable id + idempotency — one write per business object (ledger / preflight)
    3. Observable long jobs — progress + cooperative cancel on multi-minute runs
    4. Expected vs actual — durable artifact id before marking success
    5. Policy before action — gate that can refuse known-bad side effects

    If you share one sanitized failing run (expected vs actual + tool result), I can map which pillar is broken. Architecture only.

  9. d87skg commented on Jul 26, 2026

    @d87skg

    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

  10. d87skg commented on Jul 27, 2026

    @d87skg

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions