@sovorn/pi-session-memory 0.1.0

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.
@@ -0,0 +1,412 @@
1
+ """Pinned Laya decision adapter used by the JSONL worker."""
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from dataclasses import dataclass
5
+ import hashlib
6
+ import importlib.metadata
7
+ import inspect
8
+ import json
9
+ import math
10
+ import os
11
+ from pathlib import Path
12
+ import resource
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from urllib.parse import unquote, urlparse
17
+ import warnings
18
+
19
+ from worker.protocol import Gate, GateDecision, GateRequest, ProjectionDecision
20
+
21
+
22
+ LAYA_SOURCE_COMMIT = "010bacef009c855ccba814b51f7c8e1d38ab5e3f"
23
+ CHECKPOINT_REVISION = "f9ab0b228f0fc0f14d873dbc99038f135c2da1b2"
24
+ CHECKPOINT_SHA256 = "4fa56de72383a9d3efa9cfa78955733c81b9fc8067a587ca4beb82c78107a24e"
25
+ CHECKPOINT_REPO = "models--convaiinnovations--laya-typed-decisions"
26
+ Digest = Callable[[Path], str]
27
+ GitRunner = Callable[[Path, list[str]], subprocess.CompletedProcess[str]]
28
+ Question = dict[str, str | dict[str, str]]
29
+ MAX_PROJECTION_CANDIDATES = 8
30
+ MAX_PROJECTION_NEED_CHARS = 800
31
+ MAX_CANDIDATE_TEXT_CHARS = 500
32
+ MAX_MEMORY_TEXT_CHARS = 1_000
33
+ MAX_SUPERSESSION_SOURCES = 12
34
+ MAX_ENTRY_ID_CHARS = 256
35
+ MAX_SUPERSESSION_SOURCE_TEXT_CHARS = 5_000
36
+ NO_CANDIDATE = "none"
37
+ QUESTIONS: dict[Gate, Question] = {
38
+ "observation": {
39
+ "type": "noul",
40
+ "instructions": (
41
+ "Does this single session turn contain a durable user decision, constraint, unresolved commitment, "
42
+ "or project fact likely to matter later? Answer yes only for concrete reusable evidence; do not "
43
+ "retain small talk, transient status, or unsupported inference."
44
+ ),
45
+ "criteria": {
46
+ "true": "The turn contains a concrete durable fact, decision, constraint, or commitment worth retaining.",
47
+ "false": "The turn is transient, routine, unsupported, or has no likely future value.",
48
+ },
49
+ },
50
+ "reflection": {
51
+ "type": "noul",
52
+ "instructions": (
53
+ "Do these linked observations together establish a stable recurring decision or insight across turns "
54
+ "that is not already captured by any one observation? Answer yes only when the evidence supports a "
55
+ "cross-turn synthesis; unrelated facts or a single isolated observation are not mature."
56
+ ),
57
+ "criteria": {
58
+ "true": "Multiple observations support a stable, useful cross-turn synthesis.",
59
+ "false": "Evidence is unrelated, isolated, transient, or does not support a distinct synthesis.",
60
+ },
61
+ },
62
+ "resident": {
63
+ "type": "noul",
64
+ "instructions": (
65
+ "Is the already resident, linked session memory sufficient to address this current need without "
66
+ "retrieving another memory entry? Answer yes only when its evidence directly covers the request."
67
+ ),
68
+ "criteria": {
69
+ "true": "The resident source-linked memory directly covers the current need.",
70
+ "false": "The resident memory is insufficient or unrelated to the current need.",
71
+ },
72
+ },
73
+ "supersession": {
74
+ "type": "noul",
75
+ "instructions": (
76
+ "Does the exact linked newer session evidence explicitly establish that the old memory has been "
77
+ "reversed, corrected, or replaced? Do not treat recency, unrelated facts, an additive detail, or "
78
+ "uncertainty as supersession; the newer evidence must actually supersede the old claim."
79
+ ),
80
+ "criteria": {
81
+ "true": "The exact linked newer evidence explicitly contradicts or replaces the old memory; supersession is established.",
82
+ "false": "Supersession is not established: evidence is later only, unrelated, additive, ambiguous, or insufficient.",
83
+ },
84
+ },
85
+ }
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class RuntimeContext:
90
+ python_version: str
91
+ laya_version: str
92
+ laya_source_commit: str
93
+ checkpoint_revision: str
94
+ checkpoint_sha256: str
95
+ device: str
96
+ mps_available: bool
97
+ load_ms: float
98
+ max_rss_bytes: int
99
+ warnings: tuple[str, ...]
100
+
101
+
102
+ def _expanded(value: str, home: Path) -> Path:
103
+ text = os.path.expandvars(value)
104
+ if text == "~" or text.startswith("~/"):
105
+ text = f"{home}{text[1:]}"
106
+ return Path(text)
107
+
108
+
109
+ def _env_text(env: Mapping[str, str], name: str) -> str | None:
110
+ value = env.get(name)
111
+ if not isinstance(value, str) or value.strip() == "":
112
+ return None
113
+ return value
114
+
115
+
116
+ def checkpoint_directory(env: Mapping[str, str] | None = None, home: str | Path | None = None) -> Path:
117
+ if env is None:
118
+ env = os.environ
119
+ root = Path(home).expanduser() if home is not None else Path.home()
120
+ explicit = _env_text(env, "PI_SESSION_MEMORY_LAYA_CHECKPOINT")
121
+ if explicit is not None:
122
+ return _expanded(explicit, root)
123
+ hub = _env_text(env, "HF_HUB_CACHE")
124
+ if hub is None:
125
+ hf_home = _env_text(env, "HF_HOME")
126
+ if hf_home is not None:
127
+ hub_path = _expanded(hf_home, root) / "hub"
128
+ else:
129
+ xdg = _env_text(env, "XDG_CACHE_HOME")
130
+ cache_home = _expanded(xdg, root) if xdg is not None else root / ".cache"
131
+ hub_path = cache_home / "huggingface" / "hub"
132
+ else:
133
+ hub_path = _expanded(hub, root)
134
+ return hub_path / CHECKPOINT_REPO / "snapshots" / CHECKPOINT_REVISION
135
+
136
+
137
+ def _sha256(path: Path) -> str:
138
+ with path.open("rb") as source:
139
+ return hashlib.file_digest(source, "sha256").hexdigest()
140
+
141
+
142
+ def _verified_checkpoint(
143
+ env: Mapping[str, str] | None = None,
144
+ home: str | Path | None = None,
145
+ digest: Digest | None = None,
146
+ ) -> Path:
147
+ checkpoint = checkpoint_directory(env, home).resolve()
148
+ if checkpoint.name != CHECKPOINT_REVISION:
149
+ raise RuntimeError("checkpoint revision mismatch")
150
+ weights = checkpoint / "model.safetensors"
151
+ if not weights.is_file():
152
+ raise RuntimeError(f"checkpoint weights missing: {weights}")
153
+ if (digest or _sha256)(weights) != CHECKPOINT_SHA256:
154
+ raise RuntimeError("checkpoint weights mismatch")
155
+ return checkpoint
156
+
157
+
158
+ def _pinned_commit(commit_id: object) -> bool:
159
+ if not isinstance(commit_id, str) or len(commit_id) != 40:
160
+ return False
161
+ if any(character not in "0123456789abcdefABCDEF" for character in commit_id):
162
+ return False
163
+ return commit_id.lower() == LAYA_SOURCE_COMMIT
164
+
165
+
166
+ def _accept_vcs(vcs_info: object) -> None:
167
+ if not isinstance(vcs_info, dict) or vcs_info.get("vcs") != "git":
168
+ raise RuntimeError("Laya source is not the pinned local checkout")
169
+ commit_id = vcs_info.get("commit_id") if "commit_id" in vcs_info else None
170
+ if commit_id is None:
171
+ raise RuntimeError("Laya source is not the pinned local checkout")
172
+ if not _pinned_commit(commit_id):
173
+ raise RuntimeError("Laya source commit mismatch")
174
+
175
+
176
+ def _verified_source(
177
+ distribution: importlib.metadata.Distribution,
178
+ loaded_agent_path: Path,
179
+ git_runner: GitRunner | None = None,
180
+ ) -> None:
181
+ metadata_text = distribution.read_text("direct_url.json")
182
+ if metadata_text is None:
183
+ raise RuntimeError("Laya source origin is unavailable")
184
+ try:
185
+ payload = json.loads(metadata_text)
186
+ except json.JSONDecodeError:
187
+ raise RuntimeError("Laya source origin is unavailable") from None
188
+ if not isinstance(payload, dict):
189
+ raise RuntimeError("Laya source is not the pinned local checkout")
190
+ if "vcs_info" in payload and payload.get("vcs_info") is not None:
191
+ _accept_vcs(payload.get("vcs_info"))
192
+ return
193
+ source_url = payload.get("url")
194
+ parsed_url = urlparse(source_url) if isinstance(source_url, str) else None
195
+ if parsed_url is None or parsed_url.scheme != "file":
196
+ raise RuntimeError("Laya source is not the pinned local checkout")
197
+ source_root = Path(unquote(parsed_url.path)).resolve()
198
+
199
+ if git_runner is None:
200
+ commit = subprocess.run(
201
+ ["git", "-C", str(source_root), "rev-parse", "HEAD"],
202
+ check=True,
203
+ capture_output=True,
204
+ text=True,
205
+ ).stdout.strip()
206
+ clean = subprocess.run(
207
+ ["git", "-C", str(source_root), "diff", "--quiet", "HEAD", "--", "laya"],
208
+ check=False,
209
+ capture_output=True,
210
+ )
211
+ else:
212
+ commit = git_runner(source_root, ["rev-parse", "HEAD"]).stdout.strip()
213
+ clean = git_runner(source_root, ["diff", "--quiet", "HEAD", "--", "laya"])
214
+ if commit != LAYA_SOURCE_COMMIT:
215
+ raise RuntimeError("Laya source commit mismatch")
216
+ if clean.returncode != 0:
217
+ raise RuntimeError("Laya source checkout is modified")
218
+
219
+ installed_root = loaded_agent_path.parent
220
+ for filename in ("__init__.py", "agent.py", "common.py"):
221
+ if (installed_root / filename).read_bytes() != (source_root / "laya" / filename).read_bytes():
222
+ raise RuntimeError("installed Laya code differs from the pinned source")
223
+
224
+
225
+ def _probability(value: object) -> float:
226
+ if isinstance(value, bool) or not isinstance(value, (float, int)):
227
+ raise ValueError("Laya returned a non-numeric probability")
228
+ result = float(value)
229
+ if not math.isfinite(result) or not 0.0 <= result <= 1.0:
230
+ raise ValueError("Laya returned an invalid probability")
231
+ return result
232
+
233
+
234
+ def _projection_state(state: str) -> tuple[str, list[tuple[str, str, str]]]:
235
+ try:
236
+ value: object = json.loads(state)
237
+ except json.JSONDecodeError:
238
+ raise ValueError("projection state is invalid JSON") from None
239
+ if not isinstance(value, dict) or set(value) != {"need", "candidates"}:
240
+ raise ValueError("projection state fields are invalid")
241
+ need = value["need"]
242
+ candidates = value["candidates"]
243
+ if not isinstance(need, str) or not need or len(need) > MAX_PROJECTION_NEED_CHARS:
244
+ raise ValueError("projection need is invalid")
245
+ if not isinstance(candidates, list) or not candidates or len(candidates) > MAX_PROJECTION_CANDIDATES:
246
+ raise ValueError("projection candidates are empty or exceed the bound")
247
+
248
+ parsed: list[tuple[str, str, str]] = []
249
+ seen: set[str] = set()
250
+ for candidate in candidates:
251
+ if not isinstance(candidate, dict) or set(candidate) != {"entryId", "kind", "text"}:
252
+ raise ValueError("projection candidate fields are invalid")
253
+ entry_id = candidate["entryId"]
254
+ kind = candidate["kind"]
255
+ text = candidate["text"]
256
+ if (
257
+ not isinstance(entry_id, str)
258
+ or not entry_id
259
+ or len(entry_id) > 256
260
+ or entry_id in seen
261
+ or kind not in ("observation", "reflection")
262
+ or not isinstance(text, str)
263
+ or not text
264
+ or len(text) > MAX_CANDIDATE_TEXT_CHARS
265
+ ):
266
+ raise ValueError("projection candidate is invalid")
267
+ seen.add(entry_id)
268
+ parsed.append((entry_id, kind, text))
269
+ return need, parsed
270
+
271
+
272
+ def _supersession_state(state: str) -> None:
273
+ if len(state.encode("utf-8")) > 8_192:
274
+ raise ValueError("supersession state exceeds its byte bound")
275
+ try:
276
+ value: object = json.loads(state)
277
+ except json.JSONDecodeError:
278
+ raise ValueError("supersession state is invalid JSON") from None
279
+ if not isinstance(value, dict) or set(value) != {"oldMemory", "newEvidence"}:
280
+ raise ValueError("supersession state fields are invalid")
281
+ old_memory = value["oldMemory"]
282
+ evidence = value["newEvidence"]
283
+ if not isinstance(old_memory, dict) or set(old_memory) != {"entryId", "kind", "text"}:
284
+ raise ValueError("supersession old memory fields are invalid")
285
+ if (
286
+ not _valid_entry_id(old_memory["entryId"])
287
+ or old_memory["kind"] not in ("observation", "reflection")
288
+ or not _valid_memory_text(old_memory["text"])
289
+ ):
290
+ raise ValueError("supersession old memory is invalid")
291
+ if not isinstance(evidence, dict) or set(evidence) != {"entryId", "text", "sourceEntryIds", "sources"}:
292
+ raise ValueError("supersession newer evidence fields are invalid")
293
+ source_ids = evidence["sourceEntryIds"]
294
+ sources = evidence["sources"]
295
+ if (
296
+ not _valid_entry_id(evidence["entryId"])
297
+ or evidence["entryId"] == old_memory["entryId"]
298
+ or not _valid_memory_text(evidence["text"])
299
+ or not isinstance(source_ids, list)
300
+ or not source_ids
301
+ or len(source_ids) > MAX_SUPERSESSION_SOURCES
302
+ or not all(_valid_entry_id(entry_id) for entry_id in source_ids)
303
+ or len(set(source_ids)) != len(source_ids)
304
+ or evidence["entryId"] in source_ids
305
+ or old_memory["entryId"] in source_ids
306
+ or not isinstance(sources, list)
307
+ or len(sources) != len(source_ids)
308
+ ):
309
+ raise ValueError("supersession newer evidence is invalid")
310
+ for expected_id, source in zip(source_ids, sources, strict=True):
311
+ if (
312
+ not isinstance(source, dict)
313
+ or set(source) != {"entryId", "role", "text"}
314
+ or source["entryId"] != expected_id
315
+ or source["role"] not in ("user", "assistant", "toolResult")
316
+ or not isinstance(source["text"], str)
317
+ or len(source["text"]) > MAX_SUPERSESSION_SOURCE_TEXT_CHARS
318
+ ):
319
+ raise ValueError("supersession source link is invalid")
320
+
321
+
322
+ def _valid_entry_id(value: object) -> bool:
323
+ return isinstance(value, str) and 0 < len(value) <= MAX_ENTRY_ID_CHARS
324
+
325
+
326
+ def _valid_memory_text(value: object) -> bool:
327
+ return isinstance(value, str) and 0 < len(value) <= MAX_MEMORY_TEXT_CHARS
328
+
329
+
330
+ class LayaEvaluator:
331
+ def __init__(self) -> None:
332
+ from laya.agent import Agent
333
+ import torch
334
+
335
+ distribution = importlib.metadata.distribution("laya")
336
+ if distribution.version != "0.3.7":
337
+ raise RuntimeError("Laya distribution version mismatch")
338
+ _verified_source(distribution, Path(inspect.getfile(Agent)).resolve())
339
+ checkpoint = _verified_checkpoint()
340
+
341
+ start = time.perf_counter()
342
+ with warnings.catch_warnings(record=True) as captured_warnings:
343
+ warnings.simplefilter("always")
344
+ self.agent = Agent(str(checkpoint))
345
+ load_ms = (time.perf_counter() - start) * 1000.0
346
+ max_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
347
+ max_rss_bytes = int(max_rss if sys.platform == "darwin" else max_rss * 1024)
348
+ mps_available = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
349
+ self.context = RuntimeContext(
350
+ python_version=sys.version.split()[0],
351
+ laya_version=distribution.version,
352
+ laya_source_commit=LAYA_SOURCE_COMMIT,
353
+ checkpoint_revision=CHECKPOINT_REVISION,
354
+ checkpoint_sha256=CHECKPOINT_SHA256,
355
+ device=str(self.agent.device),
356
+ mps_available=bool(mps_available),
357
+ load_ms=round(load_ms, 2),
358
+ max_rss_bytes=max_rss_bytes,
359
+ warnings=tuple(str(warning.message) for warning in captured_warnings),
360
+ )
361
+
362
+ def evaluate(self, request: GateRequest) -> GateDecision | ProjectionDecision:
363
+ if request.gate == "projection":
364
+ return self._select_projection(request)
365
+ if request.gate == "supersession":
366
+ _supersession_state(request.state)
367
+ result = self.agent.system_one(
368
+ {"gate": request.gate, "evidence": request.state},
369
+ {"warranted": QUESTIONS[request.gate]},
370
+ )
371
+ answers = result.get("answers")
372
+ if not isinstance(answers, dict):
373
+ raise ValueError("Laya returned no typed answers")
374
+ answer = answers.get("warranted")
375
+ if not isinstance(answer, dict) or answer.get("type") != "noul":
376
+ raise ValueError("Laya returned an unexpected decision type")
377
+ p_true = _probability(answer.get("noul"))
378
+ confidence = _probability(answer.get("confidence"))
379
+ return GateDecision(accepted=p_true >= 0.5, p_true=round(p_true, 4), confidence=round(confidence, 4))
380
+
381
+ def _select_projection(self, request: GateRequest) -> ProjectionDecision:
382
+ need, candidates = _projection_state(request.state)
383
+ labels = {f"candidate_{index}": f"{kind}: {text}" for index, (_, kind, text) in enumerate(candidates)}
384
+ labels[NO_CANDIDATE] = "None of these source-linked memories is relevant to the current need."
385
+ result = self.agent.system_one(
386
+ {"gate": request.gate, "current_need": need},
387
+ {
388
+ "selected": {
389
+ "type": "choice",
390
+ "instructions": "Which one bounded memory candidate is most relevant to the current need? Select none if no candidate is relevant.",
391
+ "criteria": labels,
392
+ },
393
+ },
394
+ )
395
+ answers = result.get("answers")
396
+ if not isinstance(answers, dict):
397
+ raise ValueError("Laya returned no typed selection")
398
+ answer = answers.get("selected")
399
+ if not isinstance(answer, dict) or answer.get("type") != "choice":
400
+ raise ValueError("Laya returned an unexpected selection type")
401
+ selected = answer.get("choice")
402
+ if selected == NO_CANDIDATE:
403
+ return ProjectionDecision(selected_entry_id=None)
404
+ if not isinstance(selected, str) or not selected.startswith("candidate_"):
405
+ raise ValueError("Laya returned an unknown selection")
406
+ try:
407
+ index = int(selected.removeprefix("candidate_"))
408
+ except ValueError:
409
+ raise ValueError("Laya returned an unknown selection") from None
410
+ if index < 0 or index >= len(candidates) or selected != f"candidate_{index}":
411
+ raise ValueError("Laya returned an unknown selection")
412
+ return ProjectionDecision(selected_entry_id=candidates[index][0])
@@ -0,0 +1,141 @@
1
+ """Bounded JSONL protocol for the pinned, session-local Laya gate worker."""
2
+
3
+ from contextlib import redirect_stdout
4
+ from dataclasses import asdict, dataclass
5
+ import json
6
+ import re
7
+ from typing import BinaryIO, Callable, Literal, TextIO, TypeAlias
8
+
9
+
10
+ Gate: TypeAlias = Literal["observation", "reflection", "resident", "projection", "supersession"]
11
+ ErrorCode: TypeAlias = Literal["invalid_json", "invalid_request", "request_too_large", "laya_error"]
12
+ PROTOCOL_VERSION = 1
13
+ MAX_REQUEST_BYTES = 16_384
14
+ MAX_STATE_BYTES = 8_192
15
+ MAX_RESPONSE_BYTES = 2_048
16
+ _REQUEST_ID = re.compile(r"[A-Za-z0-9._:-]{1,64}\Z")
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class GateRequest:
21
+ request_id: str
22
+ gate: Gate
23
+ state: str
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class GateDecision:
28
+ accepted: bool
29
+ p_true: float
30
+ confidence: float
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ProjectionDecision:
35
+ selected_entry_id: str | None
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ProtocolError(Exception):
40
+ code: ErrorCode
41
+
42
+
43
+ def parse_request(line: bytes) -> GateRequest:
44
+ try:
45
+ payload: object = json.loads(line.decode("utf-8"))
46
+ except (UnicodeDecodeError, json.JSONDecodeError):
47
+ raise ProtocolError("invalid_json") from None
48
+
49
+ if not isinstance(payload, dict):
50
+ raise ProtocolError("invalid_request")
51
+
52
+ request_id = payload.get("request_id")
53
+ if not isinstance(request_id, str) or not _REQUEST_ID.fullmatch(request_id):
54
+ raise ProtocolError("invalid_request")
55
+ if set(payload) != {"protocol_version", "request_id", "gate", "state"}:
56
+ raise ProtocolError("invalid_request")
57
+ if type(payload["protocol_version"]) is not int or payload["protocol_version"] != PROTOCOL_VERSION:
58
+ raise ProtocolError("invalid_request")
59
+
60
+ gate = payload["gate"]
61
+ if gate == "observation":
62
+ typed_gate: Gate = "observation"
63
+ elif gate == "reflection":
64
+ typed_gate = "reflection"
65
+ elif gate == "resident":
66
+ typed_gate = "resident"
67
+ elif gate == "projection":
68
+ typed_gate = "projection"
69
+ elif gate == "supersession":
70
+ typed_gate = "supersession"
71
+ else:
72
+ raise ProtocolError("invalid_request")
73
+ state = payload["state"]
74
+ if not isinstance(state, str) or not state or len(state.encode("utf-8")) > MAX_STATE_BYTES:
75
+ raise ProtocolError("invalid_request")
76
+ return GateRequest(request_id=request_id, gate=typed_gate, state=state)
77
+
78
+
79
+ def _error_response(code: ErrorCode, request_id: str | None = None) -> dict[str, object]:
80
+ messages = {
81
+ "invalid_json": "Request is not valid UTF-8 JSON.",
82
+ "invalid_request": "Request fields do not match protocol version 1.",
83
+ "request_too_large": "Request exceeds the configured byte limit.",
84
+ "laya_error": "The Laya decision could not be trusted.",
85
+ }
86
+ return {
87
+ "protocol_version": PROTOCOL_VERSION,
88
+ "request_id": request_id,
89
+ "status": "error",
90
+ "error": {"code": code, "message": messages[code]},
91
+ }
92
+
93
+
94
+ def _write_response(output: BinaryIO, payload: dict[str, object]) -> None:
95
+ encoded = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8") + b"\n"
96
+ if len(encoded) > MAX_RESPONSE_BYTES:
97
+ encoded = json.dumps(_error_response("laya_error"), separators=(",", ":")).encode("utf-8") + b"\n"
98
+ output.write(encoded)
99
+ output.flush()
100
+
101
+
102
+ def _discard_line_remainder(input_stream: BinaryIO) -> None:
103
+ while True:
104
+ remainder = input_stream.readline(MAX_REQUEST_BYTES + 1)
105
+ if not remainder or remainder.endswith(b"\n"):
106
+ return
107
+
108
+
109
+ def serve(
110
+ input_stream: BinaryIO,
111
+ output_stream: BinaryIO,
112
+ diagnostics: TextIO,
113
+ evaluate: Callable[[GateRequest], GateDecision | ProjectionDecision],
114
+ ) -> None:
115
+ while line := input_stream.readline(MAX_REQUEST_BYTES + 1):
116
+ if len(line) > MAX_REQUEST_BYTES:
117
+ if not line.endswith(b"\n"):
118
+ _discard_line_remainder(input_stream)
119
+ _write_response(output_stream, _error_response("request_too_large"))
120
+ continue
121
+
122
+ try:
123
+ request = parse_request(line.rstrip(b"\r\n"))
124
+ except ProtocolError as error:
125
+ _write_response(output_stream, _error_response(error.code))
126
+ continue
127
+
128
+ try:
129
+ with redirect_stdout(diagnostics):
130
+ decision = evaluate(request)
131
+ response = {
132
+ "protocol_version": PROTOCOL_VERSION,
133
+ "request_id": request.request_id,
134
+ "gate": request.gate,
135
+ "status": "ok",
136
+ "decision": asdict(decision),
137
+ }
138
+ except Exception as error:
139
+ diagnostics.write(f"Laya gate failed ({type(error).__name__}).\n")
140
+ response = _error_response("laya_error", request.request_id)
141
+ _write_response(output_stream, response)