@retinue/agentkit 0.1.0 → 0.3.0

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 (246) hide show
  1. package/README.md +59 -277
  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/embeddings/openai.d.ts +45 -0
  6. package/dist/adapters/embeddings/openai.js +109 -0
  7. package/dist/adapters/memory/connections.d.ts +10 -0
  8. package/dist/adapters/memory/connections.js +87 -0
  9. package/dist/adapters/memory/graph.d.ts +17 -0
  10. package/dist/adapters/memory/graph.js +267 -0
  11. package/dist/adapters/memory/index.d.ts +3 -0
  12. package/dist/adapters/memory/index.js +3 -0
  13. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  14. package/dist/adapters/memory/rate-limit.js +28 -0
  15. package/dist/adapters/memory/runtime.js +0 -0
  16. package/dist/adapters/postgres/connections.d.ts +15 -0
  17. package/dist/adapters/postgres/connections.js +134 -0
  18. package/dist/adapters/postgres/graph.d.ts +24 -0
  19. package/dist/adapters/postgres/graph.js +325 -0
  20. package/dist/adapters/postgres/index.d.ts +2 -0
  21. package/dist/adapters/postgres/index.js +2 -0
  22. package/dist/adapters/postgres/migrations.js +198 -0
  23. package/dist/adapters/postgres/run-store.js +26 -2
  24. package/dist/adapters/postgres/schema.d.ts +61 -2
  25. package/dist/adapters/postgres/schema.js +95 -10
  26. package/dist/adapters/redis/index.d.ts +1 -0
  27. package/dist/adapters/redis/index.js +1 -0
  28. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  29. package/dist/adapters/redis/rate-limit.js +55 -0
  30. package/dist/adapters/supabase/index.d.ts +15 -0
  31. package/dist/adapters/supabase/index.js +15 -0
  32. package/dist/adapters/supabase/rls.js +33 -0
  33. package/dist/agents/agent.d.ts +99 -1
  34. package/dist/agents/agent.js +215 -16
  35. package/dist/agents/define.js +24 -4
  36. package/dist/agents/engine.d.ts +48 -0
  37. package/dist/agents/engine.js +370 -13
  38. package/dist/audio/index.d.ts +151 -0
  39. package/dist/audio/index.js +183 -0
  40. package/dist/capabilities/index.d.ts +5 -1
  41. package/dist/capabilities/index.js +23 -0
  42. package/dist/capabilities/runtime.d.ts +8 -0
  43. package/dist/connections/cipher.d.ts +103 -0
  44. package/dist/connections/cipher.js +141 -0
  45. package/dist/connections/index.d.ts +156 -0
  46. package/dist/connections/index.js +72 -0
  47. package/dist/connections/oauth/client.d.ts +84 -0
  48. package/dist/connections/oauth/client.js +141 -0
  49. package/dist/connections/oauth/index.d.ts +166 -0
  50. package/dist/connections/oauth/index.js +226 -0
  51. package/dist/connections/oauth/service.d.ts +90 -0
  52. package/dist/connections/oauth/service.js +132 -0
  53. package/dist/connections/pause.d.ts +68 -0
  54. package/dist/connections/pause.js +97 -0
  55. package/dist/connections/resolver.d.ts +59 -0
  56. package/dist/connections/resolver.js +138 -0
  57. package/dist/connections/resume.d.ts +64 -0
  58. package/dist/connections/resume.js +99 -0
  59. package/dist/connections/vault-cipher.d.ts +95 -0
  60. package/dist/connections/vault-cipher.js +206 -0
  61. package/dist/core/budget.d.ts +55 -0
  62. package/dist/core/budget.js +56 -0
  63. package/dist/core/content-parts.d.ts +26 -2
  64. package/dist/core/content-parts.js +1 -0
  65. package/dist/core/context.d.ts +25 -0
  66. package/dist/core/errors.d.ts +1 -1
  67. package/dist/core/errors.js +15 -0
  68. package/dist/core/events.d.ts +110 -2
  69. package/dist/core/events.js +33 -2
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.js +1 -0
  72. package/dist/core/validation.js +19 -0
  73. package/dist/documents/index.d.ts +14 -0
  74. package/dist/documents/parsers/text.d.ts +16 -0
  75. package/dist/documents/parsers/text.js +54 -2
  76. package/dist/entries/connections.d.ts +10 -0
  77. package/dist/entries/connections.js +10 -0
  78. package/dist/entries/guardrails.d.ts +14 -0
  79. package/dist/entries/guardrails.js +14 -0
  80. package/dist/entries/knowledge.d.ts +17 -0
  81. package/dist/entries/knowledge.js +16 -0
  82. package/dist/entries/mcp-server.d.ts +9 -0
  83. package/dist/entries/mcp-server.js +9 -0
  84. package/dist/entries/runtime.d.ts +10 -0
  85. package/dist/entries/runtime.js +8 -0
  86. package/dist/entries/testing.d.ts +28 -0
  87. package/dist/entries/testing.js +28 -0
  88. package/dist/graphql/resolvers.d.ts +7 -1
  89. package/dist/graphql/resolvers.js +20 -0
  90. package/dist/graphql/schema.d.ts +1 -1
  91. package/dist/graphql/schema.js +44 -0
  92. package/dist/guardrails/index.d.ts +115 -0
  93. package/dist/guardrails/index.js +108 -0
  94. package/dist/guardrails/moderation.d.ts +53 -0
  95. package/dist/guardrails/moderation.js +75 -0
  96. package/dist/guardrails/pii.d.ts +75 -0
  97. package/dist/guardrails/pii.js +193 -0
  98. package/dist/knowledge/communities.d.ts +166 -0
  99. package/dist/knowledge/communities.js +377 -0
  100. package/dist/knowledge/graph-global.d.ts +153 -0
  101. package/dist/knowledge/graph-global.js +246 -0
  102. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  103. package/dist/knowledge/graph-retrieval.js +303 -0
  104. package/dist/knowledge/graph.d.ts +223 -0
  105. package/dist/knowledge/graph.js +369 -0
  106. package/dist/knowledge/index.d.ts +25 -0
  107. package/dist/knowledge/index.js +37 -1
  108. package/dist/knowledge/navigate.d.ts +89 -0
  109. package/dist/knowledge/navigate.js +107 -0
  110. package/dist/knowledge/retrieval.d.ts +98 -5
  111. package/dist/knowledge/retrieval.js +158 -28
  112. package/dist/mcp-server/index.d.ts +168 -0
  113. package/dist/mcp-server/index.js +175 -0
  114. package/dist/models/extraction.d.ts +73 -0
  115. package/dist/models/extraction.js +118 -0
  116. package/dist/models/index.d.ts +40 -1
  117. package/dist/models/index.js +22 -1
  118. package/dist/models/pricing.d.ts +2 -0
  119. package/dist/models/pricing.js +17 -1
  120. package/dist/models/provider-factory.js +9 -7
  121. package/dist/models/streaming.d.ts +111 -1
  122. package/dist/models/streaming.js +184 -3
  123. package/dist/persistence/index.d.ts +230 -0
  124. package/dist/persistence/index.js +2 -0
  125. package/dist/runtime/index.d.ts +1 -1
  126. package/dist/runtime/index.js +12 -0
  127. package/dist/runtime/worker.d.ts +36 -1
  128. package/dist/runtime/worker.js +37 -4
  129. package/dist/security/checklist.js +55 -0
  130. package/dist/security/findings.js +18 -9
  131. package/dist/server/bin.d.ts +19 -0
  132. package/dist/server/bin.js +226 -0
  133. package/dist/server/boot.d.ts +20 -2
  134. package/dist/server/boot.js +29 -3
  135. package/dist/server/cli.d.ts +13 -0
  136. package/dist/server/cli.js +7 -4
  137. package/dist/server/doctor.d.ts +74 -0
  138. package/dist/server/doctor.js +280 -0
  139. package/dist/skills/catalogue.d.ts +49 -0
  140. package/dist/skills/catalogue.js +61 -0
  141. package/dist/skills/context.d.ts +61 -0
  142. package/dist/skills/context.js +107 -0
  143. package/dist/skills/index.d.ts +2 -0
  144. package/dist/skills/index.js +2 -0
  145. package/dist/telemetry/spans.js +17 -0
  146. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  147. package/dist/testing/conformance/artifact-exports.js +172 -0
  148. package/dist/testing/conformance/artifacts.d.ts +11 -0
  149. package/dist/testing/conformance/artifacts.js +316 -0
  150. package/dist/testing/conformance/audio.d.ts +38 -0
  151. package/dist/testing/conformance/audio.js +145 -0
  152. package/dist/testing/conformance/capability.d.ts +23 -0
  153. package/dist/testing/conformance/capability.js +27 -0
  154. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  155. package/dist/testing/conformance/checkpoint-store.js +72 -0
  156. package/dist/testing/conformance/connections.d.ts +14 -0
  157. package/dist/testing/conformance/connections.js +171 -0
  158. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  159. package/dist/testing/conformance/conversation-store.js +60 -0
  160. package/dist/testing/conformance/evaluation.d.ts +10 -0
  161. package/dist/testing/conformance/evaluation.js +295 -0
  162. package/dist/testing/conformance/files.d.ts +19 -0
  163. package/dist/testing/conformance/files.js +454 -0
  164. package/dist/testing/conformance/flows.d.ts +16 -0
  165. package/dist/testing/conformance/flows.js +193 -0
  166. package/dist/testing/conformance/graph.d.ts +22 -0
  167. package/dist/testing/conformance/graph.js +500 -0
  168. package/dist/testing/conformance/hitl.d.ts +25 -0
  169. package/dist/testing/conformance/hitl.js +523 -0
  170. package/dist/testing/conformance/index.d.ts +163 -0
  171. package/dist/testing/conformance/index.js +321 -0
  172. package/dist/testing/conformance/invariants.d.ts +23 -0
  173. package/dist/testing/conformance/invariants.js +80 -0
  174. package/dist/testing/conformance/knowledge.d.ts +41 -0
  175. package/dist/testing/conformance/knowledge.js +592 -0
  176. package/dist/testing/conformance/parents.d.ts +50 -0
  177. package/dist/testing/conformance/parents.js +39 -0
  178. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  179. package/dist/testing/conformance/rate-limit.js +71 -0
  180. package/dist/testing/conformance/records.d.ts +36 -0
  181. package/dist/testing/conformance/records.js +400 -0
  182. package/dist/testing/conformance/rollups.d.ts +30 -0
  183. package/dist/testing/conformance/rollups.js +425 -0
  184. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  185. package/dist/testing/conformance/run-coordinator.js +150 -0
  186. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  187. package/dist/testing/conformance/run-event-log.js +138 -0
  188. package/dist/testing/conformance/run-store.d.ts +12 -0
  189. package/dist/testing/conformance/run-store.js +294 -0
  190. package/dist/testing/conformance/session-state.d.ts +38 -0
  191. package/dist/testing/conformance/session-state.js +198 -0
  192. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  193. package/dist/testing/conformance/usage-limits.js +309 -0
  194. package/dist/testing/conformance.d.ts +8 -0
  195. package/dist/testing/conformance.js +8 -0
  196. package/dist/testing/memory-backend.d.ts +39 -0
  197. package/dist/testing/memory-backend.js +41 -0
  198. package/dist/testing/pglite.d.ts +55 -0
  199. package/dist/testing/pglite.js +78 -0
  200. package/dist/testing/stub-model.d.ts +58 -0
  201. package/dist/testing/stub-model.js +71 -0
  202. package/dist/testing/supabase-storage-double.d.ts +35 -0
  203. package/dist/testing/supabase-storage-double.js +101 -0
  204. package/dist/toolkit/files.d.ts +125 -0
  205. package/dist/toolkit/files.js +320 -0
  206. package/dist/toolkit/http.d.ts +27 -0
  207. package/dist/toolkit/http.js +48 -0
  208. package/dist/toolkit/index.d.ts +31 -1
  209. package/dist/toolkit/index.js +26 -1
  210. package/dist/toolkit/mime.d.ts +97 -0
  211. package/dist/toolkit/mime.js +233 -0
  212. package/dist/toolkit/sandbox.d.ts +119 -0
  213. package/dist/toolkit/sandbox.js +239 -0
  214. package/dist/toolkit/ssrf.d.ts +139 -0
  215. package/dist/toolkit/ssrf.js +349 -0
  216. package/dist/toolkit/vendor.d.ts +91 -0
  217. package/dist/toolkit/vendor.js +153 -0
  218. package/dist/toolkit/web.d.ts +13 -0
  219. package/dist/toolkit/web.js +7 -1
  220. package/dist/tools/budget.d.ts +28 -0
  221. package/dist/tools/budget.js +35 -0
  222. package/dist/tools/credentials.d.ts +316 -0
  223. package/dist/tools/credentials.js +369 -0
  224. package/dist/tools/define.d.ts +45 -0
  225. package/dist/tools/define.js +26 -0
  226. package/dist/tools/find.d.ts +109 -0
  227. package/dist/tools/find.js +210 -0
  228. package/dist/tools/index.d.ts +35 -2
  229. package/dist/tools/index.js +11 -0
  230. package/dist/tools/library/audio.d.ts +63 -0
  231. package/dist/tools/library/audio.js +148 -0
  232. package/dist/tools/library/fs.d.ts +24 -0
  233. package/dist/tools/library/fs.js +102 -0
  234. package/dist/tools/library/index.d.ts +37 -2
  235. package/dist/tools/library/index.js +47 -0
  236. package/dist/tools/library/shell.d.ts +45 -0
  237. package/dist/tools/library/shell.js +70 -0
  238. package/dist/tools/meta-tools.js +8 -0
  239. package/dist/tools/registry.d.ts +146 -0
  240. package/dist/tools/registry.js +250 -4
  241. package/dist/usage/index.d.ts +21 -0
  242. package/dist/usage/index.js +1 -0
  243. package/dist/usage/rate-limit.d.ts +122 -0
  244. package/dist/usage/rate-limit.js +131 -0
  245. package/dist/usage/recorder.js +3 -0
  246. package/package.json +33 -2
