@cursor/july 0.1.6 → 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 (83) hide show
  1. package/dist/bin/agent-serve.js +42 -17
  2. package/dist/channels/slack/post-update-delivery.d.ts +85 -0
  3. package/dist/channels/slack/post-update-delivery.d.ts.map +1 -0
  4. package/dist/docs/404.html +1 -1
  5. package/dist/docs/ab.html +2 -2
  6. package/dist/docs/assets/{app.DMboGKUD.js → app.COTN7wgo.js} +1 -1
  7. package/dist/docs/assets/chunks/@localSearchIndexroot.B7UcKvIn.js +1 -0
  8. package/dist/docs/assets/chunks/{VPLocalSearchBox.DkEqEnJU.js → VPLocalSearchBox.BW3TBdT0.js} +1 -1
  9. package/dist/docs/assets/chunks/{theme.ASugdMNm.js → theme.BEJW0vE7.js} +2 -2
  10. package/dist/docs/building-with-agents.html +2 -2
  11. package/dist/docs/concepts.html +2 -2
  12. package/dist/docs/deployment.html +2 -2
  13. package/dist/docs/evals.html +2 -2
  14. package/dist/docs/example-agents/approval-buddy.html +2 -2
  15. package/dist/docs/example-agents/benny.html +2 -2
  16. package/dist/docs/example-agents/bugbot.html +2 -2
  17. package/dist/docs/example-agents/codebase-wiki.html +2 -2
  18. package/dist/docs/example-agents/codeowners-review.html +2 -2
  19. package/dist/docs/example-agents/concierge.html +2 -2
  20. package/dist/docs/example-agents/fsd.html +2 -2
  21. package/dist/docs/example-agents/index.html +2 -2
  22. package/dist/docs/example-agents/knowledge-base.html +2 -2
  23. package/dist/docs/example-agents/security-reviewer.html +2 -2
  24. package/dist/docs/example-agents/slack-agent.html +2 -2
  25. package/dist/docs/example-agents/weather-agent.html +2 -2
  26. package/dist/docs/guides/agent-to-agent.html +2 -2
  27. package/dist/docs/guides/cloud-runtime.html +2 -2
  28. package/dist/docs/guides/github.html +2 -2
  29. package/dist/docs/guides/human-in-the-loop.html +2 -2
  30. package/dist/docs/guides/slack.html +2 -2
  31. package/dist/docs/guides/webhooks.html +2 -2
  32. package/dist/docs/hillclimbing.html +2 -2
  33. package/dist/docs/index.html +2 -2
  34. package/dist/docs/quickstart.html +2 -2
  35. package/dist/docs/reference/agent-config.html +2 -2
  36. package/dist/docs/reference/channels.html +2 -2
  37. package/dist/docs/reference/cli.html +2 -2
  38. package/dist/docs/reference/connections.html +2 -2
  39. package/dist/docs/reference/hooks.html +2 -2
  40. package/dist/docs/reference/http-api.html +2 -2
  41. package/dist/docs/reference/instructions.html +2 -2
  42. package/dist/docs/reference/playground.html +2 -2
  43. package/dist/docs/reference/project-layout.html +2 -2
  44. package/dist/docs/reference/schedules.html +2 -2
  45. package/dist/docs/reference/sessions.html +2 -2
  46. package/dist/docs/reference/skills.html +2 -2
  47. package/dist/docs/reference/subagents.html +2 -2
  48. package/dist/docs/reference/tools.html +2 -2
  49. package/dist/docs/scaffolding-agents.html +2 -2
  50. package/dist/docs/storage.html +2 -2
  51. package/dist/docs/troubleshooting.html +2 -2
  52. package/dist/evals.d.ts +2 -2
  53. package/dist/internal/cli-ax.d.ts +23 -10
  54. package/dist/internal/cli-ax.d.ts.map +1 -1
  55. package/dist/internal/cli-ax.js +358 -55
  56. package/dist/internal/eval-run-store.d.ts +2 -0
  57. package/dist/internal/eval-run-store.d.ts.map +1 -1
  58. package/dist/internal/eval-run-store.js +4 -4
  59. package/dist/internal/evals-client.d.ts +96 -0
  60. package/dist/internal/evals-client.d.ts.map +1 -0
  61. package/dist/internal/evals-client.js +262 -0
  62. package/dist/internal/init-project.js +3 -3
  63. package/dist/internal/json-dir-store.d.ts +32 -0
  64. package/dist/internal/json-dir-store.d.ts.map +1 -0
  65. package/dist/internal/persistence-coordinator.d.ts +127 -0
  66. package/dist/internal/persistence-coordinator.d.ts.map +1 -0
  67. package/dist/internal/server.js +11 -2
  68. package/dist/persistence.d.ts +184 -0
  69. package/dist/persistence.d.ts.map +1 -0
  70. package/dist/playground/assets/index-72vCOBWO.js +86 -0
  71. package/dist/playground/assets/index-BjnMwYoR.css +1 -0
  72. package/dist/playground/index.html +2 -2
  73. package/package.json +1 -1
  74. package/src/bin/agent-serve.ts +47 -16
  75. package/src/evals.ts +2 -2
  76. package/src/internal/cli-ax.ts +466 -75
  77. package/src/internal/eval-run-store.ts +5 -0
  78. package/src/internal/evals-client.ts +431 -0
  79. package/src/internal/init-project.ts +3 -3
  80. package/src/internal/server.ts +12 -0
  81. package/dist/docs/assets/chunks/@localSearchIndexroot.O6eP7QZr.js +0 -1
  82. package/dist/playground/assets/index-7GJIUFQj.css +0 -1
  83. package/dist/playground/assets/index-mux9aj5P.js +0 -319
