@cursor/july 0.1.5 → 0.1.7

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 (177) hide show
  1. package/dist/ab.d.ts +8 -95
  2. package/dist/ab.d.ts.map +1 -1
  3. package/dist/ab.js +9 -150
  4. package/dist/bin/agent-serve.js +41 -8
  5. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  6. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  7. package/dist/docs/404.html +2 -2
  8. package/dist/docs/ab.html +4 -4
  9. package/dist/docs/assets/{app.DabPG-io.js → app.COTN7wgo.js} +1 -1
  10. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +1 -0
  11. package/dist/docs/assets/chunks/{VPLocalSearchBox.jmyr0bU0.js → VPLocalSearchBox.BW3TBdT0.js} +1 -1
  12. package/dist/docs/assets/chunks/{theme.DysN9-VN.js → theme.BEJW0vE7.js} +2 -2
  13. package/dist/docs/assets/deployment.md.BtfEsc9S.js +55 -0
  14. package/dist/docs/assets/deployment.md.BtfEsc9S.lean.js +1 -0
  15. package/dist/docs/assets/example-agents_approval-buddy.md.8R5phXb5.js +10 -0
  16. package/dist/docs/assets/example-agents_approval-buddy.md.8R5phXb5.lean.js +1 -0
  17. package/dist/docs/assets/example-agents_benny.md.B0gjhI-p.js +7 -0
  18. package/dist/docs/assets/example-agents_benny.md.B0gjhI-p.lean.js +1 -0
  19. package/dist/docs/assets/example-agents_bugbot.md.DelIdhxB.js +11 -0
  20. package/dist/docs/assets/example-agents_bugbot.md.DelIdhxB.lean.js +1 -0
  21. package/dist/docs/assets/example-agents_codebase-wiki.md.DC6sgwn0.js +8 -0
  22. package/dist/docs/assets/example-agents_codebase-wiki.md.DC6sgwn0.lean.js +1 -0
  23. package/dist/docs/assets/example-agents_codeowners-review.md.Ku_tG2RY.js +8 -0
  24. package/dist/docs/assets/example-agents_codeowners-review.md.Ku_tG2RY.lean.js +1 -0
  25. package/dist/docs/assets/example-agents_concierge.md.4rQTSMXt.js +23 -0
  26. package/dist/docs/assets/example-agents_concierge.md.4rQTSMXt.lean.js +1 -0
  27. package/dist/docs/assets/example-agents_fsd.md.CzgUrDfi.js +15 -0
  28. package/dist/docs/assets/example-agents_fsd.md.CzgUrDfi.lean.js +1 -0
  29. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +2 -0
  30. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.lean.js +1 -0
  31. package/dist/docs/assets/example-agents_knowledge-base.md.BPJiVueF.js +11 -0
  32. package/dist/docs/assets/example-agents_knowledge-base.md.BPJiVueF.lean.js +1 -0
  33. package/dist/docs/assets/example-agents_security-reviewer.md.D2rtwDTO.js +19 -0
  34. package/dist/docs/assets/example-agents_security-reviewer.md.D2rtwDTO.lean.js +1 -0
  35. package/dist/docs/assets/example-agents_slack-agent.md.buLbgvBf.js +5 -0
  36. package/dist/docs/assets/example-agents_slack-agent.md.buLbgvBf.lean.js +1 -0
  37. package/dist/docs/assets/example-agents_weather-agent.md.C9Qv-W0o.js +24 -0
  38. package/dist/docs/assets/example-agents_weather-agent.md.C9Qv-W0o.lean.js +1 -0
  39. package/dist/docs/assets/index.md.COiu-1jL.js +20 -0
  40. package/dist/docs/assets/{index.md.Cylk70gg.lean.js → index.md.COiu-1jL.lean.js} +1 -1
  41. package/dist/docs/assets/reference_cli.md.D189RBCH.js +60 -0
  42. package/dist/docs/assets/reference_cli.md.D189RBCH.lean.js +1 -0
  43. package/dist/docs/building-with-agents.html +4 -4
  44. package/dist/docs/concepts.html +4 -4
  45. package/dist/docs/deployment.html +58 -17
  46. package/dist/docs/evals.html +4 -4
  47. package/dist/docs/example-agents/approval-buddy.html +34 -0
  48. package/dist/docs/example-agents/benny.html +31 -0
  49. package/dist/docs/example-agents/bugbot.html +35 -0
  50. package/dist/docs/example-agents/codebase-wiki.html +32 -0
  51. package/dist/docs/example-agents/codeowners-review.html +32 -0
  52. package/dist/docs/example-agents/concierge.html +47 -0
  53. package/dist/docs/example-agents/fsd.html +39 -0
  54. package/dist/docs/example-agents/index.html +26 -0
  55. package/dist/docs/example-agents/knowledge-base.html +35 -0
  56. package/dist/docs/example-agents/security-reviewer.html +43 -0
  57. package/dist/docs/example-agents/slack-agent.html +29 -0
  58. package/dist/docs/example-agents/weather-agent.html +48 -0
  59. package/dist/docs/guides/agent-to-agent.html +4 -4
  60. package/dist/docs/guides/cloud-runtime.html +5 -5
  61. package/dist/docs/guides/github.html +4 -4
  62. package/dist/docs/guides/human-in-the-loop.html +4 -4
  63. package/dist/docs/guides/slack.html +4 -4
  64. package/dist/docs/guides/webhooks.html +4 -4
  65. package/dist/docs/hashmap.json +1 -1
  66. package/dist/docs/hillclimbing.html +4 -4
  67. package/dist/docs/index.html +7 -7
  68. package/dist/docs/quickstart.html +4 -4
  69. package/dist/docs/reference/agent-config.html +4 -4
  70. package/dist/docs/reference/channels.html +4 -4
  71. package/dist/docs/reference/cli.html +52 -30
  72. package/dist/docs/reference/connections.html +4 -4
  73. package/dist/docs/reference/hooks.html +4 -4
  74. package/dist/docs/reference/http-api.html +4 -4
  75. package/dist/docs/reference/instructions.html +4 -4
  76. package/dist/docs/reference/playground.html +4 -4
  77. package/dist/docs/reference/project-layout.html +4 -4
  78. package/dist/docs/reference/schedules.html +4 -4
  79. package/dist/docs/reference/sessions.html +4 -4
  80. package/dist/docs/reference/skills.html +4 -4
  81. package/dist/docs/reference/subagents.html +4 -4
  82. package/dist/docs/reference/tools.html +4 -4
  83. package/dist/docs/scaffolding-agents.html +4 -4
  84. package/dist/docs/storage.html +4 -4
  85. package/dist/docs/troubleshooting.html +4 -4
  86. package/dist/evals.d.ts +5 -62
  87. package/dist/evals.d.ts.map +1 -1
  88. package/dist/evals.js +3 -66
  89. package/dist/index.d.ts +1 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/internal/ab-collector.d.ts +7 -5
  92. package/dist/internal/ab-collector.d.ts.map +1 -1
  93. package/dist/internal/ab-collector.js +3 -14
  94. package/dist/internal/ab-snapshot.d.ts +2 -4
  95. package/dist/internal/ab-snapshot.d.ts.map +1 -1
  96. package/dist/internal/cli-ax.d.ts +33 -5
  97. package/dist/internal/cli-ax.d.ts.map +1 -1
  98. package/dist/internal/cli-ax.js +428 -87
  99. package/dist/internal/cli-deploy.js +1 -1
  100. package/dist/internal/discovery.js +3 -3
  101. package/dist/internal/eval-run-store.d.ts +35 -30
  102. package/dist/internal/eval-run-store.d.ts.map +1 -1
  103. package/dist/internal/eval-run-store.js +88 -100
  104. package/dist/internal/evals-client.d.ts +96 -0
  105. package/dist/internal/evals-client.d.ts.map +1 -0
  106. package/dist/internal/evals-client.js +262 -0
  107. package/dist/internal/init-project.d.ts.map +1 -1
  108. package/dist/internal/init-project.js +1 -0
  109. package/dist/internal/persistence-coordinator.d.ts +127 -0
  110. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  111. package/dist/internal/playground-proxy.d.ts +5 -5
  112. package/dist/internal/playground-proxy.js +3 -3
  113. package/dist/internal/resolve-prod-target.d.ts +30 -0
  114. package/dist/internal/resolve-prod-target.d.ts.map +1 -1
  115. package/dist/internal/resolve-prod-target.js +74 -2
  116. package/dist/internal/server.d.ts.map +1 -1
  117. package/dist/internal/server.js +16 -5
  118. package/dist/internal/session-engine.d.ts +1 -2
  119. package/dist/internal/session-engine.d.ts.map +1 -1
  120. package/dist/internal/session-engine.js +14 -31
  121. package/dist/internal/storage-coordinator.d.ts +16 -15
  122. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  123. package/dist/internal/storage-coordinator.js +73 -80
  124. package/dist/persistence.d.ts +184 -0
  125. package/dist/persistence.d.ts.map +1 -0
  126. package/dist/playground/assets/cursor-icons-16-CQ50JpfO.woff2 +0 -0
  127. package/dist/playground/assets/index-72vCOBWO.js +86 -0
  128. package/dist/playground/assets/index-BjnMwYoR.css +1 -0
  129. package/dist/playground/index.html +2 -2
  130. package/dist/storage.d.ts +51 -10
  131. package/dist/storage.d.ts.map +1 -1
  132. package/dist/storage.js +27 -10
  133. package/docs/README.md +34 -5
  134. package/docs/deployment.md +352 -149
  135. package/docs/example-agents/approval-buddy.md +270 -0
  136. package/docs/example-agents/benny.md +186 -0
  137. package/docs/example-agents/bugbot.md +231 -0
  138. package/docs/example-agents/codebase-wiki.md +174 -0
  139. package/docs/example-agents/codeowners-review.md +195 -0
  140. package/docs/example-agents/concierge.md +205 -0
  141. package/docs/example-agents/fsd.md +330 -0
  142. package/docs/example-agents/index.md +102 -0
  143. package/docs/example-agents/knowledge-base.md +171 -0
  144. package/docs/example-agents/security-reviewer.md +296 -0
  145. package/docs/example-agents/slack-agent.md +146 -0
  146. package/docs/example-agents/weather-agent.md +302 -0
  147. package/docs/reference/cli.md +546 -147
  148. package/package.json +1 -1
  149. package/src/ab.ts +9 -261
  150. package/src/bin/agent-serve.ts +46 -7
  151. package/src/evals.ts +5 -119
  152. package/src/index.ts +2 -0
  153. package/src/internal/ab-collector.ts +12 -22
  154. package/src/internal/ab-snapshot.ts +2 -4
  155. package/src/internal/cli-ax.ts +551 -104
  156. package/src/internal/cli-deploy.ts +1 -1
  157. package/src/internal/discovery.ts +2 -2
  158. package/src/internal/eval-run-store.ts +91 -100
  159. package/src/internal/evals-client.ts +431 -0
  160. package/src/internal/init-project.ts +1 -0
  161. package/src/internal/playground-proxy.ts +5 -5
  162. package/src/internal/resolve-prod-target.ts +101 -3
  163. package/src/internal/server.ts +17 -3
  164. package/src/internal/session-engine.ts +9 -29
  165. package/src/internal/storage-coordinator.ts +109 -101
  166. package/src/storage.ts +79 -14
  167. package/dist/docs/assets/chunks/@localSearchIndexroot.QwK5BtEH.js +0 -1
  168. package/dist/docs/assets/deployment.md.DTKwE15Z.js +0 -14
  169. package/dist/docs/assets/deployment.md.DTKwE15Z.lean.js +0 -1
  170. package/dist/docs/assets/index.md.Cylk70gg.js +0 -20
  171. package/dist/docs/assets/reference_cli.md.Bv6pOxcF.js +0 -38
  172. package/dist/docs/assets/reference_cli.md.Bv6pOxcF.lean.js +0 -1
  173. package/dist/internal/json-dir-store.js +0 -100
  174. package/dist/playground/assets/cursor-icons-outline-BxTT_FVJ.woff2 +0 -0
  175. package/dist/playground/assets/index-BEauYlII.css +0 -1
  176. package/dist/playground/assets/index-BtM0wEGg.js +0 -319
  177. package/src/internal/json-dir-store.ts +0 -109
