Skip to content

feat(mappers): add OpenAI Agents OTel session mapper - #365

Open
liramon2 wants to merge 4 commits into
strands-agents:mainfrom
liramon2:openai-openllmetry-mapper
Open

feat(mappers): add OpenAI Agents OTel session mapper#365
liramon2 wants to merge 4 commits into
strands-agents:mainfrom
liramon2:openai-openllmetry-mapper

Conversation

@liramon2

@liramon2 liramon2 commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

Add session mapper for agents created with OpenAI Agent SDK and instrumented with OpenLLMetry Traceloop. The mapper inherits from GenericGenAISessionMapper because OpenAI Agent + Traceloop traces mostly follow GenAI conventions. Divergences from the GenAI spec are handled within the new mapper.

This also fixes parent span ids in GenericGenAISessionMapper so that they point to converted spans. This preserves the agent-tool scopes when converting from OpenAI Agent traces (or other GenAI traces) to evaluator inputs.

Related Issues

#319

Documentation PR

Type of Change

New feature

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce new warnings.

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@liramon2

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent Review this PR. Consolidate findings into 1 comment.

@liramon2
liramon2 deployed to auto-approve August 11, 2026 19:57 — with GitHub Actions Active
@github-actions github-actions Bot added enhancement New feature or request area-tracing Trace/session ingestion: providers, session mappers, extractors, telemetry/OTEL labels Aug 11, 2026
@strandly-the-agent

This comment was marked as resolved.

@liramon2

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent Review this PR. Consolidate findings into 1 comment. Accepting the risk of finding 3 because it is difficult to reach.

@strandly-the-agent

This comment was marked as resolved.

@liramon2

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent Review this PR. Consolidate any findings into a single comment.

@strandly-the-agent

Copy link
Copy Markdown

TL;DR — follow-up review of just the 5b1142b..9fb48f5 delta. All five earlier findings are now resolved, including finding 5 which this commit fixes properly. The new _apply_handoff_reparenting is the right idea and works on the real fixture; it has three edge cases around agent names being used as identity. None of them blocks — I went looking for a hang or corrupted tool ownership and found neither. Still a draft, so comments rather than approve.

9fb48f5 · mapper suite 381 passed (was 378) · ruff check + ruff format --check clean · finding 5 verified fixed: ADOT math_specialist (c2828d53371818f0) now parents under coordinator (1afcb5ca9fe89dda) · cycle-guard patch below verified (381 still passing, fixtures unchanged).

Previous findings — all closed

Finding Status
🟡1 per-agent tool attribution untested fixed at 5b1142b
🟡2 base-class bridge_parent_gaps untested fixed at 5b1142b
🟡3 nested nameless invoke_agent span risk accepted by you — not re-raised
🟡4 docstring documented a non-existent format fixed, including the cosmetic leftover — the two formats now actually differ
🟡5 agent_handoff dropped silently fixed by this commit — verified above

You also took the set.intersection(*tool_sets) nit, and added the ADOT per-agent assertions I'd flagged as missing. Nothing from my earlier comments is outstanding.


All three new findings are in _apply_handoff_reparenting (openai_agents_otel_session_mapper.py:72-101), and they share one root cause: gen_ai.agent.name is treated as a unique key for an agent span.

🟡 6. Duplicate agent names — only the last span with a given name is reparented

openai_agents_otel_session_mapper.py:89agent_spans_by_name[name] = agent_spans[span_id] is a plain dict, so when the same sub-agent is invoked twice in one trace (ordinary in a multi-turn conversation) earlier invocations are overwritten and never reparented. Repro — one coordinator → math_specialist handoff, two math_specialist invocations:

AGENT coord  parent=None
AGENT ms1    parent=None     <- stranded, never reparented
AGENT ms2    parent=coord

ms1 keeps whatever parent it had, so the tree is half-built and _find_root_agent_span sees two roots where there should be one. This is the one I'd most want addressed, and it needs a small shape change rather than a one-liner: key by name to a list and resolve each handoff edge to a specific invocation (nearest following start_time, or nearest common ancestor) instead of "the last one seen". Happy to prototype it if useful.

🟡 7. Handoff edges can build a cyclic or self-referential parent tree

