| """Three tool-call-memory arms. |
| |
| An arm exposes two operations: |
| - draft(query, functions, user_id) -> canonical draft string |
| - observe(query, functions, user_id, target_name, target_args) -> None |
| record the genuine target so future drafts can reuse it. |
| |
| Arms: |
| 1. NoMemory -- schema-only draft (best you can do with zero history). |
| 2. StaticGlobal -- ToolSpec-style: one global datastore, built during warmup |
| and then FROZEN (no growth, no eviction, no per-user view). |
| 3. PersonalMemory (ours) -- per-user store that grows across sessions, with an |
| eviction policy (LRU or LFU) capping per-user size, and |
| personalized retrieval (query only the user's own store, |
| backing off to a small shared store when empty). |
| |
| Retrieval is top-1 cosine similarity over sentence-transformer embeddings of the |
| query text. The drafted call is the canonicalized (name, arguments) of the most |
| similar past observation. |
| """ |
| from __future__ import annotations |
|
|
| import re |
| from collections import OrderedDict |
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
| import numpy as np |
|
|
| from .metrics import canonical_call_str |
|
|
|
|
| |
| |
| |
| class Embedder: |
| def __init__(self, name: str = "sentence-transformers/all-MiniLM-L6-v2"): |
| from sentence_transformers import SentenceTransformer |
| self.model = SentenceTransformer(name, device="cpu") |
| self._cache: dict[str, np.ndarray] = {} |
|
|
| def embed(self, text: str) -> np.ndarray: |
| v = self._cache.get(text) |
| if v is None: |
| v = self.model.encode(text, normalize_embeddings=True) |
| v = np.asarray(v, dtype=np.float32) |
| self._cache[text] = v |
| return v |
|
|
|
|
| def schema_draft(functions: list[dict[str, Any]]) -> str: |
| """Zero-history draft: the first tool's signature with placeholder args.""" |
| if not functions: |
| return "" |
| f = functions[0] |
| props = (f.get("parameters", {}) or {}).get("properties", {}) or {} |
| required = (f.get("parameters", {}) or {}).get("required", []) or list(props) |
| args = {k: None for k in required} |
| return canonical_call_str(f["name"], args) |
|
|
|
|
| |
| |
| |
| @dataclass |
| class Entry: |
| emb: np.ndarray |
| call: str |
| freq: int = 1 |
|
|
|
|
| def _best_match(emb: np.ndarray, entries: list[Entry]) -> tuple[int, float]: |
| if not entries: |
| return -1, -1.0 |
| mat = np.stack([e.emb for e in entries]) |
| sims = mat @ emb |
| i = int(np.argmax(sims)) |
| return i, float(sims[i]) |
|
|
|
|
| |
| |
| |
| class NoMemory: |
| name = "no_memory" |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| return schema_draft(functions) |
|
|
| def observe(self, *a, **k): |
| return None |
|
|
|
|
| class StaticGlobal: |
| """ToolSpec-style frozen global datastore.""" |
| name = "static_global" |
|
|
| def __init__(self): |
| self.entries: list[Entry] = [] |
| self.frozen = False |
|
|
| def freeze(self): |
| self.frozen = True |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| emb = embedder.embed(query) |
| i, _ = _best_match(emb, self.entries) |
| if i < 0: |
| return schema_draft(functions) |
| return self.entries[i].call |
|
|
| def observe(self, query, functions, user_id, name, args, embedder) -> None: |
| if self.frozen: |
| return |
| self.entries.append(Entry(embedder.embed(query), |
| canonical_call_str(name, args))) |
|
|
|
|
| class PersonalMemory: |
| """Ours: per-user growing store + eviction + personalization.""" |
| name = "personal_memory" |
|
|
| def __init__(self, capacity: int = 32, eviction: str = "lru", |
| sim_threshold: float = 0.35): |
| self.capacity = capacity |
| self.eviction = eviction |
| self.sim_threshold = sim_threshold |
| |
| self.stores: dict[str, "OrderedDict[int, Entry]"] = {} |
| self.shared: list[Entry] = [] |
| self._next_id = 0 |
|
|
| def _store(self, user_id) -> "OrderedDict[int, Entry]": |
| return self.stores.setdefault(user_id, OrderedDict()) |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| emb = embedder.embed(query) |
| store = self._store(user_id) |
| entries = list(store.values()) |
| i, sim = _best_match(emb, entries) |
| if i >= 0 and sim >= self.sim_threshold: |
| key = list(store.keys())[i] |
| entry = store[key] |
| if self.eviction == "lru": |
| store.move_to_end(key) |
| entry.freq += 1 |
| return entry.call |
| |
| j, sj = _best_match(emb, self.shared) |
| if j >= 0 and sj >= self.sim_threshold: |
| return self.shared[j].call |
| return schema_draft(functions) |
|
|
| def observe(self, query, functions, user_id, name, args, embedder) -> None: |
| emb = embedder.embed(query) |
| call = canonical_call_str(name, args) |
| store = self._store(user_id) |
| eid = self._next_id |
| self._next_id += 1 |
| store[eid] = Entry(emb, call) |
| store.move_to_end(eid) |
| self._evict(store) |
|
|
| def seed_shared(self, query, name, args, embedder) -> None: |
| self.shared.append(Entry(embedder.embed(query), |
| canonical_call_str(name, args))) |
|
|
| def _evict(self, store: "OrderedDict[int, Entry]") -> None: |
| while len(store) > self.capacity: |
| if self.eviction == "lru": |
| store.popitem(last=False) |
| elif self.eviction == "lfu": |
| k = min(store, key=lambda x: store[x].freq) |
| del store[k] |
| else: |
| store.popitem(last=False) |
|
|
| def total_entries(self) -> int: |
| return sum(len(s) for s in self.stores.values()) |
|
|
|
|
| |
| |
| |
| |
| |
| class PersonalNoEvict(PersonalMemory): |
| """[+personalization, -eviction]: per-user store that grows online but is |
| never bounded/evicted. Isolates how much of ours' gain is eviction.""" |
| name = "personal_noevict" |
|
|
| def __init__(self, sim_threshold: float = 0.35): |
| |
| super().__init__(capacity=10**9, eviction="lru", |
| sim_threshold=sim_threshold) |
|
|
|
|
| class GlobalEvict: |
| """[-personalization, +online-growth+eviction]: a single GLOBAL store (not |
| per-user) that keeps ingesting after warmup and LRU-evicts at a total |
| capacity. Isolates how much of ours' gain is per-user partitioning: it |
| differs from PersonalMemory only in that retrieval ignores user id.""" |
| name = "global_evict" |
|
|
| def __init__(self, capacity: int = 1920, sim_threshold: float = 0.35): |
| self.capacity = capacity |
| self.sim_threshold = sim_threshold |
| self.store: "OrderedDict[int, Entry]" = OrderedDict() |
| self._next_id = 0 |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| emb = embedder.embed(query) |
| entries = list(self.store.values()) |
| i, sim = _best_match(emb, entries) |
| if i >= 0 and sim >= self.sim_threshold: |
| key = list(self.store.keys())[i] |
| self.store.move_to_end(key) |
| self.store[key].freq += 1 |
| return self.store[key].call |
| return schema_draft(functions) |
|
|
| def observe(self, query, functions, user_id, name, args, embedder) -> None: |
| emb = embedder.embed(query) |
| eid = self._next_id |
| self._next_id += 1 |
| self.store[eid] = Entry(emb, canonical_call_str(name, args)) |
| self.store.move_to_end(eid) |
| while len(self.store) > self.capacity: |
| self.store.popitem(last=False) |
|
|
| def seed_shared(self, *a, **k): |
| return None |
|
|
|
|
| |
| |
| |
| def _schema_scaffold(functions, name) -> str: |
| """Schema-aware structural draft for a NAMED function: the function's |
| required argument keys in canonical (sorted) order with placeholder values. |
| This is the structurally-valid fallback a schema-aware FSM emits when |
| retrieval is not confident enough to commit a concrete prior call.""" |
| fn = next((f for f in functions if f.get("name") == name), None) |
| if fn is None: |
| return schema_draft(functions) |
| params = fn.get("parameters", {}) or {} |
| props = params.get("properties", {}) or {} |
| required = params.get("required", []) or list(props) |
| return canonical_call_str(name, {k: None for k in required}) |
|
|
|
|
| class ToolSpecBaseline: |
| """Faithful ToolSpec reproduction (Xia et al., 2026): a *frozen global* |
| retrieval store (no eviction, no personalization — the ToolSpec regime) |
| with the two ToolSpec mechanisms the simple ``StaticGlobal`` proxy omits: |
| |
| 1. **Confidence-gated retrieval.** Return the nearest stored call verbatim |
| only while its similarity clears ``sim_lo``; ``StaticGlobal`` instead |
| returns its single nearest neighbour unconditionally, so on a cold / |
| far query it drafts a wholly unrelated call. |
| 2. **Schema-aware fallback (FSM surrogate).** On a cold miss, rather than |
| emitting a random far neighbour we emit a *structurally valid* draft |
| for the nearest neighbour's function (its required-arg scaffold in |
| canonical order) — the acceptance a schema-constrained decoder |
| guarantees on the call's structural tokens even without a value hit. |
| This makes the arm **strictly at least as strong as ``StaticGlobal``**: |
| identical on confident hits, better on cold misses. |
| |
| ToolSpec has no public code, so the FSM is approximated by this schema-aware |
| scaffold. We also tested a |
| ``k``-NN summed-similarity vote on the target *function* (retrieval-augmented |
| denoising); it *degraded* MAT on these traces because the highly skewed |
| telecom workload (one diagnostic call dominates) lets the majority function |
| override correct top-1 picks — reported honestly in the write-up, and NOT |
| used here. Everything else (frozen, global, eviction-free) matches ToolSpec |
| and is deliberately NOT personalized — the property under test. |
| """ |
| name = "toolspec" |
|
|
| def __init__(self, sim_lo: float = 0.30): |
| self.entries: list[Entry] = [] |
| self.frozen = False |
| self.sim_lo = sim_lo |
| self._names: list[str] = [] |
|
|
| def freeze(self): |
| self.frozen = True |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| if not self.entries: |
| return schema_draft(functions) |
| emb = embedder.embed(query) |
| i, sim = _best_match(emb, self.entries) |
| if sim >= self.sim_lo: |
| return self.entries[i].call |
| return _schema_scaffold(functions, self._names[i]) |
|
|
| def observe(self, query, functions, user_id, name, args, embedder) -> None: |
| if self.frozen: |
| return |
| self.entries.append(Entry(embedder.embed(query), |
| canonical_call_str(name, args))) |
| self._names.append(name) |
|
|
| def seed_shared(self, *a, **k): |
| return None |
|
|
|
|
| |
| |
| |
| _TOK_RE = re.compile(r"\w+|[^\w\s]") |
| _SEP = " " |
|
|
|
|
| def _tokenize(text: str) -> tuple[str, ...]: |
| """Word/punctuation-level tokens (lowercased). A deliberate approximation of |
| SuffixDecoding's model-BPE tokens: the mechanistic contrast under test is |
| *exact token matching vs. embedding similarity*, which this preserves; the |
| exact subword vocabulary is not what distinguishes the two arms.""" |
| return tuple(_TOK_RE.findall(text.lower())) |
|
|
|
|
| def _suffix_key(tokens: tuple[str, ...]) -> str: |
| """Separator-delimited form so whole-token substring tests never match |
| across partial tokens (every boundary is a _SEP).""" |
| return _SEP + _SEP.join(tokens) + _SEP |
|
|
|
|
| def _longest_suffix_match(q: tuple[str, ...], stored_key: str, |
| floor: int) -> int: |
| """Longest k>floor such that the k-token *suffix* of the current query q is |
| a contiguous whole-token substring of a stored sequence (its suffix key). |
| Returns 0 if no suffix longer than `floor` matches. This is SuffixDecoding's |
| 'walk the tree to the node matching the context suffix' step, adapted to our |
| per-request query context. |
| |
| Uses binary search: the predicate ``q[-k:] is a substring of stored`` is |
| monotonic in k (if the k-token suffix matches, every shorter suffix does), |
| so we find the largest matching k in O(log|q|) containment tests rather than |
| O(|q|) — essential because dialogue-context queries run to hundreds of |
| tokens.""" |
| best = 0 |
| lo, hi = floor + 1, len(q) |
| while lo <= hi: |
| mid = (lo + hi) // 2 |
| cand = _SEP + _SEP.join(q[-mid:]) + _SEP |
| if cand in stored_key: |
| best = mid |
| lo = mid + 1 |
| else: |
| hi = mid - 1 |
| return best |
|
|
|
|
| class SuffixDecodingBaseline: |
| """Faithful adaptation of **SuffixDecoding** (Oliaro et al., *SuffixDecoding: |
| Extreme Speculative Decoding for Emerging AI Applications*, NeurIPS 2025 |
| Spotlight; arXiv 2411.04975) as a retrieval arm. |
| |
| SuffixDecoding keeps a **global suffix tree accumulated from previous |
| requests' token streams** (live/growing across the deployment, so request N |
| benefits from request N-1), matches the **suffix of the current context** |
| against the tree at each step, and speculates the highest-frequency |
| continuation with adaptive length. It is **token-level (exact match), |
| global, and NOT personalized**; the tree is **size-capped** (~10.75 B/token, |
| ~31 days on a 144 GB host), not unbounded. |
| |
| We reproduce that regime and swap **exactly one mechanism** vs. our own |
| ``GlobalEvict`` arm: retrieval is **longest-token-suffix match** on the query |
| instead of embedding cosine similarity. Everything else — global live |
| write-back, canonicalization, a size cap, no per-user partitioning — is held |
| identical, so the comparison isolates *retrieval mechanism* (exact token |
| match vs. semantic embedding), not confounds like different data pools or |
| personalization. Frequency and recency break ties in match length, mirroring |
| SuffixDecoding's frequency-ranked tree paths. |
| |
| Faithful vs. adapted: |
| - Faithful: global, live-growing, size-capped, non-personalized store; |
| exact token-suffix matching; frequency-ranked selection. |
| - Adapted: we match the *query* token context (which selects a tool call |
| in our one-call-per-request setting) rather than a running generation, |
| and our token-LCP acceptance already truncates the speculated call at the |
| first mismatch, subsuming SuffixDecoding's adaptive speculation length. |
| Tokenization is word/punct level, not the model BPE (approximation). |
| """ |
| name = "suffixdecoding" |
|
|
| def __init__(self, capacity: int = 1920, min_match: int = 1): |
| |
| |
| |
| self.capacity = capacity |
| self.min_match = min_match |
| |
| self.store: "OrderedDict[int, list]" = OrderedDict() |
| self._next_id = 0 |
|
|
| def draft(self, query, functions, user_id, embedder) -> str: |
| q = _tokenize(query) |
| if not q: |
| return schema_draft(functions) |
| floor = self.min_match - 1 |
| best_key, best_eid = None, None |
| for eid, rec in self.store.items(): |
| m = _longest_suffix_match(q, rec[1], floor) |
| if m < self.min_match: |
| continue |
| key = (m, rec[3]) |
| if best_key is None or key >= best_key: |
| best_key, best_eid = key, eid |
| if best_eid is None: |
| return schema_draft(functions) |
| rec = self.store[best_eid] |
| rec[3] += 1 |
| self.store.move_to_end(best_eid) |
| return rec[2] |
|
|
| def observe(self, query, functions, user_id, name, args, embedder) -> None: |
| q = _tokenize(query) |
| eid = self._next_id |
| self._next_id += 1 |
| self.store[eid] = [q, _suffix_key(q), canonical_call_str(name, args), 1] |
| self.store.move_to_end(eid) |
| while len(self.store) > self.capacity: |
| self.store.popitem(last=False) |
|
|
| def seed_shared(self, *a, **k): |
| return None |
|
|
| def total_entries(self) -> int: |
| return len(self.store) |
|
|