@retinue/agentkit 0.2.0 → 0.3.1

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 (209) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/memory/connections.d.ts +10 -0
  6. package/dist/adapters/memory/connections.js +87 -0
  7. package/dist/adapters/memory/graph.d.ts +17 -0
  8. package/dist/adapters/memory/graph.js +267 -0
  9. package/dist/adapters/memory/index.d.ts +3 -0
  10. package/dist/adapters/memory/index.js +3 -0
  11. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  12. package/dist/adapters/memory/rate-limit.js +28 -0
  13. package/dist/adapters/memory/runtime.js +0 -0
  14. package/dist/adapters/postgres/connections.d.ts +15 -0
  15. package/dist/adapters/postgres/connections.js +134 -0
  16. package/dist/adapters/postgres/graph.d.ts +24 -0
  17. package/dist/adapters/postgres/graph.js +325 -0
  18. package/dist/adapters/postgres/index.d.ts +2 -0
  19. package/dist/adapters/postgres/index.js +2 -0
  20. package/dist/adapters/postgres/migrations.js +198 -0
  21. package/dist/adapters/postgres/run-store.js +26 -2
  22. package/dist/adapters/postgres/schema.d.ts +61 -2
  23. package/dist/adapters/postgres/schema.js +95 -10
  24. package/dist/adapters/redis/index.d.ts +1 -0
  25. package/dist/adapters/redis/index.js +1 -0
  26. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  27. package/dist/adapters/redis/rate-limit.js +55 -0
  28. package/dist/adapters/supabase/index.d.ts +15 -0
  29. package/dist/adapters/supabase/index.js +15 -0
  30. package/dist/adapters/supabase/rls.js +33 -0
  31. package/dist/agents/agent.d.ts +78 -1
  32. package/dist/agents/agent.js +118 -5
  33. package/dist/agents/define.js +24 -4
  34. package/dist/agents/engine.d.ts +20 -0
  35. package/dist/agents/engine.js +177 -6
  36. package/dist/audio/index.d.ts +151 -0
  37. package/dist/audio/index.js +183 -0
  38. package/dist/connections/cipher.d.ts +103 -0
  39. package/dist/connections/cipher.js +141 -0
  40. package/dist/connections/index.d.ts +156 -0
  41. package/dist/connections/index.js +72 -0
  42. package/dist/connections/oauth/client.d.ts +84 -0
  43. package/dist/connections/oauth/client.js +141 -0
  44. package/dist/connections/oauth/index.d.ts +166 -0
  45. package/dist/connections/oauth/index.js +226 -0
  46. package/dist/connections/oauth/service.d.ts +90 -0
  47. package/dist/connections/oauth/service.js +132 -0
  48. package/dist/connections/pause.d.ts +68 -0
  49. package/dist/connections/pause.js +97 -0
  50. package/dist/connections/resolver.d.ts +59 -0
  51. package/dist/connections/resolver.js +138 -0
  52. package/dist/connections/resume.d.ts +64 -0
  53. package/dist/connections/resume.js +99 -0
  54. package/dist/connections/vault-cipher.d.ts +95 -0
  55. package/dist/connections/vault-cipher.js +206 -0
  56. package/dist/core/content-parts.d.ts +18 -2
  57. package/dist/core/content-parts.js +1 -0
  58. package/dist/core/context.d.ts +25 -0
  59. package/dist/core/errors.d.ts +1 -1
  60. package/dist/core/errors.js +15 -0
  61. package/dist/core/events.d.ts +44 -2
  62. package/dist/core/events.js +31 -2
  63. package/dist/core/validation.js +19 -0
  64. package/dist/entries/connections.d.ts +10 -0
  65. package/dist/entries/connections.js +10 -0
  66. package/dist/entries/knowledge.d.ts +8 -0
  67. package/dist/entries/knowledge.js +8 -0
  68. package/dist/entries/mcp-server.d.ts +9 -0
  69. package/dist/entries/mcp-server.js +9 -0
  70. package/dist/entries/runtime.d.ts +10 -0
  71. package/dist/entries/runtime.js +8 -0
  72. package/dist/entries/testing.d.ts +28 -0
  73. package/dist/entries/testing.js +28 -0
  74. package/dist/graphql/resolvers.d.ts +3 -1
  75. package/dist/graphql/resolvers.js +14 -0
  76. package/dist/knowledge/communities.d.ts +166 -0
  77. package/dist/knowledge/communities.js +377 -0
  78. package/dist/knowledge/graph-global.d.ts +153 -0
  79. package/dist/knowledge/graph-global.js +246 -0
  80. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  81. package/dist/knowledge/graph-retrieval.js +303 -0
  82. package/dist/knowledge/graph.d.ts +223 -0
  83. package/dist/knowledge/graph.js +369 -0
  84. package/dist/knowledge/index.d.ts +24 -0
  85. package/dist/knowledge/index.js +36 -1
  86. package/dist/knowledge/retrieval.d.ts +26 -1
  87. package/dist/knowledge/retrieval.js +77 -1
  88. package/dist/mcp-server/index.d.ts +168 -0
  89. package/dist/mcp-server/index.js +175 -0
  90. package/dist/models/extraction.d.ts +73 -0
  91. package/dist/models/extraction.js +118 -0
  92. package/dist/models/index.d.ts +40 -1
  93. package/dist/models/index.js +22 -1
  94. package/dist/models/pricing.d.ts +2 -0
  95. package/dist/models/pricing.js +17 -1
  96. package/dist/models/provider-factory.js +9 -7
  97. package/dist/models/streaming.d.ts +89 -0
  98. package/dist/models/streaming.js +179 -2
  99. package/dist/persistence/index.d.ts +230 -0
  100. package/dist/persistence/index.js +2 -0
  101. package/dist/runtime/index.d.ts +1 -1
  102. package/dist/runtime/index.js +12 -0
  103. package/dist/runtime/worker.d.ts +36 -1
  104. package/dist/runtime/worker.js +37 -4
  105. package/dist/security/checklist.js +46 -0
  106. package/dist/server/bin.d.ts +72 -0
  107. package/dist/server/bin.js +304 -0
  108. package/dist/server/boot.d.ts +20 -2
  109. package/dist/server/boot.js +29 -3
  110. package/dist/server/cli-worker.js +6 -3
  111. package/dist/server/cli.d.ts +13 -0
  112. package/dist/server/cli.js +10 -5
  113. package/dist/server/config.d.ts +11 -1
  114. package/dist/server/config.js +18 -0
  115. package/dist/server/doctor.d.ts +85 -0
  116. package/dist/server/doctor.js +280 -0
  117. package/dist/server/pool.d.ts +47 -0
  118. package/dist/server/pool.js +73 -0
  119. package/dist/skills/context.d.ts +61 -0
  120. package/dist/skills/context.js +107 -0
  121. package/dist/skills/index.d.ts +1 -0
  122. package/dist/skills/index.js +1 -0
  123. package/dist/telemetry/spans.js +5 -0
  124. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  125. package/dist/testing/conformance/artifact-exports.js +172 -0
  126. package/dist/testing/conformance/artifacts.d.ts +11 -0
  127. package/dist/testing/conformance/artifacts.js +316 -0
  128. package/dist/testing/conformance/audio.d.ts +38 -0
  129. package/dist/testing/conformance/audio.js +145 -0
  130. package/dist/testing/conformance/capability.d.ts +23 -0
  131. package/dist/testing/conformance/capability.js +27 -0
  132. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  133. package/dist/testing/conformance/checkpoint-store.js +72 -0
  134. package/dist/testing/conformance/connections.d.ts +14 -0
  135. package/dist/testing/conformance/connections.js +171 -0
  136. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  137. package/dist/testing/conformance/conversation-store.js +60 -0
  138. package/dist/testing/conformance/evaluation.d.ts +10 -0
  139. package/dist/testing/conformance/evaluation.js +295 -0
  140. package/dist/testing/conformance/files.d.ts +19 -0
  141. package/dist/testing/conformance/files.js +454 -0
  142. package/dist/testing/conformance/flows.d.ts +16 -0
  143. package/dist/testing/conformance/flows.js +193 -0
  144. package/dist/testing/conformance/graph.d.ts +22 -0
  145. package/dist/testing/conformance/graph.js +500 -0
  146. package/dist/testing/conformance/hitl.d.ts +25 -0
  147. package/dist/testing/conformance/hitl.js +523 -0
  148. package/dist/testing/conformance/index.d.ts +163 -0
  149. package/dist/testing/conformance/index.js +321 -0
  150. package/dist/testing/conformance/invariants.d.ts +23 -0
  151. package/dist/testing/conformance/invariants.js +80 -0
  152. package/dist/testing/conformance/knowledge.d.ts +41 -0
  153. package/dist/testing/conformance/knowledge.js +592 -0
  154. package/dist/testing/conformance/parents.d.ts +50 -0
  155. package/dist/testing/conformance/parents.js +39 -0
  156. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  157. package/dist/testing/conformance/rate-limit.js +71 -0
  158. package/dist/testing/conformance/records.d.ts +36 -0
  159. package/dist/testing/conformance/records.js +400 -0
  160. package/dist/testing/conformance/rollups.d.ts +30 -0
  161. package/dist/testing/conformance/rollups.js +425 -0
  162. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  163. package/dist/testing/conformance/run-coordinator.js +150 -0
  164. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  165. package/dist/testing/conformance/run-event-log.js +138 -0
  166. package/dist/testing/conformance/run-store.d.ts +12 -0
  167. package/dist/testing/conformance/run-store.js +294 -0
  168. package/dist/testing/conformance/session-state.d.ts +38 -0
  169. package/dist/testing/conformance/session-state.js +198 -0
  170. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  171. package/dist/testing/conformance/usage-limits.js +309 -0
  172. package/dist/testing/conformance.d.ts +8 -0
  173. package/dist/testing/conformance.js +8 -0
  174. package/dist/testing/memory-backend.d.ts +39 -0
  175. package/dist/testing/memory-backend.js +41 -0
  176. package/dist/testing/pglite.d.ts +55 -0
  177. package/dist/testing/pglite.js +78 -0
  178. package/dist/testing/stub-model.d.ts +58 -0
  179. package/dist/testing/stub-model.js +71 -0
  180. package/dist/testing/supabase-storage-double.d.ts +35 -0
  181. package/dist/testing/supabase-storage-double.js +101 -0
  182. package/dist/toolkit/http.d.ts +27 -0
  183. package/dist/toolkit/http.js +48 -0
  184. package/dist/toolkit/index.d.ts +27 -1
  185. package/dist/toolkit/index.js +24 -1
  186. package/dist/toolkit/mime.d.ts +97 -0
  187. package/dist/toolkit/mime.js +233 -0
  188. package/dist/toolkit/ssrf.d.ts +139 -0
  189. package/dist/toolkit/ssrf.js +349 -0
  190. package/dist/toolkit/vendor.d.ts +91 -0
  191. package/dist/toolkit/vendor.js +153 -0
  192. package/dist/tools/credentials.d.ts +265 -6
  193. package/dist/tools/credentials.js +318 -3
  194. package/dist/tools/define.d.ts +14 -0
  195. package/dist/tools/define.js +3 -0
  196. package/dist/tools/index.d.ts +21 -0
  197. package/dist/tools/index.js +7 -0
  198. package/dist/tools/library/audio.d.ts +63 -0
  199. package/dist/tools/library/audio.js +148 -0
  200. package/dist/tools/library/index.d.ts +8 -0
  201. package/dist/tools/library/index.js +7 -0
  202. package/dist/tools/registry.d.ts +33 -0
  203. package/dist/tools/registry.js +72 -2
  204. package/dist/usage/index.d.ts +21 -0
  205. package/dist/usage/index.js +1 -0
  206. package/dist/usage/rate-limit.d.ts +122 -0
  207. package/dist/usage/rate-limit.js +131 -0
  208. package/dist/usage/recorder.js +3 -0
  209. package/package.json +29 -2
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The rate-limit counter in memory — task #248.
3
+ *
4
+ * **For tests and a single-process deployment only.** Two workers each hold their own `Map`, so each allows the
5
+ * full rate and a limit of 60/minute becomes 60×N. That is not a caveat to be worked around; it is why
6
+ * `createRedisRateLimitStore` exists, and why the cross-process test in this task's suite runs against a real
7
+ * Redis rather than this.
8
+ *
9
+ * Old windows are dropped on write rather than on a timer: a timer in a store would keep a process alive and
10
+ * would need clearing, and the number of live windows is one per tenant.
11
+ */
12
+ export const createMemoryRateLimitStore = () => {
13
+ const counts = new Map();
14
+ return {
15
+ async consume({ tenantId, windowStartMs }) {
16
+ const key = `${tenantId}:${windowStartMs}`;
17
+ // Every other window for this tenant is over, so nothing else can be incremented again.
18
+ for (const existing of counts.keys()) {
19
+ if (existing.startsWith(`${tenantId}:`) && existing !== key)
20
+ counts.delete(existing);
21
+ }
22
+ const next = (counts.get(key) ?? 0) + 1;
23
+ counts.set(key, next);
24
+ return next;
25
+ },
26
+ };
27
+ };
28
+ //# sourceMappingURL=rate-limit.js.map
Binary file
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `ConnectionStore` over Postgres — task #261.
3
+ *
4
+ * Pure SQL over `SqlExecutor`, like every other adapter here. The store never sees a plaintext secret: it
5
+ * writes the four columns of a `SealedSecret` and reads them back, so an adapter cannot leak something it does
6
+ * not have.
7
+ *
8
+ * Every query is tenant-scoped in its `WHERE`, and there is no `findById(id)` — governing principle 1. That is
9
+ * not belt-and-braces over RLS; it is the layer RLS is defence in depth *for*, and #91 is the reminder that a
10
+ * method accepting a tenant scope and not using it typechecks perfectly.
11
+ */
12
+ import type { ConnectionStore } from "../../connections/index.js";
13
+ import type { SqlExecutor } from "./sql.js";
14
+ export declare const createPostgresConnectionStore: (sql: SqlExecutor) => ConnectionStore;
15
+ //# sourceMappingURL=connections.d.ts.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * `ConnectionStore` over Postgres — task #261.
3
+ *
4
+ * Pure SQL over `SqlExecutor`, like every other adapter here. The store never sees a plaintext secret: it
5
+ * writes the four columns of a `SealedSecret` and reads them back, so an adapter cannot leak something it does
6
+ * not have.
7
+ *
8
+ * Every query is tenant-scoped in its `WHERE`, and there is no `findById(id)` — governing principle 1. That is
9
+ * not belt-and-braces over RLS; it is the layer RLS is defence in depth *for*, and #91 is the reminder that a
10
+ * method accepting a tenant scope and not using it typechecks perfectly.
11
+ */
12
+ import { AgentPlatformError } from "../../core/errors.js";
13
+ const iso = (value) => (value instanceof Date ? value.toISOString() : value);
14
+ const toConnection = (row) => ({
15
+ id: row.id,
16
+ kind: row.kind,
17
+ provider: row.provider,
18
+ ...(row.label === null ? {} : { label: row.label }),
19
+ mode: row.mode,
20
+ scheme: row.scheme,
21
+ ...(row.metadata === null || Object.keys(row.metadata).length === 0 ? {} : { metadata: row.metadata }),
22
+ ...(row.granted_scopes === null ? {} : { grantedScopes: row.granted_scopes }),
23
+ sealed: {
24
+ keyId: row.secret_key_id,
25
+ algorithm: row.secret_algorithm,
26
+ nonce: row.secret_nonce,
27
+ ciphertext: row.secret_ciphertext,
28
+ },
29
+ ...(row.expires_at === null ? {} : { expiresAt: iso(row.expires_at) }),
30
+ createdAt: iso(row.created_at),
31
+ updatedAt: iso(row.updated_at),
32
+ ...(row.revoked_at === null ? {} : { revokedAt: iso(row.revoked_at) }),
33
+ });
34
+ const COLUMNS = `id, kind, provider, label, mode, scheme, metadata, granted_scopes,
35
+ secret_key_id, secret_algorithm, secret_nonce, secret_ciphertext,
36
+ expires_at, created_at, updated_at, revoked_at`;
37
+ const notFound = (id) => new AgentPlatformError({ code: "not_found", message: `no connection "${id}" for this tenant`, retryable: false });
38
+ export const createPostgresConnectionStore = (sql) => ({
39
+ async create({ tenantId, connection }) {
40
+ const rows = await sql.query(`INSERT INTO connections (
41
+ tenant_id, id, kind, provider, label, mode, scheme, metadata, granted_scopes,
42
+ secret_key_id, secret_algorithm, secret_nonce, secret_ciphertext, expires_at
43
+ ) VALUES ($1,$2,$14,$3,$4,$5,$6,$7::jsonb,$8,$9,$10,$11,$12,$13)
44
+ -- No upsert: a second create for the same id is a caller bug, and silently overwriting a credential is
45
+ -- the worst possible resolution of it.
46
+ RETURNING ${COLUMNS}`, [
47
+ tenantId,
48
+ connection.id,
49
+ connection.provider,
50
+ connection.label ?? null,
51
+ connection.mode,
52
+ connection.scheme,
53
+ JSON.stringify(connection.metadata ?? {}),
54
+ connection.grantedScopes === undefined ? null : [...connection.grantedScopes],
55
+ connection.sealed.keyId,
56
+ connection.sealed.algorithm,
57
+ connection.sealed.nonce,
58
+ connection.sealed.ciphertext,
59
+ connection.expiresAt ?? null,
60
+ connection.kind ?? "connection",
61
+ ]);
62
+ const row = rows[0];
63
+ if (row === undefined)
64
+ throw new AgentPlatformError({ code: "conflict", message: `connection "${connection.id}" already exists`, retryable: false });
65
+ return toConnection(row);
66
+ },
67
+ async get({ tenantId, id, kind }) {
68
+ // `revoked_at IS NULL` here rather than in the caller: a revoked connection reading as present is how a
69
+ // withdrawn credential gets used once more.
70
+ const rows = await sql.query(
71
+ // `kind` in the predicate, not filtered afterwards: a caller that did not ask for an app registration
72
+ // must not receive one, and a post-filter is a place somebody later removes.
73
+ `SELECT ${COLUMNS} FROM connections
74
+ WHERE tenant_id = $1 AND id = $2 AND revoked_at IS NULL AND kind = $3`, [tenantId, id, kind ?? "connection"]);
75
+ return rows[0] === undefined ? null : toConnection(rows[0]);
76
+ },
77
+ async list({ tenantId, provider, kind }) {
78
+ const rows = await sql.query(`SELECT ${COLUMNS} FROM connections
79
+ WHERE tenant_id = $1 AND revoked_at IS NULL AND ($2::text IS NULL OR provider = $2) AND kind = $3
80
+ ORDER BY created_at, id`, [tenantId, provider ?? null, kind ?? "connection"]);
81
+ return rows.map(toConnection);
82
+ },
83
+ async update({ tenantId, id, patch }) {
84
+ /**
85
+ * `COALESCE` per column, so an absent field means "leave it" rather than "set it to null".
86
+ *
87
+ * The alternative — building the SET list from the present keys — is how a patch with no fields becomes
88
+ * invalid SQL, and how a field named in one place and not another silently stops being updatable.
89
+ */
90
+ const rows = await sql.query(`UPDATE connections SET
91
+ label = COALESCE($3, label),
92
+ secret_key_id = COALESCE($4, secret_key_id),
93
+ secret_algorithm = COALESCE($5, secret_algorithm),
94
+ secret_nonce = COALESCE($6, secret_nonce),
95
+ secret_ciphertext = COALESCE($7, secret_ciphertext),
96
+ metadata = COALESCE($8::jsonb, metadata),
97
+ granted_scopes = COALESCE($9, granted_scopes),
98
+ expires_at = COALESCE($10, expires_at),
99
+ updated_at = now()
100
+ WHERE tenant_id = $1 AND id = $2 AND revoked_at IS NULL
101
+ RETURNING ${COLUMNS}`, [
102
+ tenantId,
103
+ id,
104
+ patch.label ?? null,
105
+ patch.sealed?.keyId ?? null,
106
+ patch.sealed?.algorithm ?? null,
107
+ patch.sealed?.nonce ?? null,
108
+ patch.sealed?.ciphertext ?? null,
109
+ patch.metadata === undefined ? null : JSON.stringify(patch.metadata),
110
+ patch.grantedScopes === undefined ? null : [...patch.grantedScopes],
111
+ patch.expiresAt ?? null,
112
+ ]);
113
+ if (rows[0] === undefined)
114
+ throw notFound(id);
115
+ return toConnection(rows[0]);
116
+ },
117
+ async revoke({ tenantId, id }) {
118
+ const rows = await sql.query(`UPDATE connections SET revoked_at = now(), updated_at = now()
119
+ WHERE tenant_id = $1 AND id = $2 AND revoked_at IS NULL
120
+ RETURNING id`, [tenantId, id]);
121
+ if (rows[0] !== undefined)
122
+ return;
123
+ // Idempotent: already revoked is success, so a retried disconnect need not distinguish it from a failure.
124
+ const existing = await sql.query(`SELECT id FROM connections WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
125
+ if (existing[0] === undefined)
126
+ throw notFound(id);
127
+ },
128
+ async purge({ tenantId }) {
129
+ // The one hard delete — `docs/18`. A soft-deleted credential is still a credential.
130
+ const rows = await sql.query(`DELETE FROM connections WHERE tenant_id = $1 RETURNING id`, [tenantId]);
131
+ return rows.length;
132
+ },
133
+ });
134
+ //# sourceMappingURL=connections.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Postgres `GraphStore` — REQ-064 (#270), task #271.
3
+ *
4
+ * **Contributions are the stored truth; the entity and relationship tables are a cache of their merge.**
5
+ *
6
+ * That split is the whole design, and it is worth being explicit about why the obvious alternative is wrong.
7
+ * Storing merged entities alone and mutating them per source needs reference counting — "how many sources still
8
+ * name this entity" — and reference counting is the thing everybody gets wrong under concurrency. Storing what
9
+ * each source asserted and rebuilding the merge makes pruning a consequence rather than a bookkeeping exercise:
10
+ * an entity exists precisely while some contribution names it, and nothing has to remember why.
11
+ *
12
+ * The merge is recomputed in TypeScript rather than SQL. A `jsonb_array_elements` aggregation could do it, but
13
+ * the merge rule — lexicographically-first canonical name, longest description, unioned surface forms, weight
14
+ * as the provenance count — would then exist twice, in two languages, and the reference adapter's copy is the
15
+ * one the conformance suite tests. One rule in one place is worth a round trip.
16
+ *
17
+ * Provenance is enforced by a `CHECK` constraint as well as here, deliberately. The application check gives a
18
+ * message that explains itself; the constraint is what still holds when somebody writes to these tables from a
19
+ * migration or a console.
20
+ */
21
+ import type { GraphStore } from "../../persistence/index.js";
22
+ import type { SqlExecutor } from "./sql.js";
23
+ export declare const createPostgresGraphStore: (sql: SqlExecutor) => GraphStore;
24
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Postgres `GraphStore` — REQ-064 (#270), task #271.
3
+ *
4
+ * **Contributions are the stored truth; the entity and relationship tables are a cache of their merge.**
5
+ *
6
+ * That split is the whole design, and it is worth being explicit about why the obvious alternative is wrong.
7
+ * Storing merged entities alone and mutating them per source needs reference counting — "how many sources still
8
+ * name this entity" — and reference counting is the thing everybody gets wrong under concurrency. Storing what
9
+ * each source asserted and rebuilding the merge makes pruning a consequence rather than a bookkeeping exercise:
10
+ * an entity exists precisely while some contribution names it, and nothing has to remember why.
11
+ *
12
+ * The merge is recomputed in TypeScript rather than SQL. A `jsonb_array_elements` aggregation could do it, but
13
+ * the merge rule — lexicographically-first canonical name, longest description, unioned surface forms, weight
14
+ * as the provenance count — would then exist twice, in two languages, and the reference adapter's copy is the
15
+ * one the conformance suite tests. One rule in one place is worth a round trip.
16
+ *
17
+ * Provenance is enforced by a `CHECK` constraint as well as here, deliberately. The application check gives a
18
+ * message that explains itself; the constraint is what still holds when somebody writes to these tables from a
19
+ * migration or a console.
20
+ */
21
+ import { AgentPlatformError } from "../../core/errors.js";
22
+ const iso = (value) => (value instanceof Date ? value.toISOString() : value);
23
+ const toEntity = (row) => ({
24
+ id: row.id,
25
+ name: row.name,
26
+ type: row.type,
27
+ ...(row.description === null ? {} : { description: row.description }),
28
+ surfaceForms: row.surface_forms,
29
+ provenance: row.provenance,
30
+ });
31
+ const toRelationship = (row) => ({
32
+ id: row.id,
33
+ fromId: row.from_id,
34
+ toId: row.to_id,
35
+ type: row.type,
36
+ ...(row.description === null ? {} : { description: row.description }),
37
+ weight: Number(row.weight),
38
+ provenance: row.provenance,
39
+ });
40
+ const toCommunity = (row) => ({
41
+ id: row.id,
42
+ level: Number(row.level),
43
+ entityIds: row.entity_ids,
44
+ relationshipIds: row.relationship_ids,
45
+ chunkIds: row.chunk_ids,
46
+ fingerprint: row.fingerprint,
47
+ ...(row.summary === null ? {} : { summary: row.summary }),
48
+ ...(row.summary_fingerprint === null ? {} : { summaryFingerprint: row.summary_fingerprint }),
49
+ ...(row.summarised_at === null ? {} : { summarisedAt: iso(row.summarised_at) }),
50
+ });
51
+ const uniqueSorted = (values) => [...new Set(values)].sort();
52
+ const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
53
+ /** Rebuilds the merged graph from every contribution. Mirrors `mergeContributions` — see the header. */
54
+ const mergeAll = (contributions) => {
55
+ const entities = new Map();
56
+ const relationships = new Map();
57
+ for (const contribution of contributions) {
58
+ for (const entity of contribution.entities) {
59
+ const existing = entities.get(entity.id);
60
+ if (existing === undefined) {
61
+ entities.set(entity.id, entity);
62
+ continue;
63
+ }
64
+ const description = (entity.description ?? "").length > (existing.description ?? "").length ? entity.description : existing.description;
65
+ entities.set(entity.id, {
66
+ id: entity.id,
67
+ name: existing.name < entity.name ? existing.name : entity.name,
68
+ type: existing.type,
69
+ ...(description === undefined ? {} : { description }),
70
+ surfaceForms: uniqueSorted([...existing.surfaceForms, ...entity.surfaceForms]),
71
+ provenance: uniqueSorted([...existing.provenance, ...entity.provenance]),
72
+ });
73
+ }
74
+ }
75
+ for (const contribution of contributions) {
76
+ for (const edge of contribution.relationships) {
77
+ const existing = relationships.get(edge.id);
78
+ if (existing === undefined) {
79
+ relationships.set(edge.id, edge);
80
+ continue;
81
+ }
82
+ const description = (edge.description ?? "").length > (existing.description ?? "").length ? edge.description : existing.description;
83
+ const provenance = uniqueSorted([...existing.provenance, ...edge.provenance]);
84
+ relationships.set(edge.id, {
85
+ ...existing,
86
+ ...(description === undefined ? {} : { description }),
87
+ weight: provenance.length,
88
+ provenance,
89
+ });
90
+ }
91
+ }
92
+ const kept = [...relationships.values()].filter((edge) => entities.has(edge.fromId) && entities.has(edge.toId));
93
+ return { entities: [...entities.values()].sort(byId), relationships: kept.sort(byId) };
94
+ };
95
+ const assertProvenance = (contribution) => {
96
+ for (const row of [...contribution.entities, ...contribution.relationships]) {
97
+ if (row.provenance.length === 0)
98
+ throw new AgentPlatformError({
99
+ code: "invalid_input",
100
+ message: `${row.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
101
+ retryable: false,
102
+ });
103
+ }
104
+ };
105
+ export const createPostgresGraphStore = (sql) => {
106
+ /** Every contribution for a tenant, which is what the merge is derived from. */
107
+ const contributionsOf = async (tenantId) => {
108
+ const rows = await sql.query(`SELECT entities, relationships FROM knowledge_graph_contributions
109
+ WHERE tenant_id = $1 ORDER BY source_type, source_id`, [tenantId]);
110
+ return rows.map((row) => ({
111
+ entities: (row.entities ?? []),
112
+ relationships: (row.relationships ?? []),
113
+ }));
114
+ };
115
+ /**
116
+ * Rewrites the merged tables from the contributions.
117
+ *
118
+ * Delete-then-insert rather than an upsert-and-sweep, for the reason `replaceSource` is: a partial merge is a
119
+ * graph that is half old and half new, and no reader can tell. Callers run this inside the same unit of work
120
+ * as the contribution write.
121
+ */
122
+ const rebuild = async (tenantId) => {
123
+ const merged = mergeAll(await contributionsOf(tenantId));
124
+ await sql.query(`DELETE FROM knowledge_graph_relationships WHERE tenant_id = $1`, [tenantId]);
125
+ await sql.query(`DELETE FROM knowledge_graph_entities WHERE tenant_id = $1`, [tenantId]);
126
+ for (const entity of merged.entities) {
127
+ await sql.query(`INSERT INTO knowledge_graph_entities
128
+ (tenant_id, id, name, type, description, surface_forms, provenance)
129
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`, [tenantId, entity.id, entity.name, entity.type, entity.description ?? null, entity.surfaceForms, entity.provenance]);
130
+ }
131
+ for (const edge of merged.relationships) {
132
+ await sql.query(`INSERT INTO knowledge_graph_relationships
133
+ (tenant_id, id, from_id, to_id, type, description, weight, provenance)
134
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [tenantId, edge.id, edge.fromId, edge.toId, edge.type, edge.description ?? null, edge.weight, edge.provenance]);
135
+ }
136
+ return { entities: merged.entities.length, relationships: merged.relationships.length };
137
+ };
138
+ const countEntities = async (tenantId) => {
139
+ const rows = await sql.query(`SELECT count(*)::text AS n FROM knowledge_graph_entities WHERE tenant_id = $1`, [tenantId]);
140
+ return Number(rows[0]?.n ?? 0);
141
+ };
142
+ return {
143
+ async getSettings({ tenantId }) {
144
+ const rows = await sql.query(`SELECT enabled, updated_at FROM knowledge_graph_settings WHERE tenant_id = $1`, [tenantId]);
145
+ const row = rows[0];
146
+ // Absent means off. A tenant that has never been configured must not be able to start paying for
147
+ // extraction because a row was missing rather than false.
148
+ return row === undefined
149
+ ? { enabled: false, updatedAt: "1970-01-01T00:00:00.000Z" }
150
+ : { enabled: row.enabled, updatedAt: iso(row.updated_at) };
151
+ },
152
+ async setEnabled({ tenantId, enabled, at }) {
153
+ await sql.query(`INSERT INTO knowledge_graph_settings (tenant_id, enabled, updated_at)
154
+ VALUES ($1, $2, $3)
155
+ ON CONFLICT (tenant_id) DO UPDATE SET enabled = EXCLUDED.enabled, updated_at = EXCLUDED.updated_at`, [tenantId, enabled, at]);
156
+ return { enabled, updatedAt: at };
157
+ },
158
+ async setSourceEnabled({ tenantId, sourceType, sourceId, enabled }) {
159
+ // Never consults the tenant switch — AC-2. The flag outlives it in both directions.
160
+ await sql.query(`INSERT INTO knowledge_graph_sources (tenant_id, source_type, source_id, enabled)
161
+ VALUES ($1, $2, $3, $4)
162
+ ON CONFLICT (tenant_id, source_type, source_id) DO UPDATE SET enabled = EXCLUDED.enabled`, [tenantId, sourceType, sourceId, enabled]);
163
+ },
164
+ async isSourceEnabled({ tenantId, sourceType, sourceId }) {
165
+ const rows = await sql.query(`SELECT enabled FROM knowledge_graph_sources
166
+ WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3`, [tenantId, sourceType, sourceId]);
167
+ return rows[0]?.enabled === true;
168
+ },
169
+ async listEnabledSources({ tenantId, limit, cursor }) {
170
+ const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
171
+ const rows = await sql.query(`SELECT source_type, source_id FROM knowledge_graph_sources
172
+ WHERE tenant_id = $1 AND enabled
173
+ ORDER BY source_type, source_id
174
+ LIMIT $2 OFFSET $3`, [tenantId, limit + 1, offset]);
175
+ const items = rows.slice(0, limit).map((row) => ({
176
+ sourceType: row.source_type,
177
+ sourceId: row.source_id,
178
+ }));
179
+ return rows.length > limit ? { items, nextCursor: String(offset + limit) } : { items };
180
+ },
181
+ async replaceSourceGraph({ tenantId, sourceType, sourceId, contribution }) {
182
+ assertProvenance(contribution);
183
+ const before = await countEntities(tenantId);
184
+ if (contribution.entities.length === 0 && contribution.relationships.length === 0) {
185
+ // An empty contribution is a withdrawal, not an empty row: leaving `{"entities":[]}` behind would make
186
+ // every future merge read a row that says nothing.
187
+ await sql.query(`DELETE FROM knowledge_graph_contributions WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3`, [tenantId, sourceType, sourceId]);
188
+ }
189
+ else {
190
+ await sql.query(`INSERT INTO knowledge_graph_contributions (tenant_id, source_type, source_id, entities, relationships)
191
+ VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
192
+ ON CONFLICT (tenant_id, source_type, source_id)
193
+ DO UPDATE SET entities = EXCLUDED.entities,
194
+ relationships = EXCLUDED.relationships,
195
+ updated_at = now()`, [tenantId, sourceType, sourceId, JSON.stringify(contribution.entities), JSON.stringify(contribution.relationships)]);
196
+ }
197
+ const rebuilt = await rebuild(tenantId);
198
+ return { ...rebuilt, pruned: Math.max(0, before - rebuilt.entities) };
199
+ },
200
+ async deleteSourceGraph({ tenantId, sourceType, sourceId }) {
201
+ const before = await countEntities(tenantId);
202
+ await sql.query(`DELETE FROM knowledge_graph_contributions WHERE tenant_id = $1 AND source_type = $2 AND source_id = $3`, [tenantId, sourceType, sourceId]);
203
+ const rebuilt = await rebuild(tenantId);
204
+ return { pruned: Math.max(0, before - rebuilt.entities) };
205
+ },
206
+ async getEntity({ tenantId, id }) {
207
+ const rows = await sql.query(`SELECT id, name, type, description, surface_forms, provenance
208
+ FROM knowledge_graph_entities WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
209
+ const row = rows[0];
210
+ return row === undefined ? null : toEntity(row);
211
+ },
212
+ async getEntities({ tenantId, ids }) {
213
+ if (ids.length === 0)
214
+ return [];
215
+ const rows = await sql.query(`SELECT id, name, type, description, surface_forms, provenance
216
+ FROM knowledge_graph_entities WHERE tenant_id = $1 AND id = ANY($2) ORDER BY id`, [tenantId, [...ids]]);
217
+ return rows.map(toEntity);
218
+ },
219
+ async resolveEntities({ tenantId, normalisedNames }) {
220
+ if (normalisedNames.length === 0)
221
+ return [];
222
+ const rows = await sql.query(`SELECT id, name, type, description, surface_forms, provenance
223
+ FROM knowledge_graph_entities
224
+ -- The id is \`type:normalisedName\`, and \`normaliseName\` cannot produce a colon, so everything after
225
+ -- the first one is the name. Matching on that keeps query-side and index-side resolution identical
226
+ -- without a second stored column to drift.
227
+ WHERE tenant_id = $1 AND substring(id from position(':' in id) + 1) = ANY($2)
228
+ ORDER BY id`, [tenantId, [...normalisedNames]]);
229
+ return rows.map(toEntity);
230
+ },
231
+ async listEntities({ tenantId, limit, cursor, type }) {
232
+ const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
233
+ const rows = await sql.query(`SELECT id, name, type, description, surface_forms, provenance
234
+ FROM knowledge_graph_entities
235
+ WHERE tenant_id = $1 AND ($2::text IS NULL OR type = $2)
236
+ ORDER BY id
237
+ LIMIT $3 OFFSET $4`, [tenantId, type ?? null, limit + 1, offset]);
238
+ const items = rows.slice(0, limit).map(toEntity);
239
+ return rows.length > limit ? { items, nextCursor: String(offset + limit) } : { items };
240
+ },
241
+ async neighbours({ tenantId, entityIds, limit }) {
242
+ if (entityIds.length === 0 || limit <= 0)
243
+ return [];
244
+ const rows = await sql.query(`SELECT id, from_id, to_id, type, description, weight, provenance
245
+ FROM knowledge_graph_relationships
246
+ WHERE tenant_id = $1 AND (from_id = ANY($2) OR to_id = ANY($2))
247
+ -- Heaviest first, then by id: a total order, so a truncated traversal is reproducible.
248
+ ORDER BY weight DESC, id
249
+ LIMIT $3`, [tenantId, [...entityIds], limit]);
250
+ return rows.map(toRelationship);
251
+ },
252
+ async replaceCommunities({ tenantId, communities }) {
253
+ // Read before the delete, so summaries can be carried over. One query rather than per-community lookups.
254
+ const previous = new Map((await sql.query(`SELECT id, summary, summary_fingerprint, summarised_at
255
+ FROM knowledge_graph_communities WHERE tenant_id = $1`, [tenantId])).map((row) => [row.id, row]));
256
+ await sql.query(`DELETE FROM knowledge_graph_communities WHERE tenant_id = $1`, [tenantId]);
257
+ let summariesKept = 0;
258
+ for (const community of communities) {
259
+ const before = previous.get(community.id);
260
+ // Identical membership only — see the memory adapter for why an id match is not enough.
261
+ const keep = before?.summary != null && before.summary_fingerprint === community.fingerprint;
262
+ if (keep)
263
+ summariesKept += 1;
264
+ await sql.query(`INSERT INTO knowledge_graph_communities
265
+ (tenant_id, id, level, entity_ids, relationship_ids, chunk_ids, fingerprint,
266
+ summary, summary_fingerprint, summarised_at)
267
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`, [
268
+ tenantId,
269
+ community.id,
270
+ community.level,
271
+ community.entityIds,
272
+ community.relationshipIds,
273
+ community.chunkIds,
274
+ community.fingerprint,
275
+ keep ? before?.summary ?? null : null,
276
+ keep ? before?.summary_fingerprint ?? null : null,
277
+ keep ? (before?.summarised_at === null || before?.summarised_at === undefined ? null : iso(before.summarised_at)) : null,
278
+ ]);
279
+ }
280
+ return { written: communities.length, summariesKept };
281
+ },
282
+ async listCommunities({ tenantId, limit, cursor, level }) {
283
+ const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
284
+ const rows = await sql.query(`SELECT id, level, entity_ids, relationship_ids, chunk_ids, fingerprint, summary, summary_fingerprint, summarised_at
285
+ FROM knowledge_graph_communities
286
+ WHERE tenant_id = $1 AND ($2::int IS NULL OR level = $2)
287
+ ORDER BY level, id
288
+ LIMIT $3 OFFSET $4`, [tenantId, level ?? null, limit + 1, offset]);
289
+ const items = rows.slice(0, limit).map(toCommunity);
290
+ return rows.length > limit ? { items, nextCursor: String(offset + limit) } : { items };
291
+ },
292
+ async getCommunity({ tenantId, id }) {
293
+ const rows = await sql.query(`SELECT id, level, entity_ids, relationship_ids, chunk_ids, fingerprint, summary, summary_fingerprint, summarised_at
294
+ FROM knowledge_graph_communities WHERE tenant_id = $1 AND id = $2`, [tenantId, id]);
295
+ const row = rows[0];
296
+ return row === undefined ? null : toCommunity(row);
297
+ },
298
+ async setCommunitySummary({ tenantId, id, summary, fingerprint, at }) {
299
+ await sql.query(`UPDATE knowledge_graph_communities
300
+ SET summary = $3, summary_fingerprint = $4, summarised_at = $5
301
+ WHERE tenant_id = $1 AND id = $2`, [tenantId, id, summary, fingerprint, at]);
302
+ },
303
+ async fingerprint({ tenantId }) {
304
+ // Read from the merged tables, not recomputed — the point is to prove *this adapter's stored state*
305
+ // matches, and recomputing would fingerprint the merge function instead of the database.
306
+ const entities = await sql.query(`SELECT id, name, type, description, surface_forms, provenance
307
+ FROM knowledge_graph_entities WHERE tenant_id = $1 ORDER BY id`, [tenantId]);
308
+ const relationships = await sql.query(`SELECT id, from_id, to_id, type, description, weight, provenance
309
+ FROM knowledge_graph_relationships WHERE tenant_id = $1 ORDER BY id`, [tenantId]);
310
+ return [
311
+ ...entities.map((row) => [
312
+ "E",
313
+ row.id,
314
+ row.name,
315
+ row.type,
316
+ row.description ?? "",
317
+ [...row.surface_forms].sort().join(","),
318
+ [...row.provenance].sort().join(","),
319
+ ].join("")),
320
+ ...relationships.map((row) => ["R", row.id, row.type, String(Number(row.weight)), row.description ?? "", [...row.provenance].sort().join(",")].join("")),
321
+ ].join("\n");
322
+ },
323
+ };
324
+ };
325
+ //# sourceMappingURL=graph.js.map
@@ -27,9 +27,11 @@ export * from "./files.js";
27
27
  export * from "./artifacts.js";
28
28
  export * from "./artifact-exports.js";
29
29
  export * from "./knowledge.js";
30
+ export * from "./graph.js";
30
31
  export * from "./rollups.js";
31
32
  export * from "./evaluation.js";
32
33
  export * from "./retention.js";
33
34
  export * from "./file-content.js";
34
35
  export * from "./flows.js";
36
+ export * from "./connections.js";
35
37
  //# sourceMappingURL=index.d.ts.map
@@ -27,9 +27,11 @@ export * from "./files.js";
27
27
  export * from "./artifacts.js";
28
28
  export * from "./artifact-exports.js";
29
29
  export * from "./knowledge.js";
30
+ export * from "./graph.js";
30
31
  export * from "./rollups.js";
31
32
  export * from "./evaluation.js";
32
33
  export * from "./retention.js";
33
34
  export * from "./file-content.js";
34
35
  export * from "./flows.js";
36
+ export * from "./connections.js";
35
37
  //# sourceMappingURL=index.js.map