weft-kernel 0.1.0__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.
- weft_kernel/__init__.py +178 -0
- weft_kernel/blocking.py +370 -0
- weft_kernel/context.py +250 -0
- weft_kernel/discovery.py +1181 -0
- weft_kernel/errors.py +73 -0
- weft_kernel/fallback.py +159 -0
- weft_kernel/payload/__init__.py +43 -0
- weft_kernel/payload/applicability.py +298 -0
- weft_kernel/payload/ext.py +172 -0
- weft_kernel/payload/ids.py +22 -0
- weft_kernel/payload/lineage.py +90 -0
- weft_kernel/payload/media_type.py +18 -0
- weft_kernel/payload/node.py +260 -0
- weft_kernel/payload/outcome.py +40 -0
- weft_kernel/payload/property.py +60 -0
- weft_kernel/payload/vector.py +28 -0
- weft_kernel/pipeline.py +748 -0
- weft_kernel/py.typed +0 -0
- weft_kernel/registry.py +692 -0
- weft_kernel/resolution.py +1582 -0
- weft_kernel/runner.py +1436 -0
- weft_kernel/seam.py +725 -0
- weft_kernel-0.1.0.dist-info/METADATA +88 -0
- weft_kernel-0.1.0.dist-info/RECORD +27 -0
- weft_kernel-0.1.0.dist-info/WHEEL +4 -0
- weft_kernel-0.1.0.dist-info/licenses/LICENSE +21 -0
- weft_kernel-0.1.0.dist-info/licenses/NOTICE +77 -0
weft_kernel/context.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""`Context` — the one passport every stage's `run()` receives, and its one seam.
|
|
2
|
+
|
|
3
|
+
Specified in `docs/06-phase-0-build.md` step 4 and `docs/02-extension-model.md`
|
|
4
|
+
section 1 ("What a plugin receives"): `tenant_id`, run and trace ids,
|
|
5
|
+
cancellation, locale and `require()`. **The admission rule for a new
|
|
6
|
+
field, quoted directly:** a field is admitted only if it is needed by *every*
|
|
7
|
+
plugin regardless of contract **and** is meaningless to resolve as a service.
|
|
8
|
+
Tuning knobs fail the second test — an unbounded context object that admits
|
|
9
|
+
them accumulates fields no plugin is obliged to honour, and callers route
|
|
10
|
+
around it by building a second, overlapping bag of ad-hoc state instead of
|
|
11
|
+
resolving through the one that already exists, which is a capability leaking
|
|
12
|
+
into the kernel by another name. This module is deliberately narrow, and G11
|
|
13
|
+
narrowed it further: four identity fields with no default, one collaborator,
|
|
14
|
+
one resolution method, and nothing else.
|
|
15
|
+
|
|
16
|
+
**One collaborator, distinct from the registry.** `require()` resolves against
|
|
17
|
+
a `ServiceRegistry`, which is separate from `weft_kernel.registry.Registry`
|
|
18
|
+
(step 2): that one maps `(contract, name) -> factory` for *plugins a pipeline
|
|
19
|
+
names in configuration*, and this one maps `contract -> an already-resolved
|
|
20
|
+
instance` for *ambient services every stage may need regardless of what
|
|
21
|
+
pipeline it runs in* — `docs/02-extension-model.md`'s example is
|
|
22
|
+
`ctx.require(LLM)` returning a typed handle with no name to disambiguate, and
|
|
23
|
+
`ctx.require(TokenSink)` for the one streaming service (`docs/03-cli.md` →
|
|
24
|
+
*Output*). It starts empty and is handed to `Context` already built, exactly
|
|
25
|
+
as `Registry.add` takes `distribution` as a parameter rather than discovering
|
|
26
|
+
it. Whatever assembles a run (the runner, step 6, or the CLI, step 9) builds
|
|
27
|
+
and populates it; this module only gives `Context` somewhere to resolve
|
|
28
|
+
against.
|
|
29
|
+
|
|
30
|
+
**There was a second seam, `t()`, and G11 retired it (2026-08-18).** A
|
|
31
|
+
`MessageCatalogue` and `Context.messages` lived here from step 4 onward, so a
|
|
32
|
+
kernel or pack error could resolve its text per locale. Three phases shipped
|
|
33
|
+
with **zero registered messages and zero `ctx.t()` call sites** — the
|
|
34
|
+
mechanism's own intended clientele, 51 first-party pack error classes, all
|
|
35
|
+
chose English literals too — and G11 settled that Weft's *interface* is
|
|
36
|
+
English-only as a product decision, investing instead in the **content**-
|
|
37
|
+
language axis it already has. A locale-keyed message store with one locale is
|
|
38
|
+
a dict with a constant key, so the catalogue, `Context.messages`, `t()` and
|
|
39
|
+
the three error classes they brought (`UnknownMessageError`,
|
|
40
|
+
`DuplicateMessageError`, `MessageFormatError`) are gone, taking this kernel
|
|
41
|
+
from 33 error classes to 30. `docs/05-grilling-sessions.md` → G11 holds the
|
|
42
|
+
session; `docs/02-extension-model.md` §1 owns what replaced it — an English
|
|
43
|
+
literal at the raise site, whose explanation surface is
|
|
44
|
+
`manual/troubleshooting.md`'s coverage ratchet, and whose *quality* is
|
|
45
|
+
fitness function 12 rather than a convention an author has to remember.
|
|
46
|
+
|
|
47
|
+
**Cancellation is not a field, on purpose.** `docs/02-extension-model.md`:
|
|
48
|
+
"cancellation is native, and under an async core that is task cancellation."
|
|
49
|
+
Storing a second, Weft-owned cancellation flag would be exactly the shadow
|
|
50
|
+
machinery G6 refuses — it could drift from the real `asyncio.Task` state, and
|
|
51
|
+
nothing would keep the two in sync. `Context.cancelled` is a computed view
|
|
52
|
+
onto the task actually running the stage: `asyncio.current_task().cancelling()
|
|
53
|
+
> 0` (Python 3.11+, native to `asyncio`, no new primitive). It exists so a
|
|
54
|
+
compute-heavy stage with no `await` in its inner loop — invisible to the
|
|
55
|
+
blocking-call detector by design, `blocking.py`'s closing note — has a
|
|
56
|
+
cooperative checkpoint; it is not how cancellation is *requested*. Requesting
|
|
57
|
+
it is `task.cancel()`, called by whoever holds the task, which is never this
|
|
58
|
+
module's job.
|
|
59
|
+
|
|
60
|
+
**Identity fields carry no default.** `tenant_id`, `run_id`, `trace_id` and
|
|
61
|
+
`locale` are supplied by the caller. A default locale, in particular, is a
|
|
62
|
+
policy choice — which language a tenant with no stated preference gets — and
|
|
63
|
+
the kernel names no capability and states no policy; the driver constructing
|
|
64
|
+
a `Context` states it instead.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
from __future__ import annotations
|
|
68
|
+
|
|
69
|
+
import asyncio
|
|
70
|
+
from dataclasses import dataclass, field
|
|
71
|
+
from typing import cast
|
|
72
|
+
|
|
73
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
74
|
+
|
|
75
|
+
from weft_kernel.errors import UnresolvedNameError, WeftError
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class UnresolvedServiceError(WeftError, UnresolvedNameError):
|
|
79
|
+
"""`ctx.require()` was asked for a contract nothing registered for this run.
|
|
80
|
+
|
|
81
|
+
The message states the contract that was wanted and every contract that
|
|
82
|
+
*is* available, so a stage author who forgot to wire a service reads the
|
|
83
|
+
error as a wiring bug rather than a mystery — the same standard
|
|
84
|
+
`registry.py`'s `UnknownPluginError` sets for plugin lookup.
|
|
85
|
+
|
|
86
|
+
Fitness function 12's family: `valid_options` is every contract that
|
|
87
|
+
*is* registered on this run.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str, *, valid_options: tuple[str, ...]) -> None:
|
|
91
|
+
super().__init__(message)
|
|
92
|
+
self.valid_options = valid_options
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DuplicateServiceError(WeftError):
|
|
96
|
+
"""Two instances were registered for the same contract on one `ServiceRegistry`.
|
|
97
|
+
|
|
98
|
+
A service registry is populated once per run by whatever assembles it; a
|
|
99
|
+
second instance for a contract already resolved is not an update, it is
|
|
100
|
+
two callers disagreeing about which instance a stage should get. Refused
|
|
101
|
+
rather than silently overwritten — `registry.py` takes the same stance for
|
|
102
|
+
plugin names, for the same reason: a silent overwrite is a bug someone
|
|
103
|
+
eventually has to find.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ServiceRole(BaseModel):
|
|
108
|
+
"""A pack's declaration, beside the contract it publishes, that `[services].<key>`
|
|
109
|
+
selects an implementation of that contract for one run.
|
|
110
|
+
|
|
111
|
+
Ledger task **9.0**, closing the hole `docs/02-extension-model.md` §1 named in its own
|
|
112
|
+
Phase 0 narrowing: a service is populated into a `ServiceRegistry` by whatever assembles
|
|
113
|
+
a run, but nothing let a pack *name* which of its contracts is selectable that way, or
|
|
114
|
+
under what `[services]` key. `ServiceRole` is that declaration — "one constant beside the
|
|
115
|
+
Protocol" (`docs/build-ledger.md:5026 'exists becaus'`, `:5370`), never a member on the Protocol
|
|
116
|
+
itself.
|
|
117
|
+
|
|
118
|
+
It is a plain constant rather than a `ClassVar` written into the contract's own body,
|
|
119
|
+
for the reason `weft_extract.contract` (`:44-56`) already states for `Extractor.version`:
|
|
120
|
+
`typing.Protocol` computes `__protocol_attrs__` once, by walking every attribute present
|
|
121
|
+
in the class body at that moment, so a marker placed there would become a *required*
|
|
122
|
+
structural member — a third-party implementation that provides the real methods but
|
|
123
|
+
never restates the marker would then fail a capability check that has nothing to do with
|
|
124
|
+
capability. Holding the declaration beside the Protocol, not inside it, keeps
|
|
125
|
+
`isinstance` checking exactly what it always checked.
|
|
126
|
+
|
|
127
|
+
The kernel names no capability here: `contract` is a bare `type`, never `NodeStore`,
|
|
128
|
+
`Embedder` or any other capability name a pack might publish — the same restraint
|
|
129
|
+
`weft_kernel.discovery.RendererOffer` keeps for a result type it never names.
|
|
130
|
+
|
|
131
|
+
`key` and `contract` state the declaration only; resolving `[services].<key>`
|
|
132
|
+
against a running `weft.toml` and building the named plugin into a `ServiceRegistry`
|
|
133
|
+
entry is the job of whatever assembles a run, not this model's.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
model_config = ConfigDict(frozen=True, extra="forbid", arbitrary_types_allowed=True)
|
|
137
|
+
|
|
138
|
+
key: str = Field(min_length=1)
|
|
139
|
+
contract: type[object]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ServiceRegistry:
|
|
143
|
+
"""Per-run map of `contract -> the one resolved instance a stage gets back.`
|
|
144
|
+
|
|
145
|
+
Distinct from `weft_kernel.registry.Registry`: that one holds *factories*,
|
|
146
|
+
keyed by `(contract, name)`, for plugins a pipeline names in
|
|
147
|
+
configuration. This one holds *already-built instances*, keyed by
|
|
148
|
+
contract alone, for services `docs/02-extension-model.md` says are
|
|
149
|
+
"meaningless to resolve" any other way — there is no name to disambiguate
|
|
150
|
+
because a run has exactly one answer for "the LLM" or "the token sink".
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self) -> None:
|
|
154
|
+
self._instances: dict[type[object], object] = {}
|
|
155
|
+
|
|
156
|
+
def add[T](self, contract: type[T], instance: T) -> None:
|
|
157
|
+
"""Register `instance` as this run's answer for `contract`.
|
|
158
|
+
|
|
159
|
+
Refuses a second instance for a contract already registered — see
|
|
160
|
+
`DuplicateServiceError`.
|
|
161
|
+
"""
|
|
162
|
+
if contract in self._instances:
|
|
163
|
+
raise DuplicateServiceError(
|
|
164
|
+
f"a service for {contract.__name__} is already registered on this run; "
|
|
165
|
+
f"a second registration would leave it ambiguous which instance a stage "
|
|
166
|
+
f"gets back. Refused rather than silently overwritten."
|
|
167
|
+
)
|
|
168
|
+
self._instances[contract] = instance
|
|
169
|
+
|
|
170
|
+
def resolve[T](self, contract: type[T]) -> T:
|
|
171
|
+
"""This run's instance for `contract`.
|
|
172
|
+
|
|
173
|
+
Raises `UnresolvedServiceError`, naming `contract` and every contract
|
|
174
|
+
that *is* available, if nothing registered one.
|
|
175
|
+
"""
|
|
176
|
+
if contract in self._instances:
|
|
177
|
+
# `add` only ever stores an instance under its own `contract` key,
|
|
178
|
+
# so this cast states exactly the invariant `resolve` relies on.
|
|
179
|
+
return cast(T, self._instances[contract])
|
|
180
|
+
|
|
181
|
+
options = tuple(sorted(c.__name__ for c in self._instances))
|
|
182
|
+
available = ", ".join(options) or "none"
|
|
183
|
+
raise UnresolvedServiceError(
|
|
184
|
+
f"no service is registered for {contract.__name__} on this run. It is "
|
|
185
|
+
f"unavailable because nothing resolved one before this stage ran. "
|
|
186
|
+
f"Services available on this run: {available}.",
|
|
187
|
+
valid_options=options,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
192
|
+
class Context:
|
|
193
|
+
"""The one passport a stage's `run()` receives. There is exactly one per run.
|
|
194
|
+
|
|
195
|
+
`tenant_id`, `run_id`, `trace_id` and `locale` are ambient identity, fixed
|
|
196
|
+
for the lifetime of one run — `frozen=True` makes that a type-level fact
|
|
197
|
+
rather than a convention a stage could break by assigning `ctx.tenant_id`,
|
|
198
|
+
consistent with CLAUDE.md's "frozen where the value is a domain object"
|
|
199
|
+
and with `registry.RegistryEntry`'s precedent in this same layer. Freezing
|
|
200
|
+
`Context` does not freeze `services`: that collaborator is populated by
|
|
201
|
+
whatever assembles the run, not carried as identity.
|
|
202
|
+
`require()` is the one resolution seam — see the module docstring for why
|
|
203
|
+
nothing else lives here, and for what G11 retired.
|
|
204
|
+
|
|
205
|
+
**`locale` is the run's configured *content* language, never an interface
|
|
206
|
+
language** (G11, 2026-08-18). Weft's interface is English-only; what this
|
|
207
|
+
field selects is material a model reads or writes, and its only consumer
|
|
208
|
+
today is prompt text selection (`weft_prompts.typed_prompt`, exact locale →
|
|
209
|
+
primary subtag → `en`). It is deliberately distinct from a *query*'s own
|
|
210
|
+
locale: `weft_retrieve.payload.Query.locale` is "a fact about the ask", and
|
|
211
|
+
a question asked in Polish against an English corpus is a different thing
|
|
212
|
+
from a run configured for Polish. Nothing selects this yet — the CLI passes
|
|
213
|
+
`"en"` — and giving an operator a way to choose it is `docs/03-cli.md`'s,
|
|
214
|
+
hence Phase 3's.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
tenant_id: str
|
|
218
|
+
run_id: str
|
|
219
|
+
trace_id: str
|
|
220
|
+
locale: str
|
|
221
|
+
services: ServiceRegistry = field(default_factory=ServiceRegistry)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def cancelled(self) -> bool:
|
|
225
|
+
"""Whether the task running this stage has a pending cancellation request.
|
|
226
|
+
|
|
227
|
+
A computed view onto `asyncio`'s own task state — see the module
|
|
228
|
+
docstring for why `Context` stores no cancellation flag of its own.
|
|
229
|
+
`False` outside a running task (for example, while unit-testing a
|
|
230
|
+
stage's pure logic with no event loop, or from inside an
|
|
231
|
+
`asyncio.to_thread` worker — the compute-heavy-stage offload path this
|
|
232
|
+
property exists to serve, which itself runs off the event loop), never
|
|
233
|
+
an error: the absence of a task is not a request to cancel.
|
|
234
|
+
`asyncio.current_task()` raises `RuntimeError` rather than returning
|
|
235
|
+
`None` when there is no running loop to ask, so that specific,
|
|
236
|
+
documented case is the one exception this property catches.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
task = asyncio.current_task()
|
|
240
|
+
except RuntimeError:
|
|
241
|
+
return False
|
|
242
|
+
return task is not None and task.cancelling() > 0
|
|
243
|
+
|
|
244
|
+
def require[T](self, contract: type[T]) -> T:
|
|
245
|
+
"""This run's instance for `contract`, resolved by type — never by name.
|
|
246
|
+
|
|
247
|
+
Raises `UnresolvedServiceError`, naming what was wanted and what is
|
|
248
|
+
available, if nothing registered one — see `ServiceRegistry.resolve`.
|
|
249
|
+
"""
|
|
250
|
+
return self.services.resolve(contract)
|