openai_agents_otel_session_mapper.py:97-101 assigns unconditionally, with no check that the edge is acyclic. Mutual handoff — which is a normal OpenAI Agents pattern, a specialist handing control back to a triage/coordinator agent — produces A.parent=B and B.parent=A. from_agent == to_agent produces A.parent=A.

Being straight about severity: I tried to make this hurt and couldn't. TraceExtractor at both TRACE_LEVEL and TOOL_LEVEL extracts fine, re-constructing a Trace from the cyclic spans completes, and tool ownership stays correct — every parent-walker I could find (bridge_parent_gaps, Trace.model_post_init) has a visited/seen guard. So this is a data-integrity wart, not a blocker. The real consequence is that with a cycle no agent is parentless, so _find_root_agent_span (types/trace.py:135-149) silently falls through to its earliest-start_time branch — "which agent is the root" stops being derived from structure. Guarding is cheap, and I verified this keeps both fixtures identical at 381 passed:

        agents_by_id = {
            a.span_info.span_id: a for a in agent_spans_by_name.values() if a.span_info.span_id
        }

        def _would_cycle(child_id: str, new_parent_id: str) -> bool:
            cur: str | None = new_parent_id
            seen: set[str] = set()
            while cur and cur not in seen:
                if cur == child_id:
                    return True
                seen.add(cur)
                ancestor = agents_by_id.get(cur)
                cur = ancestor.span_info.parent_span_id if ancestor else None
            return False

        # Re-parent sub-agents to parent agents
        for from_name, to_name in handoffs:
            from_span = agent_spans_by_name.get(from_name)
            to_span = agent_spans_by_name.get(to_name)
            if not (from_span and to_span and from_span.span_info.span_id and to_span.span_info.span_id):
                continue
            if from_span is to_span:
                logger.debug("agent=<%s> | skipping self-handoff", to_name)
                continue
            if _would_cycle(to_span.span_info.span_id, from_span.span_info.span_id):
                logger.debug("from=<%s> to=<%s> | skipping cycle-inducing handoff", from_name, to_name)
                continue
            to_span.span_info.parent_span_id = from_span.span_info.span_id

With that, A↔B resolves to A.parent=None, B.parent=A and self-handoff leaves A.parent=None.

🟡 8. Two handoffs to the same target make the tree depend on raw span order

Same lines — last-write-wins. Given alpha → gamma and beta → gamma in one trace, gamma's parent is whichever handoff span appears later in the input list. Identical logical input, two different trees:

span order [alpha->gamma, beta->gamma]  =>  C.parent=B
span order [beta->gamma, alpha->gamma]  =>  C.parent=A

Since span order isn't guaranteed stable across exporters, the same trace can evaluate differently on two runs. Worth at least a documented tie-break (earliest handoff wins, say) so it's a decision rather than an accident.

Appendix — non-blocking (3)

⚪ The new logic has no edge-case tests. test_handoff_reparents_math_specialist_under_coordinator covers the ADOT happy path only — nothing exercises duplicate names, a mutual handoff, a self-handoff, or a handoff naming an agent whose span was dropped by the wrapper-skip. Findings 6–8 are all invisible to the suite as it stands.

⚪ Docstrings that explained non-obvious ordering were deleted in this commit. The class docstring lost its explanation of why the overrides exist, and _convert_trace's docstring (:52) lost "After base conversion (which sets agent_span_id via Trace.model_post_init)". That sentence was load-bearing: reparenting at :59 runs after super()._convert_trace() has already constructed the Trace and assigned tool ownership, so the back-fill at :62-68 reads a pre-reparenting agent_span_id. Both happen to agree today, but the next person to touch the ordering has lost the note that told them why it matters. _apply_handoff_reparenting's own docstring is also silent on the name-keyed / last-write-wins behaviour behind findings 6 and 8.

⚪ Consequence of a nit I raised last round. set.intersection(*tool_sets) is the right generalisation, but it raises TypeError on zero agents and, with exactly one agent, returns that agent's own set so assert not ... fails confusingly. test_live_session_has_exactly_two_agent_spans covers the fixture, so this is theoretical — flagging it only because it came from my suggestion.

Review shape, and one thing I got wrong

Shape: a self-run follow-up scoped to the 5b1142b..9fb48f5 delta (+71/-19 across the mapper and its test file). Because this delta carries genuinely new behaviour rather than just tests, I ran an independent adversarial pass over the new logic as well — but it exceeded its time budget and did not complete, so it contributed nothing here and the findings above are all from my own repros. Aggregation was done inline. Net: findings 6–8 are single-reviewer results, so a second pair of eyes on the reparenting logic wouldn't be wasted.

