20. Agents
In plain words: Design rule 20: Agents
Defines an Agent as one typed thinking step with fixed instructions and output contract, while the model, settings, tools and permissions are attached fresh for each step. Built on Pydantic AI.
- 1An Agent is not a model, workflow or persona; its recipe and output contract are separately inspectable.
- 2Tools and delegation never gain authority from prompt text or model output.
- 3The model and permissions are bound late, per step, through the Traffic Controller (Dispatcher).
Available The Pydantic AI adapter works; shared usage limits and binary media are not yet.
Words on this page, decoded click one for its card
Context
A model call is not an Agent. An Agent is one typed cognitive step whose instructions and result contract remain stable while the selected model, generation settings, tools, and authority arrive for the particular step. This permits the VesselEngine Room to change intelligence without relocating policy, identity, or durable execution into an adapter.
Requirements
- An Agent must be distinct from a model, GraphStep Map, PatternWorkflow, Persona, Posture, and Lens.
- Its stable recipe, available capability, fresh dependencies, output contract, and result limits must be inspectable and separately owned.
- Tools and delegation must not acquire authority through prompt text, construction, or a model result; durable truth remains at domain, GraphStep Map, identity, consent, and persistence boundaries.
Considered Options
| Option | Decision | Why |
|---|---|---|
| Put model, credentials, tools, and workflow in one prompt-defined actor | Rejected | It erases capability admission, tool policy, durability, and attribution boundaries. |
| Make Pydantic AI the dispatcher and workflow ledger | Rejected | It owns the inner model/tool loop, not LychD's control plane. |
| Typed spec with late-bound capability | Selected | It keeps cognitive construction stable and binds per-step authority at the DispatcherTraffic Controller seam. |
Decision
LychD uses in-process Pydantic AI, at lockfile-pinned pydantic-ai-slim==2.55.0. The
State owns present cognitive adapter evidence;
native Graph integration preserves LychD's
serial checkpoint and wait-owner contract.
The adapter sets end_strategy="early" explicitly: a valid final output does not execute sibling
function tools. The v2 SDK's graceful default would change this effect boundary. Any later
workflow that needs sibling completion must declare and verify that policy separately.
An Agent joins immutable AgentSpec, process-local AgentForge, a step-scoped model-shaped
CapabilityGrant, fresh LychDDeps, and a declared output union. This is the delivered model path;
ADR 22's general grant union does not turn every service into an Agent dependency. The boundaries
are deliberate:
| Concern | Owner |
|---|---|
| Instructions, output types, retry ceiling, optional declared toolsets | AgentSpec |
| Construction and process-local cache | AgentForge |
| Model, settings, runtime toolsets, capability lease | Dispatcher and CapabilityGrant |
| Workflow order, persistence, suspension, recovery | Graph |
| Permission to act | Security, SigilID Badge, grant, and tool handler |
| Durable identity and attribution | Mirror |
| Consequential human authority | HitL |
Pydantic AI owns the inner loop, never a second dispatcher, identity system, ledger, or control plane.
The Agent specification
AgentSpec is frozen and hashable, with name, instructions_key, instructions,
output_types, optional toolset_names, writes, retries, and default max_tokens.
AgentForge.agent_for(spec) selects a registered builder and caches by the whole specification;
changing any field makes a distinct entry, while an unknown name or toolset fails construction.
build_agent binds no model. It can bind named construction-time toolsets, but omits a mutating
set when writes is false. Absence is the first gate; a present tool still checks live SigilID Badge,
grant, input, and domain policy at the handler.
The First One
The delivered BridgeChat Agent, THE_FIRST_ONE_SPEC, returns BridgeReply | DeferredToolRequests,
has no construction-time toolsets, and cannot request transition of the substrate leased to it.
Its stable instructions receive the ContextBriefing Pack OrchestratorStage Manager's floor at run time. BridgeReply is text
plus FragmentCalls: each names a Vessel-owned registry entry and parameters, never markup, and
the registry validates it before projection. Bottleneck represents contradiction, missing input,
policy block, or unavailable dependency, but while BridgeChat can settle it as workflow state, it is
not in The First One's current output union.
Late-bound capability
For each BridgeChat inference step, the current workflow asks the DispatcherTraffic Controller for a v1 chat grant
containing capability specification and state, step/run lease, resolved generation profile, a
required hydrated Pydantic AI model, and agent-loop toolsets only when the capability explicitly
declares supports_tools = true. The grant exposes neither its AnimatorModel Runtime nor Connector. Bridge
passes grant.model through a request-scoped Context capacity wrapper,
along with grant.model_settings() and grant.toolsets, to agent.run_stream_events.
Bridge owns the event stream's async scope so toolsets close on consumer failure before the
DispatcherTraffic Controller closes the model-provider scope and releases its lease.
Agent specifications therefore name neither endpoint nor credential;
the specification needs no separate model/tool-provider pair.
One spec can run with any admitted model meeting the adapter contract. A foreign framework is not an in-process Agent by analogy: it remains a typed delegated runtime or AnimatorModel Runtime boundary until a later amendment to the owning CovenantDesign Rule (ADR) admits a versioned interface.
Delegated runtimes and remote attempts
A coding agent is a delegated runtime when an external process owns an iterative planner/tool
loop and receives an exact repository or workspace projection under an AgentJob read,
candidate, or verify profile. One native Agent step using a remote model is instead a Portal-
backed Agent call; a sovereign node accepting a public task is A2AAgent-to-Agent Line. These classifications follow
labor and authority, not the model brand. A coding agent may obtain cognition through a local
capability or its job-scoped Provider Gate, but every child remote payload remains a separate
Security-admitted PortalCloud Model attempt. The outer job never conveys provider credentials, a general
network route, authoritative checkout, or promotion authority.
The accepted remote-attempt law also applies inside a native Agent loop: validation repair, tool-result follow- up, changed history, or another model round creates another canonical payload and therefore another PortalCloud Model road decision/EgressDecision. One grant or first-round approval never authorizes the whole loop's future payloads.
At that boundary the trusted PortalCloud Model adapter or job-scoped Provider Gate freezes the canonical child body and presents it to SpellweaverWorkflow Engine's road-decision service under the pinned parent placement policy. It must obtain a fresh road decision, a consumer-specific Cut when transformation is required, and the byte-time EgressDecision before sending. A native model loop or CoffinSandbox runtime cannot mint, cache, widen, or reuse them; inability to obtain the full chain refuses that round.
Provider compatibility under the pinned adapter
Provider PortalsCloud Models route their declared alias through the matching Pydantic AI provider or
model-profile resolver, including unsupported-setting filtering. OpenRouter, LiteLLM, and Ollama
use their provider resolvers; Google's OpenAI-shaped endpoint uses the Google model profile over
the OpenAI transport. Generic OpenAI-compatible endpoints and local runtimes instead select
LychD's conservative inline-schema, non-strict-tool profile. That local profile replaces resolved
provider defaults: a local alias such as o3 does not inherit OpenAI reasoning policy or a
provider-derived context window. Selecting Responses does not also admit its optional
input-token-counting endpoint: the generic local profile disables that endpoint. Named provider
resolution remains on the provider's model_profile(model_name) contract, separate from v2's
resolved-profile transformation callback.
OpenRouter ids must retain their
provider/model namespace. Under the pinned adapter, Google, LiteLLM, and Ollama aliases are
Chat-only; Responses is admitted only for OpenAI, OpenRouter, or an explicitly compatible generic
endpoint. This does not claim a native Gemini transport or providers outside the registered
aliases.
An explicitly admitted qwen3.8-non-thinking generation policy reaches the call through the
immutable capability grant. The grant requires its hydrated Chat model and compiles only
extra_body.chat_template_kwargs.enable_thinking = false; BridgeChat uses the existing settings
handoff. It does not translate this dialect into generic SDK thinking or reasoning_effort,
mutate model defaults, expose arbitrary request-body fields or create another provider route.
The policy bounds a declared request mode, not separate hard reasoning/final token quotas, semantic
quality or an entire Agent loop's budget. Any applicable byte-time egress decision still covers
the final child payload.
Run dependencies
Every step receives fresh, frozen LychDDeps:
| Field | Contract |
|---|---|
sigil |
Acting identity and scopes. |
grant |
Capability grant for this step. |
dispatcher |
Narrow grant-service port for tools. |
orchestrator |
Narrow lifecycle-transition port. |
context |
Context OrchestratorStage Manager for the assembled floor. |
run_id, step_id |
Correlation identities. |
priority |
Admitted run priority. |
It carries no database session, raw settings, or secret. The control plane consumes credentials to construct connectors; persistence and domain services remain narrow ports rather than cognitive smuggling.
Typed results and truth
Pydantic validation establishes a typed result's shape. Its prose can still be false, and its
type establishes neither an effect's validity, claim identity, nor permission to publish or
promote. Those proofs remain
with deterministic validators, domain handlers, SigilID Badge, grant, receipts, evaluation, HitLApproval Step, and the
PhylacteryMemory Vault. AgentSpec.retries bounds validation repair; tool arguments may use Pydantic AI
retries. Policy refusal and missing premises settle explicitly instead of demanding success
without bound.
Semantic answer completion
When the semantic source-answer casting generates an answer, it classifies the final SDK response
before ordinary answer settlement. A known normalized length finish is an answer_output_limit refusal, even when the
SDK returns a nonempty string and marks the received response complete. It must not assign that
partial result as the normal answer or new settled message history. Already-streamed text remains
provisional and may remain in RunJob Record evidence; refusal does not erase earlier observations.
This check follows the existing failure path after the admitted call, without SDK repair,
automatic retry, a larger generation budget, or another capability selection. Inspect the last
response's completion metadata; an earlier response's limit is not the final response's outcome.
Absent or other finish metadata retains current adapter compatibility and proves no natural stop.
A named qualification may require the stronger explicit stop criterion. RunDONE still records
successful GraphStep Map termination under its implemented checks, rather than factual truth, groundedness
or a universal output-completeness certificate. A successful current recall selecting no sources
may instead settle the deterministic, source-limited abstention owned by ADR27.
That branch has no Chat response or SDK history suffix to classify; it cannot fabricate provider
completion, usage, dispatch or token events. Other castings keep their own result contracts.
Mechanical cognitive postures
A Posture constrains an act through instructions, output schema, model settings, and tool grant. A durable revisioned Persona names the actor and may wear different Postures. A Lens supplies a Posture template to diversify Simulation's isolated ShadowWhat-if Simulator branch. Expansion, review, repair, and red-team work belong in separate runs when a proposal would otherwise grade itself. These distinctions are enforceable only where types, grants, settings, and GraphStep Map placement preserve the claimed separation.
Crucible uses explicitly biased Postures to pressure one decision from several declared angles. Every advocate receives the same pinned evidence and acceptance criteria, names its burden of proof and defeat condition, and runs separately from the Lead that synthesizes its claims. The posture label is neither a Persona nor authority, and a second-round rebuttal receives attributed opponent claims rather than hidden reasoning or an unbounded sibling transcript.
A local Privacy Agent is a Posture at an explicit SpellStep placement. It receives bounded raw material to produce a sanitized candidate and findings, but no PortalCloud Model tool or declassification authority; its confidence is not permission. Deterministic control comes first, policy can require an independent verifier, and Security's trusted PortalCloud Model Egress GatePrivacy Filter makes the exact decision.
Streaming, history, and limits
pump_agent_events streams one hop: text starts and deltas become raw Oculus token events, while
the final result supplies typed output and Pydantic AI history. The workflow serializes complete
history and the new suffix separately for settlement or suspension. Bridge reserves the grant's
output allowance and enforces the remainder as a per-request input-usage limit. Pre-request
counting requires a counting implementation. For the pinned OpenAI Responses counter, BridgeChat also
honors the resolved profile's endpoint support; method presence alone does not establish it.
Generic local Responses and current OpenAI Chat models therefore use reported response usage,
which cannot guarantee a preflight fence. Named Responses providers retain their own profile
policy, and other counting implementations retain their pre-request checks. ADR 21 owns the
separate character governor.
LychD does not yet share one UsageLimits object across a child-Agent tree. Future nested native
Agents need explicit budgets. Opaque delegates receive serialized limits through their own grant;
they inherit no RunContext, live toolsets, provider objects, or leases.
Consent suspension
The delivered deferred substrate allows one model round to return exactly one approval request and
no external deferred calls. It serializes suffix and call identifiers—not a live
DeferredToolRequests object—plus the capability key, durable toolset id and type, tool name,
project-owned effect id and revision, and prepared-definition digest. Resume reacquires a fresh
grant and refuses execution if that binding changed before supplying DeferredToolResults;
approval-required tools without an effect id and revision in the current prepared model round
cannot park; an earlier round's binding supplies no authority. The tool owner must revise
the effect revision whenever executable semantics change without changing the prepared definition.
The parked binding is checked in the resumed round before dispatch. After that round, a completed
one-shot tool may leave the prepared catalogue; this must not turn its executed effect into a
reported refusal. Any subsequent approval captures its own current binding.
Multiple approvals and generic CallDeferred labor fail truthfully because the record cannot
represent them. A policy stop preserves completed tool results and closes only the known refused
deferred requests as denied before appending its reply; it cannot erase earlier effects or label
uncertain work as unexecuted. No tracked production toolset currently originates an approval request; the
executable path is exercised by test-injected toolsets only. Delegated labor is a separate GraphStep Map
node with its own job and trust boundary; ADR 25 owns durable consent ordering and recovery.
Artifacts and multimodality
An Agent can return immutable ArtifactRef metadata, but metadata does not supply the payload.
LychD does not yet materialize authorized blobs as Pydantic AI binary content. Image, audio, and
other bytes need a Vessel-owned materializer that enforces authorization, media constraints,
and provenance.
Correspondence
The Agent is where the Call becomes bounded labor and the Blademind office gives its answer form. A model opens possibilities and a schema cuts a result; neither crowns itself true. The Lich's First Invocation owns the mythic correspondence.
Consequences
Positive
Definitions survive model and hardware change; outputs, dependencies, retries, and tools stay inspectable; the same Agent can use a deterministic offline model.
Negative
Schemas and tools need deliberate maintenance, adapter compliance is required, and suspension, recursive budgets, artifacts, and foreign delegation need explicit LychD machinery.
Verification
tests/agents/test_factory.py, test_the_first_one.py, test_bridge_chat_graph.py,
test_consent_resume.py, and test_state_serializable.py exercise construction, cache identity,
tool absence, typed output, grants, streaming, settlement, continuation, and the durable GraphStep Map
boundary. Dependency, message-schema, deferred-API, or stream changes require those tests and
State to move before this law claims new behavior.