Skip to content

Conversation

@sstanovnik
Copy link

@sstanovnik sstanovnik commented Nov 7, 2025

This allows users to add extra metadata to the agent's span, either as a direct string or dict, or a callable that takes the RunContext. The attributes are added/computed after the agent finishes. The metadata is under the logfire.agent.metadata attribute.

Comment on lines 677 to 717
def _run_span_end_attributes(
self,
settings: InstrumentationSettings,
usage: _usage.RunUsage,
message_history: list[_messages.ModelMessage],
new_message_index: int,
graph_ctx: GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, OutputDataT]],
):
Copy link
Author

Choose a reason for hiding this comment

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

I reworked what attributes are passed to this because the existing ones are already available in the RunContext. I hope that's okay.

@DouweM
Copy link
Collaborator

DouweM commented Nov 7, 2025

@sstanovnik Have you seen Baggage https://logfire.pydantic.dev/docs/reference/advanced/baggage/#basic-usage That could also be a solution here that doesn't require us to add any new fields.

docs/logfire.md Outdated
from pydantic_ai.models.instrumented import InstrumentationSettings


def span_attribute_callback(ctx: RunContext[None]) -> dict[str, str]:
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is useful, but not type safe unfortunately, as InstrumentationSettings is not generic in the deps type (and probably shouldn't be), meaning that you could create a span_attribute_callback that takes RunContext[Foo] and use it on an Agent[Bar] without any typing issues, which would then fail at runtime if you try to read attrs that exist on Foo but not Bar...

The obvious ways to solve that would be to make InstrumentationSettings generic or to make Agent and agent.run take the span_attributes directly, but I don't like either of those :) If baggage works for you, I'd be inclined to not add any new feature to Pydantic AI and just document that instead.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I noticed that this couldn't be made type-safe without making the carrier generic, but that seems like a huge undertaking that also changes the interface in a way that, I would think, isn't desirable until v2.

Baggage would work for my immediate usecase, even though it applies to all child spans, which I don't really want it to - conceptually, I'm wanting to add data to the one specific span.

What about only accepting a dict for now, and leaving a callable taking RunContext[TDeps] for whenever InstrumentationSettings can be adapted to take a generic parameter?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@sstanovnik Maybe instrumentation settings are not the best place for this:

In line with #3263, what do you think about arbitrary metadata: dict[str, Any] on the Agent, that's also set on the agent run span under a metadata attribute? That way you wouldn't be setting span attributes directly (assuming that's not an issue), but they would be queryable etc, and it'd feel more natural to add this on both Agent and agent.run directly, with support for RunContext-taking callables.

Copy link
Author

Choose a reason for hiding this comment

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

That's also great - any way of adding information to the span would be great. What do you mean by

That way you wouldn't be setting span attributes directly (assuming that's not an issue), but they would be queryable [...]

I see that the linked PR does add the metadata into the span.

I'm up for closing this PR and opening another with the same approach using metadata, if you'd like :)

Copy link
Collaborator

Choose a reason for hiding this comment

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

@sstanovnik I just mean that the metadata dict keys will not directly become span attributes, as they'd be nested under metadata, but if that's fine for your use case, feel free to update this PR (with force push possibly), or create a new one!

Metadata is attached to the logfire.agent.metadata span attribute.
It can either be a string, dict, or a callable taking the RunContext
and returning a string or a dict.
@sstanovnik sstanovnik force-pushed the extra-agent-span-attributes branch from c57e1bb to 11687ce Compare November 14, 2025 20:21
@sstanovnik sstanovnik changed the title Add span_attributes to InstrumentationSettings. Add metadata to the Agent class. Nov 14, 2025
Copy link
Author

@sstanovnik sstanovnik left a comment

Choose a reason for hiding this comment

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

Force-pushed this to retain the relevant conversation history of the previous implementation. I also added some comments on particulars I'm not certain about.

A major difference from your suggestion is that I did not add this to the Agent.run method. I felt the change was large enough as it is without having to figure out how to wire/override/merge that.

},
)

run_metadata: str | dict[str, str] | None = None
Copy link
Author

Choose a reason for hiding this comment

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

We could technically set a non-callable metadata even if the run fails, and the ctx isn't available. What do you think? I opted for the simpler implementation.

Copy link
Collaborator

Choose a reason for hiding this comment

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

If the run fails, wouldn't we still have RunContext, or at least deps? I think we should try to always store the metadata.

We should also document explicitly whether the callable is executed at the start or end of the run, as RunContext would change (e.g. messages, usage); I think at the end is best.


self._output_type = output_type
self.instrument = instrument
self._metadata = metadata
Copy link
Author

Choose a reason for hiding this comment

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

Not sure about the sunder, and that it's not represented in repr. I went by feeling.

### Adding Custom Metadata

Use the agent's `metadata` parameter to attach additional data to the agent's span.
Metadata can be provided as a string, a dictionary, or a callable that reads the [`RunContext`][pydantic_ai.tools.RunContext] to compute values on each run.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer to only support dict[str, Any] (or a callable returning that), as we do for other metadata fields.

)
```

When instrumentation is enabled, the resolved metadata is recorded on the agent span under the `logfire.agent.metadata` attribute.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I may still change my mind on this, but I don't think it needs the logfire. prefix as it's user-provided rather than Logfire specific (like some of the Evals stuff is)


### Adding Custom Metadata

Use the agent's `metadata` parameter to attach additional data to the agent's span.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Right now this is very instrumentation specific, although the name intentionally isn't. I think we should also make the resulting metadata available on AgentRunResult (and the other classes that have a run_id field) so the user can read/store it after the fact.

},
)

run_metadata: str | dict[str, str] | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the run fails, wouldn't we still have RunContext, or at least deps? I think we should try to always store the metadata.

We should also document explicitly whether the callable is executed at the start or end of the run, as RunContext would change (e.g. messages, usage); I think at the end is best.

toolsets: Sequence[AbstractToolset[AgentDepsT]] | _utils.Unset = _utils.UNSET,
tools: Sequence[Tool[AgentDepsT] | ToolFuncEither[AgentDepsT, ...]] | _utils.Unset = _utils.UNSET,
instructions: Instructions[AgentDepsT] | _utils.Unset = _utils.UNSET,
metadata: AgentMetadataValue[AgentDepsT] | _utils.Unset = _utils.UNSET,
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we're doing override, I'd like to do run (and all the derived methods...) as well. That run arg would be merged into the agent-construction-time metadata.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants