onec-interactive-runtime-core 0.1.19__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.
- onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
- onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
- onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
- onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
- onec_runtime/AGENTS.md +7 -0
- onec_runtime/__init__.py +3 -0
- onec_runtime/artifacts.py +59 -0
- onec_runtime/bootstrap.py +310 -0
- onec_runtime/breakpoint_workspace.py +249 -0
- onec_runtime/bsl/__init__.py +139 -0
- onec_runtime/bsl/diagnostics.py +1046 -0
- onec_runtime/bsl/full_ast_worker_projection.py +522 -0
- onec_runtime/bsl/generated_semantic_parser.py +6322 -0
- onec_runtime/bsl/lexer.py +164 -0
- onec_runtime/bsl/module_catalog.py +420 -0
- onec_runtime/bsl/module_delta.py +1079 -0
- onec_runtime/bsl/module_universe.py +1877 -0
- onec_runtime/bsl/notebook_cells.py +232 -0
- onec_runtime/bsl/notebook_method_globals.py +74 -0
- onec_runtime/bsl/notebook_methods.py +175 -0
- onec_runtime/bsl/parser_artifact_identity.py +119 -0
- onec_runtime/bsl/parser_target.py +388 -0
- onec_runtime/bsl/preprocessor.py +16 -0
- onec_runtime/bsl/semantic_lowering.py +1539 -0
- onec_runtime/bsl/source_maps.py +2411 -0
- onec_runtime/bsl/worker_dependency_resolver.py +289 -0
- onec_runtime/bsl/worker_preprocessor.py +247 -0
- onec_runtime/bsl/worker_projection_model.py +137 -0
- onec_runtime/bsl/worker_reload_source_map.py +1697 -0
- onec_runtime/capture.py +113 -0
- onec_runtime/capture_source.py +453 -0
- onec_runtime/compact_table.py +376 -0
- onec_runtime/compact_table_backend.py +500 -0
- onec_runtime/config.py +198 -0
- onec_runtime/configurator_agent.py +520 -0
- onec_runtime/controller_worker.py +400 -0
- onec_runtime/credentials.py +18 -0
- onec_runtime/epf_container.py +196 -0
- onec_runtime/errors.py +205 -0
- onec_runtime/experiment.py +130 -0
- onec_runtime/extension_bundle.py +944 -0
- onec_runtime/extension_lifecycle.py +402 -0
- onec_runtime/extension_state.py +222 -0
- onec_runtime/fault_injection.py +30 -0
- onec_runtime/kernel.py +179 -0
- onec_runtime/lease.py +123 -0
- onec_runtime/observation.py +287 -0
- onec_runtime/performance_profile.py +245 -0
- onec_runtime/privacy.py +263 -0
- onec_runtime/processes.py +265 -0
- onec_runtime/prototype_runtime.py +2644 -0
- onec_runtime/rdbg/__init__.py +1 -0
- onec_runtime/rdbg/models.py +130 -0
- onec_runtime/rdbg/reconnect.py +59 -0
- onec_runtime/rdbg/session.py +913 -0
- onec_runtime/rdbg/transport.py +138 -0
- onec_runtime/rdbg/xml_codec.py +980 -0
- onec_runtime/recovery.py +106 -0
- onec_runtime/recovery_journal.py +79 -0
- onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
- onec_runtime/resources/extension/extension-manifest.json +108 -0
- onec_runtime/runtime_api.py +5272 -0
- onec_runtime/runtime_contracts.py +196 -0
- onec_runtime/server_worker.py +2034 -0
- onec_runtime/session.py +2123 -0
- onec_runtime/startup_diagnostics.py +45 -0
- onec_runtime/stop_routing.py +65 -0
- onec_runtime/supervised_processes.py +232 -0
- onec_runtime/supervised_runtime.py +512 -0
- onec_runtime/supervisor.py +363 -0
- onec_runtime/supervisor_model.py +148 -0
- onec_runtime/supervisor_protocol.py +77 -0
- onec_runtime/table_materialization.py +471 -0
- onec_runtime/table_transfer_backend.py +203 -0
- onec_runtime/table_value.py +311 -0
- onec_runtime/toolchain.py +730 -0
- onec_runtime/value_materialization.py +432 -0
- onec_runtime/value_transfer_backend.py +199 -0
- onec_runtime/worker_breakpoints.py +1431 -0
- onec_runtime/worker_epf.py +313 -0
- onec_runtime/worker_epf_template.py +53 -0
- onec_runtime/worker_stage_protocol.py +516 -0
- onec_runtime/worker_universe.py +3772 -0
- onec_runtime_build/__init__.py +1 -0
- onec_runtime_build/parser_target_development.py +116 -0
onec_runtime/kernel.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.resources
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from onec_runtime.extension_bundle import EXTENSION_NAME, read_extension_manifest
|
|
9
|
+
from onec_runtime.rdbg.models import ModuleLocation
|
|
10
|
+
|
|
11
|
+
KERNEL_OBJECT_ID = "8fc91d24-20f5-4da4-8ff7-7a7c682f80f5"
|
|
12
|
+
OBJECT_MODULE_PROPERTY_ID = "a637f77f-3840-441d-a1c3-699c8c5cb7e0"
|
|
13
|
+
SERVICE_BREAKPOINT_MARKER = "@runtime-service-breakpoint"
|
|
14
|
+
with importlib.resources.as_file(
|
|
15
|
+
importlib.resources.files("onec_runtime").joinpath(
|
|
16
|
+
"resources", "extension", "extension-manifest.json"
|
|
17
|
+
)
|
|
18
|
+
) as _manifest_path:
|
|
19
|
+
_PRODUCT_BREAKPOINTS = read_extension_manifest(_manifest_path).breakpoints
|
|
20
|
+
|
|
21
|
+
EXTENSION_OBJECT_ID = str(_PRODUCT_BREAKPOINTS.managed.object_id)
|
|
22
|
+
MANAGED_APPLICATION_MODULE_PROPERTY_ID = "d22e852a-cf8a-4f77-8ccb-3548e7792bea"
|
|
23
|
+
EXTENSION_BREAKPOINT_MARKER = "@runtime-extension-service-breakpoint"
|
|
24
|
+
SESSION_MODULE_PROPERTY_ID = "9b7bbbae-9771-46f2-9e4d-2489e0ffc702"
|
|
25
|
+
SESSION_STARTUP_BREAKPOINT_MARKER = "@runtime-session-startup-breakpoint"
|
|
26
|
+
SERVER_COMMON_MODULE_OBJECT_ID = str(_PRODUCT_BREAKPOINTS.server_service.object_id)
|
|
27
|
+
COMMON_MODULE_PROPERTY_ID = "d5963243-262e-4398-b4d7-fb16d06484f6"
|
|
28
|
+
SERVER_EXTENSION_BREAKPOINT_MARKER = "@runtime-server-extension-service-breakpoint"
|
|
29
|
+
SERVER_EXTENSION_ENTRY_BREAKPOINT_MARKER = "@runtime-server-extension-entry-breakpoint"
|
|
30
|
+
SYNTHETIC_CAPTURE_A_MARKER = "@runtime-synthetic-capture-a"
|
|
31
|
+
SYNTHETIC_CAPTURE_B_MARKER = "@runtime-synthetic-capture-b"
|
|
32
|
+
SHIELDED_NESTED_CAPTURE_MARKER = "@runtime-shielded-nested-capture"
|
|
33
|
+
USER_BREAKPOINT_MARKER = "@runtime-user-breakpoint"
|
|
34
|
+
ZUP_GENERAL_PURPOSE_OBJECT_ID = "75e77f99-b9d1-4bc0-93aa-c77a93d760d3"
|
|
35
|
+
ZUP_SERVER_CAPTURE_LINE = 875
|
|
36
|
+
ZUP_SERVER_CAPTURE_B_LINE = 877
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def service_breakpoint_line(module_path: Path) -> int:
|
|
40
|
+
lines = module_path.read_text(encoding="utf-8").splitlines()
|
|
41
|
+
matches = [
|
|
42
|
+
index
|
|
43
|
+
for index, line in enumerate(lines, start=1)
|
|
44
|
+
if SERVICE_BREAKPOINT_MARKER in line
|
|
45
|
+
]
|
|
46
|
+
if len(matches) != 1:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Expected exactly one {SERVICE_BREAKPOINT_MARKER} marker, found {len(matches)}"
|
|
49
|
+
)
|
|
50
|
+
marker_line = lines[matches[0] - 1]
|
|
51
|
+
if marker_line.lstrip().startswith("//") or (
|
|
52
|
+
";" not in marker_line and " Тогда" not in marker_line
|
|
53
|
+
):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"The service breakpoint marker must be on an executable BSL line"
|
|
56
|
+
)
|
|
57
|
+
return matches[0]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def external_module_url(epf_path: Path) -> str:
|
|
61
|
+
normalized = str(epf_path.resolve()).replace("\\", "/")
|
|
62
|
+
return "file://" + quote(normalized, safe="/:")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extension_breakpoint_line(module_path: Path) -> int:
|
|
66
|
+
lines = module_path.read_text(encoding="utf-8-sig").splitlines()
|
|
67
|
+
matches = [
|
|
68
|
+
index
|
|
69
|
+
for index, line in enumerate(lines, start=1)
|
|
70
|
+
if EXTENSION_BREAKPOINT_MARKER in line
|
|
71
|
+
]
|
|
72
|
+
if len(matches) != 1:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Expected exactly one {EXTENSION_BREAKPOINT_MARKER} marker, found {len(matches)}"
|
|
75
|
+
)
|
|
76
|
+
return matches[0]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def server_extension_breakpoint_line(module_path: Path) -> int:
|
|
80
|
+
lines = module_path.read_text(encoding="utf-8-sig").splitlines()
|
|
81
|
+
matches = [
|
|
82
|
+
index
|
|
83
|
+
for index, line in enumerate(lines, start=1)
|
|
84
|
+
if SERVER_EXTENSION_BREAKPOINT_MARKER in line
|
|
85
|
+
]
|
|
86
|
+
if len(matches) != 1:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"Expected exactly one {SERVER_EXTENSION_BREAKPOINT_MARKER} marker, found {len(matches)}"
|
|
89
|
+
)
|
|
90
|
+
return matches[0]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def server_extension_entry_breakpoint_line(module_path: Path) -> int:
|
|
94
|
+
lines = module_path.read_text(encoding="utf-8-sig").splitlines()
|
|
95
|
+
matches = [
|
|
96
|
+
index
|
|
97
|
+
for index, line in enumerate(lines, start=1)
|
|
98
|
+
if SERVER_EXTENSION_ENTRY_BREAKPOINT_MARKER in line
|
|
99
|
+
]
|
|
100
|
+
if len(matches) != 1:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Expected exactly one {SERVER_EXTENSION_ENTRY_BREAKPOINT_MARKER} "
|
|
103
|
+
f"marker, found {len(matches)}"
|
|
104
|
+
)
|
|
105
|
+
marker_line = lines[matches[0] - 1]
|
|
106
|
+
if marker_line.lstrip().startswith("//") or ";" not in marker_line:
|
|
107
|
+
raise ValueError("The server entry breakpoint marker must be executable")
|
|
108
|
+
return matches[0]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def session_extension_breakpoint_line(module_path: Path) -> int:
|
|
112
|
+
lines = module_path.read_text(encoding="utf-8-sig").splitlines()
|
|
113
|
+
matches = [
|
|
114
|
+
index
|
|
115
|
+
for index, line in enumerate(lines, start=1)
|
|
116
|
+
if SESSION_STARTUP_BREAKPOINT_MARKER in line
|
|
117
|
+
]
|
|
118
|
+
if len(matches) != 1:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"Expected exactly one {SESSION_STARTUP_BREAKPOINT_MARKER} marker, "
|
|
121
|
+
f"found {len(matches)}"
|
|
122
|
+
)
|
|
123
|
+
marker_line = lines[matches[0] - 1]
|
|
124
|
+
if marker_line.lstrip().startswith("//") or " Тогда" not in marker_line:
|
|
125
|
+
raise ValueError("The session startup marker must be on an executable BSL line")
|
|
126
|
+
return matches[0]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _server_module_marker_location(
|
|
130
|
+
module_path: Path,
|
|
131
|
+
marker: str,
|
|
132
|
+
) -> ModuleLocation:
|
|
133
|
+
lines = module_path.read_text(encoding="utf-8-sig").splitlines()
|
|
134
|
+
matches = [index for index, line in enumerate(lines, start=1) if marker in line]
|
|
135
|
+
if len(matches) != 1:
|
|
136
|
+
raise ValueError(f"Expected exactly one {marker} marker, found {len(matches)}")
|
|
137
|
+
marker_line = lines[matches[0] - 1]
|
|
138
|
+
if marker_line.lstrip().startswith("//") or (
|
|
139
|
+
";" not in marker_line and " Тогда" not in marker_line
|
|
140
|
+
):
|
|
141
|
+
raise ValueError(f"Runtime marker {marker} must be executable")
|
|
142
|
+
return ModuleLocation(
|
|
143
|
+
"ExtensionModule",
|
|
144
|
+
"",
|
|
145
|
+
UUID(SERVER_COMMON_MODULE_OBJECT_ID),
|
|
146
|
+
UUID(COMMON_MODULE_PROPERTY_ID),
|
|
147
|
+
matches[0],
|
|
148
|
+
EXTENSION_NAME,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def synthetic_capture_locations(
|
|
153
|
+
module_path: Path,
|
|
154
|
+
) -> tuple[ModuleLocation, ModuleLocation]:
|
|
155
|
+
return _server_module_marker_location(
|
|
156
|
+
module_path,
|
|
157
|
+
SYNTHETIC_CAPTURE_A_MARKER,
|
|
158
|
+
), _server_module_marker_location(module_path, SYNTHETIC_CAPTURE_B_MARKER)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def shielded_capture_location(module_path: Path) -> ModuleLocation:
|
|
162
|
+
return _server_module_marker_location(module_path, SHIELDED_NESTED_CAPTURE_MARKER)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def user_breakpoint_location(module_path: Path) -> ModuleLocation:
|
|
166
|
+
return _server_module_marker_location(module_path, USER_BREAKPOINT_MARKER)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def zup_server_capture_location(
|
|
170
|
+
*,
|
|
171
|
+
line: int = ZUP_SERVER_CAPTURE_LINE,
|
|
172
|
+
) -> ModuleLocation:
|
|
173
|
+
return ModuleLocation(
|
|
174
|
+
module_type="ConfigModule",
|
|
175
|
+
url="",
|
|
176
|
+
object_id=UUID(ZUP_GENERAL_PURPOSE_OBJECT_ID),
|
|
177
|
+
property_id=UUID(COMMON_MODULE_PROPERTY_ID),
|
|
178
|
+
line=line,
|
|
179
|
+
)
|
onec_runtime/lease.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from math import isfinite
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from onec_runtime.errors import (
|
|
9
|
+
LeaseConflict,
|
|
10
|
+
LeaseExpired,
|
|
11
|
+
StaleLeaseEpoch,
|
|
12
|
+
StaleRuntimeGeneration,
|
|
13
|
+
)
|
|
14
|
+
from onec_runtime.supervisor_model import LeaseHandle
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Server-owned policy cap; permits the supervised live phase lease of 95 seconds.
|
|
18
|
+
MAX_LEASE_TTL_S = 120.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_lease_ttl(ttl_s: object) -> float:
|
|
22
|
+
if type(ttl_s) is int:
|
|
23
|
+
if ttl_s <= 0 or ttl_s > MAX_LEASE_TTL_S:
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"ttl_s must be greater than 0 and at most {MAX_LEASE_TTL_S}"
|
|
26
|
+
)
|
|
27
|
+
return float(ttl_s)
|
|
28
|
+
if type(ttl_s) is not float or not isfinite(ttl_s):
|
|
29
|
+
raise ValueError("ttl_s must be an exact finite built-in number")
|
|
30
|
+
if ttl_s <= 0 or ttl_s > MAX_LEASE_TTL_S:
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"ttl_s must be greater than 0 and at most {MAX_LEASE_TTL_S}"
|
|
33
|
+
)
|
|
34
|
+
return ttl_s
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class LeaseStatus:
|
|
39
|
+
owner_id: UUID | None
|
|
40
|
+
lease_epoch: int | None
|
|
41
|
+
expires_in_s: float | None
|
|
42
|
+
expired: bool
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LeaseAuthority:
|
|
46
|
+
"""Issues the single mutating-owner lease for one runtime generation."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, generation_id: int, *, clock: Callable[[], float]) -> None:
|
|
49
|
+
self._generation_id = generation_id
|
|
50
|
+
self._clock = clock
|
|
51
|
+
self._active: LeaseHandle | None = None
|
|
52
|
+
self._deadline: float | None = None
|
|
53
|
+
self._expired = False
|
|
54
|
+
self._next_epoch = 1
|
|
55
|
+
self._expiry_reported = False
|
|
56
|
+
|
|
57
|
+
def grant(self, owner_id: UUID, *, ttl_s: object) -> LeaseHandle:
|
|
58
|
+
validated_ttl_s = validate_lease_ttl(ttl_s)
|
|
59
|
+
if self._active is not None:
|
|
60
|
+
self._raise_if_expired()
|
|
61
|
+
if self._active.owner_id == owner_id:
|
|
62
|
+
return self._active
|
|
63
|
+
raise LeaseConflict("Another owner holds this generation lease")
|
|
64
|
+
if self._expired:
|
|
65
|
+
raise LeaseExpired("The generation lease has expired")
|
|
66
|
+
|
|
67
|
+
self._active = LeaseHandle(self._generation_id, owner_id, self._next_epoch)
|
|
68
|
+
self._next_epoch += 1
|
|
69
|
+
self._deadline = self._clock() + validated_ttl_s
|
|
70
|
+
return self._active
|
|
71
|
+
|
|
72
|
+
def renew(self, handle: LeaseHandle, *, ttl_s: object) -> LeaseHandle:
|
|
73
|
+
active = self._require_valid_handle(handle)
|
|
74
|
+
validated_ttl_s = validate_lease_ttl(ttl_s)
|
|
75
|
+
self._deadline = self._clock() + validated_ttl_s
|
|
76
|
+
return active
|
|
77
|
+
|
|
78
|
+
def validate(self, handle: LeaseHandle) -> LeaseHandle:
|
|
79
|
+
return self._require_valid_handle(handle)
|
|
80
|
+
|
|
81
|
+
def expire_if_due(self) -> LeaseHandle | None:
|
|
82
|
+
if self._active is None or self._expiry_reported:
|
|
83
|
+
return None
|
|
84
|
+
if not self._expired:
|
|
85
|
+
deadline = self._deadline
|
|
86
|
+
if deadline is None or self._clock() < deadline:
|
|
87
|
+
return None
|
|
88
|
+
self._expired = True
|
|
89
|
+
self._expiry_reported = True
|
|
90
|
+
return self._active
|
|
91
|
+
|
|
92
|
+
def status(self) -> LeaseStatus:
|
|
93
|
+
if self._active is None:
|
|
94
|
+
return LeaseStatus(None, None, None, self._expired)
|
|
95
|
+
deadline = self._deadline
|
|
96
|
+
if deadline is None:
|
|
97
|
+
return LeaseStatus(self._active.owner_id, self._active.lease_epoch, None, True)
|
|
98
|
+
remaining = max(0.0, deadline - self._clock())
|
|
99
|
+
return LeaseStatus(
|
|
100
|
+
self._active.owner_id,
|
|
101
|
+
self._active.lease_epoch,
|
|
102
|
+
remaining,
|
|
103
|
+
self._expired or remaining == 0.0,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _require_valid_handle(self, handle: LeaseHandle) -> LeaseHandle:
|
|
107
|
+
if handle.generation_id != self._generation_id:
|
|
108
|
+
raise StaleRuntimeGeneration(
|
|
109
|
+
f"Generation {handle.generation_id} is no longer current"
|
|
110
|
+
)
|
|
111
|
+
active = self._active
|
|
112
|
+
if active is None or handle.owner_id != active.owner_id:
|
|
113
|
+
raise LeaseConflict("The supplied owner does not hold this lease")
|
|
114
|
+
if handle.lease_epoch != active.lease_epoch:
|
|
115
|
+
raise StaleLeaseEpoch("The supplied lease epoch is not current")
|
|
116
|
+
self._raise_if_expired()
|
|
117
|
+
return active
|
|
118
|
+
|
|
119
|
+
def _raise_if_expired(self) -> None:
|
|
120
|
+
deadline = self._deadline
|
|
121
|
+
if self._expired or deadline is None or self._clock() >= deadline:
|
|
122
|
+
self._expired = True
|
|
123
|
+
raise LeaseExpired("The generation lease has expired")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Bounded, expression-free observation requests for agent operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from enum import StrEnum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
MAX_OBSERVATIONS = 100
|
|
11
|
+
MAX_SELECTION_LIMIT = 10_000
|
|
12
|
+
MAX_COLUMNS = 100
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ObservationSourceKind(StrEnum):
|
|
16
|
+
CONTEXT_BINDING = "context_binding"
|
|
17
|
+
FRAME_LOCAL = "frame_local"
|
|
18
|
+
TEMPORARY_TABLE = "temporary_table"
|
|
19
|
+
TEMPORARY_TABLE_MANAGER = "temporary_table_manager"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ObservationResult(StrEnum):
|
|
23
|
+
PROXY = "proxy"
|
|
24
|
+
PREVIEW = "preview"
|
|
25
|
+
PYTHON = "python"
|
|
26
|
+
DATAFRAME = "dataframe"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SelectionKind(StrEnum):
|
|
30
|
+
SLICE = "slice"
|
|
31
|
+
TABLE_ROWS = "table_rows"
|
|
32
|
+
FIELDS = "fields"
|
|
33
|
+
KEYS = "keys"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _identifier(value: object, *, name: str) -> str:
|
|
37
|
+
if not isinstance(value, str) or not value or len(value) > 256:
|
|
38
|
+
raise ValueError(f"{name} must be a bounded identifier")
|
|
39
|
+
if not value.isidentifier():
|
|
40
|
+
raise ValueError(f"{name} must be an identifier without expressions")
|
|
41
|
+
return value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _qualified_binding(value: object) -> str:
|
|
45
|
+
if not isinstance(value, str) or len(value) > 512:
|
|
46
|
+
raise ValueError("context binding must be bounded")
|
|
47
|
+
parts = value.split(".")
|
|
48
|
+
if len(parts) != 2 or parts[0].casefold() not in {"bsl", "python"}:
|
|
49
|
+
raise ValueError("context binding requires bsl. or python. namespace")
|
|
50
|
+
_identifier(parts[1], name="context binding")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _opaque_id(value: object, *, name: str) -> str:
|
|
55
|
+
if (
|
|
56
|
+
not isinstance(value, str)
|
|
57
|
+
or not value
|
|
58
|
+
or len(value) > 128
|
|
59
|
+
or any(not (character.isascii() and (character.isalnum() or character in "-_")) for character in value)
|
|
60
|
+
):
|
|
61
|
+
raise ValueError(f"{name} must be an opaque identifier")
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _sequence(value: object, *, name: str) -> tuple[str, ...]:
|
|
66
|
+
if isinstance(value, str) or not isinstance(value, Sequence):
|
|
67
|
+
raise TypeError(f"{name} must be a sequence")
|
|
68
|
+
result = tuple(_identifier(item, name=name) for item in value)
|
|
69
|
+
if len(result) > MAX_COLUMNS or len(set(item.casefold() for item in result)) != len(result):
|
|
70
|
+
raise ValueError(f"{name} must be unique and bounded")
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class ManagerOrigin:
|
|
76
|
+
namespace: str
|
|
77
|
+
root: str
|
|
78
|
+
fields: tuple[str, ...] = ()
|
|
79
|
+
|
|
80
|
+
def __post_init__(self) -> None:
|
|
81
|
+
if self.namespace not in {"frame", "context"}:
|
|
82
|
+
raise ValueError("manager namespace must be frame or context")
|
|
83
|
+
_identifier(self.root, name="manager root")
|
|
84
|
+
fields = _sequence(self.fields, name="manager fields")
|
|
85
|
+
object.__setattr__(self, "fields", fields)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_wire(cls, value: object) -> "ManagerOrigin":
|
|
89
|
+
if not isinstance(value, Mapping) or set(value) != {"namespace", "root", "fields"}:
|
|
90
|
+
raise ValueError("manager origin requires namespace, root and fields")
|
|
91
|
+
return cls(
|
|
92
|
+
namespace=value["namespace"], # type: ignore[arg-type]
|
|
93
|
+
root=value["root"], # type: ignore[arg-type]
|
|
94
|
+
fields=value["fields"], # type: ignore[arg-type]
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True, slots=True)
|
|
99
|
+
class ObservationSource:
|
|
100
|
+
kind: ObservationSourceKind
|
|
101
|
+
name: str | None = None
|
|
102
|
+
table: str | None = None
|
|
103
|
+
manager_id: str | None = None
|
|
104
|
+
origin: ManagerOrigin | None = None
|
|
105
|
+
|
|
106
|
+
def __post_init__(self) -> None:
|
|
107
|
+
if not isinstance(self.kind, ObservationSourceKind):
|
|
108
|
+
raise TypeError("source kind is invalid")
|
|
109
|
+
if self.kind is ObservationSourceKind.CONTEXT_BINDING:
|
|
110
|
+
object.__setattr__(self, "name", _qualified_binding(self.name))
|
|
111
|
+
if self.table is not None or self.manager_id is not None or self.origin is not None:
|
|
112
|
+
raise ValueError("context binding cannot include manager fields")
|
|
113
|
+
elif self.kind is ObservationSourceKind.FRAME_LOCAL:
|
|
114
|
+
object.__setattr__(self, "name", _identifier(self.name, name="frame local"))
|
|
115
|
+
if self.table is not None or self.manager_id is not None or self.origin is not None:
|
|
116
|
+
raise ValueError("frame local cannot include manager fields")
|
|
117
|
+
elif self.kind is ObservationSourceKind.TEMPORARY_TABLE:
|
|
118
|
+
if self.name is not None:
|
|
119
|
+
raise ValueError("temporary table uses table, not name")
|
|
120
|
+
object.__setattr__(self, "table", _identifier(self.table, name="temporary table"))
|
|
121
|
+
object.__setattr__(self, "manager_id", _opaque_id(self.manager_id, name="manager_id"))
|
|
122
|
+
if self.origin is not None:
|
|
123
|
+
raise ValueError("temporary table uses manager_id, not origin")
|
|
124
|
+
else:
|
|
125
|
+
if self.name is not None or self.table is not None or self.manager_id is not None or not isinstance(self.origin, ManagerOrigin):
|
|
126
|
+
raise ValueError("manager source requires only structured origin")
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_wire(cls, value: object) -> "ObservationSource":
|
|
130
|
+
if not isinstance(value, Mapping) or "kind" not in value:
|
|
131
|
+
raise TypeError("observation source must be a mapping")
|
|
132
|
+
allowed = {"kind", "name", "table", "manager_id", "origin"}
|
|
133
|
+
if set(value) - allowed:
|
|
134
|
+
raise ValueError("unsupported observation source field")
|
|
135
|
+
try:
|
|
136
|
+
kind = ObservationSourceKind(value["kind"])
|
|
137
|
+
except (TypeError, ValueError) as error:
|
|
138
|
+
raise ValueError("unknown observation source kind") from error
|
|
139
|
+
origin_value = value.get("origin")
|
|
140
|
+
return cls(
|
|
141
|
+
kind=kind,
|
|
142
|
+
name=value.get("name"), # type: ignore[arg-type]
|
|
143
|
+
table=value.get("table"), # type: ignore[arg-type]
|
|
144
|
+
manager_id=value.get("manager_id"), # type: ignore[arg-type]
|
|
145
|
+
origin=None if origin_value is None else ManagerOrigin.from_wire(origin_value),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class ValueSelection:
|
|
151
|
+
kind: SelectionKind
|
|
152
|
+
offset: int = 0
|
|
153
|
+
limit: int | None = None
|
|
154
|
+
columns: tuple[str, ...] = ()
|
|
155
|
+
names: tuple[str, ...] = ()
|
|
156
|
+
|
|
157
|
+
def __post_init__(self) -> None:
|
|
158
|
+
if not isinstance(self.kind, SelectionKind):
|
|
159
|
+
raise TypeError("selection kind is invalid")
|
|
160
|
+
if type(self.offset) is not int or self.offset < 0:
|
|
161
|
+
raise ValueError("selection offset must be non-negative")
|
|
162
|
+
if self.limit is not None and (
|
|
163
|
+
type(self.limit) is not int
|
|
164
|
+
or self.limit <= 0
|
|
165
|
+
or self.limit > MAX_SELECTION_LIMIT
|
|
166
|
+
):
|
|
167
|
+
raise ValueError("selection limit must be positive and bounded")
|
|
168
|
+
columns = _sequence(self.columns, name="columns")
|
|
169
|
+
names = _sequence(self.names, name="names")
|
|
170
|
+
object.__setattr__(self, "columns", columns)
|
|
171
|
+
object.__setattr__(self, "names", names)
|
|
172
|
+
if self.kind in {SelectionKind.SLICE, SelectionKind.TABLE_ROWS} and self.limit is None:
|
|
173
|
+
raise ValueError("row and slice selections require a limit")
|
|
174
|
+
if self.kind is SelectionKind.SLICE and (columns or names):
|
|
175
|
+
raise ValueError("slice does not accept fields")
|
|
176
|
+
if self.kind is SelectionKind.TABLE_ROWS and names:
|
|
177
|
+
raise ValueError("table rows use columns")
|
|
178
|
+
if self.kind in {SelectionKind.FIELDS, SelectionKind.KEYS} and (
|
|
179
|
+
self.limit is not None or self.offset != 0 or columns or not names
|
|
180
|
+
):
|
|
181
|
+
raise ValueError("field/key selection requires names only")
|
|
182
|
+
|
|
183
|
+
@classmethod
|
|
184
|
+
def from_wire(cls, value: object) -> "ValueSelection":
|
|
185
|
+
if not isinstance(value, Mapping) or "kind" not in value:
|
|
186
|
+
raise TypeError("selection must be a mapping")
|
|
187
|
+
if set(value) - {"kind", "offset", "limit", "columns", "names"}:
|
|
188
|
+
raise ValueError("unsupported selection field")
|
|
189
|
+
try:
|
|
190
|
+
kind = SelectionKind(value["kind"])
|
|
191
|
+
except (TypeError, ValueError) as error:
|
|
192
|
+
raise ValueError("unknown selection kind") from error
|
|
193
|
+
return cls(
|
|
194
|
+
kind,
|
|
195
|
+
offset=value.get("offset", 0), # type: ignore[arg-type]
|
|
196
|
+
limit=value.get("limit"), # type: ignore[arg-type]
|
|
197
|
+
columns=value.get("columns", ()), # type: ignore[arg-type]
|
|
198
|
+
names=value.get("names", ()), # type: ignore[arg-type]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass(frozen=True, slots=True)
|
|
203
|
+
class ObservationItem:
|
|
204
|
+
alias: str
|
|
205
|
+
source: ObservationSource
|
|
206
|
+
result: ObservationResult = ObservationResult.PROXY
|
|
207
|
+
select: ValueSelection | None = None
|
|
208
|
+
|
|
209
|
+
def __post_init__(self) -> None:
|
|
210
|
+
_identifier(self.alias, name="observation alias")
|
|
211
|
+
if not isinstance(self.source, ObservationSource):
|
|
212
|
+
raise TypeError("observation source is invalid")
|
|
213
|
+
if not isinstance(self.result, ObservationResult):
|
|
214
|
+
raise TypeError("observation result is invalid")
|
|
215
|
+
if self.select is not None and not isinstance(self.select, ValueSelection):
|
|
216
|
+
raise TypeError("observation selection is invalid")
|
|
217
|
+
|
|
218
|
+
@classmethod
|
|
219
|
+
def from_wire(cls, value: object) -> "ObservationItem":
|
|
220
|
+
if not isinstance(value, Mapping) or set(value) - {"alias", "source", "result", "select"}:
|
|
221
|
+
raise ValueError("observation item has unsupported fields")
|
|
222
|
+
try:
|
|
223
|
+
result = ObservationResult(value.get("result", ObservationResult.PROXY.value))
|
|
224
|
+
except (TypeError, ValueError) as error:
|
|
225
|
+
raise ValueError("unknown observation result") from error
|
|
226
|
+
selection = value.get("select")
|
|
227
|
+
return cls(
|
|
228
|
+
alias=value.get("alias"), # type: ignore[arg-type]
|
|
229
|
+
source=ObservationSource.from_wire(value.get("source")),
|
|
230
|
+
result=result,
|
|
231
|
+
select=None if selection is None else ValueSelection.from_wire(selection),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass(frozen=True, slots=True)
|
|
236
|
+
class ObservationPlan:
|
|
237
|
+
items: tuple[ObservationItem, ...]
|
|
238
|
+
budget_profile: str = "agent_metadata"
|
|
239
|
+
|
|
240
|
+
def __post_init__(self) -> None:
|
|
241
|
+
if isinstance(self.items, str) or not isinstance(self.items, Sequence):
|
|
242
|
+
raise TypeError("observation items must be a sequence")
|
|
243
|
+
items = tuple(self.items)
|
|
244
|
+
if len(items) > MAX_OBSERVATIONS or any(not isinstance(item, ObservationItem) for item in items):
|
|
245
|
+
raise ValueError("observation items must be bounded")
|
|
246
|
+
aliases = tuple(item.alias.casefold() for item in items)
|
|
247
|
+
if len(set(aliases)) != len(aliases):
|
|
248
|
+
raise ValueError("observation aliases must be unique")
|
|
249
|
+
if self.budget_profile not in {"agent_metadata", "agent_preview", "agent_dataframe"}:
|
|
250
|
+
raise ValueError("unknown observation budget profile")
|
|
251
|
+
object.__setattr__(self, "items", items)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_wire(cls, value: object) -> "ObservationPlan":
|
|
255
|
+
if not isinstance(value, Mapping) or set(value) - {"items", "budget_profile"}:
|
|
256
|
+
raise ValueError("observation plan has unsupported fields")
|
|
257
|
+
raw_items = value.get("items", ())
|
|
258
|
+
if isinstance(raw_items, str) or not isinstance(raw_items, Sequence):
|
|
259
|
+
raise TypeError("observation items must be a sequence")
|
|
260
|
+
return cls(
|
|
261
|
+
tuple(ObservationItem.from_wire(item) for item in raw_items),
|
|
262
|
+
budget_profile=value.get("budget_profile", "agent_metadata"), # type: ignore[arg-type]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@dataclass(frozen=True, slots=True)
|
|
267
|
+
class ObservationFailure:
|
|
268
|
+
alias: str
|
|
269
|
+
stage: str
|
|
270
|
+
reason: str
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@dataclass(frozen=True, slots=True)
|
|
274
|
+
class ObservationOutcome:
|
|
275
|
+
outputs: tuple[tuple[str, str], ...] = ()
|
|
276
|
+
failures: tuple[ObservationFailure, ...] = ()
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
OBSERVATION_WIRE_DATACLASSES: tuple[type[object], ...] = (
|
|
280
|
+
ManagerOrigin,
|
|
281
|
+
ObservationSource,
|
|
282
|
+
ValueSelection,
|
|
283
|
+
ObservationItem,
|
|
284
|
+
ObservationPlan,
|
|
285
|
+
ObservationFailure,
|
|
286
|
+
ObservationOutcome,
|
|
287
|
+
)
|