@@ -0,0 +1,109 @@
1
+ /**
2
+ * An OpenAI-compatible embedding adapter — REQ-050 (#209), task #219.
3
+ *
4
+ * The `EmbeddingProvider` port has existed since #136 with **no adapter of any kind**, which means the semantic
5
+ * half of hybrid retrieval has never run against a real model in this repository: every test supplies a stub, and
6
+ * a stub measures the stub. That is why this exists before the eval does — a retrieval score computed over
7
+ * hash-based pseudo-vectors is a number about nothing.
8
+ *
9
+ * OpenAI's shape rather than OpenAI specifically: the same request works against Azure OpenAI, Together, a local
10
+ * `llama.cpp` server and anything else that copied the endpoint, so `baseUrl` is the whole configuration story.
11
+ *
12
+ * ## Two properties worth stating
13
+ *
14
+ * **Order is checked, not assumed.** The API returns objects carrying an `index`, and the port's contract is one
15
+ * vector per input *in order*. A provider that returned them out of order — or dropped one — would silently pair
16
+ * every chunk with its neighbour's vector, and retrieval would still work well enough to look fine. So the
17
+ * response is sorted by `index` and the count is verified.
18
+ *
19
+ * **The model reference records a version.** Providers change what a model id returns without renaming it, and a
20
+ * corpus embedded across such a change has two incomparable halves. The port carries the ref per chunk precisely
21
+ * so that is detectable; this passes the caller's version through rather than inventing one.
22
+ */
23
+ import { AgentPlatformError } from "../../core/errors.js";
24
+ export const DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small";
25
+ export const DEFAULT_EMBEDDING_BATCH = 96;
26
+ export const createOpenAiEmbeddings = (config) => {
27
+ const modelId = config.modelId ?? DEFAULT_EMBEDDING_MODEL;
28
+ const model = {
29
+ modelId,
30
+ version: config.version ?? "1",
31
+ dimensions: config.dimensions ?? 1536,
32
+ };
33
+ const doFetch = config.fetchImpl ?? fetch;
34
+ const base = (config.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
35
+ const batchSize = config.batchSize ?? DEFAULT_EMBEDDING_BATCH;
36
+ const embedBatch = async (texts) => {
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), config.timeoutMs ?? 60_000);
39
+ let response;
40
+ try {
41
+ response = await doFetch(`${base}/embeddings`, {
42
+ method: "POST",
43
+ signal: controller.signal,
44
+ headers: { authorization: `Bearer ${config.apiKey}`, "content-type": "application/json" },
45
+ body: JSON.stringify({
46
+ model: modelId,
47
+ input: texts,
48
+ // Asked for explicitly, because the 3-series models support shortening and a deployment that pinned
49
+ // 1536 in its column must get 1536 rather than whatever the default becomes.
50
+ ...(model.dimensions === 1536 ? {} : { dimensions: model.dimensions }),
51
+ }),
52
+ });
53
+ }
54
+ catch (error) {
55
+ throw new AgentPlatformError({
56
+ code: error.name === "AbortError" ? "timeout" : "provider_unavailable",
57
+ message: `The embedding endpoint did not respond: ${error.message}`,
58
+ retryable: true,
59
+ });
60
+ }
61
+ finally {
62
+ clearTimeout(timer);
63
+ }
64
+ const payload = (await response.json().catch(() => ({})));
65
+ if (!response.ok) {
66
+ const rateLimited = response.status === 429;
67
+ throw new AgentPlatformError({
68
+ code: rateLimited ? "rate_limited" : response.status >= 500 ? "provider_unavailable" : "provider_error",
69
+ message: `The embedding endpoint returned ${response.status}: ${payload.error?.message ?? "no message"}`,
70
+ // A 5xx and a rate limit are worth retrying; a 400 means the request is wrong and will stay wrong.
71
+ retryable: rateLimited || response.status >= 500,
72
+ });
73
+ }
74
+ const data = [...(payload.data ?? [])].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
75
+ if (data.length !== texts.length) {
76
+ throw new AgentPlatformError({
77
+ code: "provider_error",
78
+ message: `Asked for ${texts.length} embeddings and received ${data.length}. Pairing them would attach every chunk to the wrong vector.`,
79
+ retryable: false,
80
+ });
81
+ }
82
+ return data.map((entry, at) => {
83
+ const vector = entry.embedding;
84
+ if (vector === undefined || vector.length !== model.dimensions) {
85
+ throw new AgentPlatformError({
86
+ code: "provider_error",
87
+ message: `Embedding ${at} has ${vector?.length ?? 0} dimensions; this deployment records ${model.dimensions}.`,
88
+ retryable: false,
89
+ });
90
+ }
91
+ return vector;
92
+ });
93
+ };
94
+ return {
95
+ model,
96
+ async embed(texts) {
97
+ if (texts.length === 0)
98
+ return [];
99
+ const out = [];
100
+ // Sequential batches, deliberately: parallel ones hit the rate limit on a first index of a real corpus,
101
+ // and the failure arrives as a 429 in the middle of a job rather than as a slower job.
102
+ for (let at = 0; at < texts.length; at += batchSize) {
103
+ out.push(...(await embedBatch(texts.slice(at, at + batchSize))));
104
+ }
105
+ return out;
106
+ },
107
+ };
108
+ };
109
+ //# sourceMappingURL=openai.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The reference `ConnectionStore` — task #261.
3
+ *
4
+ * The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
5
+ * `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
6
+ * assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
7
+ */
8
+ import type { ConnectionStore } from "../../connections/index.js";
9
+ export declare const createMemoryConnectionStore: (now?: () => string) => ConnectionStore;
10
+ //# sourceMappingURL=connections.d.ts.map
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The reference `ConnectionStore` — task #261.
3
+ *
4
+ * The baseline the other adapters are compared against, and the one the conformance suite runs first. It holds
5
+ * `SealedSecret`s exactly as Postgres does: an in-memory adapter that stored plaintext would make the raw-row
6
+ * assertion pass for the wrong reason, and the suite would then be asserting a different guarantee per adapter.
7
+ */
8
+ import { AgentPlatformError } from "../../core/errors.js";
9
+ const notFound = (id) => new AgentPlatformError({ code: "not_found", message: `no connection "${id}" for this tenant`, retryable: false });
10
+ export const createMemoryConnectionStore = (now = () => new Date().toISOString()) => {
11
+ // Keyed by tenant, so a missing tenant filter is a missing map rather than a leak.
12
+ const byTenant = new Map();
13
+ const of = (tenantId) => {
14
+ const existing = byTenant.get(tenantId);
15
+ if (existing !== undefined)
16
+ return existing;
17
+ const created = new Map();
18
+ byTenant.set(tenantId, created);
19
+ return created;
20
+ };
21
+ return {
22
+ async create({ tenantId, connection }) {
23
+ const rows = of(tenantId);
24
+ if (rows.has(connection.id))
25
+ throw new AgentPlatformError({
26
+ code: "conflict",
27
+ message: `connection "${connection.id}" already exists for this tenant`,
28
+ retryable: false,
29
+ });
30
+ const at = now();
31
+ const row = { ...connection, createdAt: at, updatedAt: at };
32
+ rows.set(connection.id, row);
33
+ return row;
34
+ },
35
+ async get({ tenantId, id, kind }) {
36
+ const row = of(tenantId).get(id);
37
+ // A caller that did not ask for an app registration must not receive one — see `ConnectionKind`.
38
+ if (row !== undefined && (row.kind ?? "connection") !== (kind ?? "connection"))
39
+ return null;
40
+ // A revoked connection reads as absent. Anything else would let a caller resolve a credential somebody
41
+ // deliberately withdrew.
42
+ return row === undefined || row.revokedAt !== undefined ? null : row;
43
+ },
44
+ async list({ tenantId, provider, kind }) {
45
+ return [...of(tenantId).values()]
46
+ .filter((c) => c.revokedAt === undefined &&
47
+ (provider === undefined || c.provider === provider) &&
48
+ (c.kind ?? "connection") === (kind ?? "connection"))
49
+ .sort((a, b) => (a.createdAt === b.createdAt ? a.id.localeCompare(b.id) : a.createdAt.localeCompare(b.createdAt)));
50
+ },
51
+ async update({ tenantId, id, patch }) {
52
+ const rows = of(tenantId);
53
+ const existing = rows.get(id);
54
+ if (existing === undefined || existing.revokedAt !== undefined)
55
+ throw notFound(id);
56
+ const updated = {
57
+ ...existing,
58
+ ...(patch.label === undefined ? {} : { label: patch.label }),
59
+ ...(patch.sealed === undefined ? {} : { sealed: patch.sealed }),
60
+ ...(patch.metadata === undefined ? {} : { metadata: patch.metadata }),
61
+ ...(patch.grantedScopes === undefined ? {} : { grantedScopes: patch.grantedScopes }),
62
+ ...(patch.expiresAt === undefined ? {} : { expiresAt: patch.expiresAt }),
63
+ updatedAt: now(),
64
+ };
65
+ rows.set(id, updated);
66
+ return updated;
67
+ },
68
+ async revoke({ tenantId, id }) {
69
+ const rows = of(tenantId);
70
+ const existing = rows.get(id);
71
+ if (existing === undefined)
72
+ throw notFound(id);
73
+ // Idempotent: revoking twice is not an error, because a caller retrying a disconnect should not have to
74
+ // distinguish "already gone" from "failed".
75
+ if (existing.revokedAt !== undefined)
76
+ return;
77
+ rows.set(id, { ...existing, revokedAt: now(), updatedAt: now() });
78
+ },
79
+ async purge({ tenantId }) {
80
+ const rows = byTenant.get(tenantId);
81
+ const count = rows?.size ?? 0;
82
+ byTenant.delete(tenantId);
83
+ return count;
84
+ },
85
+ };
86
+ };
87
+ //# sourceMappingURL=connections.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
3
+ *
4
+ * The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
5
+ * it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
6
+ * Three things it takes as seriously as a database would:
7
+ *
8
+ * - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
9
+ * prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
10
+ * - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
11
+ * the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
12
+ * - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
13
+ * document withdraws its claims and prunes only what nothing else asserts.
14
+ */
15
+ import type { GraphStore } from "../../persistence/index.js";
16
+ export declare const createMemoryGraphStore: () => GraphStore;
17
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1,267 @@
1
+ /**
2
+ * In-memory `GraphStore` — the reference implementation (REQ-064 #270, task #271).
3
+ *
4
+ * The same standard the in-memory `KnowledgeStore` holds itself to: this is not a stub that makes tests pass,
5
+ * it is the definition of the contract that the Postgres adapter is then held to by the same conformance suite.
6
+ * Three things it takes as seriously as a database would:
7
+ *
8
+ * - **Tenant isolation is structural.** Every map is keyed by tenant first, so cross-tenant reads are not
9
+ * prevented by a filter somebody could forget — there is nowhere for another tenant's rows to be.
10
+ * - **Provenance is enforced, not assumed.** A row with no chunks is refused here, so the refusal is part of
11
+ * the contract rather than a Postgres constraint that the reference adapter quietly tolerates.
12
+ * - **A source's contribution is what is replaced.** Entities are shared between sources; re-indexing one
13
+ * document withdraws its claims and prunes only what nothing else asserts.
14
+ */
15
+ import { AgentPlatformError } from "../../core/errors.js";
16
+ const sourceKey = (sourceType, sourceId) => `${sourceType} ${sourceId}`;
17
+ const DEFAULT_SETTINGS = { enabled: false, updatedAt: "1970-01-01T00:00:00.000Z" };
18
+ const uniqueSorted = (values) => [...new Set(values)].sort();
19
+ const byId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
20
+ /**
21
+ * Every contribution, merged into the graph a reader sees.
22
+ *
23
+ * Recomputed on read rather than maintained on write. That is the right trade for a reference adapter: the
24
+ * merge rule then exists in exactly one place, and a bug in it cannot leave a stored graph subtly inconsistent
25
+ * with the contributions it came from. Postgres maintains merged rows for query speed and is held to producing
26
+ * the same answer by `fingerprint`.
27
+ */
28
+ const mergeAll = (source) => {
29
+ /**
30
+ * Materialised, because this function walks it **twice** — entities, then relationships.
31
+ *
32
+ * The first version took the `Iterable` and looped it directly, which works for an array and is silently
33
+ * wrong for `Map.values()`: an iterator is exhausted by the first pass, so the second saw nothing and *every
34
+ * edge was dropped*. Entities were correct, so the graph looked plausible; only a traversal returned empty.
35
+ * Found by the neighbour-ordering test, and worth the parameter rename so the two loops cannot drift back.
36
+ */
37
+ const contributions = [...source];
38
+ const entities = new Map();
39
+ const relationships = new Map();
40
+ for (const contribution of contributions) {
41
+ for (const entity of contribution.entities) {
42
+ const existing = entities.get(entity.id);
43
+ if (existing === undefined) {
44
+ entities.set(entity.id, entity);
45
+ continue;
46
+ }
47
+ const description = (entity.description ?? "").length > (existing.description ?? "").length ? entity.description : existing.description;
48
+ entities.set(entity.id, {
49
+ id: entity.id,
50
+ // Lexicographically first, matching `mergeContributions`. "First written" would depend on source order,
51
+ // and source order is a thing that changes without the corpus changing.
52
+ name: existing.name < entity.name ? existing.name : entity.name,
53
+ type: existing.type,
54
+ ...(description === undefined ? {} : { description }),
55
+ surfaceForms: uniqueSorted([...existing.surfaceForms, ...entity.surfaceForms]),
56
+ provenance: uniqueSorted([...existing.provenance, ...entity.provenance]),
57
+ });
58
+ }
59
+ }
60
+ for (const contribution of contributions) {
61
+ for (const edge of contribution.relationships) {
62
+ const existing = relationships.get(edge.id);
63
+ if (existing === undefined) {
64
+ relationships.set(edge.id, edge);
65
+ continue;
66
+ }
67
+ const description = (edge.description ?? "").length > (existing.description ?? "").length ? edge.description : existing.description;
68
+ const provenance = uniqueSorted([...existing.provenance, ...edge.provenance]);
69
+ relationships.set(edge.id, {
70
+ ...existing,
71
+ ...(description === undefined ? {} : { description }),
72
+ weight: provenance.length,
73
+ provenance,
74
+ });
75
+ }
76
+ }
77
+ // An edge whose endpoints are gone goes with them — the state after a source is removed.
78
+ const kept = [...relationships.values()].filter((edge) => entities.has(edge.fromId) && entities.has(edge.toId));
79
+ return { entities: [...entities.values()].sort(byId), relationships: kept.sort(byId) };
80
+ };
81
+ /** Refuses an untraceable claim. See `GraphStore` — provenance is structural. */
82
+ const assertProvenance = (contribution) => {
83
+ for (const entity of contribution.entities) {
84
+ if (entity.provenance.length === 0)
85
+ throw new AgentPlatformError({
86
+ code: "invalid_input",
87
+ message: `entity ${entity.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
88
+ retryable: false,
89
+ });
90
+ }
91
+ for (const edge of contribution.relationships) {
92
+ if (edge.provenance.length === 0)
93
+ throw new AgentPlatformError({
94
+ code: "invalid_input",
95
+ message: `relationship ${edge.id} has no provenance; a graph claim with no chunk behind it is one the model would cite`,
96
+ retryable: false,
97
+ });
98
+ }
99
+ };
100
+ export const createMemoryGraphStore = () => {
101
+ const tenants = new Map();
102
+ const graphOf = (tenantId) => {
103
+ let graph = tenants.get(tenantId);
104
+ if (!graph) {
105
+ tenants.set(tenantId, (graph = { settings: DEFAULT_SETTINGS, sourceFlags: new Map(), contributions: new Map(), communities: new Map() }));
106
+ }
107
+ return graph;
108
+ };
109
+ const page = (items, limit, cursor) => {
110
+ const offset = cursor === undefined ? 0 : Number.parseInt(cursor, 10) || 0;
111
+ const slice = items.slice(offset, offset + limit);
112
+ const next = offset + slice.length;
113
+ return next < items.length ? { items: slice, nextCursor: String(next) } : { items: slice };
114
+ };
115
+ return {
116
+ async getSettings({ tenantId }) {
117
+ return graphOf(tenantId).settings;
118
+ },
119
+ async setEnabled({ tenantId, enabled, at }) {
120
+ const graph = graphOf(tenantId);
121
+ graph.settings = { enabled, updatedAt: at };
122
+ return graph.settings;
123
+ },
124
+ async setSourceEnabled({ tenantId, sourceType, sourceId, enabled }) {
125
+ // Stored regardless of the tenant switch — AC-2. Marking a handbook today and enabling the tenant next
126
+ // week must not require re-marking anything, so this never consults `settings`.
127
+ graphOf(tenantId).sourceFlags.set(sourceKey(sourceType, sourceId), enabled);
128
+ },
129
+ async isSourceEnabled({ tenantId, sourceType, sourceId }) {
130
+ return graphOf(tenantId).sourceFlags.get(sourceKey(sourceType, sourceId)) === true;
131
+ },
132
+ async listEnabledSources({ tenantId, limit, cursor }) {
133
+ const rows = [...graphOf(tenantId).sourceFlags.entries()]
134
+ .filter(([, enabled]) => enabled)
135
+ .map(([key]) => {
136
+ const at = key.indexOf(" ");
137
+ return { sourceType: key.slice(0, at), sourceId: key.slice(at + 1) };
138
+ })
139
+ .sort((a, b) => (sourceKey(a.sourceType, a.sourceId) < sourceKey(b.sourceType, b.sourceId) ? -1 : 1));
140
+ return page(rows, limit, cursor);
141
+ },
142
+ async replaceSourceGraph({ tenantId, sourceType, sourceId, contribution }) {
143
+ assertProvenance(contribution);
144
+ const graph = graphOf(tenantId);
145
+ const before = mergeAll(graph.contributions.values()).entities.length;
146
+ const key = sourceKey(sourceType, sourceId);
147
+ if (contribution.entities.length === 0 && contribution.relationships.length === 0) {
148
+ graph.contributions.delete(key);
149
+ }
150
+ else {
151
+ graph.contributions.set(key, { entities: contribution.entities, relationships: contribution.relationships });
152
+ }
153
+ const after = mergeAll(graph.contributions.values());
154
+ return {
155
+ entities: after.entities.length,
156
+ relationships: after.relationships.length,
157
+ // What this replacement removed from the graph, not what it removed from the source. An operator asking
158
+ // "did re-indexing lose anything" is asking the first question.
159
+ pruned: Math.max(0, before - after.entities.length),
160
+ };
161
+ },
162
+ async deleteSourceGraph({ tenantId, sourceType, sourceId }) {
163
+ const graph = graphOf(tenantId);
164
+ const before = mergeAll(graph.contributions.values()).entities.length;
165
+ graph.contributions.delete(sourceKey(sourceType, sourceId));
166
+ const after = mergeAll(graph.contributions.values()).entities.length;
167
+ return { pruned: Math.max(0, before - after) };
168
+ },
169
+ async getEntity({ tenantId, id }) {
170
+ return mergeAll(graphOf(tenantId).contributions.values()).entities.find((entity) => entity.id === id) ?? null;
171
+ },
172
+ async getEntities({ tenantId, ids }) {
173
+ const wanted = new Set(ids);
174
+ return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id));
175
+ },
176
+ async resolveEntities({ tenantId, normalisedNames }) {
177
+ if (normalisedNames.length === 0)
178
+ return [];
179
+ // The name is everything after the first colon; `normaliseName` cannot produce one, so the split is safe.
180
+ const wanted = new Set(normalisedNames);
181
+ return mergeAll(graphOf(tenantId).contributions.values()).entities.filter((entity) => wanted.has(entity.id.slice(entity.id.indexOf(":") + 1)));
182
+ },
183
+ async listEntities({ tenantId, limit, cursor, type }) {
184
+ const all = mergeAll(graphOf(tenantId).contributions.values()).entities;
185
+ return page(type === undefined ? all : all.filter((entity) => entity.type === type), limit, cursor);
186
+ },
187
+ async neighbours({ tenantId, entityIds, limit }) {
188
+ const wanted = new Set(entityIds);
189
+ const { relationships } = mergeAll(graphOf(tenantId).contributions.values());
190
+ return relationships
191
+ .filter((edge) => wanted.has(edge.fromId) || wanted.has(edge.toId))
192
+ // Heaviest first — the edge the most chunks asserted is the one most worth traversing. Ties broken by
193
+ // id so the order is total, which is what makes a truncated traversal reproducible.
194
+ .sort((a, b) => (b.weight - a.weight) || byId(a, b))
195
+ .slice(0, Math.max(0, limit));
196
+ },
197
+ async replaceCommunities({ tenantId, communities }) {
198
+ const graph = graphOf(tenantId);
199
+ const previous = new Map(graph.communities);
200
+ graph.communities.clear();
201
+ let summariesKept = 0;
202
+ for (const community of communities) {
203
+ const before = previous.get(community.id);
204
+ /**
205
+ * A summary survives a re-clustering **only when the membership is identical**.
206
+ *
207
+ * Carrying it over on id alone would attach a summary of the old members to the new ones — a
208
+ * confidently wrong description of a community that changed, which is worse than having none.
209
+ */
210
+ const keep = before?.summary !== undefined && before.summaryFingerprint === community.fingerprint;
211
+ if (keep)
212
+ summariesKept += 1;
213
+ graph.communities.set(community.id, {
214
+ ...community,
215
+ ...(keep && before !== undefined
216
+ ? {
217
+ summary: before.summary,
218
+ summaryFingerprint: before.summaryFingerprint,
219
+ ...(before.summarisedAt === undefined ? {} : { summarisedAt: before.summarisedAt }),
220
+ }
221
+ : {}),
222
+ });
223
+ }
224
+ return { written: communities.length, summariesKept };
225
+ },
226
+ async listCommunities({ tenantId, limit, cursor, level }) {
227
+ const all = [...graphOf(tenantId).communities.values()]
228
+ .filter((community) => level === undefined || community.level === level)
229
+ .sort((a, b) => a.level - b.level || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
230
+ return page(all, limit, cursor);
231
+ },
232
+ async getCommunity({ tenantId, id }) {
233
+ return graphOf(tenantId).communities.get(id) ?? null;
234
+ },
235
+ async setCommunitySummary({ tenantId, id, summary, fingerprint, at }) {
236
+ const graph = graphOf(tenantId);
237
+ const community = graph.communities.get(id);
238
+ if (community === undefined)
239
+ return;
240
+ graph.communities.set(id, { ...community, summary, summaryFingerprint: fingerprint, summarisedAt: at });
241
+ },
242
+ async fingerprint({ tenantId }) {
243
+ const { entities, relationships } = mergeAll(graphOf(tenantId).contributions.values());
244
+ /**
245
+ * A stable serialisation, on the port rather than in a test — AC-6.
246
+ *
247
+ * Every field that a re-index could plausibly reorder is included: surface forms and provenance are the
248
+ * two that actually caught bugs, because both are sets that arrive in whatever order extraction produced.
249
+ * Leaving them out would let the fingerprint agree while the graph differed.
250
+ */
251
+ const lines = [
252
+ ...entities.map((entity) => [
253
+ "E",
254
+ entity.id,
255
+ entity.name,
256
+ entity.type,
257
+ entity.description ?? "",
258
+ entity.surfaceForms.join(","),
259
+ entity.provenance.join(","),
260
+ ].join("")),
261
+ ...relationships.map((edge) => ["R", edge.id, edge.type, String(edge.weight), edge.description ?? "", edge.provenance.join(",")].join("")),
262
+ ];
263
+ return lines.join("\n");
264
+ },
265
+ };
266
+ };
267
+ //# sourceMappingURL=graph.js.map
@@ -20,6 +20,7 @@ export * from "./files.js";
20
20
  export * from "./artifacts.js";
