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.
Files changed (84) hide show
  1. context_engine/__init__.py +15 -0
  2. context_engine/__main__.py +3 -0
  3. context_engine/_sqlite.py +19 -0
  4. context_engine/_windows_security.py +125 -0
  5. context_engine/_windows_storage.py +110 -0
  6. context_engine/adapters.py +148 -0
  7. context_engine/answers.py +90 -0
  8. context_engine/budget.py +93 -0
  9. context_engine/cli.py +222 -0
  10. context_engine/config.py +193 -0
  11. context_engine/context.py +116 -0
  12. context_engine/demo.py +98 -0
  13. context_engine/errors.py +91 -0
  14. context_engine/evaluation/__init__.py +1 -0
  15. context_engine/evaluation/commands.py +90 -0
  16. context_engine/evaluation/corpus.py +247 -0
  17. context_engine/evaluation/data/freeze.json +26 -0
  18. context_engine/evaluation/data/ground_truth.json +287 -0
  19. context_engine/evaluation/data/pin_retention.json +8 -0
  20. context_engine/evaluation/data/protocol.json +122 -0
  21. context_engine/evaluation/data/scenario.json +1226 -0
  22. context_engine/evaluation/data/summary_fixture.json +7 -0
  23. context_engine/evaluation/execution.py +538 -0
  24. context_engine/evaluation/fixtures.py +32 -0
  25. context_engine/evaluation/grading.py +62 -0
  26. context_engine/evaluation/harness.py +165 -0
  27. context_engine/evaluation/heldout/__init__.py +260 -0
  28. context_engine/evaluation/heldout/__main__.py +40 -0
  29. context_engine/evaluation/heldout/data/ground_truth.json +18 -0
  30. context_engine/evaluation/heldout/data/manifest.json +9 -0
  31. context_engine/evaluation/heldout/data/protocol.json +14 -0
  32. context_engine/evaluation/heldout/data/scenarios.json +18 -0
  33. context_engine/evaluation/heldout/data/scoring_manifest.json +4 -0
  34. context_engine/evaluation/heldout/data/scoring_protocol.json +12 -0
  35. context_engine/evaluation/heldout/scoring.py +176 -0
  36. context_engine/evaluation/protocol.py +264 -0
  37. context_engine/evaluation/reporting.py +294 -0
  38. context_engine/evaluation/results.py +331 -0
  39. context_engine/evaluation/run_commands.py +146 -0
  40. context_engine/evaluation/strategies.py +261 -0
  41. context_engine/inspection.py +266 -0
  42. context_engine/layers/__init__.py +18 -0
  43. context_engine/layers/cap.py +99 -0
  44. context_engine/layers/common.py +162 -0
  45. context_engine/layers/pin.py +68 -0
  46. context_engine/layers/retrieve.py +233 -0
  47. context_engine/layers/summarize.py +202 -0
  48. context_engine/layers/window.py +29 -0
  49. context_engine/mcp_server.py +267 -0
  50. context_engine/memory/__init__.py +5 -0
  51. context_engine/memory/codec.py +91 -0
  52. context_engine/memory/demo.py +90 -0
  53. context_engine/memory/store.py +770 -0
  54. context_engine/models.py +384 -0
  55. context_engine/pipeline.py +383 -0
  56. context_engine/providers/__init__.py +1 -0
  57. context_engine/providers/client.py +301 -0
  58. context_engine/providers/commands.py +138 -0
  59. context_engine/providers/contracts.py +324 -0
  60. context_engine/providers/credentials.py +66 -0
  61. context_engine/providers/memory.py +112 -0
  62. context_engine/providers/store.py +408 -0
  63. context_engine/providers/transport.py +195 -0
  64. context_engine/providers/windows_credentials.py +23 -0
  65. context_engine/py.typed +0 -0
  66. context_engine/service/__init__.py +1 -0
  67. context_engine/service/__main__.py +68 -0
  68. context_engine/service/app.py +404 -0
  69. context_engine/service/auth.py +132 -0
  70. context_engine/service/client.py +52 -0
  71. context_engine/service/control.py +134 -0
  72. context_engine/service/demo.py +151 -0
  73. context_engine/service/integrations.py +28 -0
  74. context_engine/service/schemas.py +59 -0
  75. context_engine/service/worker.py +59 -0
  76. context_engine/service/workers.py +156 -0
  77. context_engine/summaries.py +196 -0
  78. context_engine/tokens.py +171 -0
  79. context_engine/work.py +46 -0
  80. context_engineering_core-0.12.0.dist-info/METADATA +203 -0
  81. context_engineering_core-0.12.0.dist-info/RECORD +84 -0
  82. context_engineering_core-0.12.0.dist-info/WHEEL +4 -0
  83. context_engineering_core-0.12.0.dist-info/entry_points.txt +3 -0
  84. context_engineering_core-0.12.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,15 @@
