awpredict 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
awpredict/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """awpredict — a small, dependency-light world-model package (JEPA + MLP engines).
2
+
3
+ One package, two engines, N environment adapters:
4
+
5
+ * ``awpredict.core.lewm.LeWorldModel`` — the LeWM-style JEPA
6
+ (two-term loss: next-latent MSE + SIGReg; CEM planner), used as the
7
+ world model behind an ARC-AGI-3 solving agent. Includes an optional
8
+ value head (``_ValueHead``/``_FsAdapter``/``value()``/``train_value_step``)
9
+ for value-guided CEM planning — a frozen-latent predictor head trained
10
+ on returns, off by default, on by constructing with a value config. This
11
+ is the same engine the solving agent runs, not a cut-down demo of it —
12
+ the goal of this package is to give you everything needed to bootstrap
13
+ your own world model, not a subset of it.
14
+ * ``awpredict.core.mlp.MLPWorldModel`` — an embedding-MLP transition
15
+ model (tabular → hybrid → neural).
16
+
17
+ Contracts live in ``awpredict.contracts`` (WorldModel, EnvironmentAdapter).
18
+ Torch and numpy are OPTIONAL at import time — engines degrade loudly
19
+ (ok == False), never raise into a caller.
20
+ """
21
+
22
+ from awpredict.contracts import EnvironmentAdapter, WorldModel, conforms
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ __all__ = ["EnvironmentAdapter", "WorldModel", "conforms", "__version__"]
awpredict/_doctor.py ADDED
@@ -0,0 +1,139 @@
1
+ """Stack-aware `doctor` for awpredict.
2
+
3
+ GENERATED BY gen_aw_doctor.py -- DO NOT EDIT.
4
+ Regenerate it with the generator named above; a hand-edit here is reverted by
5
+ the next run and fails the parity gate.
6
+
7
+ Why a doctor exists at all: the aw* bricks are designed to COMPOSE, so the
8
+ interesting failures live BETWEEN them. "awpredict is installed" is not the useful
9
+ fact -- "awpredict is installed and the thing it pairs with is not" is. This reports
10
+ the whole stack, not just itself.
11
+
12
+ stdlib only, on purpose: a diagnostic that cannot run because a dependency is
13
+ missing is worthless precisely when you need it.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import os
19
+ import shutil
20
+ import sys
21
+
22
+ #: Frozen from AitherOS/config/ecosystem.yaml at generation time. A shipped
23
+ #: package cannot read the registry, and a doctor that guessed at the family
24
+ #: would go stale in silence. Regenerate to update.
25
+ SELF = 'awpredict'
26
+ FAMILY = ['awask', 'awask', 'awbac', 'awbrowse', 'awdit', 'awevolve', 'awevolve', 'awfind', 'awgit', 'awgraph', 'awiam', 'awkno', 'awm', 'awmail', 'awnboard', 'awnest', 'awnet', 'awnode', 'awprism', 'awreason', 'awrecover', 'awrecurse', 'awrelay', 'awrepl', 'awresearch', 'awrun', 'awseal', 'awshare', 'awtunnel']
27
+ PAIRS_WITH = ['awm']
28
+
29
+ #: This brick's OWN config, read out of its source at generation time.
30
+ #: ENV_REQUIRED is `os.environ["X"]` -- absent, that is a KeyError the moment
31
+ #: the line runs. ENV_OPTIONAL is `os.getenv("X")`, which returns None and lets
32
+ #: the caller cope. Only this brick's namespace is listed: reporting the
33
+ #: platform-wide vars it also touches would be noise, and a doctor that floods
34
+ #: gets ignored.
35
+ ENV_REQUIRED = []
36
+ ENV_OPTIONAL = []
37
+
38
+
39
+ def _installed(mod: str) -> "str | None":
40
+ """Version if importable, else None. Never raises -- a broken sibling must
41
+ not take the diagnostic down with it."""
42
+ try:
43
+ if importlib.util.find_spec(mod) is None:
44
+ return None
45
+ except (ImportError, ValueError):
46
+ return None
47
+ try:
48
+ from importlib.metadata import PackageNotFoundError, version
49
+ try:
50
+ return version(mod)
51
+ except PackageNotFoundError:
52
+ return "installed"
53
+ except Exception:
54
+ return "installed"
55
+
56
+
57
+ def report(out=None) -> int:
58
+ """Print the stack picture. 0 = this brick and its pairs are present."""
59
+ out = out or sys.stdout
60
+ print(f"{SELF} doctor", file=out)
61
+
62
+ mine = _installed(SELF)
63
+ print(f" self {SELF} {mine or 'NOT IMPORTABLE'}", file=out)
64
+ shim = shutil.which(SELF)
65
+ print(f" command {shim or 'not on PATH'}", file=out)
66
+
67
+ # The stack. Siblings this brick pairs with are called out separately,
68
+ # because a missing pair is a REASON, while a missing unrelated brick is
69
+ # just a fact about your machine.
70
+ missing_pairs, present = [], []
71
+ for name in FAMILY:
72
+ v = _installed(name)
73
+ if v:
74
+ present.append(name)
75
+ elif name in PAIRS_WITH:
76
+ missing_pairs.append(name)
77
+ print(f" stack {len(present)}/{len(FAMILY)} aw* packages installed",
78
+ file=out)
79
+ if present:
80
+ print(f" {' '.join(sorted(present))}", file=out)
81
+
82
+ missing_req = [v for v in ENV_REQUIRED if not os.environ.get(v)]
83
+ if ENV_REQUIRED or ENV_OPTIONAL:
84
+ have = sum(1 for v in ENV_REQUIRED + ENV_OPTIONAL if os.environ.get(v))
85
+ total = len(ENV_REQUIRED) + len(ENV_OPTIONAL)
86
+ print(f" config {have}/{total} of this brick's own vars set", file=out)
87
+ if missing_req:
88
+ # Not a preference. os.environ[...] raises the moment it runs.
89
+ print(f" MISSING REQUIRED: {' '.join(missing_req)}", file=out)
90
+
91
+ local = _local_checks()
92
+ for line in local:
93
+ print(f" {line}", file=out)
94
+
95
+ if mine is None:
96
+ print(f"\nverdict: {SELF} itself is not importable. Reinstall it before "
97
+ f"anything else here means much.", file=out)
98
+ return 1
99
+ if missing_req:
100
+ print(f"\nverdict: {SELF} is missing required config "
101
+ f"({', '.join(missing_req)}). Those are read with os.environ[...], "
102
+ f"so the code path that needs them raises rather than degrades.",
103
+ file=out)
104
+ return 1
105
+ if missing_pairs:
106
+ print(f"\nverdict: {SELF} works, but pairs with "
107
+ f"{', '.join(sorted(missing_pairs))} which "
108
+ f"{'is' if len(missing_pairs) == 1 else 'are'} not installed. "
109
+ f"That is a capability you are missing, not an error.", file=out)
110
+ return 0
111
+ print(f"\nverdict: {SELF} and everything it pairs with are present.", file=out)
112
+ return 0
113
+
114
+
115
+ def _local_checks() -> "list[str]":
116
+ """Per-brick checks, if this package defines them.
117
+
118
+ Kept as a HOOK rather than generated guesses: the generator knows the family
119
+ from the registry, but it does not know what awpredict needs at runtime, and a
120
+ doctor that invented config requirements would be confidently wrong. A
121
+ package supplies `_doctor_local()` returning display lines; absent, the
122
+ stack picture above still stands on its own.
123
+ """
124
+ try:
125
+ mod = importlib.import_module(f"{SELF}.doctor_local")
126
+ except Exception:
127
+ return []
128
+ try:
129
+ return list(mod._doctor_local())
130
+ except Exception as exc: # noqa: BLE001
131
+ return [f"local checks raised {type(exc).__name__}: {exc}"]
132
+
133
+
134
+ def main(argv: "list[str] | None" = None) -> int:
135
+ return report()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ """Environment adapters (one per domain). Concrete adapters land per-slice:
2
+ arc_game (slice 1+), code_world (slice 2), adk_sandbox (slice 3),
3
+ gym_compat (slice 3 test harness)."""
@@ -0,0 +1,360 @@
1
+ """
2
+ CodeWorld Environment Adapter
3
+ ==============================
4
+
5
+ Maps a codebase environment into the WorldModel's observation/action space.
6
+
7
+ The adapter observes landmarks (from a code-landmark indexer) and code chunks
8
+ (from a code graph), encodes them into a fixed 768-dim embedding, and exposes
9
+ a vocabulary of focus actions (e.g. "focus on landmark X", "traverse to chunk Y").
10
+
11
+ Step execution is a loud NotImplementedError: code-world stepping (editing,
12
+ running code) is future work; this adapter is observe/rank-only today.
13
+
14
+ CONTRACT:
15
+ Conforms to awpredict.contracts.EnvironmentAdapter — stdlib + optional
16
+ numpy only, no dependency on any host framework.
17
+
18
+ USAGE:
19
+ adapter = CodeWorldAdapter()
20
+ obs = adapter.observe(env_state) # env_state: {"landmarks": [...], "chunks": [...]}
21
+ actions = adapter.actions() # List of action ids: "focus_landmark_X", "traverse_chunk_Y"
22
+ before, after = adapter.observe(old_state), adapter.observe(new_state)
23
+ sig = adapter.transition_signature(old_state, action, new_state)
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import hashlib
29
+ import logging
30
+ import math
31
+ import re
32
+ from dataclasses import dataclass
33
+ from typing import Any, Dict, List, Optional, Sequence, Tuple
34
+
35
+ logger = logging.getLogger("code_world_adapter")
36
+
37
+ # Constants from mlp.py (synchronized)
38
+ _STATE_DIM = 768
39
+ _ACTION_DIM = 128
40
+
41
+
42
+ # ============================================================================
43
+ # STATE EMBEDDING (shared idiom with mlp.py)
44
+ # ============================================================================
45
+
46
+ def _hash_state_embedding(text: str) -> List[float]:
47
+ """Deterministic hash-based pseudo-embedding for a state.
48
+
49
+ Uses the same SHA256→float scaling as MLPWorldModel._hash_state_embedding
50
+ to ensure embedding parity across adapters and engines. This is NOT a learned
51
+ embedding; it is a deterministic hash that:
52
+ - Produces the same output for the same input (repeatable)
53
+ - Distributes uniformly across [-1, 1] without learned structure
54
+ - Never requires training or model parameters
55
+
56
+ The MLPWorldModel learns on top of these embeddings, so a domain-specific
57
+ learned encoder (e.g., code-specific embeddings) would improve prediction
58
+ fidelity, but this default hash-based path is domain-agnostic and auditable.
59
+ """
60
+ h = hashlib.sha256((text or "empty").encode("utf-8")).digest()
61
+ result = []
62
+ for i in range(_STATE_DIM):
63
+ byte_val = h[i % len(h)]
64
+ result.append((byte_val / 127.5) - 1.0)
65
+ return result
66
+
67
+
68
+ # ============================================================================
69
+ # STATE DESCRIPTION (deterministic extraction for embedding)
70
+ # ============================================================================
71
+
72
+ def _landmark_descriptor(landmark: Dict[str, Any]) -> str:
73
+ """Extract a deterministic text descriptor from a landmark dict.
74
+
75
+ Landmark shape (from prospector LandmarkNode):
76
+ {
77
+ "id": str,
78
+ "name": str,
79
+ "rank": int,
80
+ "purpose": str,
81
+ "files": List[str],
82
+ "tools": List[str],
83
+ "skills": List[str],
84
+ "intent_hints": Dict[str, float],
85
+ }
86
+
87
+ Returns a compact, normalized string for hashing.
88
+ """
89
+ name = str(landmark.get("name", "")).strip()
90
+ purpose = str(landmark.get("purpose", "")).strip()
91
+ rank = int(landmark.get("rank", 0) or 0)
92
+ files_count = len(landmark.get("files", []) or [])
93
+
94
+ # Deterministic: rank and file count are normalized into buckets to reduce
95
+ # spurious changes on small variations (e.g. rank 42 vs 43).
96
+ rank_bucket = (rank // 10) * 10 # 0-9→0, 10-19→10, etc.
97
+ file_bucket = (files_count // 5) * 5
98
+
99
+ parts = [name, purpose, str(rank_bucket), str(file_bucket)]
100
+ return " | ".join(p for p in parts if p)
101
+
102
+
103
+ def _feature_hash_embedding(features: List[str]) -> List[float]:
104
+ """Signed feature hashing: each feature adds ±1 to one of 768 buckets.
105
+
106
+ Unlike the whole-string hash above, this is SMOOTH: states sharing features
107
+ (call edges, name tokens) share embedding components, so a rename (few
108
+ features change) stays close while a call-graph change (edge features
109
+ change) moves away. That property is what lets a world model GRADE
110
+ surprise on code transitions instead of answering only same/different —
111
+ measured 2026-08-01: the whole-string variant scored a logic change as
112
+ IDENTICAL (call edges were reduced to a count bucket) and a rename as
113
+ unrelated, i.e. exactly backwards.
114
+ """
115
+ vec = [0.0] * _STATE_DIM
116
+ for feat in features:
117
+ h = hashlib.sha256(feat.encode("utf-8", "replace")).digest()
118
+ idx = int.from_bytes(h[:4], "big") % _STATE_DIM
119
+ sign = 1.0 if h[4] % 2 == 0 else -1.0
120
+ vec[idx] += sign
121
+ norm = math.sqrt(sum(x * x for x in vec))
122
+ if norm > 0:
123
+ vec = [x / norm for x in vec]
124
+ return vec
125
+
126
+
127
+ def _name_tokens(name: str) -> List[str]:
128
+ """snake_case + camelCase token split, lowercased."""
129
+ parts = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name or "").replace("_", " ")
130
+ return [t.lower() for t in parts.split() if t]
131
+
132
+
133
+ def _landmark_features(landmark: Dict[str, Any]) -> List[str]:
134
+ feats = [f"lm-name:{t}" for t in _name_tokens(str(landmark.get("name", "")))]
135
+ for tok in _name_tokens(str(landmark.get("purpose", "")))[:24]:
136
+ feats.append(f"lm-purpose:{tok}")
137
+ rank = int(landmark.get("rank", 0) or 0)
138
+ feats.append(f"lm-rank:{(rank // 10) * 10}")
139
+ for f in sorted(landmark.get("files", []) or [])[:32]:
140
+ feats.append(f"lm-file:{f}")
141
+ return feats
142
+
143
+
144
+ def _chunk_features(chunk: Dict[str, Any]) -> List[str]:
145
+ feats = [f"ch-name:{t}" for t in _name_tokens(str(chunk.get("name", "")))]
146
+ feats.append(f"ch-type:{chunk.get('chunk_type', '')}")
147
+ complexity = int(chunk.get("complexity", 0) or 0)
148
+ feats.append(f"ch-cx:{(complexity // 5) * 5}")
149
+ # EVERY edge is a feature — the call graph is the semantics of the chunk.
150
+ for callee in sorted(chunk.get("calls", []) or [])[:64]:
151
+ feats.append(f"ch-call:{callee}")
152
+ for caller in sorted(chunk.get("called_by", []) or [])[:64]:
153
+ feats.append(f"ch-caller:{caller}")
154
+ return feats
155
+
156
+
157
+ def _state_features(env_state: Dict[str, Any]) -> List[str]:
158
+ feats: List[str] = []
159
+ for lm in env_state.get("landmarks", []) or []:
160
+ if isinstance(lm, dict):
161
+ feats.extend(_landmark_features(lm))
162
+ for ch in env_state.get("chunks", []) or []:
163
+ if isinstance(ch, dict):
164
+ feats.extend(_chunk_features(ch))
165
+ return feats
166
+
167
+
168
+ def _chunk_descriptor(chunk: Dict[str, Any]) -> str:
169
+ """Extract a deterministic text descriptor from a code chunk dict.
170
+
171
+ Chunk shape (CodeGraph CodeChunk):
172
+ {
173
+ "name": str,
174
+ "chunk_type": str,
175
+ "calls": List[str],
176
+ "called_by": List[str],
177
+ "complexity": int,
178
+ ...
179
+ }
180
+
181
+ Returns a compact, normalized string for hashing.
182
+ """
183
+ name = str(chunk.get("name", "")).strip()
184
+ chunk_type = str(chunk.get("chunk_type", "")).strip()
185
+ complexity = int(chunk.get("complexity", 0) or 0)
186
+ calls_count = len(chunk.get("calls", []) or [])
187
+ called_by_count = len(chunk.get("called_by", []) or [])
188
+
189
+ complexity_bucket = (complexity // 5) * 5 # Normalize to 0, 5, 10, ...
190
+ call_bucket = (calls_count // 3) * 3
191
+ caller_bucket = (called_by_count // 3) * 3
192
+
193
+ parts = [name, chunk_type, str(complexity_bucket), str(call_bucket), str(caller_bucket)]
194
+ return " | ".join(p for p in parts if p)
195
+
196
+
197
+ def _state_description(env_state: Dict[str, Any]) -> str:
198
+ """Convert environment state to a deterministic description string.
199
+
200
+ Combines landmark and chunk descriptors into one hashable text. The state
201
+ description is fed to _hash_state_embedding to produce the 768-dim obs vector.
202
+
203
+ Determinism rule: same state always produces the same description, regardless
204
+ of ordering within lists (we sort).
205
+ """
206
+ landmarks = env_state.get("landmarks", []) or []
207
+ chunks = env_state.get("chunks", []) or []
208
+
209
+ landmark_descs = sorted(
210
+ _landmark_descriptor(lm) for lm in landmarks if isinstance(lm, dict)
211
+ )
212
+ chunk_descs = sorted(
213
+ _chunk_descriptor(ch) for ch in chunks if isinstance(ch, dict)
214
+ )
215
+
216
+ # Sort before joining to make the description independent of input order
217
+ all_descs = landmark_descs + chunk_descs
218
+ if not all_descs:
219
+ return "empty_state"
220
+
221
+ return " ; ".join(all_descs)
222
+
223
+
224
+ # ============================================================================
225
+ # ACTION VOCABULARY
226
+ # ============================================================================
227
+
228
+ @dataclass
229
+ class CodeWorldAction:
230
+ """A discrete action in code-world navigation."""
231
+ action_id: str # "focus_landmark_<id>", "traverse_chunk_<id>"
232
+ action_type: str # "focus_landmark", "traverse_chunk"
233
+ target_id: str # landmark id or chunk id
234
+ target_name: str # human-readable name
235
+
236
+
237
+ class CodeWorldAdapter:
238
+ """EnvironmentAdapter for code-world environments.
239
+
240
+ Maps landmarks and chunks into observation space and exposes a discrete
241
+ action vocabulary. Stepping is a loud NotImplementedError: editing/running
242
+ code arrives with the ADK sandbox pack.
243
+
244
+ Attributes:
245
+ domain: "code"
246
+ _last_env_state: Cached last observed state (for actions() to be context-aware)
247
+ _last_obs: Cached last observation vector
248
+ """
249
+
250
+ domain = "code"
251
+
252
+ def __init__(self) -> None:
253
+ self._last_env_state: Optional[Dict[str, Any]] = None
254
+ self._last_obs: Optional[List[float]] = None
255
+
256
+ def observe(self, env_state: Dict[str, Any]) -> List[float]:
257
+ """Convert raw environment state into the observation vector.
258
+
259
+ Args:
260
+ env_state: dict with optional "landmarks" and "chunks" keys
261
+ landmarks: List[{id, name, purpose, rank, files, ...}]
262
+ chunks: List[{name, chunk_type, calls, called_by, complexity, ...}]
263
+
264
+ Returns:
265
+ List[float] of length 768 (deterministic hash-based embedding)
266
+ """
267
+ if not isinstance(env_state, dict):
268
+ env_state = {}
269
+
270
+ self._last_env_state = env_state
271
+ features = _state_features(env_state)
272
+ if not features:
273
+ self._last_obs = _hash_state_embedding("empty_state")
274
+ else:
275
+ self._last_obs = _feature_hash_embedding(features)
276
+ return self._last_obs
277
+
278
+ def actions(self) -> Sequence[str]:
279
+ """Return the discrete action vocabulary for the last observed state.
280
+
281
+ Actions are context-aware: they are derived from landmarks and chunks
282
+ in the last env_state passed to observe(). If observe() was never called,
283
+ returns an empty list.
284
+
285
+ Action ids:
286
+ - "focus_landmark_<landmark_id>": Focus attention on a landmark
287
+ - "traverse_chunk_<chunk_id>": Navigate to a code chunk
288
+
289
+ Returns:
290
+ List of action id strings
291
+ """
292
+ if not self._last_env_state:
293
+ return []
294
+
295
+ actions = []
296
+
297
+ landmarks = self._last_env_state.get("landmarks", []) or []
298
+ for lm in landmarks:
299
+ if isinstance(lm, dict):
300
+ lm_id = lm.get("id", "")
301
+ if lm_id:
302
+ actions.append(f"focus_landmark_{lm_id}")
303
+
304
+ chunks = self._last_env_state.get("chunks", []) or []
305
+ for ch in chunks:
306
+ if isinstance(ch, dict):
307
+ ch_id = ch.get("id", "")
308
+ if ch_id:
309
+ actions.append(f"traverse_chunk_{ch_id}")
310
+
311
+ return actions
312
+
313
+ def step(self, action: Any) -> Tuple[Any, float, bool, Dict[str, Any]]:
314
+ """Execute an action in code-world.
315
+
316
+ RAISES NotImplementedError with a LOUD message: stepping (editing,
317
+ running code, navigating the runtime) arrives with the ADK sandbox pack.
318
+ This adapter is observe/rank-only in slice 2.
319
+
320
+ Exploration safety is deferred: sandboxed code execution requires
321
+ careful isolation that is not part of this slice.
322
+ """
323
+ raise NotImplementedError(
324
+ "🚨 CodeWorld.step() not implemented in slice 2 — "
325
+ "code-world stepping (execute, edit, navigate) arrives with the ADK sandbox pack. "
326
+ "This adapter is observe + rank only. "
327
+ "Use awpredict.surprise() for anomaly detection, "
328
+ "not for real code execution yet."
329
+ )
330
+
331
+
332
+ # ============================================================================
333
+ # TRANSITION SIGNATURE (helper for WorldModel training)
334
+ # ============================================================================
335
+
336
+ def transition_signature(
337
+ before_state: Dict[str, Any],
338
+ action: str,
339
+ after_state: Dict[str, Any],
340
+ ) -> Tuple[List[float], str, List[float]]:
341
+ """Build a (obs, action, next_obs) triple for WorldModel training.
342
+
343
+ This helper is called to record observed transitions for training the
344
+ world model. The signature combines:
345
+ - before_obs: 768-dim observation of the codebase before the action
346
+ - action: action id string (e.g. "focus_landmark_X")
347
+ - after_obs: 768-dim observation after the action
348
+
349
+ Args:
350
+ before_state: Environment state dict before action
351
+ action: Action id string
352
+ after_state: Environment state dict after action
353
+
354
+ Returns:
355
+ (before_obs, action, after_obs) tuple ready for WorldModel.observe()
356
+ """
357
+ adapter = CodeWorldAdapter()
358
+ before_obs = adapter.observe(before_state)
359
+ after_obs = adapter.observe(after_state)
360
+ return (before_obs, action, after_obs)