tensorcode 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. tensorcode/__init__.py +84 -0
  2. tensorcode/actions.py +137 -0
  3. tensorcode/answer_type.py +222 -0
  4. tensorcode/awareness.py +344 -0
  5. tensorcode/backends/__init__.py +0 -0
  6. tensorcode/backends/builtin.py +167 -0
  7. tensorcode/backends/hf_local.py +89 -0
  8. tensorcode/backends/linear.py +133 -0
  9. tensorcode/backends/neural.py +361 -0
  10. tensorcode/causal.py +262 -0
  11. tensorcode/change.py +566 -0
  12. tensorcode/chunking.py +195 -0
  13. tensorcode/cognition.py +311 -0
  14. tensorcode/context.py +97 -0
  15. tensorcode/control.py +291 -0
  16. tensorcode/cues.py +192 -0
  17. tensorcode/expectation.py +270 -0
  18. tensorcode/frames.py +232 -0
  19. tensorcode/language/__init__.py +36 -0
  20. tensorcode/language/chart.py +558 -0
  21. tensorcode/language/discourse.py +132 -0
  22. tensorcode/language/domains/__init__.py +0 -0
  23. tensorcode/language/domains/desktop.py +552 -0
  24. tensorcode/language/english.py +459 -0
  25. tensorcode/language/features.py +112 -0
  26. tensorcode/language/generate.py +574 -0
  27. tensorcode/language/grammar.py +893 -0
  28. tensorcode/language/semantics.py +349 -0
  29. tensorcode/learning/__init__.py +30 -0
  30. tensorcode/learning/certificate.py +148 -0
  31. tensorcode/learning/induce.py +304 -0
  32. tensorcode/learning/library.py +217 -0
  33. tensorcode/learning/literals.py +126 -0
  34. tensorcode/learning/verify.py +253 -0
  35. tensorcode/memory.py +303 -0
  36. tensorcode/metacognition.py +351 -0
  37. tensorcode/ops.py +207 -0
  38. tensorcode/outcomes.py +99 -0
  39. tensorcode/permanence.py +376 -0
  40. tensorcode/priming.py +191 -0
  41. tensorcode/py.typed +0 -0
  42. tensorcode/quantity.py +311 -0
  43. tensorcode/records.py +728 -0
  44. tensorcode/relation.py +771 -0
  45. tensorcode/runtime.py +471 -0
  46. tensorcode/semantics_bridge.py +308 -0
  47. tensorcode/social.py +380 -0
  48. tensorcode/temporal.py +189 -0
  49. tensorcode/wants.py +185 -0
  50. tensorcode-0.1.0a1.dist-info/METADATA +196 -0
  51. tensorcode-0.1.0a1.dist-info/RECORD +53 -0
  52. tensorcode-0.1.0a1.dist-info/WHEEL +4 -0
  53. tensorcode-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,376 @@
