aid-runtime-python 0.1.39__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. aid_runtime/__init__.py +500 -0
  2. aid_runtime/agents/__init__.py +94 -0
  3. aid_runtime/agents/chat_model.py +351 -0
  4. aid_runtime/agents/deep_agents.py +600 -0
  5. aid_runtime/agents/engine.py +102 -0
  6. aid_runtime/agents/guardrails.py +519 -0
  7. aid_runtime/agents/ir.py +622 -0
  8. aid_runtime/agents/loop.py +1308 -0
  9. aid_runtime/agents/memory.py +241 -0
  10. aid_runtime/agents/policy.py +163 -0
  11. aid_runtime/agents/state.py +182 -0
  12. aid_runtime/cache.py +185 -0
  13. aid_runtime/concurrency.py +70 -0
  14. aid_runtime/control.py +329 -0
  15. aid_runtime/cost.py +168 -0
  16. aid_runtime/errors.py +337 -0
  17. aid_runtime/evals/__init__.py +72 -0
  18. aid_runtime/evals/dataset.py +77 -0
  19. aid_runtime/evals/gate.py +430 -0
  20. aid_runtime/evals/metrics.py +335 -0
  21. aid_runtime/evals/predicate.py +211 -0
  22. aid_runtime/evals/runner.py +224 -0
  23. aid_runtime/gateway.py +473 -0
  24. aid_runtime/pipeline.py +168 -0
  25. aid_runtime/prompts.py +185 -0
  26. aid_runtime/providers/__init__.py +20 -0
  27. aid_runtime/providers/base.py +57 -0
  28. aid_runtime/providers/fake.py +256 -0
  29. aid_runtime/providers/http.py +344 -0
  30. aid_runtime/py.typed +0 -0
  31. aid_runtime/retrieval/__init__.py +178 -0
  32. aid_runtime/retrieval/chunking.py +222 -0
  33. aid_runtime/retrieval/citations.py +131 -0
  34. aid_runtime/retrieval/connectors.py +131 -0
  35. aid_runtime/retrieval/embedding.py +293 -0
  36. aid_runtime/retrieval/gate.py +120 -0
  37. aid_runtime/retrieval/hybrid.py +164 -0
  38. aid_runtime/retrieval/metrics.py +213 -0
  39. aid_runtime/retrieval/pgvector.py +258 -0
  40. aid_runtime/retrieval/rerank.py +133 -0
  41. aid_runtime/retrieval/retriever.py +335 -0
  42. aid_runtime/retrieval/stores.py +161 -0
  43. aid_runtime/retrieval/types.py +433 -0
  44. aid_runtime/steps.py +284 -0
  45. aid_runtime/structured.py +150 -0
  46. aid_runtime/tools.py +271 -0
  47. aid_runtime/tracing.py +241 -0
  48. aid_runtime/types.py +332 -0
  49. aid_runtime_python-0.1.39.dist-info/METADATA +268 -0
  50. aid_runtime_python-0.1.39.dist-info/RECORD +52 -0
  51. aid_runtime_python-0.1.39.dist-info/WHEEL +5 -0
  52. aid_runtime_python-0.1.39.dist-info/top_level.txt +1 -0
