aid-runtime-python
The Python runtime for AID Framework: the behavior layer that
apps generated by the py-fastapi
target import. Generated code is thin glue; this is where the behavior lives.
Distribution name aid-runtime-python, import package aid_runtime. The two differ deliberately: a
distribution name is conventionally hyphenated, and a Python import package cannot be. That asymmetry is asserted
by the test suite so it cannot drift.
Install
pip install aid-runtime-python
Requires Python 3.11 or newer. Dependencies are httpx, pydantic, pydantic-ai-slim and
opentelemetry-api.
Two optional extras:
pip install "aid-runtime-python[pgvector]" # the pgvector store adapter
pip install "aid-runtime-python[deepagents]" # the deepagents agent engine, pinned
Quick start
from aid_runtime import (
Budget,
CompletionRequest,
CostProfile,
ModelGateway,
ModelSpec,
OpenAICompatibleProvider,
PinnedModel,
)
spec = ModelSpec(
name="gpt-4o-2024-08-06",
provider="openai-compatible",
cost=CostProfile(input_per_1k_usd=0.0025, output_per_1k_usd=0.01),
context_window=128_000,
max_output_tokens=4_096,
)
gateway = ModelGateway(
providers={"openai-compatible": OpenAICompatibleProvider(api_key=...)},
aliases={"small": PinnedModel(alias="small", spec=spec)},
budget=Budget(max_usd=5.0, scope="nightly"),
)
result = await gateway.generate("small", CompletionRequest.prompt("hello"))
result.text, result.usage, result.cost_usd, result.alias, result.attempts, result.fell_back
What it provides
| Module | Provides |
|---|---|
gateway.py |
ModelGateway — alias resolution, capability negotiation, ordered fallback, cost accounting, GenAI spans |
providers/ |
The Provider protocol, one HTTP adapter (OpenAICompatibleProvider), one deterministic fake (FakeProvider) |
decisions.py |
DecisionClient — TypeSafe AI (Jev) typed decisions: choice, score, and noul answers |
prompts.py |
PromptTemplate / PromptRegistry — versioned prompts, typed rendering, few-shot assembly |
structured.py |
build_agent — structured output via Pydantic AI only |
tools.py |
Tool / ToolExecutor — declared definitions, argument validation, structured results |
pipeline.py |
Pipeline / PipelineContext — plain-async named steps over a frozen context |
agents/ |
AgentRuntime — the loop, transfer vs. delegate, step/token/cost budgets with bounded degrade, memory, checkpointed human-in-the-loop |
retrieval/ |
Connectors, chunkers, the Embedder / Reranker / VectorStore protocols, hybrid fusion, citations, retrieval metrics |
evals/ |
Datasets, metrics, and the statistical baseline gate |
tracing.py |
OTel GenAI spans: llm_span, agent_span, record_usage, record_cost, record_attributes, record_hitl, record_exception |
cost.py |
Budget, CostLedger, estimate_cost_usd, estimate_tokens |
types.py |
The request/response/usage/spec models everything else is typed against |
249 names are exported. The layering is acyclic and one-directional: errors and types import nothing else in
the package, gateway is imported only by the package __init__ and the eval runner, and no core module imports
a higher layer back.
Model pinning
An alias resolves to an exact provider model id, including a pinned version. That is not cosmetic: a baseline
recorded against gpt-4o means nothing if the alias floats to gpt-4o-2026-… on the next run, so an unknown
alias is an error (UnknownModelAliasError) rather than a silent fall-through to a provider default.
resolve(alias)returns thePinnedModel(alias +spec);provider_for(alias)its provider.require(alias, capabilities)asserts the capabilities and returns the pinned model, checking both sides — a model that supports tool calling behind an adapter that does not serialise tool definitions cannot tool-call — and raisesCapabilityMismatchErrornaming what is missing.generate(alias, request, *, fallbacks=(), budget=None)tries the alias then each fallback in the order given, and reports the aliases actually attempted. Fallbacks are ordered, not conditional.- Every charged call opens a span and updates the ledger.
CostLedgertracks per-callLedgerEntryrecords and yields aSpendSummary;Budget(max_usd,max_total_tokens,scope) is enforced before the call, andestimate_usage/estimate_cost_usdprice a request from a model'sCostProfilewhen a caller wants to check before spending.
Decisions are not chat
DecisionClient calls TypeSafe AI's Jev (POST /v1/systemone), which takes a state plus typed questions and
returns typed answers instead of prose. It is deliberately not in providers/: every provider there maps messages
to text, and Jev has no text surface, so a pipeline cannot select it as the model that writes an answer.
from aid_runtime import DecisionClient, DecisionQuestion
async with DecisionClient() as client:
response = await client.decide(
model="jev-1.14.0",
state={"message": message},
questions={
"triage": DecisionQuestion(
type="choice",
instructions="Which team should handle this",
criteria=("billing", "technical"),
)
},
)
response.answers["triage"].choice
The key is TYPESAFE_API_KEY (Vercel's spelling, TYPESAFE_AI_API_KEY, is accepted too); TYPESAFE_BASE_URL
overrides the host. Two rules matter more than they look: a noul answer carries noul and no confidence
field at all, so a branch on confidence must refuse it rather than default to zero; and
decide(..., reproducible=True) refuses the moving aliases jev-latest and jev-preview, which is what eval
scoring needs. Only 429/529 are retried, honouring retry-after.
The eval gate measures statistics, not equality
A model is expected to vary between runs, so a metric that compares two runs for equality is either always red or measuring nothing. Non-determinism is not a bug to be fixed here; it is the property the gate is built to be robust against.
Dataset— versioned cases with inputs, expectations, tags and metadata;from_records/from_json/from_path,to_json, and lookups by id (case) and by tag (by_tag).- Metrics —
ExactMatch,NormalizedContains,JsonSchemaValid, andLlmJudge.metric_namesanddefault_metricscover the common set. run_evaluation(dataset, runner, metrics, runs=5)samples the runner N times and produces anEvalRunof per-run metric means, so a single unlucky run does not decide the verdict.evaluate_onceis the single-pass primitive; aDatasetRungives per-case outcomes and a renderedsummary().EvalBaseline.from_run(run, model=..., note=...)stores a baseline as JSON: per metric the sample count, mean and spread. A committed example ships atruntimes/python/tests/fixtures/capitals-baseline.json.BaselineGate(thresholds).evaluate(run, baseline)compares the current sample against the baseline using a one-sided normal test. EachThresholdcarries ametric, an optional absolutemin_mean, amax_regressionallowance, and analphasignificance level. A metric passes by clearingmin_mean, by being withinmax_regressionof the baseline, or by regressing only within sampling noise.
Two deliberate limits:
LlmJudgeis implemented and never CI-required. A judge is itself a model call, and a gate that depends on one non-deterministic call inside the gate you are trying to trust is the problem this design avoids.gateway_judge(gateway, alias)builds the judge call when a caller wants it.GateConfigurationErrorrefuses a threshold on a model-derived metric, because an N-run sample of a probabilistic judge cannot carry a fixed threshold honestly.
cost is the enforced budget. A latency p95 budget belongs with dashboards, not here.
Retrieval
retrieval/ is an IR-driven layer: binding_from_spec(retriever, embedding=..., store=...) folds the IR's
retrievers, embeddings and stores entries into one RetrieverBinding, which a Retriever then ingests and
searches through. The runtime consumes the names the IR emits, field for field.
- Three protocols, in the same style as
providers.Provider:Embedder,Reranker,VectorStore. FakeDeterministicEmbeddermaps text to a stable unit vector with a hash — keyless and offline, which is what lets a retrieval gate run in CI.OpenAICompatibleEmbedderis the one hosted adapter: a single POST to/embeddingsoverhttpx, with the retry, timeout and error-chaining behavior of the HTTP provider.LexicalRerankeris the only reranker. It is deterministic and lexical; there is no hosted rerank adapter.- Chunk ids are content hashes, so re-ingesting the same input produces the same ids and a no-op diff. Ingest is held to idempotence rather than to the byte-identical codegen guarantee.
- Pinning.
embed_modelnames an IRembeddingsentry and resolves at codegen time to an exactmodel_idplus its recorded width; the resolved id, width and storekindtravel into the generated code. An alias is never resolved at runtime, because a baseline recorded against a floating model means nothing. - Cost. Embedding calls charge the existing
CostLedgerthrough the existingBudget, using the catalog'sCostProfilefor the pinned model. There is no retrieval-specific budgeting machinery.
RetrievalMetric (RecallAtK, MeanReciprocalRank, CitationGrounding) is deliberately a separate protocol
from the shipped Metric, which scores a single string prediction. Retrieval scores a retrieved set:
recall@k and MRR need the ranked ids, and citation grounding needs the passages behind the citations. Retrieval is
therefore sampled through RetrievalGate.sample(...), producing a standard EvalRun, and then judged by the
unchanged BaselineGate. Retrieval metrics are deterministic, so they pass the gate's own eligibility check.
pgvector
PgVectorStore is a thin adapter over psycopg and the vector extension.
- It is locally provisioned and not covered by CI. No CI job starts a database; the in-memory adapter carries
the store tests, and both adapters share the same
VectorStoreprotocol andsimilaritysemantics.compose.pgvector.ymlin the repository brings up a database for local use. - The
[pgvector]extra is import-safe when absent: importingaid_runtimeoraid_runtime.retrievalnever imports the driver, and loading the adapter raises aConfigurationErrornaming the extra.
Control flow is plain async
Pipeline is a list of named async (or plain) steps over a PipelineContext that is frozen, so a step cannot
mutate what an earlier step observed — it returns a new context, which keeps a failed run's state inspectable. A
step may return a context, a mapping, or None. Each step runs in its own span, a step raising anything aborts
the run with the original exception chained (PipelineError.__cause__), and StopPipeline is the supported early
exit.
Pydantic AI is used for structured output and nothing else (structured.build_agent). The orchestration
library stays out of the control-flow path deliberately, so it remains swappable, and no graph or orchestration
dependency is taken at all: the agent loop in agents/ is this package's own.
Tracing follows the OTel GenAI semantic conventions: gen_ai.* attributes for the operation, model, provider,
request parameters and usage, with AID-specific values namespaced aid.* so the two never collide. A failing call
records its exception exactly once.
The agent runtime
AgentRuntime.from_ir(document, providers=..., ...) reads the emitted IR and runs an agent to a stop condition:
model call, tool calls, observations, until a terminal answer, max_steps, or a budget breach. Model calls go
through ModelGateway, so fallback, cost accounting and tracing apply without a second implementation.
Transfer and delegation are different, and the API says which. _transfer (handoffs) hands the conversation
to the target and never resumes the caller, so RunResult.agent_id is the target and path grows. _delegate
(delegates) runs the target in an isolated [system, user] context and returns its answer as a single tool
observation, so RunResult.agent_id is the caller and path is unchanged. Both refuse to re-enter an agent
already on the path.
A budget breach fails hard. BudgetExceededError stops the run; the gateway preflight and the ledger both
enforce the ceiling. An agent declaring on_budget_exhausted: "degrade" (spec v0.2) instead steps exactly one rung
down its declared alias ladder — no retry loop, and the build-time gate rejects the declaration when no strictly
cheaper alias exists. Each degraded step is recorded in StepRecord.degraded_from, and a checkpoint carries its
current alias so a resumed run re-routes from where it stopped.
Memory is in-process plus one store-backed adapter. thread scope is the checkpointer unit: trimmed to its
token_budget, never written to a store. user and global scopes read and write the store and render as one
extra system message, so injection is visible in the request rather than hidden.
Human-in-the-loop pauses by returning a checkpoint (completed steps, pending call, current alias), and
resume(checkpoint, decision) continues it. The pause is recorded as an aid.hitl span event, not an attribute,
because a run can pause more than once.
Limits, stated plainly
These are refused explicitly rather than approximated — each raises FeatureDeferredError or a
ConfigurationError naming the reason:
ModelGateway.stream— the transport is request/response only, and no provider advertisesCapability.STREAMING.- Retrieval: non-file sources,
semanticchunking,incrementalrefresh, retrieverfilters, a hosted reranker, and theqdrant/pinecone/weaviate/customstore kinds. - Escalation routing and third-party orchestration frameworks.
Development
From a checkout of the repository:
pip install -e ".[dev]"
pytest
ruff check .
ruff format --check .
mypy
mypy runs strict over src and tests, so the tests are type-checked too.
Non-responsibility: no app-specific prompts, tools, or rubrics. Those belong to the generated app's
business/ tier — the seam this runtime is called from.
See also
@aid-framework/runtime— the same runtime in TypeScript, module for module@aid-framework/generator-py-fastapi— the generator that emits apps against this runtime
License
Apache-2.0. Code generated by AID Framework is not subject to this license.
Metadata
Release files for aid-runtime-python 0.1.95
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| aid_runtime_python-0.1.95.tar.gz | 284.8 kB | With provenance | Details for aid_runtime_python-0.1.95.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| aid_runtime_python-0.1.95-py3-none-any.whl | Python 3 | none | any | Details for aid_runtime_python-0.1.95-py3-none-any.whl |
Total release size: 480.7 kB
Release files / aid_runtime_python-0.1.95.tar.gz
| Download URL | aid_runtime_python-0.1.95.tar.gz |
|---|---|
| Size | 284.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1d11af2051932baee4ac1307470db92b2726ebf632bf16fdfa235483c1b444a5
|
|
BLAKE2b-256 checksum How to use checksums |
5595bbad72caeb5ab9f66d822ee9de1839077dd567e192d1cbf87fac2de2357e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / aid_runtime_python-0.1.95-py3-none-any.whl
| Download URL | aid_runtime_python-0.1.95-py3-none-any.whl |
|---|---|
| Size | 195.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b6631a282ef714c08a096b24eee569881c8851f25f5380704f31cd42faf8cea8
|
|
BLAKE2b-256 checksum How to use checksums |
2171efdfd8ba683ef12bbd5cc23ecda0aeaf2e4955e454e67e8ac4ec9cdb03ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for aid-runtime-python 0.1.95
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for aid_runtime_python-0.1.95.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.