@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.
- package/README.md +224 -0
- package/package.json +17 -0
- package/src/config.ts +140 -0
- package/src/extension.ts +10 -0
- package/src/formation.ts +700 -0
- package/src/hydration.ts +327 -0
- package/src/memory-command.ts +68 -0
- package/src/projection.ts +349 -0
- package/src/supersession.ts +223 -0
- package/worker/__init__.py +1 -0
- package/worker/__main__.py +31 -0
- package/worker/laya_runtime.py +412 -0
- package/worker/protocol.py +141 -0
|
@@ -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)
|