Moving from the old packages¶
django-spec-agents is two packages merged under one import path. The agent
host was published as django-pydantic-agent, and django_spec_agents.spec_tools
as djangorestframework-pydantic-ai. Both have had their final release, and both
keep working exactly as that release did: nothing in them warns, breaks or
re-exports. This page is for the day you move.
A project that used both moves both halves at once. Read the two sections below, then the seams 0.2.0 changes, which apply to anyone serving specs as tools.
From django-pydantic-agent¶
Install¶
anthropic, openai, google, drf-mcp and harness keep their names. The
[spec-tools] extra, which pulled djangorestframework-pydantic-ai, becomes
django-spec-agents[specs].
Imports¶
The package's layout carries over under the new root:
| Was | Now |
|---|---|
django_pydantic_agent |
django_spec_agents, with the same root names |
django_pydantic_agent.<module> |
django_spec_agents.<module>, such as django_spec_agents.contrib.store |
django_pydantic_agent.constants |
one module per name under django_spec_agents.types |
ToolCategory, X_DESTRUCTIVE_KEY, X_CATEGORY_KEY, X_CONFIRM_KEY,
X_SUMMARY_KEY and DESTRUCTIVE_METADATA_KEY keep their spelling and are
imported from the root:
ATTACHMENT_STORAGE_ALIAS is not a root name; import it from
django_spec_agents.types.attachment_storage_alias.
INSTALLED_APPS and the store's tables¶
- Run
migrateon django-pydantic-agent's final release first. The new store renames the old store's tables in place, and needs every one of the old store's migrations applied before it runs; short of them it stops withOldInstallBehindErrorand renames nothing. - Replace the dependency, and change every
django_pydantic_agentimport todjango_spec_agents. -
Change the
INSTALLED_APPSentry: -
Run
migrateonce. The app's label isdjango_spec_agents_store, and its one migration,0001_initial, renames the tables, their named constraints and indexes, the content types and the migration records. Nothing is copied.
What the rename touches, what it leaves under the old names on PostgreSQL and how to roll it back are on the storage page.
A project migration that names the old label stops loading once the old app
leaves INSTALLED_APPS: a dependency such as
("django_pydantic_agent_store", "0005_storedmemory_storedmemoryoperation")
raises NodeNotFoundError, and a foreign key to
"django_pydantic_agent_store.StoredConversation" fails the system check.
Point the foreign key, in the model and in the migration, at
django_spec_agents_store.<Model>, and the dependency at
("django_spec_agents_store", "0001_initial"). Your migration is recorded as
applied and now depends on one that is not, which Django refuses as
InconsistentMigrationHistory, so run that first migrate with the apps holding
such migrations left out of INSTALLED_APPS, then put them back. The rows your
foreign keys point at are kept.
Names that keep their spelling¶
Each is stored or configured outside Python, so a rename would break a project silently. They keep django-pydantic-agent's spelling:
- the attachment storage alias
django_pydantic_agent_attachments; - the session key
django_pydantic_agent_conversations; - the attachment upload prefix
django_pydantic_agent/attachments/; - the
django_pydantic_agent.auditanddjango_pydantic_agent.failureloggers; - the
agent_store_*management commands.
Names that change¶
DESTRUCTIVE_METADATA_KEYis"django_spec_agents.destructive".- The attachments toolset id is
django-spec-agents-attachments. - The audit
ContextVaris nameddjango_spec_agents.audit.execution. - Model-build and failure-policy messages start
django-spec-agents:. - A registry tool registered under its own name, with
@tool(registry, name="...")or as aToolSpecregistered directly, is offered to the model under that name, where django-pydantic-agent offered the function's. Arequire_approvalorexemptentry, an approval prompt, or a query on the audit or step store'stool_namethat named such a tool by its function name uses the registered name after moving. Arequire_approvalentry left on the function name no longer matches, so that tool runs without approval.
From djangorestframework-pydantic-ai¶
Install¶
[specs] installs the spec kernel, django-service-specs, and nothing else.
There is no [filter] extra: a project declaring a filter_set has
django-filter installed already, and django-service-specs[filter] declares it.
A project reaching the toolset through django-ag-ui installs django-ag-ui[specs]
where it installed [spec-tools].
Where each name went¶
| Was | Now |
|---|---|
rest_framework_pydantic_ai.SpecToolset |
django_spec_agents.spec_tools.SpecToolset |
rest_framework_pydantic_ai.SpecCapability |
django_spec_agents.spec_tools.SpecCapability |
rest_framework_pydantic_ai.AgentConventions |
django_spec_agents.spec_tools.AgentConventions |
rest_framework_pydantic_ai.AgentDeps |
django_spec_agents.AgentDeps, the host's own deps type |
rest_framework_pydantic_ai.QueryParam, UrlKwarg |
a kernel Parameter in the spec's reads= or shapes=; see Specs |
rest_framework_pydantic_ai.testing |
django_spec_agents.testing, with the same three doubles |
rest_framework_pydantic_ai.__version__ |
django_spec_agents.__version__ |
rest_framework_pydantic_ai.spec_toolset, .spec_capability, .types.agent_conventions |
the same module under django_spec_agents.spec_tools, such as django_spec_agents.spec_tools.spec_toolset for the SpecSource alias a subclass annotates with |
from django_spec_agents import AgentDeps
from django_spec_agents.spec_tools import AgentConventions, SpecCapability, SpecToolset
The logger is django_spec_agents.spec_tools. A LOGGING entry under
rest_framework_pydantic_ai stops receiving the toolset's records, and nothing
fails to tell you.
The host's AgentDeps has more fields. It is user, ip_address, state,
progress, call_context, so a positional AgentDeps(user, reporter) binds the
reporter to ip_address without complaint. Pass progress= by keyword.
Specs¶
The toolset serves the kernel's specs, ServiceSpec and SelectorSpec from
django_service_specs, in a mapping or a SpecRegistry. A
djangorestframework-services spec is not a supported source: declare each spec
the agent serves on the kernel. What that changes for a tool:
- A read is a
Parameter. What the model sends for the callable goes inreads=, and what shapes the output and reaches only the presenter inshapes=. A selector or provider takes a read by its name, as an argument. - Audiences are markings on the output. What
agent_contractdeclared moves onto the spec: its reads toreads=andshapes=, its field audiences to aFieldMarkingon each field of the presenter's output, as hiding plumbing from the model shows. - Permissions are the kernel's
PermissionCheckinstances, inpermissions=. A spec declaring none is refused when the toolset is built;require_permissions=Falsedowngrades that to a warning while migrating, andpermissions=[Unrestricted()]says anyone may call it. - A filter's
orderingis an array of sort names. A string is handed back to the model asordering: Expected an array., so an agent prompt or a tool description that describes it as one comma-separated value says the wrong thing.
from django_service_specs import (
DataclassPresenter,
Parameter,
Parameters,
SelectorKind,
SelectorSpec,
Unrestricted,
)
list_orders_spec = SelectorSpec(
kind=SelectorKind.LIST,
selector=list_orders, # def list_orders(*, user, status=None)
reads=Parameters.of(Parameter("status", "string")),
presenter=DataclassPresenter(OrderOut),
permissions=[Unrestricted()],
)
Mount keywords¶
query_params=, tool_query_params=, url_kwargs= and tool_url_kwargs= are
gone, and passing one raises TypeError. A read the model sends is declared on
the spec, in reads=. A value the server supplies, such as a capture bound to
the acting user's project, goes in tool_server_arguments=, one kernel
ServerArguments per tool, built per run where the value depends on the user:
from django_service_specs import ServerArguments
toolset = SpecToolset(
registry,
tool_server_arguments={
"list_document_vendors": ServerArguments(
values={"project_pk": str(document.project_id)},
withheld=frozenset({"draw_request_pk"}),
),
},
)
A bound name is filled whatever the model sends, a withheld one is left to its default, and neither is advertised. See arguments the server supplies.
The seams 0.2.0 changes¶
These changed between django-spec-agents 0.1.0 and 0.2.0, so they apply to a project moving from either old package and to one already on 0.1.0.
What a call carries besides its arguments¶
The toolset builds no request and no view for a call. What each part of a spec sees:
- A permission check sees the call's arguments. A kernel
PermissionCheckmethod declaring the keywordargumentsis handed them, one declaringcall_contextthe call's context, with the tool's name underaction, and one named after a registered seed that seed's value. See what a permission check sees. - A provider or selector declaring
requestorviewreceivesNoneunless the run puts one incall_contextor acall_seedsoverride returns one. A value it used to read offview.kwargsorrequest.query_paramsit now takes by name, as a read.
The request a run arrived on goes in the run's context, where the toolset's
http_request= and get_http_request= used to name it. A presenter can read it
there to make a URL absolute, for example with request.build_absolute_uri, where
host= named one; the kernel's presenters do not do this:
from django_spec_agents import AgentDeps
deps = AgentDeps(user=request.user)
deps.call_context["request"] = request
result = await agent.run(prompt, deps=deps)
build_context becomes call_seeds¶
An override of build_context that varied the context per run becomes an
override of call_seeds(*, ctx, tool_name), returning the call's own seeds,
name to resolver. A view or request it returns replaces the call's own, a
name the mount's pool_seeds registers too is refused with ValueError, and a
seed for a read the spec declares is refused with ImproperlyConfigured:
from django_spec_agents.spec_tools import SpecToolset
class TenantToolset(SpecToolset):
def call_seeds(self, *, ctx, tool_name):
tenant = ctx.deps.tenant
return {"tenant": lambda: tenant}
See per-call seeds and the run's context.
Rendering and paging¶
render_output(spec, result, *, ctx, projection)takes the kernel'sDispatchResultand defaults to the kernel'spresent_for_audience. Themany,request,viewandextrasarguments are gone; an override that wants the unprojected payload calls the kernel'spresent(spec, result).output_extrasis gone, and with it thepage,resultandinstancekeys an output-context provider was handed. The kernel presents a result row by row, so a provider that batched lookups over the rendered page has no page to batch over.shape_page(rows, *, ctx, page, limit, max_page_size)returns the kernel'sOutputPage, fromdjango_service_specs. A subclass of another library's page type subclasses that one.is_tool_listed(name, ctx)is unchanged: the seam for a per-run catalog, such as hiding a staff-only tool from a non-staff run. Hiding a tool grants nothing: the spec's permissions are asked on every call, whatever it returns.
Other changes¶
json_schema_registry=is gone. A field's schema is whatever the spec compiles for it; describe it on the spec.- The default
idofSpecToolsetandSpecCapabilityisspec-tools, where it wasdrf-specs. Passid="drf-specs"to keep anything keyed on it. - A call is answered by the kernel's refusals only. Django REST Framework's
PermissionDeniedandValidationErrorraised inside a callable escape the call unchanged; raise the kernel'sNotPermittedandServiceValidationErrorinstead.
The full list is the changelog.