1
+ """Object permanence: a thing that leaves view is occluded, not destroyed.
2
+
3
+ Snapshot scopes retract what is no longer perceived, which is right for *beliefs about now* and
4
+ wrong for *objects*. A window scrolled behind another still exists; a file still exists when the
5
+ terminal is cleared. Without permanence an agent cannot say "the Files window is still open, I
6
+ just can't see it", and identity does not survive occlusion.
7
+
8
+ The trap is that naive permanence re-introduces the stale-belief bug that retraction was there to
9
+ prevent. So an object file keeps two different things apart:
10
+
11
+ exists the object is known to exist (survives leaving view)
12
+ present it was perceived in the latest observation (does not)
13
+
14
+ Those two are independent, which is the point: three states, not two. In view; out of view but
15
+ believed to exist (occluded, scrolled away, behind another window); and *gone* — positively known
16
+ not to exist any more, because the window it lived in was seen to close. Only evidence of
17
+ destruction moves an object to the third state; simply not seeing it never does.
18
+
19
+ and every attribute is dated. Asking for an attribute of an absent object gets the value, when it
20
+ was last seen, and ``stale=True`` — never a bare assertion about the present. ``assertable``
21
+ exists so a caller can refuse to act on a stale attribute at all.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime, timedelta, timezone
28
+ from typing import Any, Iterable, Mapping
29
+
30
+ from .change import Item, Snapshot
31
+ from .records import Claim, Evidence, Ref, Store
32
+
33
+ OBJECTS = Ref("scope:objects")
34
+
35
+ # what an object file writes to the store that can change from one look to the next
36
+ _CHANGING = frozenset({"last_seen", "in_view", "state", "exists"})
37
+
38
+
39
+ def _now() -> datetime:
40
+ return datetime.now(timezone.utc)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class Attribute:
45
+ """A value, when it was seen, and whether that is old news."""
46
+
47
+ name: str
48
+ value: Any
49
+ seen_at: datetime
50
+ stale: bool
51
+
52
+ def describe(self) -> str:
53
+ when = self.seen_at.isoformat(timespec="seconds")
54
+ return f"{self.name}={self.value!r}" + (f" (as of {when}, not visible now)" if self.stale else "")
55
+
56
+
57
+ @dataclass
58
+ class ObjectFile:
59
+ """What is known about one thing, across every time it has been seen."""
60
+
61
+ ref: Ref
62
+ key: str
63
+ kind: str = "control"
64
+ label: str = ""
65
+ window: str = ""
66
+ attributes: dict[str, Any] = field(default_factory=dict)
67
+ seen_at: dict[str, datetime] = field(default_factory=dict)
68
+ first_seen: datetime = field(default_factory=_now)
69
+ last_seen: datetime = field(default_factory=_now)
70
+ times_seen: int = 1
71
+ present: bool = True
72
+ exists: bool = True # positively known to be gone only when its container was seen to close
73
+ gone_at: datetime | None = None
74
+ gone_because: str = ""
75
+ returns: int = 0 # how many times it came back after going away
76
+
77
+ @property
78
+ def state(self) -> str:
79
+ """in_view | out_of_view | gone — the distinction a snapshot scope cannot make."""
80
+ if not self.exists:
81
+ return "gone"
82
+ return "in_view" if self.present else "out_of_view"
83
+
84
+ def attribute(self, name: str) -> Attribute | None:
85
+ if name not in self.attributes:
86
+ return None
87
+ return Attribute(name, self.attributes[name], self.seen_at.get(name, self.last_seen), not self.present)
88
+
89
+ def assertable(self, name: str, *, within: timedelta | None = None, now: datetime | None = None) -> bool:
90
+ """May a caller state this attribute as current? Only if seen now, or fresh enough to risk."""
91
+ if name not in self.attributes:
92
+ return False
93
+ if not self.exists:
94
+ return False # an attribute of a destroyed object is never current
95
+ if self.present:
96
+ return True
97
+ if within is None:
98
+ return False
99
+ return ((now or _now()) - self.seen_at.get(name, self.last_seen)) <= within
100
+
101
+ def describe(self) -> str:
102
+ if not self.exists:
103
+ state = f"gone since {(self.gone_at or self.last_seen).isoformat(timespec='seconds')}"
104
+ state += f" ({self.gone_because})" if self.gone_because else ""
105
+ elif self.present:
106
+ state = "in view"
107
+ else:
108
+ state = f"not in view since {self.last_seen.isoformat(timespec='seconds')}"
109
+ return f"{self.label or self.key} ({self.kind}{', ' + self.window if self.window else ''}) — {state}"
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class ObservationReport:
114
+ """What one observation did to the registry, so permanence can be measured."""
115
+
116
+ seen: int = 0
117
+ new: int = 0
118
+ rematched: int = 0 # same object under a different key
119
+ returned: int = 0 # was absent, is present again
120
+ absent: int = 0
121
+ identities_kept: int = 0 # objects that kept their file across this observation
122
+ wrong_about_gone: int = 0 # we had written something off, and there it was
123
+
124
+
125
+ class Objects:
126
+ """A registry of object files, updated by observation.
127
+
128
+ Identity is by key when the key is stable, and by (kind, window, name) when it is not — the
129
+ scene-graph keys carry an occurrence index, so a list losing an earlier row renames every row
130
+ after it. Matching on the name first is what keeps a file attached to its object.
131
+
132
+ What counts as the name depends on what kind of thing it is, and getting this wrong is how a
133
+ measurement of permanence comes back empty. A control is named by its label: its value changes
134
+ while it stays the same control. A *line of read-only text* has no label and no life apart from
135
+ its content — the line "report.pdf" in a terminal **is** that content, and its key is only its
136
+ position in a scrolling region. Identify such an item by its value, or a file follows the slot
137
+ instead of the object, and a line that scrolls away looks like a line whose text changed.
138
+ """
139
+
140
+ def __init__(self, *, forget_absent_after: timedelta | None = None) -> None:
141
+ # keyed by the file's own id, never by a screen key: screen keys are recycled, and a
142
+ # registry keyed by them loses one object every time another takes over its slot
143
+ self.files: dict[str, ObjectFile] = {}
144
+ self._by_key: dict[str, str] = {}
145
+ self._by_identity: dict[tuple[str, str, str], str] = {}
146
+ self.forget_absent_after = forget_absent_after
147
+ self._counter = 0
148
+
149
+ # ------------------------------------------------------------- observing
150
+
151
+ def observe(self, snap: Snapshot) -> ObservationReport:
152
+ """Update from one snapshot: what is here, what is merely not visible, what is new."""
153
+ seen: set[str] = set()
154
+ new = rematched = returned = wrong_about_gone = 0
155
+ for key, item in snap.items.items():
156
+ file = self._match(item, key)
157
+ if file is None:
158
+ file = self._create(item, snap.at)
159
+ new += 1
160
+ else:
161
+ if file.key != key:
162
+ rematched += 1
163
+ self._rekey(file, key)
164
+ if not file.present:
165
+ returned += 1
166
+ file.returns += 1
167
+ if not file.exists: # seeing it is stronger evidence than our belief that it was gone
168
+ file.exists, file.gone_at, file.gone_because = True, None, ""
169
+ wrong_about_gone += 1
170
+ file.times_seen += 1
171
+ file.present = True
172
+ file.last_seen = snap.at
173
+ file.label = item.label or file.label
174
+ file.window = item.window or file.window
175
+ self._by_key[item.key] = file.ref.id
176
+ self._by_identity[(file.kind, file.window, _name_of(item))] = file.ref.id
177
+ for name, value in _attributes(item).items():
178
+ if file.attributes.get(name) != value or name not in file.seen_at:
179
+ file.attributes[name] = value
180
+ file.seen_at[name] = snap.at
181
+ seen.add(file.ref.id)
182
+ absent = 0
183
+ for fid, file in list(self.files.items()):
184
+ if fid in seen:
185
+ continue
186
+ if file.present:
187
+ file.present = False
188
+ if file.exists:
189
+ absent += 1 # gone objects are not "absent": we are not waiting for them to come back
190
+ if self.forget_absent_after is not None and (snap.at - file.last_seen) > self.forget_absent_after:
191
+ self._drop(file)
192
+ return ObservationReport(len(snap.items), new, rematched, returned, absent,
193
+ identities_kept=len(snap.items) - new, wrong_about_gone=wrong_about_gone)
194
+
195
+ def closed(self, window: str, *, at: datetime | None = None, why: str = "its window closed") -> int:
196
+ """A window was *seen* to close: what lived in it is gone, not merely out of sight.
197
+
198
+ This is the only way an object stops existing. Absence never implies it, because absence is
199
+ what occlusion looks like; a close event is evidence of destruction and nothing else is.
200
+ """
201
+ when = at or _now()
202
+ marked = 0
203
+ for file in self.files.values():
204
+ if file.window != window or not file.exists:
205
+ continue
206
+ file.exists, file.present = False, False
207
+ file.gone_at, file.gone_because = when, why
208
+ marked += 1
209
+ return marked
210
+
211
+ def gone(self) -> list[ObjectFile]:
212
+ return sorted((f for f in self.files.values() if not f.exists), key=lambda f: f.key)
213
+
214
+ # ------------------------------------------------------------- reading
215
+
216
+ def get(self, key: str) -> ObjectFile | None:
217
+ """The file for a screen key, or for a file id."""
218
+ fid = self._by_key.get(key)
219
+ return self.files.get(fid) if fid else self.files.get(key)
220
+
221
+ def find(self, label: str, *, kind: str | None = None, window: str | None = None) -> list[ObjectFile]:
222
+ rows = [f for f in self.files.values() if f.label == label
223
+ and (kind is None or f.kind == kind) and (window is None or f.window == window)]
224
+ return sorted(rows, key=lambda f: (not f.present, f.key))
225
+
226
+ def present(self) -> list[ObjectFile]:
227
+ return sorted((f for f in self.files.values() if f.present), key=lambda f: f.key)
228
+
229
+ def absent(self) -> list[ObjectFile]:
230
+ """Out of view but believed to exist — not the same set as ``gone``."""
231
+ return sorted((f for f in self.files.values() if not f.present and f.exists), key=lambda f: f.key)
232
+
233
+ def known(self) -> list[ObjectFile]:
234
+ return sorted(self.files.values(), key=lambda f: f.key)
235
+
236
+ def windows(self, *, including_absent: bool = True) -> list[str]:
237
+ rows = self.files.values() if including_absent else self.present()
238
+ return sorted({f.window for f in rows if f.window})
239
+
240
+ # ------------------------------------------------------------- claims
241
+
242
+ def remember(self, mind: Store, *, source: str = "obs:objects", at: datetime | None = None) -> int:
243
+ """Write existence and identity into a non-snapshot scope, so they survive retraction.
244
+
245
+ Only existence and identity go here. Attributes stay on the file, dated, because writing
246
+ them as plain claims is exactly how a stale belief would get asserted as current.
247
+
248
+ The handful that do change — whether it is in view, when it was last seen — are *replaced*,
249
+ not appended. Appending looks harmless and is not: a claim per object per turn is a leak
250
+ with a scope name, and on a desktop of a few hundred objects it outgrows perception itself
251
+ within a few dozen turns.
252
+ """
253
+ when = at or _now()
254
+ written = 0
255
+ for file in self.known():
256
+ for predicate, value in (("exists", file.exists), ("is_a", file.kind), ("label", file.label),
257
+ ("last_seen", file.last_seen), ("in_view", file.present),
258
+ ("state", file.state)):
259
+ if value == "" or value is None:
260
+ continue
261
+ live = [r for r in mind.claims(file.ref, predicate, scope=OBJECTS) if not r.retracted]
262
+ if any(r.claim.object == value for r in live):
263
+ continue
264
+ if predicate in _CHANGING and live: # supersede: one current value, no history kept
265
+ mind.forget([r.id for r in live])
266
+ mind.tell(Claim(file.ref, predicate, value, scope=OBJECTS),
267
+ Evidence(Ref(source), when, method="object-permanence"))
268
+ written += 1
269
+ return written
270
+
271
+ # ------------------------------------------------------------- internals
272
+
273
+ def _create(self, item: Item, at: datetime) -> ObjectFile:
274
+ self._counter += 1
275
+ file = ObjectFile(Ref(f"object:{self._counter}"), item.key, item.kind, item.label, item.window,
276
+ first_seen=at, last_seen=at)
277
+ self.files[file.ref.id] = file
278
+ self._by_key[item.key] = file.ref.id
279
+ self._by_identity[(item.kind, item.window, _name_of(item))] = file.ref.id
280
+ return file
281
+
282
+ def _match(self, item: Item, key: str) -> ObjectFile | None:
283
+ """The file this item belongs to, if any.
284
+
285
+ Name before key, and never a key whose file names something else: in a scrolling region the
286
+ keys are recycled, so trusting the key first hands one object's file to the next occupant of
287
+ its slot and quietly overwrites what was known about it.
288
+ """
289
+ named = self._by_name(item)
290
+ if named is not None:
291
+ return named
292
+ held = self.files.get(self._by_key.get(key, ""))
293
+ if held is None:
294
+ return None
295
+ name, held_name = _name_of(item), _identity_name(held)
296
+ if name and held_name and name != held_name:
297
+ return None
298
+ return held
299
+
300
+ def _by_name(self, item: Item) -> ObjectFile | None:
301
+ name = _name_of(item)
302
+ if not name:
303
+ return None
304
+ fid = self._by_identity.get((item.kind, item.window, name))
305
+ return self.files.get(fid) if fid else None
306
+
307
+ def _rekey(self, file: ObjectFile, key: str) -> None:
308
+ """The same object under a new screen key. Only the index moves; the file stays put."""
309
+ if self._by_key.get(file.key) == file.ref.id:
310
+ self._by_key.pop(file.key, None)
311
+ file.key = key
312
+ self._by_key[key] = file.ref.id
313
+
314
+ def _drop(self, file: ObjectFile) -> None:
315
+ self.files.pop(file.ref.id, None)
316
+ if self._by_key.get(file.key) == file.ref.id:
317
+ self._by_key.pop(file.key, None)
318
+ if self._by_identity.get((file.kind, file.window, _identity_name(file))) == file.ref.id:
319
+ self._by_identity.pop((file.kind, file.window, _identity_name(file)), None)
320
+
321
+
322
+ def _name_of(item: Item) -> str:
323
+ """What identifies this item across observations (see ``Objects``)."""
324
+ if item.label:
325
+ return item.label
326
+ return item.value if item.kind == "text" else ""
327
+
328
+
329
+ def _identity_name(file: ObjectFile) -> str:
330
+ if file.label:
331
+ return file.label
332
+ return str(file.attributes.get("value", "")) if file.kind == "text" else ""
333
+
334
+
335
+ def _attributes(item: Item) -> Mapping[str, Any]:
336
+ out: dict[str, Any] = {}
337
+ if item.value != "":
338
+ out["value"] = item.value
339
+ if item.where is not None:
340
+ out["where"] = item.where
341
+ out["focused"] = item.focused
342
+ return out
343
+
344
+
345
+ def still_there(objects: Objects, label: str) -> str:
346
+ """A sentence an agent can say honestly about something not in view."""
347
+ files = objects.find(label)
348
+ if not files:
349
+ return f"I have no record of {label}."
350
+ file = files[0]
351
+ if file.present:
352
+ return f"{label} is on screen now."
353
+ if not file.exists:
354
+ return (f"{label} is gone — {file.gone_because} at "
355
+ f"{(file.gone_at or file.last_seen).isoformat(timespec='seconds')}.")
356
+ return (f"{label} was there when I last saw it "
357
+ f"({file.last_seen.isoformat(timespec='seconds')}); I can't see it now, so I can't say it still is.")
358
+
359
+
360
+ def describe_attribute(objects: Objects, label: str, name: str) -> str:
361
+ """Report an attribute without pretending an old reading is a current one."""
362
+ files = objects.find(label)
363
+ if not files:
364
+ return f"I have no record of {label}."
365
+ attribute = files[0].attribute(name)
366
+ if attribute is None:
367
+ return f"I never noted the {name} of {label}."
368
+ return f"{label}: {attribute.describe()}"
369
+
370
+
371
+ def known_objects(snapshots: Iterable[Snapshot]) -> Objects:
372
+ """Replay snapshots into a fresh registry (used by measurements and tests)."""
373
+ objects = Objects()
374
+ for snap in snapshots:
375
+ objects.observe(snap)
376
+ return objects
tensorcode/priming.py ADDED
@@ -0,0 +1,191 @@
1
+ """Action priming: procedures warmed by what is aware, firing when activation crosses.
2
+
3
+ Dispatch asks "which procedure handles this act?" and gets one answer. Priming asks nothing:
4
+ every procedure whose cues appear in the aware set gains activation, several are partly
5
+ warm at once, and one fires when it crosses its threshold. Lookup falls out as the special
6
+ case where a single cue (the parsed act) matches.
7
+
8
+ The synfire part is the chain. A procedure may declare ordered stages, and a stage only
9
+ accumulates once the stage before it has fired within a window of cycles; a gap lets the
10
+ chain cool and fall back. So "the terminal is open, then a command was typed, then the
11
+ prompt returned" is a sequence the priming can require, rather than three conditions a
12
+ guard happens to check together.
13
+
14
+ Nothing here executes: ``ready()`` reports what is warm enough, and ``why()`` says which
15
+ claims warmed it, so a caller keeps the decision.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from dataclasses import dataclass, field
21
+ from typing import Any, Iterable, Sequence
22
+
23
+ from .records import ClaimRecord, Ref
24
+
25
+ _ANY = object()
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Cue:
30
+ """A claim pattern that warms a procedure, and how much it contributes."""
31
+
32
+ predicate: str | None = None
33
+ subject: Ref | None = None
34
+ object: Any = _ANY
35
+ weight: float = 1.0
36
+
37
+ def matches(self, rec: ClaimRecord) -> bool:
38
+ c = rec.claim
39
+ return ((self.predicate is None or c.predicate == self.predicate)
40
+ and (self.subject is None or c.subject == self.subject)
41
+ and (self.object is _ANY or c.object == self.object))
42
+
43
+ def describe(self) -> str:
44
+ parts = [f"{self.subject}" if self.subject else "?", self.predicate or "?",
45
+ "?" if self.object is _ANY else repr(self.object)]
46
+ return " ".join(parts)
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class Primeable:
51
+ """Something that can be primed: an id, its cues, and optionally an ordered chain."""
52
+
53
+ id: str
54
+ cues: tuple[Cue, ...] = ()
55
+ threshold: float = 0.6
56
+ chain: tuple[tuple[Cue, ...], ...] = () # ordered stages; a stage opens only after the one before
57
+ window: int = 3 # cycles a stage stays open before the chain cools
58
+ payload: Any = None # whatever the caller wants back (a Procedure, a callable, a name)
59
+
60
+ @property
61
+ def staged(self) -> bool:
62
+ return bool(self.chain)
63
+
64
+
65
+ def primeable_from(obj: Any, *, threshold: float = 0.6) -> Primeable:
66
+ """Read a primeable off an object that may already describe its own cues.
67
+
68
+ Honours ``cues``, ``prime_threshold``, ``chain`` and ``window`` when present. With none
69
+ of them, a procedure that declares ``act`` gets the single cue that reproduces dispatch:
70
+ a claim ``(request, "act", <act>)``. So a body of procedures becomes primeable without
71
+ being rewritten, and declaring cues is how one stops being merely dispatchable.
72
+ """
73
+ ident = str(getattr(obj, "id", obj))
74
+ cues = tuple(getattr(obj, "cues", ()) or ())
75
+ chain = tuple(tuple(stage) for stage in (getattr(obj, "chain", ()) or ()))
76
+ act = getattr(obj, "act", None)
77
+ if not cues and not chain and act:
78
+ cues = (Cue(predicate="act", object=act),)
79
+ return Primeable(ident, cues, float(getattr(obj, "prime_threshold", threshold)), chain,
80
+ int(getattr(obj, "window", 3)), payload=obj)
81
+
82
+
83
+ @dataclass
84
+ class Priming:
85
+ """Activation over a set of primeables, driven by the aware set."""
86
+
87
+ primeables: list[Primeable] = field(default_factory=list)
88
+ decay: float = 0.5
89
+ floor: float = 0.02
90
+ activation: dict[str, float] = field(default_factory=dict)
91
+ stage: dict[str, int] = field(default_factory=dict)
92
+ _last_stage_cycle: dict[str, int] = field(default_factory=dict)
93
+ _matched: dict[str, list[tuple[str, str, float]]] = field(default_factory=dict) # id -> (cue, claim id, weight)
94
+ cycle: int = 0
95
+
96
+ def add(self, primeable: Primeable) -> None:
97
+ self.primeables.append(primeable)
98
+
99
+ def observe(self, aware: Any, *, saliences: dict[str, float] | None = None) -> None:
100
+ """One cycle: warm what the aware set matches, cool what it does not."""
101
+ records, salience_of = _records_and_salience(aware, saliences)
102
+ self.cycle += 1
103
+ for p in self.primeables:
104
+ before = self.activation.get(p.id, 0.0) * self.decay
105
+ matched: list[tuple[str, str, float]] = []
106
+ if p.staged:
107
+ stage_index = self.stage.get(p.id, 0)
108
+ gained = 0.0
109
+ if stage_index < len(p.chain):
110
+ for cue in p.chain[stage_index]:
111
+ for rec in records:
112
+ if cue.matches(rec):
113
+ gained += cue.weight * (salience_of(rec.id) or 1e-3)
114
+ matched.append((cue.describe(), rec.id, cue.weight))
115
+ if gained > 0:
116
+ self.stage[p.id] = stage_index + 1
117
+ self._last_stage_cycle[p.id] = self.cycle
118
+ elif self.cycle - self._last_stage_cycle.get(p.id, self.cycle) > p.window:
119
+ self.stage[p.id] = 0 # the chain cooled: back to the beginning
120
+ before *= 0.0
121
+ self.activation[p.id] = min(1.5, before + gained)
122
+ else:
123
+ gained = 0.0
124
+ for cue in p.cues:
125
+ for rec in records:
126
+ if cue.matches(rec):
127
+ gained += cue.weight * (salience_of(rec.id) or 1e-3)
128
+ matched.append((cue.describe(), rec.id, cue.weight))
129
+ self.activation[p.id] = min(1.5, before + gained)
130
+ if self.activation[p.id] < self.floor:
131
+ self.activation.pop(p.id, None)
132
+ if matched:
133
+ self._matched[p.id] = matched
134
+
135
+ def partial(self) -> dict[str, float]:
136
+ """Everything warm, whether or not it is ready — several procedures are usually partly on."""
137
+ return dict(sorted(self.activation.items(), key=lambda kv: (-kv[1], kv[0])))
138
+
139
+ def ready(self) -> list[tuple[Primeable, float]]:
140
+ """What has crossed its threshold (and finished its chain), strongest first."""
141
+ out = []
142
+ for p in self.primeables:
143
+ level = self.activation.get(p.id, 0.0)
144
+ if level < p.threshold:
145
+ continue
146
+ if p.staged and self.stage.get(p.id, 0) < len(p.chain):
147
+ continue
148
+ out.append((p, level))
149
+ return sorted(out, key=lambda pair: (-pair[1], pair[0].id))
150
+
151
+ def why(self, primeable_id: str) -> list[str]:
152
+ level = self.activation.get(primeable_id, 0.0)
153
+ p = next((q for q in self.primeables if q.id == primeable_id), None)
154
+ if p is None:
155
+ return [f"{primeable_id} is not primed here"]
156
+ lines = [f"{primeable_id} at {level:.3f} (threshold {p.threshold:.2f})"]
157
+ if p.staged:
158
+ lines.append(f" chain stage {self.stage.get(primeable_id, 0)} of {len(p.chain)}")
159
+ for cue, claim_id, weight in self._matched.get(primeable_id, []):
160
+ lines.append(f" {cue} matched {claim_id} (+{weight:.2f})")
161
+ return lines
162
+
163
+ def reset(self, primeable_id: str) -> None:
164
+ """After firing, or after the situation changed: this one starts cold."""
165
+ self.activation.pop(primeable_id, None)
166
+ self.stage.pop(primeable_id, None)
167
+ self._last_stage_cycle.pop(primeable_id, None)
168
+ self._matched.pop(primeable_id, None)
169
+
170
+
171
+ def prime(primeables: Sequence[Any], aware: Any, *, threshold: float = 0.6) -> Priming:
172
+ """Build priming over a set of procedure-like objects and observe one cycle."""
173
+ p = Priming([q if isinstance(q, Primeable) else primeable_from(q, threshold=threshold) for q in primeables])
174
+ p.observe(aware)
175
+ return p
176
+
177
+
178
+ def _records_and_salience(aware: Any, saliences: dict[str, float] | None) -> tuple[list[ClaimRecord], Any]:
179
+ """Read the aware set and its saliences once; a cue match must not recompute them."""
180
+ if hasattr(aware, "aware") and hasattr(aware, "salience"): # an Awareness
181
+ records = aware.aware()
182
+ table = {rec.id: max(aware.salience(rec.id), 1e-3) for rec in records}
183
+ return records, table.get
184
+ records = list(aware.claims() if hasattr(aware, "claims") else aware)
185
+ table = saliences or {}
186
+ return records, lambda cid: table.get(cid, 1.0)
187
+
188
+
189
+ def cues_for_sequence(*stages: Iterable[Cue]) -> tuple[tuple[Cue, ...], ...]:
190
+ """Spell a chain: ``cues_for_sequence([Cue(...)], [Cue(...)])`` in the order they must occur."""
191
+ return tuple(tuple(stage) for stage in stages)
tensorcode/py.typed ADDED
File without changes