@stigmer/runner 3.15.2 → 3.15.3-dev.20260914121148
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/dist/.build-fingerprint +1 -1
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +12 -9
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +5 -2
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +5 -2
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +11 -27
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +13 -10
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +70 -26
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +82 -32
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
- package/dist/activities/execute-cursor/adapter.d.ts +4 -3
- package/dist/activities/execute-cursor/adapter.js +9 -5
- package/dist/activities/execute-cursor/adapter.js.map +1 -1
- package/dist/activities/execute-cursor/sdk-warmup.d.ts +15 -6
- package/dist/activities/execute-cursor/sdk-warmup.js +15 -6
- package/dist/activities/execute-cursor/sdk-warmup.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +38 -59
- package/dist/activities/execute-cursor/session-lifecycle.js +59 -96
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/session-store.d.ts +100 -0
- package/dist/activities/execute-cursor/session-store.js +157 -0
- package/dist/activities/execute-cursor/session-store.js.map +1 -0
- package/dist/shared/attachment-vision.d.ts +2 -1
- package/dist/shared/attachment-vision.js.map +1 -1
- package/package.json +5 -5
- package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +31 -7
- package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +10 -8
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +5 -2
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +20 -28
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +94 -38
- package/src/activities/execute-cursor/__tests__/cursor-baseurl-routing.test.ts +10 -8
- package/src/activities/execute-cursor/__tests__/cursor-fetch-interceptor-bypass.test.ts +5 -4
- package/src/activities/execute-cursor/__tests__/cursor-generate-image-live.test.ts +7 -2
- package/src/activities/execute-cursor/__tests__/cursor-hook-protocol-live.test.ts +163 -0
- package/src/activities/execute-cursor/__tests__/cursor-sdk-auth-smoke.test.ts +13 -12
- package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +3 -0
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +3 -0
- package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +3 -0
- package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +4 -1
- package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +3 -0
- package/src/activities/execute-cursor/__tests__/session-lifecycle.test.ts +86 -58
- package/src/activities/execute-cursor/__tests__/session-store.test.ts +220 -0
- package/src/activities/execute-cursor/adapter.ts +9 -5
- package/src/activities/execute-cursor/sdk-warmup.ts +15 -6
- package/src/activities/execute-cursor/session-lifecycle.ts +63 -109
- package/src/activities/execute-cursor/session-store.ts +187 -0
- package/src/shared/attachment-vision.ts +2 -1
|
@@ -9,17 +9,20 @@
|
|
|
9
9
|
* - `nextRunId` names the run the next send will be given, so a script can be
|
|
10
10
|
* built with matching event ids before the send happens.
|
|
11
11
|
* - `agentForWorkspaceRef` hands `Agent.create` the agent declared for the
|
|
12
|
-
* session the create names (via `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* list as before.
|
|
12
|
+
* session the create names (via the `workspaceRef` of the scripted store it
|
|
13
|
+
* passes as `local.store`), consulted before the ordered list and
|
|
14
|
+
* registered for `Agent.resume` by id; a create with no store, or a session
|
|
15
|
+
* the resolver does not know, falls through to the list as before.
|
|
16
|
+
* - `ScriptedLocalAgentStore.open` (the `@cursor/sdk/sqlite` double) records
|
|
17
|
+
* every open and dispose on the bound SDK, so a scenario can assert the
|
|
18
|
+
* adapter's session-store lifetime.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { describe, it, expect } from "vitest";
|
|
19
22
|
import type { AgentOptions, SDKMessage } from "@cursor/sdk";
|
|
20
23
|
|
|
21
24
|
import { ScriptedCursorAgent, defaultRunId, sdkEvents, step } from "../scripted-agent.js";
|
|
22
|
-
import { ScriptedCursorSdk } from "../scripted-sdk.js";
|
|
25
|
+
import { ScriptedCursorSdk, ScriptedLocalAgentStore, bindScriptedSdk } from "../scripted-sdk.js";
|
|
23
26
|
|
|
24
27
|
/** An agent whose declared turns each play the given events and finish. */
|
|
25
28
|
function agent(id: string, turns: SDKMessage[][] = []): ScriptedCursorAgent {
|
|
@@ -76,8 +79,14 @@ describe("ScriptedCursorAgent.nextRunId", () => {
|
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
describe("ScriptedCursorSdk.agentForWorkspaceRef", () => {
|
|
82
|
+
// The scripted store carries only the members the runner touches, not the
|
|
83
|
+
// SDK's whole `LocalAgentStore`, hence the cast — the same one the mocked
|
|
84
|
+
// module makes when it stands in for `@cursor/sdk/sqlite`.
|
|
79
85
|
const createFor = (workspaceRef: string | undefined): AgentOptions =>
|
|
80
|
-
({
|
|
86
|
+
({
|
|
87
|
+
apiKey: "k",
|
|
88
|
+
local: workspaceRef !== undefined ? { store: new ScriptedLocalAgentStore(workspaceRef, "/tmp/x") } : undefined,
|
|
89
|
+
}) as unknown as AgentOptions;
|
|
81
90
|
|
|
82
91
|
it("hands out the session's agent before the ordered list and registers it for resume", () => {
|
|
83
92
|
const forSession = agent("session-agent");
|
|
@@ -94,9 +103,24 @@ describe("ScriptedCursorSdk.agentForWorkspaceRef", () => {
|
|
|
94
103
|
expect(sdk.resolutions.map((r) => `${r.kind}:${r.agentId}`)).toEqual(["create:session-agent", "resume:session-agent", "create:listed-agent"]);
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
it("a create without a
|
|
106
|
+
it("a create without a store falls through to the ordered list", () => {
|
|
98
107
|
const listed = agent("listed-only");
|
|
99
108
|
const sdk = new ScriptedCursorSdk({ agents: [listed], catalog: [], agentForWorkspaceRef: () => agent("never") });
|
|
100
109
|
expect(sdk.create(createFor(undefined))).toBe(listed);
|
|
101
110
|
});
|
|
102
111
|
});
|
|
112
|
+
|
|
113
|
+
describe("ScriptedLocalAgentStore (the @cursor/sdk/sqlite double)", () => {
|
|
114
|
+
it("open hands out a store carrying the ref and root asked for, and records opens and disposes on the bound SDK", async () => {
|
|
115
|
+
const sdk = new ScriptedCursorSdk({ agents: [], catalog: [] });
|
|
116
|
+
bindScriptedSdk(sdk);
|
|
117
|
+
|
|
118
|
+
const store = await ScriptedLocalAgentStore.open({ workspaceRef: "stigmer-session:ses-9", stateRoot: "/tmp/ses-9" });
|
|
119
|
+
expect(store.workspaceRef).toBe("stigmer-session:ses-9");
|
|
120
|
+
expect(store.stateRoot).toBe("/tmp/ses-9");
|
|
121
|
+
expect(sdk.stores).toEqual([store]);
|
|
122
|
+
|
|
123
|
+
await store.dispose();
|
|
124
|
+
expect(store.disposeCalls).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -95,7 +95,7 @@ import type {
|
|
|
95
95
|
} from "../../../__test-utils__/harness-contract/types.js";
|
|
96
96
|
import type { HermeticEnvironment } from "../../../__test-utils__/hermetic-activity.js";
|
|
97
97
|
import { createCursorAdapter } from "../adapter.js";
|
|
98
|
-
import {
|
|
98
|
+
import { resolveSessionStoreLocation } from "../session-store.js";
|
|
99
99
|
import { hookInputFor, streamArgsFor, STREAM_NAME, type GatedActionKind } from "./cursor-hook-harness.js";
|
|
100
100
|
import { FIXTURE, SDK_CATALOG, hermeticCursorConfig, runWorkspaceHook } from "./hermetic-cursor.js";
|
|
101
101
|
import { ScriptedCursorAgent, sdkEvents, step, type ScriptStep, type ScriptedRun } from "./scripted-agent.js";
|
|
@@ -216,9 +216,9 @@ class CursorSubject implements CursorContractSubject {
|
|
|
216
216
|
private mintAgent(sessionId: string): ScriptedCursorAgent {
|
|
217
217
|
const agent = new ScriptedCursorAgent({ agentId: `agent-kit-${++this.mintCounter}`, turns: [] });
|
|
218
218
|
this.agents.set(sessionId, agent);
|
|
219
|
-
// The adapter asks the SDK to create an agent for the session
|
|
220
|
-
// ref; the double answers with this agent.
|
|
221
|
-
const { workspaceRef } =
|
|
219
|
+
// The adapter asks the SDK to create an agent for the session over a store
|
|
220
|
+
// opened under this ref; the double answers with this agent.
|
|
221
|
+
const { workspaceRef } = resolveSessionStoreLocation(sessionId, this.config.workspaceRootDir);
|
|
222
222
|
this.agentsByWorkspaceRef.set(workspaceRef, agent);
|
|
223
223
|
return agent;
|
|
224
224
|
}
|
|
@@ -248,11 +248,13 @@ class CursorSubject implements CursorContractSubject {
|
|
|
248
248
|
break;
|
|
249
249
|
case "usage":
|
|
250
250
|
script.push(
|
|
251
|
+
// The kit's delta leaves a count optional; the SDK's `turn-ended`
|
|
252
|
+
// states all four, and an absent count is zero, as the pricer reads it.
|
|
251
253
|
step.turnEnded({
|
|
252
|
-
inputTokens: s.delta.inputTokens,
|
|
253
|
-
outputTokens: s.delta.outputTokens,
|
|
254
|
-
cacheReadTokens: s.delta.cacheReadTokens,
|
|
255
|
-
cacheWriteTokens: s.delta.cacheWriteTokens,
|
|
254
|
+
inputTokens: s.delta.inputTokens ?? 0,
|
|
255
|
+
outputTokens: s.delta.outputTokens ?? 0,
|
|
256
|
+
cacheReadTokens: s.delta.cacheReadTokens ?? 0,
|
|
257
|
+
cacheWriteTokens: s.delta.cacheWriteTokens ?? 0,
|
|
256
258
|
}),
|
|
257
259
|
);
|
|
258
260
|
break;
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
* registry the harness reads at setup, the Cursor `Config` slice, and the
|
|
12
12
|
* per-scenario reset of the harness's module-level caches.
|
|
13
13
|
*
|
|
14
|
-
* What the
|
|
15
|
-
* hoisted by vitest and so cannot live here
|
|
14
|
+
* What the three `vi.mock` calls a scenario file must carry look like — they
|
|
15
|
+
* are hoisted by vitest and so cannot live here (the SDK's sqlite entry is a
|
|
16
|
+
* separate module id, so it is doubled separately; `scripted-sdk.ts`):
|
|
16
17
|
*
|
|
17
18
|
* ```ts
|
|
18
19
|
* vi.mock("@cursor/sdk", async () =>
|
|
19
20
|
* (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule());
|
|
21
|
+
* vi.mock("@cursor/sdk/sqlite", async () =>
|
|
22
|
+
* (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule());
|
|
20
23
|
* vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
21
24
|
* (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule());
|
|
22
25
|
* ```
|
|
@@ -24,16 +24,12 @@
|
|
|
24
24
|
* The same shape as the native harness's `__test-utils__/scripted-model.ts`
|
|
25
25
|
* (`ScriptStep`, driven in order), applied to the SDK's event surface.
|
|
26
26
|
*
|
|
27
|
-
* Typing: `SDKMessage`, `Run`, `RunResult
|
|
28
|
-
* types (`@cursor/sdk` 1.0.
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* code too (`turn-stream.ts`, `delta-enricher.ts` read it untyped). The one
|
|
34
|
-
* delta this double emits, the `turn-ended` usage, is therefore typed HERE
|
|
35
|
-
* ({@link TurnEndedUsageDelta}) against what `turn-stream.ts` reads from it,
|
|
36
|
-
* and that gap is recorded in the entry's findings rather than papered over.
|
|
27
|
+
* Typing: `SDKMessage`, `Run`, `RunResult`, `SDKAgent` and the delta channel's
|
|
28
|
+
* `TurnEndedUpdate` are the SDK's own types (`@cursor/sdk` 1.0.31, concrete in
|
|
29
|
+
* `messages.d.ts` / `run.d.ts` / `agent.d.ts` / the vendored `delta-types`),
|
|
30
|
+
* so a script that drifts from the real event shape fails `tsc`. (At 1.0.13
|
|
31
|
+
* the delta types resolved to `any` and the one delta this double emits was
|
|
32
|
+
* typed locally; stigmer/stigmer#1053 closed that gap.)
|
|
37
33
|
*
|
|
38
34
|
* Cancellation: `run.cancel()` is how the harness stops a turn (first denial,
|
|
39
35
|
* cost cap, stall). The double honours it the way the SDK does — the stream
|
|
@@ -50,6 +46,8 @@
|
|
|
50
46
|
*/
|
|
51
47
|
|
|
52
48
|
import type {
|
|
49
|
+
AgentUsage,
|
|
50
|
+
GetUsageOptions,
|
|
53
51
|
ModelSelection,
|
|
54
52
|
Run,
|
|
55
53
|
RunOperation,
|
|
@@ -59,6 +57,7 @@ import type {
|
|
|
59
57
|
SDKMessage,
|
|
60
58
|
SDKUserMessage,
|
|
61
59
|
SendOptions,
|
|
60
|
+
TurnEndedUpdate,
|
|
62
61
|
} from "@cursor/sdk";
|
|
63
62
|
import type { ConversationTurn } from "@cursor/sdk";
|
|
64
63
|
|
|
@@ -66,21 +65,6 @@ import type { ConversationTurn } from "@cursor/sdk";
|
|
|
66
65
|
// Script steps
|
|
67
66
|
// ---------------------------------------------------------------------------
|
|
68
67
|
|
|
69
|
-
/**
|
|
70
|
-
* The `turn-ended` delta as `turn-stream.ts` reads it (`update.type ===
|
|
71
|
-
* "turn-ended" && update.usage` -> `usageAccumulator.addTurn(update.usage)`).
|
|
72
|
-
* See the module header for why this is typed locally.
|
|
73
|
-
*/
|
|
74
|
-
export interface TurnEndedUsageDelta {
|
|
75
|
-
readonly type: "turn-ended";
|
|
76
|
-
readonly usage: {
|
|
77
|
-
readonly inputTokens?: number;
|
|
78
|
-
readonly outputTokens?: number;
|
|
79
|
-
readonly cacheWriteTokens?: number;
|
|
80
|
-
readonly cacheReadTokens?: number;
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
68
|
/** What an `effect` step can reach: the run it is part of. */
|
|
85
69
|
export interface EffectContext {
|
|
86
70
|
readonly run: ScriptedRun;
|
|
@@ -89,7 +73,7 @@ export interface EffectContext {
|
|
|
89
73
|
|
|
90
74
|
export type ScriptStep =
|
|
91
75
|
| { readonly kind: "event"; readonly event: SDKMessage }
|
|
92
|
-
| { readonly kind: "delta"; readonly update:
|
|
76
|
+
| { readonly kind: "delta"; readonly update: TurnEndedUpdate }
|
|
93
77
|
| { readonly kind: "effect"; readonly label: string; readonly run: (ctx: EffectContext) => void | Promise<void> }
|
|
94
78
|
| { readonly kind: "result"; readonly result: Omit<RunResult, "id"> };
|
|
95
79
|
|
|
@@ -101,7 +85,8 @@ export const step = {
|
|
|
101
85
|
event(event: SDKMessage): ScriptStep {
|
|
102
86
|
return { kind: "event", event };
|
|
103
87
|
},
|
|
104
|
-
|
|
88
|
+
/** The `turn-ended` delta `turn-stream.ts` prices; the SDK's shape, all four counts stated. */
|
|
89
|
+
turnEnded(usage: NonNullable<TurnEndedUpdate["usage"]>): ScriptStep {
|
|
105
90
|
return { kind: "delta", update: { type: "turn-ended", usage } };
|
|
106
91
|
},
|
|
107
92
|
effect(label: string, run: (ctx: EffectContext) => void | Promise<void>): ScriptStep {
|
|
@@ -224,7 +209,7 @@ export class ScriptedRun implements Run {
|
|
|
224
209
|
yield s.event;
|
|
225
210
|
break;
|
|
226
211
|
case "delta":
|
|
227
|
-
await this.onDelta?.({ update: s.update
|
|
212
|
+
await this.onDelta?.({ update: s.update });
|
|
228
213
|
break;
|
|
229
214
|
case "effect":
|
|
230
215
|
await s.run({ run: this, agent: this.agent });
|
|
@@ -365,4 +350,11 @@ export class ScriptedCursorAgent implements SDKAgent {
|
|
|
365
350
|
async downloadArtifact(_path: string): Promise<Buffer> {
|
|
366
351
|
throw new Error("ScriptedCursorAgent: artifacts are not part of the scripted surface");
|
|
367
352
|
}
|
|
353
|
+
|
|
354
|
+
async getUsage(_options?: GetUsageOptions): Promise<AgentUsage> {
|
|
355
|
+
// The harness prices the `turn-ended` delta itself (`usage-pricing.ts`) and
|
|
356
|
+
// never asks the SDK for billed usage; a call here is a test bug, like a
|
|
357
|
+
// `send()` with no script.
|
|
358
|
+
throw new Error("ScriptedCursorAgent: getUsage() is not part of the scripted surface");
|
|
359
|
+
}
|
|
368
360
|
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `@cursor/sdk` module a hermetic `ExecuteCursor` run imports — every
|
|
3
|
-
* runtime surface the activity path touches, backed by scripted agents
|
|
3
|
+
* runtime surface the activity path touches, backed by scripted agents — and
|
|
4
|
+
* the `@cursor/sdk/sqlite` module beside it, backed by a store that holds
|
|
5
|
+
* nothing.
|
|
4
6
|
*
|
|
5
|
-
* The activity reaches the SDK at
|
|
7
|
+
* The activity reaches the SDK at four runtime sites and nowhere else:
|
|
6
8
|
*
|
|
7
|
-
* - `session-lifecycle.ts`: `Agent.create(options)`, `Agent.resume(id, options)
|
|
8
|
-
*
|
|
9
|
+
* - `session-lifecycle.ts`: `Agent.create(options)`, `Agent.resume(id, options)`
|
|
10
|
+
* — the agent handle.
|
|
11
|
+
* - `session-store.ts`: `SqliteLocalAgentStore.open({ workspaceRef, stateRoot })`
|
|
12
|
+
* from `@cursor/sdk/sqlite` — the session's store the handle is created and
|
|
13
|
+
* resumed over (`local.store`).
|
|
9
14
|
* - `service-tier.ts`: `Cursor.models.list({ apiKey })` — the catalog the
|
|
10
15
|
* variant params (`fast`, `thinking`) are pinned from.
|
|
11
16
|
* - `index.ts` outer catch: `import("@cursor/sdk")` for `CursorSdkError`, the
|
|
@@ -13,35 +18,42 @@
|
|
|
13
18
|
*
|
|
14
19
|
* Neither the SDK nor the client is injectable into the activity (the real
|
|
15
20
|
* seam arrives with the harness contract; parent Q1/Q2), so — exactly as the
|
|
16
|
-
* native activity tests already do for their boundary modules — the
|
|
17
|
-
* substituted with `vi.mock`. Three existing tests each mock ONE of these
|
|
21
|
+
* native activity tests already do for their boundary modules — the modules
|
|
22
|
+
* are substituted with `vi.mock`. Three existing tests each mock ONE of these
|
|
18
23
|
* surfaces (`session-lifecycle.test.ts`, `service-tier.test.ts`,
|
|
19
|
-
* `sdk-warmup.test.ts`); a whole-activity run needs all
|
|
20
|
-
* what this module is.
|
|
24
|
+
* `sdk-warmup.test.ts`); a whole-activity run needs all of them at once, which
|
|
25
|
+
* is what this module is.
|
|
21
26
|
*
|
|
22
|
-
* `vi.mock` is hoisted and must appear in the test file
|
|
23
|
-
*
|
|
24
|
-
*
|
|
27
|
+
* `vi.mock` is hoisted and must appear in the test file, once per module id
|
|
28
|
+
* (a subpath is its own module id, so `@cursor/sdk/sqlite` needs its own
|
|
29
|
+
* `vi.mock`); the factories `await import()` this module and return
|
|
30
|
+
* {@link scriptedCursorSdkModule} and {@link scriptedCursorSqliteModule}. The
|
|
31
|
+
* modules' statics read the {@link ScriptedCursorSdk} bound for the current
|
|
25
32
|
* scenario ({@link bindScriptedSdk}), so one test file can run several
|
|
26
|
-
* scenarios with different agents against the same mocked
|
|
33
|
+
* scenarios with different agents against the same mocked modules.
|
|
27
34
|
*
|
|
28
35
|
* Resolution rules a scenario declares, mirroring what the real SDK does:
|
|
29
36
|
* - `Agent.create` hands out the agent declared for the session it is asked
|
|
30
|
-
* for (`
|
|
31
|
-
* the harness contract kit runs several sessions concurrently on
|
|
32
|
-
* adapter, and the order their creates arrive in is real I/O timing),
|
|
33
|
-
* the NEXT unclaimed agent in `agents` (turn 1 of a fresh session; the
|
|
34
|
-
* agent of a poisoned-handle recovery) — unless the scenario declared
|
|
35
|
-
* `createFailure`, which the FIRST create throws instead (the SDK refusing
|
|
37
|
+
* for (`agentForWorkspaceRef`, when the scenario resolves agents per
|
|
38
|
+
* session — the harness contract kit runs several sessions concurrently on
|
|
39
|
+
* one adapter, and the order their creates arrive in is real I/O timing),
|
|
40
|
+
* else the NEXT unclaimed agent in `agents` (turn 1 of a fresh session; the
|
|
41
|
+
* fresh agent of a poisoned-handle recovery) — unless the scenario declared
|
|
42
|
+
* a `createFailure`, which the FIRST create throws instead (the SDK refusing
|
|
36
43
|
* to mint an agent: a 401 on the key, a validation error).
|
|
37
44
|
* - `Agent.resume(id)` hands back the agent with that id if the scenario
|
|
38
45
|
* declared it (in `agents` or `resumableAgents`, or handed out for a
|
|
39
46
|
* session), else throws — `resolveAgent` then falls back to `create`, which
|
|
40
47
|
* is the production shape of a lost handle.
|
|
41
48
|
* - `Cursor.models.list` answers the scenario's catalog.
|
|
49
|
+
* - `SqliteLocalAgentStore.open` hands out a {@link ScriptedLocalAgentStore}
|
|
50
|
+
* carrying the ref and root it was asked for, and records the open; its
|
|
51
|
+
* `dispose()` is recorded too, so a scenario can assert the adapter's
|
|
52
|
+
* session-store lifetime (`releaseSession`, `shutdown`).
|
|
42
53
|
*/
|
|
43
54
|
|
|
44
55
|
import type { AgentOptions, ModelListItem, SDKAgent } from "@cursor/sdk";
|
|
56
|
+
import type { SqliteLocalAgentStore, SqliteLocalAgentStoreOptions } from "@cursor/sdk/sqlite";
|
|
45
57
|
import type { ScriptedCursorAgent } from "./scripted-agent.js";
|
|
46
58
|
|
|
47
59
|
export interface ScriptedSdkOptions {
|
|
@@ -50,12 +62,12 @@ export interface ScriptedSdkOptions {
|
|
|
50
62
|
/**
|
|
51
63
|
* The agent `Agent.create` hands out for the session the create names,
|
|
52
64
|
* consulted before the ordered list. The SDK's create options carry the
|
|
53
|
-
* session as `
|
|
54
|
-
* `
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* every create, so a scenario may mint the session's
|
|
58
|
-
* learns of the session.
|
|
65
|
+
* session as the store's `workspaceRef` (`local.store`, opened by
|
|
66
|
+
* `session-store.ts`: `stigmer-session:<sessionId>`), so the resolver is
|
|
67
|
+
* keyed by that ref — a scenario computes the ref the same way the adapter
|
|
68
|
+
* does (`resolveSessionStoreLocation`). `undefined` falls through to the
|
|
69
|
+
* list. Consulted live on every create, so a scenario may mint the session's
|
|
70
|
+
* agent only when it learns of the session.
|
|
59
71
|
*/
|
|
60
72
|
readonly agentForWorkspaceRef?: (workspaceRef: string) => ScriptedCursorAgent | undefined;
|
|
61
73
|
/**
|
|
@@ -82,21 +94,46 @@ export interface RecordedResolution {
|
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
/**
|
|
85
|
-
* The
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
97
|
+
* The session a create names: the `workspaceRef` of the store it passes as
|
|
98
|
+
* `local.store`. In a hermetic run that store is the one this module's sqlite
|
|
99
|
+
* double opened; anything else (a test that mocks `@cursor/sdk` with its own
|
|
100
|
+
* stub and passes no store) names no session and falls through to the list.
|
|
89
101
|
*/
|
|
90
102
|
function workspaceRefOf(options: AgentOptions): string | undefined {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
const store = options.local?.store;
|
|
104
|
+
return store instanceof ScriptedLocalAgentStore ? store.workspaceRef : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The `@cursor/sdk/sqlite` store the adapter opens per session, holding
|
|
109
|
+
* nothing: the scripted agents carry their own state, so the store only has
|
|
110
|
+
* to be the thing `Agent.create` / `Agent.resume` are handed and the thing
|
|
111
|
+
* `releaseSession` / `shutdown` dispose. `open` and `dispose` are recorded on
|
|
112
|
+
* the bound {@link ScriptedCursorSdk} for lifetime assertions.
|
|
113
|
+
*/
|
|
114
|
+
export class ScriptedLocalAgentStore {
|
|
115
|
+
disposeCalls = 0;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
readonly workspaceRef: string,
|
|
119
|
+
readonly stateRoot: string,
|
|
120
|
+
) {}
|
|
121
|
+
|
|
122
|
+
static async open(options: SqliteLocalAgentStoreOptions): Promise<ScriptedLocalAgentStore> {
|
|
123
|
+
const store = new ScriptedLocalAgentStore(options.workspaceRef, options.stateRoot ?? "");
|
|
124
|
+
current().stores.push(store);
|
|
125
|
+
return store;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async dispose(): Promise<void> {
|
|
129
|
+
this.disposeCalls++;
|
|
130
|
+
}
|
|
95
131
|
}
|
|
96
132
|
|
|
97
133
|
export class ScriptedCursorSdk {
|
|
98
134
|
readonly resolutions: RecordedResolution[] = [];
|
|
99
|
-
|
|
135
|
+
/** Every store `SqliteLocalAgentStore.open` handed out, in order. */
|
|
136
|
+
readonly stores: ScriptedLocalAgentStore[] = [];
|
|
100
137
|
private nextCreate = 0;
|
|
101
138
|
private createFailurePending: boolean;
|
|
102
139
|
private readonly byId = new Map<string, ScriptedCursorAgent>();
|
|
@@ -145,10 +182,6 @@ export class ScriptedCursorSdk {
|
|
|
145
182
|
return agent;
|
|
146
183
|
}
|
|
147
184
|
|
|
148
|
-
archive(agentId: string): void {
|
|
149
|
-
this.archived.push(agentId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
185
|
listModels(): readonly ModelListItem[] {
|
|
153
186
|
return this.options.catalog;
|
|
154
187
|
}
|
|
@@ -242,7 +275,6 @@ export function scriptedCursorSdkModule(): Record<string, unknown> {
|
|
|
242
275
|
create: async (options: AgentOptions): Promise<SDKAgent> => current().create(options),
|
|
243
276
|
resume: async (agentId: string, options?: Partial<AgentOptions>): Promise<SDKAgent> =>
|
|
244
277
|
current().resume(agentId, options),
|
|
245
|
-
archive: async (agentId: string): Promise<void> => current().archive(agentId),
|
|
246
278
|
},
|
|
247
279
|
Cursor: {
|
|
248
280
|
models: {
|
|
@@ -252,3 +284,27 @@ export function scriptedCursorSdkModule(): Record<string, unknown> {
|
|
|
252
284
|
CursorSdkError: ScriptedCursorSdkError,
|
|
253
285
|
};
|
|
254
286
|
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The factory a test passes to `vi.mock("@cursor/sdk/sqlite", ...)`, beside
|
|
290
|
+
* the one above:
|
|
291
|
+
*
|
|
292
|
+
* ```ts
|
|
293
|
+
* vi.mock("@cursor/sdk/sqlite", async () =>
|
|
294
|
+
* (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSqliteModule(),
|
|
295
|
+
* );
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* The one export the runner reads from that entry (`session-store.ts`). The
|
|
299
|
+
* class is exported under the SDK's name so `session-store.ts`'s dynamic
|
|
300
|
+
* import destructures it unchanged; its type is the SDK's, which is why
|
|
301
|
+
* {@link ScriptedLocalAgentStore} carries `workspaceRef`, `stateRoot` and
|
|
302
|
+
* `dispose()` — the members the runner touches — and nothing else.
|
|
303
|
+
*/
|
|
304
|
+
export function scriptedCursorSqliteModule(): Record<string, unknown> {
|
|
305
|
+
return {
|
|
306
|
+
SqliteLocalAgentStore: ScriptedLocalAgentStore satisfies {
|
|
307
|
+
open(options: SqliteLocalAgentStoreOptions): Promise<Pick<SqliteLocalAgentStore, "workspaceRef" | "stateRoot" | "dispose">>;
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
@@ -41,14 +41,15 @@ describeWithCursorKey("SDK routing with CURSOR_BACKEND_URL unset", () => {
|
|
|
41
41
|
mkdirSync(stateRoot, { recursive: true });
|
|
42
42
|
|
|
43
43
|
const { Agent } = await import("@cursor/sdk");
|
|
44
|
+
const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
|
|
44
45
|
|
|
45
46
|
const agent = await Agent.create({
|
|
46
47
|
apiKey: CURSOR_API_KEY,
|
|
47
48
|
model: { id: "claude-sonnet-4" },
|
|
48
|
-
local: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
stateRoot,
|
|
49
|
+
local: {
|
|
50
|
+
cwd: stateRoot,
|
|
51
|
+
// 1.0.31: the store is caller-owned (`session-store.ts` in production).
|
|
52
|
+
store: await SqliteLocalAgentStore.open({ workspaceRef: `routing-test-${Date.now()}`, stateRoot }),
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
|
|
@@ -62,14 +63,15 @@ describeWithCursorKey("SDK routing with CURSOR_BACKEND_URL unset", () => {
|
|
|
62
63
|
mkdirSync(stateRoot, { recursive: true });
|
|
63
64
|
|
|
64
65
|
const { Agent } = await import("@cursor/sdk");
|
|
66
|
+
const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
|
|
65
67
|
|
|
66
68
|
const agent = await Agent.create({
|
|
67
69
|
apiKey: CURSOR_API_KEY,
|
|
68
70
|
model: { id: "claude-sonnet-4" },
|
|
69
|
-
local: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
stateRoot,
|
|
71
|
+
local: {
|
|
72
|
+
cwd: stateRoot,
|
|
73
|
+
// 1.0.31: the store is caller-owned (`session-store.ts` in production).
|
|
74
|
+
store: await SqliteLocalAgentStore.open({ workspaceRef: `routing-send-test-${Date.now()}`, stateRoot }),
|
|
73
75
|
},
|
|
74
76
|
});
|
|
75
77
|
|
|
@@ -41,16 +41,17 @@ describeWithCursorKey("Fetch Interceptor vs Connect-Node Transport", () => {
|
|
|
41
41
|
|
|
42
42
|
it("Agent.create() does NOT go through globalThis.fetch (proving interceptor is useless)", async () => {
|
|
43
43
|
const { Agent } = await import("@cursor/sdk");
|
|
44
|
+
const { SqliteLocalAgentStore } = await import("@cursor/sdk/sqlite");
|
|
44
45
|
|
|
45
46
|
const stateRoot = join(tmpdir(), `cursor-intercept-test-${Date.now()}`);
|
|
46
47
|
mkdirSync(stateRoot, { recursive: true });
|
|
47
48
|
|
|
48
49
|
const agent = await Agent.create({
|
|
49
50
|
apiKey: CURSOR_API_KEY,
|
|
50
|
-
local: {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stateRoot,
|
|
51
|
+
local: {
|
|
52
|
+
cwd: stateRoot,
|
|
53
|
+
// 1.0.31: the store is caller-owned (`session-store.ts` in production).
|
|
54
|
+
store: await SqliteLocalAgentStore.open({ workspaceRef: `intercept-test-${Date.now()}`, stateRoot }),
|
|
54
55
|
},
|
|
55
56
|
});
|
|
56
57
|
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { describe, it, expect } from "vitest";
|
|
38
|
+
import { SqliteLocalAgentStore } from "@cursor/sdk/sqlite";
|
|
38
39
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, readdirSync } from "node:fs";
|
|
39
40
|
import { tmpdir } from "node:os";
|
|
40
41
|
import { join } from "node:path";
|
|
@@ -87,8 +88,12 @@ describeWithCursorKey("generateImage live ground truth (issue #965)", () => {
|
|
|
87
88
|
const agent = await Agent.create({
|
|
88
89
|
apiKey: CURSOR_API_KEY,
|
|
89
90
|
model: { id: "composer-2.5" },
|
|
90
|
-
local: {
|
|
91
|
-
|
|
91
|
+
local: {
|
|
92
|
+
cwd: workspaceRoot,
|
|
93
|
+
settingSources: ["project"],
|
|
94
|
+
// 1.0.31: the store is caller-owned (`session-store.ts` in production).
|
|
95
|
+
store: await SqliteLocalAgentStore.open({ workspaceRef: `genimage-spike-${Date.now()}`, stateRoot }),
|
|
96
|
+
},
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
// The runner's exact consumption shape (index.ts / turn-stream.ts):
|