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 +26 -0
- awpredict/_doctor.py +139 -0
- awpredict/adapters/__init__.py +3 -0
- awpredict/adapters/code_world.py +360 -0
- awpredict/adapters/obs_adapter.py +213 -0
- awpredict/contracts.py +114 -0
- awpredict/core/__init__.py +3 -0
- awpredict/core/lewm.py +2646 -0
- awpredict/core/mlp.py +654 -0
- awpredict/explorer/__init__.py +14 -0
- awpredict/explorer/fs_cartographer.py +429 -0
- awpredict/explorer/histogram_encoder.py +63 -0
- awpredict/memory/__init__.py +6 -0
- awpredict/memory/landmark.py +166 -0
- awpredict/training/__init__.py +1 -0
- awpredict/training/online.py +132 -0
- awpredict-0.1.0.dist-info/METADATA +14 -0
- awpredict-0.1.0.dist-info/RECORD +21 -0
- awpredict-0.1.0.dist-info/WHEEL +5 -0
- awpredict-0.1.0.dist-info/licenses/LICENSE +21 -0
- awpredict-0.1.0.dist-info/top_level.txt +1 -0
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,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)
|