Source the nameless ManagedAgent model via the get_model selector; drop the pre-get_model fallback

pydantic-ai#6333's `get_model` now returns `AgentModel` — a static model
or a selector callable receiving `ModelSelectionContext` (agent + deps).
A named `ManagedAgent` still sources the model statically at run setup; a
nameless one returns a per-run selector that derives its variable from
`ModelSelectionContext.agent` on first model selection and memoizes the
choice for the rest of the run (one resolve per run, not per step). So a
model-less agent — named or nameless — now runs entirely from managed
config, closing the one gap the docstrings claimed but couldn't deliver
for the nameless case.

Both paths keep the bare-read discipline (read `.value`, never enter the
`ResolvedVariable` CM), so model selection contributes no baggage and
`wrap_run` stays the sole owner of the run's resolution baggage — the
model runs before the run span exists, so this matters.

Requires the latest pydantic-ai: drops `_FRAMEWORK_HAS_GET_MODEL` and the
`before_model_request` model-swap fallback (now snapshot-only). Extracts
`_ensure_variable_for_agent` so run-time hooks and model selection share
one derivation. Pins the temporary source to an explicit `capability-run-spec`
rev rather than the branch, for reproducibility.
This commit is contained in:
Douwe Maan
2026-07-17 18:36:38 +00:00
parent c1f97a6189
commit b3a7ef657d
8 changed files with 157 additions and 79 deletions
+10 -9
View File
@@ -32,10 +32,9 @@ a nameless capability on a nameless agent raises a clear error. The derived name
exactly the way the Logfire UI normalizes observed agent names (lowercased, non-alphanumeric runs
collapsed to `_`), so the variable the SDK resolves is the variable the UI creates when you manage
an observed agent. Pass an explicit `name` to decouple the variable from the agent's name, or to
share one variable across agents. One edge: a nameless `ManagedAgent` can't *source* the model at
run setup (there's no agent yet at that point) -- it can only override the model on an agent that
already has one. Pass an explicit `name` when you need it to source the model for a model-less
agent.
share one variable across agents. A nameless capability derives its variable -- and can source the
model for a model-less agent -- from the agent's `name` the first time it's needed in a run, so
naming the agent is all it takes.
They share one contract: **the code-defined agent is the fallback.** Every managed value is a
patch on what's written in code -- absent fields keep their code values, and a missing, invalid,
@@ -352,11 +351,13 @@ The variable holds an `AgentConfig`:
with a warning (other patches still apply) rather than breaking the run.
- An override keyed to a tool that no longer exists is inert -- that's the drift case (the tool was
removed or renamed in code), and the Logfire UI is where it becomes visible.
- **Model precedence:** the managed `model` is sourced at run setup via the capability's `get_model`
hook, so it slots in with the right precedence -- a call-site `run(model=...)` beats it, it beats
the agent's constructor model, and a fully model-less agent can be driven entirely from Logfire.
(On older pydantic-ai without the `get_model` hook, the model is instead swapped per request, which
requires a code-side model and can't beat a per-run `model=`.)
- **Model precedence:** the managed `model` is sourced during model selection via the capability's
`get_model` hook, so it slots in with the right precedence -- a call-site `run(model=...)` beats
it, it beats the agent's constructor model, and a fully model-less agent (named or nameless) can be
driven entirely from Logfire. A named capability sources it statically (once per run); a nameless
one derives its variable from the agent when the model is first selected and then reuses that
choice for the run. Callable `targeting_key`/`attributes` don't participate in model selection (it
runs before a run context exists) -- only the static `label` and static targeting inputs do.
- **Settings precedence:** managed settings merge **over** the agent's constructor `model_settings`
and **under** per-run `model_settings=`, so run arguments always win.
- **Adoption reporting:** for the run's duration, `logfire.managed.applied_sections` baggage names
+77 -35
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import dataclasses
import json
import warnings
from collections.abc import Callable, Mapping
@@ -13,9 +12,9 @@ import logfire
from logfire.variables import Variable
from pydantic import BaseModel, ConfigDict, Field
from pydantic_ai import AbstractToolset, RunContext, TemplateStr, ToolDefinition, WrapperToolset
from pydantic_ai.capabilities import AbstractCapability
from pydantic_ai.exceptions import UserError
from pydantic_ai.messages import ModelRequest
from pydantic_ai.models import Model, ModelRequestContext, infer_model
from pydantic_ai.models import ModelRequestContext
from pydantic_ai.settings import ModelSettings
from pydantic_ai.tools import AgentDepsT
from pydantic_ai.toolsets.abstract import ToolsetTool
@@ -24,11 +23,12 @@ from pydantic_ai_harness.logfire._managed_variable import ManagedVariableCapabil
if TYPE_CHECKING:
from pydantic_ai.agent.abstract import AgentModelSettings
from pydantic_ai.capabilities import AgentModel, ModelSelection
from pydantic_ai.capabilities.abstract import WrapRunHandler
from pydantic_ai.models import ModelSelectionContext
from pydantic_ai.run import AgentRunResult
_AGENT_VARIABLE_PREFIX = 'agent__'
_FRAMEWORK_HAS_GET_MODEL = 'get_model' in vars(AbstractCapability)
class AgentConfigSettings(BaseModel):
@@ -240,12 +240,14 @@ class ManagedAgent(ManagedVariableCapability[AgentDepsT, AgentConfig]):
version baggage remains active for the whole run. Managed settings override constructor settings;
settings passed to `run()` override both.
On Pydantic AI versions with the `get_model` capability surface, an explicitly named variable can
supply the model during run setup. This lets a model-less `Agent(None, ...)` run from managed
config, while a call-site `run(model=...)` still wins. Nameless capabilities cannot source a model
at setup because the agent name is only available from a run context. They use the per-request
model swap instead, as do older Pydantic AI versions without `get_model`. Inferred models are
cached by model string.
The managed `model` is sourced during model selection, so it slots in with the right precedence:
a call-site `run(model=...)` beats it, it beats the agent's constructor model, and a fully
model-less `Agent(None, ...)` -- named or nameless -- can be driven entirely from managed config.
A named capability supplies the model statically (resolved once at run setup); a nameless one
supplies a selector that derives its variable from the agent when the model is first selected,
then reuses that choice for the rest of the run. Callable `targeting_key`/`attributes` don't
participate in model selection (it runs before a run context exists) -- only the static `label`
and static targeting inputs do.
Tool overrides change only the definitions shown to the model. Renames route back to the original
implementation, collisions retain the original name with a warning, and unknown tool keys are
@@ -276,8 +278,8 @@ class ManagedAgent(ManagedVariableCapability[AgentDepsT, AgentConfig]):
name: str | Variable[AgentConfig] | None = None
"""Bare variable name, pre-built variable, or `None` to derive it from the agent name.
Nameless derivation cannot source a managed model during run setup; it uses the per-request
fallback instead.
A nameless capability derives its variable (and can source the model) from the agent's `name`
the first time it's needed in a run; the agent must then have a `name`.
"""
default: AgentConfig | None = None
"""Code-side fallback config; omitted sections preserve the corresponding agent behavior."""
@@ -287,7 +289,6 @@ class ManagedAgent(ManagedVariableCapability[AgentDepsT, AgentConfig]):
_auto_create_in_wrap_run: ClassVar[bool] = False
def __post_init__(self) -> None:
self._model_cache: dict[str, Model] = {}
self._setup_variable(
self.name, prefix=_AGENT_VARIABLE_PREFIX, value_type=AgentConfig, default=self.default or AgentConfig()
)
@@ -315,18 +316,68 @@ class ManagedAgent(ManagedVariableCapability[AgentDepsT, AgentConfig]):
return model_settings
def get_model(self) -> str | None:
"""Source the managed model during run setup when the backing variable is already known.
def get_model(self) -> AgentModel[AgentDepsT] | None:
"""Source the managed model with the right precedence (`run(model=...)` > managed > constructor).
Pydantic AI applies an explicit `run(model=...)` after capability models, so the call-site
model wins. A nameless capability returns `None` because deriving its variable requires the
run context; `before_model_request` supplies its compatibility fallback.
When the backing variable is already known (an explicit `name` or `Variable`), the model is
sourced statically here, so Pydantic AI resolves it once at run setup. When the capability is
nameless, the variable is derived from the agent's `name`, which isn't available until a
[`ModelSelectionContext`][pydantic_ai.models.ModelSelectionContext] exists, so a selector
callable is returned instead: it derives and resolves the variable the first time Pydantic AI
selects the model for the run. Either way a fully model-less agent can be driven entirely
from Logfire, and a call-site `run(model=...)` still wins.
Both paths read the value with a **bare** `variable.get()` -- they read `.value` and never
enter the [`ResolvedVariable`][logfire.variables.ResolvedVariable] as a context manager -- so
model selection contributes no baggage: [`wrap_run`][pydantic_ai_harness.logfire.ManagedAgent.wrap_run]
stays the sole owner of the run's resolution baggage. Model selection runs before a
`RunContext` exists, so callable `targeting_key`/`attributes` can't participate (a
`ModelSelectionContext` is deliberately narrower); only the static `label` and static
targeting inputs do. That early read and `wrap_run`'s authoritative resolve return a
consistent value -- `get()` is a cheap lookup over the SDK's cached config, and resolution is
deterministic within a run.
"""
if self._name_omitted:
return None
if not self._name_omitted:
return self._resolve_model_value(self._variable)
return self._model_selector()
def _resolve_model_value(self, variable: Variable[AgentConfig]) -> str | None:
"""Bare-read the managed value's `model` via static targeting -- no CM entry, so no baggage."""
targeting_key = None if callable(self.targeting_key) else self.targeting_key
attributes = None if callable(self.attributes) else self.attributes
return self._variable.get(targeting_key=targeting_key, attributes=attributes, label=self.label).value.model
return variable.get(targeting_key=targeting_key, attributes=attributes, label=self.label).value.model
def _model_selector(self) -> Callable[[ModelSelectionContext[AgentDepsT]], ModelSelection]:
"""Build the per-run selector a nameless `get_model` returns.
Pydantic AI evaluates a selector once per new request step, but the managed model is a
run-stable config value, so the selector memoizes its first choice and every later step
reuses it -- one resolve per run, matching the static (named) path rather than re-reading the
variable each step. A fresh selector (and fresh memo) is built per `get_model` call, i.e. per
run, so nothing leaks across runs. On the first evaluation it derives the backing variable
from `ctx.agent` (the same derivation `wrap_run` performs), reads the managed model, and
falls back to the model Pydantic AI already selected (`ctx.model`) when none is managed --
raising only when there is no model at all (a nameless, model-less agent with nothing
published yet), so the misconfiguration surfaces clearly instead of downstream.
"""
selected: list[ModelSelection] = []
def select(ctx: ModelSelectionContext[AgentDepsT]) -> ModelSelection:
if not selected:
model = self._resolve_model_value(self._ensure_variable_for_agent(ctx.agent))
if model is not None:
selected.append(model)
elif ctx.model is not None:
selected.append(ctx.model)
else:
raise UserError(
'A nameless `ManagedAgent` on a model-less agent has no model to run: the agent '
'defines no model and none is published in Logfire yet. Give the agent a model, '
'pass one to `run(model=...)`, or publish a `model` in the managed config.'
)
return selected[0]
return select
def get_wrapper_toolset(self, toolset: AbstractToolset[AgentDepsT]) -> AbstractToolset[AgentDepsT]:
"""Wrap the agent toolset with managed LLM-facing definition overlays."""
@@ -358,23 +409,14 @@ class ManagedAgent(ManagedVariableCapability[AgentDepsT, AgentConfig]):
async def before_model_request(
self, ctx: RunContext[AgentDepsT], request_context: ModelRequestContext
) -> ModelRequestContext:
"""Capture the creation baseline and apply the per-request managed-model fallback.
"""Capture the code-side creation baseline on the first eligible model request.
Modern explicitly named capabilities source their model through `get_model`. Nameless
capabilities and older Pydantic AI versions instead swap the request model here, reusing an
inferred-model cache across runs.
The managed model itself is sourced at run setup by
[`get_model`][pydantic_ai_harness.logfire.ManagedAgent.get_model], so this hook only snapshots
the code-side agent for auto-create and leaves the request untouched.
"""
self._auto_create_snapshot(request_context)
if _FRAMEWORK_HAS_GET_MODEL and not self._name_omitted:
return request_context
resolved = self.resolved
if resolved is None or resolved.value.model is None:
return request_context
model_string = resolved.value.model
model = self._model_cache.get(model_string)
if model is None:
model = self._model_cache[model_string] = infer_model(model_string)
return dataclasses.replace(request_context, model=model)
return request_context
def _auto_create_snapshot(self, request_context: ModelRequestContext) -> None:
"""Create the code-side `AgentConfig` baseline at the first eligible model request.
@@ -28,6 +28,7 @@ from typing_extensions import TypeVar
if TYPE_CHECKING:
from logfire import Logfire
from logfire.variables import ResolvedVariable, VariableConfig
from pydantic_ai.agent.abstract import AbstractAgent
from pydantic_ai.capabilities.abstract import WrapRunHandler
from pydantic_ai.run import AgentRunResult
@@ -228,11 +229,23 @@ class ManagedVariableCapability(AbstractCapability[AgentDepsT], Generic[AgentDep
def _ensure_variable(self, ctx: RunContext[AgentDepsT]) -> Variable[ValueT]:
"""Return the backing variable, building it from the running agent's `name` on first use.
Thin wrapper over [`_ensure_variable_for_agent`][] reading the agent off a `RunContext`. Model
selection (which has a `ModelSelectionContext`, not a `RunContext`) calls the agent-based
method directly, so both entry points build the same variable and share the cache.
"""
return self._ensure_variable_for_agent(ctx.agent)
def _ensure_variable_for_agent(self, agent: AbstractAgent[AgentDepsT, Any] | None) -> Variable[ValueT]:
"""Return the backing variable, building it from the agent's `name` on first use.
For an eagerly-built variable (an explicit `name` or `Variable` was given) this just returns
it. For a nameless capability it derives `<prefix><agent name>` from `ctx.agent.name` once,
it. For a nameless capability it derives `<prefix><agent name>` from `agent.name` once,
caching the result on the capability so later runs and the other run-time surfaces reuse it.
Raises [`UserError`][pydantic_ai.exceptions.UserError] when there is no agent name to derive
from -- a nameless managed capability requires the agent to have a `name`.
Takes the agent directly (rather than a `RunContext`) so both the run-time hooks and model
selection -- which sees a narrower `ModelSelectionContext` before any `RunContext` exists --
derive the same variable. Raises [`UserError`][pydantic_ai.exceptions.UserError] when there
is no agent name to derive from -- a nameless managed capability requires the agent to have a
`name`.
"""
variable = self._built_variable
if variable is not None:
@@ -244,7 +257,7 @@ class ManagedVariableCapability(AbstractCapability[AgentDepsT], Generic[AgentDep
variable = self._built_variable
if variable is not None:
return variable
agent_name = ctx.agent.name if ctx.agent is not None else None
agent_name = agent.name if agent is not None else None
if not agent_name:
raise UserError(
'A managed capability without an explicit `name` derives its backing variable from '
+9 -5
View File
@@ -82,11 +82,15 @@ lint = [
[tool.uv.sources]
# TEMPORARY PIN — remove when pydantic-ai#6333 is released.
# pydantic-ai#6333 (branch `capability-run-spec`) adds the `AbstractCapability.get_model()` hook that
# `ManagedSettings` and `ManagedAgentSpec` use to source the agent's model with correct precedence
# (fixing the model-less-agent and `run(model=...)`-loses-to-managed-model limits). Once that PR is
# released to PyPI, drop this source so we resolve `pydantic-ai-slim` from the `>=` floor above.
# NOTE: `pydantic-ai-slim` lives in the `pydantic_ai_slim/` subdirectory of the monorepo.
pydantic-ai-slim = { git = 'https://github.com/pydantic/pydantic-ai.git', branch = 'capability-run-spec', subdirectory = 'pydantic_ai_slim' }
# `ManagedAgent` uses to source the agent's model with correct precedence (fixing the
# model-less-agent and `run(model=...)`-loses-to-managed-model limits). Its selector form —
# `get_model()` returning a callable that receives `ModelSelectionContext` (with `.agent`/`.deps`) —
# is what lets a *nameless* `ManagedAgent` source the model too, deriving its variable from the
# agent at selection time. Once that PR is released to PyPI, drop this source so we resolve
# `pydantic-ai-slim` from the `>=` floor above.
# NOTE: `pydantic-ai-slim` lives in the `pydantic_ai_slim/` subdirectory of the monorepo. Pinned to
# an explicit rev (not the branch) so the resolution is reproducible and doesn't drift as the branch moves.
pydantic-ai-slim = { git = 'https://github.com/pydantic/pydantic-ai.git', rev = '3b3bab4220d9086b00cac3d2f8975a178c06b9b6', subdirectory = 'pydantic_ai_slim' }
[tool.hatch.version]
source = 'uv-dynamic-versioning'
+15 -13
View File
@@ -299,23 +299,25 @@ async def test_managed_model_runs_model_less_agent_and_run_model_wins() -> None:
assert result.output == 'call-site'
async def test_nameless_model_fallback_and_cache(monkeypatch: pytest.MonkeyPatch) -> None:
from pydantic_ai_harness.logfire import _managed_agent
async def test_nameless_model_selector_resolves_once_per_run(monkeypatch: pytest.MonkeyPatch) -> None:
# A nameless capability's selector is evaluated once per request step, but the managed model is a
# run-stable value, so it memoizes and resolves the variable exactly once even across steps.
resolves: list[str] = []
original = ManagedAgent[Any]._resolve_model_value
inferred: list[str] = []
original_infer = _managed_agent.infer_model
def counting(self: ManagedAgent[Any], variable: Variable[Any]) -> str | None:
resolves.append(variable.name)
return original(self, variable)
def recording_infer(model: str):
inferred.append(model)
return original_infer(model)
monkeypatch.setattr(ManagedAgent, '_resolve_model_value', counting)
def a_tool() -> str:
return 'ok'
monkeypatch.setattr(_managed_agent, '_FRAMEWORK_HAS_GET_MODEL', False)
monkeypatch.setattr(_managed_agent, 'infer_model', recording_infer)
capability = ManagedAgent(default=AgentConfig(model='test'))
agent = Agent(TestModel(), name='nameless_model', capabilities=[capability])
await agent.run('hello')
await agent.run('again')
assert inferred == ['test']
# A model-less agent with one tool: `TestModel` calls the tool (step 1) then answers (step 2).
await Agent(None, name='multi_step', tools=[a_tool], capabilities=[capability]).run('hello')
assert resolves == ['agent__multi_step']
async def test_known_variable_skips_snapshot_build(capfire: CaptureLogfire, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -116,8 +116,8 @@ def test_ensure_variable_returns_variable_built_while_awaiting_lock() -> None:
return False
capability._build_lock = cast(Any, _RaceLock())
# `ctx` is only touched after the second check builds a new variable; here it returns first.
assert capability._ensure_variable(cast(Any, None)) is built
# `agent` is only touched after the second check builds a new variable; here it returns first.
assert capability._ensure_variable_for_agent(cast(Any, None)) is built
def test_auto_create_marking_rechecks_guard_under_lock(monkeypatch: pytest.MonkeyPatch) -> None:
+18 -2
View File
@@ -42,5 +42,21 @@ def test_explicit_name_rules_unchanged() -> None:
ManagedAgent('Checkout Agent')
def test_nameless_get_model_returns_none() -> None:
assert ManagedAgent().get_model() is None
def test_nameless_get_model_returns_selector() -> None:
# A nameless capability can't source the model statically (no agent yet), so `get_model` hands
# back a selector Pydantic AI evaluates once it has a `ModelSelectionContext`.
assert callable(ManagedAgent().get_model())
async def test_nameless_sources_model_for_model_less_agent() -> None:
# The nameless selector derives `agent__solo` from the agent's name and drives a model-less agent.
capability = ManagedAgent(default=AgentConfig(model='test'))
result = await Agent(None, name='solo', capabilities=[capability]).run('hello')
assert result.output.startswith('success')
assert capability._variable.name == 'agent__solo'
async def test_nameless_model_less_agent_without_managed_model_raises() -> None:
agent = Agent(None, name='unpublished', capabilities=[ManagedAgent()])
with pytest.raises(UserError, match='no model to run'):
await agent.run('hello')
Generated
+9 -9
View File
@@ -1059,10 +1059,10 @@ lint = [
requires-dist = [
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "logfire", marker = "extra == 'logfire'", specifier = ">=4.31.0" },
{ name = "pydantic-ai-slim", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec" },
{ name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec" },
{ name = "pydantic-ai-slim", extras = ["spec"], marker = "extra == 'logfire'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec" },
{ name = "pydantic-ai-slim", extras = ["temporal"], marker = "extra == 'temporal'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec" },
{ name = "pydantic-ai-slim", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6" },
{ name = "pydantic-ai-slim", extras = ["dbos"], marker = "extra == 'dbos'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6" },
{ name = "pydantic-ai-slim", extras = ["spec"], marker = "extra == 'logfire'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6" },
{ name = "pydantic-ai-slim", extras = ["temporal"], marker = "extra == 'temporal'", git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6" },
{ name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = ">=0.0.16" },
{ name = "pydantic-monty", marker = "extra == 'codemode'", specifier = ">=0.0.16" },
]
@@ -1076,7 +1076,7 @@ dev = [
{ name = "inline-snapshot", specifier = ">=0.32.5" },
{ name = "logfire", extras = ["httpx"], specifier = ">=4.31.0" },
{ name = "pydantic-ai-harness", extras = ["code-mode"] },
{ name = "pydantic-ai-slim", extras = ["spec"], git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec" },
{ name = "pydantic-ai-slim", extras = ["spec"], git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6" },
{ name = "pytest", specifier = ">=9.0.0" },
{ name = "pytest-anyio" },
{ name = "pytest-examples", specifier = ">=0.0.18" },
@@ -1089,8 +1089,8 @@ lint = [
[[package]]
name = "pydantic-ai-slim"
version = "2.5.2.dev15+b0ed7683"
source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&branch=capability-run-spec#b0ed768337a7c2134b0fcb43555f1bc28488e8c8" }
version = "2.11.1.dev11+3b3bab42"
source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_ai_slim&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6#3b3bab4220d9086b00cac3d2f8975a178c06b9b6" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "genai-prices" },
@@ -1232,8 +1232,8 @@ wheels = [
[[package]]
name = "pydantic-graph"
version = "2.5.2.dev15+b0ed7683"
source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_graph&branch=capability-run-spec#b0ed768337a7c2134b0fcb43555f1bc28488e8c8" }
version = "2.11.1.dev11+3b3bab42"
source = { git = "https://github.com/pydantic/pydantic-ai.git?subdirectory=pydantic_graph&rev=3b3bab4220d9086b00cac3d2f8975a178c06b9b6#3b3bab4220d9086b00cac3d2f8975a178c06b9b6" }
dependencies = [
{ name = "httpx" },
{ name = "logfire-api" },