@@ -204,8 +204,7 @@ export class SessionEngine {
204
204
  * Runtime for `agent/storage.ts` (`defineStorage`), when
205
205
  * authored. All durable state changes funnel through it (session
206
206
  * records, event chunks, A/B samples/snapshots); the eval run store
207
- * attaches via {@link StorageCoordinator.asEvalRunPersistence} at
208
- * serve start.
207
+ * attaches via {@link StorageCoordinator.evalRuns} at serve start.
209
208
  */
210
209
  readonly storage: StorageCoordinator | undefined;
211
210
  /** In-flight lazy restores, deduped per channel + continuation key. */
@@ -261,18 +260,12 @@ export class SessionEngine {
261
260
  projectRoot: options.project.rootDir,
262
261
  logger: this.logger,
263
262
  });
264
- // Domain-specific hooks win; the defineStorage sink is the fallback.
265
- const persistSamples =
266
- options.project.abConfig?.persistSamples ??
267
- this.storage?.asABSamplePersistence();
268
263
  this.abCollector = new ABCollector(
269
264
  options.project.abs,
270
265
  this.logger,
271
266
  async (sessionId) => (await this.logs.get(sessionId)).snapshot(0),
272
- {
273
- projectRoot: options.project.rootDir,
274
- persistSamples,
275
- }
267
+ // Samples flow to the defineStorage `abs` table (no-op without one).
268
+ { persistSample: (sample) => this.storage?.abSample(sample) }
276
269
  );
