@cursor/july 0.1.39 → 0.1.41
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.
- package/AGENTS.md +4 -3
- package/dist/bin/agent-serve.js +28 -9
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +18 -8
- package/dist/channels/slack/attachments.d.ts +82 -0
- package/dist/channels/slack/attachments.d.ts.map +1 -0
- package/dist/channels/slack/attachments.js +520 -0
- package/dist/channels/slack/cursor-account.d.ts +7 -1
- package/dist/channels/slack/cursor-account.d.ts.map +1 -1
- package/dist/channels/slack/cursor-account.js +2 -2
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +26 -17
- package/dist/channels/slack/dispatch.d.ts +4 -3
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +84 -21
- package/dist/channels/slack/inbound.d.ts +6 -0
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +48 -2
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +16 -4
- package/dist/channels/slack/manifest.d.ts.map +1 -1
- package/dist/channels/slack/manifest.js +1 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +7 -4
- package/dist/channels/slack/types.d.ts +49 -1
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +6 -6
- package/dist/docs/assets/{ab.md.6cLOW7--.js → ab.md.hdxsvX5M.js} +2 -2
- package/dist/docs/assets/{app.BQ8Hihdf.js → app.mIw24pb1.js} +1 -1
- package/dist/docs/assets/{building-with-agents.md.txrcGU2B.js → building-with-agents.md.PeZaZA1P.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BA7V8dEJ.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.hOzpPwrG.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.BtFhUYmh.js} +2 -2
- package/dist/docs/assets/{deployment.md.CuK5SNjN.js → deployment.md.BHVjVqRa.js} +4 -4
- package/dist/docs/assets/{deployment.md.CuK5SNjN.lean.js → deployment.md.BHVjVqRa.lean.js} +1 -1
- package/dist/docs/assets/{evals.md.BQXI3rXy.js → evals.md.qeoGgPw0.js} +3 -3
- package/dist/docs/assets/example-agents_index.md.xmJ87d_3.js +2 -0
- package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.js → guides_webhooks.md.DB-r_er9.js} +47 -4
- package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.lean.js → guides_webhooks.md.DB-r_er9.lean.js} +1 -1
- package/dist/docs/assets/index.md.BKpbkCa6.js +24 -0
- package/dist/docs/assets/{index.md.CmhptOmN.lean.js → index.md.BKpbkCa6.lean.js} +1 -1
- package/dist/docs/assets/{quickstart.md.C_b6ESpD.js → quickstart.md.rIbfahJM.js} +4 -4
- package/dist/docs/assets/{reference_cli.md.Byvrg8eu.js → reference_cli.md.DqEqQpGn.js} +5 -5
- package/dist/docs/assets/{reference_cli.md.Byvrg8eu.lean.js → reference_cli.md.DqEqQpGn.lean.js} +1 -1
- package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.js → reference_hooks.md.BHeG-vjK.js} +2 -2
- package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.lean.js → reference_hooks.md.BHeG-vjK.lean.js} +1 -1
- package/dist/docs/assets/{storage.md.mQDtIULc.js → storage.md.DcGSnIxX.js} +10 -4
- package/dist/docs/assets/{storage.md.mQDtIULc.lean.js → storage.md.DcGSnIxX.lean.js} +1 -1
- package/dist/docs/assets/troubleshooting.md.CkgwmVI0.js +1 -0
- package/dist/docs/assets/{troubleshooting.md.B5RVX_tL.lean.js → troubleshooting.md.CkgwmVI0.lean.js} +1 -1
- package/dist/docs/building-with-agents.html +5 -5
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +7 -7
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +3 -3
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +5 -5
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +3 -3
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +3 -3
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/slack.html +3 -3
- package/dist/docs/guides/webhooks.html +49 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +7 -7
- package/dist/docs/quickstart.html +8 -8
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +8 -8
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +5 -5
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +14 -8
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/internal/cli-ax.d.ts.map +1 -1
- package/dist/internal/cli-ax.js +22 -2
- package/dist/internal/cli-deploy.d.ts +6 -0
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +14 -4
- package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
- package/dist/internal/cli-mcp-oauth.js +3 -1
- package/dist/internal/cli-mcp.js +4 -4
- package/dist/internal/cursor-event-relay.d.ts +45 -0
- package/dist/internal/cursor-event-relay.d.ts.map +1 -1
- package/dist/internal/cursor-event-relay.js +96 -2
- package/dist/internal/deploy-client.js +1 -1
- package/dist/internal/discovery.d.ts.map +1 -1
- package/dist/internal/discovery.js +35 -7
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +3 -1
- package/dist/internal/durable-session-store.d.ts +48 -0
- package/dist/internal/durable-session-store.d.ts.map +1 -0
- package/dist/internal/durable-session-store.js +86 -0
- package/dist/internal/hosting.d.ts +1 -1
- package/dist/internal/hosting.js +2 -2
- package/dist/internal/init-project.d.ts +34 -0
- package/dist/internal/init-project.d.ts.map +1 -1
- package/dist/internal/init-project.js +121 -2
- package/dist/internal/ledger-serving.d.ts +101 -0
- package/dist/internal/ledger-serving.d.ts.map +1 -0
- package/dist/internal/ledger-serving.js +174 -0
- package/dist/internal/listen-port.d.ts +22 -0
- package/dist/internal/listen-port.d.ts.map +1 -0
- package/dist/internal/listen-port.js +77 -0
- package/dist/internal/mcp-oauth.d.ts +12 -1
- package/dist/internal/mcp-oauth.d.ts.map +1 -1
- package/dist/internal/mcp-oauth.js +25 -3
- package/dist/internal/playground/static.d.ts +21 -4
- package/dist/internal/playground/static.d.ts.map +1 -1
- package/dist/internal/playground/static.js +82 -18
- package/dist/internal/sdk-runner.d.ts.map +1 -1
- package/dist/internal/sdk-runner.js +10 -1
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +67 -49
- package/dist/internal/session-engine.d.ts +57 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +117 -28
- package/dist/internal/slack-provision-client.js +1 -1
- package/dist/internal/workspace.d.ts +6 -2
- package/dist/internal/workspace.d.ts.map +1 -1
- package/dist/internal/workspace.js +10 -2
- package/dist/kv/memory-kv.d.ts +23 -0
- package/dist/kv/memory-kv.d.ts.map +1 -0
- package/dist/kv/memory-kv.js +103 -0
- package/dist/kv.d.ts +22 -0
- package/dist/kv.d.ts.map +1 -0
- package/dist/kv.js +20 -0
- package/dist/ledger/session-ledger.d.ts +204 -0
- package/dist/ledger/session-ledger.d.ts.map +1 -0
- package/dist/ledger/session-ledger.js +382 -0
- package/dist/ledger.d.ts +11 -0
- package/dist/ledger.d.ts.map +1 -0
- package/dist/ledger.js +9 -0
- package/dist/playground/assets/index-B3HBneZZ.js +85 -0
- package/dist/playground/assets/index-Bqk9nplh.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/storage-backends/cursor-hosted.d.ts +25 -3
- package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
- package/dist/storage-backends/cursor-hosted.js +50 -2
- package/dist/storage-backends/file-kv.d.ts +5 -2
- package/dist/storage-backends/file-kv.d.ts.map +1 -1
- package/dist/storage-backends/file-kv.js +107 -7
- package/dist/storage.d.ts +60 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +28 -4
- package/dist/types.d.ts +123 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -0
- package/docs/README.md +10 -7
- package/docs/ab.md +2 -2
- package/docs/building-with-agents.md +1 -1
- package/docs/deployment.md +34 -11
- package/docs/evals.md +3 -3
- package/docs/example-agents/index.md +1 -1
- package/docs/guides/webhooks.md +87 -0
- package/docs/quickstart.md +7 -8
- package/docs/reference/cli.md +15 -6
- package/docs/reference/hooks.md +8 -2
- package/docs/storage.md +67 -7
- package/docs/troubleshooting.md +10 -2
- package/package.json +20 -7
- package/skills/create-agent/SKILL.md +1 -1
- package/src/bin/agent-serve.ts +27 -11
- package/src/channels/slack/api.ts +19 -14
- package/src/channels/slack/attachments.ts +635 -0
- package/src/channels/slack/cursor-account.ts +8 -0
- package/src/channels/slack/defaults.ts +34 -18
- package/src/channels/slack/dispatch.ts +121 -6
- package/src/channels/slack/inbound.ts +60 -0
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/live-delivery.ts +17 -3
- package/src/channels/slack/manifest.ts +1 -0
- package/src/channels/slack/slack-channel.ts +7 -0
- package/src/channels/slack/types.ts +47 -1
- package/src/index.ts +9 -0
- package/src/internal/cli-ax.ts +25 -1
- package/src/internal/cli-deploy.ts +22 -4
- package/src/internal/cli-mcp-oauth.ts +3 -1
- package/src/internal/cli-mcp.ts +4 -4
- package/src/internal/cursor-event-relay.ts +168 -0
- package/src/internal/deploy-client.ts +1 -1
- package/src/internal/discovery.ts +42 -5
- package/src/internal/distribution.ts +3 -1
- package/src/internal/durable-session-store.ts +95 -0
- package/src/internal/hosting.ts +2 -2
- package/src/internal/init-project.ts +151 -2
- package/src/internal/ledger-serving.ts +240 -0
- package/src/internal/listen-port.ts +85 -0
- package/src/internal/mcp-oauth.ts +29 -4
- package/src/internal/playground/static.ts +111 -20
- package/src/internal/sdk-runner.ts +11 -1
- package/src/internal/server.ts +76 -62
- package/src/internal/session-engine.ts +192 -16
- package/src/internal/slack-provision-client.ts +1 -1
- package/src/internal/workspace.ts +14 -3
- package/src/kv/memory-kv.ts +99 -0
- package/src/kv.ts +27 -0
- package/src/ledger/session-ledger.ts +535 -0
- package/src/ledger.ts +29 -0
- package/src/storage-backends/cursor-hosted.ts +108 -3
- package/src/storage-backends/file-kv.ts +128 -12
- package/src/storage.ts +90 -6
- package/src/types.ts +130 -4
- package/dist/channels/slack/post-update-delivery.d.ts +0 -85
- package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.B-VTH4As.js +0 -1
- package/dist/docs/assets/example-agents_index.md.DgGBwckv.js +0 -2
- package/dist/docs/assets/index.md.CmhptOmN.js +0 -24
- package/dist/docs/assets/troubleshooting.md.B5RVX_tL.js +0 -1
- package/dist/internal/json-dir-store.d.ts +0 -32
- package/dist/internal/json-dir-store.d.ts.map +0 -1
- package/dist/internal/persistence-coordinator.d.ts +0 -127
- package/dist/internal/persistence-coordinator.d.ts.map +0 -1
- package/dist/multi-tenant.d.ts +0 -80
- package/dist/multi-tenant.d.ts.map +0 -1
- package/dist/multi-tenant.js +0 -69
- package/dist/persistence.d.ts +0 -184
- package/dist/persistence.d.ts.map +0 -1
- package/dist/playground/assets/index-BRhBbnd4.css +0 -1
- package/dist/playground/assets/index-_2WKw9a-.js +0 -85
- package/dist/storage-backends/postgres-kv.d.ts +0 -82
- package/dist/storage-backends/postgres-kv.d.ts.map +0 -1
- package/dist/storage-backends/postgres-kv.js +0 -185
- package/src/storage-backends/postgres-kv.ts +0 -253
- /package/dist/docs/assets/{ab.md.6cLOW7--.lean.js → ab.md.hdxsvX5M.lean.js} +0 -0
- /package/dist/docs/assets/{building-with-agents.md.txrcGU2B.lean.js → building-with-agents.md.PeZaZA1P.lean.js} +0 -0
- /package/dist/docs/assets/{evals.md.BQXI3rXy.lean.js → evals.md.qeoGgPw0.lean.js} +0 -0
- /package/dist/docs/assets/{example-agents_index.md.DgGBwckv.lean.js → example-agents_index.md.xmJ87d_3.lean.js} +0 -0
- /package/dist/docs/assets/{quickstart.md.C_b6ESpD.lean.js → quickstart.md.rIbfahJM.lean.js} +0 -0
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
* On Cursor-managed hosting the start script pins `CURSOR_API_BASE_URL` to
|
|
11
11
|
* `https://api.cursor.com` (where the proxy is mounted). Locally, point
|
|
12
12
|
* `CURSOR_API_BASE_URL` at a backend that mounts Agent Serve routes and use
|
|
13
|
-
* a deployment SA key, or use `@cursor/july/storage/
|
|
14
|
-
* `file-kv` instead.
|
|
13
|
+
* a deployment SA key, or use `@cursor/july/storage/file-kv` instead.
|
|
15
14
|
*
|
|
16
15
|
* ```ts
|
|
17
16
|
* import { defineStorage } from "@cursor/july/storage";
|
|
@@ -27,11 +26,12 @@ import type { ABSnapshot } from "../ab.js";
|
|
|
27
26
|
import type { EvalRunSnapshot } from "../evals.js";
|
|
28
27
|
import type {
|
|
29
28
|
StorageABTable,
|
|
29
|
+
StorageCasTable,
|
|
30
30
|
StorageConfig,
|
|
31
31
|
StorageEntry,
|
|
32
32
|
StorageEvalsTable,
|
|
33
33
|
} from "../storage.js";
|
|
34
|
-
import type { JsonValue } from "../types.js";
|
|
34
|
+
import type { JsonValue, KvCasResult, KvVersionedEntry } from "../types.js";
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Same default as {@link cursorExternalApiUrl} in the CLI credentials module,
|
|
@@ -103,10 +103,75 @@ type CursorHostedStorageMethods = Required<
|
|
|
103
103
|
Pick<StorageConfig, "put" | "get" | "delete" | "list">
|
|
104
104
|
> & {
|
|
105
105
|
readonly [CURSOR_HOSTED_STORAGE]: true;
|
|
106
|
+
cas: StorageCasTable;
|
|
106
107
|
evals: StorageEvalsTable;
|
|
107
108
|
abs: StorageABTable;
|
|
108
109
|
};
|
|
109
110
|
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// CAS wire protocol
|
|
113
|
+
// ============================================================================
|
|
114
|
+
//
|
|
115
|
+
// Served by the backend Agent Serve storage proxy
|
|
116
|
+
// (`backend/server/src/agentServe/agentServeHostedStorage.ts`, routes in
|
|
117
|
+
// `agentServeEndpoints.ts`) over `agent_serve_kv` and its `version BIGINT`
|
|
118
|
+
// column. Each call is atomic server-side:
|
|
119
|
+
//
|
|
120
|
+
// - `POST /internal/agent-serve/storage/kv/get-with-version`
|
|
121
|
+
// `{ key }` → `{ found, value, version }` ({@link KvGetWithVersionResponse}).
|
|
122
|
+
// - `POST /internal/agent-serve/storage/kv/put-if-absent`
|
|
123
|
+
// `{ key, value }` → `{ ok, version, current }` ({@link KvCasResponse});
|
|
124
|
+
// plain INSERT, unique-violation conflict returns the existing row as
|
|
125
|
+
// `current`.
|
|
126
|
+
// - `POST /internal/agent-serve/storage/kv/put-if-version`
|
|
127
|
+
// `{ key, value, expectedVersion }` → `{ ok, version, current }`;
|
|
128
|
+
// one conditional `UPDATE … SET value, version = version + 1 WHERE key
|
|
129
|
+
// AND version = expected`, conflict returns the current row (or
|
|
130
|
+
// `current: null` when the row is gone).
|
|
131
|
+
// - `POST /internal/agent-serve/storage/kv/list-keys`
|
|
132
|
+
// `{ prefix }` → `{ keys }` ({@link KvListKeysResponse}) in ascending
|
|
133
|
+
// key order (prefix scan on the `(deployment_id, key)` primary key).
|
|
134
|
+
|
|
135
|
+
/** Wire shape of the `get-with-version` route. `null` ⇒ JSON `undefined`. */
|
|
136
|
+
export interface KvGetWithVersionResponse {
|
|
137
|
+
found: boolean;
|
|
138
|
+
value: JsonValue | null;
|
|
139
|
+
version: string | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Wire shape of the `put-if-absent` / `put-if-version` routes. */
|
|
143
|
+
export interface KvCasResponse {
|
|
144
|
+
ok: boolean;
|
|
145
|
+
/** New version on success, else `null`. */
|
|
146
|
+
version: string | null;
|
|
147
|
+
/** Entry observed on conflict; `null` when the key is absent. */
|
|
148
|
+
current: { value: JsonValue | null; version: string } | null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Wire shape of the `list-keys` route. */
|
|
152
|
+
export interface KvListKeysResponse {
|
|
153
|
+
keys: string[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function toCasResult(response: KvCasResponse): KvCasResult {
|
|
157
|
+
if (response.ok) {
|
|
158
|
+
if (response.version === null) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"cursorHostedStorage: CAS success without a version token"
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, version: response.version };
|
|
164
|
+
}
|
|
165
|
+
const current: KvVersionedEntry | undefined =
|
|
166
|
+
response.current === null
|
|
167
|
+
? undefined
|
|
168
|
+
: {
|
|
169
|
+
value: response.current.value ?? null,
|
|
170
|
+
version: response.current.version,
|
|
171
|
+
};
|
|
172
|
+
return { ok: false, current };
|
|
173
|
+
}
|
|
174
|
+
|
|
110
175
|
function resolveApiKey(options: CursorHostedStorageOptions): string {
|
|
111
176
|
const key = (options.apiKey ?? process.env.CURSOR_API_KEY)?.trim();
|
|
112
177
|
if (key === undefined || key === "") {
|
|
@@ -203,6 +268,46 @@ export function cursorHostedStorage(
|
|
|
203
268
|
}>(options, "/internal/agent-serve/storage/kv/list", { prefix });
|
|
204
269
|
return result.entries;
|
|
205
270
|
},
|
|
271
|
+
// Conditional writes over the same proxy (server routes: see the CAS
|
|
272
|
+
// wire-protocol block above). The proxy scopes rows from the service
|
|
273
|
+
// account, so ctx is not part of the wire contract.
|
|
274
|
+
cas: {
|
|
275
|
+
async getWithVersion(key): Promise<KvVersionedEntry | undefined> {
|
|
276
|
+
const result = await storageRpc<KvGetWithVersionResponse>(
|
|
277
|
+
options,
|
|
278
|
+
"/internal/agent-serve/storage/kv/get-with-version",
|
|
279
|
+
{ key }
|
|
280
|
+
);
|
|
281
|
+
if (!result.found || result.version === null) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
return { value: result.value ?? null, version: result.version };
|
|
285
|
+
},
|
|
286
|
+
async putIfAbsent(key, value): Promise<KvCasResult> {
|
|
287
|
+
const result = await storageRpc<KvCasResponse>(
|
|
288
|
+
options,
|
|
289
|
+
"/internal/agent-serve/storage/kv/put-if-absent",
|
|
290
|
+
{ key, value }
|
|
291
|
+
);
|
|
292
|
+
return toCasResult(result);
|
|
293
|
+
},
|
|
294
|
+
async putIfVersion(key, value, expectedVersion): Promise<KvCasResult> {
|
|
295
|
+
const result = await storageRpc<KvCasResponse>(
|
|
296
|
+
options,
|
|
297
|
+
"/internal/agent-serve/storage/kv/put-if-version",
|
|
298
|
+
{ key, value, expectedVersion }
|
|
299
|
+
);
|
|
300
|
+
return toCasResult(result);
|
|
301
|
+
},
|
|
302
|
+
async listKeys(prefix): Promise<string[]> {
|
|
303
|
+
const result = await storageRpc<KvListKeysResponse>(
|
|
304
|
+
options,
|
|
305
|
+
"/internal/agent-serve/storage/kv/list-keys",
|
|
306
|
+
{ prefix }
|
|
307
|
+
);
|
|
308
|
+
return result.keys;
|
|
309
|
+
},
|
|
310
|
+
},
|
|
206
311
|
evals: {
|
|
207
312
|
async put(run, ctx): Promise<void> {
|
|
208
313
|
await storageRpc(options, "/internal/agent-serve/storage/evals/put", {
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Implements the whole `defineStorage` surface with zero external
|
|
6
6
|
* dependencies: the four KV methods against a flat directory of JSON files
|
|
7
|
-
* (one file per key, filename = URI-encoded key),
|
|
7
|
+
* (one file per key, filename = URI-encoded key), the `cas`
|
|
8
|
+
* group (single-process conditional writes — see the `cas` member below),
|
|
9
|
+
* eval runs as
|
|
8
10
|
* `eval-runs/<agent>/<runId>.json`, and A/B metrics as
|
|
9
11
|
* `ab/<agent>/samples.ndjson` (append-only) plus `ab/<agent>/snapshot.json`
|
|
10
12
|
* (latest aggregate). KV keys carry the agent name inside the framework
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* throw correctly fails the follow-up instead of forking the session.
|
|
26
28
|
*/
|
|
27
29
|
|
|
28
|
-
import { randomUUID } from "node:crypto";
|
|
30
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
29
31
|
import {
|
|
30
32
|
appendFile,
|
|
31
33
|
mkdir,
|
|
@@ -35,17 +37,18 @@ import {
|
|
|
35
37
|
rm,
|
|
36
38
|
writeFile,
|
|
37
39
|
} from "node:fs/promises";
|
|
38
|
-
import { join } from "node:path";
|
|
40
|
+
import { isAbsolute, join } from "node:path";
|
|
39
41
|
import type { ABSnapshot } from "../ab.js";
|
|
40
42
|
import type { EvalRunSnapshot } from "../evals.js";
|
|
41
43
|
import type {
|
|
42
44
|
StorageABTable,
|
|
45
|
+
StorageCasTable,
|
|
43
46
|
StorageConfig,
|
|
44
47
|
StorageContext,
|
|
45
48
|
StorageEntry,
|
|
46
49
|
StorageEvalsTable,
|
|
47
50
|
} from "../storage.js";
|
|
48
|
-
import type { JsonValue } from "../types.js";
|
|
51
|
+
import type { JsonValue, KvVersionedEntry } from "../types.js";
|
|
49
52
|
|
|
50
53
|
export interface FileKvOptions {
|
|
51
54
|
/**
|
|
@@ -58,14 +61,17 @@ export interface FileKvOptions {
|
|
|
58
61
|
type FileKvMethods = Required<
|
|
59
62
|
Pick<StorageConfig, "put" | "get" | "delete" | "list">
|
|
60
63
|
> & {
|
|
64
|
+
cas: StorageCasTable;
|
|
61
65
|
evals: StorageEvalsTable;
|
|
62
66
|
abs: StorageABTable;
|
|
63
67
|
};
|
|
64
68
|
|
|
65
69
|
/** File-backed `defineStorage` methods, ready to spread into `defineStorage({...})`. */
|
|
66
70
|
export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
67
|
-
const resolveDir = (ctx: StorageContext): string =>
|
|
68
|
-
|
|
71
|
+
const resolveDir = (ctx: StorageContext): string => {
|
|
72
|
+
const dir = options?.dir ?? ".agent-serve/kv";
|
|
73
|
+
return isAbsolute(dir) ? dir : join(ctx.projectRoot, dir);
|
|
74
|
+
};
|
|
69
75
|
// The evals/abs tables are scoped per agent (KV keys already carry the
|
|
70
76
|
// agent name via the framework key scheme).
|
|
71
77
|
const evalRunsDir = (ctx: StorageContext): string =>
|
|
@@ -74,14 +80,12 @@ export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
|
74
80
|
join(resolveDir(ctx), "ab", encodeURIComponent(ctx.agentName));
|
|
75
81
|
const filePath = (ctx: StorageContext, key: string): string =>
|
|
76
82
|
join(resolveDir(ctx), `${encodeURIComponent(key)}.json`);
|
|
77
|
-
const
|
|
83
|
+
const readRaw = async (
|
|
78
84
|
ctx: StorageContext,
|
|
79
85
|
key: string
|
|
80
|
-
): Promise<
|
|
86
|
+
): Promise<string | undefined> => {
|
|
81
87
|
try {
|
|
82
|
-
return
|
|
83
|
-
await readFile(filePath(ctx, key), "utf8")
|
|
84
|
-
) as JsonValue;
|
|
88
|
+
return await readFile(filePath(ctx, key), "utf8");
|
|
85
89
|
} catch (error) {
|
|
86
90
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
87
91
|
return undefined; // definitive miss — key was never written
|
|
@@ -89,6 +93,37 @@ export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
|
89
93
|
throw error; // real failure: let lazy restore fail the follow-up
|
|
90
94
|
}
|
|
91
95
|
};
|
|
96
|
+
const readKey = async (
|
|
97
|
+
ctx: StorageContext,
|
|
98
|
+
key: string
|
|
99
|
+
): Promise<JsonValue | undefined> => {
|
|
100
|
+
const text = await readRaw(ctx, key);
|
|
101
|
+
return text === undefined ? undefined : (JSON.parse(text) as JsonValue);
|
|
102
|
+
};
|
|
103
|
+
const readVersioned = async (
|
|
104
|
+
ctx: StorageContext,
|
|
105
|
+
key: string
|
|
106
|
+
): Promise<KvVersionedEntry | undefined> => {
|
|
107
|
+
const text = await readRaw(ctx, key);
|
|
108
|
+
if (text === undefined) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return { value: JSON.parse(text) as JsonValue, version: versionOf(text) };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// In-process mutex: CAS check-then-write pairs queue on one promise
|
|
115
|
+
// chain so they cannot interleave with each other. Plain `put` stays
|
|
116
|
+
// outside the chain on purpose — it is unconditional LWW and always
|
|
117
|
+
// wins the key, like a SQL upsert would win the row.
|
|
118
|
+
let casChain: Promise<unknown> = Promise.resolve();
|
|
119
|
+
const locked = <T>(task: () => Promise<T>): Promise<T> => {
|
|
120
|
+
const result = casChain.then(task, task);
|
|
121
|
+
casChain = result.then(
|
|
122
|
+
() => undefined,
|
|
123
|
+
() => undefined
|
|
124
|
+
);
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
92
127
|
|
|
93
128
|
return {
|
|
94
129
|
async put(key, value, ctx): Promise<void> {
|
|
@@ -132,6 +167,69 @@ export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
|
132
167
|
return entries;
|
|
133
168
|
},
|
|
134
169
|
|
|
170
|
+
// Conditional writes over the same raw value files — no on-disk format
|
|
171
|
+
// change, so data written before the CAS group existed stays readable
|
|
172
|
+
// and plain `put` stays byte-identical. The version token is a content
|
|
173
|
+
// hash of the stored JSON text: it changes whenever the value does,
|
|
174
|
+
// and the in-process mutex makes read-check-write atomic, so a token
|
|
175
|
+
// can never go stale between the check and the rename. The one caveat
|
|
176
|
+
// of hash versions — ABA when a value returns to identical bytes — is
|
|
177
|
+
// moot for coordination values that carry a monotonic counter. CAS
|
|
178
|
+
// correctness is **single-process only**: two pods sharing this
|
|
179
|
+
// directory would race between read and rename, so a file store is
|
|
180
|
+
// only safe for single-instance deployments.
|
|
181
|
+
cas: {
|
|
182
|
+
getWithVersion: (key, ctx): Promise<KvVersionedEntry | undefined> =>
|
|
183
|
+
readVersioned(ctx, key),
|
|
184
|
+
|
|
185
|
+
putIfAbsent: (key, value, ctx) =>
|
|
186
|
+
locked(async () => {
|
|
187
|
+
const current = await readVersioned(ctx, key);
|
|
188
|
+
if (current !== undefined) {
|
|
189
|
+
return { ok: false, current };
|
|
190
|
+
}
|
|
191
|
+
const text = JSON.stringify(value);
|
|
192
|
+
await writeTextAtomic({
|
|
193
|
+
dir: resolveDir(ctx),
|
|
194
|
+
fileName: `${encodeURIComponent(key)}.json`,
|
|
195
|
+
text,
|
|
196
|
+
});
|
|
197
|
+
return { ok: true, version: versionOf(text) };
|
|
198
|
+
}),
|
|
199
|
+
|
|
200
|
+
putIfVersion: (key, value, expectedVersion, ctx) =>
|
|
201
|
+
locked(async () => {
|
|
202
|
+
const current = await readVersioned(ctx, key);
|
|
203
|
+
if (current === undefined || current.version !== expectedVersion) {
|
|
204
|
+
return { ok: false, current };
|
|
205
|
+
}
|
|
206
|
+
const text = JSON.stringify(value);
|
|
207
|
+
await writeTextAtomic({
|
|
208
|
+
dir: resolveDir(ctx),
|
|
209
|
+
fileName: `${encodeURIComponent(key)}.json`,
|
|
210
|
+
text,
|
|
211
|
+
});
|
|
212
|
+
return { ok: true, version: versionOf(text) };
|
|
213
|
+
}),
|
|
214
|
+
|
|
215
|
+
async listKeys(prefix, ctx): Promise<string[]> {
|
|
216
|
+
let names: string[];
|
|
217
|
+
try {
|
|
218
|
+
names = await readdir(resolveDir(ctx));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return names
|
|
226
|
+
.filter((name) => name.endsWith(".json"))
|
|
227
|
+
.map((name) => decodeURIComponent(name.slice(0, -".json".length)))
|
|
228
|
+
.filter((key) => key.startsWith(prefix))
|
|
229
|
+
.sort();
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
|
|
135
233
|
// Playground eval batches: one JSON file per run under eval-runs/<agent>/.
|
|
136
234
|
evals: {
|
|
137
235
|
async put(run, ctx): Promise<void> {
|
|
@@ -208,14 +306,32 @@ export function fileKv(options?: FileKvOptions): FileKvMethods {
|
|
|
208
306
|
};
|
|
209
307
|
}
|
|
210
308
|
|
|
309
|
+
/** Content-hash version token for a stored JSON text. */
|
|
310
|
+
function versionOf(text: string): string {
|
|
311
|
+
return `sha256:${createHash("sha256").update(text, "utf8").digest("hex")}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
211
314
|
/** tmp + rename write so concurrent readers never see a torn file. */
|
|
212
315
|
async function writeJsonAtomic(args: {
|
|
213
316
|
dir: string;
|
|
214
317
|
fileName: string;
|
|
215
318
|
value: unknown;
|
|
319
|
+
}): Promise<void> {
|
|
320
|
+
await writeTextAtomic({
|
|
321
|
+
dir: args.dir,
|
|
322
|
+
fileName: args.fileName,
|
|
323
|
+
text: JSON.stringify(args.value),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** tmp + rename write of exact text (CAS hashes the same bytes it wrote). */
|
|
328
|
+
async function writeTextAtomic(args: {
|
|
329
|
+
dir: string;
|
|
330
|
+
fileName: string;
|
|
331
|
+
text: string;
|
|
216
332
|
}): Promise<void> {
|
|
217
333
|
await mkdir(args.dir, { recursive: true });
|
|
218
334
|
const tmp = join(args.dir, `.tmp-${randomUUID()}`);
|
|
219
|
-
await writeFile(tmp,
|
|
335
|
+
await writeFile(tmp, args.text, "utf8");
|
|
220
336
|
await rename(tmp, join(args.dir, args.fileName));
|
|
221
337
|
}
|
package/src/storage.ts
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
* pipeline, …). Without it, state lives under `--state-root` on local disk
|
|
7
7
|
* only, and eval/A/B history stays in process memory.
|
|
8
8
|
*
|
|
9
|
-
* The sink is a plain key-value store —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The sink is a plain key-value store — put/get/delete/list, no schema —
|
|
10
|
+
* plus a conditional-write group ({@link StorageConfig.cas}, the substrate
|
|
11
|
+
* for framework coordination; optional for plain storage, required by
|
|
12
|
+
* coordination features) and two optional dedicated tables
|
|
13
|
+
* ({@link StorageConfig.evals} and {@link StorageConfig.abs}) for
|
|
14
|
+
* eval-run and A/B history:
|
|
12
15
|
*
|
|
13
16
|
* ```ts
|
|
14
17
|
* import { defineStorage } from "@anysphere/agent-serve/storage";
|
|
@@ -18,6 +21,7 @@
|
|
|
18
21
|
* get: (key) => db.get(key),
|
|
19
22
|
* delete: (key) => db.delete(key),
|
|
20
23
|
* list: (prefix) => db.listByPrefix(prefix), // [{ key, value }] in key order
|
|
24
|
+
* cas: { getWithVersion: ..., putIfAbsent: ..., putIfVersion: ..., listKeys: ... },
|
|
21
25
|
* evals: { put: ..., delete: ..., list: ... }, // eval-runs table
|
|
22
26
|
* abs: { putSample: ... }, // A/B table
|
|
23
27
|
* });
|
|
@@ -50,7 +54,7 @@ import type { ABMetricSample } from "./ab.js";
|
|
|
50
54
|
import type { EvalRunSnapshot } from "./evals.js";
|
|
51
55
|
import type { ABSnapshot } from "./internal/ab-snapshot.js";
|
|
52
56
|
import { brandDefinition } from "./internal/brand.js";
|
|
53
|
-
import type { JsonValue } from "./types.js";
|
|
57
|
+
import type { JsonValue, KvCasResult, KvVersionedEntry } from "./types.js";
|
|
54
58
|
|
|
55
59
|
// ============================================================================
|
|
56
60
|
// Sink interface
|
|
@@ -73,8 +77,10 @@ export interface StorageContext {
|
|
|
73
77
|
* - `"policy"` — a flush trigger fired (turn end, debounce, change)
|
|
74
78
|
* - `"shutdown"` — the serve process is draining; last chance to write
|
|
75
79
|
* - `"restore"` — serve start or a lazy restore; reads rebuilding state
|
|
80
|
+
* - `"coordination"` — framework coordination driving
|
|
81
|
+
* {@link StorageCasTable} (conditional writes)
|
|
76
82
|
*/
|
|
77
|
-
reason: "policy" | "shutdown" | "restore";
|
|
83
|
+
reason: "policy" | "shutdown" | "restore" | "coordination";
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
export interface StoragePolicy {
|
|
@@ -151,6 +157,50 @@ export interface StorageABTable {
|
|
|
151
157
|
): ABSnapshot | undefined | null | Promise<ABSnapshot | undefined | null>;
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Conditional writes (CAS) — the coordination tier of the storage backend
|
|
162
|
+
* contract. Plain storage works without it, so existing backends keep
|
|
163
|
+
* working across a platform upgrade; framework coordination (the upcoming
|
|
164
|
+
* session ledger — see `@cursor/july/kv` for the backend-agnostic
|
|
165
|
+
* interface) requires it and fails at startup, with an actionable
|
|
166
|
+
* message, when the backend lacks it. The built-in backends all include
|
|
167
|
+
* it, so implementing this group yourself only comes up for BYO storage.
|
|
168
|
+
*
|
|
169
|
+
* Grouped under {@link StorageConfig.cas} rather than as loose methods —
|
|
170
|
+
* matching the `evals` / `abs` table groups — so `defineStorage`
|
|
171
|
+
* validates the whole group at once: there is no meaningful
|
|
172
|
+
* half-implementation.
|
|
173
|
+
*
|
|
174
|
+
* Version tokens are opaque per-key strings that change on every
|
|
175
|
+
* successful write (counter, UUID, content hash — whatever is cheap for
|
|
176
|
+
* the backend). See {@link KvVersionedEntry} / {@link KvCasResult}.
|
|
177
|
+
*/
|
|
178
|
+
export interface StorageCasTable {
|
|
179
|
+
/** Read one value with its version token. Miss → `undefined`. */
|
|
180
|
+
getWithVersion(
|
|
181
|
+
key: string,
|
|
182
|
+
ctx: StorageContext
|
|
183
|
+
): Promise<KvVersionedEntry | undefined> | KvVersionedEntry | undefined;
|
|
184
|
+
/** Create the key only when absent. Conflict returns the existing entry. */
|
|
185
|
+
putIfAbsent(
|
|
186
|
+
key: string,
|
|
187
|
+
value: JsonValue,
|
|
188
|
+
ctx: StorageContext
|
|
189
|
+
): Promise<KvCasResult> | KvCasResult;
|
|
190
|
+
/**
|
|
191
|
+
* Replace the value only when the key still carries `expectedVersion`.
|
|
192
|
+
* Conflict (version moved, or key deleted) returns the current entry.
|
|
193
|
+
*/
|
|
194
|
+
putIfVersion(
|
|
195
|
+
key: string,
|
|
196
|
+
value: JsonValue,
|
|
197
|
+
expectedVersion: string,
|
|
198
|
+
ctx: StorageContext
|
|
199
|
+
): Promise<KvCasResult> | KvCasResult;
|
|
200
|
+
/** All keys under a prefix, in ascending key order. */
|
|
201
|
+
listKeys(prefix: string, ctx: StorageContext): Promise<string[]> | string[];
|
|
202
|
+
}
|
|
203
|
+
|
|
154
204
|
export interface StorageConfig {
|
|
155
205
|
/** Optional label surfaced on `GET /v1/info` diagnostics. */
|
|
156
206
|
name?: string;
|
|
@@ -166,6 +216,14 @@ export interface StorageConfig {
|
|
|
166
216
|
* (session event logs remain the assignment/fold source of truth).
|
|
167
217
|
*/
|
|
168
218
|
abs?: StorageABTable;
|
|
219
|
+
/**
|
|
220
|
+
* Conditional writes (CAS). Optional for plain storage — existing
|
|
221
|
+
* backends keep working without it — but required by coordination
|
|
222
|
+
* features (the session ledger), which fail fast when it is absent.
|
|
223
|
+
* The built-in backends (`fileKv`, `cursorHostedStorage`) both include
|
|
224
|
+
* it. See {@link StorageCasTable}.
|
|
225
|
+
*/
|
|
226
|
+
cas?: StorageCasTable;
|
|
169
227
|
/**
|
|
170
228
|
* Store one value under a key (upsert, last-write-wins). Called on the
|
|
171
229
|
* framework's schedule — never concurrently, always in order. Keep it
|
|
@@ -228,6 +286,29 @@ export function defineStorage(config: StorageConfig): StorageDefinition {
|
|
|
228
286
|
}
|
|
229
287
|
}
|
|
230
288
|
}
|
|
289
|
+
if (config.cas !== undefined) {
|
|
290
|
+
const casHooks = [
|
|
291
|
+
"getWithVersion",
|
|
292
|
+
"putIfAbsent",
|
|
293
|
+
"putIfVersion",
|
|
294
|
+
"listKeys",
|
|
295
|
+
] as const;
|
|
296
|
+
const cas = config.cas;
|
|
297
|
+
// A partial group is a bug, not a legacy config: half-implemented CAS
|
|
298
|
+
// would pass the capability probe and corrupt coordination.
|
|
299
|
+
const missingCasHooks = casHooks.filter(
|
|
300
|
+
(hook) => typeof cas[hook] !== "function"
|
|
301
|
+
);
|
|
302
|
+
if (missingCasHooks.length > 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`defineStorage: ${missingCasHooks
|
|
305
|
+
.map((hook) => `cas.${hook}`)
|
|
306
|
+
.join(
|
|
307
|
+
", "
|
|
308
|
+
)} must be ${missingCasHooks.length === 1 ? "a function" : "functions"} when the cas group is set`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
231
312
|
if (config.abs !== undefined) {
|
|
232
313
|
if (typeof config.abs.putSample !== "function") {
|
|
233
314
|
throw new Error(
|
|
@@ -321,8 +402,11 @@ export const MAX_KEY_SEGMENT_BYTES = 256;
|
|
|
321
402
|
* digest of the raw value when the encoding exceeds
|
|
322
403
|
* {@link MAX_KEY_SEGMENT_BYTES}. Deterministic, so key construction on the
|
|
323
404
|
* write path and the continuation-lookup path always agree.
|
|
405
|
+
*
|
|
406
|
+
* Exported for the session ledger, which builds its `session/` and
|
|
407
|
+
* `ledger/` keys with the same encoding rules.
|
|
324
408
|
*/
|
|
325
|
-
function keySegment(raw: string): string {
|
|
409
|
+
export function keySegment(raw: string): string {
|
|
326
410
|
const encoded = encodeURIComponent(raw);
|
|
327
411
|
if (Buffer.byteLength(encoded, "utf8") <= MAX_KEY_SEGMENT_BYTES) {
|
|
328
412
|
return encoded;
|