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
tensorcode/chunking.py ADDED
@@ -0,0 +1,195 @@
1
+ """Automatization: a sequence done the same way often enough stops being deliberated.
2
+
3
+ The deliberate-to-automatic transition is the most reliable finding in skill learning, and a
4
+ procedure interpreter has none of it: a learned skill walks its steps, evaluates every guard and
5
+ records every decision, on the thousandth run exactly as on the first. That is not carefulness,
6
+ it is an inability to learn *how* it does something as opposed to *that* it works.
7
+
8
+ A chunk here is the compiled form of a sequence that has run identically :attr:`Chunks.repeats`
9
+ times: the steps actually taken, the guards whose outcome was the same every time, and nothing
10
+ else. Running a chunk skips the deliberation, not the acts — a click is still a click. What it
11
+ skips is evaluating guards whose answer has never varied and recording a decision per step.
12
+
13
+ Automatization trades adaptability for speed, so the trade is made explicit:
14
+
15
+ * the expanded form is never discarded; a chunk is an *index* into it;
16
+ * a chunk is retired the moment a step's outcome diverges from what the recorded runs saw
17
+ (:func:`divergent`), and the walk continues expanded from that point — the fallback is the
18
+ whole reason this is safe;
19
+ * every chunk carries how often it was used and how often it fell back, so a chunk that keeps
20
+ breaking is visible rather than quietly wrong.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import json
27
+ from dataclasses import dataclass, field
28
+ from typing import Iterable, Mapping, Sequence
29
+
30
+ #: bindings that mean a body step did not do what the recorded runs saw it do.
31
+ #: Deliberately *not* ``error_summary``: the assistant's own ``output_facts`` always fills that
32
+ #: in, falling back to the first line of a perfectly good output, so reading it as trouble would
33
+ #: retire every chunk on its first use. ``ok`` and ``errors`` are the marks that mean trouble.
34
+ TROUBLE = ("error", "problem", "timed_out", "unavailable")
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Trace:
39
+ """One run of a procedure: which steps ran, which were skipped, and how it ended."""
40
+
41
+ procedure: str
42
+ taken: tuple[int, ...] # program counters whose step executed, in order
43
+ skipped: tuple[int, ...] # program counters whose guard did not hold
44
+ status: str = "done"
45
+ #: (program counter, binding that showed trouble) for the steps that showed any. Some steps
46
+ #: are *meant* to fail — "stat the folder before creating it" expects "no such file" — so a
47
+ #: chunk has to know what normal looked like rather than treating any error as a surprise.
48
+ marks: tuple[tuple[int, str], ...] = ()
49
+
50
+ @property
51
+ def shape(self) -> str:
52
+ """Identity of the *path* through the procedure, which is what can be compiled."""
53
+ return hashlib.sha256(json.dumps([self.procedure, list(self.taken), sorted(self.skipped),
54
+ sorted(self.marks)]).encode()).hexdigest()[:12]
55
+
56
+
57
+ @dataclass
58
+ class Chunk:
59
+ """A compiled path: run these steps in this order, without re-deciding."""
60
+
61
+ procedure: str
62
+ taken: tuple[int, ...]
63
+ skipped: frozenset[int]
64
+ shape: str
65
+ from_runs: int
66
+ expected: Mapping[int, str] = field(default_factory=dict) # pc -> the trouble every run saw there
67
+ uses: int = 0
68
+ fallbacks: int = 0
69
+ retired: bool = False
70
+ retired_because: str = ""
71
+
72
+ def assumes_skipped(self, pc: int) -> bool:
73
+ return pc in self.skipped
74
+
75
+ def expects(self, pc: int) -> str:
76
+ """The trouble mark every recorded run saw at this step ("" if they all saw none)."""
77
+ return self.expected.get(pc, "")
78
+
79
+ def describe(self) -> str:
80
+ state = "retired" if self.retired else "live"
81
+ return (f"chunk {self.procedure}/{self.shape} ({state}): {len(self.taken)} steps, "
82
+ f"{len(self.skipped)} guards assumed, from {self.from_runs} runs, "
83
+ f"used {self.uses}, fell back {self.fallbacks}")
84
+
85
+
86
+ def _marked(bindings: Mapping[str, object], name: str):
87
+ """Values a step bound under ``name``, including under a step's ``as:`` prefix.
88
+
89
+ A step that names its results (``"as": "sib"``) binds ``sib_ok`` and ``sib_errors``, so a
90
+ check that only looked for ``ok`` would never see trouble in exactly the procedures that
91
+ keep several commands' results apart.
92
+ """
93
+ for key, value in bindings.items():
94
+ if key == name or key.endswith(f"_{name}"):
95
+ yield key, value
96
+
97
+
98
+ def mark(bindings: Mapping[str, object]) -> str:
99
+ """The name of the binding that says this step had trouble, or ``""`` if none did.
100
+
101
+ Deliberately shallow: it reads only the marks the procedures already set. A chunk that skips
102
+ guards cannot notice a subtle difference, which is exactly the known cost of automatization —
103
+ so what is watched is trouble, and the *name* is what is compared, because the text of an
104
+ error varies while its kind does not.
105
+ """
106
+ for name in TROUBLE:
107
+ for key, value in _marked(bindings, name):
108
+ if isinstance(value, bool) and value:
109
+ return key
110
+ if isinstance(value, str) and value.strip():
111
+ return key
112
+ for key, value in _marked(bindings, "errors"):
113
+ if isinstance(value, (list, tuple)) and value:
114
+ return key
115
+ for key, value in _marked(bindings, "ok"):
116
+ if value is False:
117
+ return key
118
+ return ""
119
+
120
+
121
+ def divergent(bindings: Mapping[str, object], expected: str = "") -> str:
122
+ """Why this step differs from the runs a chunk was compiled from ("" when it does not).
123
+
124
+ Both directions count. Trouble where there was none is the obvious case; *no* trouble where
125
+ every recorded run had some is equally a divergence, because a guard that branched on it —
126
+ and is now being skipped — would have gone the other way.
127
+ """
128
+ found = mark(bindings)
129
+ if found == expected:
130
+ return ""
131
+ if found and expected:
132
+ return f"{found} where the recorded runs had {expected}"
133
+ if found:
134
+ return f"{found}, which the recorded runs never had"
135
+ return f"no {expected}, which every recorded run had"
136
+
137
+
138
+ @dataclass
139
+ class Chunks:
140
+ """What this mind has automatized, and how well each chunk is holding up."""
141
+
142
+ repeats: int = 3 # identical paths before a chunk is compiled
143
+ enabled: bool = True
144
+ runs: dict[str, list[str]] = field(default_factory=dict) # procedure -> recent path shapes
145
+ traces: dict[str, Trace] = field(default_factory=dict) # shape -> the path it stands for
146
+ compiled: dict[str, Chunk] = field(default_factory=dict) # procedure -> live chunk
147
+ history: list[str] = field(default_factory=list) # compile/retire events, in order
148
+
149
+ def record(self, trace: Trace) -> Chunk | None:
150
+ """Note how a run went; compile a chunk once the same path has repeated enough."""
151
+ if trace.status != "done":
152
+ self.runs.setdefault(trace.procedure, []).clear()
153
+ return None
154
+ shapes = self.runs.setdefault(trace.procedure, [])
155
+ shapes.append(trace.shape)
156
+ self.traces[trace.shape] = trace
157
+ recent = shapes[-self.repeats:]
158
+ if len(recent) < self.repeats or len(set(recent)) != 1:
159
+ return None
160
+ live = self.compiled.get(trace.procedure)
161
+ if live is not None and live.shape == trace.shape and not live.retired:
162
+ return live
163
+ chunk = Chunk(trace.procedure, trace.taken, frozenset(trace.skipped), trace.shape, self.repeats,
164
+ dict(trace.marks))
165
+ self.compiled[trace.procedure] = chunk
166
+ self.history.append(f"compiled {trace.procedure}/{trace.shape} after {self.repeats} identical runs")
167
+ return chunk
168
+
169
+ def chunk_for(self, procedure: str | None) -> Chunk | None:
170
+ if not self.enabled or procedure is None:
171
+ return None
172
+ chunk = self.compiled.get(procedure)
173
+ return None if chunk is None or chunk.retired else chunk
174
+
175
+ def used(self, chunk: Chunk) -> None:
176
+ chunk.uses += 1
177
+
178
+ def retire(self, chunk: Chunk, why: str) -> None:
179
+ """Abandon a chunk and fall back to deliberating. The expanded form was never lost."""
180
+ chunk.retired, chunk.retired_because = True, why
181
+ chunk.fallbacks += 1
182
+ self.runs.setdefault(chunk.procedure, []).clear()
183
+ self.history.append(f"retired {chunk.procedure}/{chunk.shape}: {why}")
184
+
185
+ def stats(self) -> dict:
186
+ return {
187
+ "compiled": len([c for c in self.compiled.values() if not c.retired]),
188
+ "retired": len([c for c in self.compiled.values() if c.retired]),
189
+ "uses": sum(c.uses for c in self.compiled.values()),
190
+ "fallbacks": sum(c.fallbacks for c in self.compiled.values()),
191
+ "procedures": {p: c.describe() for p, c in self.compiled.items()},
192
+ }
193
+
194
+
195
+ __all__ = ["Chunk", "Chunks", "Trace", "divergent", "mark", "TROUBLE"]
@@ -0,0 +1,311 @@
1
+ """A small cognitive substrate over the claim store.
2
+
3
+ perceive / parse -> Fragment (claims extracted from one input, with locators)
4
+ integrate(mind, *fragments) -> Thought (merge into working memory; report what changed)
5
+ think(mind, rules, since=thought) -> Thought (bottom-up rules fire on what changed)
6
+ explain(mind, claim) (why a claim is believed: observations and rules)
7
+
8
+ A ``Fragment`` can be a *snapshot* of a scope (for example, everything currently on
9
+ screen). Claims in that scope that the new snapshot no longer contains are retracted,
10
+ and derivations that depended only on them are withdrawn with them.
11
+
12
+ Perception can be wrong for a frame. An optional ``Corroboration`` policy keeps chosen
13
+ perceived claims *tentative* until they are seen the same way in ``k`` frames (or confirmed
14
+ by an action's outcome); derivations inherit tentativeness from their premises, and a
15
+ tentative claim a later frame contradicts is retracted. Rules and intentions that must not
16
+ act on a single glance ask for the established view.
17
+
18
+ Rules are ordinary Python: patterns select premises, a function derives claims. Every
19
+ derived claim cites its premises, so every thought can be explained. Rules may call
20
+ other operations (``tc.classify``, ``tc.parse``); those calls appear in the trace.
21
+ Nothing here decides what to *do*; that is ``choose`` over intentions.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import time as _time
27
+ from dataclasses import dataclass, field
28
+ from datetime import datetime, timezone
29
+ from typing import Any, Callable, Iterable, Mapping, Sequence
30
+
31
+ from .outcomes import Score
32
+ from .records import Claim, ClaimRecord, Evidence, Patch, Put, Ref, Retract, Store, Tell
33
+
34
+
35
+ def _now() -> datetime:
36
+ return datetime.now(timezone.utc)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class Fragment:
41
+ """Claims extracted from one input."""
42
+
43
+ source: Ref # the input these claims came from (a frame, a message, a document)
44
+ claims: tuple[tuple[Claim, str | None], ...] # (claim, locator within the source)
45
+ entities: tuple[tuple[Ref, Any], ...] = () # typed payloads, e.g. geometry for a control
46
+ snapshot_of: Ref | None = None # if set, this fragment is everything currently true in that scope
47
+ method: str = "parse"
48
+ observed_at: datetime = field(default_factory=_now)
49
+ confidence: Mapping[str, Score] = field(default_factory=dict) # claim id -> extractor's score
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class Thought:
54
+ """What changed in the mind. Empty thoughts are normal."""
55
+
56
+ added: tuple[ClaimRecord, ...] = ()
57
+ retracted: tuple[ClaimRecord, ...] = ()
58
+ established: tuple[ClaimRecord, ...] = () # tentative claims that became established (see ``Corroboration``)
59
+
60
+ def __add__(self, other: Thought) -> Thought:
61
+ return Thought(self.added + other.added, self.retracted + other.retracted, self.established + other.established)
62
+
63
+ @property
64
+ def empty(self) -> bool:
65
+ return not self.added and not self.retracted and not self.established
66
+
67
+ def about(self, predicate: str) -> list[Claim]:
68
+ return [r.claim for r in self.added if r.claim.predicate == predicate]
69
+
70
+
71
+ @dataclass
72
+ class Corroboration:
73
+ """When a perceived claim may be acted on: after ``k`` consistent frames, or confirmation.
74
+
75
+ * ``predicates``: which perceived predicates this governs (None = all). Others are
76
+ established on sight, which keeps, say, button geometry instant while text readings
77
+ wait for a second look.
78
+ * A frame counts toward ``k`` only if the extractor's confidence (when it gave one) is at
79
+ least ``min_confidence``; below that a claim stays tentative until ``confirm``-ed.
80
+ * Derived claims are established only when every premise of some line of support is.
81
+ * A tentative claim that a later frame contradicts is retracted: in a snapshot scope by
82
+ no longer being perceived; elsewhere by a different object for a functional predicate.
83
+
84
+ State is per mind and per run: create a fresh policy for each episode.
85
+ """
86
+
87
+ k: int = 2
88
+ min_confidence: float = 0.0
89
+ predicates: frozenset[str] | None = None
90
+ frames: dict[str, int] = field(default_factory=dict) # claim id -> frames that corroborated it
91
+ confirmed: set[str] = field(default_factory=set)
92
+
93
+ def governs(self, predicate: str) -> bool:
94
+ return self.predicates is None or predicate in self.predicates
95
+
96
+ def established(self, mind: Store, claim_id: str, _seen: frozenset[str] = frozenset()) -> bool:
97
+ rec = mind._claims.get(claim_id)
98
+ if rec is None or rec.retracted or claim_id in _seen:
99
+ return False
100
+ if claim_id in self.confirmed:
101
+ return True
102
+ for e in rec.evidence:
103
+ if e.derived_from:
104
+ if all(self.established(mind, p, _seen | {claim_id}) for p in e.derived_from):
105
+ return True
106
+ elif not self.governs(rec.claim.predicate) or self.frames.get(claim_id, 0) >= self.k:
107
+ return True
108
+ return False
109
+
110
+ def tentative(self, mind: Store, claim_id: str) -> bool:
111
+ return claim_id in mind._claims and not self.established(mind, claim_id)
112
+
113
+ def confirm(self, mind: Store, claim_id: str) -> Thought:
114
+ """An action's outcome bore the claim out: establish it (and what now rests on it)."""
115
+ before = self.established(mind, claim_id)
116
+ self.confirmed.add(claim_id)
117
+ return Thought(established=tuple(mind.claim(i) for i in self._closure(mind, [claim_id]))) if not before else Thought()
118
+
119
+ def view(self, mind: Store) -> EstablishedView:
120
+ return EstablishedView(mind, self)
121
+
122
+ def _count(self, claim_id: str, score: Score | None) -> None:
123
+ if score is None or score.value >= self.min_confidence:
124
+ self.frames[claim_id] = self.frames.get(claim_id, 0) + 1
125
+
126
+ def _closure(self, mind: Store, ids: Iterable[str]) -> list[str]:
127
+ """Claims now established among ``ids`` and the derivations resting on them."""
128
+ out, stack, seen = [], [i for i in ids], set()
129
+ while stack:
130
+ cid = stack.pop()
131
+ if cid in seen:
132
+ continue
133
+ seen.add(cid)
134
+ if self.established(mind, cid):
135
+ out.append(cid)
136
+ stack.extend(sorted(mind._dependents.get(cid, ())))
137
+ return list(dict.fromkeys(out))
138
+
139
+
140
+ class EstablishedView:
141
+ """A read-only look at a mind that hides tentative claims (everything else delegates)."""
142
+
143
+ def __init__(self, mind: Store, policy: Corroboration) -> None:
144
+ self._mind, self._policy = mind, policy
145
+
146
+ def __getattr__(self, name: str) -> Any:
147
+ return getattr(self._mind, name)
148
+
149
+ def claims(self, *args: Any, **kwargs: Any) -> list[ClaimRecord]:
150
+ return [r for r in self._mind.claims(*args, **kwargs) if self._policy.established(self._mind, r.id)]
151
+
152
+ def match(self, *patterns: Any, with_support: bool = False) -> list:
153
+ found = [(b, s) for b, s in self._mind.match(*patterns, with_support=True) if all(self._policy.established(self._mind, c) for c in s)]
154
+ return found if with_support else [b for b, _ in found]
155
+
156
+
157
+ def integrate(mind: Store, *fragments: Fragment, remember_retracted: bool = False, corroboration: Corroboration | None = None) -> Thought:
158
+ """Merge fragments into working memory.
159
+
160
+ Snapshot scopes are ephemeral: claims that fall out of a snapshot are retracted and,
161
+ unless ``remember_retracted``, forgotten along with derivations that depended on them.
162
+ With a ``Corroboration`` policy, each fragment is one frame of evidence for its claims
163
+ (see there); ``Thought.established`` lists claims that crossed the threshold.
164
+ """
165
+ policy = corroboration
166
+ was_tentative = {cid for f in fragments for c, _ in f.claims if (cid := c.id) in mind._claims and policy.tentative(mind, cid)} if policy else set()
167
+ edits: list[Any] = []
168
+ for f in fragments:
169
+ edits += [Put(ref, value) for ref, value in f.entities]
170
+ present = set()
171
+ for claim, locator in f.claims:
172
+ if f.snapshot_of is not None and claim.scope != f.snapshot_of:
173
+ raise ValueError(f"snapshot of {f.snapshot_of} contains a claim in scope {claim.scope}")
174
+ present.add(claim.id)
175
+ live = claim.id in mind._claims and not mind._claims[claim.id].retracted
176
+ if policy is not None:
177
+ policy._count(claim.id, f.confidence.get(claim.id))
178
+ if f.snapshot_of is None and claim.predicate in mind.functional:
179
+ edits += [Retract(r.id, "contradicted by a later observation", (Evidence(f.source, f.observed_at, locator, f.method),))
180
+ for r in mind.claims(claim.subject, claim.predicate, scope=claim.scope) if r.claim.object != claim.object and policy.tentative(mind, r.id)]
181
+ if f.snapshot_of is not None and live:
182
+ continue # still perceived; no need to pile up evidence every frame
183
+ edits.append(Tell(claim, (Evidence(f.source, f.observed_at, locator, f.method, f.confidence.get(claim.id)),)))
184
+ if f.snapshot_of is not None:
185
+ edits += [Retract(rec.id, "no longer perceived", (Evidence(f.source, f.observed_at, method=f.method),)) for rec in mind.claims(scope=f.snapshot_of) if rec.id not in present]
186
+ if not edits:
187
+ return _newly_established(mind, policy, was_tentative, ())
188
+ commit = mind.apply(Patch(tuple(edits), mind.revision))
189
+ thought = Thought(tuple(mind.claim(i) for i in commit.added), tuple(mind.claim(i) for i in commit.retracted))
190
+ thought += _newly_established(mind, policy, was_tentative, commit.added)
191
+ if policy is not None:
192
+ for cid in commit.retracted: # contradicted or no longer perceived: the count starts over
193
+ policy.frames.pop(cid, None)
194
+ policy.confirmed.discard(cid)
195
+ if not remember_retracted:
196
+ mind.forget(commit.retracted)
197
+ return thought
198
+
199
+
200
+ def _newly_established(mind: Store, policy: Corroboration | None, was_tentative: set[str], added: Sequence[str]) -> Thought:
201
+ if policy is None or not was_tentative:
202
+ return Thought()
203
+ flipped = [cid for cid in was_tentative if cid not in added and policy.established(mind, cid)]
204
+ return Thought(established=tuple(mind.claim(i) for i in policy._closure(mind, flipped) if i not in added))
205
+
206
+
207
+ @dataclass(frozen=True)
208
+ class Rule:
209
+ """When the patterns match (and at least one matched claim is new), derive claims."""
210
+
211
+ name: str
212
+ when: tuple[tuple[Any, str, Any], ...]
213
+ then: Callable[[Mapping[str, Any], Store], Iterable[Claim | tuple[Claim, Score] | Fragment]]
214
+ version: str = "1" # a Fragment output is an act of reading: integrated as knowledge, not as a derivation
215
+ reacts_to: str = "new_premises" # or "any_change": also fire when something was retracted (e.g. a spinner vanished)
216
+ established_only: bool = False # with a Corroboration policy: premises must be established, and ``then`` sees the established view
217
+
218
+
219
+ @dataclass
220
+ class ThinkStats:
221
+ rounds: int = 0
222
+ firings: int = 0
223
+ derived: int = 0
224
+ ms: float = 0.0
225
+
226
+
227
+ def think(mind: Store, rules: Sequence[Rule], *, since: Thought, max_rounds: int = 8, stats: ThinkStats | None = None, forget_withdrawn: bool = True,
228
+ corroboration: Corroboration | None = None) -> Thought:
229
+ """Bottom-up inference: fire rules on matches that involve something new, until quiet.
230
+
231
+ Semi-naive evaluation: a match whose premises are all old already fired earlier. A claim
232
+ that just became established counts as new for ``established_only`` rules.
233
+ """
234
+ t0 = _time.perf_counter()
235
+ stats = stats if stats is not None else ThinkStats()
236
+ total = Thought()
237
+ frontier = {r.id for r in since.added} | {r.id for r in since.established}
238
+ changed = bool(since.added or since.retracted or since.established)
239
+ view = corroboration.view(mind) if corroboration is not None else None
240
+ for round_no in range(max_rounds):
241
+ if not frontier and not (round_no == 0 and changed):
242
+ break
243
+ stats.rounds += 1
244
+ edits: list[Tell] = []
245
+ readings: list[Fragment] = []
246
+ for rule in rules:
247
+ strict = rule.established_only and view is not None
248
+ for bindings, support in (view if strict else mind).match(*rule.when, with_support=True):
249
+ if frontier.isdisjoint(support) and not (rule.reacts_to == "any_change" and round_no == 0 and changed):
250
+ continue
251
+ stats.firings += 1
252
+ for out in rule.then(bindings, view if strict else mind):
253
+ if isinstance(out, Fragment):
254
+ readings.append(out)
255
+ continue
256
+ claim, score = out if isinstance(out, tuple) else (out, None)
257
+ edits.append(Tell(claim, (Evidence(Ref(f"rule:{rule.name}"), _now(), method=f"derive@{rule.version}", confidence=score, derived_from=tuple(sorted(set(support)))),)))
258
+ if not edits and not readings:
259
+ break
260
+ added_ids: list[str] = []
261
+ if edits:
262
+ added_ids += mind.apply(Patch(tuple(edits), mind.revision)).added
263
+ if readings:
264
+ added_ids += [r.id for r in integrate(mind, *readings).added]
265
+ added = tuple(mind.claim(i) for i in dict.fromkeys(added_ids))
266
+ stats.derived += len(added)
267
+ total += Thought(added)
268
+ frontier = {r.id for r in added}
269
+ stats.ms += (_time.perf_counter() - t0) * 1e3
270
+ return total
271
+
272
+
273
+ def _shown(value: Any) -> Any:
274
+ """How a claim's object reads in an explanation.
275
+
276
+ ``Score`` and friends are worth unwrapping to their number, but a value that carries a
277
+ unit alongside it (a quantity) loses its point when reduced to a bare float: "60.0"
278
+ instead of "60 coin" is exactly the confusion the unit exists to prevent.
279
+ """
280
+ if hasattr(value, "value") and hasattr(value, "unit"):
281
+ return str(value)
282
+ return getattr(value, "value", value)
283
+
284
+
285
+ def explain(mind: Store, claim_id: str, *, depth: int = 6) -> list[str]:
286
+ """Indented lines: the claim, then each line of support down to observations."""
287
+ lines: list[str] = []
288
+
289
+ def show(cid: str, level: int, seen: frozenset[str]) -> None:
290
+ rec = mind._claims.get(cid)
291
+ pad = " " * level
292
+ if rec is None:
293
+ lines.append(f"{pad}(forgotten {cid})")
294
+ return
295
+ c = rec.claim
296
+ obj = _shown(c.object)
297
+ lines.append(f"{pad}{c.subject} {c.predicate} {obj!r}" + (" [retracted]" if rec.retracted else ""))
298
+ if level >= depth or cid in seen:
299
+ return
300
+ for e in rec.evidence:
301
+ conf = f" ({e.confidence.kind} {e.confidence.value:.2f})" if e.confidence else ""
302
+ if e.derived_from:
303
+ how = f" via {e.method}" if e.method else ""
304
+ lines.append(f"{pad} ← {e.source}{conf}{how} from:")
305
+ for p in e.derived_from:
306
+ show(p, level + 2, seen | {cid})
307
+ else:
308
+ lines.append(f"{pad} ← observed in {e.source}" + (f" at {e.locator}" if e.locator else "") + f" via {e.method}{conf}")
309
+
310
+ show(claim_id, 0, frozenset())
311
+ return lines
tensorcode/context.py ADDED
@@ -0,0 +1,97 @@
1
+ """Deterministic context assembly: redundancy removal and budgeted packing.
2
+
3
+ Packing is a lossy conversion, so the result says exactly what was dropped and why.
4
+ Required evidence is never silently dropped: if it cannot fit, the answer is Unknown.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Callable, Generic, Hashable, Sequence, TypeVar
12
+
13
+ from .outcomes import Score, Unknown
14
+
15
+ T = TypeVar("T")
16
+
17
+ _TOKEN = re.compile(r"\w+|[^\w\s]")
18
+
19
+
20
+ def approx_tokens(text: str) -> int:
21
+ """Word-and-punctuation count; a stand-in for a real tokenizer.
22
+
23
+ Measured against the Qwen3 tokenizer on 12,187 HotpotQA sentences: 0.82x the true total
24
+ (per-sentence median 0.84, p5 0.64), so it *undercounts*. Pass a real tokenizer as ``cost``
25
+ when a budget is a hard model limit.
26
+ """
27
+ return len(_TOKEN.findall(text))
28
+
29
+
30
+ def shingle_similarity(a: str, b: str, n: int = 3) -> Score:
31
+ """Jaccard overlap of word n-grams. A similarity, not a probability of duplication."""
32
+
33
+ def grams(s: str) -> set[tuple[str, ...]]:
34
+ words = s.lower().split()
35
+ return {tuple(words[i : i + n]) for i in range(max(1, len(words) - n + 1))}
36
+
37
+ ga, gb = grams(a), grams(b)
38
+ return Score(len(ga & gb) / len(ga | gb) if ga | gb else 0.0, "similarity")
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class Packed(Generic[T]):
43
+ items: tuple[T, ...]
44
+ used: int
45
+ budget: int
46
+ dropped: tuple[tuple[T, str], ...] # (item, reason)
47
+
48
+
49
+ def dedupe(
50
+ ranked: Sequence[tuple[T, Score]],
51
+ *,
52
+ similarity: Callable[[T, T], Score],
53
+ threshold: float,
54
+ keep: Callable[[T], bool] = lambda item: False,
55
+ key: Callable[[T], Hashable] = id,
56
+ ) -> tuple[list[tuple[T, Score]], list[tuple[T, str]]]:
57
+ """Walk in rank order; drop an item too similar to one already kept (unless ``keep`` says otherwise)."""
58
+ kept: list[tuple[T, Score]] = []
59
+ dropped: list[tuple[T, str]] = []
60
+ for item, score in ranked:
61
+ dup = next((k for k, _ in kept if similarity(item, k).value >= threshold), None)
62
+ if dup is not None and not keep(item):
63
+ dropped.append((item, f"near-duplicate of {key(dup)!r}"))
64
+ else:
65
+ kept.append((item, score))
66
+ return kept, dropped
67
+
68
+
69
+ def pack(
70
+ ranked: Sequence[tuple[T, Score]],
71
+ *,
72
+ budget: int,
73
+ cost: Callable[[T], int],
74
+ required: Sequence[T] = (),
75
+ key: Callable[[T], Hashable] = id,
76
+ dropped: Sequence[tuple[T, str]] = (),
77
+ ) -> Packed[T] | Unknown:
78
+ """Required items first, then ranked items greedily while they fit.
79
+
80
+ ``dropped`` carries removals made earlier (e.g. by ``dedupe``) into the same report.
81
+ """
82
+ required_keys = {key(r) for r in required}
83
+ chosen: list[T] = list(required)
84
+ used = sum(cost(r) for r in required)
85
+ if used > budget:
86
+ return Unknown("required_evidence_exceeds_budget", f"required items cost {used} > budget {budget}")
87
+ dropped = list(dropped)
88
+ for item, _ in ranked:
89
+ if key(item) in required_keys:
90
+ continue
91
+ c = cost(item)
92
+ if used + c <= budget:
93
+ chosen.append(item)
94
+ used += c
95
+ else:
96
+ dropped.append((item, f"budget: needs {c}, {budget - used} left"))
97
+ return Packed(tuple(chosen), used, budget, tuple(dropped))