@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,171 @@
1
+ /**
2
+ * `ConnectionStore` conformance — REQ-063 (#259), task #261.
3
+ *
4
+ * Every clause here is a defect somebody would otherwise ship, and two of them are the reason the port exists
5
+ * at all: a store that leaked across tenants would hand one customer another's credential, and a store that
6
+ * kept a revoked connection readable would let a withdrawn credential be used once more.
7
+ *
8
+ * The store never sees plaintext, so this suite never supplies any: it seals nothing and asserts nothing about
9
+ * encryption. That belongs to `SecretCipher`'s own tests and to the raw-row assertion, which needs a real
10
+ * database and lives with the adapter.
11
+ */
12
+ import { describe, expect, it } from "vitest";
13
+ import { asId } from "../../core/ids.js";
14
+ const sealed = (marker) => ({
15
+ keyId: "k1",
16
+ algorithm: "aes-256-gcm",
17
+ nonce: Buffer.alloc(12, 1).toString("base64"),
18
+ ciphertext: Buffer.from(`sealed-${marker}`).toString("base64"),
19
+ });
20
+ const connection = (over) => ({
21
+ provider: "github",
22
+ mode: "token",
23
+ scheme: "bearer",
24
+ sealed: sealed(over.id),
25
+ ...over,
26
+ });
27
+ export function connectionStoreConformance(makeStore) {
28
+ describe("ConnectionStore conformance", () => {
29
+ const T1 = asId("conn-t1");
30
+ const T2 = asId("conn-t2");
31
+ it("creates and reads back, scoped to its tenant", async () => {
32
+ const store = await makeStore();
33
+ await store.create({ tenantId: T1, connection: connection({ id: "c1", label: "Acme org" }) });
34
+ const read = await store.get({ tenantId: T1, id: "c1" });
35
+ expect(read).toMatchObject({ id: "c1", provider: "github", label: "Acme org", scheme: "bearer" });
36
+ expect(read?.sealed.ciphertext).toBe(sealed("c1").ciphertext);
37
+ expect(read?.createdAt).toBeTruthy();
38
+ });
39
+ it("returns null for another tenant's connection", async () => {
40
+ // Indistinguishable from absent, so the store cannot be used to probe which ids exist — and, far worse,
41
+ // cannot hand T2 a credential belonging to T1.
42
+ const store = await makeStore();
43
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
44
+ expect(await store.get({ tenantId: T2, id: "c1" })).toBeNull();
45
+ });
46
+ it("lists only this tenant's live connections", async () => {
47
+ const store = await makeStore();
48
+ await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
49
+ await store.create({ tenantId: T2, connection: connection({ id: "b" }) });
50
+ expect((await store.list({ tenantId: T1 })).map((c) => c.id)).toEqual(["a"]);
51
+ });
52
+ it("allows several connections to one provider for one tenant", async () => {
53
+ // A customer with three GitHub organisations is the normal case, not an edge one.
54
+ const store = await makeStore();
55
+ await store.create({ tenantId: T1, connection: connection({ id: "org-a", label: "A" }) });
56
+ await store.create({ tenantId: T1, connection: connection({ id: "org-b", label: "B" }) });
57
+ expect((await store.list({ tenantId: T1, provider: "github" })).map((c) => c.id)).toEqual(["org-a", "org-b"]);
58
+ });
59
+ it("narrows a listing by provider", async () => {
60
+ const store = await makeStore();
61
+ await store.create({ tenantId: T1, connection: connection({ id: "gh" }) });
62
+ await store.create({ tenantId: T1, connection: connection({ id: "sl", provider: "slack" }) });
63
+ expect((await store.list({ tenantId: T1, provider: "slack" })).map((c) => c.id)).toEqual(["sl"]);
64
+ });
65
+ it("orders a listing stably, so a default connection does not move", async () => {
66
+ // The resolver's default is "the oldest", which is only stable if the order is. Newest-first would
67
+ // silently re-point every agent the moment somebody connects a second account.
68
+ const store = await makeStore();
69
+ for (const id of ["first", "second", "third"]) {
70
+ await store.create({ tenantId: T1, connection: connection({ id }) });
71
+ }
72
+ const once = (await store.list({ tenantId: T1 })).map((c) => c.id);
73
+ const twice = (await store.list({ tenantId: T1 })).map((c) => c.id);
74
+ expect(once).toEqual(twice);
75
+ expect(once[0]).toBe("first");
76
+ });
77
+ it("refuses to create the same id twice rather than overwriting", async () => {
78
+ // Silently overwriting is the worst resolution of a duplicate: it replaces a working credential with
79
+ // whatever the second caller had.
80
+ const store = await makeStore();
81
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
82
+ await expect(store.create({ tenantId: T1, connection: connection({ id: "c1" }) })).rejects.toThrow();
83
+ });
84
+ it("updates the secret, which is what a refresh does", async () => {
85
+ const store = await makeStore();
86
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
87
+ const updated = await store.update({ tenantId: T1, id: "c1", patch: { sealed: sealed("rotated") } });
88
+ expect(updated.sealed.ciphertext).toBe(sealed("rotated").ciphertext);
89
+ expect((await store.get({ tenantId: T1, id: "c1" }))?.sealed.ciphertext).toBe(sealed("rotated").ciphertext);
90
+ });
91
+ it("leaves unpatched fields alone", async () => {
92
+ // An absent field means "leave it", not "set it to null" — otherwise a label-only rename would wipe the
93
+ // credential.
94
+ const store = await makeStore();
95
+ await store.create({
96
+ tenantId: T1,
97
+ connection: connection({ id: "c1", label: "before", metadata: { cloudId: "x" } }),
98
+ });
99
+ const updated = await store.update({ tenantId: T1, id: "c1", patch: { label: "after" } });
100
+ expect(updated.label).toBe("after");
101
+ expect(updated.metadata?.cloudId).toBe("x");
102
+ expect(updated.sealed.ciphertext).toBe(sealed("c1").ciphertext);
103
+ });
104
+ it("refuses to update another tenant's connection", async () => {
105
+ const store = await makeStore();
106
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
107
+ await expect(store.update({ tenantId: T2, id: "c1", patch: { label: "mine now" } })).rejects.toThrow();
108
+ });
109
+ it("makes a revoked connection unreadable, while keeping the row", async () => {
110
+ // The audit trail survives — "who connected this and when was it removed" is a question a security review
111
+ // asks — and the credential does not resolve.
112
+ const store = await makeStore();
113
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
114
+ await store.revoke({ tenantId: T1, id: "c1" });
115
+ expect(await store.get({ tenantId: T1, id: "c1" })).toBeNull();
116
+ expect(await store.list({ tenantId: T1 })).toEqual([]);
117
+ });
118
+ it("treats revoking twice as success", async () => {
119
+ // A retried disconnect should not have to distinguish "already gone" from "failed".
120
+ const store = await makeStore();
121
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
122
+ await store.revoke({ tenantId: T1, id: "c1" });
123
+ await expect(store.revoke({ tenantId: T1, id: "c1" })).resolves.toBeUndefined();
124
+ });
125
+ it("refuses to revoke another tenant's connection", async () => {
126
+ const store = await makeStore();
127
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
128
+ await expect(store.revoke({ tenantId: T2, id: "c1" })).rejects.toThrow();
129
+ });
130
+ it("refuses to update a revoked connection", async () => {
131
+ const store = await makeStore();
132
+ await store.create({ tenantId: T1, connection: connection({ id: "c1" }) });
133
+ await store.revoke({ tenantId: T1, id: "c1" });
134
+ await expect(store.update({ tenantId: T1, id: "c1", patch: { label: "back" } })).rejects.toThrow();
135
+ });
136
+ it("purges one tenant's connections and only that tenant's", async () => {
137
+ // `docs/18`. A soft-deleted credential is still a credential, so deletion has to be real — and it has to
138
+ // stop at the tenant boundary, or a retention job for one customer removes another's access.
139
+ const store = await makeStore();
140
+ await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
141
+ await store.create({ tenantId: T1, connection: connection({ id: "b" }) });
142
+ await store.create({ tenantId: T2, connection: connection({ id: "c" }) });
143
+ expect(await store.purge({ tenantId: T1 })).toBe(2);
144
+ expect(await store.list({ tenantId: T1 })).toEqual([]);
145
+ expect((await store.list({ tenantId: T2 })).map((x) => x.id)).toEqual(["c"]);
146
+ });
147
+ it("purges revoked connections too, since a retention promise covers them", async () => {
148
+ const store = await makeStore();
149
+ await store.create({ tenantId: T1, connection: connection({ id: "a" }) });
150
+ await store.revoke({ tenantId: T1, id: "a" });
151
+ expect(await store.purge({ tenantId: T1 })).toBe(1);
152
+ });
153
+ it("round-trips metadata and granted scopes without reshaping them", async () => {
154
+ // Both are read on the request path — the cloud id goes into the URL, the scopes decide whether to tell
155
+ // somebody to reconnect — so a store that dropped or reordered them would break the call, not the listing.
156
+ const store = await makeStore();
157
+ await store.create({
158
+ tenantId: T1,
159
+ connection: connection({
160
+ id: "c1",
161
+ metadata: { cloudId: "abc", username: "me@example.com" },
162
+ grantedScopes: ["read:issues", "write:issues"],
163
+ }),
164
+ });
165
+ const read = await store.get({ tenantId: T1, id: "c1" });
166
+ expect(read?.metadata).toEqual({ cloudId: "abc", username: "me@example.com" });
167
+ expect(read?.grantedScopes).toEqual(["read:issues", "write:issues"]);
168
+ });
169
+ });
170
+ }
171
+ //# sourceMappingURL=connections.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared storage conformance harness — `docs/02` "Conformance suite".
3
+ *
4
+ * Every `ConversationStore` adapter (memory today, Postgres/Supabase later) must pass the same
5
+ * suite, so "swap the database" is provably safe. Adapter packages call this from a test file.
6
+ * Not part of the published build (excluded in tsconfig); imported by tests only.
7
+ */
8
+ import type { ConversationStore } from "../../persistence/index.js";
9
+ export declare function conversationStoreConformance(makeStore: () => ConversationStore): void;
10
+ //# sourceMappingURL=conversation-store.d.ts.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared storage conformance harness — `docs/02` "Conformance suite".
3
+ *
4
+ * Every `ConversationStore` adapter (memory today, Postgres/Supabase later) must pass the same
5
+ * suite, so "swap the database" is provably safe. Adapter packages call this from a test file.
6
+ * Not part of the published build (excluded in tsconfig); imported by tests only.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ export function conversationStoreConformance(makeStore) {
10
+ const t1 = "tenant-1";
11
+ const t2 = "tenant-2";
12
+ const cid = (s) => s;
13
+ describe("ConversationStore conformance", () => {
14
+ it("create then findById returns the row, scoped to its tenant", async () => {
15
+ const store = makeStore();
16
+ const created = await store.create({ tenantId: t1, id: cid("c1"), title: "Hello" });
17
+ expect(created).toMatchObject({ id: "c1", tenantId: t1, title: "Hello", version: 1 });
18
+ expect(await store.findById({ tenantId: t1, id: cid("c1") })).toMatchObject({ id: "c1" });
19
+ });
20
+ it("enforces tenant isolation", async () => {
21
+ const store = makeStore();
22
+ await store.create({ tenantId: t1, id: cid("c1"), title: "t1 only" });
23
+ expect(await store.findById({ tenantId: t2, id: cid("c1") })).toBeNull();
24
+ expect((await store.list({ tenantId: t2, limit: 10 })).items).toHaveLength(0);
25
+ });
26
+ it("rejects a duplicate id", async () => {
27
+ const store = makeStore();
28
+ await store.create({ tenantId: t1, id: cid("c1"), title: "a" });
29
+ await expect(store.create({ tenantId: t1, id: cid("c1"), title: "b" })).rejects.toThrow();
30
+ });
31
+ it("paginates by stable cursor", async () => {
32
+ const store = makeStore();
33
+ for (const n of [1, 2, 3, 4, 5])
34
+ await store.create({ tenantId: t1, id: cid(`c${n}`), title: `#${n}` });
35
+ const first = await store.list({ tenantId: t1, limit: 2 });
36
+ expect(first.items).toHaveLength(2);
37
+ expect(first.nextCursor).toBeDefined();
38
+ const second = await store.list({ tenantId: t1, limit: 2, cursor: first.nextCursor });
39
+ expect(second.items).toHaveLength(2);
40
+ // no overlap between pages
41
+ const ids = new Set(first.items.map((c) => c.id));
42
+ expect(second.items.some((c) => ids.has(c.id))).toBe(false);
43
+ });
44
+ it("optimistic concurrency: a stale expectedVersion is rejected", async () => {
45
+ const store = makeStore();
46
+ await store.create({ tenantId: t1, id: cid("c1"), title: "v1" });
47
+ const updated = await store.update({ tenantId: t1, id: cid("c1"), expectedVersion: 1, patch: { title: "v2" } });
48
+ expect(updated.version).toBe(2);
49
+ await expect(store.update({ tenantId: t1, id: cid("c1"), expectedVersion: 1, patch: { title: "stale" } })).rejects.toThrow();
50
+ });
51
+ it("soft delete hides the row from findById and list", async () => {
52
+ const store = makeStore();
53
+ await store.create({ tenantId: t1, id: cid("c1"), title: "bye" });
54
+ await store.softDelete({ tenantId: t1, id: cid("c1") });
55
+ expect(await store.findById({ tenantId: t1, id: cid("c1") })).toBeNull();
56
+ expect((await store.list({ tenantId: t1, limit: 10 })).items).toHaveLength(0);
57
+ });
58
+ });
59
+ }
60
+ //# sourceMappingURL=conversation-store.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `EvaluationStore` conformance (#141).
3
+ *
4
+ * The cases are mostly about *aggregates agreeing with their evidence* and about *idempotency*, because those
5
+ * are what a release gate depends on: a mean that disagrees with its case rows is a number nobody can defend,
6
+ * and a double-counted case makes the gate pass or fail for the wrong reason.
7
+ */
8
+ import type { EvaluationStore } from "../../persistence/index.js";
9
+ export declare function evaluationStoreConformance(make: () => EvaluationStore | Promise<EvaluationStore>): void;
10
+ //# sourceMappingURL=evaluation.d.ts.map
@@ -0,0 +1,295 @@
1
+ /**
2
+ * `EvaluationStore` conformance (#141).
3
+ *
4
+ * The cases are mostly about *aggregates agreeing with their evidence* and about *idempotency*, because those
5
+ * are what a release gate depends on: a mean that disagrees with its case rows is a number nobody can defend,
6
+ * and a double-counted case makes the gate pass or fail for the wrong reason.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ import { asId } from "../../core/ids.js";
10
+ const T1 = asId("conf-eval-tenant-1");
11
+ const T2 = asId("conf-eval-tenant-2");
12
+ const STARTED = "2026-08-23T10:00:00.000Z";
13
+ const FINISHED = "2026-08-23T10:05:00.000Z";
14
+ const result = (overrides) => ({
15
+ caseId: overrides.id,
16
+ dimension: overrides.dimension ?? "task-completion",
17
+ expectKind: overrides.expectKind ?? "contains",
18
+ verdict: overrides.verdict ?? { pass: true, score: 1, reason: "ok" },
19
+ graderId: overrides.graderId ?? "contains",
20
+ graderVersion: overrides.graderVersion ?? "1",
21
+ ...(overrides.modelId === undefined ? {} : { modelId: overrides.modelId }),
22
+ ...(overrides.promptVersion === undefined ? {} : { promptVersion: overrides.promptVersion }),
23
+ costMinorUnits: overrides.costMinorUnits ?? 0,
24
+ });
25
+ export function evaluationStoreConformance(make) {
26
+ describe("EvaluationStore conformance", () => {
27
+ const started = async (id = "run-1", release = "v1") => {
28
+ const store = await make();
29
+ await store.startRun({ tenantId: T1, id, release, startedAt: STARTED });
30
+ return store;
31
+ };
32
+ it("opens a run with zeroed totals", async () => {
33
+ const store = await make();
34
+ const run = await store.startRun({ tenantId: T1, id: "run-1", release: "v1", startedAt: STARTED });
35
+ expect(run).toMatchObject({ id: "run-1", release: "v1", total: 0, passed: 0, meanScore: 0 });
36
+ // Unfinished, and visibly so: an interrupted run must be distinguishable from an absent one.
37
+ expect(run.finishedAt).toBeUndefined();
38
+ });
39
+ it("refuses to open the same run twice", async () => {
40
+ // Silently reopening a completed run would discard the numbers a release was gated on.
41
+ const store = await started();
42
+ await expect(store.startRun({ tenantId: T1, id: "run-1", release: "v1", startedAt: STARTED })).rejects.toMatchObject({ code: "conflict" });
43
+ });
44
+ it("computes aggregates from the recorded cases", async () => {
45
+ const store = await started();
46
+ for (const r of [
47
+ result({ id: "a", verdict: { pass: true, score: 1, reason: "ok" } }),
48
+ result({ id: "b", verdict: { pass: false, score: 0, reason: "no" } }),
49
+ result({ id: "c", verdict: { pass: true, score: 0.5, reason: "partial" } }),
50
+ ]) {
51
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: r });
52
+ }
53
+ const run = await store.completeRun({
54
+ tenantId: T1,
55
+ runId: "run-1",
56
+ finishedAt: FINISHED,
57
+ graderVersions: { contains: "1" },
58
+ });
59
+ expect(run).toMatchObject({ total: 3, passed: 2, finishedAt: FINISHED });
60
+ expect(run.meanScore).toBeCloseTo(0.5, 5);
61
+ });
62
+ it("breaks the run down by dimension", async () => {
63
+ // AC-4. An aggregate without a breakdown cannot tell "authorization got worse" from "everything got
64
+ // slightly worse", and those need different responses.
65
+ const store = await started();
66
+ for (const r of [
67
+ result({ id: "a", dimension: "authorization", verdict: { pass: true, score: 1, reason: "ok" } }),
68
+ result({ id: "b", dimension: "authorization", verdict: { pass: false, score: 0, reason: "no" } }),
69
+ result({ id: "c", dimension: "groundedness", verdict: { pass: true, score: 1, reason: "ok" } }),
70
+ ]) {
71
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: r });
72
+ }
73
+ const run = await store.completeRun({
74
+ tenantId: T1,
75
+ runId: "run-1",
76
+ finishedAt: FINISHED,
77
+ graderVersions: {},
78
+ });
79
+ expect(run.byDimension).toEqual([
80
+ { dimension: "authorization", total: 2, passed: 1, meanScore: 0.5 },
81
+ { dimension: "groundedness", total: 1, passed: 1, meanScore: 1 },
82
+ ]);
83
+ });
84
+ it("totals the run's cost", async () => {
85
+ // AC-6: the gate's own expense. A gate whose cost is unknown is one nobody can decide to run less often.
86
+ const store = await started();
87
+ for (const r of [
88
+ result({ id: "a", costMinorUnits: 0 }),
89
+ result({ id: "b", costMinorUnits: 7, modelId: "judge-1", promptVersion: "1" }),
90
+ result({ id: "c", costMinorUnits: 11, modelId: "judge-1", promptVersion: "1" }),
91
+ ]) {
92
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: r });
93
+ }
94
+ const run = await store.completeRun({
95
+ tenantId: T1,
96
+ runId: "run-1",
97
+ finishedAt: FINISHED,
98
+ graderVersions: {},
99
+ });
100
+ expect(run.costMinorUnits).toBe(18);
101
+ });
102
+ it("does not double count a case recorded twice", async () => {
103
+ // A resumed run re-recording a case must replace it. Double-counting makes the gate pass or fail for the
104
+ // wrong reason, and the aggregate looks plausible either way.
105
+ const store = await started();
106
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: result({ id: "a", costMinorUnits: 5 }) });
107
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: result({ id: "a", costMinorUnits: 5 }) });
108
+ const run = await store.completeRun({
109
+ tenantId: T1,
110
+ runId: "run-1",
111
+ finishedAt: FINISHED,
112
+ graderVersions: {},
113
+ });
114
+ expect(run.total).toBe(1);
115
+ expect(run.costMinorUnits).toBe(5);
116
+ });
117
+ it("lets a re-record correct a verdict", async () => {
118
+ // The other half of idempotency: replacing, not ignoring. A re-scored case must take its new verdict, or
119
+ // a fixed grader could never update a run.
120
+ const store = await started();
121
+ await store.recordCase({
122
+ tenantId: T1,
123
+ runId: "run-1",
124
+ result: result({ id: "a", verdict: { pass: false, score: 0, reason: "first" } }),
125
+ });
126
+ await store.recordCase({
127
+ tenantId: T1,
128
+ runId: "run-1",
129
+ result: result({ id: "a", verdict: { pass: true, score: 1, reason: "second" } }),
130
+ });
131
+ const page = await store.listCaseResults({ tenantId: T1, runId: "run-1", limit: 10 });
132
+ expect(page.items).toHaveLength(1);
133
+ expect(page.items[0]?.verdict).toMatchObject({ pass: true, reason: "second" });
134
+ });
135
+ it("reports rather than throws when the run is gone", async () => {
136
+ // A harness losing that race is ordinary; a thrown error would abandon a whole scoring pass.
137
+ const store = await make();
138
+ expect(await store.recordCase({ tenantId: T1, runId: "missing", result: result({ id: "a" }) })).toEqual({ recorded: false });
139
+ });
140
+ it("stores the grader and prompt versions on a judged result", async () => {
141
+ // AC-2 and AC-3 both depend on it: a score that moved after a prompt edit is not a quality change, and
142
+ // without the version on the result the two are indistinguishable.
143
+ const store = await started();
144
+ await store.recordCase({
145
+ tenantId: T1,
146
+ runId: "run-1",
147
+ result: result({ id: "a", graderId: "refuses-judged", graderVersion: "1+p2", modelId: "judge-1", promptVersion: "2" }),
148
+ });
149
+ const page = await store.listCaseResults({ tenantId: T1, runId: "run-1", limit: 10 });
150
+ expect(page.items[0]).toMatchObject({
151
+ graderId: "refuses-judged",
152
+ graderVersion: "1+p2",
153
+ modelId: "judge-1",
154
+ promptVersion: "2",
155
+ });
156
+ });
157
+ it("records the grader versions on the run", async () => {
158
+ const store = await started();
159
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: result({ id: "a" }) });
160
+ const run = await store.completeRun({
161
+ tenantId: T1,
162
+ runId: "run-1",
163
+ finishedAt: FINISHED,
164
+ graderVersions: { contains: "1", "refuses-judged": "1+p2" },
165
+ });
166
+ expect(run.graderVersions).toEqual({ contains: "1", "refuses-judged": "1+p2" });
167
+ });
168
+ it("breaks a tie by start then id, so the latest run is never arbitrary", async () => {
169
+ // Two runs *can* finish in the same instant — a fixed clock, or simply a fast pair. An unstable answer
170
+ // here is a gate comparing against an arbitrary one of two runs, and it would be intermittent.
171
+ const store = await make();
172
+ for (const [id, startedAt] of [
173
+ ["a", "2026-08-23T09:00:00.000Z"],
174
+ ["b", "2026-08-23T09:30:00.000Z"],
175
+ ]) {
176
+ await store.startRun({ tenantId: T1, id, release: "v1", startedAt });
177
+ await store.completeRun({ tenantId: T1, runId: id, finishedAt: FINISHED, graderVersions: {} });
178
+ }
179
+ // Same `finishedAt`; the later start wins.
180
+ expect((await store.latest({ tenantId: T1 }))?.id).toBe("b");
181
+ });
182
+ it("returns the latest completed run, never an in-flight one", async () => {
183
+ // An unfinished run's totals are partial, and comparing against one reports every case it has not reached
184
+ // yet as a regression.
185
+ const store = await started("done", "v1");
186
+ await store.recordCase({ tenantId: T1, runId: "done", result: result({ id: "a" }) });
187
+ await store.completeRun({ tenantId: T1, runId: "done", finishedAt: FINISHED, graderVersions: {} });
188
+ await store.startRun({ tenantId: T1, id: "running", release: "v2", startedAt: "2026-08-23T11:00:00.000Z" });
189
+ expect((await store.latest({ tenantId: T1 }))?.id).toBe("done");
190
+ });
191
+ it("returns null when the only run is still in flight", async () => {
192
+ // The discriminating case. With a completed run also present, an unfiltered `latest` can still return the
193
+ // right answer by accident — an undefined `finishedAt` sorts last — so the filter is only really tested
194
+ // when there is nothing else to fall back to. Found by sabotage.
195
+ const store = await started("running", "v1");
196
+ expect(await store.latest({ tenantId: T1 })).toBeNull();
197
+ expect(await store.latest({ tenantId: T1, release: "v1" })).toBeNull();
198
+ });
199
+ it("finds the latest run for one release", async () => {
200
+ const store = await started("v1-run", "v1");
201
+ await store.completeRun({ tenantId: T1, runId: "v1-run", finishedAt: FINISHED, graderVersions: {} });
202
+ await store.startRun({ tenantId: T1, id: "v2-run", release: "v2", startedAt: "2026-08-23T11:00:00.000Z" });
203
+ await store.completeRun({
204
+ tenantId: T1,
205
+ runId: "v2-run",
206
+ finishedAt: "2026-08-23T11:05:00.000Z",
207
+ graderVersions: {},
208
+ });
209
+ expect((await store.latest({ tenantId: T1, release: "v1" }))?.id).toBe("v1-run");
210
+ expect((await store.latest({ tenantId: T1 }))?.id).toBe("v2-run");
211
+ });
212
+ it("returns null when a release has never been scored", async () => {
213
+ const store = await started();
214
+ expect(await store.latest({ tenantId: T1, release: "never" })).toBeNull();
215
+ });
216
+ it("does not resolve another tenant's run", async () => {
217
+ // One tenant's quality gate is not another's business.
218
+ const store = await started();
219
+ expect(await store.get({ tenantId: T2, runId: "run-1" })).toBeNull();
220
+ expect(await store.latest({ tenantId: T2 })).toBeNull();
221
+ expect((await store.listCaseResults({ tenantId: T2, runId: "run-1", limit: 10 })).items).toEqual([]);
222
+ });
223
+ it("does not record into another tenant's run", async () => {
224
+ const store = await started();
225
+ expect(await store.recordCase({ tenantId: T2, runId: "run-1", result: result({ id: "a" }) })).toEqual({ recorded: false });
226
+ expect((await store.listCaseResults({ tenantId: T1, runId: "run-1", limit: 10 })).items).toEqual([]);
227
+ });
228
+ it("lists runs newest first", async () => {
229
+ const store = await make();
230
+ for (const [id, at] of [
231
+ ["old", "2026-08-21T10:00:00.000Z"],
232
+ ["new", "2026-08-23T10:00:00.000Z"],
233
+ ["mid", "2026-08-22T10:00:00.000Z"],
234
+ ]) {
235
+ await store.startRun({ tenantId: T1, id, release: id, startedAt: at });
236
+ }
237
+ const page = await store.list({ tenantId: T1, limit: 10 });
238
+ expect(page.items.map((r) => r.id)).toEqual(["new", "mid", "old"]);
239
+ });
240
+ it("pages case results without repeating or skipping one", async () => {
241
+ const store = await started();
242
+ for (const n of [1, 2, 3, 4, 5]) {
243
+ await store.recordCase({ tenantId: T1, runId: "run-1", result: result({ id: `case-${n}` }) });
244
+ }
245
+ const seen = [];
246
+ let cursor;
247
+ do {
248
+ const page = await store.listCaseResults({
249
+ tenantId: T1,
250
+ runId: "run-1",
251
+ limit: 2,
252
+ ...(cursor === undefined ? {} : { cursor }),
253
+ });
254
+ seen.push(...page.items.map((c) => c.caseId));
255
+ cursor = page.nextCursor;
256
+ } while (cursor !== undefined);
257
+ expect(seen).toEqual(["case-1", "case-2", "case-3", "case-4", "case-5"]);
258
+ });
259
+ it("completes an empty run as zero rather than refusing", async () => {
260
+ // A run that scored nothing has a mean of zero. NaN would propagate into every comparison that touched it.
261
+ const store = await started();
262
+ const run = await store.completeRun({
263
+ tenantId: T1,
264
+ runId: "run-1",
265
+ finishedAt: FINISHED,
266
+ graderVersions: {},
267
+ });
268
+ expect(run).toMatchObject({ total: 0, passed: 0, meanScore: 0, byDimension: [] });
269
+ });
270
+ it("recomputes aggregates when completed again", async () => {
271
+ // A re-completion after a corrected case must reflect it. Accumulated totals would keep the old number,
272
+ // and the number that gates a release must be derivable from its evidence.
273
+ const store = await started();
274
+ await store.recordCase({
275
+ tenantId: T1,
276
+ runId: "run-1",
277
+ result: result({ id: "a", verdict: { pass: false, score: 0, reason: "first" } }),
278
+ });
279
+ await store.completeRun({ tenantId: T1, runId: "run-1", finishedAt: FINISHED, graderVersions: {} });
280
+ await store.recordCase({
281
+ tenantId: T1,
282
+ runId: "run-1",
283
+ result: result({ id: "a", verdict: { pass: true, score: 1, reason: "fixed" } }),
284
+ });
285
+ const run = await store.completeRun({
286
+ tenantId: T1,
287
+ runId: "run-1",
288
+ finishedAt: "2026-08-23T10:10:00.000Z",
289
+ graderVersions: {},
290
+ });
291
+ expect(run).toMatchObject({ total: 1, passed: 1, meanScore: 1 });
292
+ });
293
+ });
294
+ }
295
+ //# sourceMappingURL=evaluation.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `FileMetadataStore` and `FileContentStore` conformance (#129).
3
+ *
4
+ * Both carry safety properties rather than only a data contract, and both are asserted here rather than
5
+ * assumed:
6
+ *
7
+ * - **A `contentKey` from one tenant must not resolve another's bytes.** The key is opaque and a caller
8
+ * cannot construct one — but an adapter that took the key as sufficient would leak across tenants the
9
+ * moment a key was guessed, logged or copied. #91 found an `AgentStore` that accepted `TenantScope` and
10
+ * ignored it, which is why every adapter is asked the question directly.
11
+ * - **`transition` is a compare-and-set.** A blind write lets a conversation delete racing an upload leave a
12
+ * file `stored` after its bytes were scheduled for removal — bytes that then never get swept, because
13
+ * nothing is looking for a `stored` file's object.
14
+ */
15
+ import type { FileContentStore, FileMetadataStore } from "../../persistence/index.js";
16
+ import { type FixtureOrStore } from "./parents.js";
17
+ export declare function fileMetadataStoreConformance(makeFixture: () => FixtureOrStore<FileMetadataStore>): void;
18
+ export declare function fileContentStoreConformance(make: () => FileContentStore): void;
19
+ //# sourceMappingURL=files.d.ts.map