@@ -0,0 +1,335 @@
1
+ """Ingesting a corpus and retrieving from it.
2
+
3
+ This is the module a later layer's generated code calls: ``ingest`` once per
4
+ refresh, ``retrieve`` per query. It is deliberately not a command-line entry point
5
+ -- the generated module is the intended surface, and a runtime that also shipped a
6
+ ``__main__`` would grow a second, competing one.
7
+
8
+ Both halves are orchestration over injected seams. Nothing here chooses a model by
9
+ name, because the binding already carries the resolved model id, the vector width and
10
+ the store kind; resolving any of those at run time is exactly what the pinning rule
11
+ forbids.
12
+
13
+ Cost is charged to the existing ledger using the existing profile. The IR quotes
14
+ embedding prices per MTok and :class:`~aid_runtime.types.CostProfile` quotes per 1k
15
+ tokens; that conversion happens once, in
16
+ :func:`~aid_runtime.retrieval.types.pinned_embedding`, so no call site repeats it.
17
+
18
+ Candidates: the retriever fetches more than ``topK`` candidates from each ranking
19
+ before fusing, because fusion and reranking both reorder, and a fused list built from
20
+ only ``topK`` inputs has nothing to reorder. ``topK`` is applied to the *result*,
21
+ which is what the IR field means.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from collections.abc import Sequence
27
+
28
+ from pydantic import BaseModel, ConfigDict, Field
29
+
30
+ from aid_runtime.agents.guardrails import (
31
+ ACTION_BLOCK,
32
+ ACTION_ESCALATE,
33
+ ACTION_REDACT,
34
+ GuardrailEngine,
35
+ GuardrailRecord,
36
+ )
37
+ from aid_runtime.agents.ir import GuardrailDefinition
38
+ from aid_runtime.cost import Budget, CostLedger, LedgerEntry
39
+ from aid_runtime.errors import ConfigurationError, FeatureDeferredError, GuardrailBlockedError
40
+ from aid_runtime.retrieval.chunking import chunker_for
41
+ from aid_runtime.retrieval.citations import RetrievalResult, RetrievedItem, cite
42
+ from aid_runtime.retrieval.connectors import Connector, SourceDocument, open_connector
43
+ from aid_runtime.retrieval.embedding import Embedder, EmbeddingBatch, embedder_for
44
+ from aid_runtime.retrieval.hybrid import fuse, lexical_ranking
45
+ from aid_runtime.retrieval.rerank import Reranker, reranker_for
46
+ from aid_runtime.retrieval.stores import SearchHit, StoreRecord, VectorStore, create_store
47
+ from aid_runtime.retrieval.types import RefreshMode, RetrieverBinding, ScoredChunk
48
+ from aid_runtime.types import TokenUsage
49
+
50
+ __all__ = [
51
+ "DEFAULT_CANDIDATE_FACTOR",
52
+ "IngestReport",
53
+ "Retriever",
54
+ ]
55
+
56
+ #: How many candidates per ranking are fetched before fusion. Four is a round number
57
+ #: with a defensible property: it lets a passage ranked 4th lexically and 1st by
58
+ #: vector similarity reach the top of the fused list, which is the case fusion exists
59
+ #: to fix.
60
+ DEFAULT_CANDIDATE_FACTOR = 4
61
+
62
+ #: Texts per embedding request. Batching bounds one request's payload, which is what
63
+ #: matters; a fixed size keeps the request count a function of the corpus.
64
+ _BATCH_SIZE = 64
65
+
66
+
67
+ class IngestReport(BaseModel):
68
+ """What one ingest wrote.
69
+
70
+ ``chunk_ids`` is the load-bearing field for idempotence: it is sorted, so two
71
+ ingests of identical input compare equal without depending on the order the
72
+ filesystem happened to yield. ``cost_usd`` is what *this* ingest charged, not the
73
+ ledger's running total, so ingesting twice reads as two separate spends.
74
+ """
75
+
76
+ model_config = ConfigDict(frozen=True, extra="forbid")
77
+
78
+ retriever: str = Field(min_length=1)
79
+ documents: int = Field(ge=0)
80
+ chunks: int = Field(ge=0)
81
+ written: int = Field(ge=0)
82
+ tokens: int = Field(ge=0)
83
+ cost_usd: float = Field(ge=0.0)
84
+ chunk_ids: tuple[str, ...]
85
+
86
+ @property
87
+ def is_empty(self) -> bool:
88
+ return self.chunks == 0
89
+
90
+
91
+ class Retriever:
92
+ """A bound retriever: its source, its chunker, its embedder, its store.
93
+
94
+ Seams are injectable and default from the binding. Tests inject the fake
95
+ embedder and an in-memory store; generated code passes the real ones.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ binding: RetrieverBinding,
101
+ *,
102
+ embedder: Embedder | None = None,
103
+ store: VectorStore | None = None,
104
+ reranker: Reranker | None = None,
105
+ connector: Connector | None = None,
106
+ ledger: CostLedger | None = None,
107
+ budget: Budget | None = None,
108
+ base_dir: str | None = None,
109
+ candidate_factor: int = DEFAULT_CANDIDATE_FACTOR,
110
+ guardrails: Sequence[GuardrailDefinition] = (),
111
+ ) -> None:
112
+ if binding.refresh_mode is not RefreshMode.FULL:
113
+ raise FeatureDeferredError(
114
+ f"refresh mode {binding.refresh_mode.value!r} is declared but not implemented: "
115
+ "spec v0.1 reindexes the whole source, and incremental refresh needs change "
116
+ "detection the runtime does not have"
117
+ )
118
+ if candidate_factor < 1:
119
+ raise ConfigurationError("candidate_factor must be at least 1")
120
+ self._binding = binding
121
+ self._embedder = embedder or embedder_for(binding.embedding)
122
+ self._store = store or create_store(
123
+ binding.store_kind,
124
+ uri=binding.store_uri,
125
+ dimensions=binding.embedding.dimensions,
126
+ )
127
+ self._reranker = reranker or reranker_for(binding.rerank)
128
+ self._connector = connector
129
+ self._base_dir = base_dir
130
+ self._ledger = ledger
131
+ self._budget = budget
132
+ self._candidate_factor = candidate_factor
133
+ self._guardrails = tuple(guardrails)
134
+ self._guardrail_engine = GuardrailEngine()
135
+ self._closed = False
136
+
137
+ @property
138
+ def binding(self) -> RetrieverBinding:
139
+ return self._binding
140
+
141
+ @property
142
+ def store(self) -> VectorStore:
143
+ return self._store
144
+
145
+ async def ingest(self, documents: Sequence[SourceDocument] | None = None) -> IngestReport:
146
+ """Chunk a source, embed it, and write it.
147
+
148
+ Idempotent by construction: chunk ids are content hashes and the store
149
+ upserts on them, so ingesting the same input twice replaces rather than
150
+ duplicates. Re-ingesting is therefore a legitimate way to refresh a corpus.
151
+ """
152
+ source_documents = tuple(documents) if documents is not None else await self._read_source()
153
+ chunker = chunker_for(self._binding.chunking)
154
+ chunks = [chunk for document in source_documents for chunk in chunker.chunk(document)]
155
+
156
+ records: list[StoreRecord] = []
157
+ tokens = 0
158
+ cost = 0.0
159
+ for start in range(0, len(chunks), _BATCH_SIZE):
160
+ batch = chunks[start : start + _BATCH_SIZE]
161
+ embedded, batch_cost = await self._embed(tuple(chunk.text for chunk in batch))
162
+ tokens += embedded.usage.input_tokens
163
+ cost += batch_cost
164
+ records.extend(
165
+ StoreRecord(chunk=chunk, vector=vector)
166
+ for chunk, vector in zip(batch, embedded.vectors, strict=True)
167
+ )
168
+
169
+ written = await self._store.upsert(records)
170
+ return IngestReport(
171
+ retriever=self._binding.retriever_id,
172
+ documents=len(source_documents),
173
+ chunks=len(chunks),
174
+ written=written,
175
+ tokens=tokens,
176
+ cost_usd=cost,
177
+ chunk_ids=tuple(sorted(chunk.chunk_id for chunk in chunks)),
178
+ )
179
+
180
+ async def retrieve(self, query: str, *, top_k: int | None = None) -> RetrievalResult:
181
+ """Search the store for ``query`` and return ranked, cited passages."""
182
+ wanted = top_k if top_k is not None else self._binding.top_k
183
+ if wanted < 1:
184
+ raise ConfigurationError("top_k must be at least 1")
185
+
186
+ batch, _ = await self._embed((query,))
187
+ vector = batch.vectors[0]
188
+ candidates = wanted * self._candidate_factor
189
+ hits = await self._store.search(vector, top_k=candidates, metric=self._binding.index.metric)
190
+ ranked = await self._rank(query, hits, candidates)
191
+ items = _to_items(ranked)
192
+ if self._reranker is not None:
193
+ top_n = self._binding.rerank.top_n if self._binding.rerank is not None else wanted
194
+ items = await self._reranker.rerank(query, items, top_n=top_n)
195
+ guarded, records = self._guard_retrieval(items[:wanted])
196
+ return RetrievalResult(
197
+ retriever=self._binding.retriever_id,
198
+ query=query,
199
+ items=guarded,
200
+ guardrails=records,
201
+ )
202
+
203
+ def _guard_retrieval(
204
+ self, items: Sequence[RetrievedItem]
205
+ ) -> tuple[tuple[RetrievedItem, ...], tuple[GuardrailRecord, ...]]:
206
+ """Run the ``retrieval`` stage over each returned passage.
207
+
208
+ Per item, not over the concatenation: a redaction has to rewrite the one
209
+ passage it matched, and a joined string cannot be split back into passages
210
+ without guessing where each ended.
211
+
212
+ A retriever has no channel to ask a person on, so ``escalate`` blocks here
213
+ instead of pausing. Turning it into a silent pass would be the exact failure
214
+ mode the stage exists to prevent, and the agent loop -- which does have a
215
+ channel -- is where an escalation can pause a run.
216
+ """
217
+ if not self._guardrails:
218
+ return tuple(items), ()
219
+ guarded: list[RetrievedItem] = []
220
+ records: list[GuardrailRecord] = []
221
+ for item in items:
222
+ verdict = self._guardrail_engine.check_retrieval(self._guardrails, (item.chunk.text,))
223
+ records.extend(verdict.records)
224
+ deciding = verdict.deciding
225
+ if deciding is None:
226
+ guarded.append(item)
227
+ continue
228
+ if deciding.action == ACTION_BLOCK:
229
+ raise GuardrailBlockedError(deciding.guardrail, "retrieval", deciding.message)
230
+ if deciding.action == ACTION_ESCALATE:
231
+ raise GuardrailBlockedError(
232
+ deciding.guardrail,
233
+ "retrieval",
234
+ f"{deciding.message} (a retriever has no human channel, so an "
235
+ "escalation here blocks rather than passing unchecked)",
236
+ )
237
+ if deciding.action == ACTION_REDACT:
238
+ item = item.model_copy(
239
+ update={"chunk": item.chunk.model_copy(update={"text": verdict.text})}
240
+ )
241
+ guarded.append(item)
242
+ return tuple(guarded), tuple(records)
243
+
244
+ async def aclose(self) -> None:
245
+ if self._closed:
246
+ return
247
+ self._closed = True
248
+ await self._store.aclose()
249
+ await self._embedder.aclose()
250
+ if self._connector is not None:
251
+ await self._connector.aclose()
252
+
253
+ async def _read_source(self) -> tuple[SourceDocument, ...]:
254
+ if self._connector is None:
255
+ self._connector = open_connector(self._binding.source, base_dir=self._base_dir)
256
+ return await self._connector.documents()
257
+
258
+ async def _embed(self, texts: Sequence[str]) -> tuple[EmbeddingBatch, float]:
259
+ """Embed a batch, charge it to the ledger, and report what it cost.
260
+
261
+ The cost is returned as well as charged so a caller can report what one
262
+ operation spent: the ledger's total is cumulative across every call the
263
+ process made, and reporting that as this ingest's cost would double-count.
264
+ """
265
+ batch = await self._embedder.embed(texts, model=self._binding.embedding)
266
+ usage = TokenUsage(
267
+ input_tokens=batch.usage.input_tokens,
268
+ output_tokens=batch.usage.output_tokens,
269
+ )
270
+ # Priced through CostProfile rather than a ModelSpec: a ModelSpec requires a
271
+ # context window and an output ceiling, and fabricating those to reuse a
272
+ # pricing helper would put invented numbers in a generated artifact.
273
+ cost = self._binding.embedding.cost.cost_usd(usage)
274
+ if self._ledger is not None:
275
+ self._ledger.charge(
276
+ self._budget,
277
+ LedgerEntry(
278
+ alias=self._binding.embed_model,
279
+ model=self._binding.embedding.model_id,
280
+ provider=self._binding.embedding.provider,
281
+ usage=usage,
282
+ cost_usd=cost,
283
+ ),
284
+ )
285
+ return batch, cost
286
+
287
+ async def _rank(
288
+ self,
289
+ query: str,
290
+ hits: Sequence[SearchHit],
291
+ candidates: int,
292
+ ) -> tuple[ScoredChunk, ...]:
293
+ """The candidate ranking: vector only, or fused with a lexical ranking.
294
+
295
+ Fusion is skipped when ``index.hybrid`` is false, and that is the whole
296
+ meaning of the flag -- not "use a different metric", but "do not consult the
297
+ lexical ranking at all".
298
+ """
299
+ if not self._binding.index.hybrid:
300
+ return tuple(ScoredChunk(chunk=hit.record.chunk, score=hit.score) for hit in hits)
301
+
302
+ corpus = await self._store.corpus()
303
+ by_id: dict[str, StoreRecord] = {record.chunk.chunk_id: record for record in corpus}
304
+ for hit in hits:
305
+ by_id.setdefault(hit.record.chunk.chunk_id, hit.record)
306
+ lexical = lexical_ranking(
307
+ [record.chunk.text for record in corpus],
308
+ [record.chunk.chunk_id for record in corpus],
309
+ query,
310
+ top_k=candidates,
311
+ )
312
+ fused = fuse(
313
+ [
314
+ [hit.record.chunk.chunk_id for hit in hits],
315
+ [entry.chunk_id for entry in lexical],
316
+ ]
317
+ )
318
+ return tuple(
319
+ ScoredChunk(chunk=by_id[entry.chunk_id].chunk, score=entry.score)
320
+ for entry in fused
321
+ if entry.chunk_id in by_id
322
+ )
323
+
324
+
325
+ def _to_items(ranked: Sequence[ScoredChunk]) -> tuple[RetrievedItem, ...]:
326
+ """Attach provenance and assign contiguous 1-based ranks."""
327
+ return tuple(
328
+ RetrievedItem(
329
+ chunk=scored.chunk,
330
+ score=scored.score,
331
+ rank=rank,
332
+ citation=cite(scored.chunk),
333
+ )
334
+ for rank, scored in enumerate(ranked, start=1)
335
+ )
@@ -0,0 +1,161 @@
1
+ """Where vectors live and how they are searched.
2
+
3
+ Two adapters behind one protocol: in-memory and pgvector. The in-memory store is not
4
+ a stand-in for a "real" one -- it is the adapter every gate runs against, so it is
5
+ the one whose behaviour is the contract. The pgvector adapter is covered by the same
6
+ expectations and by nothing else: it is locally provisioned, and no required gate
7
+ touches a database.
8
+
9
+ Similarity is always reported as *higher is better*, including for L2, where the raw
10
+ quantity is a distance. A store that inverted its sign for one metric would make
11
+ every caller responsible for remembering which metric it asked for, and fusion and
12
+ reranking downstream would silently rank backwards.
13
+
14
+ Lexical search is deliberately absent from the store. The store owns the vector
15
+ index; BM25 lives in :mod:`aid_runtime.retrieval.hybrid` and reads the corpus through
16
+ :meth:`VectorStore.corpus`. That keeps one lexical implementation instead of one per
17
+ backend, at the cost of fetching the corpus per query -- a Phase 1 simplification,
18
+ correct at test scale and worth revisiting when a corpus outgrows memory.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+ from collections.abc import Sequence
25
+ from typing import Protocol, runtime_checkable
26
+
27
+ from aid_runtime.errors import ConfigurationError, FeatureDeferredError
28
+ from aid_runtime.retrieval.types import DistanceMetric, SearchHit, StoreKind, StoreRecord
29
+
30
+ __all__ = [
31
+ "InMemoryVectorStore",
32
+ "SearchHit",
33
+ "StoreRecord",
34
+ "VectorStore",
35
+ "create_store",
36
+ "similarity",
37
+ ]
38
+
39
+
40
+ def similarity(
41
+ left: Sequence[float],
42
+ right: Sequence[float],
43
+ *,
44
+ metric: DistanceMetric,
45
+ ) -> float:
46
+ """Similarity of two equal-length vectors under ``metric``.
47
+
48
+ Always oriented so that a larger number means a better match.
49
+ """
50
+ if len(left) != len(right):
51
+ raise ConfigurationError(
52
+ f"cannot compare vectors of {len(left)} and {len(right)} dimensions; the query and "
53
+ "the stored vectors must share the embedding's pinned width"
54
+ )
55
+ dot = sum(a * b for a, b in zip(left, right, strict=True))
56
+ if metric is DistanceMetric.DOT:
57
+ return dot
58
+ if metric is DistanceMetric.L2:
59
+ return -math.sqrt(sum((a - b) ** 2 for a, b in zip(left, right, strict=True)))
60
+ left_norm = math.sqrt(sum(a * a for a in left))
61
+ right_norm = math.sqrt(sum(b * b for b in right))
62
+ if left_norm == 0.0 or right_norm == 0.0:
63
+ # A zero vector has no direction, so cosine similarity is undefined rather
64
+ # than zero; returning 0.0 keeps it last without inventing a ranking.
65
+ return 0.0
66
+ return dot / (left_norm * right_norm)
67
+
68
+
69
+ @runtime_checkable
70
+ class VectorStore(Protocol):
71
+ """A vector index the retriever can ingest into and search.
72
+
73
+ ``aclose`` must be idempotent, matching the other seams in this runtime.
74
+ """
75
+
76
+ @property
77
+ def kind(self) -> StoreKind: ...
78
+
79
+ async def upsert(self, records: Sequence[StoreRecord]) -> int:
80
+ """Write records, keyed by ``chunk_id``. Returns the number written."""
81
+ ...
82
+
83
+ async def search(
84
+ self, vector: Sequence[float], *, top_k: int, metric: DistanceMetric
85
+ ) -> tuple[SearchHit, ...]: ...
86
+
87
+ async def corpus(self) -> tuple[StoreRecord, ...]:
88
+ """Every stored record.
89
+
90
+ Exists for the lexical half of hybrid search, and for assertions about a
91
+ store's contents after re-ingest.
92
+ """
93
+ ...
94
+
95
+ async def aclose(self) -> None: ...
96
+
97
+
98
+ class InMemoryVectorStore:
99
+ """A dict keyed by ``chunk_id``.
100
+
101
+ The key choice is what makes ingest idempotent: re-ingesting identical input
102
+ recomputes identical ids and overwrites in place, so the store's contents are a
103
+ function of the input rather than of how many times it was ingested.
104
+ """
105
+
106
+ def __init__(self, *, kind: StoreKind = StoreKind.MEMORY) -> None:
107
+ self._kind = kind
108
+ self._records: dict[str, StoreRecord] = {}
109
+ self._closed = False
110
+
111
+ @property
112
+ def kind(self) -> StoreKind:
113
+ return self._kind
114
+
115
+ async def upsert(self, records: Sequence[StoreRecord]) -> int:
116
+ for record in records:
117
+ self._records[record.chunk.chunk_id] = record
118
+ return len(records)
119
+
120
+ async def search(
121
+ self, vector: Sequence[float], *, top_k: int, metric: DistanceMetric
122
+ ) -> tuple[SearchHit, ...]:
123
+ if top_k < 1:
124
+ raise ConfigurationError("top_k must be at least 1")
125
+ hits = [
126
+ SearchHit(record=record, score=similarity(vector, record.vector, metric=metric))
127
+ for record in self._records.values()
128
+ ]
129
+ # Ties break on chunk id, never on insertion order: two runs that ingest the
130
+ # same corpus in a different order must produce the same ranking.
131
+ hits.sort(key=lambda hit: (-hit.score, hit.record.chunk.chunk_id))
132
+ return tuple(hits[:top_k])
133
+
134
+ async def corpus(self) -> tuple[StoreRecord, ...]:
135
+ return tuple(self._records[chunk_id] for chunk_id in sorted(self._records))
136
+
137
+ async def aclose(self) -> None:
138
+ self._closed = True
139
+
140
+
141
+ def create_store(
142
+ kind: StoreKind,
143
+ *,
144
+ uri: str | None = None,
145
+ dimensions: int | None = None,
146
+ ) -> VectorStore:
147
+ """Select a store adapter.
148
+
149
+ Imported lazily so that importing this package never pulls in a database driver:
150
+ the driver is an optional extra, and CI installs only the dev extra.
151
+ """
152
+ if kind is StoreKind.MEMORY:
153
+ return InMemoryVectorStore()
154
+ if kind is StoreKind.PGVECTOR:
155
+ from aid_runtime.retrieval.pgvector import PgVectorStore
156
+
157
+ return PgVectorStore(uri=uri, dimensions=dimensions)
158
+ raise FeatureDeferredError(
159
+ f"vector store kind {kind.value!r} is declared in the IR but has no adapter in this "
160
+ "runtime; Phase 1 ships 'memory' and 'pgvector'"
161
+ )