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.
Files changed (87) hide show
  1. onec_interactive_runtime_core-0.1.19.dist-info/METADATA +12 -0
  2. onec_interactive_runtime_core-0.1.19.dist-info/RECORD +87 -0
  3. onec_interactive_runtime_core-0.1.19.dist-info/WHEEL +4 -0
  4. onec_interactive_runtime_core-0.1.19.dist-info/licenses/COPYRIGHT +9 -0
  5. onec_interactive_runtime_core-0.1.19.dist-info/licenses/LICENSE +232 -0
  6. onec_interactive_runtime_core-0.1.19.dist-info/licenses/THIRD_PARTY_NOTICES.txt +32 -0
  7. onec_runtime/AGENTS.md +7 -0
  8. onec_runtime/__init__.py +3 -0
  9. onec_runtime/artifacts.py +59 -0
  10. onec_runtime/bootstrap.py +310 -0
  11. onec_runtime/breakpoint_workspace.py +249 -0
  12. onec_runtime/bsl/__init__.py +139 -0
  13. onec_runtime/bsl/diagnostics.py +1046 -0
  14. onec_runtime/bsl/full_ast_worker_projection.py +522 -0
  15. onec_runtime/bsl/generated_semantic_parser.py +6322 -0
  16. onec_runtime/bsl/lexer.py +164 -0
  17. onec_runtime/bsl/module_catalog.py +420 -0
  18. onec_runtime/bsl/module_delta.py +1079 -0
  19. onec_runtime/bsl/module_universe.py +1877 -0
  20. onec_runtime/bsl/notebook_cells.py +232 -0
  21. onec_runtime/bsl/notebook_method_globals.py +74 -0
  22. onec_runtime/bsl/notebook_methods.py +175 -0
  23. onec_runtime/bsl/parser_artifact_identity.py +119 -0
  24. onec_runtime/bsl/parser_target.py +388 -0
  25. onec_runtime/bsl/preprocessor.py +16 -0
  26. onec_runtime/bsl/semantic_lowering.py +1539 -0
  27. onec_runtime/bsl/source_maps.py +2411 -0
  28. onec_runtime/bsl/worker_dependency_resolver.py +289 -0
  29. onec_runtime/bsl/worker_preprocessor.py +247 -0
  30. onec_runtime/bsl/worker_projection_model.py +137 -0
  31. onec_runtime/bsl/worker_reload_source_map.py +1697 -0
  32. onec_runtime/capture.py +113 -0
  33. onec_runtime/capture_source.py +453 -0
  34. onec_runtime/compact_table.py +376 -0
  35. onec_runtime/compact_table_backend.py +500 -0
  36. onec_runtime/config.py +198 -0
  37. onec_runtime/configurator_agent.py +520 -0
  38. onec_runtime/controller_worker.py +400 -0
  39. onec_runtime/credentials.py +18 -0
  40. onec_runtime/epf_container.py +196 -0
  41. onec_runtime/errors.py +205 -0
  42. onec_runtime/experiment.py +130 -0
  43. onec_runtime/extension_bundle.py +944 -0
  44. onec_runtime/extension_lifecycle.py +402 -0
  45. onec_runtime/extension_state.py +222 -0
  46. onec_runtime/fault_injection.py +30 -0
  47. onec_runtime/kernel.py +179 -0
  48. onec_runtime/lease.py +123 -0
  49. onec_runtime/observation.py +287 -0
  50. onec_runtime/performance_profile.py +245 -0
  51. onec_runtime/privacy.py +263 -0
  52. onec_runtime/processes.py +265 -0
  53. onec_runtime/prototype_runtime.py +2644 -0
  54. onec_runtime/rdbg/__init__.py +1 -0
  55. onec_runtime/rdbg/models.py +130 -0
  56. onec_runtime/rdbg/reconnect.py +59 -0
  57. onec_runtime/rdbg/session.py +913 -0
  58. onec_runtime/rdbg/transport.py +138 -0
  59. onec_runtime/rdbg/xml_codec.py +980 -0
  60. onec_runtime/recovery.py +106 -0
  61. onec_runtime/recovery_journal.py +79 -0
  62. onec_runtime/resources/extension/OnecInteractiveRuntime.cfe +0 -0
  63. onec_runtime/resources/extension/extension-manifest.json +108 -0
  64. onec_runtime/runtime_api.py +5272 -0
  65. onec_runtime/runtime_contracts.py +196 -0
  66. onec_runtime/server_worker.py +2034 -0
  67. onec_runtime/session.py +2123 -0
  68. onec_runtime/startup_diagnostics.py +45 -0
  69. onec_runtime/stop_routing.py +65 -0
  70. onec_runtime/supervised_processes.py +232 -0
  71. onec_runtime/supervised_runtime.py +512 -0
  72. onec_runtime/supervisor.py +363 -0
  73. onec_runtime/supervisor_model.py +148 -0
  74. onec_runtime/supervisor_protocol.py +77 -0
  75. onec_runtime/table_materialization.py +471 -0
  76. onec_runtime/table_transfer_backend.py +203 -0
  77. onec_runtime/table_value.py +311 -0
  78. onec_runtime/toolchain.py +730 -0
  79. onec_runtime/value_materialization.py +432 -0
  80. onec_runtime/value_transfer_backend.py +199 -0
  81. onec_runtime/worker_breakpoints.py +1431 -0
  82. onec_runtime/worker_epf.py +313 -0
  83. onec_runtime/worker_epf_template.py +53 -0
  84. onec_runtime/worker_stage_protocol.py +516 -0
  85. onec_runtime/worker_universe.py +3772 -0
  86. onec_runtime_build/__init__.py +1 -0
  87. 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
+ )