Releases: Artui/django-ag-ui
Release list
v0.26.3
Changed
-
[drf-mcp]→djangorestframework-mcp-server>=0.17,<0.22, taking in both
0.20.0 and 0.21.0. Two consumer-reported blockers, neither of which touches
this transport:- 0.21.0 —
DjangoOAuthToolkitBackendrejected every bearer token once a
resource URL was configured. Audience enforcement read aresourcefield
that DOT'sAccessTokendoes not have, so it could never succeed;
enforcement is now the separateENFORCE_AUDIENCE, default off. - 0.20.0 — dynamically registered clients could not be issued an ID token
(Application.algorithmwas never set), so the token endpoint 500'd whenever
the advertisedopenidscope was requested.
Both are confined to drf-mcp's OAuth surface. The bridge consumes
MCPServer
in-process and never constructs an auth backend, so no adaptation was needed —
verified with the lock updated to 0.21.0 and the suite green.0.20.0 also added
UndescribedToolWarning, which the bridge fixtures trip by
registering throwaway tools; filtered inpyproject.toml. - 0.21.0 —
v0.26.2
Changed
-
[drf-mcp]→djangorestframework-mcp-server>=0.17,<0.20, so 0.19.0 is
installable. That release fixes dynamic client registration, which issued
credentials that could never authenticate:token_endpoint_auth_methodwas
not modelled, so every registration silently became a confidential client, and
theclient_secrethanded back was the stored PBKDF2 digest rather than the
secret. Both are confined to drf-mcp'scontrib.oauth— the bridge this
extra backs consumesMCPServer, which did not change, so the widening is
purely a ceiling lift and this transport's own behaviour is unaffected.Verified rather than assumed: the suite runs green against 0.19.0 with the
lock updated.
v0.26.1
Changed
-
Widened the two agent-tool integration pins, matching
django-pydantic-agent0.4.2, so the current majors of both backing packages
are installable:[spec-tools]→djangorestframework-pydantic-ai>=0.9,<0.11. 0.10.0 adds
SpecToolset(host=…), the origin that makes DRF'sFileField/
Hyperlinked*fields render absolute URLs off the HTTP path. Nothing here
uses it — the widening is what lets a consumer pass it.[drf-mcp]→djangorestframework-mcp-server>=0.17,<0.19. 0.18.0 fixes two
reported crashes: serializer-context providers called positionally
(TypeErrorfor any provider not leading withview, request) and the
missing DRF baseline context (KeyError: 'request'). Its one deliberate
break — a provider whose first two parameters are named something other than
view/requestnow raises — applies to user provider signatures, not to
anything this package calls.
The floors stay at
0.9/0.17: neither integration uses new API, so both
ranges are honestly satisfiable, and a project already pinned to an older
release isn't forced forward.
v0.26.0
Added
-
CompactionObserver— tell the client when history was compacted.
Compaction is deliberately invisible upstream: it runs inside
before_model_request, mutates the message list, and emits nothing. Right
for the model, wrong for a person watching a long run, who sees earlier turns
quietly stop informing the answers with no explanation. Wrapping the capability
is the seam for saying so:capabilities=[CompactionObserver(SlidingWindow(max_messages=80, keep_messages=40))]
Opt-in by construction — pass the strategy unwrapped and nothing is emitted.
- The compaction itself is untouched.
CompactionObserversubclasses
pydantic-ai'sWrapperCapabilityand overrides one hook, so ordering,
deferral and hook introspection all delegate to the wrapped capability.
A hand-rolled proxy would have silently dropped those. - The wire stays vanilla AG-UI. Each firing emits a standard
ACTIVITY_SNAPSHOTwithactivityType: "compaction"(exported as
COMPACTION_ACTIVITY_TYPE) andcontentcarryingremoved/before/
after— not aCUSTOMevent, so any AG-UI client can render it and ours
is not privileged. Handle it with@ag-ui/client's
onActivitySnapshotEvent. - Placed where a reader wants it: interleaved immediately before the
events of the turn that ran with the shortened history, with a fresh
messageIdper firing — a compaction is a distinct occurrence, not a
mutation of a previous one. - What it does not detect, stated plainly: detection is a message-count
comparison across the wrapped call, because that is all the upstream
contract exposes. A strategy that rewrites history without shortening it
does not register — matching what the indicator claims (turns were dropped)
rather than over-promising a general "history changed" signal. - Per-run, not per-instance. The observer records into a
ContextVar. A
consumer builds the capability once at configuration time and the same
instance serves every request, so instance state would interleave concurrent
runs into each other's transcripts.
- The compaction itself is untouched.
Documentation
- New page: Compaction & skills — adopting the harness
compactionandSkillscapabilities through theCAPABILITIESseam (pure
composition, no code here), then the indicator above. Strategy-by-strategy
guidance lives in django-pydantic-agent's integrations guide, where the seam
itself is documented.
v0.25.0
Changed
[harness]now requirespydantic-ai-harness>=0.12,<0.13(was>=0.7,<0.8)
anddjango-pydantic-agent>=0.4,<0.5(was>=0.3,<0.4). The harness ceiling
had gone five minors stale, which also gated
pydantic_ai_harness.skills—
it does not exist below 0.11.- The resume / fork path needed the substrate fix that came with it. Harness's
StepStoreprotocol gainedlatest_snapshot(*, run_id, include_interrupted=False)
and itscontinue_run()passes the new argument, soresume/<run_id>/raised
TypeError: latest_snapshot() got an unexpected keyword argumentunder 0.12
untilDefaultStepStorewas adapted (django-pydantic-agent 0.4.0, which adds a
statecolumn and migration0002— runmigratewhen upgrading).
This package's resume tests are what surfaced the break; the substrate's own
suite could not see it, because it callslatest_snapshot()directly rather
than through the harness helper that drives the protocol. GET runs/'scontinuableflag still means what it says. It is computed
fromlatest_snapshot(run_id=…)precisely because that is the callresume
makes, and both now default toinclude_interrupted=False— so a run reported
continuable is still exactly a runresumewould find a snapshot for. Had the
new argument defaulted the other way upstream, the flag would have started
lying about interrupted-only runs rather than failing loudly.StepPersistence's constructor is unchanged at 0.12, so the capability wiring
needed no adaptation.- Raise the drf-chain ceilings:
[drf-mcp]→djangorestframework-mcp-server>=0.17,<0.18
(was>=0.15,<0.16) and[spec-tools]→djangorestframework-pydantic-ai>=0.9,<0.10
(was>=0.8,<0.9). The MCP ceiling had gone stale a wave earlier — drf-mcp
0.16.0 (MCP Apps) was already excluded — so two upstream releases were
unreachable from here rather than one. No adaptation was needed, which the
three relevant upstream changes explain:- MCP Apps (drf-mcp 0.16.0) adds
ui://resources and_meta.uilinks on
tool definitions. The bridge readsname/description/inputSchema/
outputSchema/annotationsofftools/listand ignores_meta, so the
addition is inert here. The resource-encoding fix in the same release (non-JSON
resource bodies no longer come back as quoted JSON string literals) touches
the resource surface, which this bridge does not use — it calls tools only. - The shared
UrlKwarg/QueryParam(drf-mcp 0.17.0, PAI 0.9.0) are
re-exported fromdjangorestframework-servicesrather than defined locally,
behind permanently preserved import paths. Neither is imported here. PAI's
switch fromValueErrortoImproperlyConfiguredfor a bad channel
declaration is likewise unreachable:SpecCapabilityis constructed with a
spec mapping and no channel registrations. InputRequiredenforcement (drf-services 0.28) makes a missing
marked-required input raiseServiceValidationErrorat dispatch. Over the
MCP bridge that already arrives as anisErrorresult with
type == "validation_error", whichcall_toolmaps toModelRetry— so a
spec adopting the marker gets a model-correctable failure through this path
with no change here.
- MCP Apps (drf-mcp 0.16.0) adds
v0.24.0
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 offRunContext.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
readsctx.deps.user; this package used to override it with a closure,
purely because deps wereNone. - AG-UI
statenow lands somewhere.AgentDepssatisfies pydantic-ai's
StateHandlerprotocol, so a run'sRunAgentInput.stateis validated into
deps.staterather than dropped with aUserWarning. 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
userattribute
at all — that is an anonymous run (deps.user is None), matching what
materialize_request_useralready does, not anAttributeError.
- Spec tools bind the user natively.
-
deps_factory— supply the run'sAgentDepsyourself. A
request -> AgentDepscallable onAGUIServer/DjangoAGUIViewreplacing
the default, which binds only the acting user.- This closes a gap the previous release opened. Typed deps shipped, but
the endpoint constructedAgentDeps(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 againsttype(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
AgentDepsto carry anything else per run (a tenant, a
feature-flag snapshot); whatever the factory returns reaches every tool,
toolset and capability asctx.deps.
- This closes a gap the previous release opened. Typed deps shipped, but
Changed
django-pydantic-agentfloor raised to>=0.3,<0.4—AgentDepsand the
request-freebuild_spec_capabilitycome from there.AgentSession(...)now requires a keyword-onlydeps. Deliberately
required rather than defaulted: a forgottendepswould mean spec tools
silently acting as nobody. Only affects code constructingAgentSession
directly; going throughDjangoAGUIView/AGUIServerneeds no change.
Documentation
-
New Shared state guide.
AG-UI's state channel works end to end and needed no package code — a tool
readsctx.deps.stateand writes back by returning aStateSnapshotEventas
ToolReturnmetadata, 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_factoryseam (above) — without which
inbound state could not be validated into a model at all. The guide shows
the validated shape rather than a workaround.
- Writing it surfaced the missing
-
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 indocs/andREADME.mdmust parse, every
from X import Ymust 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,
tyand
mkdocs --strictall 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.agentis 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.
- It closes the one gap the other gates share: ruff,
v0.23.0
Added
-
GET runs/— the discovery half of resume/fork. Mounted alongside
resume/<run_id>/andfork/<run_id>/whenever astep_storeis configured,
the same waythreads/mounts with a conversation store.Both existing endpoints address a run by id, so on their own a client can
only continue a run whose id it still holds — which rules out resuming after a
page reload or from another device, most of what durable step persistence is
for.list_runs()was already implemented on the store; nothing exposed it.Each row carries
run_id,thread_id,parent_run_id,started_atand
continuable— whether the run has a snapshot to seed from, answered by
making the samelatest_snapshotcallresumeitself makes rather than
inferring it from event counts. A run that never reached a provider-valid
boundary has no snapshot, so resuming it would start from nothing: a client
should offer the action only wherecontinuableis true and treat the rest as
informational.parent_run_idexposes fork lineage so a UI can show a branch
rather than listing near-identical transcripts.Owner-scoped by the store, and nothing on the wire names an owner — another
user's runs are simply absent, never a403that would confirm the id exists.
Carries the same authentication seam as every other mounted view, and
authorization runs before the store is built, so a denied request never
reaches the database.
v0.22.0
Added
AGUIServer(service_specs=…)accepts aSpecRegistryas well as a
name -> specmapping (djangorestframework-services0.27+). A project
exposing the same specs over this endpoint and an MCP server and HTTP views
declares them once in the registry; each transport reads that one source
instead of repeating the list. A filtered view (by_tag/subset) is itself
a registry, so two endpoints can be given different projections with no shared
state.- Normalised once, at construction, into a plain dict — which is the whole
design. Three things downstream consume this value, and a registry reaching
any of them unresolved fails differently:build_tool_catalogcalls
.items()(AttributeError), and the view's tool-name reservation
iterates it — a registry yieldsRegisteredSpecrecords rather than name
strings, which would fill the collision set with dataclasses and silently
stop detecting duplicate tool names between the@toolregistry, the drf-mcp
bridge and the spec tools. Resolving at the entry point means nothing
downstream changes. - The mapping is copied, so a caller mutating theirs afterwards no longer
leaks into a configured server.
- Normalised once, at construction, into a plain dict — which is the whole
Changed
- Dependency pins advance together:
django-pydantic-agent>=0.2,<0.3
(forresolve_spec_mapping/SpecSource),[spec-tools]
djangorestframework-pydantic-ai>=0.8,<0.9,[drf-mcp]
djangorestframework-mcp-server>=0.15,<0.16. The last two are not optional
housekeeping: drf-mcp 0.12 capped drf-services at<0.26while PAI 0.8
requires>=0.27, so raising one without the other leaves the two extras
mutually uninstallable.
Fixed
- The persistence docs pointed at a module that no longer exists. Four pages
told readers to add"django_ag_ui.contrib.store"toINSTALLED_APPSand to
importDefaultConversationStore/DefaultAttachmentStore/
DefaultStepStorefrom it. That app moved to
django_pydantic_agent.contrib.storein 0.21.0's extraction and the docs were
not updated, so following them raisedModuleNotFoundErrorat startup. All
references repointed.
Documentation
- The
service_specs=reference was three ways stale. It described the
argument as "a dotted path" —import_stringwas removed package-wide in
0.19.0, and the value has been the mapping itself since. Its example built
SelectorSpec(serializer=…, queryset=…), and neither is a field (nor was the
requiredkindpresent), so the snippet raisedTypeErroras written. And it
creditedSpecCapabilitywith emitting the spec conventions, which moved onto
SpecToolset.get_instructions()in PAI 0.6.0 — the capability delegates so the
block reaches the prompt exactly once. All three corrected, plus a section on
declaring specs once across transports. - A snippet in the
agent_factory=section put a bare...after a keyword
argument, which is aSyntaxError— so it could not be copied, and it made
the whole fence invisible to doc-checking tooling. That is how the broken
SelectorSpecexample above survived in the same file.
v0.21.0
Changed
-
The agent-host substrate now lives in
django-pydantic-agent, a new
settings-agnostic package this release depends on. Agent construction, the tool
registry, toolset/capability composition, audit, the tool guard, user resolution,
the storage contracts and the reference store models moved there; this package
keeps the AG-UI transport — the view, the SSE stream,AGUIServer.urls, the
browser-facing sub-views, skills and transcription. It is the lift-down that lets
a second transport share one substrate.The public surface is unchanged: every moved symbol is permanently
re-exported, sofrom django_ag_ui import ToolRegistry(and friends) keeps
working and downstream projects need only a version bump.
Breaking
INSTALLED_APPS: the reference store app moved — replace
"django_ag_ui.contrib.store"with"django_pydantic_agent.contrib.store".- The model stores no longer read
DJANGO_AG_UI["ALLOW_ANONYMOUS"]. A
settings-agnostic substrate cannot read a transport's settings key, so pass
allow_anonymous=Trueto the store constructor instead. Conversation.messagesholds JSON-serialisable records, notag_uiMessage
objects. The substrate persists transport-owned records verbatim (client
message ids survive untouched) and the AG-UI wire shape is converted at this
package's boundary. Code that readmessage.contentoff a loaded conversation
now readsmessage["content"];messages_to_jsonable/messages_from_jsonable
live indjango_ag_ui.persistence.utils.- The attachment toolset's internal id is now
django-pydantic-agent-attachments.
Tool names (read_attachment) and the wire are unaffected.
v0.20.0
Added
- Durable step persistence — a model-backed, owner-scoped store for
pydantic-ai-harness'sStepPersistencecapability. Pass
AGUIServer(step_store=DefaultStepStore)(the constructor is the
request -> StepStorefactory — the harness protocol carries no request, so
the store binds one and is built per run) and every run records an append-only
event log, a(run_id, tool_call_id)tool-effect ledger, and a continuable
snapshot at each provider-valid boundary, keyed on the AG-UIrun_id. Four new
models underdjango_ag_ui.contrib.store(StoredRun/StoredStepEvent/
StoredSnapshot/StoredToolEffect, migration0003) back
DefaultStepStore, which structurally satisfies the harnessStepStore
protocol. Every row filters by the resolved owner, so arun_idfrom one user
can't read another's runs; an anonymous request withoutALLOW_ANONYMOUS
degrades to no-op rather than aborting the run. Requires the[harness]extra.
A custom backend is anyrequest -> StepStorecallable. See
Durable step persistence. - Resume / fork endpoints — configuring a
step_storealso mounts owner-scoped
resume/<run_id>/andfork/<run_id>/endpoints. Both seed a new run from a
prior run's last continuable snapshot: the server loads it (arun_idfrom
another owner is a clean404), injects it as the run'smessage_history
(AgentSessiongained the seam;run_stream_nativecomposes it ahead of the
client's new turn), and records the new run withparent_run_idpointing back
at the source — so the parent is never mutated.resumeandforkare two
names for one mechanism (the harness'scontinue_run/fork_runare
data-identical). The web-component checkpoint UI rides a downstream release.