context-engineering-core 0.12.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.
- context_engine/__init__.py +15 -0
- context_engine/__main__.py +3 -0
- context_engine/_sqlite.py +19 -0
- context_engine/_windows_security.py +125 -0
- context_engine/_windows_storage.py +110 -0
- context_engine/adapters.py +148 -0
- context_engine/answers.py +90 -0
- context_engine/budget.py +93 -0
- context_engine/cli.py +222 -0
- context_engine/config.py +193 -0
- context_engine/context.py +116 -0
- context_engine/demo.py +98 -0
- context_engine/errors.py +91 -0
- context_engine/evaluation/__init__.py +1 -0
- context_engine/evaluation/commands.py +90 -0
- context_engine/evaluation/corpus.py +247 -0
- context_engine/evaluation/data/freeze.json +26 -0
- context_engine/evaluation/data/ground_truth.json +287 -0
- context_engine/evaluation/data/pin_retention.json +8 -0
- context_engine/evaluation/data/protocol.json +122 -0
- context_engine/evaluation/data/scenario.json +1226 -0
- context_engine/evaluation/data/summary_fixture.json +7 -0
- context_engine/evaluation/execution.py +538 -0
- context_engine/evaluation/fixtures.py +32 -0
- context_engine/evaluation/grading.py +62 -0
- context_engine/evaluation/harness.py +165 -0
- context_engine/evaluation/heldout/__init__.py +260 -0
- context_engine/evaluation/heldout/__main__.py +40 -0
- context_engine/evaluation/heldout/data/ground_truth.json +18 -0
- context_engine/evaluation/heldout/data/manifest.json +9 -0
- context_engine/evaluation/heldout/data/protocol.json +14 -0
- context_engine/evaluation/heldout/data/scenarios.json +18 -0
- context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
- context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
- context_engine/evaluation/heldout/scoring.py +176 -0
- context_engine/evaluation/protocol.py +264 -0
- context_engine/evaluation/reporting.py +294 -0
- context_engine/evaluation/results.py +331 -0
- context_engine/evaluation/run_commands.py +146 -0
- context_engine/evaluation/strategies.py +261 -0
- context_engine/inspection.py +266 -0
- context_engine/layers/__init__.py +18 -0
- context_engine/layers/cap.py +99 -0
- context_engine/layers/common.py +162 -0
- context_engine/layers/pin.py +68 -0
- context_engine/layers/retrieve.py +233 -0
- context_engine/layers/summarize.py +202 -0
- context_engine/layers/window.py +29 -0
- context_engine/mcp_server.py +267 -0
- context_engine/memory/__init__.py +5 -0
- context_engine/memory/codec.py +91 -0
- context_engine/memory/demo.py +90 -0
- context_engine/memory/store.py +770 -0
- context_engine/models.py +384 -0
- context_engine/pipeline.py +383 -0
- context_engine/providers/__init__.py +1 -0
- context_engine/providers/client.py +301 -0
- context_engine/providers/commands.py +138 -0
- context_engine/providers/contracts.py +324 -0
- context_engine/providers/credentials.py +66 -0
- context_engine/providers/memory.py +112 -0
- context_engine/providers/store.py +408 -0
- context_engine/providers/transport.py +195 -0
- context_engine/providers/windows_credentials.py +23 -0
- context_engine/py.typed +0 -0
- context_engine/service/__init__.py +1 -0
- context_engine/service/__main__.py +68 -0
- context_engine/service/app.py +404 -0
- context_engine/service/auth.py +132 -0
- context_engine/service/client.py +52 -0
- context_engine/service/control.py +134 -0
- context_engine/service/demo.py +151 -0
- context_engine/service/integrations.py +28 -0
- context_engine/service/schemas.py +59 -0
- context_engine/service/worker.py +59 -0
- context_engine/service/workers.py +156 -0
- context_engine/summaries.py +196 -0
- context_engine/tokens.py +171 -0
- context_engine/work.py +46 -0
- context_engineering_core-0.12.0.dist-info/METADATA +203 -0
- context_engineering_core-0.12.0.dist-info/RECORD +84 -0
- context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
- context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
- context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Public one-call context assembly with immutable outputs and final budget admission."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from dataclasses import asdict, dataclass, replace
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .budget import validate_request
|
|
9
|
+
from .config import BudgetConfig, CapConfig, RetrievalConfig
|
|
10
|
+
from .context import ContextCarrier, LayerDiagnostic
|
|
11
|
+
from .errors import ContractError, LayerStateError, RequiredContextTooLarge
|
|
12
|
+
from .layers import CapLayer, PinLayer, RetrieveLayer, SummaryLayer, WindowLayer
|
|
13
|
+
from .layers.common import ORDER, history_digest, render_request, require_prepared, window_block
|
|
14
|
+
from .layers.retrieve import evidence_block, represented_in_window, window_messages
|
|
15
|
+
from .layers.summarize import SummaryPolicy
|
|
16
|
+
from .models import (
|
|
17
|
+
BlockKind,
|
|
18
|
+
ChatRequest,
|
|
19
|
+
ContextBlock,
|
|
20
|
+
KeyedPins,
|
|
21
|
+
Message,
|
|
22
|
+
Role,
|
|
23
|
+
SourceRef,
|
|
24
|
+
ToolDefinition,
|
|
25
|
+
Turn,
|
|
26
|
+
aware_time,
|
|
27
|
+
canonical_json,
|
|
28
|
+
freeze_sequence,
|
|
29
|
+
)
|
|
30
|
+
from .tokens import TiktokenCounter, TokenCounter, TokenEstimate
|
|
31
|
+
from .work import Cancellation, raise_if_cancelled, validate_cancellation
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class AssemblyOptions:
|
|
36
|
+
"""Independent optional stages; mandatory reservation and WINDOW always run."""
|
|
37
|
+
|
|
38
|
+
cap: bool = True
|
|
39
|
+
pins: bool = True
|
|
40
|
+
retrieve: bool = True
|
|
41
|
+
summarize: bool = True
|
|
42
|
+
|
|
43
|
+
def __post_init__(self):
|
|
44
|
+
if any(type(value) is not bool for value in asdict(self).values()):
|
|
45
|
+
raise ContractError("Assembly options must be booleans")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class BlockUsage:
|
|
50
|
+
kind: BlockKind
|
|
51
|
+
content_tokens: int
|
|
52
|
+
input_delta_tokens: int
|
|
53
|
+
sources: tuple[SourceRef, ...]
|
|
54
|
+
included: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class Drop:
|
|
59
|
+
kind: BlockKind
|
|
60
|
+
source_id: str | None
|
|
61
|
+
reason: str = "final_request_over_budget"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class AssemblyDiagnostics:
|
|
66
|
+
input_allowance: int
|
|
67
|
+
estimate: TokenEstimate
|
|
68
|
+
base_request_tokens: int
|
|
69
|
+
blocks: tuple[BlockUsage, ...]
|
|
70
|
+
planned_window_turn_ids: tuple[str, ...]
|
|
71
|
+
kept_turn_ids: tuple[str, ...]
|
|
72
|
+
omitted_turn_ids: tuple[str, ...]
|
|
73
|
+
retrieved_turn_ids: tuple[str, ...]
|
|
74
|
+
retrieved_chunk_ids: tuple[str, ...]
|
|
75
|
+
layer_diagnostics: tuple[LayerDiagnostic, ...]
|
|
76
|
+
drops: tuple[Drop, ...]
|
|
77
|
+
history_fingerprint: str
|
|
78
|
+
input_fingerprint: str
|
|
79
|
+
config_fingerprint: str
|
|
80
|
+
evaluated_at: datetime
|
|
81
|
+
package_version: str = __version__
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def remaining_tokens(self) -> int:
|
|
85
|
+
return self.input_allowance - self.estimate.estimated_tokens
|
|
86
|
+
|
|
87
|
+
def to_dict(self) -> dict:
|
|
88
|
+
return {
|
|
89
|
+
"input_allowance": self.input_allowance,
|
|
90
|
+
"estimate": self.estimate.to_dict(),
|
|
91
|
+
"remaining_tokens": self.remaining_tokens,
|
|
92
|
+
"base_request_tokens": self.base_request_tokens,
|
|
93
|
+
"blocks": [asdict(b) for b in self.blocks],
|
|
94
|
+
"planned_window_turn_ids": list(self.planned_window_turn_ids),
|
|
95
|
+
"kept_turn_ids": list(self.kept_turn_ids),
|
|
96
|
+
"omitted_turn_ids": list(self.omitted_turn_ids),
|
|
97
|
+
"retrieved_turn_ids": list(self.retrieved_turn_ids),
|
|
98
|
+
"retrieved_chunk_ids": list(self.retrieved_chunk_ids),
|
|
99
|
+
"summary_used": any(b.kind == BlockKind.SUMMARY and b.included for b in self.blocks),
|
|
100
|
+
"layer_diagnostics": [asdict(d) for d in self.layer_diagnostics],
|
|
101
|
+
"drops": [asdict(d) for d in self.drops],
|
|
102
|
+
"history_fingerprint": self.history_fingerprint,
|
|
103
|
+
"input_fingerprint": self.input_fingerprint,
|
|
104
|
+
"config_fingerprint": self.config_fingerprint,
|
|
105
|
+
"evaluated_at": self.evaluated_at.isoformat(),
|
|
106
|
+
"package_version": self.package_version,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class AssembledContext:
|
|
112
|
+
request: ChatRequest
|
|
113
|
+
blocks: tuple[ContextBlock, ...]
|
|
114
|
+
diagnostics: AssemblyDiagnostics
|
|
115
|
+
|
|
116
|
+
def __post_init__(self) -> None:
|
|
117
|
+
if not isinstance(self.request, ChatRequest) or not isinstance(
|
|
118
|
+
self.diagnostics, AssemblyDiagnostics
|
|
119
|
+
):
|
|
120
|
+
raise ContractError("Assembly requires validated request and diagnostics")
|
|
121
|
+
freeze_sequence(self, "blocks", ContextBlock)
|
|
122
|
+
if self.diagnostics.remaining_tokens < 0:
|
|
123
|
+
raise ContractError("Cannot return an over-budget assembled context")
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def messages(self) -> list[dict]:
|
|
127
|
+
"""Fresh API-ready message dictionaries; callers cannot mutate the result."""
|
|
128
|
+
return self.request.to_wire()["messages"]
|
|
129
|
+
|
|
130
|
+
def to_dict(self, *, include_content: bool = False) -> dict:
|
|
131
|
+
if type(include_content) is not bool:
|
|
132
|
+
raise ContractError("include_content must be an explicit boolean")
|
|
133
|
+
result = {"diagnostics": self.diagnostics.to_dict(), "content_included": include_content}
|
|
134
|
+
if include_content:
|
|
135
|
+
result["request"] = self.request.to_wire()
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _replace_block(
|
|
140
|
+
blocks: tuple[ContextBlock, ...], replacement: ContextBlock
|
|
141
|
+
) -> tuple[ContextBlock, ...]:
|
|
142
|
+
return tuple(
|
|
143
|
+
sorted(
|
|
144
|
+
tuple(b for b in blocks if b.kind != replacement.kind) + (replacement,),
|
|
145
|
+
key=lambda b: ORDER.index(b.kind),
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _finalize(
|
|
151
|
+
context: ContextCarrier,
|
|
152
|
+
counter: TokenCounter,
|
|
153
|
+
budget: BudgetConfig,
|
|
154
|
+
*,
|
|
155
|
+
recover_capped_window: bool = False,
|
|
156
|
+
):
|
|
157
|
+
"""Bounded, deterministic optional removal; required payload stays byte-identical."""
|
|
158
|
+
require_prepared(context, counter)
|
|
159
|
+
if context.plan.input_allowance != budget.input_allowance or not context.window_planned:
|
|
160
|
+
raise LayerStateError("Finalization requires a matching completed selection plan")
|
|
161
|
+
blocks = context.blocks
|
|
162
|
+
ids = context.plan.window_turn_ids
|
|
163
|
+
chunks = context.retrieved_chunks
|
|
164
|
+
turns = {turn.turn_id: turn for turn in context.original_turns}
|
|
165
|
+
visible = window_messages(context)
|
|
166
|
+
for chunk in chunks:
|
|
167
|
+
if chunk.source.turn_id not in turns:
|
|
168
|
+
raise LayerStateError("Retrieved source conflicts with the history/window plan")
|
|
169
|
+
chunk.validate_source(turns[chunk.source.turn_id])
|
|
170
|
+
if chunk.source.turn_id in ids and (
|
|
171
|
+
not recover_capped_window or represented_in_window(chunk, visible)
|
|
172
|
+
):
|
|
173
|
+
raise LayerStateError("Retrieved source duplicates or violates the WINDOW policy")
|
|
174
|
+
# Content/provenance must describe the same selected chunks and recent turns.
|
|
175
|
+
for kind, expected in (
|
|
176
|
+
(BlockKind.RETRIEVED, evidence_block(chunks, counter)),
|
|
177
|
+
(BlockKind.WINDOW, window_block(context, ids, counter)),
|
|
178
|
+
):
|
|
179
|
+
actual = next((b for b in blocks if b.kind == kind), None)
|
|
180
|
+
if (
|
|
181
|
+
actual is None
|
|
182
|
+
or actual.content != expected.content
|
|
183
|
+
or actual.sources != expected.sources
|
|
184
|
+
):
|
|
185
|
+
raise LayerStateError("Selected content and provenance do not agree")
|
|
186
|
+
drops: list[Drop] = []
|
|
187
|
+
# One initial count, one summary removal, one per window turn, one per chunk.
|
|
188
|
+
for _ in range(2 + len(ids) + len(chunks)):
|
|
189
|
+
raise_if_cancelled(context.cancellation)
|
|
190
|
+
request = render_request(blocks, context.question, context.required_request.tools)
|
|
191
|
+
estimate = counter.count_request(request)
|
|
192
|
+
if estimate.estimated_tokens <= budget.input_allowance:
|
|
193
|
+
validated = validate_request(request=request, config=budget, counter=counter)
|
|
194
|
+
if validated != estimate:
|
|
195
|
+
raise LayerStateError("Token counter changed for the identical final request")
|
|
196
|
+
sources = tuple(
|
|
197
|
+
SourceRef(
|
|
198
|
+
t.scope, t.turn_id, m.message_id, t.revision, 0, len(m.content), m.content_hash
|
|
199
|
+
)
|
|
200
|
+
for t in context.original_turns
|
|
201
|
+
if t.turn_id in ids
|
|
202
|
+
for m in t.messages
|
|
203
|
+
if m.content
|
|
204
|
+
)
|
|
205
|
+
final_window = replace(window_block(context, ids, counter), sources=sources)
|
|
206
|
+
blocks = _replace_block(blocks, final_window)
|
|
207
|
+
blocks = _replace_block(
|
|
208
|
+
blocks,
|
|
209
|
+
ContextBlock(
|
|
210
|
+
BlockKind.QUESTION,
|
|
211
|
+
context.question.content,
|
|
212
|
+
text_tokens=counter.count_text(context.question.content),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
return (
|
|
216
|
+
request,
|
|
217
|
+
tuple(sorted(blocks, key=lambda b: ORDER.index(b.kind))),
|
|
218
|
+
validated,
|
|
219
|
+
ids,
|
|
220
|
+
chunks,
|
|
221
|
+
tuple(drops),
|
|
222
|
+
)
|
|
223
|
+
summary = next((b for b in blocks if b.kind == BlockKind.SUMMARY), None)
|
|
224
|
+
if summary is not None and summary.content:
|
|
225
|
+
blocks = _replace_block(blocks, ContextBlock(BlockKind.SUMMARY, "", text_tokens=0))
|
|
226
|
+
drops.append(Drop(BlockKind.SUMMARY, None))
|
|
227
|
+
elif ids:
|
|
228
|
+
dropped, ids = ids[0], ids[1:]
|
|
229
|
+
blocks = _replace_block(blocks, window_block(context, ids, counter))
|
|
230
|
+
drops.append(Drop(BlockKind.WINDOW, dropped))
|
|
231
|
+
elif chunks:
|
|
232
|
+
dropped, chunks = chunks[-1], chunks[:-1]
|
|
233
|
+
blocks = _replace_block(blocks, evidence_block(chunks, counter))
|
|
234
|
+
drops.append(Drop(BlockKind.RETRIEVED, dropped.chunk_id))
|
|
235
|
+
else:
|
|
236
|
+
raise RequiredContextTooLarge(
|
|
237
|
+
"Required context exceeds the final input allowance",
|
|
238
|
+
required_tokens=estimate.estimated_tokens,
|
|
239
|
+
input_allowance=budget.input_allowance,
|
|
240
|
+
overflow_tokens=estimate.estimated_tokens - budget.input_allowance,
|
|
241
|
+
)
|
|
242
|
+
raise LayerStateError("Finalization exceeded its bounded removal steps")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _block_accounting(blocks, question, tools, counter, estimate):
|
|
246
|
+
empty_question = replace(question, content="")
|
|
247
|
+
running: tuple[ContextBlock, ...] = ()
|
|
248
|
+
previous = counter.count_request(render_request((), empty_question, tools)).estimated_tokens
|
|
249
|
+
base = previous
|
|
250
|
+
result = []
|
|
251
|
+
by_kind = {b.kind: b for b in blocks}
|
|
252
|
+
for kind in ORDER:
|
|
253
|
+
item = by_kind.get(kind, ContextBlock(kind, "", text_tokens=0))
|
|
254
|
+
current_question = question if kind == BlockKind.QUESTION else empty_question
|
|
255
|
+
if kind != BlockKind.QUESTION:
|
|
256
|
+
running += (item,)
|
|
257
|
+
current = counter.count_request(
|
|
258
|
+
render_request(running, current_question, tools)
|
|
259
|
+
).estimated_tokens
|
|
260
|
+
content = question.content if kind == BlockKind.QUESTION else item.content
|
|
261
|
+
result.append(
|
|
262
|
+
BlockUsage(
|
|
263
|
+
kind,
|
|
264
|
+
counter.count_text(content),
|
|
265
|
+
current - previous,
|
|
266
|
+
item.sources,
|
|
267
|
+
kind == BlockKind.QUESTION or bool(content),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
previous = current
|
|
271
|
+
if previous != estimate.estimated_tokens:
|
|
272
|
+
raise LayerStateError("Per-block accounting disagrees with the final request estimate")
|
|
273
|
+
return base, tuple(result)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def assemble_context(
|
|
277
|
+
history: tuple[Turn, ...] | list[Turn],
|
|
278
|
+
question: Message | str,
|
|
279
|
+
pinned_facts: KeyedPins,
|
|
280
|
+
budget: BudgetConfig,
|
|
281
|
+
*,
|
|
282
|
+
system: str,
|
|
283
|
+
at: datetime | None = None,
|
|
284
|
+
token_counter: TokenCounter | None = None,
|
|
285
|
+
cap_config: CapConfig | None = None,
|
|
286
|
+
retrieval_config: RetrievalConfig | None = None,
|
|
287
|
+
summary_policy: SummaryPolicy | None = None,
|
|
288
|
+
tools: tuple[ToolDefinition, ...] | list[ToolDefinition] = (),
|
|
289
|
+
options: AssemblyOptions | None = None,
|
|
290
|
+
chunk_index=None,
|
|
291
|
+
cancellation: Cancellation | None = None,
|
|
292
|
+
) -> AssembledContext:
|
|
293
|
+
"""Assemble authorized history into an estimated-token-safe request, or raise a typed error.
|
|
294
|
+
|
|
295
|
+
Scope is derived from KeyedPins, including for empty history. Authorization is
|
|
296
|
+
the caller's responsibility. Supply `at` for reproducible pin/summary expiry.
|
|
297
|
+
Every call, including calls after tools, should use this boundary.
|
|
298
|
+
"""
|
|
299
|
+
validate_cancellation(cancellation)
|
|
300
|
+
raise_if_cancelled(cancellation)
|
|
301
|
+
if not isinstance(pinned_facts, KeyedPins) or not isinstance(budget, BudgetConfig):
|
|
302
|
+
raise ContractError("Expected KeyedPins and BudgetConfig")
|
|
303
|
+
options = AssemblyOptions() if options is None else options
|
|
304
|
+
if not isinstance(options, AssemblyOptions):
|
|
305
|
+
raise ContractError("Expected validated assembly options")
|
|
306
|
+
if not options.pins:
|
|
307
|
+
pinned_facts = KeyedPins(pinned_facts.scope)
|
|
308
|
+
budget = replace(
|
|
309
|
+
budget,
|
|
310
|
+
retrieval_reserve=budget.retrieval_reserve if options.retrieve else 0,
|
|
311
|
+
summary_reserve=budget.summary_reserve if options.summarize else 0,
|
|
312
|
+
)
|
|
313
|
+
if not isinstance(history, (list, tuple)) or any(not isinstance(t, Turn) for t in history):
|
|
314
|
+
raise ContractError("History must be a sequence of validated turns")
|
|
315
|
+
if not isinstance(tools, (list, tuple)) or any(
|
|
316
|
+
not isinstance(t, ToolDefinition) for t in tools
|
|
317
|
+
):
|
|
318
|
+
raise ContractError("Tools must be a sequence of validated definitions")
|
|
319
|
+
at = datetime.now(UTC) if at is None else at
|
|
320
|
+
aware_time("at", at)
|
|
321
|
+
cap_config = CapConfig() if cap_config is None else cap_config
|
|
322
|
+
retrieval_config = RetrievalConfig() if retrieval_config is None else retrieval_config
|
|
323
|
+
if not isinstance(cap_config, CapConfig) or not isinstance(retrieval_config, RetrievalConfig):
|
|
324
|
+
raise ContractError("Expected validated layer configuration")
|
|
325
|
+
if isinstance(question, str):
|
|
326
|
+
used = {m.message_id for t in history for m in t.messages}
|
|
327
|
+
identifier = "current-question"
|
|
328
|
+
while identifier in used:
|
|
329
|
+
identifier += ":next"
|
|
330
|
+
question = Message(identifier, Role.USER, question)
|
|
331
|
+
context = ContextCarrier(
|
|
332
|
+
pinned_facts.scope, history, history, pinned_facts, question, cancellation=cancellation
|
|
333
|
+
)
|
|
334
|
+
counter = TiktokenCounter() if token_counter is None else token_counter
|
|
335
|
+
if options.cap:
|
|
336
|
+
context = CapLayer(counter, cap_config).apply(context)
|
|
337
|
+
else:
|
|
338
|
+
context = replace(context, diagnostics=(LayerDiagnostic("CAP", "layer_disabled"),))
|
|
339
|
+
for layer in (
|
|
340
|
+
PinLayer(counter, budget, system, at, tuple(tools)),
|
|
341
|
+
RetrieveLayer(counter, retrieval_config, chunk_index),
|
|
342
|
+
WindowLayer(counter),
|
|
343
|
+
SummaryLayer(counter, summary_policy if options.summarize else None, at),
|
|
344
|
+
):
|
|
345
|
+
raise_if_cancelled(cancellation)
|
|
346
|
+
context = layer.apply(context)
|
|
347
|
+
raise_if_cancelled(cancellation)
|
|
348
|
+
request, blocks, estimate, kept, chunks, drops = _finalize(
|
|
349
|
+
context, counter, budget, recover_capped_window=retrieval_config.recover_capped_window
|
|
350
|
+
)
|
|
351
|
+
base, accounting = _block_accounting(blocks, question, tuple(tools), counter, estimate)
|
|
352
|
+
config_data = {
|
|
353
|
+
"budget": asdict(budget),
|
|
354
|
+
"cap": asdict(cap_config),
|
|
355
|
+
"retrieval": asdict(retrieval_config),
|
|
356
|
+
"options": asdict(options),
|
|
357
|
+
"counting": {
|
|
358
|
+
"tokenizer": estimate.tokenizer_name,
|
|
359
|
+
"version": estimate.tokenizer_version,
|
|
360
|
+
"serializer": estimate.serializer_name,
|
|
361
|
+
"factor": estimate.calibration_factor,
|
|
362
|
+
"overhead": estimate.adapter_overhead_tokens,
|
|
363
|
+
},
|
|
364
|
+
}
|
|
365
|
+
diagnostics = AssemblyDiagnostics(
|
|
366
|
+
budget.input_allowance,
|
|
367
|
+
estimate,
|
|
368
|
+
base,
|
|
369
|
+
accounting,
|
|
370
|
+
context.plan.window_turn_ids,
|
|
371
|
+
kept,
|
|
372
|
+
tuple(t.turn_id for t in context.original_turns if t.turn_id not in kept),
|
|
373
|
+
tuple(dict.fromkeys(c.source.turn_id for c in chunks)),
|
|
374
|
+
tuple(c.chunk_id for c in chunks),
|
|
375
|
+
context.diagnostics,
|
|
376
|
+
drops,
|
|
377
|
+
history_digest(context.original_turns),
|
|
378
|
+
context.prepared_fingerprint,
|
|
379
|
+
hashlib.sha256(canonical_json(config_data).encode()).hexdigest(),
|
|
380
|
+
at,
|
|
381
|
+
)
|
|
382
|
+
raise_if_cancelled(cancellation)
|
|
383
|
+
return AssembledContext(request, blocks, diagnostics)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Optional provider integration. Importing the core never loads this package."""
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Scoped provider execution, final token admission, quota reservations and local replay."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from ..budget import validate_request
|
|
8
|
+
from ..config import BudgetConfig
|
|
9
|
+
from ..errors import ContextEngineError, ContractError, StorageError
|
|
10
|
+
from ..models import ChatRequest, Scope, canonical_json, text_value
|
|
11
|
+
from ..tokens import TiktokenCounter
|
|
12
|
+
from .contracts import (
|
|
13
|
+
ADAPTER_VERSION,
|
|
14
|
+
ENDPOINT,
|
|
15
|
+
Completion,
|
|
16
|
+
GenerationConfig,
|
|
17
|
+
InceptionGenerationConfig,
|
|
18
|
+
ModelResult,
|
|
19
|
+
PriceCard,
|
|
20
|
+
QuotaPolicy,
|
|
21
|
+
ReplayPolicy,
|
|
22
|
+
RetryConfig,
|
|
23
|
+
TransportFailure,
|
|
24
|
+
fingerprint,
|
|
25
|
+
)
|
|
26
|
+
from .store import RuntimeStore
|
|
27
|
+
from .transport import GroqTransport, InceptionTransport
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProviderClient:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
*,
|
|
34
|
+
store: RuntimeStore,
|
|
35
|
+
quota: QuotaPolicy,
|
|
36
|
+
prices: PriceCard,
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
generation: GenerationConfig | None = None,
|
|
39
|
+
retries: RetryConfig | None = None,
|
|
40
|
+
replay: ReplayPolicy | None = None,
|
|
41
|
+
transport=None,
|
|
42
|
+
counter=None,
|
|
43
|
+
clock=time.time,
|
|
44
|
+
monotonic=time.monotonic,
|
|
45
|
+
sleep=asyncio.sleep,
|
|
46
|
+
):
|
|
47
|
+
self.store, self.quota, self.prices = store, quota, prices
|
|
48
|
+
self._api_key = api_key # Never exported, represented or persisted.
|
|
49
|
+
self.generation = generation or GenerationConfig()
|
|
50
|
+
self.retries, self.replay = retries or RetryConfig(), replay or ReplayPolicy()
|
|
51
|
+
default = (
|
|
52
|
+
InceptionTransport
|
|
53
|
+
if isinstance(self.generation, InceptionGenerationConfig)
|
|
54
|
+
else GroqTransport
|
|
55
|
+
)
|
|
56
|
+
self.transport = transport if transport is not None else default()
|
|
57
|
+
self.counter = counter if counter is not None else TiktokenCounter()
|
|
58
|
+
self.clock, self.monotonic, self.sleep = clock, monotonic, sleep
|
|
59
|
+
for value, cls in (
|
|
60
|
+
(store, RuntimeStore),
|
|
61
|
+
(quota, QuotaPolicy),
|
|
62
|
+
(prices, PriceCard),
|
|
63
|
+
(self.generation, (GenerationConfig, InceptionGenerationConfig)),
|
|
64
|
+
(self.retries, RetryConfig),
|
|
65
|
+
(self.replay, ReplayPolicy),
|
|
66
|
+
):
|
|
67
|
+
if not isinstance(value, cls):
|
|
68
|
+
raise ContractError("Provider client requires validated configuration")
|
|
69
|
+
quota.validate_prices(prices)
|
|
70
|
+
inception = isinstance(self.generation, InceptionGenerationConfig)
|
|
71
|
+
if (inception and type(self.transport) is GroqTransport) or (
|
|
72
|
+
not inception and isinstance(self.transport, InceptionTransport)
|
|
73
|
+
):
|
|
74
|
+
raise ContractError("Generation settings and provider transport do not match")
|
|
75
|
+
|
|
76
|
+
async def complete(
|
|
77
|
+
self,
|
|
78
|
+
request: ChatRequest,
|
|
79
|
+
budget: BudgetConfig,
|
|
80
|
+
*,
|
|
81
|
+
scope: Scope,
|
|
82
|
+
security_scope: str,
|
|
83
|
+
snapshot_revision: str,
|
|
84
|
+
policy_version="v1",
|
|
85
|
+
) -> ModelResult:
|
|
86
|
+
"""Caller authorizes scope. Task cancellation preserves uncertain accounting."""
|
|
87
|
+
start = self.monotonic()
|
|
88
|
+
generation = self.generation
|
|
89
|
+
if (
|
|
90
|
+
not isinstance(request, ChatRequest)
|
|
91
|
+
or not isinstance(budget, BudgetConfig)
|
|
92
|
+
or not isinstance(scope, Scope)
|
|
93
|
+
):
|
|
94
|
+
raise ContractError("Provider call requires validated request, budget and scope")
|
|
95
|
+
for name, value in (
|
|
96
|
+
("security_scope", security_scope),
|
|
97
|
+
("snapshot_revision", snapshot_revision),
|
|
98
|
+
("policy_version", policy_version),
|
|
99
|
+
):
|
|
100
|
+
text_value(name, value)
|
|
101
|
+
if (
|
|
102
|
+
budget.completion_reservation != generation.max_completion_tokens
|
|
103
|
+
or budget.model_capacity > 131072
|
|
104
|
+
):
|
|
105
|
+
raise ContractError("Assembly and provider completion/context budgets must agree")
|
|
106
|
+
estimate = validate_request(request=request, config=budget, counter=self.counter)
|
|
107
|
+
payload = generation.to_wire(request)
|
|
108
|
+
if len(canonical_json(payload).encode()) > 2_000_000:
|
|
109
|
+
raise ContractError("Provider request exceeds the byte limit")
|
|
110
|
+
key = fingerprint(
|
|
111
|
+
{
|
|
112
|
+
# Groq keys are unchanged; other transports bind their own endpoint.
|
|
113
|
+
"adapter": getattr(self.transport, "adapter", ADAPTER_VERSION),
|
|
114
|
+
"endpoint": getattr(self.transport, "endpoint", ENDPOINT),
|
|
115
|
+
"payload": payload,
|
|
116
|
+
"account": self.quota.account_id,
|
|
117
|
+
"scope": asdict(scope),
|
|
118
|
+
"security_scope": security_scope,
|
|
119
|
+
"snapshot_revision": snapshot_revision,
|
|
120
|
+
"policy_version": policy_version,
|
|
121
|
+
"budget": asdict(budget),
|
|
122
|
+
"counting": estimate.to_dict(),
|
|
123
|
+
"prices": asdict(self.prices),
|
|
124
|
+
"transport": getattr(
|
|
125
|
+
self.transport, "namespace", type(self.transport).__qualname__
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
key_error = None
|
|
130
|
+
if not isinstance(self._api_key, str) or not self._api_key.strip():
|
|
131
|
+
key_error = "missing_api_key"
|
|
132
|
+
elif any(ord(c) < 33 or ord(c) > 126 for c in self._api_key):
|
|
133
|
+
key_error = "invalid_api_key"
|
|
134
|
+
attempts, known_cost = [], 0
|
|
135
|
+
reserved_tokens = estimate.estimated_tokens + generation.max_completion_tokens
|
|
136
|
+
reserve_cost = self.prices.reserve_cost(
|
|
137
|
+
estimate.estimated_tokens, generation.max_completion_tokens
|
|
138
|
+
)
|
|
139
|
+
for number in range(self.retries.max_attempts):
|
|
140
|
+
remaining = self.retries.total_timeout - (self.monotonic() - start)
|
|
141
|
+
if remaining <= 0:
|
|
142
|
+
return ModelResult(
|
|
143
|
+
"error",
|
|
144
|
+
key,
|
|
145
|
+
tuple(attempts),
|
|
146
|
+
error_code="deadline",
|
|
147
|
+
new_cost_microusd=known_cost,
|
|
148
|
+
)
|
|
149
|
+
try:
|
|
150
|
+
admission = self.store.admit(
|
|
151
|
+
policy=self.quota,
|
|
152
|
+
key=key,
|
|
153
|
+
model=generation.model,
|
|
154
|
+
reserved_tokens=reserved_tokens,
|
|
155
|
+
reserved_cost=reserve_cost,
|
|
156
|
+
prices=self.prices,
|
|
157
|
+
replay=self.replay,
|
|
158
|
+
now=self.clock(),
|
|
159
|
+
send_allowed=key_error is None,
|
|
160
|
+
)
|
|
161
|
+
except ContextEngineError as error:
|
|
162
|
+
return ModelResult(
|
|
163
|
+
"error",
|
|
164
|
+
key,
|
|
165
|
+
tuple(attempts),
|
|
166
|
+
error_code=key_error or error.code,
|
|
167
|
+
new_cost_microusd=known_cost,
|
|
168
|
+
)
|
|
169
|
+
if admission.completion is not None:
|
|
170
|
+
return ModelResult(
|
|
171
|
+
"replay",
|
|
172
|
+
key,
|
|
173
|
+
tuple(attempts),
|
|
174
|
+
admission.completion,
|
|
175
|
+
replay_of=admission.source_attempt,
|
|
176
|
+
new_cost_microusd=known_cost,
|
|
177
|
+
original_cost_microusd=admission.original_cost,
|
|
178
|
+
)
|
|
179
|
+
attempt = admission.attempt_id
|
|
180
|
+
attempts.append(attempt)
|
|
181
|
+
try:
|
|
182
|
+
self.store.mark_inflight(attempt)
|
|
183
|
+
except StorageError:
|
|
184
|
+
return ModelResult(
|
|
185
|
+
"error",
|
|
186
|
+
key,
|
|
187
|
+
tuple(attempts),
|
|
188
|
+
error_code="runtime_storage_error",
|
|
189
|
+
new_cost_microusd=known_cost,
|
|
190
|
+
warning="reservation_held_no_dispatch",
|
|
191
|
+
)
|
|
192
|
+
try:
|
|
193
|
+
remaining = self.retries.total_timeout - (self.monotonic() - start)
|
|
194
|
+
if remaining <= 0:
|
|
195
|
+
raise TransportFailure("deadline_not_sent", uncertain=False)
|
|
196
|
+
timeout = min(remaining, self.retries.attempt_timeout)
|
|
197
|
+
async with asyncio.timeout(timeout):
|
|
198
|
+
completion = await self.transport.send(payload, self._api_key, timeout)
|
|
199
|
+
if not isinstance(completion, Completion) or completion.model != generation.model:
|
|
200
|
+
raise TransportFailure("invalid_response", uncertain=True)
|
|
201
|
+
if completion.tool_calls:
|
|
202
|
+
names = {tool.name for tool in request.tools}
|
|
203
|
+
if generation.tool_choice != "auto" or any(
|
|
204
|
+
call.name not in names for call in completion.tool_calls
|
|
205
|
+
):
|
|
206
|
+
raise TransportFailure(
|
|
207
|
+
"unexpected_tool_call", uncertain=True, usage=completion.usage
|
|
208
|
+
)
|
|
209
|
+
except asyncio.CancelledError:
|
|
210
|
+
# A cancelled task may already have reached the provider. No automatic release.
|
|
211
|
+
try:
|
|
212
|
+
self.store.settle(attempt, now=self.clock(), uncertain=True, reason="cancelled")
|
|
213
|
+
except StorageError:
|
|
214
|
+
pass # Existing inflight reservation remains conservatively held.
|
|
215
|
+
raise
|
|
216
|
+
except TimeoutError:
|
|
217
|
+
failure = TransportFailure("timeout", uncertain=True)
|
|
218
|
+
except TransportFailure as error:
|
|
219
|
+
failure = error
|
|
220
|
+
except Exception:
|
|
221
|
+
failure = TransportFailure("transport", uncertain=True)
|
|
222
|
+
else:
|
|
223
|
+
try:
|
|
224
|
+
cost, warning = self.store.settle(
|
|
225
|
+
attempt, now=self.clock(), completion=completion, replay=self.replay
|
|
226
|
+
)
|
|
227
|
+
except StorageError:
|
|
228
|
+
return ModelResult(
|
|
229
|
+
"error",
|
|
230
|
+
key,
|
|
231
|
+
tuple(attempts),
|
|
232
|
+
error_code="runtime_storage_error",
|
|
233
|
+
new_cost_microusd=None,
|
|
234
|
+
warning="provider_may_have_charged_reservation_held",
|
|
235
|
+
)
|
|
236
|
+
return ModelResult(
|
|
237
|
+
{
|
|
238
|
+
"stop": "success",
|
|
239
|
+
"length": "truncated",
|
|
240
|
+
"tool_calls": "tool_calls",
|
|
241
|
+
"content_filter": "filtered",
|
|
242
|
+
}[completion.finish_reason],
|
|
243
|
+
key,
|
|
244
|
+
tuple(attempts),
|
|
245
|
+
completion,
|
|
246
|
+
new_cost_microusd=known_cost + cost,
|
|
247
|
+
original_cost_microusd=cost,
|
|
248
|
+
warning=warning,
|
|
249
|
+
)
|
|
250
|
+
try:
|
|
251
|
+
cost, warning = self.store.settle(
|
|
252
|
+
attempt,
|
|
253
|
+
now=self.clock(),
|
|
254
|
+
usage=failure.usage,
|
|
255
|
+
uncertain=failure.uncertain,
|
|
256
|
+
reason=failure.code,
|
|
257
|
+
sent=failure.code
|
|
258
|
+
not in ("connection_not_sent", "deadline_not_sent", "provider_extra_missing"),
|
|
259
|
+
)
|
|
260
|
+
except StorageError:
|
|
261
|
+
return ModelResult(
|
|
262
|
+
"error",
|
|
263
|
+
key,
|
|
264
|
+
tuple(attempts),
|
|
265
|
+
error_code="runtime_storage_error",
|
|
266
|
+
new_cost_microusd=None,
|
|
267
|
+
warning="reservation_held",
|
|
268
|
+
)
|
|
269
|
+
if cost is None:
|
|
270
|
+
return ModelResult(
|
|
271
|
+
"error",
|
|
272
|
+
key,
|
|
273
|
+
tuple(attempts),
|
|
274
|
+
error_code=failure.code,
|
|
275
|
+
new_cost_microusd=None,
|
|
276
|
+
warning=warning,
|
|
277
|
+
)
|
|
278
|
+
known_cost += cost
|
|
279
|
+
delay = (
|
|
280
|
+
failure.retry_after
|
|
281
|
+
if failure.retry_after is not None
|
|
282
|
+
else min(0.25 * 2**number, self.retries.max_backoff)
|
|
283
|
+
)
|
|
284
|
+
remaining = self.retries.total_timeout - (self.monotonic() - start)
|
|
285
|
+
if (
|
|
286
|
+
not failure.retryable
|
|
287
|
+
or failure.uncertain
|
|
288
|
+
or number + 1 >= self.retries.max_attempts
|
|
289
|
+
or delay > self.retries.max_backoff
|
|
290
|
+
or delay >= remaining
|
|
291
|
+
):
|
|
292
|
+
return ModelResult(
|
|
293
|
+
"error",
|
|
294
|
+
key,
|
|
295
|
+
tuple(attempts),
|
|
296
|
+
error_code=failure.code,
|
|
297
|
+
new_cost_microusd=known_cost,
|
|
298
|
+
warning=warning,
|
|
299
|
+
)
|
|
300
|
+
await self.sleep(delay)
|
|
301
|
+
raise AssertionError("Bounded provider loop exhausted unexpectedly")
|