muster-memory 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.
muster/cut.py ADDED
@@ -0,0 +1,291 @@
1
+ """Cutting: store text -> units, the parent of each unit, and its order.
2
+
3
+ Three shapes, chosen by the plan:
4
+ record / prose atoms (blank-line records, or sentences) packed greedily up to `chunk` o200k tokens; an atom is
5
+ split only if it alone exceeds `chunk`. Parents label each unit with the `mscale`-token block its
6
+ first token falls in ("m<k>"), or one parent per unit ("u<i>") when there is no parent layer.
7
+ numbered one unit per "<serial>. <body>" line, serial stripped; order = the serial; parent = the slot the
8
+ record updates ("s:" + entity | relation skeleton). A plan with serial "lead" reads each line's
9
+ own prefix instead ("Step 3: <body>") and keeps it for the render.
10
+ rounds a LongMemEval-style literal [date, turns, date, turns, ...]: one unit per user turn with the
11
+ assistant reply, parent = the session.
12
+
13
+ In exact mode every step is the harness's (harness/lme_pf29.py:1224-1291, 2089-2146, 2232-2247, 1059-1101), and the
14
+ harness's refusals become StoreRefused. Native mode departs only where the harness would crash or split a character.
15
+ """
16
+ import ast
17
+ import os
18
+ import re
19
+ from dataclasses import dataclass, field
20
+ from typing import Optional, Tuple
21
+
22
+ from . import bpe
23
+ from .errors import AssetMissing, PlanError, StoreRefused
24
+ from .stats import LEAD, lead_match
25
+
26
+ _BLANK = re.compile(r"\n\s*\n")
27
+ JOIN = {"blank": "\n\n", "newline": "\n", "sentence": " "}
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class Cut:
32
+ units: Tuple[str, ...]
33
+ keys: Tuple[str, ...] # what is embedded: the unit, except rounds (the user turn)
34
+ parent: tuple # "m%d", "u%d", "s:<slot>" or an int session, as the harness labels them
35
+ order: tuple # the serial on numbered logs, else 0..N-1
36
+ spans: Tuple[Optional[Tuple[int, int]], ...]
37
+ n_o200k: Tuple[int, ...]
38
+ sep: Optional[str]
39
+ diagnostics: dict = field(default_factory=dict)
40
+ prefixes: Optional[Tuple[str, ...]] = None # serial "lead": each line's own prefix, "" where it has none
41
+
42
+ def __len__(self):
43
+ return len(self.units)
44
+
45
+
46
+ # ---------------------------------------------------------------- atoms
47
+ def _ensure_punkt(exact):
48
+ import nltk
49
+ d = os.path.join(bpe.asset_dir(), "nltk_data")
50
+ if os.path.isdir(d) and d not in nltk.data.path:
51
+ nltk.data.path.insert(0, d)
52
+ try:
53
+ nltk.data.find("tokenizers/punkt_tab")
54
+ except LookupError:
55
+ if exact:
56
+ raise AssetMissing("sentence splitting needs nltk's punkt_tab; run `muster fetch`")
57
+ os.makedirs(d, exist_ok=True)
58
+ nltk.download("punkt_tab", download_dir=d, quiet=True)
59
+ if d not in nltk.data.path:
60
+ nltk.data.path.insert(0, d)
61
+ return nltk
62
+
63
+
64
+ def _with_spans(text, pieces):
65
+ out, cur = [], 0
66
+ for p in pieces:
67
+ j = text.find(p, cur)
68
+ if j < 0:
69
+ out.append((p, None))
70
+ else:
71
+ out.append((p, (j, j + len(p))))
72
+ cur = j + len(p)
73
+ return out
74
+
75
+
76
+ def atoms(text: str, kind: str, *, exact: bool = False):
77
+ """[(atom, (start, end) or None)] for kind blank, newline or sentence (lme_pf29.py:1224-1236)."""
78
+ if kind == "blank":
79
+ out, pos = [], 0
80
+ for m in _BLANK.finditer(text):
81
+ piece = text[pos:m.start()]
82
+ if piece.strip():
83
+ out.append((piece, (pos, m.start())))
84
+ pos = m.end()
85
+ piece = text[pos:]
86
+ if piece.strip():
87
+ out.append((piece, (pos, len(text))))
88
+ return out
89
+ if kind == "newline":
90
+ out, pos = [], 0
91
+ for piece in text.split("\n"):
92
+ if piece.strip():
93
+ out.append((piece, (pos, pos + len(piece))))
94
+ pos += len(piece) + 1
95
+ return out
96
+ if kind == "sentence":
97
+ nltk = _ensure_punkt(exact)
98
+ return _with_spans(text, nltk.sent_tokenize(text))
99
+ raise PlanError("unknown atom kind %r" % kind)
100
+
101
+
102
+ # ---------------------------------------------------------------- packing
103
+ def _special_strings(enc, text):
104
+ return sorted(s for s in enc.special_tokens_set if s in text)
105
+
106
+
107
+ def pack(atom_list, chunk: int, joiner: str, *, exact: bool = False):
108
+ """Greedy packing (lme_pf29.py:1255-1291). Returns [(unit, [atom indices])] and the oversize-atom count."""
109
+ enc = bpe.o200k()
110
+ out, cur, curi, n, over = [], [], [], 0, 0
111
+ for i, a in enumerate(atom_list):
112
+ if exact:
113
+ t = bpe.encode_pack(enc, a)
114
+ else:
115
+ t = enc.encode(a, disallowed_special=())
116
+ if len(t) > chunk:
117
+ over += 1
118
+ if cur:
119
+ out.append((joiner.join(cur), curi))
120
+ cur, curi, n = [], [], 0
121
+ if exact:
122
+ for o in range(0, len(t), chunk):
123
+ out.append((enc.decode(t[o:o + chunk]), [i]))
124
+ else: # N5: slice on character boundaries, so the slices rebuild the atom
125
+ _, offs = enc.decode_with_offsets(t)
126
+ cuts = [offs[o] for o in range(0, len(t), chunk)] + [len(a)]
127
+ for s, e in zip(cuts, cuts[1:]):
128
+ if e > s:
129
+ out.append((a[s:e], [i]))
130
+ continue
131
+ if n + len(t) > chunk and cur:
132
+ out.append((joiner.join(cur), curi))
133
+ cur, curi, n = [], [], 0
134
+ cur.append(a)
135
+ curi.append(i)
136
+ n += len(t)
137
+ if cur:
138
+ out.append((joiner.join(cur), curi))
139
+ return out, over
140
+
141
+
142
+ # ---------------------------------------------------------------- numbered logs (AUTOSTRUCT, AS_SLOT=5)
143
+ _SERIAL = re.compile(r"^\s*(\d+)\.\s+(.+)$", re.DOTALL)
144
+ _CAPRE = re.compile(r"\b([A-Z][\w.'-]+(?:\s+(?:of|the|and|de|van|von|di)?\s*[A-Z][\w.'-]+)*)\b")
145
+ _CAPSTOP = {"The", "A", "An", "In", "On", "At", "Of", "This", "That", "These", "Those", "It", "Its", "Which",
146
+ "What", "Who", "Where", "When", "There", "He", "She", "They", "His", "Her", "Their"}
147
+ AS_MINREC = 0.8
148
+
149
+
150
+ def entities(t):
151
+ """Capitalised spans, minus sentence-initial function words (lme_pf29.py:2107-2110)."""
152
+ return [m.group(1) for m in _CAPRE.finditer(t or "") if m.group(1).split()[0] not in _CAPSTOP]
153
+
154
+
155
+ def slot_of(body):
156
+ """The slot a numbered record updates: its first entity, then the sentence with every entity masked."""
157
+ e = entities(body)
158
+ sub = re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]", " ", e[0].lower())).strip() if e else ""
159
+ sk = re.sub(r"\s+", " ", _CAPRE.sub("_", body or "")).strip().rstrip(".").strip()
160
+ return sub + "|" + sk
161
+
162
+
163
+ def _numbered(text, exact, serial="dot"):
164
+ lines = [ln for ln in text.split("\n") if ln.strip()]
165
+ lead = serial == "lead"
166
+ ms = [(lead_match(ln) if not exact else LEAD.match(ln)) if lead else _SERIAL.match(ln) for ln in lines]
167
+ share = sum(1 for x in ms if x) / float(max(len(lines), 1))
168
+ if share < AS_MINREC:
169
+ if exact:
170
+ raise StoreRefused("numbered log: only %.3f of lines are `<serial>. <body>` (needs %.1f; lme_pf29.py:2126)"
171
+ % (share, AS_MINREC))
172
+ raise PlanError("not a numbered log: %.3f of all lines carry a %sserial" % (share, "leading " if lead else ""))
173
+ units, order, parent, prefixes = [], [], [], []
174
+ unmarked = []
175
+ for x, ln in zip(ms, lines):
176
+ if x and lead:
177
+ ser, body, pre = int(x.group(2)), ln[x.end():].strip(), ln[:x.end()]
178
+ elif x:
179
+ ser, body, pre = int(x.group(1)), x.group(2).strip(), None
180
+ else:
181
+ ser, body, pre = len(units), ln.strip(), ""
182
+ if lead and not exact:
183
+ ser, body = -1, ln # preserve the literal line; no invented revision clock
184
+ unmarked.append(len(units))
185
+ units.append(body)
186
+ order.append(ser)
187
+ parent.append("u:rawline%d" % (len(units) - 1) if lead and not exact and x is None else "s:" + slot_of(body))
188
+ prefixes.append(pre)
189
+ spans = tuple(sp for _, sp in _with_spans(text, lines))
190
+ enc = bpe.o200k()
191
+ serials = [o for o, x in zip(order, ms) if x]
192
+ diag = dict(numbered_share=round(share, 4), distinct_serial_share=round(len(set(serials)) / float(max(len(serials), 1)), 4),
193
+ n_parents=len(set(parent)))
194
+ if unmarked:
195
+ diag["unmarked_records"] = unmarked
196
+ return Cut(tuple(units), tuple(units), tuple(parent), tuple(order), spans,
197
+ tuple(bpe.count(enc, u) for u in units), "\n", diag, tuple(prefixes) if lead else None)
198
+
199
+
200
+ # ---------------------------------------------------------------- rounds (LongMemEval literal)
201
+ def sessions_from_literal(literal: str):
202
+ """[(date, turns)] from "[date, turns, date, turns, ...]" (lme_pf29.py:1059-1072)."""
203
+ lit = ast.literal_eval(literal)
204
+ out, i = [], 0
205
+ while i < len(lit):
206
+ if isinstance(lit[i], str) and i + 1 < len(lit) and isinstance(lit[i + 1], list):
207
+ out.append((lit[i], lit[i + 1]))
208
+ i += 2
209
+ else:
210
+ i += 1
211
+ return out
212
+
213
+
214
+ def rounds_of(sessions):
215
+ """One unit per user turn with the assistant turn that answers it (lme_pf29.py:1075-1101)."""
216
+ rtext, rkey, sess_id, rdate = [], [], [], []
217
+ for si, (date, turns) in enumerate(sessions):
218
+ for ti, t in enumerate(turns):
219
+ if not isinstance(t, dict) or t.get("role") != "user":
220
+ continue
221
+ nxt = ""
222
+ if ti + 1 < len(turns) and isinstance(turns[ti + 1], dict) and turns[ti + 1].get("role") != "user":
223
+ nxt = turns[ti + 1].get("content", "")
224
+ rtext.append(("[" + date + "] " + t.get("content", "") + "\n" + nxt).strip())
225
+ rkey.append(t.get("content", "").strip())
226
+ sess_id.append(si)
227
+ rdate.append(date)
228
+ return rtext, rkey, sess_id, rdate
229
+
230
+
231
+ def _rounds(literal):
232
+ sess = sessions_from_literal(literal)
233
+ rtext, rkey, sid, rdate = rounds_of(sess)
234
+ enc = bpe.o200k()
235
+ return Cut(tuple(rtext), tuple(rkey), tuple(sid), tuple(range(len(rtext))), tuple([None] * len(rtext)),
236
+ tuple(bpe.count(enc, u) for u in rtext), None, dict(n_sessions=len(sess), dates=tuple(rdate)))
237
+
238
+
239
+ # ---------------------------------------------------------------- entry point
240
+ def cut(text: str, plan, *, exact: Optional[bool] = None) -> Cut:
241
+ """Cut `text` under `plan`. `exact` defaults to plan.mode == "exact"."""
242
+ exact = (plan.mode == "exact") if exact is None else exact
243
+ if plan.branch == "empty":
244
+ return Cut((), (), (), (), (), (), None, {})
245
+ if plan.branch == "numbered":
246
+ return _numbered(text, exact, plan.serial)
247
+ if plan.branch == "rounds":
248
+ return _rounds(text)
249
+ enc = bpe.o200k()
250
+ if exact:
251
+ bad = [s for s in _special_strings(enc, text) if s != "<|endoftext|>"]
252
+ if bad:
253
+ raise StoreRefused("the store contains special-token text %r, which the harness's packer cannot encode "
254
+ "(lme_pf29.py:1273)" % bad[0])
255
+ at = atoms(text, plan.atom, exact=exact)
256
+ packed, over = pack([a for a, _ in at], plan.chunk, JOIN[plan.atom], exact=exact)
257
+ units = [u for u, _ in packed]
258
+ spans = []
259
+ for _, idx in packed:
260
+ s, e = at[idx[0]][1], at[idx[-1]][1]
261
+ spans.append((s[0], e[1]) if (s and e) else None)
262
+ diag = dict(atoms=len(at), oversize_atoms=over)
263
+ if plan.mscale:
264
+ if exact and not units:
265
+ raise StoreRefused("MSCALE produced no units (lme_pf29.py:2239)")
266
+ parent, cum = [], 0
267
+ for u in units:
268
+ parent.append("m%d" % (cum // plan.mscale))
269
+ if exact:
270
+ try:
271
+ cum += len(bpe.encode_label(enc, u))
272
+ except ValueError as e:
273
+ raise StoreRefused("a unit holds special-token text the harness's parent labels cannot encode "
274
+ "(lme_pf29.py:2237): %s" % e)
275
+ else:
276
+ cum += bpe.count(enc, u)
277
+ if len(set(parent)) >= len(units):
278
+ if exact:
279
+ raise StoreRefused("MSCALE produced a flat layer (%d units, %d parents) (lme_pf29.py:2240-2243)"
280
+ % (len(units), len(set(parent))))
281
+ parent = ["u%d" % i for i in range(len(units))]
282
+ diag["flat_parents"] = True
283
+ if exact:
284
+ ns = lambda t: len(re.sub(r"\s+", "", t))
285
+ lost = ns(text) - ns("".join(units))
286
+ if abs(lost) > 0.01 * max(1, ns(text)):
287
+ raise StoreRefused("MSCALE lost or duplicated text: %d non-space characters (lme_pf29.py:2244-2247)" % lost)
288
+ else:
289
+ parent = ["u%d" % i for i in range(len(units))]
290
+ return Cut(tuple(units), tuple(units), tuple(parent), tuple(range(len(units))), tuple(spans),
291
+ tuple(bpe.count(enc, u) for u in units), "\n\n" if plan.atom != "newline" else "\n", diag)
muster/derive.py ADDED
@@ -0,0 +1,146 @@
1
+ """derive: a store's plan from its own text, with no model, no query and nothing to tune.
2
+
3
+ The rule (paper, Algorithm 1, Part I; thresholds fixed before any experiment, harness/derive_config.py):
4
+
5
+ numbered log >= 80% of lines open with a serial "12. ..." -> one unit per record, parent = the slot it updates,
6
+ the newest serial of a slot wins
7
+ record store >= 90% of the text sits in blank-line records of at most the reference chunk P
8
+ -> units of max(P/8, 512) tokens with records kept whole
9
+ prose otherwise -> units of P/4 tokens (step 8: the P-token unit searched at a quarter of its size with four
10
+ times as many units, so the token budget is unchanged)
11
+
12
+ Parents are 8 units, or 4 or 2 if fewer than ten parents would remain; else there is no parent layer.
13
+
14
+ Added after the paper (2026-11, thresholds set on public benchmark logs; `derive` only, not `derive_collection` or the
15
+ presets): a store the serial fraction misses is still a numbered log when >= 95% of its lines after the first carry a
16
+ leading serial one above the previous line's, written in the log's own style ("Step 3: ..."), and its median line is
17
+ at most 20 tokens. The serial is read from that prefix, and the reader sees each line as written.
18
+ """
19
+ from typing import Iterable, Sequence, Union
20
+ from dataclasses import replace
21
+
22
+ from .errors import PlanError
23
+ from .plan import Plan
24
+ from .records import DEFAULT_TEMPLATE, as_record, normalize_newlines, serialize
25
+ from .record_serial import body_serial_stats
26
+ from .stats import LEAD_LINE_TOKENS, LEAD_TAU, Stats, collection_stats, lead_serial, store_stats
27
+
28
+ NUMBERED_TAU, RECORD_TAU, MIN_PARENTS = 0.80, 0.90, 10
29
+ RECORD_STEP, MIN_UNIT, PROSE_STEP = 8, 512, 4
30
+ U_DEFAULT = {"numbered": 10, "record": 10, "prose": 40}
31
+
32
+
33
+ def store_text(store, *, template=DEFAULT_TEMPLATE, normalize=True):
34
+ """(text, record spans or None, records or None) for a string or an iterable of records."""
35
+ if isinstance(store, str):
36
+ return (normalize_newlines(store) if normalize else store), None, None
37
+ recs = [as_record(x) for x in store]
38
+ text, spans = serialize(recs, template=template, normalize=normalize)
39
+ return text, spans, recs
40
+
41
+
42
+ def _parents(units_est, chunk):
43
+ for m in (8, 4, 2):
44
+ if units_est // m >= MIN_PARENTS:
45
+ return chunk * m, units_est // m, m
46
+ return 0, 0, None
47
+
48
+
49
+ def _plan(branch, chunk, units_est, st: Stats, P, trace, serial="dot"):
50
+ if branch == "numbered":
51
+ if serial == "dot":
52
+ trace.append("serial fraction %.4f >= %.2f: numbered log; one unit per record, parent = slot, newest "
53
+ "serial wins" % (st.serial, NUMBERED_TAU))
54
+ return Plan(branch="numbered", atom=None, chunk=None, mscale=0, U=U_DEFAULT["numbered"],
55
+ policy="attprio_sess", render="numbered", supersede="numbered", reference_chunk=P,
56
+ stats=st.to_dict(), trace=tuple(trace), serial=serial)
57
+ atom = "blank" if branch == "record" else "sentence"
58
+ ms, npar, m = _parents(units_est, chunk)
59
+ trace.append("%d units estimated at %d tokens -> %s" % (
60
+ units_est, chunk, ("parents of %d units (%d tokens), %d of them" % (m, ms, npar)) if ms
61
+ else "no parent layer (fewer than %d parents at 2 units each)" % MIN_PARENTS))
62
+ return Plan(branch=branch, atom=atom, chunk=chunk, mscale=ms, U=U_DEFAULT[branch], policy="flrs", render="r4",
63
+ reference_chunk=P, stats=st.to_dict(), trace=tuple(trace))
64
+
65
+
66
+ def derive(store, *, reference_chunk: int = 4096, template: str = DEFAULT_TEMPLATE,
67
+ normalize_newlines: bool = True) -> Plan:
68
+ """The plan of one store, with the documented native serial-format extension."""
69
+ P = int(reference_chunk)
70
+ text, _, recs = store_text(store, template=template, normalize=normalize_newlines)
71
+ st = store_stats(text, P)
72
+ body_stats = body_serial_stats(recs, normalize=normalize_newlines) if recs is not None else None
73
+ if body_stats is not None:
74
+ st = replace(st, serial=body_stats[0]) # metadata never supplies a record serial
75
+ trace = ["serial fraction %.4f, record share %.4f, %d tokens (cl100k_base)" % (st.serial, st.record_share, st.tokens)]
76
+ if st.tokens == 0:
77
+ trace.append("empty store")
78
+ return Plan(branch="empty", atom=None, chunk=None, mscale=0, U=0, policy="flr", render="flat",
79
+ reference_chunk=P, stats=st.to_dict(), trace=tuple(trace))
80
+ if st.serial >= NUMBERED_TAU:
81
+ if body_stats is not None:
82
+ trace.append("Serials are read only at the start of original record bodies; metadata is retrieval/display data.")
83
+ return _plan("numbered", None, 0, st, P, trace)
84
+ share, med = lead_serial(text) if body_stats is None else body_stats[1:]
85
+ if med is not None and med <= LEAD_LINE_TOKENS:
86
+ if body_stats is None:
87
+ trace.append("leading serial one above the previous line's on %.4f >= %.2f of lines, median line %g <= %d "
88
+ "tokens: numbered log under its own spelling; one unit per record, parent = slot, newest serial "
89
+ "wins" % (share, LEAD_TAU, med, LEAD_LINE_TOKENS))
90
+ else:
91
+ trace.append("leading body serial one above the previous record's on %.4f >= %.2f of original record "
92
+ "pairs, median entire record body %g <= %d tokens: numbered records; metadata kept for "
93
+ "retrieval/display, not slot/order" % (share, LEAD_TAU, med, LEAD_LINE_TOKENS))
94
+ return _plan("numbered", None, 0, st, P, trace, serial="lead")
95
+ if st.record_share >= RECORD_TAU:
96
+ chunk = max(P // RECORD_STEP, MIN_UNIT)
97
+ trace.append("record share %.4f >= %.2f: record store; units of %d tokens, records kept whole"
98
+ % (st.record_share, RECORD_TAU, chunk))
99
+ return _plan("record", chunk, st.tokens // chunk, st, P, trace)
100
+ chunk = P // PROSE_STEP if P > MIN_UNIT else P
101
+ trace.append("record share %.4f < %.2f: prose; units of %d tokens (P/%d, step 8)"
102
+ % (st.record_share, RECORD_TAU, chunk, PROSE_STEP))
103
+ return _plan("prose", chunk, st.tokens // chunk, st, P, trace)
104
+
105
+
106
+ def derive_collection(stores: Sequence[str], *, reference_chunk: int = 4096, sample: int = 3, step8: bool = True,
107
+ mode: str = "native") -> Plan:
108
+ """One plan for a benchmark cell from its first `sample` stores (harness/derive_config.py, then step 8)."""
109
+ P = int(reference_chunk)
110
+ if P not in (512, 4096):
111
+ raise PlanError("reference_chunk must be 512 or 4096, MemoryAgentBench's protocol sizes; got %d" % P)
112
+ st = collection_stats(stores, P, sample=sample, mode=mode)
113
+ trace = ["%d stores: serial fraction %.4f (mean), record share %.4f (pooled), %d tokens per store"
114
+ % (st.rows, st.serial, st.record_share, st.tokens)]
115
+ if st.serial >= NUMBERED_TAU:
116
+ return _plan("numbered", None, 0, st, P, trace)
117
+ record = st.record_share >= RECORD_TAU
118
+ chunk = P // RECORD_STEP if (record and P // RECORD_STEP >= MIN_UNIT) else P
119
+ if not record and step8 and P > MIN_UNIT:
120
+ chunk = P // PROSE_STEP
121
+ trace.append("%s; units of %d tokens" % ("record store" if record else "prose", chunk))
122
+ return _plan("record" if record else "prose", chunk, max(1, st.tokens // chunk), st, P, trace)
123
+
124
+
125
+ def explain(plan: Plan) -> str:
126
+ """A plain description of a plan, then the decision step by step."""
127
+ rows = [("store", {"numbered": "numbered log", "record": "record store", "prose": "prose", "rounds": "conversation rounds",
128
+ "empty": "empty"}[plan.branch])]
129
+ if plan.source == "declared-order:record-boundaries-v1":
130
+ rows += [("units", "one per declared record"), ("parents", "one per source occurrence"),
131
+ ("order", "declared source sequence; all versions kept")]
132
+ elif plan.branch == "numbered":
133
+ rows += [("units", "one per record"), ("parents", "one per slot"), ("order", "newest per slot wins")]
134
+ if plan.serial == "lead":
135
+ rows.append(("serials", "each line's own prefix, shown as written"))
136
+ elif plan.branch in ("record", "prose"):
137
+ rows += [("units", "%s tokens%s" % (format(plan.chunk, ","), ", records whole" if plan.atom == "blank" else "")),
138
+ ("parents", ("%s tokens" % format(plan.mscale, ",")) if plan.mscale else "none")]
139
+ elif plan.branch == "rounds":
140
+ rows += [("units", "one user turn with its answer"), ("parents", "one per session")]
141
+ rows += [("read", "%d units per query" % plan.U), ("mode", plan.mode), ("source", plan.source)]
142
+ out = [" %-10s %s" % r for r in rows]
143
+ if plan.trace:
144
+ out.append(" how:")
145
+ out += [" - " + t for t in plan.trace]
146
+ return "\n".join(out)
muster/errors.py ADDED
@@ -0,0 +1,39 @@
1
+ """Every error the library raises is a MusterError, so a caller can catch one class."""
2
+
3
+
4
+ class MusterError(Exception):
5
+ """Base class."""
6
+
7
+
8
+ class PlanError(MusterError, ValueError):
9
+ """A plan that cannot be built or does not fit the store it was given."""
10
+
11
+
12
+ class RecordError(MusterError, ValueError):
13
+ """A record the library cannot read (missing text, ambiguous time, multi-line numbered text)."""
14
+
15
+
16
+ class StoreRefused(MusterError):
17
+ """Exact mode refuses a store or a step that the paper's harness refuses or would crash on.
18
+
19
+ The message names the harness line, so the refusal can be checked against the code that produced the paper."""
20
+
21
+
22
+ class EnvironmentRefused(MusterError):
23
+ """Exact mode needs the gate machine's environment (hash seed off, pinned versions, pinned models)."""
24
+
25
+
26
+ class AssetMissing(MusterError):
27
+ """A tokenizer or sentence-splitter file is missing or does not match its pinned hash. Run `muster fetch`."""
28
+
29
+
30
+ class StaleSelection(MusterError):
31
+ """A selection made on one version of a memory was rendered on another."""
32
+
33
+
34
+ class PlanViolation(MusterError):
35
+ """An add would break a frozen exact plan; nothing was changed."""
36
+
37
+
38
+ class StoreLocked(MusterError):
39
+ """Another process holds the store's writer lock."""
muster/index.py ADDED
@@ -0,0 +1,102 @@
1
+ """The model-free index of a store: entities, the entity graph, IDF, slots, positions and BM25.
2
+
3
+ Every structure is built with the harness's own expressions (src/real_e1_conflict_killer.py:26-54,
4
+ harness/lme_pf29.py:2466-2525, 2592-2596; core2.py:40-61, 429-438). In exact mode the sets are kept as sets,
5
+ because their iteration order under the paper's hash seed is part of the published output; native mode reads them
6
+ in a canonical order wherever order can matter (muster/search.py).
7
+ """
8
+ import collections
9
+ import math
10
+ import re
11
+
12
+ _CAP = re.compile(r"\b([A-Z][\w.'-]+(?:\s+(?:of|the|and|de|van|von|di)?\s*[A-Z][\w.'-]+)*)\b")
13
+ HUB_FRAC = 0.05
14
+ ORDER_DISTINCT = 0.99
15
+
16
+
17
+ def entities(text):
18
+ """Capitalised spans of more than two characters, as a set."""
19
+ return {m.strip() for m in _CAP.findall(text or "") if len(m.strip()) > 2}
20
+
21
+
22
+ def build_entity_index(fact_ents, n_facts, hub_frac=HUB_FRAC):
23
+ """({entity: set of units}, hubs). An entity in more than max(3, 5% of units) units is a hub and leaves the index."""
24
+ ent_facts = {}
25
+ for i, es in enumerate(fact_ents):
26
+ for e in es:
27
+ ent_facts.setdefault(e, set()).add(i)
28
+ hub_cut = max(3, int(hub_frac * n_facts))
29
+ hubs = {e for e, fs in ent_facts.items() if len(fs) > hub_cut}
30
+ return {e: fs for e, fs in ent_facts.items() if e not in hubs}, hubs
31
+
32
+
33
+ def slotkey(a):
34
+ return " ".join(re.sub(r"[^a-z0-9 ]", " ", str(a).lower()).split())
35
+
36
+
37
+ def btok(t):
38
+ """BM25 tokens: lower case, punctuation to spaces, split on white space (BM25LC=1)."""
39
+ t = t.lower()
40
+ t = "".join(c if (c.isalnum() or c.isspace()) else " " for c in t)
41
+ return t.split()
42
+
43
+
44
+ def newest_per_parent(pool, parent_of, order_of):
45
+ newest = {}
46
+ for i in pool:
47
+ k = parent_of[i]
48
+ if k is not None and (k not in newest or order_of[i] > order_of[newest[k]]):
49
+ newest[k] = i
50
+ return newest
51
+
52
+
53
+ class Index:
54
+ """Built once per store version; a pure function of its units, keys, parents and order."""
55
+
56
+ def __init__(self, units, keys, parent, order, *, has_order=False):
57
+ N = self.N = len(units)
58
+ self.units, self.keys, self.parent = list(units), list(keys), list(parent)
59
+ self.r_ents = [entities(t) for t in self.keys]
60
+ self.ef, self.hubs = build_entity_index(self.r_ents, N)
61
+ self.idf = {e: math.log(N / max(len(v), 1)) for e, v in self.ef.items()}
62
+ self.r_slot = [slotkey(t) for t in self.keys]
63
+ sm = collections.defaultdict(list)
64
+ for i, s in enumerate(self.r_slot):
65
+ if s:
66
+ sm[s].append(i)
67
+ self.newest_of_slot = {k: max(v) for k, v in sm.items()}
68
+ # the order a structural resolver may trust: declared (numbered logs), and separating almost every unit
69
+ self.order = list(order) if has_order else list(range(N))
70
+ order_ok = has_order and len(set(self.order)) >= ORDER_DISTINCT * N
71
+ self.structural = bool(order_ok and len(set(self.parent)) < N)
72
+ self.children_of = {}
73
+ for i, p in enumerate(self.parent):
74
+ if p is not None:
75
+ self.children_of.setdefault(p, []).append(i)
76
+ self.pos_of, self.at = {}, {}
77
+ cnt = collections.Counter()
78
+ for i, si in enumerate(self.parent):
79
+ self.pos_of[i] = cnt[si]
80
+ self.at[(si, cnt[si])] = i
81
+ cnt[si] += 1
82
+ self.newest_row = newest_per_parent(range(N), self.parent, self.order) if self.structural else {}
83
+ self.adj_slot = {}
84
+ if self.structural:
85
+ ent_slots = collections.defaultdict(set)
86
+ for s2, i2 in self.newest_row.items():
87
+ for e2 in (self.r_ents[i2] - self.hubs):
88
+ ent_slots[e2].add(s2)
89
+ cut = max(3, int(HUB_FRAC * max(len(self.newest_row), 1)))
90
+ ent_slots = {e2: v2 for e2, v2 in ent_slots.items() if len(v2) <= cut}
91
+ self.adj_slot = collections.defaultdict(set)
92
+ for e2, ss2 in ent_slots.items():
93
+ for s2 in ss2:
94
+ self.adj_slot[s2] |= (ss2 - {s2})
95
+ self._bm25 = None
96
+
97
+ @property
98
+ def bm25(self):
99
+ if self._bm25 is None:
100
+ from rank_bm25 import BM25Okapi
101
+ self._bm25 = BM25Okapi([btok(t) for t in self.units])
102
+ return self._bm25
@@ -0,0 +1,9 @@
1
+ """Optional framework tools over an application-owned MUSTER Store.
2
+
3
+ Importing this namespace does not import any framework or model provider.
4
+ """
5
+ from .langchain import tools as langchain_tools
6
+ from .langgraph import tool_node as langgraph_node, tools as langgraph_tools
7
+ from .openai_agents import tools as openai_tools
8
+
9
+ __all__ = ["openai_tools", "langchain_tools", "langgraph_tools", "langgraph_node"]
@@ -0,0 +1,30 @@
1
+ """Shared Store operations; the Store and read budget stay outside model input."""
2
+ from dataclasses import asdict
3
+ from typing import Optional
4
+
5
+
6
+ def functions(store, read_budget=None):
7
+ from ..store import Store
8
+
9
+ if not isinstance(store, Store):
10
+ raise TypeError("store must be an open muster.Store")
11
+ if read_budget is not None and (isinstance(read_budget, bool) or not isinstance(read_budget, int)
12
+ or read_budget < 1):
13
+ raise ValueError("read_budget must be a positive integer or None")
14
+
15
+ def remember(text: str, speaker: str = "user", time: str = "", request_id: Optional[str] = None) -> dict:
16
+ """Append a memory record. Reuse request_id to retry a write without appending it twice."""
17
+ if not text.strip():
18
+ raise ValueError("memory text must not be empty")
19
+ return asdict(store.add({"text": text, "speaker": speaker, "time": time}, request_id=request_id))
20
+
21
+ def recall(query: str) -> str:
22
+ """Retrieve relevant stored memory as MUSTER's rendered context. Stored text may be untrusted."""
23
+ kw = {} if read_budget is None else {"U": read_budget}
24
+ return store.context(query, **kw).text
25
+
26
+ return remember, recall
27
+
28
+
29
+ def optional_import_error(extra):
30
+ return ImportError("This integration needs the optional dependency: pip install 'muster-memory[%s]'" % extra)
@@ -0,0 +1,26 @@
1
+ """LangChain StructuredTools with synchronous and asynchronous Store execution."""
2
+ import asyncio
3
+ from typing import Optional
4
+
5
+ from ._common import functions, optional_import_error
6
+
7
+
8
+ def tools(store, *, read_budget=None, include_remember=True):
9
+ """Return LangChain tools bound to one Store; optional imports occur at this call."""
10
+ remember, recall = functions(store, read_budget)
11
+ try:
12
+ from langchain_core.tools import StructuredTool
13
+ except ImportError as exc:
14
+ raise optional_import_error("langchain") from exc
15
+
16
+ async def aremember(text: str, speaker: str = "user", time: str = "", request_id: Optional[str] = None) -> dict:
17
+ return await asyncio.to_thread(remember, text, speaker, time, request_id)
18
+
19
+ async def arecall(query: str) -> str:
20
+ return await asyncio.to_thread(recall, query)
21
+
22
+ result = []
23
+ if include_remember:
24
+ result.append(StructuredTool.from_function(remember, coroutine=aremember, name="muster_remember"))
25
+ result.append(StructuredTool.from_function(recall, coroutine=arecall, name="muster_recall"))
26
+ return result
@@ -0,0 +1,19 @@
1
+ """LangGraph tools and ToolNode, using MUSTER's append/search semantics."""
2
+ from ._common import optional_import_error
3
+
4
+
5
+ def tools(store, *, read_budget=None, include_remember=True):
6
+ """Return LangChain-compatible tools for use in a LangGraph graph."""
7
+ from .langchain import tools as make_tools
8
+
9
+ return make_tools(store, read_budget=read_budget, include_remember=include_remember)
10
+
11
+
12
+ def tool_node(store, *, read_budget=None, include_remember=True):
13
+ """Create a ToolNode that executes remember and recall against the supplied Store."""
14
+ try:
15
+ from langgraph.prebuilt import ToolNode
16
+ except ImportError as exc:
17
+ raise optional_import_error("langgraph") from exc
18
+ return ToolNode(tools(store, read_budget=read_budget, include_remember=include_remember),
19
+ handle_tool_errors=False)