Something I got wrong, in case the diff misleads anyone else: reading the unified diff for the new test file, it looked like it contained two syntax errors (a stray ) and an unclosed next(). That was a diff-rendering artifact — the file on disk is fine, python -m py_compile exits 0, and all 381 tests collect and pass. No syntax problem exists; I'd rather say so than leave a phantom blocker implied.

head 9fb48f50e2bd234f16d3c05c7b07e6cc2c8995c3 (delta from 5b1142b: +71/-19, 2 files)

pytest tests/strands_evals/mappers/ -q                  -> 381 passed, 3.93s
ruff check / ruff format --check (2 changed files)      -> All checks passed / already formatted
py_compile on the new test file                         -> exit 0 (no syntax error)
ADOT fixture through the mapper                         -> math_specialist parent=coordinator (finding 5 fixed)
repro: duplicate agent name                             -> ms1 parent=None, ms2 parent=coord
repro: mutual handoff A<->B                             -> A.parent=B, B.parent=A (cycle)
repro: self-handoff                                     -> A.parent=A
repro: two handoffs -> same target, order swapped        -> C.parent=B vs C.parent=A
hang hunt: TRACE_LEVEL, TOOL_LEVEL, Trace re-construction under a cycle -> no hang, ownership intact
cycle-guard patch above                                 -> 381 passed, ruff clean, fixtures unchanged

Tree restored to PR head after every experiment; nothing left modified.

Automated review — solid work for a human to approve, not a gate.

@liramon2

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent Review this PR. Consolidate any findings into a single comment. The 3 findings found in the last round are theoretical, not realistic.

@strandly-the-agent

Copy link
Copy Markdown

No new findings. Reviewed the 9fb48f5..ebc5c5a delta — it's a docstring only, functionally inert, and accurate. Findings 6–8 withdrawn per your call; nothing outstanding from any round. Still a draft so I won't mark approve, but there's nothing blocking from my side.

ebc5c5a · mapper suite 381 passed · ruff check + ruff format --check clean · diff contains no functional lines, docstring only.

Your call on 6–8 is a fair one, and my own evidence supports it. All three needed a multi-agent topology that appears in neither capture, and when I went looking for consequences I found none worth the name: no hang anywhere, tool ownership stayed correct, and the only reachable effect was _find_root_agent_span falling through to its earliest-start_time tiebreak. I tiered them 🟡 out of caution; ⚪ would have been the better call.

One thing worth recording, since it's the useful outcome here: the new docstring on _apply_handoff_reparenting (openai_agents_otel_session_mapper.py:77-81) — "Uses agent name as span identity (last-write-wins for duplicate names or competing handoffs)" — is exactly right, and I re-ran my repros against it to confirm rather than assume. Competing handoffs alpha→gamma then beta→gamma give gamma.parent=beta; reverse the span order and you get gamma.parent=alpha. So the docstring now states the real contract, which turns the behaviour I flagged into a documented design decision. That's a legitimate resolution and, for a limitation you judge unreachable in practice, the cheaper one.

What I ran
head ebc5c5ad4038298bed6b844fa436fc390d077e5e (delta from 9fb48f5: docstring only, +4)

pytest tests/strands_evals/mappers/ -q            -> 381 passed, 3.44s
ruff check / ruff format --check                  -> All checks passed / already formatted
diff filtered to non-docstring lines              -> none (functionally inert)
repro: competing handoffs, span order swapped      -> gamma.parent=beta vs gamma.parent=alpha
                                                      (confirms the documented last-write-wins)

Self-run follow-up on a 4-line docs delta — no fan-out, which would have been overkill here. Full history for anyone joining: findings 1, 2, 4 fixed at 5b1142b; finding 5 fixed at 9fb48f5; finding 3 risk-accepted; findings 6–8 withdrawn here.

Automated review — solid work for a human to approve, not a gate.

@liramon2
liramon2 marked this pull request as ready for review August 12, 2026 19:15
@liramon2
liramon2 requested a review from a team as a code owner August 12, 2026 19:15
@liramon2
liramon2 requested a review from poshinchen August 12, 2026 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-tracing Trace/session ingestion: providers, session mappers, extractors, telemetry/OTEL enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants