Skip to content

feat(pydantic_ai): propagate conversation_id as LLMObs session_id - #19238

Open
mlorthiois wants to merge 1 commit into
DataDog:mainfrom
mlorthiois:feat/pydantic-ai-session-id-from-conversation-id
Open

feat(pydantic_ai): propagate conversation_id as LLMObs session_id#19238
mlorthiois wants to merge 1 commit into
DataDog:mainfrom
mlorthiois:feat/pydantic-ai-session-id-from-conversation-id

Conversation

@mlorthiois

Copy link
Copy Markdown

Summary

  • Propagates the explicit conversation_id kwarg passed to Agent.run/run_stream/iter as the LLMObs session_id, mirroring the pattern already used by the google_adk integration (PydanticAIIntegration.set_session_id, called right after span creation).
  • conversation_id=None (the default, i.e. not passed explicitly) and the 'new' sentinel are intentionally not propagated:
    • None is just the common no-op case (most calls don't pass it).
    • 'new' tells pydantic-ai to fork a fresh conversation into an id generated internally (a fresh UUID7) that isn't available to the integration at span-creation time. Propagating the literal string 'new' would incorrectly group every unrelated fresh conversation under one shared session.
  • Only the raw kwarg is used, not pydantic-ai's internally resolved id (e.g. AgentRun.conversation_id, which also accounts for inheritance from message_history) — resolving it would require entering the run before the session id could be set, which is after child tool spans already need it.

Test plan

  • Added TestLLMObsPydanticAISessionId to tests/contrib/pydantic_ai/test_pydantic_ai_llmobs.py, covering: explicit conversation_id via run/run_stream/iter, the 'new' sentinel being skipped, and an omitted-but-inherited-from-message_history case also being skipped (documenting the raw-kwarg-only design choice).

Closes #19237

Agent.run/run_stream/iter's conversation_id kwarg is now forwarded as the
LLMObs session_id, so multi-turn conversations don't need a separate
LLMObs.workflow(session_id=...) wrapper. `None` (default) and the `'new'`
sentinel are skipped: `None` is the common no-op case, and `'new'` tells
pydantic-ai to fork a fresh conversation into an id we don't have access
to at span-creation time — propagating the literal string would
incorrectly group unrelated conversations under one shared session.

Fixes DataDog#19237
@mlorthiois
mlorthiois force-pushed the feat/pydantic-ai-session-id-from-conversation-id branch from 0983ba6 to a853c56 Compare July 23, 2026 08:16
@mlorthiois
mlorthiois marked this pull request as ready for review July 23, 2026 08:17
@mlorthiois
mlorthiois requested review from a team as code owners July 23, 2026 08:17
@mlorthiois
mlorthiois requested review from dubloom and r1viollet July 23, 2026 08:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a853c56592

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"ddtrace_global_config",
[dict(_llmobs_enabled=True, _llmobs_ml_app="<ml-app-name>")],
)
class TestLLMObsPydanticAISessionId:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate session-id tests to supported pydantic-ai versions

The pydantic_ai riot env still runs this test module against pydantic-ai-slim==0.8.1, ==1.0.0, and ==1.63.0 (see riotfile.py), but conversation_id is not accepted by Agent.run/run_stream/iter in those existing pinned versions. As written, this whole new class raises TypeError before the integration is exercised in those jobs; please version-gate these tests or update the supported/tested version matrix.

Useful? React with 👍 / 👎.

"""
if not self.llmobs_enabled or not conversation_id or conversation_id == "new":
return
_annotate_llmobs_span_data(span, session_id=conversation_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the session id for distributed propagation

When this is the first session id in a trace and the agent makes an outbound HTTP call, this only writes the value into the LLMObs metastruct/span store; unlike the core LLMObs span path, it never mirrors it into span.context._meta[PROPAGATED_SESSION_ID_KEY], which is what the HTTP propagator injects. Downstream services therefore won't inherit the conversation_id session even though the local agent/tool spans do, so please also stamp the propagated session key when accepting the explicit conversation_id.

Useful? React with 👍 / 👎.

@mlorthiois

Copy link
Copy Markdown
Author

I tried adding a pydantic-ai-slim >= 2 riot env (2.0 being the first release that actually accepts the conversation_id kwarg), and it turns out pydantic-ai 2.0 shipped breaking changes that go well beyond conversation_id and break the existing test module and cassettes:

  • Bare model names are no longer inferred: Agent(model="gpt-4o") now raises UserError: Unknown model: gpt-4o. Every test in this module builds its agent with "gpt-4o", so they all fail at agent construction, not just the new session-id ones.
  • Even with a provider prefix, openai:gpt-4o resolves to OpenAIResponsesModel (the Responses API, /v1/responses) instead of OpenAIChatModel (/v1/chat/completions). Our recorded cassettes are all /v1/chat/completions, so they no longer match — a v2 env would need openai-chat:gpt-4o and/or re-recorded cassettes.

So properly supporting v2 in the matrix means reworking the model strings across the whole module (and likely re-recording cassettes), which feels like a separate compatibility effort rather than part of this session-id change.

For this PR I can instead make TestLLMObsPydanticAISessionId version-aware so it no longer raises TypeError on the currently pinned versions (0.8.1 / 1.0.0 / 1.63.0): on < 2.0 the tests run without the conversation_id kwarg and assert that no session_id is set (so the integration is still exercised there), and on >= 2.0 they pass conversation_id and assert it becomes the session_id. But it will not check it effectively works in > 2.0.

Does officially supporting pydantic-ai v2 in the test matrix sound like it should be its own follow-up, out of scope here?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pydantic_ai integration: auto-populate LLMObs session_id from conversation_id

1 participant