@@ -0,0 +1,262 @@
1
+ /**
2
+ * HTTP client for playground eval batches (`/v1/dev/evals/runs`).
3
+ *
4
+ * Used by `agentkit eval --prod` (or `--url`) so the batch runs on the serve
5
+ * process and is mirrored to `defineStorage` when an `evals` table is
6
+ * configured. Without those flags, eval keeps the in-process ephemeral path
7
+ * in `cli-ax.ts`.
8
+ */
9
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
10
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
11
+ return new (P || (P = Promise))(function (resolve, reject) {
12
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
15
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
16
+ });
17
+ };
18
+ import { matchesEvalIdFilter } from "../evals.js";
19
+ import { mergeRequestHeaders } from "./request-headers.js";
20
+ const DEFAULT_POLL_MS = 1000;
21
+ export class RemoteEvalError extends Error {
22
+ constructor(message, status, code, activeRunId) {
23
+ super(message);
24
+ this.status = status;
25
+ this.code = code;
26
+ this.activeRunId = activeRunId;
27
+ this.name = "RemoteEvalError";
28
+ }
29
+ }
30
+ /**
31
+ * Start a batch on the target serve via `POST /v1/dev/evals/runs`, poll until
32
+ * terminal, and return CLI-shaped case results.
33
+ */
34
+ export function runRemoteEvals(options) {
35
+ return __awaiter(this, void 0, void 0, function* () {
36
+ var _a;
37
+ var _b, _c;
38
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
39
+ const fetchImpl = (_b = options.fetchImpl) !== null && _b !== void 0 ? _b : fetch;
40
+ const auth = {
41
+ bearerToken: options.bearerToken,
42
+ headers: options.headers,
43
+ };
44
+ const started = yield startRemoteEvalRun({
45
+ baseUrl,
46
+ auth,
47
+ fetchImpl,
48
+ filterIds: options.filterIds,
49
+ tags: options.tags,
50
+ timeoutMs: options.timeoutMs,
51
+ verbose: options.verbose,
52
+ signal: options.signal,
53
+ });
54
+ (_a = options.onSnapshot) === null || _a === void 0 ? void 0 : _a.call(options, started);
55
+ const snapshot = yield pollRemoteEvalRun({
56
+ baseUrl,
57
+ runId: started.runId,
58
+ auth,
59
+ fetchImpl,
60
+ pollMs: (_c = options.pollMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_MS,
61
+ signal: options.signal,
62
+ onSnapshot: options.onSnapshot,
63
+ });
64
+ return { snapshot, results: snapshotToEvalRunResults(snapshot) };
65
+ });
66
+ }
67
+ /** List datapoints from `GET /v1/dev/evals` (same surface a remote run uses). */
68
+ export function listRemoteEvals(args) {
69
+ return __awaiter(this, void 0, void 0, function* () {
70
+ var _a, _b;
71
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
72
+ const fetchImpl = (_a = args.fetchImpl) !== null && _a !== void 0 ? _a : fetch;
73
+ let resp;
74
+ try {
75
+ resp = yield fetchImpl(`${baseUrl}/v1/dev/evals`, {
76
+ headers: mergeRequestHeaders(args.auth),
77
+ signal: args.signal,
78
+ });
79
+ }
80
+ catch (error) {
81
+ throw new RemoteEvalError(error instanceof Error ? error.message : "Could not list evals.", undefined, "start_failed");
82
+ }
83
+ if (!resp.ok) {
84
+ throw new RemoteEvalError(`Could not list evals (HTTP ${resp.status}).`, resp.status, "start_failed");
85
+ }
86
+ const body = (yield resp.json());
87
+ const listed = [];
88
+ for (const row of (_b = body.evals) !== null && _b !== void 0 ? _b : []) {
89
+ if (typeof row.id !== "string" || row.id === "") {
90
+ continue;
91
+ }
92
+ if (typeof row.fileId !== "string" || row.fileId === "") {
93
+ continue;
94
+ }
95
+ const tags = Array.isArray(row.tags)
96
+ ? row.tags.filter((t) => typeof t === "string")
97
+ : undefined;
98
+ listed.push(Object.assign(Object.assign({ id: row.id, fileId: row.fileId }, (typeof row.description === "string"
99
+ ? { description: row.description }
100
+ : {})), (tags === undefined || tags.length === 0 ? {} : { tags })));
101
+ }
102
+ let selected = listed;
103
+ if (args.filterIds !== undefined && args.filterIds.length > 0) {
104
+ selected = selected.filter((e) => args.filterIds.some((f) => matchesEvalIdFilter(e.id, f)));
105
+ }
106
+ if (args.tags !== undefined && args.tags.length > 0) {
107
+ selected = selected.filter((e) => { var _a; return ((_a = e.tags) !== null && _a !== void 0 ? _a : []).some((t) => args.tags.includes(t)); });
108
+ }
109
+ return selected;
110
+ });
111
+ }
112
+ export function startRemoteEvalRun(args) {
113
+ return __awaiter(this, void 0, void 0, function* () {
114
+ var _a;
115
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
116
+ const fetchImpl = (_a = args.fetchImpl) !== null && _a !== void 0 ? _a : fetch;
117
+ const body = {};
118
+ if (args.filterIds !== undefined && args.filterIds.length > 0) {
119
+ body.filterIds = args.filterIds;
120
+ }
121
+ if (args.tags !== undefined && args.tags.length > 0) {
122
+ body.tags = args.tags;
123
+ }
124
+ if (args.timeoutMs !== undefined) {
125
+ body.timeoutMs = args.timeoutMs;
126
+ }
127
+ if (args.verbose === true) {
128
+ body.verbose = true;
129
+ }
130
+ let resp;
131
+ try {
132
+ resp = yield fetchImpl(`${baseUrl}/v1/dev/evals/runs`, {
133
+ method: "POST",
134
+ headers: Object.assign({ "content-type": "application/json" }, mergeRequestHeaders(args.auth)),
135
+ body: JSON.stringify(body),
136
+ signal: args.signal,
137
+ });
138
+ }
139
+ catch (error) {
140
+ throw new RemoteEvalError(error instanceof Error ? error.message : "Could not start eval run.", undefined, "start_failed");
141
+ }
142
+ const json = (yield resp.json().catch(() => ({})));
143
+ if (json.error === "no_matching_evals") {
144
+ throw new RemoteEvalError("No matching evals found.", 404, "no_matching_evals");
145
+ }
146
+ if (resp.status === 409 || json.error === "eval_run_in_progress") {
147
+ throw new RemoteEvalError(json.activeRunId !== undefined && json.activeRunId !== ""
148
+ ? `An eval run is already in progress (${json.activeRunId}).`
149
+ : "An eval run is already in progress.", 409, "eval_run_in_progress", typeof json.activeRunId === "string" ? json.activeRunId : undefined);
150
+ }
151
+ if (!resp.ok) {
152
+ const detail = typeof json.message === "string" && json.message !== ""
153
+ ? json.message
154
+ : typeof json.error === "string" && json.error !== ""
155
+ ? json.error
156
+ : `HTTP ${resp.status}`;
157
+ throw new RemoteEvalError(`Could not start eval run: ${detail}`, resp.status, "start_failed");
158
+ }
159
+ if (typeof json.runId !== "string" || json.runId === "") {
160
+ throw new RemoteEvalError("Eval start response missing runId.", undefined, "start_failed");
161
+ }
162
+ return json;
163
+ });
164
+ }
165
+ /** One-shot `GET /v1/dev/evals/runs/:runId` (no polling). */
166
+ export function getRemoteEvalRun(args) {
167
+ return __awaiter(this, void 0, void 0, function* () {
168
+ var _a;
169
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
170
+ const fetchImpl = (_a = args.fetchImpl) !== null && _a !== void 0 ? _a : fetch;
171
+ let resp;
172
+ try {
173
+ resp = yield fetchImpl(`${baseUrl}/v1/dev/evals/runs/${encodeURIComponent(args.runId)}`, {
174
+ headers: mergeRequestHeaders(args.auth),
175
+ signal: args.signal,
176
+ });
177
+ }
178
+ catch (error) {
179
+ throw new RemoteEvalError(error instanceof Error ? error.message : "Could not fetch eval run.", undefined, "poll_failed");
180
+ }
181
+ if (resp.status === 404) {
182
+ throw new RemoteEvalError(`Unknown eval run ${args.runId}.`, 404, "unknown_eval_run");
183
+ }
184
+ if (!resp.ok) {
185
+ throw new RemoteEvalError(`Could not fetch eval run (HTTP ${resp.status}).`, resp.status, "poll_failed");
186
+ }
187
+ return (yield resp.json());
188
+ });
189
+ }
190
+ /** Recent batches from `GET /v1/dev/evals/runs`. */
191
+ export function listRemoteEvalRuns(args) {
192
+ return __awaiter(this, void 0, void 0, function* () {
193
+ var _a;
194
+ const baseUrl = args.baseUrl.replace(/\/$/, "");
195
+ const fetchImpl = (_a = args.fetchImpl) !== null && _a !== void 0 ? _a : fetch;
196
+ let resp;
197
+ try {
198
+ resp = yield fetchImpl(`${baseUrl}/v1/dev/evals/runs`, {
199
+ headers: mergeRequestHeaders(args.auth),
200
+ signal: args.signal,
201
+ });
202
+ }
203
+ catch (error) {
204
+ throw new RemoteEvalError(error instanceof Error ? error.message : "Could not list eval runs.", undefined, "poll_failed");
205
+ }
206
+ if (!resp.ok) {
207
+ throw new RemoteEvalError(`Could not list eval runs (HTTP ${resp.status}).`, resp.status, "poll_failed");
208
+ }
209
+ const body = (yield resp.json());
210
+ const runs = Array.isArray(body.runs)
211
+ ? body.runs.filter((r) => r !== null &&
212
+ typeof r === "object" &&
213
+ typeof r.runId === "string")
214
+ : [];
215
+ return Object.assign({ runs }, (typeof body.activeRunId === "string" && body.activeRunId !== ""
216
+ ? { activeRunId: body.activeRunId }
217
+ : {}));
218
+ });
219
+ }
220
+ export function pollRemoteEvalRun(args) {
221
+ return __awaiter(this, void 0, void 0, function* () {
222
+ var _a, _b;
223
+ var _c;
224
+ const pollMs = (_c = args.pollMs) !== null && _c !== void 0 ? _c : DEFAULT_POLL_MS;
225
+ let lastJson = "";
226
+ for (;;) {
227
+ if (((_a = args.signal) === null || _a === void 0 ? void 0 : _a.aborted) === true) {
228
+ throw new RemoteEvalError("Eval wait aborted.", undefined, "aborted");
229
+ }
230
+ const snap = yield getRemoteEvalRun(args);
231
+ const serialized = JSON.stringify(snap);
232
+ if (serialized !== lastJson) {
233
+ lastJson = serialized;
234
+ (_b = args.onSnapshot) === null || _b === void 0 ? void 0 : _b.call(args, snap);
235
+ }
236
+ if (snap.status !== "running") {
237
+ return snap;
238
+ }
239
+ yield sleep(pollMs, args.signal);
240
+ }
241
+ });
242
+ }
243
+ export function snapshotToEvalRunResults(snapshot) {
244
+ return snapshot.cases.map((c) => { var _a, _b, _c, _d; return (Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ id: c.id, path: c.fileId }, (c.description === undefined ? {} : { description: c.description })), { ok: c.ok === true, assertions: (_a = c.assertions) !== null && _a !== void 0 ? _a : [] }), (c.sessionId === undefined ? {} : { sessionId: c.sessionId })), { inputs: (_b = c.inputs) !== null && _b !== void 0 ? _b : [] }), (c.finalText === undefined ? {} : { finalText: c.finalText })), (c.tools === undefined ? {} : { tools: c.tools })), (c.toolCalls === undefined ? {} : { toolCalls: c.toolCalls })), (c.error === undefined ? {} : { error: c.error })), { logs: (_c = c.logs) !== null && _c !== void 0 ? _c : [], durationMs: (_d = c.durationMs) !== null && _d !== void 0 ? _d : 0 })); });
245
+ }
246
+ function sleep(ms, signal) {
247
+ return new Promise((resolve, reject) => {
248
+ if ((signal === null || signal === void 0 ? void 0 : signal.aborted) === true) {
249
+ reject(new RemoteEvalError("Eval wait aborted.", undefined, "aborted"));
250
+ return;
251
+ }
252
+ const timer = setTimeout(() => {
253
+ signal === null || signal === void 0 ? void 0 : signal.removeEventListener("abort", onAbort);
254
+ resolve();
255
+ }, ms);
256
+ const onAbort = () => {
257
+ clearTimeout(timer);
258
+ reject(new RemoteEvalError("Eval wait aborted.", undefined, "aborted"));
259
+ };
260
+ signal === null || signal === void 0 ? void 0 : signal.addEventListener("abort", onAbort, { once: true });
261
+ });
262
+ }
@@ -301,15 +301,15 @@ Recommended loop once the project exists:
301
301
  1. Edit agent/ + tools under agent/tools/