1
+ """Core contracts. Importing this package performs no configuration or network I/O."""
2
+
3
+ __version__ = "0.12.0"
4
+
5
+
6
+ def __getattr__(name):
7
+ """Expose the real typed API lazily, without loading tokenizers on package import."""
8
+ if name in ("assemble_context", "AssembledContext", "AssemblyOptions"):
9
+ from . import pipeline
10
+
11
+ return getattr(pipeline, name)
12
+ raise AttributeError(name)
13
+
14
+
15
+ __all__ = ["assemble_context", "AssembledContext", "AssemblyOptions", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,19 @@
1
+ """SQLite connection policy shared by local stores, without provider dependencies."""
2
+
3
+ import os
4
+ import sqlite3
5
+
6
+
7
+ def connect(*args, **kwargs):
8
+ db = sqlite3.connect(*args, **kwargs)
9
+ try:
10
+ if os.name == "nt":
11
+ # Sort/index temporary data must not spill into an unchecked OS temp
12
+ # directory. Durable rollback/WAL files still use the private DB dir.
13
+ db.execute("PRAGMA temp_store=MEMORY")
14
+ if db.execute("PRAGMA temp_store").fetchone()[0] != 2:
15
+ raise sqlite3.OperationalError("Private SQLite temporary storage unavailable")
16
+ return db
17
+ except BaseException:
18
+ db.close()
19
+ raise
@@ -0,0 +1,125 @@
1
+ """Fail-closed Windows credential reads; never change an operator's ACL.
2
+
3
+ Parent directories and local administrators are trusted, as on POSIX. Only the
4
+ effective user, LocalSystem and built-in Administrators may own the file or have
5
+ applicable allow ACEs. Complex/unknown ACE forms are rejected, not approximated.
6
+ Native imports are lazy so this module is harmless on non-Windows platforms.
7
+ """
8
+
9
+ from pathlib import Path
10
+
11
+ from .errors import ContractError
12
+
13
+ TRUSTED_SYSTEM_SIDS = frozenset({"S-1-5-18", "S-1-5-32-544"})
14
+
15
+
16
+ def check_acl(owner, user, entries):
17
+ """Evaluate normalized (type, flags, mask, SID) ACEs, including inherited ACEs."""
18
+ trusted = TRUSTED_SYSTEM_SIDS | {user}
19
+ if not user or owner not in trusted or entries is None:
20
+ raise ContractError("Configuration must have a private Windows ACL")
21
+ for kind, flags, mask, sid in entries:
22
+ if flags & 0x08: # INHERIT_ONLY_ACE does not apply to the file itself.
23
+ continue
24
+ if kind not in (0, 1): # Standard allow/deny only; fail closed on complex ACEs.
25
+ raise ContractError("Unsupported Windows configuration ACL")
26
+ # A deny ACE never excuses a broad allow. This intentionally conservative
27
+ # policy avoids pretending to implement Windows' effective-access algorithm.
28
+ if kind == 0 and mask and sid not in trusted:
29
+ raise ContractError("Configuration must have a private Windows ACL")
30
+
31
+
32
+ class WindowsReader:
33
+ def __init__(self):
34
+ import pywintypes
35
+ import win32api
36
+ import win32con
37
+ import win32file
38
+ import win32security
39
+
40
+ self.error = pywintypes.error
41
+ self.api, self.con = win32api, win32con
42
+ self.file, self.security = win32file, win32security
43
+
44
+ def local_path(self, path):
45
+ path = Path(path).absolute()
46
+ # Exclude device/UNC namespaces, alternate streams and remote drives before
47
+ # CreateFile can open a pipe/device or initiate a remote connection.
48
+ if (
49
+ len(path.drive) != 2
50
+ or path.drive[1] != ":"
51
+ or not path.drive[0].isascii()
52
+ or not path.drive[0].isalpha()
53
+ or path.is_reserved()
54
+ or any(
55
+ any(char in part for char in ':<>"|?*') or part.endswith((".", " "))
56
+ for part in path.parts[1:]
57
+ )
58
+ or self.file.GetDriveType(path.anchor) != 3 # DRIVE_FIXED
59
+ ):
60
+ raise ContractError("Configuration requires a local disk file")
61
+ return path
62
+
63
+ def open(self, path):
64
+ c = self.con
65
+ path = self.local_path(path)
66
+ return self.file.CreateFile(
67
+ str(path),
68
+ c.GENERIC_READ | c.READ_CONTROL,
69
+ c.FILE_SHARE_READ, # No concurrent writer or delete/rename handle.
70
+ None, # Non-inheritable handle; no ACL mutation.
71
+ c.OPEN_EXISTING,
72
+ self.file.FILE_FLAG_OPEN_REPARSE_POINT | c.FILE_FLAG_BACKUP_SEMANTICS,
73
+ None,
74
+ )
75
+
76
+ def user(self):
77
+ s, c = self.security, self.con
78
+ try:
79
+ token = s.OpenThreadToken(self.api.GetCurrentThread(), c.TOKEN_QUERY, True)
80
+ except self.error as exc:
81
+ if exc.winerror != 1008: # ERROR_NO_TOKEN only; other errors fail closed.
82
+ raise
83
+ token = s.OpenProcessToken(self.api.GetCurrentProcess(), c.TOKEN_QUERY)
84
+ try:
85
+ return s.ConvertSidToStringSid(s.GetTokenInformation(token, s.TokenUser)[0])
86
+ finally:
87
+ token.Close()
88
+
89
+ def validate(self, handle, *, directory=False):
90
+ # Validate the opened object, not a path that can be replaced before reading.
91
+ attributes = self.file.GetFileInformationByHandle(handle)[0]
92
+ if (
93
+ self.file.GetFileType(handle) != 1
94
+ or attributes & 0x400
95
+ or bool(attributes & 0x10) != directory
96
+ ):
97
+ raise ContractError("Configuration must be a regular non-reparse file")
98
+ s = self.security
99
+ sd = s.GetSecurityInfo(
100
+ handle, s.SE_FILE_OBJECT, s.OWNER_SECURITY_INFORMATION | s.DACL_SECURITY_INFORMATION
101
+ )
102
+ owner = s.ConvertSidToStringSid(sd.GetSecurityDescriptorOwner())
103
+ acl = sd.GetSecurityDescriptorDacl()
104
+ entries = None
105
+ if acl is not None:
106
+ entries = []
107
+ for index in range(acl.GetAceCount()):
108
+ ace = acl.GetAce(index)
109
+ kind, flags = ace[0]
110
+ # Object/callback ACE layouts differ; do not misinterpret their SID.
111
+ sid = s.ConvertSidToStringSid(ace[2]) if kind in (0, 1) else None
112
+ entries.append((kind, flags, ace[1], sid))
113
+ user = self.user()
114
+ check_acl(owner, user, entries)
115
+ if directory:
116
+ # Include inherit-only grants: SQLite-created journals inherit them.
117
+ check_acl(owner, user, [(k, f & ~0x08, m, s) for k, f, m, s in entries])
118
+ if not any(
119
+ k == 0 and f & 0x01 and s == user and m & 0x1F01FF == 0x1F01FF
120
+ for k, f, m, s in entries
121
+ ):
122
+ raise ContractError("Storage directory requires private file inheritance")
123
+
124
+ def read(self, handle, size):
125
+ return self.file.ReadFile(handle, size)[1]
@@ -0,0 +1,110 @@
1
+ """Private local SQLite files and inheritable directory ACLs on Windows.
2
+
3
+ SQLite reopens paths, so ancestors, the effective user, SYSTEM and administrators
4
+ must be trusted. This is not a sandbox against those principals. Existing ACLs
5
+ are never rewritten. All native imports are lazy; no unchecked fallback exists.
6
+ """
7
+
8
+ from glob import escape
9
+
10
+ from ._windows_security import TRUSTED_SYSTEM_SIDS, WindowsReader
11
+ from .errors import StorageError
12
+
13
+
14
+ class WindowsStorage(WindowsReader):
15
+ def attributes(self):
16
+ import pywintypes
17
+
18
+ s = self.security
19
+ acl = s.ACL()
20
+ for sid in sorted(TRUSTED_SYSTEM_SIDS | {self.user()}):
21
+ acl.AddAccessAllowedAceEx(
22
+ s.ACL_REVISION, 0x01 | 0x02, 0x1F01FF, s.ConvertStringSidToSid(sid)
23
+ )
24
+ sd = s.SECURITY_DESCRIPTOR()
25
+ sd.SetSecurityDescriptorDacl(True, acl, False)
26
+ sd.SetSecurityDescriptorOwner(s.ConvertStringSidToSid(self.user()), False)
27
+ sd.SetSecurityDescriptorControl(s.SE_DACL_PROTECTED, s.SE_DACL_PROTECTED)
28
+ attributes = pywintypes.SECURITY_ATTRIBUTES()
29
+ attributes.SECURITY_DESCRIPTOR = sd
30
+ attributes.bInheritHandle = False
31
+ return attributes
32
+
33
+ def open_storage(self, path, *, create=False, exclusive=False):
34
+ c = self.con
35
+
36
+ def open_with(disposition, attributes):
37
+ return self.file.CreateFile(
38
+ str(path),
39
+ c.READ_CONTROL,
40
+ c.FILE_SHARE_READ | c.FILE_SHARE_WRITE | c.FILE_SHARE_DELETE,
41
+ attributes,
42
+ disposition,
43
+ self.file.FILE_FLAG_OPEN_REPARSE_POINT | c.FILE_FLAG_BACKUP_SEMANTICS,
44
+ None,
45
+ )
46
+
47
+ if create:
48
+ try:
49
+ return open_with(c.CREATE_NEW, self.attributes())
50
+ except self.error as exc:
51
+ if exclusive or exc.winerror not in (80, 183):
52
+ raise
53
+ return open_with(c.OPEN_EXISTING, None)
54
+
55
+ def directory(self, path, *, create):
56
+ try:
57
+ handle = self.open_storage(path)
58
+ except self.error as exc:
59
+ if not create or exc.winerror not in (2, 3) or path == path.parent:
60
+ raise
61
+ # Only missing directories get new ACLs. Ancestors remain trusted.
62
+ self.make_directory(path)
63
+ handle = self.open_storage(path)
64
+ try:
65
+ self.validate(handle, directory=True)
66
+ finally:
67
+ handle.Close()
68
+
69
+ def make_directory(self, path):
70
+ # Security is applied at creation, never via a later permission rewrite.
71
+ if not path.parent.exists():
72
+ self.make_directory(path.parent)
73
+ try:
74
+ self.file.CreateDirectory(str(path), self.attributes())
75
+ except self.error as exc:
76
+ if exc.winerror != 183: # Concurrent creator: validate below.
77
+ raise
78
+
79
+ def file_boundary(self, path, *, create=False, exclusive=False, optional=False):
80
+ try:
81
+ handle = self.open_storage(path, create=create, exclusive=exclusive)
82
+ except self.error as exc:
83
+ if optional and exc.winerror == 2:
84
+ return
85
+ raise
86
+ try:
87
+ self.validate(handle)
88
+ if self.file.GetFileInformationByHandle(handle)[7] != 1:
89
+ raise StorageError("Private storage cannot have hard-link aliases")
90
+ finally:
91
+ handle.Close()
92
+
93
+
94
+ def private_file(path, *, create=False, exclusive=False):
95
+ """Validate the DB, parent inheritance and any existing SQLite sidecars."""
96
+ try:
97
+ boundary = WindowsStorage()
98
+ path = boundary.local_path(path)
99
+ boundary.directory(path.parent, create=create)
100
+ boundary.file_boundary(path, create=create, exclusive=exclusive)
101
+ for suffix in ("-journal", "-wal", "-shm"):
102
+ boundary.file_boundary(path.with_name(path.name + suffix), optional=True)
103
+ # Multi-database DELETE transactions can leave super-journals.
104
+ for sidecar in path.parent.glob(escape(path.name) + "-mj*"):
105
+ boundary.file_boundary(sidecar, optional=True)
106
+ return path
107
+ except Exception:
108
+ raise StorageError(
109
+ "Storage requires private local Windows files and directory ACLs"
110
+ ) from None
@@ -0,0 +1,148 @@
1
+ """Provider and framework message adapters. Optional; no provider SDK is imported.
2
+
3
+ Assembly output is one system message, user data blocks and the user question, plus
4
+ optional tool schemas. These helpers reshape it for common clients:
5
+
6
+ client.chat.completions.create(model=..., **openai_chat(result))
7
+ client.messages.create(model=..., max_tokens=..., **anthropic_messages(result))
8
+ chat_model.invoke(langchain_messages(result)) # needs the [langchain] extra
9
+
10
+ `turns_from_openai` goes the other way: it converts an existing OpenAI-style chat
11
+ history into `Turn`s for `assemble_context` or `MemoryStore`.
12
+ """
13
+
14
+ import json
15
+ from collections.abc import Iterable, Mapping
16
+ from datetime import UTC, datetime
17
+
18
+ from .errors import ContractError
19
+ from .models import ChatRequest, Message, Role, Scope, ToolCall, Turn
20
+
21
+
22
+ def _request(result) -> ChatRequest:
23
+ request = getattr(result, "request", result)
24
+ if not isinstance(request, ChatRequest):
25
+ raise ContractError("Expected an AssembledContext or ChatRequest")
26
+ return request
27
+
28
+
29
+ def openai_chat(result) -> dict:
30
+ """`messages` (and `tools`, when present) for OpenAI-compatible chat APIs."""
31
+ return _request(result).to_wire()
32
+
33
+
34
+ def anthropic_messages(result) -> dict:
35
+ """`system`, `messages` (and `tools`) for the Anthropic Messages API.
36
+
37
+ The system prompt becomes the top-level `system` field; consecutive user data
38
+ blocks are merged into one user turn of text blocks, as the API expects.
39
+ """
40
+ request = _request(result)
41
+ system, messages = [], []
42
+ for message in request.messages:
43
+ if message.role == Role.SYSTEM:
44
+ system.append(message.content)
45
+ elif message.role == Role.USER and not message.tool_calls:
46
+ block = {"type": "text", "text": message.content}
47
+ if messages and messages[-1]["role"] == "user":
48
+ messages[-1]["content"].append(block)
49
+ else:
50
+ messages.append({"role": "user", "content": [block]})
51
+ else:
52
+ raise ContractError("Anthropic adapter supports assembled system/user messages only")
53
+ payload: dict = {"system": "\n\n".join(system), "messages": messages}
54
+ if request.tools:
55
+ payload["tools"] = [
56
+ {
57
+ "name": tool.name,
58
+ "description": tool.description,
59
+ "input_schema": json.loads(tool.parameters_json),
60
+ }
61
+ for tool in request.tools
62
+ ]
63
+ return payload
64
+
65
+
66
+ def langchain_messages(result) -> list:
67
+ """LangChain `SystemMessage`/`HumanMessage` objects; needs `langchain-core`."""
68
+ try:
69
+ from langchain_core.messages import HumanMessage, SystemMessage
70
+ except ImportError:
71
+ raise ImportError(
72
+ "langchain_messages needs langchain-core: "
73
+ "pip install 'context-engineering-core[langchain]'"
74
+ ) from None
75
+ converted = []
76
+ for message in _request(result).messages:
77
+ if message.role == Role.SYSTEM:
78
+ converted.append(SystemMessage(content=message.content))
79
+ elif message.role == Role.USER and not message.tool_calls:
80
+ converted.append(HumanMessage(content=message.content))
81
+ else:
82
+ raise ContractError("LangChain adapter supports assembled system/user messages only")
83
+ return converted
84
+
85
+
86
+ def _text(content, index: int) -> str:
87
+ if content is None:
88
+ return ""
89
+ if isinstance(content, str):
90
+ return content
91
+ if isinstance(content, list) and all(
92
+ isinstance(part, Mapping) and part.get("type") == "text" for part in content
93
+ ):
94
+ return "".join(str(part.get("text", "")) for part in content)
95
+ raise ContractError("Only text message content is supported", field=f"messages[{index}]")
96
+
97
+
98
+ def turns_from_openai(
99
+ messages: Iterable[Mapping],
100
+ scope: Scope,
101
+ *,
102
+ id_prefix: str = "",
103
+ timestamp: datetime | None = None,
104
+ ) -> tuple[Turn, ...]:
105
+ """Group an OpenAI-style history into turns: each user message starts a new turn.
106
+
107
+ IDs come from positions (`{id_prefix}t{n}` / `{id_prefix}m{i}`), so converting
108
+ the same history twice yields the same IDs and provenance. Pass the system prompt
109
+ to `assemble_context(system=...)` instead of including it here.
110
+ """
111
+ if not isinstance(scope, Scope):
112
+ raise ContractError("turns_from_openai requires a Scope")
113
+ at = timestamp or datetime.now(UTC)
114
+ groups: list[list[Message]] = []
115
+ for index, item in enumerate(messages):
116
+ if not isinstance(item, Mapping):
117
+ raise ContractError("Each message must be a mapping", field=f"messages[{index}]")
118
+ role = item.get("role")
119
+ if role == "system" or role == "developer":
120
+ raise ContractError(
121
+ "Pass system instructions via system=, not history", field=f"messages[{index}]"
122
+ )
123
+ if role not in ("user", "assistant", "tool"):
124
+ raise ContractError("Unsupported message role", field=f"messages[{index}]")
125
+ try:
126
+ calls = tuple(
127
+ ToolCall(call["id"], call["function"]["name"], call["function"]["arguments"])
128
+ for call in item.get("tool_calls") or ()
129
+ )
130
+ except (KeyError, TypeError):
131
+ raise ContractError("Malformed tool call", field=f"messages[{index}]") from None
132
+ message = Message(
133
+ f"{id_prefix}m{index}",
134
+ Role(role),
135
+ _text(item.get("content"), index),
136
+ tool_call_id=item.get("tool_call_id"),
137
+ tool_calls=calls,
138
+ )
139
+ if role == "user" or not groups:
140
+ groups.append([])
141
+ groups[-1].append(message)
142
+ return tuple(
143
+ Turn(f"{id_prefix}t{number}", scope, tuple(group), timestamp=at)
144
+ for number, group in enumerate(groups)
145
+ )
146
+
147
+
148
+ __all__ = ["anthropic_messages", "langchain_messages", "openai_chat", "turns_from_openai"]
@@ -0,0 +1,90 @@
1
+ """Opt-in exact JSON answer conformance; never truth grading or inference."""
2
+
3
+ import json
4
+ import re
5
+ import unicodedata
6
+ from dataclasses import dataclass
7
+
8
+ from .errors import ContractError
9
+
10
+ MAX_DOCUMENT_BYTES = 16384
11
+ PATTERNS = {
12
+ "ascii_identifier": r"[A-Za-z0-9][A-Za-z0-9._:/-]*",
13
+ "integer": r"(?:0|-?[1-9][0-9]*|UNKNOWN)",
14
+ }
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class AnswerContract:
19
+ """Strict value preservation. The caller owns evidence and authorization checks."""
20
+
21
+ kind: str = "text"
22
+ max_characters: int = 512
23
+
24
+ def __post_init__(self):
25
+ if self.kind not in ("text", "ascii_identifier", "integer"):
26
+ raise ContractError("Unsupported answer kind")
27
+ if type(self.max_characters) is not int or not 7 <= self.max_characters <= 2048:
28
+ raise ContractError("Answer length limit must be between7 and2048")
29
+
30
+ def json_schema(self) -> dict:
31
+ value = {"type": "string", "minLength": 1, "maxLength": self.max_characters}
32
+ if self.kind in PATTERNS:
33
+ value["pattern"] = "^(?:" + PATTERNS[self.kind] + ")$"
34
+ return {
35
+ "type": "object",
36
+ "additionalProperties": False,
37
+ "required": ["answer"],
38
+ "properties": {"answer": value},
39
+ }
40
+
41
+ def instructions(self) -> str:
42
+ constraint = {
43
+ "text": "Preserve the exact value, including case and Unicode characters.",
44
+ "ascii_identifier": "Use an exact ASCII identifier; never replace its hyphens or case.",
45
+ "integer": "Use a base-ten integer string without plus signs or leading zeros.",
46
+ }[self.kind]
47
+ return (
48
+ 'Return only one JSON object with exactly one string field named "answer". '
49
+ 'If evidence is missing or unresolved, return {"answer":"UNKNOWN"}. '
50
+ "Do not include Markdown, explanations, extra fields, or surrounding value whitespace. "
51
+ "The value must be a single line with no control characters. "
52
+ f"Maximum value length: {self.max_characters} characters. " + constraint
53
+ )
54
+
55
+ def parse(self, document: str) -> str:
56
+ """Reject, never repair/extract/normalize. Sanitized errors omit response bodies."""
57
+ try:
58
+ if not isinstance(document, str) or len(document) > MAX_DOCUMENT_BYTES:
59
+ raise ValueError
60
+ if len(document.encode("utf-8")) > MAX_DOCUMENT_BYTES:
61
+ raise ValueError
62
+
63
+ def pairs(items):
64
+ result = {}
65
+ for key, value in items:
66
+ if key in result:
67
+ raise ValueError
68
+ result[key] = value
69
+ return result
70
+
71
+ def invalid_constant(_value):
72
+ raise ValueError
73
+
74
+ value = json.loads(document, object_pairs_hook=pairs, parse_constant=invalid_constant)
75
+ if not isinstance(value, dict) or set(value) != {"answer"}:
76
+ raise ValueError
77
+ answer = value["answer"]
78
+ if (
79
+ not isinstance(answer, str)
80
+ or not 1 <= len(answer) <= self.max_characters
81
+ or answer != answer.strip()
82
+ or any(unicodedata.category(c) in {"Cc", "Cs", "Zl", "Zp"} for c in answer)
83
+ ):
84
+ raise ValueError
85
+ answer.encode("utf-8")
86
+ if self.kind in PATTERNS and not re.fullmatch(PATTERNS[self.kind], answer):
87
+ raise ValueError
88
+ return answer
89
+ except (ValueError, TypeError, UnicodeError, RecursionError):
90
+ raise ContractError("Answer does not conform to the exact JSON contract") from None
@@ -0,0 +1,93 @@
1
+ """Mandatory budget reservation and complete serialized-request admission."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .config import BudgetConfig
6
+ from .errors import ContractError, LayerAllocationError, PromptTooLarge, RequiredContextTooLarge
7
+ from .models import ChatRequest, freeze_sequence, integer_value
8
+ from .tokens import TokenCounter, TokenEstimate
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class BudgetPlan:
13
+ input_allowance: int
14
+ required_estimate: TokenEstimate
15
+ retrieval_tokens: int
16
+ summary_tokens: int
17
+ window_tokens: int
18
+ window_turn_ids: tuple[str, ...] = ()
19
+
20
+ def __post_init__(self) -> None:
21
+ integer_value("input_allowance", self.input_allowance, minimum=1)
22
+ if not isinstance(self.required_estimate, TokenEstimate):
23
+ raise ContractError("Budget plan requires a token estimate")
24
+ for name in ("retrieval_tokens", "summary_tokens", "window_tokens"):
25
+ integer_value(name, getattr(self, name))
26
+ ids = freeze_sequence(self, "window_turn_ids", str)
27
+ if len(set(ids)) != len(ids) or any(not identifier.strip() for identifier in ids):
28
+ raise ContractError("Window turn IDs must be unique and nonempty")
29
+ allocated = (
30
+ self.required_estimate.estimated_tokens
31
+ + self.retrieval_tokens
32
+ + self.summary_tokens
33
+ + self.window_tokens
34
+ )
35
+ if allocated != self.input_allowance:
36
+ raise ContractError("Budget allocations must equal the input allowance")
37
+
38
+ def to_dict(self) -> dict:
39
+ return {
40
+ "input_allowance": self.input_allowance,
41
+ "required": self.required_estimate.to_dict(),
42
+ "retrieval_tokens": self.retrieval_tokens,
43
+ "summary_tokens": self.summary_tokens,
44
+ "window_tokens": self.window_tokens,
45
+ "window_turn_ids": list(self.window_turn_ids),
46
+ }
47
+
48
+
49
+ def plan_budget(
50
+ *, required_request: ChatRequest, config: BudgetConfig, counter: TokenCounter
51
+ ) -> BudgetPlan:
52
+ """Reserve a caller-supplied mandatory request without dropping any content.
53
+
54
+ The assembly layer must supply system, active pins, question and tool schemas.
55
+ Layer allocations use calibrated token units and include incremental framing;
56
+ they are not raw text-token limits. Assembly recounts the final whole request.
57
+ """
58
+ estimate = counter.count_request(required_request)
59
+ allowance = config.input_allowance
60
+ remaining = allowance - estimate.estimated_tokens
61
+ if remaining < 0:
62
+ raise RequiredContextTooLarge(
63
+ "Required context exceeds the input allowance; "
64
+ "reduce supplied content or raise the budget",
65
+ required_tokens=estimate.estimated_tokens,
66
+ input_allowance=allowance,
67
+ overflow_tokens=-remaining,
68
+ )
69
+ reserve = config.retrieval_reserve + config.summary_reserve
70
+ if reserve > remaining:
71
+ raise LayerAllocationError(
72
+ "Layer reserves exceed space remaining after required context",
73
+ available_tokens=remaining,
74
+ reserved_tokens=reserve,
75
+ )
76
+ return BudgetPlan(
77
+ allowance, estimate, config.retrieval_reserve, config.summary_reserve, remaining - reserve
78
+ )
79
+
80
+
81
+ def validate_request(
82
+ *, request: ChatRequest, config: BudgetConfig, counter: TokenCounter
83
+ ) -> TokenEstimate:
84
+ """Recount an entire final request. Never infer safety from per-block sums."""
85
+ estimate = counter.count_request(request)
86
+ if estimate.estimated_tokens > config.input_allowance:
87
+ raise PromptTooLarge(
88
+ "Serialized request exceeds the estimated input allowance",
89
+ estimated_tokens=estimate.estimated_tokens,
90
+ input_allowance=config.input_allowance,
91
+ overflow_tokens=estimate.estimated_tokens - config.input_allowance,
92
+ )
93
+ return estimate