Skip to content

v0.24.0

Choose a tag to compare

@github-actions github-actions released this 27 Jul 14:18
081ad05

Added

  • Every run is given typed dependencies. The endpoint now builds an
    AgentDeps(user=request.user) per run and passes it to the agent, so tools,
    toolsets and capabilities read request-scoped values off RunContext.deps
    pydantic-ai's own seam — instead of the agent closing over the request:

    @tool(registry)
    def whoami(ctx: RunContext[AgentDeps]) -> str:
        """Report the acting user."""
        return str(ctx.deps.user)
    • Spec tools bind the user natively. SpecToolset's default extractor
      reads ctx.deps.user; this package used to override it with a closure,
      purely because deps were None.
    • AG-UI state now lands somewhere. AgentDeps satisfies pydantic-ai's
      StateHandler protocol, so a run's RunAgentInput.state is validated into
      deps.state rather than dropped with a UserWarning. Emitting state back
      is a tool's job — see the new Shared state
      guide.
    • It is the precondition for reusing a built agent. A capability that
      closes over a request can only serve that request, which is why the agent
      (and every tool's JSON schema) is rebuilt on each call today. Nothing about
      that caching changes here; this removes the blocker.
    • A request served without Django's auth middleware has no user attribute
      at all — that is an anonymous run (deps.user is None), matching what
      materialize_request_user already does, not an AttributeError.
  • deps_factory — supply the run's AgentDeps yourself. A
    request -> AgentDeps callable on AGUIServer / DjangoAGUIView replacing
    the default, which binds only the acting user.

    • This closes a gap the previous release opened. Typed deps shipped, but
      the endpoint constructed AgentDeps(user=request.user) itself with no hook,
      so a project could not carry its own per-run context — and, specifically,
      could not have AG-UI's inbound shared state validated, since pydantic-ai
      validates it against type(deps.state) and that requires the deps to arrive
      pre-seeded with a model instance. Now:
      deps_factory=lambda request: AgentDeps(user=request.user, state=DocumentState()).
    • Subclass AgentDeps to carry anything else per run (a tenant, a
      feature-flag snapshot); whatever the factory returns reaches every tool,
      toolset and capability as ctx.deps.

Changed

  • django-pydantic-agent floor raised to >=0.3,<0.4AgentDeps and the
    request-free build_spec_capability come from there.
  • AgentSession(...) now requires a keyword-only deps. Deliberately
    required rather than defaulted: a forgotten deps would mean spec tools
    silently acting as nobody. Only affects code constructing AgentSession
    directly; going through DjangoAGUIView / AGUIServer needs no change.

Documentation

  • New Shared state guide.
    AG-UI's state channel works end to end and needed no package code — a tool
    reads ctx.deps.state and writes back by returning a StateSnapshotEvent as
    ToolReturn metadata, which pydantic-ai's adapter streams verbatim. The guide
    is the recipe joining the two ends, plus when to prefer a tool instead: a
    tool call is visible in the transcript and can be gated by a confirmation
    card, which state events cannot.

    • Writing it surfaced the missing deps_factory seam (above) — without which
      inbound state could not be validated into a model at all. The guide shows
      the validated shape rather than a workaround.
  • CI now checks doc snippets against the installed packages
    make docs-check / scripts/check_docs_snippets.py, wired into the docs job.
    Every Python fence in docs/ and README.md must parse, every
    from X import Y must resolve, and every keyword argument at a resolvable
    call must exist in the real signature.

    • It closes the one gap the other gates share: ruff, ty and
      mkdocs --strict all stop at this package's boundary, so nothing checked a
      claim about a dependency's API — where drift is likeliest, since the
      dependency moves on its own schedule and no test imports the snippet.
    • A module the reader is meant to own is told apart from one that moved by
      whether its root package is installed, not by a name list and not by
      "the import failed". myproject.agent is a placeholder; a dependency
      submodule that has been relocated is a failure — which is the shape of a
      real, boot-breaking defect found in these docs before.
    • Verified against known-bad snippets, not just a clean run: a moved
      dependency module, a bad keyword on one of our types, and a bad keyword on a
      dependency's type are each caught.
    • Limits, stated so nobody over-trusts it: it does not execute snippets or
      check semantics, and it cannot see non-Python fences — a JavaScript
      example remains a matter for review.