302
302
  2. ${CLI_COMMAND_NAME} info --dir . --json
303
303
  3. ${CLI_COMMAND_NAME} run --dir . --message "..."
304
- 4. ${CLI_COMMAND_NAME} eval --local --dir .
304
+ 4. ${CLI_COMMAND_NAME} eval --dir .
305
305
 
306
306
  Evals:
307
307
  Create evals/**/*.eval.ts using defineEval from ${PACKAGE_NAME}/evals
308
308
  Case id is the path: evals/weather/nyc.eval.ts → weather/nyc
309
309
  Required: evals/evals.config.ts with defineEvalConfig({ maxConcurrency })
310
310
  (hard-capped at 200 due to model provider request limits)
311
- Run: ${CLI_COMMAND_NAME} eval --local [--json] [evalId...]
312
- (omit --local to run against the hosted deployment)
311
+ Run: ${CLI_COMMAND_NAME} eval [--json] [evalId...]
312
+ (pass --prod to run against the hosted deployment)
313
313
 
314
314
  See AGENTS.md in the ${PACKAGE_NAME} package for the full loop.
315
315
  `);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Small filesystem helpers for one-JSON-file-per-id persistence under a
3
+ * project-relative directory (eval run snapshots, A/B aggregate snapshots).
4
+ */
5
+ export declare function assertNonEmptyDir(dir: string, fnName: string): string;
6
+ /** Resolve `dir` against `projectRoot` when relative. */
7
+ export declare function resolveProjectRelativeDir(args: {
8
+ dir: string;
9
+ projectRoot: string;
10
+ }): string;
11
+ /** List `*.json` basenames in `root` (any order). Missing dir → []. */
12
+ export declare function listJsonBasenames(root: string): Promise<string[]>;
13
+ /**
14
+ * Read and JSON.parse every `*.json` file under `root`. Missing dir → [].
15
+ * Unreadable or corrupt files are skipped so one bad snapshot cannot wipe
16
+ * the rest of the history.
17
+ */
18
+ export declare function readJsonDir<T>(root: string, options?: {
19
+ sortBasenames?: "asc" | "desc";
20
+ }): Promise<T[]>;
21
+ /** Atomically write JSON under `root`/`fileName` (temp file + rename). */
22
+ export declare function writeJsonFile(args: {
23
+ root: string;
24
+ fileName: string;
25
+ value: unknown;
26
+ }): Promise<void>;
27
+ /** Best-effort delete; ignores missing files. */
28
+ export declare function deleteFileInDir(args: {
29
+ root: string;
30
+ fileName: string;
31
+ }): Promise<void>;
32
+ //# sourceMappingURL=json-dir-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json-dir-store.d.ts","sourceRoot":"","sources":["../../src/internal/json-dir-store.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,yDAAyD;AACzD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,MAAM,CAET;AAWD,uEAAuE;AACvE,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAWvE;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,CAAC,EACjC,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;IAAE,aAAa,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,GAC3C,OAAO,CAAC,CAAC,EAAE,CAAC,CAiBd;AAED,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUhB;AAED,iDAAiD;AACjD,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Runtime for `definePersistence` (`agent/persistence.ts`).
3
+ *
4
+ * The coordinator is the single funnel between the engine's hot paths and
5
+ * the author's key-value sink. The sink is four functions (`put` / `get` /
6
+ * `delete` / `list`); the coordinator owns everything else: it mints every
7
+ * key from the versioned scheme ({@link persistenceKeys}), coalesces
8
+ * session records and batches event chunks per turn (or debounce window),
9
+ * serializes delivery on one bounded queue per agent, and isolates
10
+ * failures — a throwing sink is logged and its write dropped; persistence
11
+ * must never stall or fail a turn.
12
+ *
13
+ * Scaling shape: sessions, evals, and A/Bs all reduce to keyed puts on the
14
+ * same queue, so one author-owned sink covers every domain, and new
15
+ * domains are new key prefixes — not new config surface.
16
+ */
17
+ import type { ABSamplePersistence } from "../ab.js";
18
+ import type { EvalRunPersistence } from "../evals.js";
19
+ import { type PersistenceContext, type PersistenceDefinition, type ResolvedPersistencePolicy } from "../persistence.js";
20
+ import type { SessionEvent, SessionRecord } from "../types.js";
21
+ import type { ABSnapshot } from "./ab-snapshot.js";
22
+ export interface PersistenceCoordinatorOptions {
23
+ definition: PersistenceDefinition;
24
+ agentName: string;
25
+ projectRoot: string;
26
+ logger: (line: string) => void;
27
+ }
28
+ export declare class PersistenceCoordinator {
29
+ readonly policy: ResolvedPersistencePolicy;
30
+ private readonly definition;
31
+ private readonly agentName;
32
+ private readonly projectRoot;
33
+ private readonly logger;
34
+ /** Per-session pending state (events + coalesced record). */
35
+ private readonly buffers;
36
+ /**
37
+ * Last continuation key written to the sink per session, so a token
38
+ * change emits a delete for the stale index entry alongside the new put.
39
+ */
40
+ private readonly continuationIndex;
41
+ /** Single delivery chain: writes reach the sink serialized, in order. */
42
+ private queue;
43
+ /** Ops on the queue not yet delivered (bounded by MAX_PENDING_OPS). */
44
+ private pending;
45
+ /** Ops dropped because the queue was full. */
46
+ private dropped;
47
+ private lastAbSnapshotAt;
48
+ private closed;
49
+ constructor(options: PersistenceCoordinatorOptions);
50
+ /** Durable session-record update (engine calls after every store save). */
51
+ sessionRecord(record: SessionRecord): void;
52
+ /** One appended session event (engine calls from its dispatch funnel). */
53
+ event(event: SessionEvent): void;
54
+ /**
55
+ * A boundary event's dispatch settled: channel/hook handlers ran and
56
+ * their record updates (channel state, continuation tokens) are
57
+ * buffered. This — not the append — is the turn-end flush point, so the
58
+ * persisted session record includes the boundary event's own handler
59
+ * mutations. Debounced batching intentionally ignores boundaries.
60
+ */
61
+ eventDispatched(event: SessionEvent): void;
62
+ /** Flush everything buffered for one session (ordered: events, record). */
63
+ flushSession(sessionId: string, reason?: PersistenceContext["reason"]): void;
64
+ /**
65
+ * Index maintenance for `continuation/{channelId}/{token}` → sessionId:
66
+ * put the current token, delete the previous one when it changed.
67
+ */
68
+ private continuationOps;
69
+ /** Whether the sink can serve the startup bulk restore (`list`). */
70
+ get canBulkRestore(): boolean;
71
+ /**
72
+ * Saved session records for this agent (startup restore). Errors are
73
+ * logged and read as "nothing saved" — a broken store must never block
74
+ * serve start.
75
+ */
76
+ listSessions(): Promise<SessionRecord[]>;
77
+ /**
78
+ * Saved event stream for one session: chunk values concatenated in key
79
+ * order (chunks are keyed by their first event's index, zero-padded, so
80
+ * ascending key order is append order). Errors read as an empty stream.
81
+ */
82
+ listSessionEvents(sessionId: string): Promise<SessionEvent[]>;
83
+ /**
84
+ * One saved session by channel continuation key (lazy restore). Unlike
85
+ * the startup reads, sink errors **propagate**: a failing store must
86
+ * fail the follow-up (which the caller can retry) rather than read as
87
+ * "unknown token" — that would mint a new session under the same
88
+ * continuation key and permanently shadow the real one on this host.
89
+ * A definitive miss (no index entry, or a write-only sink without
90
+ * `get`) resolves undefined.
91
+ */
92
+ getSessionByContinuation(channelId: string, continuationKey: string): Promise<SessionRecord | undefined>;
93
+ /**
94
+ * Adapter for {@link EvalRunPersistence} so the playground eval store
95
+ * can fall back to this sink when `evals.config.ts` sets no
96
+ * `persistRuns`.
97
+ */
98
+ asEvalRunPersistence(): EvalRunPersistence;
99
+ /**
100
+ * Adapter for {@link ABSamplePersistence} so the AB collector can fall
101
+ * back to this sink when `ab.config.ts` sets no `persistSamples`.
102
+ */
103
+ asABSamplePersistence(): ABSamplePersistence;
104
+ /**
105
+ * Refresh the persisted aggregate A/B snapshot, throttled to once per
106
+ * {@link AB_SNAPSHOT_MIN_INTERVAL_MS} (the playground recomputes the
107
+ * fold on every `GET /v1/abs` poll).
108
+ */
109
+ abSnapshot(snapshot: ABSnapshot): void;
110
+ /** Latest persisted aggregate A/B snapshot; errors read as absent. */
111
+ getLatestAbSnapshot(): Promise<ABSnapshot | undefined>;
112
+ /**
113
+ * Flush all buffers and drain the queue, giving the sink at most
114
+ * {@link CLOSE_DRAIN_TIMEOUT_MS} — a hung sink must not stall shutdown
115
+ * past the pod grace period. Called from engine close.
116
+ */
117
+ close(): Promise<void>;
118
+ /** Pending deliveries (exposed for tests and drain instrumentation). */
119
+ whenIdle(): Promise<void>;
120
+ private buffer;
121
+ private armDebounce;
122
+ /** `list` wrapper for the startup reads: missing hook or throw ⇒ empty. */
123
+ private tryList;
124
+ private enqueue;
125
+ private context;
126
+ }
127
+ //# sourceMappingURL=persistence-coordinator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence-coordinator.d.ts","sourceRoot":"","sources":["../../src/internal/persistence-coordinator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAkB,mBAAmB,EAAE,MAAM,UAAU,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AACvE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAE1B,KAAK,yBAAyB,EAE/B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAa,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAyCnD,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,qBAAqB,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AASD,qBAAa,sBAAsB;IACjC,QAAQ,CAAC,MAAM,EAAE,yBAAyB,CAAC;IAE3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD,6DAA6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAoC;IACtE,yEAAyE;IACzE,OAAO,CAAC,KAAK,CAAoC;IACjD,uEAAuE;IACvE,OAAO,CAAC,OAAO,CAAK;IACpB,8CAA8C;IAC9C,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,MAAM,CAAS;IAEvB,YAAY,OAAO,EAAE,6BAA6B,EAMjD;IAMD,2EAA2E;IAC3E,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAMzC;IAED,0EAA0E;IAC1E,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CAoB/B;IAED;;;;;;OAMG;IACH,eAAe,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI,CASzC;IAED,2EAA2E;IAC3E,YAAY,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,GAAE,kBAAkB,CAAC,QAAQ,CAAY,GAC9C,IAAI,CAiCN;IAED;;;OAGG;IACH,OAAO,CAAC,eAAe;IAoCvB,oEAAoE;IACpE,IAAI,cAAc,IAAI,OAAO,CAE5B;IAED;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAO7C;IAED;;;;OAIG;IACG,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAWlE;IAED;;;;;;;;OAQG;IACG,wBAAwB,CAC5B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAgDpC;IAMD;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAkCzC;IAMD;;;OAGG;IACH,qBAAqB,IAAI,mBAAmB,CAmB3C;IAED;;;;OAIG;IACH,UAAU,CAAC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAmBrC;IAED,sEAAsE;IAChE,mBAAmB,IAAI,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAiB3D;IAMD;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqB3B;IAED,wEAAwE;IACxE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAExB;IAED,OAAO,CAAC,MAAM;IASd,OAAO,CAAC,WAAW;IAgBnB,2EAA2E;YAC7D,OAAO;IAkBrB,OAAO,CAAC,OAAO;IAqCf,OAAO,CAAC,OAAO;CAGhB"}
@@ -846,14 +846,16 @@ function mountAgentRoutes(router, runtime, args) {
846
846
  const body = yield readJsonObject(request);
847
847
  const filterIds = asStringArray(body === null || body === void 0 ? void 0 : body.filterIds);
848
848
  const tags = asStringArray(body === null || body === void 0 ? void 0 : body.tags);
849
+ const timeoutMs = asPositiveInt(body === null || body === void 0 ? void 0 : body.timeoutMs);
850
+ const verbose = (body === null || body === void 0 ? void 0 : body.verbose) === true;
849
851
  const authHeader = request.headers.get("authorization");
850
852
  const bearerToken = (authHeader === null || authHeader === void 0 ? void 0 : authHeader.toLowerCase().startsWith("bearer "))
851
853
  ? authHeader.slice("bearer ".length).trim()
852
854
  : undefined;
853
855
  try {
854
- const snap = yield runtime.evalRuns.start(Object.assign(Object.assign(Object.assign({}, (filterIds === undefined ? {} : { filterIds })), (tags === undefined ? {} : { tags })), (bearerToken === undefined || bearerToken === ""
856
+ const snap = yield runtime.evalRuns.start(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (filterIds === undefined ? {} : { filterIds })), (tags === undefined ? {} : { tags })), (bearerToken === undefined || bearerToken === ""
855
857
  ? {}
856
- : { bearerToken })));
858
+ : { bearerToken })), (timeoutMs === undefined ? {} : { timeoutMs })), (verbose ? { verbose: true } : {})));
857
859
  return jsonResponse(snap, { status: 202 });
858
860
  }
859
861
  catch (error) {
@@ -1488,6 +1490,13 @@ function asStringArray(value) {
1488
1490
  }
1489
1491
  return out.length === 0 ? undefined : out;
1490
1492
  }
1493
+ function asPositiveInt(value) {
1494
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1495
+ return undefined;
1496
+ }
1497
+ const n = Math.floor(value);
1498
+ return n > 0 ? n : undefined;
1499
+ }
1491
1500
  function assertReminderReadable(input) {
1492
1501
  return __awaiter(this, void 0, void 0, function* () {
1493
1502
  const existing = yield input.engine.sessions.findByContinuation(input.reminder.channelId, input.reminder.continuationToken);
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Durable persistence plug-in for agent-serve.
3
+ *
4
+ * Author `agent/persistence.ts` with {@link definePersistence} to mirror the
5
+ * framework's durable state into storage you own (a database, S3, a data
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).
9
+ *
10
+ * The sink is a plain key-value store — four functions, no schema:
11
+ *
12
+ * ```ts
13
+ * import { definePersistence } from "@anysphere/agent-serve/persistence";
14
+ *
15
+ * export default definePersistence({
16
+ * put: (key, value) => db.upsert(key, value),
17
+ * get: (key) => db.get(key),
18
+ * delete: (key) => db.delete(key),
19
+ * list: (prefix) => db.listByPrefix(prefix), // [{ key, value }] in key order
20
+ * });
21
+ * ```
22
+ *
23
+ * The **framework mints every key** from a stable, versioned scheme (see
24
+ * {@link persistenceKeys}) and decides **when** to call the sink: session
25
+ * records and event chunks flush when a turn's handlers have settled,
26
+ * reads happen at serve start (bulk restore), on continuation-token misses
27
+ * (lazy restore), and at playground hydration. Authors do not schedule
28
+ * reads or writes — the only timing knobs are {@link PersistencePolicy}'s
29
+ * `debounceMs` (event write batching) and `restore` (startup hydration).
30
+ *
31
+ * Because keys are opaque strings to the sink, new kinds of durable state
32
+ * (reminders, channel cursors, thread affinity, …) are new key prefixes —
33
+ * existing sinks store them with no code changes.
34
+ *
35
+ * Delivery semantics: writes are **serialized** (one sink call in flight
36
+ * per agent, in order), **bounded** (a sink that falls behind sheds writes
37
+ * rather than growing memory), and **at-most-once** — a throwing `put` is
38
+ * logged and dropped, never retried, and never fails a turn. The local
39
+ * event log under `--state-root` remains the live source of truth; this
40
+ * interface is the durable mirror.
41
+ */
42
+ import type { JsonValue } from "./types.js";
43
+ /** One `{ key, value }` pair returned by {@link PersistenceConfig.list}. */
44
+ export interface PersistenceEntry {
45
+ key: string;
46
+ value: JsonValue;
47
+ }
48
+ /** Context passed to every sink call. */
49
+ export interface PersistenceContext {
50
+ /** Agent name (also baked into every key; see {@link persistenceKeys}). */
51
+ agentName: string;
52
+ /** Absolute agent project root (directory that contains `agent/`). */
53
+ projectRoot: string;
54
+ /**
55
+ * Why the framework is calling:
56
+ * - `"policy"` — a flush trigger fired (turn end, debounce, change)
57
+ * - `"shutdown"` — the serve process is draining; last chance to write
58
+ * - `"restore"` — serve start or a lazy restore; reads rebuilding state
59
+ */
60
+ reason: "policy" | "shutdown" | "restore";
61
+ }
62
+ export interface PersistencePolicy {
63
+ /**
64
+ * Batch event-chunk writes on a quiet-period timer instead of flushing
65
+ * once per turn. The debounce **spans turn boundaries** — a rapid
66
+ * multi-turn exchange becomes one write when the session goes quiet —
67
+ * so it is the right choice for chatty sessions where per-turn writes
68
+ * are too many. Unset (default): one event chunk per turn.
69
+ */
70
+ debounceMs?: number;
71
+ /**
72
+ * Guardrails for the **startup bulk restore**. Whatever `list` returns
73
+ * is filtered to these caps before anything is written to local disk, so
74
+ * a large store cannot blow up `--state-root` or stall serve start.
75
+ * Newest sessions (by `updatedAt`) win within each cap.
76
+ *
77
+ * `"off"` disables the startup restore entirely — sessions then restore
78
+ * one at a time as follow-ups actually arrive (lazy-only; recommended
79
+ * for high-traffic deployments).
80
+ */
81
+ restore?: PersistenceRestorePolicy | "off";
82
+ }
83
+ /** Caps applied to the startup bulk restore. See {@link PersistencePolicy.restore}. */
84
+ export interface PersistenceRestorePolicy {
85
+ /** Max sessions materialized (default {@link PERSISTENCE_DEFAULT_RESTORE_MAX_SESSIONS}). */
86
+ maxSessions?: number;
87
+ /**
88
+ * Skip sessions whose `updatedAt` is older than this (default
89
+ * {@link PERSISTENCE_DEFAULT_RESTORE_MAX_AGE_MS}). Older sessions remain
90
+ * reachable lazily on their next follow-up.
91
+ */
92
+ maxAgeMs?: number;
93
+ /**
94
+ * Stop restoring once this many bytes of records + events have been
95
+ * written (default {@link PERSISTENCE_DEFAULT_RESTORE_MAX_TOTAL_BYTES}).
96
+ * Checked before each session is written, so one oversized stream
97
+ * cannot blow past the budget.
98
+ */
99
+ maxTotalBytes?: number;
100
+ }
101
+ export interface PersistenceConfig {
102
+ /** Optional label surfaced on `GET /v1/info` diagnostics. */
103
+ name?: string;
104
+ /** Timing knobs; see {@link PersistencePolicy}. */
105
+ policy?: PersistencePolicy;
106
+ /**
107
+ * Store one value under a key (upsert, last-write-wins). Called on the
108
+ * framework's schedule — never concurrently, always in order. Keep it
109
+ * fast or buffer internally: the delivery queue is bounded, so a sink
110
+ * that falls behind sustained traffic sheds writes (logged) instead of
111
+ * growing memory; it never stalls the agent loop.
112
+ */
113
+ put(key: string, value: JsonValue, ctx: PersistenceContext): void | Promise<void>;
114
+ /** Remove a key. Optional — without it, deletions are skipped. */
115
+ delete?(key: string, ctx: PersistenceContext): void | Promise<void>;
116
+ /**
117
+ * Point lookup. Optional — required for **lazy restore** (resolving a
118
+ * continuation token on a replacement host) and the A/B backfill.
119
+ * Return `undefined`/`null` only for a **definitive** miss: on the lazy
120
+ * restore path a throw propagates and fails the follow-up (retryable) —
121
+ * a store outage must not read as "unknown token", which would fork the
122
+ * conversation onto a new session.
123
+ */
124
+ get?(key: string, ctx: PersistenceContext): JsonValue | undefined | null | Promise<JsonValue | undefined | null>;
125
+ /**
126
+ * All entries under a key prefix, in ascending key order. Optional —
127
+ * required for the **startup bulk restore** (sessions + event streams)
128
+ * and playground eval history.
129
+ */
130
+ list?(prefix: string, ctx: PersistenceContext): PersistenceEntry[] | Promise<PersistenceEntry[]>;
131
+ }
132
+ export type PersistenceDefinition = PersistenceConfig & {
133
+ readonly __agentServe: "persistence";
134
+ };
135
+ /**
136
+ * Author the project persistence sink (`agent/persistence.ts`, default
137
+ * export). See the module doc for semantics and an example.
138
+ */
139
+ export declare function definePersistence(config: PersistenceConfig): PersistenceDefinition;
140
+ /**
141
+ * The framework-owned key scheme. Keys are a **stable, versioned contract**
142
+ * (the `v1/` root): sinks may treat them as opaque strings, or route on
143
+ * prefixes (e.g. event chunks to object storage, everything else to a
144
+ * database). Channel ids and continuation tokens are the only segments
145
+ * that may contain caller-controlled characters; they are URI-encoded.
146
+ *
147
+ * | Key | Value |
148
+ * | --- | --- |
149
+ * | `v1/{agent}/session/{sessionId}` | `SessionRecord` |
150
+ * | `v1/{agent}/session-events/{sessionId}/{index}` | `SessionEvent[]` chunk (index = first event's index, zero-padded) |
151
+ * | `v1/{agent}/continuation/{channelId}/{token}` | `{ sessionId }` |
152
+ * | `v1/{agent}/eval-run/{runId}` | `EvalRunSnapshot` |
153
+ * | `v1/{agent}/ab-sample/{sessionId}/{at}` | `ABMetricSample` |
154
+ * | `v1/{agent}/ab-snapshot` | latest aggregate `ABSnapshot` |
155
+ */
156
+ export declare const persistenceKeys: {
157
+ readonly session: (agent: string, sessionId: string) => string;
158
+ readonly sessionPrefix: (agent: string) => string;
159
+ readonly sessionEvents: (agent: string, sessionId: string, firstIndex: number) => string;
160
+ readonly sessionEventsPrefix: (agent: string, sessionId: string) => string;
161
+ readonly continuation: (agent: string, channelId: string, continuationKey: string) => string;
162
+ readonly evalRun: (agent: string, runId: string) => string;
163
+ readonly evalRunPrefix: (agent: string) => string;
164
+ readonly abSample: (agent: string, sessionId: string, at: string) => string;
165
+ readonly abSnapshot: (agent: string) => string;
166
+ };
167
+ export declare const PERSISTENCE_DEFAULT_RESTORE_MAX_SESSIONS = 1000;
168
+ export declare const PERSISTENCE_DEFAULT_RESTORE_MAX_AGE_MS: number;
169
+ export declare const PERSISTENCE_DEFAULT_RESTORE_MAX_TOTAL_BYTES = 1073741824;
170
+ /** {@link PersistencePolicy} with defaults applied. */
171
+ export interface ResolvedPersistencePolicy {
172
+ /** Event-chunk flush trigger: per turn, or debounced across turns. */
173
+ events: "turnEnd" | {
174
+ debounceMs: number;
175
+ };
176
+ restore: "off" | {
177
+ maxSessions: number;
178
+ maxAgeMs: number;
179
+ maxTotalBytes: number;
180
+ };
181
+ }
182
+ /** Apply {@link PersistencePolicy} defaults (exposed for tooling/tests). */
183
+ export declare function resolvePersistencePolicy(policy: PersistencePolicy | undefined): ResolvedPersistencePolicy;
184
+ //# sourceMappingURL=persistence.d.ts.map