@pentatonic-ai/ai-agent-sdk 0.10.11 → 0.10.12

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.
package/dist/index.cjs CHANGED
@@ -878,7 +878,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
878
878
  }
879
879
 
880
880
  // src/telemetry.js
881
- var VERSION = "0.10.11";
881
+ var VERSION = "0.10.12";
882
882
  var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
883
883
  function machineId() {
884
884
  const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
package/dist/index.js CHANGED
@@ -847,7 +847,7 @@ function fireAndForgetEmit(clientConfig, sessionOpts, messages, result, model) {
847
847
  }
848
848
 
849
849
  // src/telemetry.js
850
- var VERSION = "0.10.11";
850
+ var VERSION = "0.10.12";
851
851
  var TELEMETRY_URL = "https://sdk-telemetry.philip-134.workers.dev";
852
852
  function machineId() {
853
853
  const raw = typeof process !== "undefined" ? `${process.env?.USER || process.env?.USERNAME || "u"}:${process.platform || "x"}:${process.arch || "x"}` : "browser";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.10.11",
3
+ "version": "0.10.12",
4
4
  "description": "TES SDK — LLM observability and lifecycle tracking via Pentatonic Thing Event System. Track token usage, tool calls, and conversations. Manage things through event-sourced lifecycle stages with AI enrichment and vector search.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -213,22 +213,33 @@ def _execute_entity_plan(cur, plan) -> None:
213
213
  cur.execute(
214
214
  """INSERT INTO entity_merges (id, arena, canonical_id, deprecated_id,
215
215
  deprecated_canonical_name, deprecated_aliases, merge_signal,
216
- facts_repointed, rollback_payload)
217
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s::jsonb)""",
216
+ facts_repointed, relationships_repointed, merged_by, rollback_payload)
217
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s::jsonb)""",
218
218
  ("em_" + uuid.uuid4().hex[:20], a["arena"], a["canonical_id"], a["deprecated_id"],
219
219
  a["deprecated_canonical_name"], a["deprecated_aliases"], a["merge_signal"],
220
220
  len(plan.fact_subject_repoints) + len(plan.fact_object_repoints),
221
+ len(plan.rel_endpoint_repoints), "fusion-drive",
221
222
  json.dumps(a["rollback_payload"], default=str)),
222
223
  )
223
224
  cur.execute("DELETE FROM entities WHERE id = ANY(%s)", (plan.deprecated_entity_ids,))
224
225
 
225
226
 
226
227
  def _dedup_master_facts(cur, arena: str, master_id: str) -> int:
227
- """After repointing facts onto the master, the master can hold several
228
- facts with the same (subject, predicate, object) but different statements
229
- (fact id is content_id(arena, statement), so they didn't collapse on
230
- insert). Fuse each such triple-group via build_fact_merge_plan: keep the
231
- best, union provenance, delete dups with a fact_merges receipt."""
228
+ """After repointing facts onto the master, collapse facts that are now
229
+ TRUE duplicates same (subject, predicate, object) AND the same normalized
230
+ statement. These exist because the fact id is content_id(arena, statement):
231
+ two rows with statements differing only in case/whitespace hash to distinct
232
+ ids and so survived insert-time dedup; once their subject/object entities
233
+ are unified they are genuinely the same assertion and fuse safely.
234
+
235
+ The statement is PART OF THE KEY on purpose. Grouping on the triple alone is
236
+ NOT identity: a NULL object with a generic predicate (e.g. subject "said"
237
+ NULL) buckets together unrelated assertions, and build_fact_merge_plan would
238
+ keep one and DELETE the rest — destroying distinct facts (it deleted 33% of
239
+ one arena's facts that way before this fix). Same-triple / different-meaning
240
+ facts are left untouched here; the LLM semantic tier (_semantic_fact_groups
241
+ + adjudicate_facts) is the only thing allowed to fuse facts whose statements
242
+ actually differ, and only on an affirmative same-assertion verdict."""
232
243
  cur.execute(
233
244
  """SELECT id, predicate, object_entity_id, statement, confidence, provenance_event_ids
234
245
  FROM facts
@@ -238,8 +249,12 @@ def _dedup_master_facts(cur, arena: str, master_id: str) -> int:
238
249
  rows = cur.fetchall()
239
250
  groups: dict[tuple, list[dict]] = {}
240
251
  for r in rows:
241
- # group key uses the master as the subject anchor + predicate + object
242
- groups.setdefault((master_id, r["predicate"], r["object_entity_id"]), []).append(r)
252
+ # key = master subject anchor + predicate + object + NORMALIZED STATEMENT.
253
+ # statement in the key => only byte-equal-after-normalization dupes fuse.
254
+ groups.setdefault(
255
+ (master_id, r["predicate"], r["object_entity_id"], _norm(r["statement"] or "")),
256
+ [],
257
+ ).append(r)
243
258
  deduped = 0
244
259
  for dup in groups.values():
245
260
  plan = build_fact_merge_plan(arena=arena, dup_facts=dup)
@@ -0,0 +1,83 @@
1
+ """Guards _dedup_master_facts against the over-fusion that deleted 33% of an
2
+ arena's facts (2026-06-14): grouping post-merge facts by (subject, predicate,
3
+ object) alone treats a NULL object + generic predicate as identity and deletes
4
+ distinct assertions. The statement must be part of the dedup key so ONLY
5
+ byte-equal-after-normalization duplicates fuse; same-triple/different-meaning
6
+ facts are left for the LLM semantic tier."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import os
12
+ import sys
13
+ import types
14
+
15
+ HERE = os.path.dirname(__file__)
16
+
17
+
18
+ def _load_fuse(monkeypatch):
19
+ fake_psycopg = types.ModuleType("psycopg")
20
+ fake_rows = types.ModuleType("psycopg.rows")
21
+ fake_rows.dict_row = object()
22
+ fake_psycopg.rows = fake_rows
23
+ monkeypatch.setitem(sys.modules, "psycopg", fake_psycopg)
24
+ monkeypatch.setitem(sys.modules, "psycopg.rows", fake_rows)
25
+ monkeypatch.syspath_prepend(os.path.join(HERE, "..", "fusion_drive"))
26
+ monkeypatch.syspath_prepend(HERE)
27
+ sys.modules.pop("fusion_drive_fuse", None)
28
+ return importlib.import_module("fusion_drive_fuse")
29
+
30
+
31
+ class FakeCursor:
32
+ """Returns preset fact rows on the SELECT; records ids passed to DELETE."""
33
+
34
+ def __init__(self, rows):
35
+ self._rows = rows
36
+ self.deleted_ids = []
37
+
38
+ def execute(self, sql, params=None):
39
+ s = " ".join(sql.split())
40
+ if s.startswith("DELETE FROM facts WHERE id = ANY"):
41
+ # params is a 1-tuple holding the id list
42
+ self.deleted_ids.extend(params[0])
43
+ # SELECT / UPDATE / INSERT: no-op (fetchall serves the preset rows)
44
+
45
+ def fetchall(self):
46
+ return self._rows
47
+
48
+
49
+ def _fact(fid, predicate, obj, statement, conf):
50
+ return {
51
+ "id": fid,
52
+ "predicate": predicate,
53
+ "object_entity_id": obj,
54
+ "statement": statement,
55
+ "confidence": conf,
56
+ "provenance_event_ids": [f"ev_{fid}"],
57
+ }
58
+
59
+
60
+ def test_distinct_statements_same_triple_are_NOT_fused(monkeypatch):
61
+ fuse = _load_fuse(monkeypatch)
62
+ rows = [
63
+ _fact("f1", "said", None, "Standing by", 0.9),
64
+ _fact("f2", "said", None, "yeah ship it", 0.8), # distinct meaning
65
+ _fact("f3", "said", None, "modules/deep-memory is vestigial", 0.7),
66
+ ]
67
+ cur = FakeCursor(rows)
68
+ deleted = fuse._dedup_master_facts(cur, "arena", "m")
69
+ assert deleted == 0, "must not fuse same-triple facts with different statements"
70
+ assert cur.deleted_ids == []
71
+
72
+
73
+ def test_only_normalized_statement_duplicates_fuse(monkeypatch):
74
+ fuse = _load_fuse(monkeypatch)
75
+ rows = [
76
+ _fact("f1", "said", None, "Standing by", 0.9),
77
+ _fact("f2", "said", None, "standing by", 0.5), # same after _norm
78
+ _fact("f3", "said", None, "something else entirely", 0.7),
79
+ ]
80
+ cur = FakeCursor(rows)
81
+ deleted = fuse._dedup_master_facts(cur, "arena", "m")
82
+ assert deleted == 1, "the case/whitespace duplicate should fuse"
83
+ assert cur.deleted_ids == ["f2"], "lower-confidence true-dupe is the one deleted"