277
270
  // Peer and Cursor-account transports are symbolic until the serve host
278
271
  // resolves them; the host-side MCP registry starts with the concrete
@@ -1592,6 +1585,7 @@ export class SessionEngine {
1592
1585
  return restoredSnapshot;
1593
1586
  }
1594
1587
  }
1588
+ const abTable = this.project.storage?.abs;
1595
1589
  const snapshot = await buildABSnapshot({
1596
1590
  experiments: this.project.abs,
1597
1591
  agentName: this.project.name,
@@ -1602,27 +1596,13 @@ export class SessionEngine {
1602
1596
  logger: this.logger,
1603
1597
  config: {
1604
1598
  maxPlaygroundSessions,
1605
- durableSamples: abConfig?.persistSamples !== undefined,
1606
- durableSnapshots: abConfig?.persistSnapshots !== undefined,
1599
+ durableSamples: abTable !== undefined,
1600
+ durableSnapshots: abTable?.putSnapshot !== undefined,
1607
1601
  },
1608
1602
  });
1609
- // Side effect on the read path: playground polls GET /v1/abs ~every 4s.
1610
- // Authors who set persistSnapshots accept that write cadence.
1611
- const persistSnapshots = abConfig?.persistSnapshots;
1612
- if (persistSnapshots !== undefined) {
1613
- try {
1614
- await persistSnapshots.save(snapshot, {
1615
- projectRoot: this.project.rootDir,
1616
- });
1617
- } catch (error) {
1618
- this.logger(
1619
- `[agentkit] ab persistSnapshots.save threw: ${describeError(error)}`
1620
- );
1621
- }
1622
- } else {
1623
- // defineStorage fallback (throttled inside the coordinator).
1624
- this.storage?.abSnapshot(snapshot);
1625
- }
1603
+ // Side effect on the read path: playground polls GET /v1/abs ~every 4s;
1604
+ // the coordinator throttles the actual table write.
1605
+ this.storage?.abSnapshot(snapshot);
1626
1606
  return snapshot;
1627
1607
  }