21
21
  export * from "./artifact-exports.js";
22
22
  export * from "./knowledge.js";
23
+ export * from "./graph.js";
23
24
  export * from "./evaluation.js";
24
25
  /** Idempotency store reference adapter. */
25
26
  export * from "./idempotency.js";
@@ -36,4 +37,6 @@ export * from "./principal-memory.js";
36
37
  /** Message + agent reference adapters (used by the embedded agent facade). */
37
38
  export * from "./message-store.js";
38
39
  export * from "./flows.js";
40
+ export * from "./rate-limit.js";
41
+ export * from "./connections.js";
39
42
  //# sourceMappingURL=index.d.ts.map
@@ -88,6 +88,7 @@ export * from "./files.js";
88
88
  export * from "./artifacts.js";
89
89
  export * from "./artifact-exports.js";
90
90
  export * from "./knowledge.js";
91
+ export * from "./graph.js";
91
92
  export * from "./evaluation.js";
92
93
  /** Idempotency store reference adapter. */
93
94
  export * from "./idempotency.js";
@@ -104,4 +105,6 @@ export * from "./principal-memory.js";
104
105
  /** Message + agent reference adapters (used by the embedded agent facade). */
105
106
  export * from "./message-store.js";
106
107
  export * from "./flows.js";
108
+ export * from "./rate-limit.js";
109
+ export * from "./connections.js";
107
110
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,14 @@
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
+ import type { RateLimitStore } from "../../usage/index.js";
13
+ export declare const createMemoryRateLimitStore: () => RateLimitStore;
14
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -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