@pentatonic-ai/ai-agent-sdk 0.7.8 → 0.7.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pentatonic-ai/ai-agent-sdk",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
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",
@@ -41,6 +41,18 @@ import { distill } from "./distill.js";
41
41
  * ids) — pass the raw form here so retries of the same logical event
42
42
  * match across runs whose prefixes differ by a few ms. Defaults to
43
43
  * `content`.
44
+ * @param {"client" | "user"} [opts.dedupScope="client"] - Scope of the
45
+ * dedup match. Default `"client"`: byte-equal content for the tenant
46
+ * collapses to one row regardless of who emitted it (today's behaviour,
47
+ * appropriate when each row is single-owner). Set `"user"` to also
48
+ * require `user_id` equality, which lets multiple users legitimately
49
+ * own their own copy of the same shared content (private chat-channel
50
+ * members, group meeting attendees) — emit one ingest per member with
51
+ * the same content but different `userId`, all with
52
+ * `dedupScope: "user"`, and each user gets their own row with native
53
+ * per-user access counting / recency / decay. Requires `userId` to be
54
+ * set; when `userId` is null the option degrades to `"client"` scope
55
+ * (a global shared row still collapses cross-emit).
44
56
  * @param {number} [opts.dedupLegacyWindowDays=7] - How far back the
45
57
  * `[<iso>] <content>` legacy-form `LIKE` match scans. Default 7 days.
46
58
  * The leading-wildcard `LIKE` can't use a btree index, so without a
@@ -94,6 +106,11 @@ export async function ingest(db, ai, llm, content, opts = {}) {
94
106
  opts.dedupLegacyWindowDays === undefined
95
107
  ? 7
96
108
  : Number(opts.dedupLegacyWindowDays);
109
+ // Per-user dedup requires `userId`; degrade to client-scope otherwise so
110
+ // a misuse can't accidentally relax the dedup boundary (we'd rather over-
111
+ // collapse than fragment the corpus on a missing userId).
112
+ const userScopedDedup = opts.dedupScope === "user" && !!opts.userId;
113
+ const userClause = userScopedDedup ? ` AND user_id = $4` : "";
97
114
  try {
98
115
  const sql =
99
116
  legacyWindowDays > 0
@@ -107,17 +124,18 @@ export async function ingest(db, ai, llm, content, opts = {}) {
107
124
  content LIKE '%] ' || $2
108
125
  AND created_at > NOW() - ($3 || ' days')::interval
109
126
  )
110
- )
127
+ )${userScopedDedup ? "\n AND user_id = $4" : ""}
111
128
  LIMIT 1`
112
129
  : `SELECT id, 'exact' AS match_kind
113
130
  FROM memory_nodes
114
131
  WHERE client_id = $1
115
- AND content = $2
132
+ AND content = $2${userClause}
116
133
  LIMIT 1`;
117
- const params =
134
+ const baseParams =
118
135
  legacyWindowDays > 0
119
136
  ? [clientId, dedupKey, String(legacyWindowDays)]
120
137
  : [clientId, dedupKey];
138
+ const params = userScopedDedup ? [...baseParams, opts.userId] : baseParams;
121
139
  const dupCheck = await db(sql, params);
122
140
  if (dupCheck.rows?.length) {
123
141
  const matchKind = dupCheck.rows[0].match_kind || "exact";