1628
1608
 
@@ -10,13 +10,13 @@
10
10
  * failures — a throwing sink is logged and its write dropped; storage
11
11
  * must never stall or fail a turn.
12
12
  *
13
- * Scaling shape: sessions, reminders, evals, and A/Bs all reduce to keyed
14
- * puts on the same queue, so one author-owned sink covers every domain,
15
- * and new domains are new key prefixes not new config surface.
13
+ * Scaling shape: sessions and reminders reduce to keyed puts on the KV
14
+ * sink; eval runs and A/B metrics go to their dedicated tables
15
+ * (`evals` / `abs`). Everything shares one bounded, serialized queue.
16
16
  */
17
17
 
18
- import type { ABMetricSample, ABSamplePersistence } from "../ab.js";
19
- import type { EvalRunPersistence, EvalRunSnapshot } from "../evals.js";
18
+ import type { ABMetricSample } from "../ab.js";
19
+ import type { EvalRunSnapshot } from "../evals.js";
20
20
  import {
21
21
  type ResolvedStoragePolicy,
22
22
  resolveStoragePolicy,
@@ -27,6 +27,7 @@ import {
27
27
  import type { JsonValue, SessionEvent, SessionRecord } from "../types.js";
28
28
  import type { ABSnapshot } from "./ab-snapshot.js";
29
29
  import { describeError } from "./describe-error.js";
30
+ import type { EvalRunStorage } from "./eval-run-store.js";
30
31
  import { isReminderRecord, type ReminderRecord } from "./reminder-store.js";
31
32
 
32
33
  /** Event types that close a unit of work — flush point for turn-end batching. */
@@ -62,10 +63,12 @@ const DEBOUNCE_MAX_BATCH = 200;
62
63
  */
63
64
  const AB_SNAPSHOT_MIN_INTERVAL_MS = 60_000;
64
65
 
65
- /** One queued sink call. */
66
- type StorageOp =
67
- | { op: "put"; key: string; value: JsonValue }
68
- | { op: "delete"; key: string };
66
+ /** One queued sink call (KV put/delete or a dedicated-table write). */
67
+ interface StorageOp {
68
+ /** Human-readable op label for failure logs, e.g. `put(agentkit/v1/...)`. */
69
+ label: string;
70
+ run: (ctx: StorageContext) => void | Promise<void>;
71
+ }
69
72
 
70
73
  export interface StorageCoordinatorOptions {
71
74
  definition: StorageDefinition;
@@ -182,18 +185,20 @@ export class StorageCoordinator {
182
185
  const ops: StorageOp[] = [];
183
186
  const first = buffer.events[0];
184
187
  if (first !== undefined) {
185
- ops.push({
186
- op: "put",
187
- key: storageKeys.sessionEvents(this.agentName, sessionId, first.index),
188
- value: buffer.events as unknown as JsonValue,
189
- });
188
+ ops.push(
189
+ this.putOp(
190
+ storageKeys.sessionEvents(this.agentName, sessionId, first.index),
191
+ buffer.events as unknown as JsonValue
192
+ )
193
+ );
190
194
  }
191
195
  if (buffer.record !== undefined) {
192
- ops.push({
193
- op: "put",
194
- key: storageKeys.session(this.agentName, sessionId),
195
- value: buffer.record as unknown as JsonValue,
196
- });
196
+ ops.push(
197
+ this.putOp(
198
+ storageKeys.session(this.agentName, sessionId),
199
+ buffer.record as unknown as JsonValue
200
+ )
201
+ );
197
202
  ops.push(...this.continuationOps(buffer.record));
198
203
  }
199
204
  if (ops.length > 0) {
@@ -214,25 +219,39 @@ export class StorageCoordinator {
214
219
  this.continuationIndex.set(record.sessionId, next);
215
220
  const ops: StorageOp[] = [];
216
221
  if (previous != null) {
217
- ops.push({
218
- op: "delete",
219
- key: storageKeys.continuation(
220
- this.agentName,
221
- record.channelId,
222
- previous
223
- ),
224
- });
222
+ ops.push(
223
+ this.deleteOp(
224
+ storageKeys.continuation(this.agentName, record.channelId, previous)
225
+ )
226
+ );
225
227
  }
226
228
  if (next != null) {
227
- ops.push({
228
- op: "put",
229
- key: storageKeys.continuation(this.agentName, record.channelId, next),
230
- value: { sessionId: record.sessionId },
231
- });
229
+ ops.push(
230
+ this.putOp(
231
+ storageKeys.continuation(this.agentName, record.channelId, next),
232
+ { sessionId: record.sessionId }
233
+ )
234
+ );
232
235
  }
233
236
  return ops;
234
237
  }
235
238
 
239
+ /** KV upsert op on the sink's `put`. */
240
+ private putOp(key: string, value: JsonValue): StorageOp {
241
+ return {
242
+ label: `put(${key})`,
243
+ run: (ctx) => this.definition.put(key, value, ctx),
244
+ };
245
+ }
246
+
247
+ /** KV delete op on the sink's `delete` (skipped when the hook is absent). */
248
+ private deleteOp(key: string): StorageOp {
249
+ return {
250
+ label: `delete(${key})`,
251
+ run: (ctx) => this.definition.delete?.(key, ctx),
252
+ };
253
+ }
254
+
236
255
  // ==========================================================================
237
256
  // Sessions (reads / restore)
238
257
  // ==========================================================================
@@ -351,11 +370,10 @@ export class StorageCoordinator {
351
370
  }
352
371
  this.enqueue(
353
372
  [
354
- {
355
- op: "put",
356
- key: storageKeys.reminder(this.agentName, record.id),
357
- value: record as unknown as JsonValue,
358
- },
373
+ this.putOp(
374
+ storageKeys.reminder(this.agentName, record.id),
375
+ record as unknown as JsonValue
376
+ ),
359
377
  ],
360
378
  "policy"
361
379
  );
@@ -375,31 +393,29 @@ export class StorageCoordinator {
375
393
  }
376
394
 
377
395
  // ==========================================================================
378
- // Evals
396
+ // Evals (dedicated `evals` table)
379
397
  // ==========================================================================
380
398
 
381
399
  /**
382
- * Adapter for {@link EvalRunPersistence} so the playground eval store
383
- * can fall back to this sink when `evals.config.ts` sets no
384
- * `persistRuns`.
400
+ * Adapter over the sink's dedicated `evals` table for the playground
401
+ * eval-run store. Undefined when the table is not configured — eval
402
+ * history then stays in process memory.
385
403
  */
386
- asEvalRunPersistence(): EvalRunPersistence {
404
+ evalRuns(): EvalRunStorage | undefined {
405
+ const table = this.definition.evals;
406
+ if (table === undefined) {
407
+ return undefined;
408
+ }
387
409
  return {
388
- load: async () => {
389
- const entries = await this.tryList(
390
- storageKeys.evalRunPrefix(this.agentName)
391
- );
392
- return entries
393
- .map((entry) => entry.value as unknown as EvalRunSnapshot)
394
- .filter((run) => run != null);
395
- },
396
410
  save: (run: EvalRunSnapshot) => {
411
+ // Clone at enqueue time: the store mutates snapshots in place as
412
+ // cases finish, and delivery is async.
413
+ const snapshot = structuredClone(run);
397
414
  this.enqueue(
398
415
  [
399
416
  {
400
- op: "put",
401
- key: storageKeys.evalRun(this.agentName, run.runId),
402
- value: run as unknown as JsonValue,
417
+ label: `evals.put(${run.runId})`,
418
+ run: (ctx) => table.put(snapshot, ctx),
403
419
  },
404
420
  ],
405
421
  "policy"
@@ -409,86 +425,82 @@ export class StorageCoordinator {
409
425
  this.enqueue(
410
426
  [
411
427
  {
412
- op: "delete",
413
- key: storageKeys.evalRun(this.agentName, runId),
428
+ label: `evals.delete(${runId})`,
429
+ run: (ctx) => table.delete(runId, ctx),
414
430
  },
415
431
  ],
416
432
  "policy"
417
433
  );
418
434
  },
435
+ list: async () => {
436
+ try {
437
+ const runs = await table.list(this.context("restore"));
438
+ return Array.isArray(runs) ? runs : [];
439
+ } catch (error) {
440
+ this.logger(
441
+ `[agentkit] storage evals.list() failed: ${describeError(error)}`
442
+ );
443
+ return [];
444
+ }
445
+ },
419
446
  };
420
447
  }
421
448
 
422
449
  // ==========================================================================
423
- // A/Bs
450
+ // A/Bs (dedicated `abs` table)
424
451
  // ==========================================================================
425
452
 
426
- /**
427
- * Adapter for {@link ABSamplePersistence} so the AB collector can fall
428
- * back to this sink when `ab.config.ts` sets no `persistSamples`.
429
- */
430
- asABSamplePersistence(): ABSamplePersistence {
431
- return {
432
- save: (sample: ABMetricSample) => {
433
- this.enqueue(
434
- [
435
- {
436
- op: "put",
437
- key: storageKeys.abSample(
438
- this.agentName,
439
- sample.sessionId,
440
- sample.at
441
- ),
442
- value: sample as unknown as JsonValue,
443
- },
444
- ],
445
- "policy"
446
- );
447
- },
448
- };
453
+ /** Append one metric sample to the `abs` table (no-op without it). */
454
+ abSample(sample: ABMetricSample): void {
455
+ const table = this.definition.abs;
456
+ if (this.closed || table === undefined) {
457
+ return;
458
+ }
459
+ this.enqueue(
460
+ [
461
+ {
462
+ label: `abs.putSample(${sample.sessionId})`,
463
+ run: (ctx) => table.putSample(sample, ctx),
464
+ },
465
+ ],
466
+ "policy"
467
+ );
449
468
  }
450
469
 
451
470
  /**
452
471
  * Refresh the persisted aggregate A/B snapshot, throttled to once per
453
472
  * {@link AB_SNAPSHOT_MIN_INTERVAL_MS} (the playground recomputes the
454
- * fold on every `GET /v1/abs` poll).
473
+ * fold on every `GET /v1/abs` poll). No-op without `abs.putSnapshot`.
455
474
  */
456
475
  abSnapshot(snapshot: ABSnapshot): void {
476
+ const putSnapshot = this.definition.abs?.putSnapshot;
457
477
  const now = Date.now();
458
478
  if (
459
479
  this.closed ||
480
+ putSnapshot === undefined ||
460
481
  now - this.lastAbSnapshotAt < AB_SNAPSHOT_MIN_INTERVAL_MS
461
482
  ) {
462
483
  return;
463
484
  }
464
485
  this.lastAbSnapshotAt = now;
465
486
  this.enqueue(
466
- [
467
- {
468
- op: "put",
469
- key: storageKeys.abSnapshot(this.agentName),
470
- value: snapshot as unknown as JsonValue,
471
- },
472
- ],
487
+ [{ label: "abs.putSnapshot", run: (ctx) => putSnapshot(snapshot, ctx) }],
473
488
  "policy"
474
489
  );
475
490
  }
476
491
 
477
492
  /** Latest persisted aggregate A/B snapshot; errors read as absent. */
478
493
  async getLatestAbSnapshot(): Promise<ABSnapshot | undefined> {
479
- const get = this.definition.get;
480
- if (get === undefined) {
494
+ const getSnapshot = this.definition.abs?.getSnapshot;
495
+ if (getSnapshot === undefined) {
481
496
  return undefined;
482
497
  }
483
498
  try {
484
- const value = await get(
485
- storageKeys.abSnapshot(this.agentName),
486
- this.context("restore")
487
- );
488
- return value == null ? undefined : (value as unknown as ABSnapshot);
499
+ const value = await getSnapshot(this.context("restore"));
500
+ return value ?? undefined;
489
501
  } catch (error) {
490
502
  this.logger(
491
- `[agentkit] storage get(ab-snapshot) failed: ${describeError(error)}`
503
+ `[agentkit] storage abs.getSnapshot() failed: ${describeError(error)}`
492
504
  );
493
505
  return undefined;
494
506
  }
@@ -593,14 +605,10 @@ export class StorageCoordinator {
593
605
  this.queue = this.queue.then(async () => {
594
606
  for (const op of ops) {
595
607
  try {
596
- if (op.op === "put") {
597
- await this.definition.put(op.key, op.value, ctx);
598
- } else {
599
- await this.definition.delete?.(op.key, ctx);
600
- }
608
+ await op.run(ctx);
601
609
  } catch (error) {
602
610
  this.logger(
603
- `[agentkit] storage ${op.op}(${op.key}) failed: ${describeError(error)}`
611
+ `[agentkit] storage ${op.label} failed: ${describeError(error)}`
604
612
  );
605
613
  } finally {
606
614
  this.pending -= 1;
package/src/storage.ts CHANGED
@@ -4,10 +4,11 @@
4
4
  * Author `agent/storage.ts` with {@link defineStorage} to mirror the
5
5
  * framework's durable state into storage you own (a database, S3, a data
6
6
  * pipeline, …). Without it, state lives under `--state-root` on local disk
7
- * only (and eval/A/B history follows the narrower `persistRuns` /
8
- * `persistSamples` / `persistSnapshots` hooks).
7
+ * only, and eval/A/B history stays in process memory.
9
8
  *
10
- * The sink is a plain key-value store — four functions, no schema:
9
+ * The sink is a plain key-value store — four functions, no schema — plus
10
+ * two optional dedicated tables ({@link StorageConfig.evals} and
11
+ * {@link StorageConfig.abs}) for eval-run and A/B history:
11
12
  *
12
13
  * ```ts
13
14
  * import { defineStorage } from "@anysphere/agent-serve/storage";
@@ -17,6 +18,8 @@
17
18
  * get: (key) => db.get(key),
18
19
  * delete: (key) => db.delete(key),
19
20
  * list: (prefix) => db.listByPrefix(prefix), // [{ key, value }] in key order
21
+ * evals: { put: ..., delete: ..., list: ... }, // eval-runs table
22
+ * abs: { putSample: ... }, // A/B table
20
23
  * });
21
24
  * ```
22
25
  *
@@ -41,6 +44,9 @@
41
44
  */
42
45
 
43
46
  import { createHash } from "node:crypto";
47
+ import type { ABMetricSample } from "./ab.js";
48
+ import type { EvalRunSnapshot } from "./evals.js";
49
+ import type { ABSnapshot } from "./internal/ab-snapshot.js";
44
50
  import { brandDefinition } from "./internal/brand.js";
45
51
  import type { JsonValue } from "./types.js";
46
52
 
@@ -110,11 +116,54 @@ export interface StorageRestorePolicy {
110
116
  maxTotalBytes?: number;
111
117
  }
112
118
 
119
+ /**
120
+ * Dedicated table for playground eval batches, keyed by `runId`. The
121
+ * framework saves a full {@link EvalRunSnapshot} as a batch starts,
122
+ * progresses, and finishes; prunes runs past the playground history
123
+ * window; and lists everything back at serve start.
124
+ */
125
+ export interface StorageEvalsTable {
126
+ /** Upsert one run snapshot under `run.runId` (last write wins). */
127
+ put(run: EvalRunSnapshot, ctx: StorageContext): void | Promise<void>;
128
+ /** Remove a run pruned out of the playground history window. */
129
+ delete(runId: string, ctx: StorageContext): void | Promise<void>;
130
+ /** All saved runs (any order). Called once when the serve process hydrates. */
131
+ list(ctx: StorageContext): EvalRunSnapshot[] | Promise<EvalRunSnapshot[]>;
132
+ }
133
+
134
+ /**
135
+ * Dedicated table for live A/B metrics: one row per cumulative metric
136
+ * sample (unique per `experiment` + `sessionId` + `at` — one boundary
137
+ * event emits a sample per enrolled experiment), plus — optionally — the latest
138
+ * aggregate snapshot so a replacement host with no local sessions can
139
+ * still serve the A/Bs surface.
140
+ */
141
+ export interface StorageABTable {
142
+ /** Append one cumulative metric sample (turn complete/fail). */
143
+ putSample(sample: ABMetricSample, ctx: StorageContext): void | Promise<void>;
144
+ /** Upsert the latest aggregate snapshot (framework-throttled). Optional. */
145
+ putSnapshot?(snapshot: ABSnapshot, ctx: StorageContext): void | Promise<void>;
146
+ /** Latest saved aggregate snapshot (replacement-host backfill). Optional. */
147
+ getSnapshot?(
148
+ ctx: StorageContext
149
+ ): ABSnapshot | undefined | null | Promise<ABSnapshot | undefined | null>;
150
+ }
151
+
113
152
  export interface StorageConfig {
114
153
  /** Optional label surfaced on `GET /v1/info` diagnostics. */
115
154
  name?: string;
116
155
  /** Timing knobs; see {@link StoragePolicy}. */
117
156
  policy?: StoragePolicy;
157
+ /**
158
+ * Dedicated eval-runs table. Omit it and playground eval history stays
159
+ * in process memory (lost on restart).
160
+ */
161
+ evals?: StorageEvalsTable;
162
+ /**
163
+ * Dedicated A/B table. Omit it and metric samples are not exported
164
+ * (session event logs remain the assignment/fold source of truth).
165
+ */
166
+ abs?: StorageABTable;
118
167
  /**
119
168
  * Store one value under a key (upsert, last-write-wins). Called on the
120
169
  * framework's schedule — never concurrently, always in order. Keep it
@@ -168,6 +217,30 @@ export function defineStorage(config: StorageConfig): StorageDefinition {
168
217
  );
169
218
  }
170
219
  }
220
+ if (config.evals !== undefined) {
221
+ for (const hook of ["put", "delete", "list"] as const) {
222
+ if (typeof config.evals[hook] !== "function") {
223
+ throw new Error(
224
+ `defineStorage: evals.${hook} must be a function when the evals table is set`
225
+ );
226
+ }
227
+ }
228
+ }
229
+ if (config.abs !== undefined) {
230
+ if (typeof config.abs.putSample !== "function") {
231
+ throw new Error(
232
+ "defineStorage: abs.putSample must be a function when the abs table is set"
233
+ );
234
+ }
235
+ for (const hook of ["putSnapshot", "getSnapshot"] as const) {
236
+ const value = config.abs[hook];
237
+ if (value !== undefined && typeof value !== "function") {
238
+ throw new Error(
239
+ `defineStorage: abs.${hook} must be a function when set`
240
+ );
241
+ }
242
+ }
243
+ }
171
244
  // Validate eagerly so a bad policy fails at discovery, not first flush.
172
245
  resolveStoragePolicy(config.policy);
173
246
  return brandDefinition("storage", config);
@@ -201,9 +274,9 @@ export const STORAGE_KEY_ROOT = "agentkit/v1" as const;
201
274
  * | `agentkit/v1/{agent}/session-events/{sessionId}/{index}` | `SessionEvent[]` chunk (index = first event's index, zero-padded) |
202
275
  * | `agentkit/v1/{agent}/continuation/{channelId}/{token}` | `{ sessionId }` |
203
276
  * | `agentkit/v1/{agent}/reminder/{reminderId}` | `ReminderRecord` |
204
- * | `agentkit/v1/{agent}/eval-run/{runId}` | `EvalRunSnapshot` |
205
- * | `agentkit/v1/{agent}/ab-sample/{sessionId}/{at}` | `ABMetricSample` |
206
- * | `agentkit/v1/{agent}/ab-snapshot` | latest aggregate `ABSnapshot` |
277
+ *
278
+ * Eval-run and A/B history do not flow through this KV scheme — they have
279
+ * dedicated tables ({@link StorageConfig.evals} / {@link StorageConfig.abs}).
207
280
  */
208
281
  export const storageKeys = {
209
282
  session: (agent: string, sessionId: string): string =>
@@ -228,14 +301,6 @@ export const storageKeys = {
228
301
  `${STORAGE_KEY_ROOT}/${agent}/reminder/${reminderId}`,
229
302
  reminderPrefix: (agent: string): string =>
230
303
  `${STORAGE_KEY_ROOT}/${agent}/reminder/`,
231
- evalRun: (agent: string, runId: string): string =>
232
- `${STORAGE_KEY_ROOT}/${agent}/eval-run/${runId}`,
233
- evalRunPrefix: (agent: string): string =>
234
- `${STORAGE_KEY_ROOT}/${agent}/eval-run/`,
235
- abSample: (agent: string, sessionId: string, at: string): string =>
236
- `${STORAGE_KEY_ROOT}/${agent}/ab-sample/${sessionId}/${at}`,
237
- abSnapshot: (agent: string): string =>
238
- `${STORAGE_KEY_ROOT}/${agent}/ab-snapshot`,
239
304
  } as const;
240
305
 
241
306
  /**