The agent spec¶
The most load-bearing type in the platform: the Builder edits it, the database versions it, the factory instantiates it, and clients export it to their own git repositories as YAML.
Generated from the source, because the reasoning lives in the docstrings.
app.agents.spec.AgentSpec
¶
Bases: BaseModel
Everything that defines an agent's behaviour.
Deliberately excluded: anything about where the agent runs (surfaces, channels) and anything about who may use it (owner, sharing). Those are deployment and access facts; keeping them out means the same spec can be exported, reviewed and reused across organizations.
to_yaml
¶
Render the spec for a client's git repository.
Keys keep spec order rather than being sorted, so a diff reflects what changed rather than where it happens to sort. UUIDs become strings so the file round-trips through any YAML reader.
from_yaml
classmethod
¶
Parse a spec written or edited by hand.
Raises:
| Type | Description |
|---|---|
ValueError
|
If the document is not a mapping. Pydantic reports field problems itself, but a list or a bare string reaches it as an unhelpful type error. |
Delegation¶
Two shapes, and Concepts explains
which to reach for. subagents holds the first; the second lives in the
subagents capability's own config.
app.agents.spec.SubagentRef
¶
Bases: BaseModel
One published agent this agent may delegate to, pinned to a version.
Two ids rather than one, and the second is the whole point. A delegate is a real agent - versioned, permission-checked at publish, with its own capabilities, its own model and its own collections - and a reference that named only the agent would let its behaviour change under a published parent with nothing recording that anything had changed. Pinning means a fix to a delegate reaches its callers when somebody says so, which is the same guarantee publishing gives everywhere else in this product.
The cost of pinning is real and is paid in the Builder: a parent whose delegate has moved on is stale, and staleness that nothing surfaces is a bug frozen in place forever. The draft compares each pin against the delegate's current version and offers to move it.
A pin whose version no longer exists fails the run, loudly, naming the delegate. Never a quiet fall back to the current version: the reason to pin is that nothing changes without a decision, and a silent upgrade is worse than a refusal because nobody finds out.
app.agents.spec.SpecialistSpec
¶
Bases: BaseModel
A specialist defined inside another agent, rather than published.
Worth having, and worth being honest about. A "summarise this in three bullets" specialist should not require somebody to publish an agent, and this is that: a name, a description the parent's model reads before delegating, instructions, and - because a summariser that cannot read the collection is useless - its own capabilities, collections and skills.
Which makes it an agent in every way except one, and the exception is the
important one: a specialist is not versioned. It has no version row, it
cannot be pinned, nothing else can reference it, and editing the parent
changes it. That is the difference between this and :class:SubagentRef, and
it is why the Builder must present them as two different things rather than
two tabs of one.
The risk this shape exists to contain is a second, parallel notion of
"agent" - one that publish validation does not walk and the permission model
cannot see. It is contained by refusing to write a second format: this is a
typed subset of :class:AgentSpec, using the same
:class:CapabilityBindingSpec, validated by the same recursive pass in
validate_spec, and assembled by the same build_agent. One spec type, one
validator, one builder, one Builder component, each used recursively. If any
of those five grows a second copy for specialists, the copy is the bug.
Deliberately absent, each because it only means something for a thing with a version or an owner:
budget- inside a delegation the parent's caps bind. Two budget guards metering one shared ledger would double-count every request.notificationsandobservability- a specialist is not the subject of an alert or a Logfire service; the run it happens inside is.mcp_server_ids- an MCP connection is organization-scoped configuration, and reaching one through a specialist nobody published is the wrong door. There is deliberately no route to one from here:share_with_delegateslends capability bindings, and an MCP connection is not a capability, so naming one there would configure nothing. A specialist that needs an external tool is a specialist that should be a published agent.subagents- a specialist does not delegate further. Nesting is whatmax_depthbounds, and it is bounded for published delegates, which are reviewable.
to_agent_spec
¶
This specialist as the spec the factory already knows how to build.
The one method that keeps "one spec type, one validator, one builder" true
rather than aspirational. A specialist is a subset of an agent, so the way
to build one is to say which agent it is and hand it to build_agent -
not to write a second assembly path that will drift from the first the
moment a field is added to either.
fallback_model_profile_id is the parent's, used when the specialist
names none: a specialist with no model of its own runs on the model of the
agent that called it, which is both the least surprising answer and the
only one that works when the parent is the only agent whose profile the
author chose.
The fields this drops are the ones :class:SpecialistSpec deliberately
does not have - budget, notifications, observability, MCP connections,
subagents - so they arrive at their AgentSpec defaults: no cap of its
own (the run's caps bind), no alerts of its own, no Logfire project of its
own, no connections, and no delegating further.
Budgets¶
app.agents.spec.BudgetSpec
¶
Bases: BaseModel
Spending limit for this agent.
One monthly cap, metered against this agent's own runs. The platform has exactly two budget levels - the agent's and the organization's - and this is the agent's half.
Alerts¶
app.agents.spec.NotificationSpec
¶
Bases: BaseModel
Which of this agent's alerts are sent, and who hears each one.
Per agent because the alerts are about an agent. A deployment-wide switch made the noisy agent and the one nobody may miss the same setting, so the only way to quieten the first was to go deaf to the second.
What is not here: the organization's own monthly cap. That limit is not this agent's to describe - it stops every agent in the organization, its ceiling is set in the organization's settings, and an agent's author cannot raise it. Its alert goes to the organization's admins and is not configurable from a spec.
app.agents.spec.AlertSpec
¶
Bases: BaseModel
Whether one kind of alert is sent for this agent, and to whom.
Every audience is resolved and the addresses are merged, so naming the same
person twice mails them once. Each recipient's own
/settings/notifications switch is applied last and can only ever remove
them: an agent cannot conscript somebody into an inbox they opted out of.
app.agents.spec.AlertAudience
¶
Bases: StrEnum
Who an agent's alert reaches.
A list of these rather than one value, because the real answers are unions: "the person who asked, and the admins" is the sensible default for an approval and cannot be said with a single choice.
Deliberately roles rather than addresses, with CHOSEN as the one escape
hatch. An audience of user ids goes stale the moment somebody leaves, and a
spec is exported to a client's repository - ADMINS still means the right
people after a reorganisation, and it means them in whichever organization
the spec is imported into.
ADMINS
class-attribute
instance-attribute
¶
The organization's owners and admins, plus the deployment's app admins.
OWNER
class-attribute
instance-attribute
¶
Whoever owns the agent - the person who would fix its configuration.
INITIATOR
class-attribute
instance-attribute
¶
Whoever started the run. Nobody, for a run no person began.
CHOSEN
class-attribute
instance-attribute
¶
Exactly the members named in user_ids.
Model settings¶
app.agents.spec.ModelSettingsSpec
¶
Bases: BaseModel
How this agent asks its model to behave.
A deliberately small window onto Pydantic AI's ModelSettings. The full
set includes escape hatches for someone debugging a provider - raw bodies,
raw headers, token biases - which in a Builder are an invitation to paste
something that breaks a published agent, and knobs that only some providers
implement, which quietly mean something else after a model swap.
_MODEL_SETTINGS_WITHDRAWN says why each excluded key went. What is left
is what an agent author reaches for: how varied the answer is, how long it
may be, how long it may take, and whether tools may run at once.
Reasoning is not here. It is the thinking capability, and a second
control writing the same provider parameter would disagree with it silently.
Every field is optional and unset means unset, which is the one property
the rest of this model is arranged around. None is not "send the
provider's default" - it is "do not send this parameter", and the difference
is a run that fails: reasoning models reject temperature outright, so an
agent that never chose one must produce a request with no temperature key
at all. Hence the serializer: an unset field is absent from the stored
spec rather than stored as null, so nothing downstream - the merge in
app/agents/factory.py, a YAML export, the Builder deciding whether to
show a field as touched - has to know that a null here means "no".
Observability¶
app.agents.spec.ObservabilitySpec
¶
Bases: BaseModel
Where this agent's traces go.
Per agent rather than per deployment because the interesting case is a client's agent whose traces belong in the client's own Logfire project, not in the operator's. The deployment-wide configuration stays as it is and keeps receiving everything else; this only redirects the runs of the agent that asks for it.
The token is a reference, never a value - like every other credential a spec names. A spec is exported as YAML into somebody's repository, and a write token in a checked-in file is a token that has to be rotated.
organization and project are the other half of that redirection, and they
are here rather than in deployment settings for the same reason the token is:
a token is a write credential and names neither, so a deployment knows
where it sends an agent's traces and not where to read them. Both are slugs a
client tells us; nothing can derive them. With neither set the run's trace id
is still recorded and no link is offered - which is a configuration fact
rather than a promise the schema is failing to keep (#206).